音频播放减慢游戏速度

时间:2021-04-28 02:38:06

标签: javascript audio html5-audio nwjs

我正在尝试使用 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,但很难创建一个并处理瞬间的声音(可能是另一个问题)。

1 个答案:

答案 0 :(得分:0)

可以重用缓冲区,有点hackish。
先创建

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。使用类型化数组和 worker 提供了真正高性能的组合,具有 60 fps、清晰的声音和完全没有延迟。

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

经过测试,AudioWorklets 似乎按预期工作,但它们很笨重、难以维护并消耗大量资源,编写简单地将输入复制到输出的处理器违背了它的目的。 PostMessaging 系统确实是一个繁重的过程,你要么用标准方式连接并重新创建缓冲区,要么将其复制到 Worklet 空间并通过共享数组和原子操作手动管理。

您可能还对以下内容感兴趣:Writeup about WebAudio design 作者有相同的担忧并遇到完全相同的问题,引用

<块引用>

我知道我在这里打了一场艰苦的战斗,但 GC 不是我们想要的 实时音频播放时需要。

保留一个 AudioBuffers 池似乎可行,尽管在我自己的测试中 在主要 GC 擦除之前,我仍然看到应用程序随着时间的推移缓慢增长到 12MB, 根据 Chrome 分析器。

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

<块引用>

考虑以下场景:

  1. 执行了大量分配。
  2. 这些元素中的大多数(或所有元素)都被标记为无法访问(假设我们将一个指向缓存的引用设为 null,我们不知道 不再需要)。
  3. 不会执行进一步的分配。
<块引用>

在这种情况下,大多数 GC 将不会运行任何进一步的收集传递。 换句话说,即使有无法访问的引用可用 对于收藏,收藏家不要求这些。这些不是 严格泄漏,但仍然会导致内存使用量高于平常。

相关问题