You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将Socket.io接收的ArrayBuffer作为音频播放?

问题:如何通过Socket.io接收ArrayBuffer并播放音频流

我现在遇到一个问题:通过Socket.io接收ArrayBuffer格式的音频流,想把它实时播放出来。目前接收缓冲区的代码是这样的:

retrieveAudioStream = () => { 
  this.socket.on('stream', (arrayBuffer) => { 
    console.log('arrayBuffer', arrayBuffer) 
  }) 
}

能不能直接把<audio/>元素的src设置成这个缓冲区?如果不行的话,该怎么播放传入的缓冲区流?

另外补充下发送端的代码,方便理解场景:
获取音频输入并初始化录制的代码:

window.navigator.getUserMedia(constraints, this.initializeRecorder, this.handleError); 

initializeRecorder = (stream) => { 
  const audioContext = window.AudioContext; 
  const context = new audioContext(); 
  const audioInput = context.createMediaStreamSource(stream); 
  const bufferSize = 2048; 

  // 创建JavaScript处理节点
  const recorder = context.createScriptProcessor(bufferSize, 1, 1); 
  // 指定处理函数
  recorder.onaudioprocess = this.recorderProcess; 
  // 连接流到录制节点
  audioInput.connect(recorder); 
  // 连接录制节点到输出
  recorder.connect(context.destination); 
}

接收inputBuffer并通过Socket.io发送的代码:

recorderProcess = (e) => { 
  const left = e.inputBuffer.getChannelData(0); 
  this.socket.emit('stream', this.convertFloat32ToInt16(left)) 
}

我之前尝试用AudioContext处理,但遇到了错误,代码和错误信息如下:

retrieveAudioStream = () => { 
  const audioContext = new window.AudioContext(); 
  this.socket.on('stream', (buffer) => { 
    const b = audioContext.createBuffer(1, buffer.length, audioContext.sampleRate); 
    b.copyToChannel(buffer, 0, 0) 
    const s = audioContext.createBufferSource(); 
    s.buffer = b 
  }) 
}

错误:

NotSupportedError: Failed to execute 'createBuffer' on 'BaseAudioContext': The number of frames provided (0) is less than or equal to the minimum bound (0).

解决方案

首先明确说:不能直接把ArrayBuffer设为audio元素的src。因为audio的src需要的是一个可访问的音频资源URL(比如Blob URL、远程音频文件地址),而你接收的是流式的原始音频数据片段,没有音频文件格式的头信息,直接设置src根本识别不了。针对你的流式音频场景,用Web Audio API(AudioContext)来实时处理播放是正确的方向,下面给你修正后的方案:

1. 先修正数据格式转换

你的发送端把Float32格式的音频数据转成了Int16来传输,所以接收端必须先把Int16Array转回Float32Array——因为Web Audio API只认Float32格式的音频数据:

// 工具函数:把Int16Array转回Float32Array
function convertInt16ToFloat32(int16Array) {
  const float32Array = new Float32Array(int16Array.length);
  for (let i = 0; i < int16Array.length; i++) {
    // Int16的范围是-32768到32767,转成Float32的-1到1范围
    float32Array[i] = int16Array[i] / 32768;
  }
  return float32Array;
}

2. 用ScriptProcessorNode处理流式音频(避免报错+实时播放)

你之前的错误是因为流式音频是片段化的,每次接收的buffer是一小段,而且可能存在空数据的情况;另外,每次创建新的BufferSource会导致音频断断续续,正确的做法是用ScriptProcessorNode(或者更现代的AudioWorklet)来维护一个音频队列,实时填充音频数据:

retrieveAudioStream = () => {
  const audioContext = new window.AudioContext();
  // 创建ScriptProcessorNode,缓冲区大小和发送端保持一致(2048)
  const scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1);
  
  // 用来存储接收到的音频数据片段队列
  const audioQueue = [];

  // 实时处理音频输出
  scriptProcessor.onaudioprocess = (e) => {
    const outputBuffer = e.outputBuffer;
    const outputData = outputBuffer.getChannelData(0);
    
    // 从队列里取数据填充输出缓冲区
    let offset = 0;
    while (offset < outputData.length && audioQueue.length > 0) {
      const chunk = audioQueue.shift();
      const chunkLength = chunk.length;
      // 取当前能填充的最大长度
      const copyLength = Math.min(outputData.length - offset, chunkLength);
      
      // 把数据复制到输出缓冲区
      outputData.set(chunk.subarray(0, copyLength), offset);
      offset += copyLength;
      
      // 如果当前片段还有剩余,放回队列开头
      if (chunkLength > copyLength) {
        audioQueue.unshift(chunk.subarray(copyLength));
      }
    }
    
    // 如果队列里没数据了,填充静音(避免爆音)
    while (offset < outputData.length) {
      outputData[offset] = 0;
      offset++;
    }
  };

  // 把处理节点连接到音频输出
  scriptProcessor.connect(audioContext.destination);

  // 接收Socket.io的流数据
  this.socket.on('stream', (arrayBuffer) => {
    // 先把ArrayBuffer转成Int16Array,再转回Float32Array
    const int16Array = new Int16Array(arrayBuffer);
    const float32Array = convertInt16ToFloat32(int16Array);
    
    // 把转换好的数据加入队列
    audioQueue.push(float32Array);
  });
}

