MediaRecorder API未记录WebAudio API AudioBufferSourceNode间停顿问题
问题解决:WebAudio + MediaRecorder 丢失音符间停顿的录音
问题原因
你当前的代码中,只有在用户点击按钮时才会创建AudioBufferSourceNode并连接到MediaStreamDestination播放音符。两次点击的间隔期间,MediaStreamDestination没有任何音频输出,MediaRecorder不会记录这段静音时段,最终录音会把所有音符直接连续拼接,丢失了原本的停顿。
解决方案
方案一:用持续静音源保持MediaStream输出
通过创建一个循环播放的静音音频源,让MediaStreamDestination始终有输出(哪怕是静音),这样MediaRecorder就能完整记录包括停顿在内的所有时段。
修改初始化代码,添加静音源:
let chunks = []; let context = new AudioContext(); let stream_destination = context.createMediaStreamDestination(); let recording = new MediaRecorder(stream_destination.stream); // 创建静音AudioBuffer(默认全0即为静音) function createSilentBuffer(ctx, duration = 1) { return ctx.createBuffer(2, ctx.sampleRate * duration, ctx.sampleRate); } // 初始化并启动持续静音源 const silentBuffer = createSilentBuffer(context); const silentSource = new AudioBufferSourceNode(context, { buffer: silentBuffer, loop: true }); silentSource.connect(stream_destination); silentSource.start(); // 原有的MediaRecorder逻辑保持不变 recording.ondataavailable = event => { chunks.push(event.data) } recording.onstop = () => { document.getElementById("recording").src = URL.createObjectURL(new Blob(chunks,{'type':'audio/mp3'})) } document.getElementById("start").addEventListener("click", () => { recording.start(1000); console.log("recording started"); }) document.getElementById("stop").addEventListener("click", () => { recording.stop(); console.log("recording stopped"); }) // 原有的音符播放逻辑保持不变 document.getElementById('sound-button').addEventListener('click', () => { const audioFilePath = './media/temp1.mp3'; const audio = new Audio(audioFilePath); audio.play(); let request = new XMLHttpRequest(); request.open("GET", audioFilePath, true); request.responseType = "arraybuffer"; request.onload = () => { let audioData = request.response; context.decodeAudioData( audioData, (buf) => { const sourceNode = new AudioBufferSourceNode(context, {buffer: buf}) sourceNode.connect(stream_destination); sourceNode.start(); } ) } request.send(); });
方案二:记录时间戳后拼接完整音频
不实时录制,而是记录每个音符的播放时间偏移,录音结束后将所有音符按时间位置拼接成完整的AudioBuffer,再导出为音频文件。这种方式能生成更紧凑的音频文件,避免全程静音的冗余数据。
完整代码示例:
let context = new AudioContext(); let recordingStartTime = null; let recordedNotes = []; // 存储结构:{ offset: 相对于录音开始的时间偏移, buffer: AudioBuffer } // 开始录音时记录起始时间 document.getElementById("start").addEventListener("click", () => { recordingStartTime = context.currentTime; recordedNotes = []; console.log("recording started"); }); // 停止录音时拼接音频并导出 document.getElementById("stop").addEventListener("click", () => { if (!recordingStartTime || recordedNotes.length === 0) return; // 计算总录音时长:最后一个音符的结束时间 const totalDuration = Math.max(...recordedNotes.map(note => note.offset + note.buffer.duration)); // 创建对应时长的空音频Buffer const mergedBuffer = context.createBuffer(2, totalDuration * context.sampleRate, context.sampleRate); // 将每个音符写入到对应时间位置 recordedNotes.forEach(note => { const startSample = Math.floor(note.offset * context.sampleRate); // 遍历左右声道写入数据 for (let channel = 0; channel < 2; channel++) { const sourceData = note.buffer.getChannelData(channel); mergedBuffer.getChannelData(channel).set(sourceData, startSample); } }); // 将拼接后的Buffer转换为可播放的音频Blob const sourceNode = new AudioBufferSourceNode(context, { buffer: mergedBuffer }); const streamDest = context.createMediaStreamDestination(); sourceNode.connect(streamDest); const exportRecorder = new MediaRecorder(streamDest.stream); let exportChunks = []; exportRecorder.ondataavailable = e => exportChunks.push(e.data); exportRecorder.onstop = () => { document.getElementById("recording").src = URL.createObjectURL(new Blob(exportChunks, { type: 'audio/mp3' })); }; exportRecorder.start(); sourceNode.start(); // 播放完成后停止录制 setTimeout(() => exportRecorder.stop(), totalDuration * 1000); console.log("recording stopped"); }); // 点击按钮播放音符并记录时间偏移 document.getElementById('sound-button').addEventListener('click', () => { const audioFilePath = './media/temp1.mp3'; const audio = new Audio(audioFilePath); audio.play(); let request = new XMLHttpRequest(); request.open("GET", audioFilePath, true); request.responseType = "arraybuffer"; request.onload = () => { let audioData = request.response; context.decodeAudioData( audioData, (buf) => { if (recordingStartTime) { // 计算当前音符相对于录音开始的时间偏移 const offset = context.currentTime - recordingStartTime; recordedNotes.push({ offset, buffer: buf }); } } ) } request.send(); });
方案对比
- 方案一:实现简单,实时录制,无需后期处理,但录音文件会包含全程静音,体积稍大。
- 方案二:文件更紧凑,仅保留必要的停顿,但需要处理时间戳和音频拼接,逻辑相对复杂。
内容的提问来源于stack exchange,提问作者jacob matias
相关产品推荐
相关产品推荐

