音频播放减慢游戏速度

Audio playback slows down game

我正在尝试使用 nw.js(node.js + chromium 页面)开发一个简单的游戏。

<canvas width="1200" height="800" id="main"></canvas>
<script>
var Mouse = {x: 0, y: 0, fire: false};

(async function() {
"use strict";
const reload = 25;
var ireload = 0;
const audioCtx = new AudioContext();
let fire = await fetch('shotgun.mp3');
let bgMusic = await fetch('hard.mp3');
    fire = await fire.arrayBuffer();
    bgMusic = await bgMusic.arrayBuffer();
    
    const bgMdecoded = await audioCtx.decodeAudioData(bgMusic);
    const fireDecoded = await audioCtx.decodeAudioData(fire);
    const bgM = audioCtx.createBufferSource();
    bgM.buffer = bgMdecoded;
    bgM.loop = true;
    bgM.connect(audioCtx.destination)
    bgM.start(0);
    
    let shot = audioCtx.createBufferSource();
    shot.buffer = fireDecoded;
    shot.connect(audioCtx.destination);
    
    document.getElementById('main').onmousedown = function(e) {
        Mouse.x = e.layerX;
        Mouse.y = e.layerY;
        Mouse.fire = true;
    }
    
    function main(tick) {
        var dt =  lastTick - tick;
        lastTick = tick;
        
        ///take fire
        if(--ireload < 0 && Mouse.fire) {
            ireload = reload;
            shot.start(0);
            shot = audioCtx.createBufferSource();
            shot.buffer = fireDecoded;
            shot.connect(audioCtx.destination);
    
            Mouse.fire = false;
        }
    
        /* moving objects, rendering on thread with offscreen canvas */
        requestAnimationFrame(main);
    }   

    let lastTick = performance.now();
    main(lastTick);
})();
</script>

我已将代码精简为最小的工作示例。
问题是射击,每次我开火(///开火),游戏都会降低 FPS。完全相同的情况发生在 Kaiido 示例 (https://jsfiddle.net/sLpx6b3v/) 中。这很好用,可以长时间使用,但多次播放多个声音(游戏是射击游戏),帧率下降,一段时间后 GC 打嗝。
不到一年的游戏笔记本电脑正在下降 60fps 到大约 40fps,在 Kaidos 示例中下降到大约 44fps。

什么可以用声音解决?

期望的行为是没有滞后/没有 gc/没有由于声音而导致的帧丢失。背景中的那个效果很好。 我会尝试 AudioWorklet,但很难创建一个并处理瞬时声音(可能是另一个问题)。

重用缓冲区是可能的,有点老套。
首先创建

const audioCtx = new AudioContext();

然后照常获取资源:

let fire = await fetch('shotgun.mp3');
fire = await fire.arrayBuffer();
fire = await audioCtx.decodeAudioData(fire);

const shot = audioCtx.createBufferSource();
shot.buffer = fire;
shot.loopEnd = 0.00001; //some small value to make it unplayable
shot.start(0);

然后,在活动期间(在我的例子中是按下鼠标):

shot.loopEnd = 1; //that restarts sound and plays in a loop.

接下来,播放完再设置

shot.loopEnd = 0.00001;

在我的例子中,我在 requestAnimationFrame 中停止了它

<canvas width="1200" height="800" id="main"></canvas>
<script>
var Mouse = {x: 0, y: 0, fire: false};

(async function() {
"use strict";
const reload = 25;
var ireload = 0;
const audioCtx = new AudioContext();
let fire = await fetch('shotgun.mp3');
let bgMusic = await fetch('hard.mp3');
    fire = await fire.arrayBuffer();
    bgMusic = await bgMusic.arrayBuffer();
    
    const bgMdecoded = await audioCtx.decodeAudioData(bgMusic);
    const fireDecoded = await audioCtx.decodeAudioData(fire);
    const bgM = audioCtx.createBufferSource();
    bgM.buffer = bgMdecoded;
    bgM.loop = true;
    bgM.connect(audioCtx.destination)
    bgM.start(0);
    
    let shot = audioCtx.createBufferSource();
    shot.buffer = fireDecoded;
    shot.connect(audioCtx.destination);
    shot.loopEnd = 0.00001; //some small value to make it unplayable
    shot.start(0);
    
    document.getElementById('main').onmousedown = function(e) {
        Mouse.x = e.layerX;
        Mouse.y = e.layerY;
        Mouse.fire = true;
    }
    
    function main(tick) {
        var dt =  lastTick - tick;
        lastTick = tick;
        
        ///take fire
        //asuming 60fps, which is true in my case, I stop it after a second
        if(reload < -35) {
            shot.loopEnd = 0.00001;
        }
        
        if(--ireload < 0 && Mouse.fire) {
            ireload = reload;
            shot.loopEnd = 1; //that restarts sound and plays in a loop.
            Mouse.fire = false;
        }
    
        /* moving objects, rendering on thread with offscreen canvas */
        requestAnimationFrame(main);
    }   

    let lastTick = performance.now();
    main(lastTick);
})();
</script>

关于 GC 的说明,它确实可以快速处理音频缓冲区,但我已经检查过,只有在有分配和内存重新分配时才会触发 GC。垃圾收集器会中断所有脚本的执行,因此会出现卡顿、滞后。
我将内存池与这个技巧结合使用,在初始化时分配池,然后只重用对象,第二次扫描后几乎没有 GC,它运行一次,初始化后第二次启动,优化后减少未使用的内存。之后就完全没有GC了。使用类型化数组和 workers 提供真正高效的组合,60 fps,清脆的声音,完全没有延迟。

您可能认为锁定 GC 是个坏主意。也许你是对的,但说到底,仅仅因为有GC而浪费资源似乎也不是一个好主意。

经过测试,AudioWorklets 似乎可以按预期工作,但它们很重、难以维护并且会消耗大量资源,而编写仅将输入复制到输出的处理器违背了它的目的。 PostMessaging 系统是一个非常繁重的过程,您必须以标准方式连接并重新创建缓冲区,或者将其复制到 Worklet space 并通过共享数组和原子操作手动管理它。

您可能还对以下内容感兴趣:Writeup about WebAudio design 作者分享了关注点并遇到了完全相同的问题,引用

I know I’m fighting an uphill battle here, but a GC is not what we need during realtime audio playback.

Keeping a pool of AudioBuffers seems to work, though in my own test app I still see slow growth to 12MB over time before a major GC wipes, according to the Chrome profiler.

Writeup about GC,其中描述了JavaScript中的内存泄漏。引用:

Consider the following scenario:

  1. 执行了一组相当大的分配。
  2. 这些元素中的大多数(或所有元素)都被标记为不可访问(假设我们将指向我们没有的缓存的引用设为空 需要更长的时间)。
  3. 没有执行进一步的分配。

In this scenario, most GCs will not run any further collection passes. In other words, even though there are unreachable references available for collection, these are not claimed by the collector. These are not strictly leaks but still, result in higher-than-usual memory usage.