无需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
相关产品推荐
相关产品推荐

