
本文介绍如何在 web audio api 中正确实现音频的播放与停止功能,避免重复播放、声音叠加等问题,核心是管理 audiobuffersourcenode 而非滥用 audiocontext.suspend() 或 close()。
本文介绍如何在 web audio api 中正确实现音频的播放与停止功能,避免重复播放、声音叠加等问题,核心是管理 audiobuffersourcenode 而非滥用 audiocontext.suspend() 或 close()。
在 Web Audio API 中,AudioContext 是整个音频系统的中枢,负责调度和混合所有音频节点。但它本身并不直接“播放”或“停止”声音——真正承载音频数据并触发播放的是 AudioBufferSourceNode(如 ctx.createBufferSource() 创建的节点)。因此,当用户点击“停止”按钮时,若仅调用 ctx.suspend(),已启动但尚未结束的音源仍会继续播放(因其 start() 已被调度),而再次点击“播放”又会创建新音源,导致多声部叠加。
✅ 正确做法是:显式保存所有活跃的 AudioBufferSourceNode 实例,并在停止时调用其 stop() 方法。
以下是优化后的关键代码结构(含完整可运行逻辑):
let ctx;
let c4;
let activeSources = []; // 存储当前所有正在播放的 source 节点
// 初始化 AudioContext
try {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
ctx = new AudioContext();
} catch (e) {
alert('Web Audio API is not supported in this browser');
}
// 加载音频资源
fetch("http://localhost:8000/3saudio/C4v9.wav")
.then(res => res.arrayBuffer())
.then(buf => ctx.decodeAudioData(buf))
.then(decoded => c4 = decoded);
// 播放单个音符(返回创建的 source 节点,便于后续控制)
function playNote(buffer, delay = 0) {
const source = ctx.createBufferSource();
const gainNode = ctx.createGain();
source.buffer = buffer;
source.connect(gainNode).connect(ctx.destination);
// 包络控制(音量淡出)
gainNode.gain.setValueAtTime(1, ctx.currentTime + delay);
gainNode.gain.exponentialRampToValueAtTime(0.0005, ctx.currentTime + 1.25 + delay);
source.start(ctx.currentTime + delay);
activeSources.push(source); // 记录该音源
return source;
}
// 批量播放序列(如 step51)
function step51() {
const keys = ['c4', 'c4', 'c4', 'c4', 'c4']; // 示例占位,实际应映射到 buffer
let del = 0;
const start = 12;
for (let i = 0; i < 10; i++) {
playNote(c4, del + 0);
playNote(c4, del + 0.25);
playNote(c4, del + 0.5);
playNote(c4, del + 0.75);
playNote(c4, del + 1.0);
playNote(c4, del + 1.25);
del += 1.5; // 避免时间重叠(可按需调整)
}
}
// 【关键】停止所有当前播放的音源
function stopAll() {
activeSources.forEach(source => {
if (source && source.state !== 'ended') {
try {
source.stop(); // 强制终止播放
} catch (e) {
// ignore: 已自然结束的节点调用 stop() 会抛错,属正常
}
}
});
activeSources.length = 0; // 清空数组
}
// 绑定事件
document.querySelector('button').addEventListener('click', () => {
if (ctx.state === 'suspended') ctx.resume();
step51();
});
document.querySelector('#button_stop').addEventListener('click', () => {
stopAll(); // ✅ 核心:停止所有 source,而非 suspend context
});? 重要注意事项:
- AudioBufferSourceNode 是一次性节点:一旦 stop() 或自然播放完毕,不可再次 start();每次播放必须新建 createBufferSource()。
- ctx.suspend() 仅用于暂停整个上下文(如页面失焦时节能),不适用于实时停止播放;滥用会导致恢复播放时需用户交互触发 resume()(Chrome 等浏览器策略)。
- ctx.close() 会彻底销毁上下文,之后无法重建(除非刷新页面),应避免在交互流程中使用。
- 建议为每个 playNote() 返回的 source 添加 onended 回调自动清理 activeSources 数组(进阶优化):
source.onended = () => { const idx = activeSources.indexOf(source); if (idx > -1) activeSources.splice(idx, 1); };
通过精准控制 AudioBufferSourceNode 的生命周期,即可实现干净、可复用、无叠加的播放/停止体验,为后续添加音色选择、速度调节等交互功能打下坚实基础。

