3. 为什么之前的createBuffer会报错?

你遇到的NotSupportedError大概率是这两个原因:

  • 接收的arrayBuffer是空的,导致转成TypedArray后长度为0,createBuffer要求帧数必须大于0
  • 你没有把接收的ArrayBuffer转成正确的TypedArray(应该是Int16Array,因为发送端转的是Int16),直接用原始的ArrayBuffer去调用copyToChannel,导致数据格式不匹配

4. 如果非要用audio元素怎么办?

如果你的场景不是实时流式播放,而是接收完完整音频再播放,可以把所有ArrayBuffer片段拼接成一个完整的TypedArray,然后封装成带格式头的Blob(比如WAV格式),再生成Blob URL给audio元素的src:

// 示例:拼接所有片段后生成WAV Blob播放(非实时场景)
let allAudioData = [];

this.socket.on('stream', (arrayBuffer) => {
  allAudioData.push(new Int16Array(arrayBuffer));
});

// 假设接收完后调用这个函数播放
function playCompleteAudio() {
  // 拼接所有Int16数据
  const totalLength = allAudioData.reduce((sum, chunk) => sum + chunk.length, 0);
  const combinedInt16 = new Int16Array(totalLength);
  let offset = 0;
  allAudioData.forEach(chunk => {
    combinedInt16.set(chunk, offset);
    offset += chunk.length;
  });

  // 生成WAV格式的Blob(需要添加WAV文件头)
  const wavBlob = createWavBlob(combinedInt16, 44100); // 采样率和发送端一致
  const audioUrl = URL.createObjectURL(wavBlob);
  
  const audio = document.createElement('audio');
  audio.src = audioUrl;
  audio.play();
}

// 工具函数:把Int16Array转成WAV格式的Blob
function createWavBlob(int16Array, sampleRate) {
  const numChannels = 1;
  const bytesPerSample = 2;
  const buffer = new ArrayBuffer(44 + int16Array.length * bytesPerSample);
  const view = new DataView(buffer);

  // 写入WAV文件头
  view.setUint8(0, 0x52); // R
  view.setUint8(1, 0x49); // I
  view.setUint8(2, 0x46); // F
  view.setUint8(3, 0x46); // F
  view.setUint32(4, 36 + int16Array.length * bytesPerSample, true); // 文件大小
  view.setUint8(8, 0x57); // W
  view.setUint8(9, 0x41); // A
  view.setUint8(10, 0x56); // V
  view.setUint8(11, 0x45); // E
  view.setUint8(12, 0x66); // f
  view.setUint8(13, 0x6D); // m
  view.setUint8(14, 0x74); // t
  view.setUint8(15, 0x20); // 空格
  view.setUint32(16, 16, true); // 子块大小
  view.setUint16(20, 1, true); // 音频格式(PCM)
  view.setUint16(22, numChannels, true); // 声道数
  view.setUint32(24, sampleRate, true); // 采样率
  view.setUint32(28, sampleRate * numChannels * bytesPerSample, true); // 字节率
  view.setUint16(32, numChannels * bytesPerSample, true); // 块对齐
  view.setUint16(34, 8 * bytesPerSample, true); // 位深度
  view.setUint8(36, 0x64); // d
  view.setUint8(37, 0x61); // a
  view.setUint8(38, 0x74); // t
  view.setUint8(39, 0x61); // a
  view.setUint32(40, int16Array.length * bytesPerSample, true); // 数据大小

  // 写入音频数据
  for (let i = 0; i < int16Array.length; i++) {
    view.setInt16(44 + i * 2, int16Array[i], true);
  }

  return new Blob([buffer], { type: 'audio/wav' });
}

但注意,这个方法只适合非实时的完整音频,如果你需要实时通话那种流式播放,还是用前面的Web Audio API方案更合适。

内容的提问来源于stack exchange,提问作者Stretch0

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 10:18:39