如何停止 Web Audio API 数组中的所有 sources
结论
问题出在数组删除逻辑,AudioBufferSourceNode.stop() 本身没有问题。
原代码中的 sources.splice(sources[i], 1) 把 source 对象当成了数组索引,而 splice() 的第一个参数必须是数字。正向遍历数组时直接删除元素,还会让后面的元素向前移动,导致部分 source 被跳过。
可以从数组末尾逐个取出 source,再调用 stop() 和 disconnect()。如果 source 是自然播放结束,只需断开连接并将其从数组中删除,无须再次调用 stop()。
原代码为什么会漏掉声音源
下面这行代码传错了参数:
sources.splice(sources[i], 1);
正确形式应该类似:
sources.splice(i, 1);
不过,只改这一处仍然无法解决正向循环的问题。假设数组中有三个元素:
[A, B, C]
删除索引 0 的 A 后,数组会变成:
[B, C]
循环变量随后增加到 1,下次访问的元素是 C,于是 B 被跳过。原代码还缓存了删除前的 len,继续循环时可能读到 undefined。
ended 回调里也有同样的错误:
sources.splice(source, 1);
这里需要先用 indexOf() 找到 source 对应的数字索引。
推荐写法
使用 pop() 从数组末尾取出元素,可以避开删除元素后索引移动的问题:
var sources = [];
function stopAudio() {
console.log("function: stopAudio");
while (sources.length > 0) {
var source = sources.pop();
// 批量停止时不再需要等待 ended 回调清理
source.onended = null;
source.stop();
source.disconnect();
}
}
播放结束后,ended 回调只负责清理:
function playAudio(audioBuffer) {
console.log("function: playAudio");
if (!audioBuffer || !context) {
console.log("Audio not loaded");
return;
}
var source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(gainNode);
source.onended = function () {
source.disconnect();
var index = sources.indexOf(source);
if (index !== -1) {
sources.splice(index, 1);
}
};
sources.push(source);
source.start();
}
这里把参数命名为 audioBuffer,是因为 AudioBufferSourceNode.buffer 接收的是已经解码的 AudioBuffer,不能直接传入普通的 URL 字符串。
如果必须使用正向遍历
可以先复制一份 source 数组并清空原数组,再逐个停止复制出来的 source。这样就不会在遍历过程中修改同一个数组:
function stopAudio() {
var activeSources = sources.slice();
sources.length = 0;
activeSources.forEach(function (source) {
source.onended = null;
source.stop();
source.disconnect();
});
}
也可以改用倒序遍历:
function stopAudio() {
for (var i = sources.length - 1; i >= 0; i--) {
var source = sources[i];
source.onended = null;
source.stop();
source.disconnect();
sources.splice(i, 1);
}
}
倒序删除不会改变尚未处理元素的索引。
注意事项
AudioBufferSourceNode 是一次性节点。调用 start() 后,即使节点已经停止,也不能再次播放。下次播放时必须重新调用 createBufferSource() 创建节点,同一个 AudioBuffer 则可以重复使用。
自然触发 ended 时,source 已经停止,回调中无须再调用:
source.stop();
调整 GainNode.gain 只会让输出静音,不会停止 source 的播放进度。要真正结束这些节点,需要调用 stop();disconnect() 用于断开音频图连接。批量清理时通常会同时调用两者。
如果 source 在调用 start() 前就进入停止流程,stop() 可能抛出 InvalidStateError。按上面的顺序处理,不要在 start() 前让停止逻辑访问 source,就能避开这个问题。
备注:内容仅供参考。
