web 的能力随着移动端的发展被扩展的越来越好,这篇记录想记录下使用 Web Audio API 进行录音分析的过程。所使用的浏览器为 Chrome 最新版本。

概念

Web Audio API 使用户可以在音频上下文(AudioContext)中进行音频操作,具有模块化路由的特点。简单来说使用 Web Audio API 就是创建一个 AudioContext 在此基础环境上进行一系列的后续操作。

AudioContext

声明一个 AudioContext:

this.audioContext = new AudioContext;

创建录音/获取录音对象

使用浏览器的录音能力,需要获取用户的权限,才能调用。

window.navigator.mediaDevices.getUserMedia({
  audio: true,
}).then(stream => {
  // audioInput 表示 音频源节点
  // stream是通过navigator.getUserMedia获取的外部(如麦克风)stream音频输出,对于这就是输入
  this.audioInput = this.audioContext.createMediaStreamSource(stream);
  // createMediaStreamSource()方法用于创建一个新的 MediaStreamAudioSourceNode 对象, 需要传入一个媒体流对象(MediaStream对象), 然后来自MediaStream的音频就可以被播放和操作。
  this.stream = stream;
}).catch((err) => {
  console.log('发生错误');
}).then(function() {
  this.audioInput.connect(this.recorder); // 音频源节点连接音频处理节点
  this.recorder.connect(this.audioContext.destination); // 音频处理节点连接音频渲染设备
});

通过 mediaDevices.getUserMedia 会在此时弹出授权窗口,用户授权之后就可以开始调用 recorder 上的能力。

createScriptProcessor 方法

创建一个ScriptProcessorNode用于通过JavaScript音频处理节点,具体参数

this.recorder = this.audioContext.createScriptProcessor(4096, 1, 1);

// onaudioprocess 方法是 在一次达到设定的创建scriptNode时缓冲区大小时就出发一次
this.recorder.onaudioprocess = (e) => {
  // getChannelData返回 Float32Array 类型的 pcm 数据
  const data = e.inputBuffer.getChannelData(0);
  pcmData.push(data);
  audioSize += data.length;
}

这样我们就能实时的获取到用户的录音的PCM内容,经过处理之后就可以播放使用。

注意

在某些项目中,需求可能需要对用户的录音内容进行语音识别,在目前的市场上的大多数语音识别接口只支持 16K 的采样率的 wav 音频。然而目前的 chrome 浏览器的采样率是只读状态且不能修改(修改无效),所以需要在每一次onaudioprocess 触发的时候对 PCM 数据进行相应的转化处理,来达到使用要求。

推荐一个转化采样率的方法: Recorder源码

已有音频的实时分析

实时获取录音的内容经过上面的操作已经可以实现,在笔者的需求中还需要给用户一个音频处理实例,针对一个已有的音频进行实时分析。所以就需要实时获取到音频然后播放,且获取到正在播放的PCM数据。

// 一定要把 context source 的定义写在异步方法之前,否则在 safari 浏览器无法触发 source 的一些事件
const context = new (window.AudioContext || window.webkitAudioContext)();
const source = context.createBufferSource();

axios.get('音频URL', { responseType: 'arraybuffer' }).then((res) => {
  const blob = new Blob([res.data], { type: 'audio/wav' }); // 实时分析wav文件,但是这个文件经过 decodeAudioData 之后是没有 wav 头信息的,如果需要请自行加上
  const read = new FileReader();
  read.onloadend = () => {
    context.decodeAudioData(read.result, (buffer) => {
      const processor = context.createScriptProcessor(4096, 1, 1);
      processor.connect(context.destination);
      source.connect(processor);
      source.connect(context.destination); // 资源连接设备 如果不连接则不会发出声音
      // 实时回调
      processor.onaudioprocess = (e) => {
        // getChannelData返回 Float32Array 类型的 pcm 数据
        const data = e.inputBuffer.getChannelData(0);
        pcmData.push(data);
        audioSize += data.length;
        // 实时处理...
      }

      source.buffer = buffer;
      // 播完断开
      source.onended = () => {
        processor.disconnect(context.destination);
        source.disconnect(processor);
        source.disconnect(context.destination);
      };
    }
  };

  read.readAsArrayBuffer(blob);
})

以上是项目中使用 Web Audio API 的记录,当然需求不仅如此,还有可视化音频的处理等,不过既然已经能拿到实时的 PCM 数据,只需要对这些数据进行实时处理加工,绘制即可。

其中的技术参考了一个开源的录音库: Recorder

附录

创建空的wav头信息

const createWavHeader = () => {
  const WAV_HEAD_SIZE = 44;
  const buffer = new ArrayBuffer(WAV_HEAD_SIZE);
  // 需要用一个view来操控buffer
  const data = new DataView(buffer);
  let offset = 0;
  const writeString = (str) => {
    for (let i = 0; i < str.length; i += 1, offset += 1) {
      data.setUint8(offset, str.charCodeAt(i));
    };
  };
  const write16 = (v) => {
    data.setUint16(offset, v, true);
    offset += 2;
  };
  const write32 = (v) => {
    data.setUint32(offset, v, true);
    offset += 4;
  };
  /* RIFF identifier */
  writeString('RIFF');
  /* RIFF chunk length */
  write32(36 + 0); // 0 这个参数是代表wav数据长度 可通过 pcmData 获得,此处写0是因为录音开始无法获取到wav总大小
  /* RIFF type */
  writeString('WAVE');
  /* format chunk identifier */
  writeString('fmt ');
  /* format chunk length */
  write32(16);
  /* sample format (raw) */
  write16(1);
  /* channel count */
  write16(1);
  /* sample rate */
  write32(16000);
  /* byte rate (sample rate * block align) */
  write32(16000 * (16 / 8));
  /* block align (channel count * bytes per sample) */
  write16(16 / 8);
  /* bits per sample */
  write16(16);
  /* data chunk identifier */
  writeString('data');
  /* data chunk length */
  write32(0);
  return data;
};

export default createWavHeader;

# JavaScript