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

无需MediaRecorder的JavaScript音频录制方案求助

替代MediaRecorder的音频录制方案(适配Android Web Speech API)

问题背景

Android设备上,new MediaRecorder(stream)会与Web Speech API抢占音频输入资源,导致后者无法正常工作。以下是无需使用MediaRecorder构造函数的音频录制实现方案,基于Web Audio API直接捕获并处理音频数据。


核心思路

通过Web Audio API的MediaStreamSourceNode接入音频流,使用ScriptProcessorNode(兼容性优先)或AudioWorklet(现代标准)直接捕获原始音频样本,手动收集并编码为可导出的音频格式(如WAV),避免与Web Speech API的资源冲突。


具体实现代码(ScriptProcessorNode版,适配Android)

let audioContext;
let mediaStreamSource;
let scriptProcessor;
let audioChunks = [];
let isRecording = false;

// 初始化录制(需用户交互触发,比如按钮点击)
async function startRecording() {
  if (isRecording) return;

  // 1. 获取用户音频流
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  
  // 2. 初始化AudioContext(Android需用户交互激活)
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
  mediaStreamSource = audioContext.createMediaStreamSource(stream);
  
  // 3. 创建ScriptProcessorNode(缓冲区大小可选256/512/1024/2048/4096/8192/16384)
  scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
  
  // 4. 监听音频数据,收集原始样本
  scriptProcessor.onaudioprocess = function(e) {
    if (!isRecording) return;
    // 获取左声道的音频样本(Float32Array格式)
    const inputBuffer = e.inputBuffer;
    const channelData = inputBuffer.getChannelData(0);
    // 深拷贝数据到数组,避免引用被覆盖
    audioChunks.push(new Float32Array(channelData));
  };
  
  // 5. 连接音频节点
  mediaStreamSource.connect(scriptProcessor);
  scriptProcessor.connect(audioContext.destination);
  
  isRecording = true;
}

// 停止录制并导出音频Blob
async function stopRecording() {
  if (!isRecording) return;
  
  isRecording = false;
  
  // 断开连接,释放资源
  mediaStreamSource.disconnect();
  scriptProcessor.disconnect();
  audioContext.close();
  
  // 6. 将收集的Float32样本转换为WAV格式的Blob
  const wavBlob = convertFloat32ToWav(audioChunks);
  audioChunks = []; // 清空缓存
  
  // 可用于下载或上传
  const url = URL.createObjectURL(wavBlob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'recording.wav';
  a.click();
  URL.revokeObjectURL(url);
}

// 辅助函数:将Float32Array音频样本转换为WAV格式Blob
function convertFloat32ToWav(audioChunks) {
  // 合并所有音频块
  const length = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
  const mergedBuffer = new Float32Array(length);
  let offset = 0;
  for (const chunk of audioChunks) {
    mergedBuffer.set(chunk, offset);
    offset += chunk.length;
  }
  
  // 将Float32(范围-1到1)转换为Int16(范围-32768到32767)
  const int16Array = new Int16Array(mergedBuffer.length);
  for (let i = 0; i < mergedBuffer.length; i++) {
    const sample = Math.max(-1, Math.min(1, mergedBuffer[i]));
    int16Array[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
  }
  
  // 构建WAV文件头
  const wavHeader = new ArrayBuffer(44);
  const view = new DataView(wavHeader);
  
  // RIFF标识
  view.setUint8(0, 'R'.charCodeAt(0));
  view.setUint8(1, 'I'.charCodeAt(0));
  view.setUint8(2, 'F'.charCodeAt(0));
  view.setUint8(3, 'F'.charCodeAt(0));
  // 文件大小(44 + 数据长度)
  view.setUint32(4, 44 + int16Array.length * 2, true);
  // WAVE标识
  view.setUint8(8, 'W'.charCodeAt(0));
  view.setUint8(9, 'A'.charCodeAt(0));
  view.setUint8(10, 'V'.charCodeAt(0));
  view.setUint8(11, 'E'.charCodeAt(0));
  // fmt子块
  view.setUint8(12, 'f'.charCodeAt(0));
  view.setUint8(13, 'm'.charCodeAt(0));
  view.setUint8(14, 't'.charCodeAt(0));
  view.setUint8(15, ' '.charCodeAt(0));
  // 子块大小(PCM为16)
  view.setUint32(16, 16, true);
  // 音频格式(PCM为1)
  view.setUint16(20, 1, true);
  // 声道数(单声道为1)
  view.setUint16(22, 1, true);
  // 采样率(比如44100)
  view.setUint32(24, audioContext.sampleRate, true);
  // 字节率 = 采样率 * 声道数 * 位深/8
  view.setUint32(28, audioContext.sampleRate * 1 * 2, true);
  // 块对齐 = 声道数 * 位深/8
  view.setUint16(32, 1 * 2, true);
  // 位深(16位)
  view.setUint16(34, 16, true);
  // data子块
  view.setUint8(36, 'd'.charCodeAt(0));
  view.setUint8(37, 'a'.charCodeAt(0));
  view.setUint8(38, 't'.charCodeAt(0));
  view.setUint8(39, 'a'.charCodeAt(0));
  // 数据长度
  view.setUint32(40, int16Array.length * 2, true);
  
  // 合并头和数据
  const wavBuffer = new Blob([wavHeader, int16Array], { type: 'audio/wav' });
  return wavBuffer;
}

关键注意事项

  • 用户交互触发:Android浏览器要求AudioContext必须通过用户手势(如按钮点击)初始化,否则会被阻塞。
  • 资源释放:录制结束后务必断开音频节点并关闭AudioContext,避免占用音频资源影响Web Speech API。
  • 格式适配:上述代码导出WAV格式,若需要其他格式(如MP3),可在收集完样本后使用第三方音频编码库(如lamejs)转换,注意不要引入会抢占音频资源的库。
  • 现代替代方案:如果目标设备支持,可使用AudioWorklet替代ScriptProcessorNode(后者已标记为deprecated),实现更高效的音频处理,避免主线程阻塞。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 00:37:08