首页 > 解决方案 > 将 PCM 原始字节保存到 DataView 对象中

问题描述

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

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

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>

为了将此记录发送到我的后端,我需要将数组转换为 a 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都会返回一个列表,所以我不明白我应该做什么来转换这些值以使其适合缓冲区...Float32ArraysampleRateInt16

标签: javascriptweb-audio-api

解决方案


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

另请注意,您创建的 ArrayBuffer 的长度等于 中的样本数PCM32fSamples,应该是以字节为单位的大小,调用也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;
    }
  }
  

最后,数据将在PCM16iSamples和 中buffer,您可以像在示例中那样从缓冲区构造数据视图

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;
    }
  }
  

推荐阅读