PAYATHON 2026

如何停止 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,就能避开这个问题。

备注:内容仅供参考。