将 PCM 原始字节保存到 DataView 对象中

Save PCM raw bytes into a DataView object

我正在使用 new AudioContext({ sampleRate: 16000 }) 获取 PCM 原始字节(注意,我正在使用 Chrome,它支持 sampleRate 选项),我想将将结果数组转换为 DataView 对象。

我当前的代码如下,在 stop 方法中我读取左声道并将其存储为 Float32Arrays 的数组。

function mergeBuffers(channelBuffer, recordingLength) {
  let result = new Float32Array(recordingLength);
  let offset = 0;

  for (let i = 0; i < channelBuffer.length; i++) {
    result.set(channelBuffer[i], offset);
    offset += channelBuffer[i].length;
  }

  return Array.prototype.slice.call(result);
}

class AudioRecorder {
  constructor(audioStream, config) {
    this.audioStream = audioStream;

    // creates the an instance of audioContext
    this.audioContext = new AudioContext({ sampleRate: 16000 });

    // retrieve the current sample rate of microphone the browser is using
    this.sampleRate = this.audioContext.sampleRate;

    // creates a gain node
    this.volume = this.audioContext.createGain();

    // creates an audio node from the microphone incoming stream
    this.audioInput = this.audioContext.createMediaStreamSource(audioStream);

    this.leftChannel = [];
    this.recordingLength = 0;

    /*
     * From the spec: This value controls how frequently the audioprocess event is
     * dispatched and how many sample-frames need to be processed each call.
     * Lower values for buffer size will result in a lower (better) latency.
     * Higher values will be necessary to avoid audio breakup and glitches
     */
    const bufferSize = config?.bufferSize ?? 2048;
    this.recorder = (
      this.audioContext.createScriptProcessor ||
      this.audioContext.createJavaScriptNode
    ).call(this.audioContext, bufferSize, 1, 1);

    // connect the stream to the gain node
    this.audioInput.connect(this.volume);

    this.recorder.onaudioprocess = (event) => {
      const samples = event.inputBuffer.getChannelData(0);

      // we clone the samples
      this.leftChannel.push(new Float32Array(samples));

      this.recordingLength += bufferSize;
    };
    
    this.ondataavailable = config?.ondataavailable;
  }

  start() {
    // we connect the recorder
    this.volume.connect(this.recorder);

    // start recording
    this.recorder.connect(this.audioContext.destination);
    
    
  }

  stop() {
    this.recorder.disconnect();
    const PCM32fSamples = mergeBuffers(this.leftChannel, this.recordingLength);
    const PCM16iSamples = [];

    for (let i = 0; i < PCM32fSamples.length; i++) {
      let val = Math.floor(32767 * PCM32fSamples[i]);
      val = Math.min(32767, val);
      val = Math.max(-32768, val);

      PCM16iSamples.push(val);
    }
    
    return PCM16iSamples;
  }
}

(async () => {
  if (!navigator.getUserMedia) {
    alert("getUserMedia not supported in this browser.");
  }

  let audioStream;
  try {
    audioStream = await navigator.mediaDevices.getUserMedia({
      audio: true
    });
  } catch (err) {
    alert("Error capturing audio.");
  }

  const recorder = new AudioRecorder(audioStream);
  
  document.querySelector('#start').addEventListener('click', () => recorder.start());
  document.querySelector('#stop').addEventListener('click', () => console.log(recorder.stop()));
})();
<button id="start">Start</button>
<button id="stop">Stop</button>

为了将此录音发送到我的后端,我需要将数组转换为 DataView,因此我尝试执行以下操作:

stop() {
    this.recorder.disconnect();
    const PCM32fSamples = mergeBuffers(this.leftChannel, this.recordingLength);
    const buffer = new ArrayBuffer(PCM32fSamples.length + 1);
    const PCM16iSamples = new DataView(buffer);

    for (let i = 0; i < PCM32fSamples.length; i++) {
      let val = Math.floor(32767 * PCM32fSamples[i]);
      val = Math.min(32767, val);
      val = Math.max(-32768, val);

      PCM16iSamples.setInt16(i, val);
    }
    
    return PCM16iSamples;
  }

但问题是生成的音频听不见。

根据我的理解,AudioContext 正在返回一个 Float32Array 的列表,无论我将其设置为 sampleRate 使用什么,所以我不明白我应该做什么做转换值,使它们适合 Int16 缓冲区...

根据您的评论,我假设您知道如何处理 s16le 原始音频。

另请注意,您正在创建长度等于 PCM32fSamples 中样本数的 ArrayBuffer,应该是以字节为单位的大小,而且对 setInt16 的调用应该传递以字节为单位的偏移量.

另一种设置数组缓冲区的方法是构造一个 Int16Array。使用 DataView 的动机是能够编写混合类型的数据。这将使您的代码更具可读性。

  const buffer = new ArrayBuffer(this.recordingLength * 2);
  const PCM16iSamples = new Int16Array(buffer);
  let offset = 0;
  for(const chunk of this.leftChannel){
    for(const sample of chunk){
      let val = Math.floor(32767 * sample);
      val = Math.min(32767, val);
      val = Math.max(-32768, val);
      PCM16iSamples[offset++] = val;
    }
  }
  

最终数据将位于 PCM16iSamplesbuffer 中,您可以像示例中那样从缓冲区构造数据视图

PS我没测试过,这里截图是不行的

使用DataView

填充
  const buffer = new ArrayBuffer(this.recordingLength * 2);
  const data = new DataView(buffer);
  let offset = 0;
  for(const chunk of this.leftChannel){
    for(const sample of chunk){
      let val = Math.floor(32767 * sample);
      val = Math.min(32767, val);
      val = Math.max(-32768, val);
      data.setInt16(offset, val) = val;
      offset += 2;
    }
  }