在web 端实现一个按下收音,抬起获取音频的需求

613 阅读6分钟

前言

最近在做一些关于AIGC的项目,整体感受:aigc 真的很强大🐮,我们的产品刚开始只是对内使用,现在开放对外了,我们是做教育的,有很多关于教育场景的功。下面会给大家贴上我们项目的链接,如果有需要购买的可以找我要邀请码,一个月几十块钱,即可拥有! AIGC 体验链接

在众多功能中,使用频率最多就是智能对话功能,在对话里可以选择多种大模型,例如:gpt4.0、讯飞星火、文心一言、通义千问、智谱等等,原来的功能是只支持文字输入,后面支持移动端了以后,需要添加一个录音的功能:按下收音,抬起将音频转成文字,回填到输入框里,后面在其他的项目里也添加了这个功能,所以这里就做个详细记录。

一、获取音频

音频数据获取代码如下:

const getChannelAndSampleRate = (
  fileType: "blob" | "file",
  file: File | Blob,
) => {
  const audioContext = new window.AudioContext();
  // 如果拿到的是file 文件,需要转成blob 文件后,再进行数据的获取
  const fileBlob =
    fileType === "file" ? new Blob([file], { type: file.type }) : file;

  const fileReader = new FileReader();
  fileReader.readAsArrayBuffer(fileBlob);
  fileReader.onloadend = () => {
    const arrayBuffer = fileReader.result as ArrayBuffer;
    if (arrayBuffer) {
      audioContext.decodeAudioData(
        arrayBuffer,
        (audioBuffer) => {
          console.log("采样率:", audioBuffer.sampleRate);
          console.log("通道:", audioBuffer.numberOfChannels);
          message.success("音频解码成功!");
        },
        (e) => {
          message.success("解码音频数据时出错,请重新上传音频!");
          console.error("解码音频数据时出错", e);
        },
      );
    }
  };
};

三、在web 浏览器开发录音功能

最近接到了一个新需求,在web端实现一个类似手机微信的录音转文字的功能,如下图

录音:

WechatIMG158.jpeg

将录音文件转文字:

WechatIMG157.jpeg

下面我们来看一下技术方面该怎么处理:

  1. 获取浏览器的麦克风权限
  2. 怎么获取录制后的音频
  3. 将音频发送给后端,调用后端接口实现

1. 当按下收音键后,获取麦克风权限:

// 使用useState钩子初始化mediaRecorder状态,初始值为undefined
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder>();

....

// 这里尝试获取用户媒体设备的权限
navigator.mediaDevices
  ?.getUserMedia({ audio: true }) // 使用getUserMedia方法请求麦克风权限,audio:true表示请求音频输入
  .then(function (stream) { // 成功获取权限后的回调函数,参数stream是包含音频数据的媒体流对象
    let recorder = new MediaRecorder(stream); // 创建一个MediaRecorder实例,用于录制音频,以流为数据源
    setMediaRecorder(recorder); // 更新组件状态,保存MediaRecorder实例
    recorder.start(); // 开始录制音频
  })
  .catch(function (err) { // 获取麦克风权限失败的错误处理
    message.error("无法获取麦克风权限");  
    console.error("无法获取麦克风权限", err);  
  });

2. 当抬起收音键后的状态修改:

....

 if (mediaRecorder && mediaRecorder.state === "recording") {
      mediaRecorder.stop();
  }

3. 监听音频变化,当录音停止后,获取音频 ,处理音频,进行文字转换

  let audioChunks: BlobPart[] = [];
  const [userInput, setUserInput] = useState<string>(""); // 存储转换后的文字
  
  
    // 重置状态
  const resetAudioData = () => {
    audioChunks = [];
  };
  
