代码之家  ›  专栏  ›  技术社区  ›  Evil

AudioWorklet播放背景声音并允许点击操作

  •  0
  • Evil  · 技术社区  · 5 年前

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

    0 回复  |  直到 5 年前
        1
  •  0
  •   Evil    5 年前

    可以重用缓冲区,这有点老套。
    首先创建

    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只在有分配和内存重新分配时才会触发。垃圾回收器会中断所有脚本执行,因此会出现jank、lag。
    我使用内存池来配合这个技巧,在初始化时分配池,然后只重用对象,在第二次扫描后实际上没有GC,它在初始化后运行一次,在优化后第二次启动,减少了未使用的内存。在那之后,根本没有GC。使用类型数组和workers可以获得非常高的性能组合,帧率为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将不会运行任何进一步的收集过程。 换句话说,即使有无法访问的引用可用 对于收藏,收藏家不会认领这些物品。这些不是 严格来说是泄漏,但仍然会导致比平时更高的内存使用率。

    推荐文章