Web Audio API深度实战:从零实现浏览器端录音与音频处理

张开发
2026/4/4 19:14:30 15 分钟阅读
Web Audio API深度实战:从零实现浏览器端录音与音频处理
一、引言在浏览器端实现录音功能过去依赖Flash等插件。随着Web Audio API和MediaDevices API的成熟现代浏览器已原生支持高质量的音频采集与处理。本文将从零实现一个浏览器端录音器涵盖麦克风权限获取AudioContext音频处理图构建PCM数据采集WAV格式封装与下载实时音量监测所有代码均为原生JavaScript可直接复制使用。二、技术栈API用途MediaDevices.getUserMedia获取麦克风权限和音频流AudioContextWeb Audio核心管理音频处理图AudioWorklet高性能音频处理替代ScriptProcessorNodeBlob URL.createObjectURL导出音频文件三、完整实现3.1 获取麦克风权限javascriptasync function getMicrophoneStream() { try { const stream await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }); return stream; } catch (err) { console.error(获取麦克风失败:, err); throw err; } }3.2 创建AudioContext并连接javascriptlet audioContext null; let mediaStream null; let workletNode null; let pcmChunks []; async function initRecording() { // 获取流 mediaStream await getMicrophoneStream(); // 创建AudioContext采样率默认设备可用targetSampleRate指定 audioContext new AudioContext({ sampleRate: 48000, latencyHint: interactive }); // 创建MediaStreamSource const source audioContext.createMediaStreamSource(mediaStream); // 加载AudioWorklet模块 await audioContext.audioWorklet.addModule(recorder-worklet.js); // 创建AudioWorkletNode workletNode new AudioWorkletNode(audioContext, recorder-worklet); // 连接source → workletNode → destination可选用于监听 source.connect(workletNode); workletNode.connect(audioContext.destination); // 监听PCM数据 workletNode.port.onmessage (event) { const pcmData event.data; pcmChunks.push(pcmData); }; // 启动AudioContext await audioContext.resume(); }3.3 AudioWorklet处理器实现recorder-worklet.js:javascriptclass RecorderWorklet extends AudioWorkletProcessor { static get parameterDescriptors() { return []; } constructor() { super(); this.bufferSize 4096; } process(inputs, outputs, parameters) { const input inputs[0]; if (input input.length 0) { const channelData input[0]; // 复制数据因为原数据会被复用 const copy new Float32Array(channelData.length); copy.set(channelData); this.port.postMessage(copy); } return true; // 保持处理器活跃 } } registerProcessor(recorder-worklet, RecorderWorklet);3.4 PCM转WAV浏览器采集的是Float32格式PCM范围-1.0 ~ 1.0需要转换为16bit PCM并封装WAV头。javascriptfunction float32ToInt16PCM(float32Array) { const int16Array new Int16Array(float32Array.length); for (let i 0; i float32Array.length; i) { // 将[-1,1]映射到[-32768,32767] const s Math.max(-1, Math.min(1, float32Array[i])); int16Array[i] s 0 ? s * 0x8000 : s * 0x7FFF; } return int16Array; } function encodeWAV(int16Array, sampleRate, numChannels 1) { const byteRate sampleRate * numChannels * 2; // 16bit 2字节 const blockAlign numChannels * 2; const dataSize int16Array.length * 2; const buffer new ArrayBuffer(44 dataSize); const view new DataView(buffer); // RIFF chunk writeString(view, 0, RIFF); view.setUint32(4, 36 dataSize, true); writeString(view, 8, WAVE); // fmt subchunk writeString(view, 12, fmt ); view.setUint32(16, 16, true); // chunk size view.setUint16(20, 1, true); // audio format (PCM) view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, byteRate, true); view.setUint16(32, blockAlign, true); view.setUint16(34, 16, true); // bits per sample // data subchunk writeString(view, 36, data); view.setUint32(40, dataSize, true); // 写入PCM数据 for (let i 0; i int16Array.length; i) { view.setInt16(44 i * 2, int16Array[i], true); } return new Blob([buffer], { type: audio/wav }); } function writeString(view, offset, str) { for (let i 0; i str.length; i) { view.setUint8(offset i, str.charCodeAt(i)); } }3.5 停止录音并导出javascriptasync function stopRecordingAndExport() { if (!audioContext) return; // 停止所有音轨 if (mediaStream) { mediaStream.getTracks().forEach(track track.stop()); } // 关闭AudioContext await audioContext.close(); // 合并所有PCM块 const totalLength pcmChunks.reduce((sum, chunk) sum chunk.length, 0); const combined new Float32Array(totalLength); let offset 0; for (const chunk of pcmChunks) { combined.set(chunk, offset); offset chunk.length; } // 转换为16bit并封装WAV const int16Data float32ToInt16PCM(combined); const sampleRate audioContext.sampleRate; const wavBlob encodeWAV(int16Data, sampleRate); // 下载 const url URL.createObjectURL(wavBlob); const a document.createElement(a); a.href url; a.download recording_${Date.now()}.wav; a.click(); URL.revokeObjectURL(url); // 重置状态 pcmChunks []; }3.6 实时音量监测扩展AudioWorklet增加音量计算javascript// recorder-worklet.js 扩展 process(inputs, outputs, parameters) { const input inputs[0]; if (input input.length 0) { const channelData input[0]; // 计算RMS音量 let sum 0; for (let i 0; i channelData.length; i) { sum channelData[i] * channelData[i]; } const rms Math.sqrt(sum / channelData.length); const db 20 * Math.log10(rms 0.00001); // 避免log(0) // 发送音量数据 this.port.postMessage({ pcm: channelData, rms: rms, db: db }); } return true; }四、完整HTML示例html!DOCTYPE html html head titleWeb Audio Recorder/title /head body button idstartBtn开始录音/button button idstopBtn disabled停止并导出/button div音量: span idvolume0/span dB/div script let isRecording false; document.getElementById(startBtn).onclick async () { await initRecording(); isRecording true; document.getElementById(startBtn).disabled true; document.getElementById(stopBtn).disabled false; }; document.getElementById(stopBtn).onclick async () { await stopRecordingAndExport(); isRecording false; document.getElementById(startBtn).disabled false; document.getElementById(stopBtn).disabled true; }; /script /body /html五、注意事项问题解决方案iOS Safari要求用户手势启动录音按钮必须在用户点击后调用采样率不一致使用AudioContext.sampleRate记录WAV头中正确填写内存溢出长时间录音定期清理pcmChunks或采用流式写入AudioWorklet兼容性提供ScriptProcessorNode降级方案六、扩展对接配音工具录制的音频可以上传到云端进行后期处理也可以与配音工具结合——比如先用叮叮配音生成AI解说再混入自己录制的旁白。叮叮配音特点完全免费无限使用音色丰富50种覆盖悬疑、温情、激情等风格API支持可直接从代码调用javascript// 示例生成配音并播放 async function generateVoice(text) { const response await fetch(https://api.dingdingpeiyin.com/tts, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ text, voice: suspense_male }) }); const audioBlob await response.blob(); const url URL.createObjectURL(audioBlob); const audio new Audio(url); audio.play(); }七、总结本文完整实现了浏览器端录音功能涵盖AudioWorklet高性能音频采集Float32 PCM转16bit WAV实时音量监测文件导出下载完整代码可直接用于生产项目。欢迎评论区交流Web Audio开发心得。

更多文章