....

  if (mediaRecorder && mediaRecorder.state === "recording") {
  // 当录音数据可用时的事件处理
    mediaRecorder.ondataavailable = function (event) {
    // 将录音数据块(event.data)添加到数组中,用于后续处理
      audioChunks.push(event.data);
    };
    
     // 录音停止之后的回调函数
    mediaRecorder.onstop = async function () {
      const audioBlob = new Blob(audioChunks);
      const file = new File([audioBlob], "filename.wav", {
        type: "audio/wav",
        lastModified: new Date().getTime(),
      });

      // 解析音频,拿到采样率和声道(业务需要)
      getChannelAndSampleRate(audioBlob, {
        onSuccess: (audioBuffer) => {
          message.success("音频解码成功!");
          const params = {
            file,
            audioType: "wav",
            sampleRate: audioBuffer.sampleRate, // 音频采样率
            channel: audioBuffer.numberOfChannels, // 音频声道数量
            lang: "multilingual",
          };

          // 调用后端接口,将音频文件发送到后端进行文字转换
          audioToText(params, {
            onSuccess: (res: { asrResult: string }) => {
             // 将转换得到的文字结果填入用户输入框,完成语音到文字的转换
              setUserInput(res.asrResult);
              // 重置录音状态,用于下一次录音
              resetAudioData();
            },
            onError: function (
              error: Error,
              statusCode?: number | undefined,
            ): void {
              message.error("转换失败,请重试!");
              resetAudioData();
              console.error(error);
            },
          });
        },
        onError: (e: Error) => {
          message.error("解码音频数据时出错,请重新录制音频!");
          resetAudioData();
          console.error(e);
        },
      });
      // 遍历并停止所有的媒体输入轨道,结束录音
      mediaRecorder.stream.getTracks().forEach((track) => track.stop());
    };
  }

⚠️ 常规浏览器 && 旧版本浏览器:

if(navigator.mediaDevices){

// 常规浏览器,参考上面👆的代码

}
// 兼容旧版本浏览器
else if('getUserMedia' in navigator){
     navigator
        ?.getUserMedia({ audio: true })
        .then((stream: any) => {
          const recorder = new MediaRecorder(stream);
          setMediaRecorder(recorder);
          recorder.start();
        })
        .catch(() => {
          handleErrorToast('无法获取麦克风权限');
        });
} else {
  message.info('当前手机版本不支持录音');
}


三、通过File 或者 Blob 文件获取音频的采样率和通道

什么是音频的采样率?

音频采样率就像是我们用来记录声音的“快照”频率。采样率越高,你记录的声音就越细致,就像高分辨率的相机能够捕捉更清晰的图片一样。更高的采样率可以更准确地捕捉和再现声音的高频细节,所以在理论上是可以提供更精确的声音再现。

举个例子,如果用相机比喻,较低分辨率的照片可能会模糊或丢失细节,而高分辨率的照片则能够更准确地显示细节。同样,在音频中,较低的采样率可能导致声音的某些细微部分被忽略,而高的采样率则能够捕捉到这些细节。

什么是声道?

音频的声道是指录制和播放音频时所用的独立音频信号路径。每个声道可以承载一定的音频信息,一般分为单声道、立体声、环绕声。

  • 单声道:在单声道系统中,所有的声音都通过一个单一的声道播放

  • 立体声:系统使用两个声道,通常标记为“左”和“右”。在录制时通常会使用两个麦克风分别捕捉到两个声道的声音。对于听者而言,立体声可以创造一种空间感,仿佛声音从不同方向到达听者耳朵

  • 环绕声:环绕声系统使用多个声道(通常是五个以上,如5.1或7.1声道系统)来创造更为沉浸和三维的音响效果。这种配置可以更准确地模拟现实环境中声音的位置和动态,为听者提供更立体、更真实的听觉体验。


四、以下是关于交互方面的优化:

1. 按下和抬起时,给一个快速震动:

  // 快速震动
  const vibrateOnce: any = () => {
    if ("vibrate" in navigator) {
      navigator.vibrate(50);
    } else {
      console.error("Vibration API is not supported by this browser.");
    }
  };

2. 当用户按下收音键后,移动点击位置,当移动数值超过安全数值后,则认为是误操作:

按下时记录点击位置,抬起收音键时,可以拿到当前点击的位置,当移动距离超过指定数值,取消收音:

  // 计算移动距离(容错率为100px)
  const hasMovedTooFar = (
    start: { x: number; y: number },
    end: { x: number; y: number },
  ) => {
    const tolerance = 100;

    var diff = {
      x: end.x - start.x,
      y: end.y - start.y,
    };
    return Math.abs(diff.x) >= tolerance || Math.abs(diff.y) >= tolerance;
  };

3. 当收音结束后,获取的音频时间过短,提示录音时间过短

  const intervalTime = (Date.now() - startTime) / 1000;
    // 不可低于1秒
    if (intervalTime < 1) {
      message.error("录音时间过短");
      ....
      return;
    }