前言
原本使用的是 egret 的 egret.SoundChannel
和 egret.Sound
来管理音频,但不知为何在重复将游戏切换到前后台后,很容易出现背景音播放不全、播放重复和无法播放的问题,懒得再去排查原因干脆使用小游戏提供的音频播放 API 重写了一个音频管理工具。
API 相关
参考官方文档 教程/音频播放 ,大致了解音频播放是利用微信接口 wx.createInnerAudioContext() 创建 InnerAudioContext 对象作为唯一实现方式,此外,同时播放的音频实例在 Android 平台上有 10 个的上限限制,对于不再使用的音频实例可以通过 InnerAudioContext.destroy() 接口销毁。
大致思路
管理器可以设计为单例模式,其中包含:
一个播放音效的音频实例池,并设置池子的容量为 9 (留一个给背景音乐播放使用);
一个单独用于播放背景音乐的音频实例(不放入池内管理)
创建管理器时可以预创建少量实例在实例池中,每次播放音效的步骤:
从池内获取可用的(状态 paused == true)播放实例;
当实例用完但还未达到池子容量时,可创建新的实例并放在池内;
当无可用且池子已满时,停掉最早创建的音频实例,用于播放新的音频(视音效的重要性的选择,以新音效为重)。
为了防止每次都重新下载音频,需要结合资源缓存机制,即播放一个新的音频时,先将音频下载到本地,再将本地文件地址赋值给
.src
属性,再播放音频。
实现代码
class WeChatSoundManager { private static _instance: WeChatSoundManager; public static get instance() { if (!this._instance) { this._instance = new WeChatSoundManager(); } return this._instance; } private _bgmCtx = null; private _bgmPath = ""; private soundCtxPool = new Array<any>(); // 预创建实例 private PreBuildNum = 3; private MaxSoundNum = 10; // 实例使用指针 private _index = 0; public constructor() { let soundCtx = null; for (let i = 0; i++; i < this.PreBuildNum) { soundCtx = wx.createInnerAudioContext(); this.soundCtxPool.push(soundCtx); } } public clean() { this.soundCtxPool = new Array<any>(); this._index = 0; this._bgmCtx = null; this._bgmPath = ""; } // 播放背景音乐 public playBGM(bgmPath: string) { //if (gGame.chanelType != ChanelType.WXMini) return; gLog('背景音乐播放地址:' + bgmPath); if (this._bgmPath == bgmPath && this._bgmCtx != null) { this._bgmCtx.play(); } else { this._bgmPath = bgmPath; if (!this._bgmCtx) { this._bgmCtx = wx.createInnerAudioContext(); } this._bgmCtx.src = bgmPath; this._bgmCtx.loop = true; this._bgmCtx.autoplay = true; } } // 背景音乐是否正在播放 public isBGMPlaying() { if (this._bgmCtx) { return !this._bgmCtx.paused; } return false; } // 暂停背景音乐 public pauseBGM() { if (this._bgmCtx) { this._bgmCtx.pause(); } } // 恢复背景音乐 public resumeBGM() { if (this._bgmCtx) { this._bgmCtx.play(); } else { if (this._bgmPath) { this.playBGM(this._bgmPath); } } } // 停止背景音乐 public stopBGM() { if (this._bgmCtx) { this._bgmCtx.stop(); this._bgmCtx.destroy(); this._bgmCtx = null; } } // 播放音频 public playSound(soundPath: string) { //if (gGame.chanelType != ChanelType.WXMini) return; gLog('音频播放地址:' + soundPath); let soundCtx = this.getSoundCtx(); if (soundCtx) { soundCtx.src = soundPath; soundCtx.stop(); soundCtx.play(); } } public stopAllSound() { this.soundCtxPool.forEach(soundCtx => { // 暂停或停止了 if (!soundCtx.paused) { soundCtx.stop(); } }); } // 获取一个音频实例 private getSoundCtx() { let soundCtx = null; for (let i = 0; i++; i < this.soundCtxPool.length) { soundCtx = this.soundCtxPool[i]; // 暂停或停止了 if (soundCtx.paused) { return soundCtx; } } if (!soundCtx) { // 留一个实例用于播放背景音乐 if (this.soundCtxPool.length < this.MaxSoundNum - 1) { soundCtx = wx.createInnerAudioContext(); } else { soundCtx = this.soundCtxPool[this._index]; this._index++; if (this._index > this.MaxSoundNum) { this._index = 0; } soundCtx.stop(); } } return soundCtx; } }
调用方式:
WeChatSoundManager.instance.playBGM(url); // 播放背景音乐 WeChatSoundManager.instance.playSound(url); // 播放普通音效
其他
在手机有外部事件需要暂停音频和事件结束恢复音频播放,分别通过游戏中全局监听 wx.onAudioInterruptionBegin
和 wx.onAudioInterruptionEnd
两个事件来实现,当然为了方便和防止重复创建,可以将其写在音频管理器的构建方法中,如下:
public constructor() { let soundCtx = null; for (let i = 0; i++; i < this.PreBuildNum) { soundCtx = wx.createInnerAudioContext(); this.soundCtxPool.push(soundCtx); } // 中断事件开始 wx.onAudioInterruptionBegin(() => { if (this.musicEnabled) { gLog('------------ 暂停背景音乐'); this.pauseBGM(); } }); // 中断事件结束 wx.onAudioInterruptionEnd(() => { if (this.musicEnabled) { gLog('------------ 恢复背景音乐'); this.resumeBGM(); } }); }