Files
Monopoly/extensions/app/assets/manager/sound/SoundManager.ts
2026-03-30 09:39:59 +08:00

717 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AssetManager, AudioClip, Button, Game, _decorator, game, isValid, sys } from 'cc';
import { IEffectName, IMusicName } from '../../../../../assets/app-builtin/app-admin/executor';
import Core from '../../Core';
import BaseManager from '../../base/BaseManager';
import AudioEngine from './AudioEngine';
const { ccclass } = _decorator;
interface playMusic<T> { name: T, volume?: number, force?: boolean, onPlay?: Function, onError?: Function }
interface playEffect<T> { name: T, volume?: number, loop?: boolean, interval?: number, onPlay?: (audioID: number) => any, onError?: Function, onEnded?: Function }
interface playMusicAsync<T> { name: T, volume?: number, force?: boolean }
interface playEffectAsync<T> { name: T, volume?: number, loop?: boolean, interval?: number, onEnded?: Function }
interface playMusicWidthBundle { name: string, bundle: string, volume?: number, force?: boolean, onPlay?: Function, onError?: Function }
interface playEffectWidthBundle { name: string, bundle: string, volume?: number, loop?: boolean, interval?: number, onPlay?: (audioID: number) => any, onError?: Function, onEnded?: Function }
interface playMusicWidthBundleAsync { name: string, bundle: string, volume?: number, force?: boolean }
interface playEffectWidthBundleAsync { name: string, bundle: string, volume?: number, loop?: boolean, interval?: number, onEnded?: Function }
const storage = {
set(key: string, value: any) {
sys.localStorage.setItem(key, JSON.stringify(value));
},
get(key: string) {
const data = sys.localStorage.getItem(key);
if (data && typeof data === 'string') {
return JSON.parse(data);
}
return undefined;
}
};
/**
* 音乐名字枚举
*/
const MusicName: { [key in IMusicName]: key } = new Proxy({} as any, {
get: function (target, key) {
if (target[key]) return target[key];
target[key] = key;
return key;
}
});
/**
* 音效名字枚举
*/
const EffectName: { [key in IEffectName]: key } = new Proxy({} as any, {
get: function (target, key) {
if (target[key]) return target[key];
target[key] = key;
return key;
}
});
const BundleName = 'app-sound';
@ccclass('SoundManager')
export default class SoundManager<E extends string, M extends string> extends BaseManager {
/**静态设置 */
static setting: {
/**预加载 */
preload?: (IMusicName | IEffectName)[],
/**默认播放的音乐名 */
defaultMusicName?: IMusicName | '',
/**默认音乐的音量 */
defaultMusicVolume?: number,
/**默认按钮的音效名 */
defaultEffectName?: IEffectName | '',
/**默认按钮音效的音量 */
defaultEffectVolume?: number
} = {};
/**音乐名字枚举 */
static MusicName = MusicName;
/**音效名字枚举 */
static EffectName = EffectName;
private musicMuteCacheKey = 'SoundManager:MusicMute';
private effectMuteCacheKey = 'SoundManager:EffectMute';
private musicVolumeScaleCacheKey = 'SoundManager:MusicVolumeScale';
private effectVolumeScaleCacheKey = 'SoundManager:EffectVolumeScale';
private defaultMusicName = '';
private defaultMusicVolume = 0.2;
private defaultEffectName = '';
private defaultEffectVolume = 1;
private audioCache = {};
private effectInterval: { [key in string]: number } = {};
private playingMusic = { uuid: '', id: -1, name: '', volume: 1, playing: false, paused: false };
protected init(finish: Function) {
const setting = SoundManager.setting;
// 默认音乐
if (setting.defaultMusicName) this.defaultMusicName = setting.defaultMusicName;
if (typeof setting.defaultMusicVolume === 'number') this.defaultMusicVolume = setting.defaultMusicVolume;
// 默认按钮音效
if (setting.defaultEffectName) this.defaultEffectName = setting.defaultEffectName;
if (typeof setting.defaultEffectVolume === 'number') this.defaultEffectVolume = setting.defaultEffectVolume;
if (this.musicMuteCacheKey) {
const musicMute = storage.get(this.musicMuteCacheKey) === true;
AudioEngine.inst.setMusicMute(musicMute);
} else {
this.warn('musicMuteCacheKey不能为空');
}
if (this.effectMuteCacheKey) {
const effectMute = storage.get(this.effectMuteCacheKey) === true;
AudioEngine.inst.setGlobalEffectsMute(effectMute);
} else {
this.warn('effectMuteCacheKey不能为空');
}
if (this.musicVolumeScaleCacheKey) {
const musicVolumeScale = storage.get(this.musicVolumeScaleCacheKey);
if (typeof musicVolumeScale === 'number') AudioEngine.inst.setMusicVolumeScale(musicVolumeScale);
} else {
this.warn('musicVolumeScaleCacheKey不能为空');
}
if (this.effectVolumeScaleCacheKey) {
const effectVolumeScale = storage.get(this.effectVolumeScaleCacheKey);
if (typeof effectVolumeScale === 'number') AudioEngine.inst.setGlobalEffectsVolumeScale(effectVolumeScale);
} else {
this.warn('effectVolumeScaleCacheKey不能为空');
}
super.init(finish);
// 预加载
setting.preload?.forEach((path: string) => {
Core.inst.manager.loader.preload({
bundle: BundleName,
type: AudioClip,
path: path
});
});
}
protected onLoad() {
game.on(Game.EVENT_HIDE, function () {
AudioEngine.inst.pauseAll();
});
game.on(Game.EVENT_SHOW, function () {
AudioEngine.inst.resumeAll();
});
}
/**
* 预加载声音资源
* @param name sound路径
* @param bundle Bundle名默认为app-sound
*/
public preload(name: string, bundle: string, complete?: (item: AssetManager.RequestItem[] | null) => any): void;
public preload(name: (E | M), complete?: (item: AssetManager.RequestItem[] | null) => any): void;
public preload(name: string, ...args: any[]) {
const bundleName = (args.length >= 1
&& (typeof args[0] === 'string')
? (args[0] || BundleName)
: BundleName
) as string;
const complete = (args.length >= 1
&& (args[args.length - 1] instanceof Function)
? args[args.length - 1]
: null
) as (item: AssetManager.RequestItem[] | null) => any;
if (!name) {
this.error('preload', 'fail');
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(null);
});
return;
}
if (name.indexOf('effect') !== 0 && name.indexOf('music') !== 0) {
this.error('preload', 'fail', name);
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(null);
});
return;
}
// 远程加载
Core.inst.manager.loader.preload({
bundle: bundleName,
path: name,
type: AudioClip,
onComplete: complete
});
}
/**
* 加载声音资源
* @param name sound路径
* @param bundle Bundle名默认为app-sound
* @param progress 加载进度回调
* @param complete 加载完成回调
*/
public load(name: string, bundle: string): void;
public load(name: string, bundle: string, complete: (result: AudioClip | null) => any): void;
public load(name: string, bundle: string, progress: (finish: number, total: number, item: AssetManager.RequestItem) => void, complete: (result: AudioClip | null) => any): void;
public load(name: (E | M)): void;
public load(name: (E | M), complete: (result: AudioClip | null) => any): void;
public load(name: (E | M), progress: (finish: number, total: number, item: AssetManager.RequestItem) => void, complete: (result: AudioClip | null) => any): void;
public load(name: string, ...args: any[]): void {
const bundleName = (args.length >= 1
&& (typeof args[0] === 'string')
? (args[0] || BundleName)
: BundleName
) as string;
const progress = (args.length >= 2
&& (args[args.length - 1] instanceof Function)
&& (args[args.length - 2] instanceof Function)
? args[args.length - 2]
: null
) as (finish: number, total: number, item: AssetManager.RequestItem) => void;
const complete = (args.length >= 1
&& (args[args.length - 1] instanceof Function)
? args[args.length - 1]
: null
) as (result: AudioClip | null) => any;
if (!name) {
this.error('load', 'fail');
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(null);
});
return;
}
const soundName = `${bundleName}://${name}`;
// 判断有无缓存
const audio = this.audioCache[soundName];
if (audio) {
complete && setTimeout(() => {
if (!isValid(this)) return;
complete(audio);
});
return;
}
// 远程加载
Core.inst.manager.loader.load({
path: name,
bundle: bundleName,
type: AudioClip,
onProgress: progress,
onComplete: (audioClip) => {
if (!isValid(this)) return;
if (audioClip) {
this.audioCache[soundName] = audioClip;
complete && complete(audioClip);
} else {
complete && complete(null);
}
}
});
}
/**
* 释放声音资源
* @param name 声音路径
* @param bundle Bundle名默认为app-sound
*/
public release(name: string, bundle: string): void;
public release(name: E | M): void;
public release(name: string, bundle?: string) {
const bundleName = bundle || BundleName;
const soundName = `${bundleName}://${name}`;
delete this.audioCache[soundName];
Core.inst.manager.loader.release({ bundle: bundleName, path: name, type: AudioClip });
}
/**
* 播放默认音乐
*/
public playDefaultMusic(onPlay?: Function) {
if (this.defaultMusicName) {
this.playMusic({ name: <M>this.defaultMusicName, volume: this.defaultMusicVolume, onPlay });
} else {
this.warn('defaultMusicName 不存在');
}
}
/**
* 播放默认音效
*/
public playDefaultEffect(onPlay?: (audioID: number) => void) {
if (this.defaultEffectName) {
this.playEffect({ name: <E>this.defaultEffectName, volume: this.defaultEffectVolume, onPlay });
} else {
this.warn('defaultEffectName 不存在');
}
}
/**
* 设置按钮点击播放的音效,优先级高于默认音效
* @param name 音效(如果为空,则使用默认音效)
* @param opts.volume 音量
* @param opts.interval 多少秒内不会重复播放
*/
public setButtonEffect(target: Button, name?: E, opts?: {
volume: number,
interval: number
}) {
if (name) {
const { volume = 1, interval = 0 } = opts || {};
//@ts-ignore
target.node['useDefaultEffect'] = false;
target.node.targetOff(this);
target.node.on(Button.EventType.CLICK, function (this: SoundManager<E, M>) {
this.playEffect({ name, volume, interval });
}, this);
} else {
//@ts-ignore
target.node['useDefaultEffect'] = true;
target.node.targetOff(this);
}
}
/**
* 播放音效
* @param name 音效
* @param bundle Bundle名默认为app-sound
* @param loop 循环播放
* @param volume 音量
* @param interval 多少秒内不会重复播放
*/
public playEffect({ name, volume, loop, interval, onEnded, onPlay, onError }: playEffect<E>): void;
public playEffect({ name, bundle, volume, loop, interval, onEnded, onPlay, onError }: playEffectWidthBundle): void;
public playEffect({ name, bundle, volume = 1, loop = false, interval = 0, onEnded, onPlay, onError }) {
if (!name) {
onError && onError();
return;
}
const bundleName = bundle || BundleName;
const soundName = `${bundleName}://${name}`;
// 静音不允许播放
if (this.isEffectMute) {
onError && onError();
return;
}
// 正在播放中,不允许重复播放
if (this.effectInterval[soundName] && Date.now() < this.effectInterval[soundName]) {
onError && onError();
return;
}
// 加载音乐
this.load(name, bundleName, (audioClip) => {
if (!isValid(this)) {
onError && onError();
return;
}
// 静音不允许播放
if (this.isEffectMute) {
onError && onError();
return;
}
// 正在播放中,不允许重复播放
if (this.effectInterval[soundName] && Date.now() < this.effectInterval[soundName]) {
onError && onError();
return;
}
if (!audioClip) {
this.error(`playEffect ${name} 不存在或加载失败`);
onError && onError();
return;
}
if (interval > 0) {
this.effectInterval[soundName] = Date.now() + interval * 1000;
}
AudioEngine.inst.playEffect(audioClip, volume, loop, onPlay, onEnded);
});
}
/**
* 播放音效
* @param name 音效
* @param bundle Bundle名默认为app-sound
* @param loop 循环播放
* @param volume 音量
* @param interval 多少秒内不会重复播放
* @returns 如果Promise返回值是null(非真),则播放失败
*/
public async playEffectAsync(params: playEffectAsync<E>): Promise<number>;
public async playEffectAsync(params: playEffectWidthBundleAsync): Promise<number>;
public async playEffectAsync(params: any): Promise<number> {
return new Promise((resolve) => {
this.playEffect({
...params,
onPlay: (audioID) => {
resolve(audioID);
},
onError: () => {
resolve(null);
}
});
});
}
/**
* 暂停音效
* @param id
* @returns
*/
public pauseEffect(id: number) {
return AudioEngine.inst.pauseEffect(id);
}
/**
* 暂停所有音效
* @returns
*/
public pauseAllEffects() {
return AudioEngine.inst.pauseAllEffects();
}
/**
* 恢复音效
* @param id
* @returns
*/
public resumeEffect(id: number) {
return AudioEngine.inst.resumeEffect(id);
}
/**
* 恢复所有音效
* @returns
*/
public resumeAllEffects() {
return AudioEngine.inst.resumeAllEffects();
}
/**
* 停止音效
* @param id
* @returns
*/
public stopEffect(id: number) {
return AudioEngine.inst.stopEffect(id);
}
/**
* 停止所有音效
* @returns
*/
public stopAllEffects() {
return AudioEngine.inst.stopAllEffects();
}
/**
* 播放音乐
* @param volume 音量
* @param bundle Bundle名默认为app-sound
* @param force 是否强制重新播放
*/
public playMusic(params: playMusic<M>): void;
public playMusic(params: playMusicWidthBundle): void;
public playMusic({ name, bundle, volume = 1, force = false, onPlay, onError }): void {
if (!name) {
onError && onError();
return;
}
const bundleName = bundle || BundleName;
const soundName = `${bundleName}://${name}`;
// 该音乐正在播放中
if (!force && this.playingMusic.id !== -1 && this.playingMusic.name === soundName) {
AudioEngine.inst.setMusicVolume(volume);
onPlay && onPlay();
return;
}
// 先停止当前音乐
this.stopMusic();
// 播放操作uuid
const uuid = this.createUUID();
this.playingMusic.uuid = uuid;
// 记录要播放音乐的名字
this.playingMusic.name = soundName;
// 记录要播放音乐的音量
this.playingMusic.volume = volume;
// 记录音乐状态
this.playingMusic.playing = true;
this.playingMusic.paused = false;
// 静音
if (this.isMusicMute) {
onPlay && onPlay();
return;
}
// 加载音乐
this.load(name, bundleName, (audioClip) => {
if (!isValid(this)) {
onError && onError();
return;
}
// 不合法
if (this.playingMusic.id !== -1) {
onError && onError();
return;
}
if (this.playingMusic.name !== soundName) {
onError && onError();
return;
}
if (this.playingMusic.uuid !== this.playingMusic.uuid) {
onError && onError();
return;
}
// 不存在
if (!audioClip) {
this.error(`playMusic ${name} 不存在或加载失败`);
onError && onError();
return;
}
// 静音
if (this.isMusicMute) {
onPlay && onPlay();
return;
}
this.playingMusic.id = AudioEngine.inst.playMusic(audioClip, volume, onPlay);
});
}
/**
* 播放音乐
* @param volume 音量
* @param bundle Bundle名默认为app-sound
* @param force 是否强制重新播放
* @returns 如果Promise返回值是false则播放失败
*/
public playMusicAsync(params: playMusicAsync<M>): Promise<boolean>;
public playMusicAsync(params: playMusicWidthBundleAsync): Promise<boolean>;
public playMusicAsync(params: any): Promise<boolean> {
return new Promise((resolve) => {
this.playMusic({
...params,
onPlay: () => {
resolve(true);
},
onError: () => {
resolve(false);
}
});
});
}
/**
* 重新播放音乐
*/
public replayMusic(onPlay?: Function) {
if (!this.playingMusic.playing) return;
if (!this.playingMusic.name) return;
if (this.playingMusic.name.indexOf('://') > 0) {
const [bundle, name] = this.playingMusic.name.split('://');
this.playMusic({
name,
bundle,
volume: this.playingMusic.volume,
force: true,
onPlay
});
} else {
this.playMusic({
name: this.playingMusic.name as any,
volume: this.playingMusic.volume,
force: true,
onPlay
});
}
}
/**
* 暂停音乐
*/
public pauseMusic() {
if (!this.playingMusic.playing) return false;
this.playingMusic.paused = true;
return AudioEngine.inst.pauseMusic();
}
/**
* 恢复音乐
*/
public resumeMusic() {
if (!this.playingMusic.playing) return false;
this.playingMusic.paused = false;
return AudioEngine.inst.resumeMusic();
}
/**
* 停止音乐
*/
public stopMusic() {
this.playingMusic.playing = false;
this.playingMusic.paused = false;
this.playingMusic.volume = 1;
this.playingMusic.name = '';
this.playingMusic.uuid = '';
this.playingMusic.id = -1;
return AudioEngine.inst.stopMusic();
}
/**
* 设置音乐静音
* @param mute 是否静音
* @param isCache 静音状态是否写入缓存(通过localstorage)
*/
public setMusicMute(mute: boolean, isCache = false) {
isCache && storage.set(this.musicMuteCacheKey, mute);
AudioEngine.inst.setMusicMute(mute);
if (!mute && this.playingMusic.name) {
if (this.playingMusic.name.indexOf('://') > 0) {
const [bundle, name] = this.playingMusic.name.split('://');
this.playMusic({
name,
bundle,
volume: this.playingMusic.volume,
});
} else {
this.playMusic({
name: this.playingMusic.name as any,
volume: this.playingMusic.volume
});
}
}
}
/**
* 音乐是否正在播放
*/
get isMusicPlaying() {
return this.playingMusic.playing;
}
/**
* 音乐是否暂停
*/
get isMusicPaused() {
return this.playingMusic.paused;
}
/**
* 音乐是否静音
*/
public get isMusicMute() {
return AudioEngine.inst.getMusicMute();
}
/**
* 设置音效静音
* @param mute 是否静音
* @param isCache 静音状态是否写入缓存(通过localstorage)
*/
public setEffectMute(mute: boolean, isCache = false) {
AudioEngine.inst.setGlobalEffectsMute(mute);
isCache && storage.set(this.effectMuteCacheKey, mute);
}
/**
* 音效是否静音
*/
public get isEffectMute() {
return AudioEngine.inst.getGlobalEffectsMute();
}
/**
* 设置音乐音量倍率
* @param scale
* @param isCache 音量倍率是否写入缓存(通过localstorage)
*/
public setMusicVolumeScale(scale: number, isCache = false) {
AudioEngine.inst.setMusicVolumeScale(scale);
isCache && storage.set(this.musicVolumeScaleCacheKey, scale);
}
/**
* 音乐音量倍率
*/
public get musicVolumeScale() {
return AudioEngine.inst.getMusicVolumeScale();
}
/**
* 设置音效音量倍率
* @param scale
* @param isCache 音量倍率是否写入缓存(通过localstorage)
*/
public setEffectVolumeScale(scale: number, isCache = false) {
AudioEngine.inst.setGlobalEffectsVolumeScale(scale);
isCache && storage.set(this.effectVolumeScaleCacheKey, scale);
}
/**
* 音效音量倍率
*/
public get effectVolumeScale() {
return AudioEngine.inst.getGlobalEffectsVolumeScale();
}
}