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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 12:22:18