关于微信小游戏音效播放的一个小优化

本文前面说说问题和解决思路,有点长,可以直接跳到后面的代码部分。

文档中是使用 AudioSource 的 playOneShot 播放音效,在 web 调试时一切正常,但是打包成小游戏后就发现了两个问题:
1、音频播放存在延迟,真机环境上,iOS 的延迟不明显,并不影响体验;但是安卓的延迟就严重到不可容忍的程度了;
2、游戏进行一段时间后,会出现 “Too many InnerAudioContext instance.” 的 warning。

如果音效播放不是很频繁,这两个问题并不突出,但是需要频繁的实时音效时这个问题就无法忍受了,游戏月初上线,至今将近一个月这个问题都困扰着我,同时我也发现,其他使用 cocoscreator 开发的,有实时音效的微信小游戏也有类似音效延迟甚至丢失的问题,看来这不是一个孤例。
今天我决定一鼓作气彻底修复这个问题。先分析问题,微信小游戏的代码已经经过压缩,很难被人读懂,先从已知的信息入手吧。

从问题 2 的 warning 提示来看,问题点在于 cocoscreator 打包成小游戏代码以后,每一次播放音频都会重新创建音频播放上下文实例,而在音效播放完毕后并没有回收,导致过度占用内存;同时结合微信小游戏文档中的说明,上下文实例创建过程中需要重新加载音频文件,这也就是问题 1 音效延迟的原因。
这么梳理下来问题就很清晰了,问题是出在 playOneShot 方法上,打包成小游戏以后这个方法每次调用都会重新创建一个音频播放上下文实例,并且实例播放完毕并没有释放。

解决思路也逐渐成行:使用 AudioSource 组件,clip 固定设置为要播放的音效,关闭 loop,只需要要在播放某个音效的时候找到存放该音效的 AudioSource 组件并调用 play 方法就好了。
照着这个思路我重新写了一个 AudioManager 类,代码如下:

import { AudioClip, AudioSource, Node, resources } from "cc";

export class AudioManager {

    private container: Node = null;

    private audioSources: {[key: string]: AudioSource[]} = {};

    public constructor(containerNode: Node) {
        this.container = containerNode;
    }

    public async playSound(name: string) {
        const audioSource: AudioSource = await this.findIdleAudioSource(name);

        if (audioSource) {
            audioSource.play();
        }
    }

    private async findIdleAudioSource(name: string): Promise<AudioSource> {
        if (!this.audioSources[name]) {
            this.audioSources[name] = [];
            await this.creatAudioSource(name);
        }

        for (let i = 0; i < this.audioSources[name].length; i++) {
            const audioSource: AudioSource = this.audioSources[name][i];
            if (!audioSource.playing) {
                return audioSource;
            }
        }
        if (this.audioSources[name].length < 10) {
            return await this.creatAudioSource(name);
        }
        return null;
    }

    private creatAudioSource(name: string): Promise<AudioSource> {
        return new Promise((resolve, reject) => {
            resources.load('audios/' + name, AudioClip, (err, clip: AudioClip) => {
                const audioSource: AudioSource = this.container.addComponent(AudioSource);
                audioSource.playOnAwake = false;
                audioSource.loop = false;
                audioSource.volume = 1;
                audioSource.clip = clip;
                this.audioSources[name].push(audioSource);

                resolve(audioSource);
            });
        });
    }
}

我没有使用单例,因为我的音效只在游戏场景中使用,所以直接用了一普通类供音频管理组件调用。大家也可以根据实际情况具体问题具体分析。
该类里我维护了一个 audioSources 属性,用于存放不同的 AudioSource 实例。当 playSound 方法被调用,如果是新的音效,则会创建一个新的 AudioSource,同时我也允许在当前音效没有空闲的 AudioSource 时创建新的 AudioSource,最高限制十个,用于避免因为上一个音效未播放完导致下一个音效无法正常播放的情况。

问题得到了完美解决,再也不存在音效延迟的问题了(当然首次播放依然有少许延迟,但是在可忍受范围内,如果这个问题无法忍受的话,也可以考虑增加预加载机制),内存占用也被降低了。

还是希望官方能够尽早优化 playOneShot 方法,末了打个小广告,我做的两个小游戏,欢迎各位大佬提点建议:

2048Q弹数字球
qrcode

郝小贱的穿越历险:
qrcode

6赞

AudioSource.maxAudioChannel
这个值可以判断最大可以创建多少,不同平台策略不一样

2赞

也碰到这个问题,应该是个bug,会内存溢出
游戏老是因为内存超出被杀掉,查了好久才发现是音效播放的问题,去掉音效完美60fps​:sweat:

我这段代码可以解决内存超出和音效延迟问题:stuck_out_tongue:
不过代码还是有点问题,我之前没考虑音道问题,给设置了每个音效可以同时播放10个。但是微信的音道只有10。并发的音效超过10个会自动关闭超出的。会导致BGM无法播放,以后要优化

我觉得问题不大,背景音乐最先循环播放,会一直占用一个,后续的频繁音效超出部分不播放也听不出来的,哈哈

不是,问题就在于背景音乐第一个播放,音效超出音道后第一个关掉的就是背景音乐 :rofl:

3.8解决了playOneShot内存泄漏问题,可以愉快的玩耍了

请问下,我用 AudioSource play一个WAV音效文件,cocos 浏览器 微信开发者工具里音量都正常,但是在微信小游戏里就特别小,是怎么回事?用微信的 createInnerAudioContext 播放也是一样 特别轻。

好像是wav文件的原因,转成mp3就好了