//********************* // create by 流云 // time: 2022年2月15日 // desc: 音频管理器 //********************* import { _decorator, Node, AudioSource, AudioClip, game, Game, assetManager, director, Asset, AssetManager, sys } from 'cc'; /** * 音频管理器 */ export class SoundManager { private static _ins: SoundManager = null; public static get Ins() { if (!this._ins) this._ins = new SoundManager(); return this._ins; } private _node: Node = null; private _music: AudioSource = null; private _effect: AudioSource = null; private _game_id = "default_game"; private _user_name = "default_user"; private _click_name = ""; private _bundle = ""; private _path = ""; private _curent_music = ""; private _isPause = false; /** playMusic 请求代数,丢弃过期异步回调 */ private _musicReq = 0; /** 切后台或加载未完成时记住要播的 BGM,resume 再播 */ private _pendingMusic: { name: string; path: string; bundle: string; loop: boolean; isRelease: boolean } | null = null; MusicVolume = 1; EffectVolume = 1; /** 战斗音效播放时临时压低 BGM(如 0.7 = -30%) */ MusicDuckRatio = 1; MuteMusic = false; MuteEffect = false; private _clip_map: Map = new Map(); init({ gameid = "default_game", userName = "default_user", bundle = "resources", path = "sfx/ui/", clickEffname = ``, music_volume = 1, effect_volume = 1, mute_music = false, mute_effect = false }) { this._game_id = gameid; this._user_name = userName; this._click_name = clickEffname; this._bundle = bundle; this._path = path; this.MusicVolume = music_volume; this.EffectVolume = effect_volume; this.MuteMusic = mute_music; this.MuteEffect = mute_effect; if (!this._node) { this._node = new Node("soundNode"); director.addPersistRootNode(this._node); } this._music = this._node.addComponent(AudioSource); this._effect = this._node.addComponent(AudioSource); game.on(Game.EVENT_HIDE, this.onHide, this); game.on(Game.EVENT_SHOW, this.onShow, this); this._node.off(AudioSource.EventType.ENDED, this.onAudioEnded, this); this._node.on(AudioSource.EventType.ENDED, this.onAudioEnded, this); this.getStatus(); } clear() { this._clip_map.clear(); director.removePersistRootNode(this._node); this._node = null; } onHide() { this.pause(); } onShow() { this.resume(); } /** * 暂停 */ pause() { this._isPause = true; if (this._music.playing) this._music.pause(); if (this._effect.playing) this._effect.pause(); } /** * 恢复 */ resume() { this._isPause = false; this.updateStatus(); if (!this.MuteMusic && this._pendingMusic) { const pending = this._pendingMusic; void this.playMusic(pending); } else if (this._music?.clip && this._music.clip.isValid !== false && !this.MuteMusic) { this._music.play(); } if (this._effect?.clip && !this.MuteEffect) { this._effect.play(); } } /** 仅同步音量,不触发 play(避免 duck 时把 BGM 从头重播) */ updateStatus() { if (this._music) { this._music.volume = this._isPause || this.MuteMusic ? 0 : this.MusicVolume * this.MusicDuckRatio; } if (this._effect) { this._effect.volume = this._isPause || this.MuteEffect ? 0 : this.EffectVolume; } } /** 战斗音效 duck:只改 BGM 音量倍率,不重播音乐 */ applyMusicDuck(ratio: number): void { this.MusicDuckRatio = ratio; if (this._music) { this._music.volume = this._isPause || this.MuteMusic ? 0 : this.MusicVolume * this.MusicDuckRatio; } } /** * 从本地存储获取音频设置状态 */ private getStatus() { let temp = sys.localStorage.getItem(`${this._game_id + this._user_name}_music_volume`); if (temp) this.MusicVolume = Number(temp); temp = sys.localStorage.getItem(`${this._game_id + this._user_name}_music_mute`); if (temp) this.MuteMusic = Boolean(temp); temp = sys.localStorage.getItem(`${this._game_id + this._user_name}_effect_volume`); if (temp) this.EffectVolume = Number(temp); temp = sys.localStorage.getItem(`${this._game_id + this._user_name}_effect_mute`); if (temp) this.MuteEffect = Boolean(temp); } /** * 保存音频设置到持久化存储 */ saveStatus() { sys.localStorage.setItem(`${this._game_id + this._user_name}_music_volume`, this.MusicVolume + ``); sys.localStorage.setItem(`${this._game_id + this._user_name}_music_mute`, this.MuteMusic + ``); sys.localStorage.setItem(`${this._game_id + this._user_name}_effect_volume`, this.EffectVolume + ``); sys.localStorage.setItem(`${this._game_id + this._user_name}_effect_mute`, this.MuteEffect + ``); } /** * 释放所有允许释放的音频资源 */ release() { this._clip_map.forEach((v, k) => { if (v.isRelease && v.clip) { v.clip.decRef(); v.clip = null; } }) } /** 按目录释放可释放音效(退战斗卸战斗 warm-up 用) */ releaseClipsByPath(path: string, bundle = ''): void { const toDelete: string[] = []; this._clip_map.forEach((v, k) => { if (!v.isRelease || !v.clip) return; if (v.path !== path) return; if (bundle && v.bundle !== bundle) return; v.clip.decRef(); v.clip = null; toDelete.push(k); }); for (const k of toDelete) { this._clip_map.delete(k); } } /** * 播放音乐 * @param param * @returns */ async playMusic({ name, path = ``, bundle = ``, loop = true, isRelease = false, onEnd = null, clip = null }) { const req = ++this._musicReq; if (this.MuteMusic) { this._pendingMusic = null; return; } this._pendingMusic = { name, path, bundle, loop, isRelease }; if (this._isPause) return; if (this._music && this._music.playing && name == this._curent_music) { this._pendingMusic = null; return; } let ready: AudioClip | null = clip; if (!ready) { const rec = await this.getClip(bundle, path, name, isRelease); if (req !== this._musicReq) return; ready = rec?.clip ?? null; } if (req !== this._musicReq) return; if (!ready || ready.isValid === false) { console.warn(`[SoundManager] 背景音乐加载失败,跳过播放 name=${name} path=${path || this._path} bundle=${bundle || this._bundle}`); return; } if (this.MuteMusic) return; if (this._isPause) return; this._haltMusicSource(); this._curent_music = name; this._pendingMusic = null; this._music.clip = ready; this._music.loop = loop; this._music.volume = this.MusicVolume * this.MusicDuckRatio; this._music.play(); } stopMusic() { this._musicReq++; this._pendingMusic = null; this._haltMusicSource(); } /** 停当前 BGM 源,不取消进行中的 playMusic 请求 */ private _haltMusicSource(): void { this._curent_music = ""; if (this._music) { this._music.stop(); this._music.clip = null; } } private _PlayingEffects: AudioRecord[] = []; /** * 播放音效 * @param param */ async playEffect({ name, path = ``, bundle = ``, isRelease = true, onEnd = null, clip = null, finishRelease = false, stopBefore = false }) { if (this._isPause || this.MuteEffect) { let t = 2; if (clip != null) t = clip.getDuration(); if (onEnd) setTimeout(() => { onEnd(); }, t * 1000); return; } let a: AudioRecord = null; if (clip == null) { a = await this.getClip(bundle, path, name, isRelease); } else { a = new AudioRecord(); a.clip = clip; a.bundle = bundle; a.path = path; a.name = name; a.key = `${bundle}_${path}_${name}`; a.isRelease = isRelease; } if (stopBefore) { let arr = this._PlayingEffects.filter(v => v.key == a.key); for (let i = 0; i < arr.length; i++) { arr[i].isPlaying = false; let index = this._PlayingEffects.indexOf(arr[i]) if (index > -1 && arr[i] != a) { this._PlayingEffects.splice(index, 1); if (arr[i].clip) { arr[i].clip.decRef(); arr[i].clip.destroy(); arr[i].clip = null; } } } } if (!a.clip) { if (onEnd) { setTimeout(() => { onEnd(); }, 2 * 1000); } return a; } this._effect.playOneShot(a.clip, this.EffectVolume); let len = a.clip.getDuration(); a.isPlaying = true; this._PlayingEffects.push(a); setTimeout(() => { a.isPlaying = false; let index = this._PlayingEffects.indexOf(a); if (index > -1) { this._PlayingEffects.splice(index, 1); } if (finishRelease) { if (a && a.clip) { a.clip.decRef(); a.clip = null; } if (this._clip_map.has(a.key)) this._clip_map.delete(a.key); } if (onEnd) onEnd(); }, len * 1000); return a; } /** * 预加载音效(不播放),减少首帧 playEffect 等待 */ preloadEffect({ name, path = ``, bundle = ``, isRelease = false }) { void this.getClip(bundle, path, name, isRelease); } /** * 播放点击音效 */ playClick() { if (this._click_name != "") this.playEffect({ name: this._click_name }); } /** * 当音效播放结束 * @param e */ onAudioEnded(e) { if (e["onSoundEnd"]) { e.onSoundEnd(); e.onSoundEnd = null; } } private async getClip(bundle, path, audioName, isRelease) { let b = bundle == `` ? this._bundle : bundle; let p = path == `` ? this._path : path; let k = `${b}_${p}_${audioName}`; let a: AudioRecord = this._clip_map.get(k); if (!a) { a = new AudioRecord(); this._clip_map.set(k, a); } a.bundle = b; a.path = p; a.name = audioName; a.key = k; a.isRelease = isRelease; if (a.clip && a.clip.isValid === false) { a.clip = null; } if (!a.clip) { a.clip = await this.loadClip(b, p + audioName) as AudioClip; if (!a.clip) { console.warn(`[SoundManager] 音效加载失败 bundle=${b} path=${p}${audioName}`); return a; } a.clip.addRef(); } return a; } private async loadClip(bundleName, assetPath) { let bundle = assetManager.getBundle(bundleName); if (!bundle) bundle = await this.loadBundle(bundleName); if (!bundle) { console.warn(`[SoundManager] Bundle 不存在或加载失败 bundle=${bundleName} path=${assetPath}`); return null; } let res = await this.loadRes(bundle, assetPath); return res; } private async loadBundle(bundleName) { return new Promise((resolve, reject) => { assetManager.loadBundle(bundleName, (err, bundle) => { if (err) { console.warn(`[SoundManager] 加载 Bundle 失败 bundle=${bundleName}`, String(err)); resolve(null); } else { resolve(bundle); } }) }) } private async loadRes(bundle: AssetManager.Bundle, assetPath) { return new Promise((resolve, reject) => { if (!bundle) { console.warn(`[SoundManager] loadRes bundle 为空 path=${assetPath}`); resolve(null); return; } bundle.load(assetPath, AudioClip, (err, asset) => { if (err) { console.warn(`[SoundManager] 加载 AudioClip 失败 bundle=${bundle.name} path=${assetPath}`, String(err)); resolve(null); } else { //asset.addRef(); resolve(asset); } }) }) } } class AudioRecord { bundle: string = ``; path: string = ``; name: string = ``; key: string = ``; clip: AudioClip = null; isRelease: boolean = true; isPlaying: boolean = false; }