You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
425 lines
13 KiB
425 lines
13 KiB
//*********************
|
|
// 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<string, AudioRecord> = new Map<string, AudioRecord>();
|
|
|
|
|
|
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<AssetManager.Bundle>((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<Asset>((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;
|
|
}
|
|
|