MatchMaster/assets/Script/module/Music/AudioManager.ts
2026-09-01 18:47:54 +08:00

1217 lines
42 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.

const { ccclass, property } = cc._decorator;
@ccclass
export default class AudioManager extends cc.Component {
static _instance: any;
private static readonly CACHE_STATE_KEY = 'audio_cache_state_v1';
private static readonly DEFAULT_AUDIO_VERSION = '1.0.1';
// 远程音频 CDN 基础地址
// public CDN_AUDIO_BASE: string = "https://pay.nika4games.com/remote/music/";
public CDN_AUDIO_BASE: string = "https://cdn.pay.nika4games.com/remote/music/";
// 本地缓存的 AudioClip(按需懒加载)
private audioCache: { [key: string]: cc.AudioClip } = {};
// 合并同一音频的并发加载请求,避免预加载和场景播放重复下载。
private audioLoadingCallbacks: {
[key: string]: Array<(clip: cc.AudioClip) => void>
} = {};
// 远程 URL 映射表
private audioUrlMap: { [key: string]: string } = {};
// 本地持久化缓存目录(微信小游戏)
private localCacheDir: string = '';
private audioVersion: string = '';
private previousAudioVersion: string = '';
private audioVersionConfirmed: boolean = false;
private audioVersionReady: Promise<void> = null;
/**
* 获取本地缓存目录
* 微信小游戏使用 USER_DATA_PATH 下的 audio_cache 子目录
*/
getLocalCacheDir(version?: string): string {
if (!this.localCacheDir) {
// @ts-ignore
if (typeof wx !== 'undefined' && wx.env && wx.env.USER_DATA_PATH) {
// @ts-ignore
this.localCacheDir = wx.env.USER_DATA_PATH + '/audio_cache/';
}
}
if (!this.localCacheDir) return '';
const selectedVersion = version === undefined ? this.audioVersion : version;
return selectedVersion ? this.localCacheDir + selectedVersion + '/' : this.localCacheDir;
}
/**
* 获取本地缓存文件路径
*/
getLocalCachePath(name: string, version?: string): string {
const dir = this.getLocalCacheDir(version);
return dir ? dir + name + '.mp3' : '';
}
// 背景音乐
@property(cc.AudioClip)
main_bgm: cc.AudioClip = null;
@property(cc.AudioClip)
xiaochu: cc.AudioClip = null;
@property(cc.AudioClip)
hit: cc.AudioClip = null;
@property(cc.AudioClip)
down: cc.AudioClip = null;
@property(cc.AudioClip)
win: cc.AudioClip = null;
@property(cc.AudioClip)
anniu_Big: cc.AudioClip = null;
@property(cc.AudioClip)
anniu_little: cc.AudioClip = null;
@property(cc.AudioClip)
tanchuang: cc.AudioClip = null;
@property(cc.AudioClip)
zhuan1: cc.AudioClip = null;
@property(cc.AudioClip)
zhuan2: cc.AudioClip = null;
@property(cc.AudioClip)
adhesive: cc.AudioClip = null;
@property(cc.AudioClip)
freezeBlock: cc.AudioClip = null;
@property(cc.AudioClip)
freezeDoor: cc.AudioClip = null;
@property(cc.AudioClip)
hammer: cc.AudioClip = null;
@property(cc.AudioClip)
lockBlock1: cc.AudioClip = null;
@property(cc.AudioClip)
lockBlock2: cc.AudioClip = null;
@property(cc.AudioClip)
lockDoor: cc.AudioClip = null;
@property(cc.AudioClip)
magic1: cc.AudioClip = null;
@property(cc.AudioClip)
magic2: cc.AudioClip = null;
@property(cc.AudioClip)
simpleColor: cc.AudioClip = null;
@property(cc.AudioClip)
stacking: cc.AudioClip = null;
@property(cc.AudioClip)
starBlock: cc.AudioClip = null;
@property(cc.AudioClip)
timePause1: cc.AudioClip = null;
@property(cc.AudioClip)
timePause2: cc.AudioClip = null;
@property(cc.AudioClip)
hit1: cc.AudioClip = null;
@property(cc.AudioClip)
hit2: cc.AudioClip = null;
@property(cc.AudioClip)
hit3: cc.AudioClip = null;
@property(cc.AudioClip)
hit4: cc.AudioClip = null;
@property(cc.AudioClip)
hit5: cc.AudioClip = null;
mAudioMap: { [name: string]: number };
bgMusicVolume: number;
effectMusicVolume: number;
mMusicSwitch: number;
mEffectSwitch: number;
brickSound: any;
reward: boolean;
finish: boolean;
rewardCount: number;
mMusicKey: any;
private musicRequestId: number = 0;
private shouldRestartMusicOnShow: boolean = false;
private restartMusicTimer: number = null;
private lowMemoryAudioMode: boolean = false;
onLoad() {
if (AudioManager._instance == null) {
AudioManager._instance = this;
cc.fx.AudioManager = AudioManager;
cc.game.addPersistRootNode(this.node);
}
else {
return;
}
this.reward = false;
this.finish = false;
this.rewardCount = 0;
this.ctor();
this.registerAndroidWechatLifecycle();
this.registerMemoryWarning();
this.lowMemoryAudioMode = !!(cc.fx.GameTool
&& cc.fx.GameTool.isLowMemoryMode
&& cc.fx.GameTool.isLowMemoryMode());
this.initializeAudioVersion().then(() => this.preload());
}
onDestroy() {
this.unregisterAndroidWechatLifecycle();
this.unregisterMemoryWarning();
}
ctor() {
this.mAudioMap = {};
/**
* 默认音量大小
* @type {number}
*/
this.bgMusicVolume = 0.1;
this.effectMusicVolume = 1;
this.mMusicSwitch = 1;
this.mEffectSwitch = 1;
// 初始化远程音频 URL 映射
this.initAudioUrlMap();
}
/**
* 初始化远程音频 URL 映射表
* 将所有音频文件路径映射到 CDN 完整 URL
*/
initAudioUrlMap() {
const base = this.CDN_AUDIO_BASE;
// 音效列表
const names = [
'main_bgm', 'homeScene', 'gameScene',
'xiaochu', 'hit', 'down', 'win',
'anniu_Big', 'anniu_little', 'tanchuang', 'zhuan1', 'zhuan2',
'adhesive', 'freezeBlock', 'freezeDoor', 'hammer',
'lockBlock1', 'lockBlock2', 'lockDoor',
'magic1', 'magic2', 'simpleColor', 'stacking',
'starBlock', 'timePause1', 'timePause2',
'hit1', 'hit2', 'hit3', 'hit4', 'hit5'
];
names.forEach((name) => {
this.audioUrlMap[name] = base + name + '.mp3';
});
}
private initializeAudioVersion(): Promise<void> {
if (this.audioVersionReady) return this.audioVersionReady;
this.audioVersionReady = this.selectAudioVersion().then((version) => {
const previous = this.readAudioCacheVersion();
this.audioVersion = version;
this.previousAudioVersion = version === previous ? '' : previous;
});
return this.audioVersionReady;
}
/** 一次启动只选择一个音频版本,切换场景时不重新检查。 */
private selectAudioVersion(): Promise<string> {
const fallback = this.readAudioCacheVersion() || AudioManager.DEFAULT_AUDIO_VERSION;
const url = this.CDN_AUDIO_BASE + 'version.json?_t=' + Date.now();
// @ts-ignore
const sdk = typeof wx !== 'undefined' && wx.request ? wx
// @ts-ignore
: typeof tt !== 'undefined' && tt.request ? tt : null;
if (!sdk) return Promise.resolve(fallback);
return new Promise<string>((resolve) => {
let finished = false;
const finish = (version?: string) => {
if (finished) return;
finished = true;
clearTimeout(timer);
resolve(version || fallback);
};
const timer = setTimeout(() => finish(), 5000);
try {
sdk.request({
url: url,
method: 'GET',
timeout: 5000,
header: { 'Cache-Control': 'no-cache' },
success: (response: any) => {
if (response.statusCode !== 200) {
finish();
return;
}
let data = response.data;
try {
if (typeof data === 'string') data = JSON.parse(data);
} catch (error) {
finish();
return;
}
if (data && this.isValidAudioVersion(data.version)) {
this.audioVersionConfirmed = true;
finish(data.version);
} else {
finish();
}
},
fail: () => finish()
});
} catch (error) {
finish();
}
});
}
private isValidAudioVersion(value: any): boolean {
return typeof value === 'string' && value.length <= 32 && /^\d+\.\d+\.\d+$/.test(value);
}
private readAudioCacheVersion(): string {
try {
const version = cc.sys.localStorage.getItem(AudioManager.CACHE_STATE_KEY);
return this.isValidAudioVersion(version) ? version : '';
} catch (error) {
return '';
}
}
private writeAudioCacheVersion(): void {
try {
cc.sys.localStorage.setItem(AudioManager.CACHE_STATE_KEY, this.audioVersion);
} catch (error) {
// localStorage 不可用时仍可按本次选中的版本运行。
}
}
private isAndroidWechatGame(): boolean {
return cc.sys.platform === cc.sys.WECHAT_GAME
&& cc.sys.os === cc.sys.OS_ANDROID;
}
private logLifecycleAudioError(stage: string, error: any) {
try {
console.error('AudioManager 前后台音乐处理失败:', stage, error);
} catch (e) {
// 日志本身也不能影响前后台事件链。
}
}
private registerAndroidWechatLifecycle() {
try {
if (!this.isAndroidWechatGame()) {
return;
}
} catch (error) {
this.logLifecycleAudioError('注册监听前的平台判断', error);
return;
}
try {
cc.game.on(cc.game.EVENT_HIDE, this.onHide, this);
} catch (error) {
this.logLifecycleAudioError('注册 EVENT_HIDE 监听', error);
}
try {
cc.game.on(cc.game.EVENT_SHOW, this.onShow, this);
} catch (error) {
this.logLifecycleAudioError('注册 EVENT_SHOW 监听', error);
}
}
private unregisterAndroidWechatLifecycle() {
try {
cc.game.off(cc.game.EVENT_HIDE, this.onHide, this);
} catch (error) {
this.logLifecycleAudioError('移除 EVENT_HIDE 监听', error);
}
try {
cc.game.off(cc.game.EVENT_SHOW, this.onShow, this);
} catch (error) {
this.logLifecycleAudioError('移除 EVENT_SHOW 监听', error);
}
if (this.restartMusicTimer != null) {
try {
clearTimeout(this.restartMusicTimer);
} catch (error) {
this.logLifecycleAudioError('清理恢复定时器', error);
}
this.restartMusicTimer = null;
}
}
private isBackgroundMusicName(name: string): boolean {
return name === 'main_bgm'
|| name === 'homeScene'
|| name === 'gameScene';
}
private isBackgroundMusicUsedByCurrentUI(name: string): boolean {
if (!this.isBackgroundMusicName(name)) {
return true;
}
return name === 'homeScene' || name === 'gameScene';
}
private isAudioEnabled(name: string): boolean {
const gameInfo = cc.fx.GameConfig && cc.fx.GameConfig.GM_INFO;
if (this.isBackgroundMusicName(name)) {
return this.isBackgroundMusicUsedByCurrentUI(name)
&& !this.lowMemoryAudioMode
&& !!(gameInfo && gameInfo.musicOpen);
}
return !!(gameInfo && gameInfo.effectOpen);
}
private cacheLoadedAudio(name: string, clip: cc.AudioClip): cc.AudioClip {
if (!clip) {
return null;
}
if (!this.isAudioEnabled(name)) {
this.releaseAudioClip(clip);
return null;
}
this.audioCache[name] = clip;
return clip;
}
private releaseAudioClip(clip: cc.AudioClip) {
if (!clip) {
return;
}
try {
cc.audioEngine.uncache(clip);
} catch (error) {
cc.warn('Uncache audio failed:', clip.name, error);
}
try {
cc.assetManager.releaseAsset(clip);
} catch (error) {
cc.warn('Release audio asset failed:', clip.name, error);
}
}
private releaseCachedAudio(name: string) {
const clip = this.audioCache[name];
delete this.audioCache[name];
if (this.mAudioMap) {
delete this.mAudioMap[name];
if (clip && clip.name) {
delete this.mAudioMap[clip.name];
}
}
this.releaseAudioClip(clip);
}
private registerMemoryWarning() {
if (!cc.fx.GameTool || !cc.fx.GameTool.MEMORY_WARNING_EVENT) {
return;
}
cc.game.on(cc.fx.GameTool.MEMORY_WARNING_EVENT, this.onMemoryWarning, this);
}
private unregisterMemoryWarning() {
if (!cc.fx.GameTool || !cc.fx.GameTool.MEMORY_WARNING_EVENT) {
return;
}
cc.game.off(cc.fx.GameTool.MEMORY_WARNING_EVENT, this.onMemoryWarning, this);
}
private onMemoryWarning() {
this.lowMemoryAudioMode = true;
this.releaseBackgroundMusicFromMemory();
}
public releaseBackgroundMusicFromMemory() {
this.shouldRestartMusicOnShow = false;
if (this.restartMusicTimer != null) {
clearTimeout(this.restartMusicTimer);
this.restartMusicTimer = null;
}
this.stopMusic();
this.releaseCachedAudio('homeScene');
this.releaseCachedAudio('gameScene');
console.log('Background music released from memory');
}
public releaseEffectsFromMemory() {
try {
cc.audioEngine.stopAllEffects();
} catch (error) {
cc.warn('Stop all effects failed:', error);
}
Object.keys(this.audioCache).forEach((name) => {
if (!this.isBackgroundMusicName(name)) {
this.releaseCachedAudio(name);
}
});
console.log('Sound effects released from memory');
}
private getCurrentSceneName(sceneName?: string): string {
if (sceneName) {
return sceneName;
}
const scene = cc.director.getScene && cc.director.getScene();
if (scene && scene.name) {
return scene.name;
}
const gameInfo = cc.fx.GameConfig && cc.fx.GameConfig.GM_INFO;
return gameInfo && gameInfo.sceneValue ? gameInfo.sceneValue : '';
}
public getSceneBackgroundMusicName(sceneName?: string): string {
const currentSceneName = this.getCurrentSceneName(sceneName);
if (currentSceneName === 'HomeScene') {
return 'homeScene';
}
if (currentSceneName === 'GameScene') {
return 'gameScene';
}
return '';
}
/**
* 根据名称获取远程音频 URL
*/
getRemoteUrl(name: string): string {
if (name == null || name == "null") {
return "";
}
const url = this.audioUrlMap[name] || (this.CDN_AUDIO_BASE + name + '.mp3');
return url + '?v=' + encodeURIComponent(this.audioVersion || AudioManager.DEFAULT_AUDIO_VERSION);
}
/**
* 加载远程音频
* 优先从缓存获取,没有则从 CDN 下载
*/
loadRemoteAudio(name: string, callback?: (clip: cc.AudioClip) => void) {
this.initializeAudioVersion().then(() => this.loadRemoteAudioReady(name, callback));
}
private loadRemoteAudioReady(name: string, callback?: (clip: cc.AudioClip) => void) {
if (!this.isBackgroundMusicUsedByCurrentUI(name)) {
console.warn('方案 2 不使用该背景音乐:', name);
callback && callback(null);
return;
}
if (!this.isAudioEnabled(name)) {
callback && callback(null);
return;
}
// 1. 已有缓存直接返回
if (this.audioCache[name]) {
callback && callback(this.audioCache[name]);
return;
}
// 已有相同音频正在加载时只追加回调,不重复请求 CDN。
if (this.audioLoadingCallbacks[name]) {
callback && this.audioLoadingCallbacks[name].push(callback);
return;
}
this.audioLoadingCallbacks[name] = [];
callback && this.audioLoadingCallbacks[name].push(callback);
const finishLoading = (clip: cc.AudioClip) => {
const retainedClip = this.cacheLoadedAudio(name, clip);
const callbacks = this.audioLoadingCallbacks[name] || [];
delete this.audioLoadingCallbacks[name];
for (let i = 0; i < callbacks.length; i++) {
callbacks[i](retainedClip);
}
};
// 2. 从当前版本的本地缓存或 CDN 加载
const url = this.getRemoteUrl(name);
// console.log('远程加载音频:', name, url);
if (url == null || url == "") {
finishLoading(null);
return;
}
// 微信小游戏:先查本地缓存,没有再下载
if (cc.sys.platform === cc.sys.WECHAT_GAME) {
// @ts-ignore
if (typeof wx !== 'undefined' && wx.downloadFile) {
this.loadRemoteAudioWithCache(name, url, finishLoading);
return;
}
}
// 3. 其他平台直接 loadRemote
cc.assetManager.loadRemote(url, (err, audioClip: cc.AudioClip) => {
if (err) {
console.error('远程音频加载失败:', name, err);
this.loadPackagedAudioFallback(name, finishLoading);
return;
}
finishLoading(audioClip);
});
}
/**
* 微信小游戏:优先从本地缓存加载音频,没有则下载并保存到本地
* 缓存路径: wx.env.USER_DATA_PATH/audio_cache/<version>/<name>.mp3
* 微信本地缓存总上限 50MB,超出 saveFile 会失败但不影响本次播放
*/
loadRemoteAudioWithCache(name: string, url: string, callback?: (clip: cc.AudioClip) => void) {
const localPath = this.getLocalCachePath(name);
if (!localPath) {
// 拿不到本地路径,回退到直接下载
this.downloadAndCacheAudioWithFallback(name, url, '', callback);
return;
}
// @ts-ignore
const fs = wx.getFileSystemManager();
// 确保缓存目录存在(递归创建)
try {
fs.mkdirSync(this.getLocalCacheDir(), true);
} catch (e) {
// 目录已存在或其他错误,忽略
}
// 1. 检查本地缓存文件是否存在
fs.access({
path: localPath,
success: () => {
// 本地缓存命中,直接从本地路径加载
// console.log('____________音频本地缓存命中:', name, localPath);
cc.assetManager.loadRemote(localPath, (err, audioClip: cc.AudioClip) => {
if (err) {
//console.log('____________本地缓存加载失败,重新下载:', name, err);
// 加载失败,删除坏文件并重新下载
try { fs.unlinkSync(localPath); } catch (e) { }
this.downloadAndCacheAudioWithFallback(name, url, localPath, callback);
return;
}
//console.log('____________本地缓存加载成功:', name, this.audioCache);
callback && callback(audioClip);
});
},
fail: () => {
// 本地缓存未命中,从 CDN 下载
//console.log('____________本地缓存未命中,下载音频:', name, url);
this.downloadAndCacheAudioWithFallback(name, url, localPath, callback);
}
});
}
private downloadAndCacheAudioWithFallback(
name: string,
url: string,
localPath: string,
callback?: (clip: cc.AudioClip) => void
) {
this.downloadAndCacheAudio(name, url, localPath, (clip) => {
if (clip) {
callback && callback(clip);
return;
}
this.loadPreviousAudioFallback(name, callback);
});
}
/** 新版本下载失败时使用上个版本或升级前的旧缓存。 */
private loadPreviousAudioFallback(name: string, callback?: (clip: cc.AudioClip) => void) {
// @ts-ignore
if (typeof wx === 'undefined' || !wx.getFileSystemManager) {
this.loadPackagedAudioFallback(name, callback);
return;
}
// @ts-ignore
const fs = wx.getFileSystemManager();
const paths: string[] = [];
if (this.previousAudioVersion) {
paths.push(this.getLocalCachePath(name, this.previousAudioVersion));
}
// 兼容升级前的 USER_DATA_PATH/audio_cache/<name>.mp3。
paths.push(this.getLocalCachePath(name, ''));
const tryNext = (index: number) => {
if (index >= paths.length) {
this.loadPackagedAudioFallback(name, callback);
return;
}
const path = paths[index];
if (!path) {
tryNext(index + 1);
return;
}
fs.access({
path: path,
success: () => {
cc.assetManager.loadRemote(path, (err, audioClip: cc.AudioClip) => {
if (err || !audioClip) {
try { fs.unlinkSync(path); } catch (error) { }
tryNext(index + 1);
return;
}
callback && callback(audioClip);
});
},
fail: () => tryNext(index + 1)
});
};
tryNext(0);
}
private loadPackagedAudioFallback(name: string, callback?: (clip: cc.AudioClip) => void) {
const clip = this[name] instanceof cc.AudioClip ? this[name] : null;
callback && callback(clip);
}
/**
* 下载音频,保存到本地后再从本地路径加载(避免 loadRemote 与 saveFile 竞争)
* 关键:wx.saveFile 是移动 temp 文件,而 cc.assetManager.loadRemote 是异步读,
* 如果先 loadRemote(tempPath) 再 saveFile,引擎真正读 temp 时文件已被移走 → 500。
* 正确顺序:saveFile 落盘 → loadRemote(本地路径)
* @param localPath 本地保存路径,为空时只下载不缓存
*/
downloadAndCacheAudio(name: string, url: string, localPath: string, callback?: (clip: cc.AudioClip) => void) {
// @ts-ignore
const fs = wx.getFileSystemManager();
// @ts-ignore
wx.downloadFile({
url: url,
success: (res: any) => {
if (res.statusCode !== 200) {
//console.error('____________下载音频失败:', name, res.statusCode);
callback && callback(null);
return;
}
const tempFilePath = res.tempFilePath;
if (!localPath) {
// 不需要缓存,直接从 temp 加载
cc.assetManager.loadRemote(tempFilePath, (err, audioClip: cc.AudioClip) => {
if (err) {
//console.error('____________远程音频加载失败:', name, err);
callback && callback(null);
return;
}
callback && callback(audioClip);
});
return;
}
// 先 saveFile 落盘到本地缓存,再从稳定的本地路径 loadRemote
fs.saveFile({
tempFilePath: tempFilePath,
filePath: localPath,
success: (saveRes: any) => {
const savedPath = saveRes.savedFilePath || localPath;
//console.log('____________音频已缓存到本地:', name, savedPath);
cc.assetManager.loadRemote(savedPath, (err, audioClip: cc.AudioClip) => {
if (err) {
console.error('____________本地缓存加载失败:', name, err);
try { fs.unlinkSync(savedPath); } catch (e) { }
callback && callback(null);
return;
}
//console.log('____________远程音频加载成功:', name);
callback && callback(audioClip);
});
},
fail: (saveErr: any) => {
// saveFile 失败(50MB 超出 / temp 已被清理等),回退到 temp 直接加载
//console.log('____________saveFile 失败,回退到 temp 直接加载:', name, saveErr);
cc.assetManager.loadRemote(tempFilePath, (err, audioClip: cc.AudioClip) => {
if (err) {
//console.error('____________远程音频加载失败:', name, err);
callback && callback(null);
return;
}
callback && callback(audioClip);
});
}
});
},
fail: (err: any) => {
//console.error('____________downloadFile 失败:', name, err);
callback && callback(null);
}
});
}
/**
* 清除所有音频本地缓存(调试/换包用)
*/
clearLocalAudioCache(onComplete?: () => void) {
if (!(cc.sys.platform === cc.sys.WECHAT_GAME)) {
onComplete && onComplete();
return;
}
// @ts-ignore
const fs = wx.getFileSystemManager();
const dir = this.getLocalCacheDir('');
if (!dir) {
onComplete && onComplete();
return;
}
try {
fs.rmdirSync(dir, true);
} catch (error) {
// 目录不存在,视为清理完成。
}
onComplete && onComplete();
}
private finalizeAudioCacheVersion(names: string[]): void {
if (!(cc.sys.platform === cc.sys.WECHAT_GAME)) {
this.previousAudioVersion = '';
this.writeAudioCacheVersion();
return;
}
// 只有全部音频都已稳定落盘,才删除上个版本和升级前的旧缓存。
if (!names.every((name) => this.isAudioCacheFileValid(this.getLocalCachePath(name)))) return;
this.previousAudioVersion = '';
this.writeAudioCacheVersion();
if (this.audioVersionConfirmed) this.clearObsoleteAudioCaches();
}
private isAudioCacheFileValid(path: string): boolean {
if (!path) return false;
try {
// @ts-ignore
const stat = wx.getFileSystemManager().statSync(path);
return stat.isFile() && stat.size > 0;
} catch (error) {
return false;
}
}
private clearObsoleteAudioCaches(): void {
// @ts-ignore
const fs = wx.getFileSystemManager();
const root = this.getLocalCacheDir('');
if (!root) return;
try {
const files: string[] = fs.readdirSync(root) || [];
files.forEach((file) => {
if (file === this.audioVersion) return;
const path = root + file;
try {
const stat = fs.statSync(path);
if (stat.isDirectory()) fs.rmdirSync(path, true);
else fs.unlinkSync(path);
} catch (error) { }
});
} catch (error) {
// 清理失败不影响当前版本播放。
}
}
play(audioSource: cc.AudioClip, loop: boolean, callback: Function, isBgMusic: boolean) {
// if (isBgMusic && !this.mMusicSwitch) return;
// if (!isBgMusic && !this.mEffectSwitch) return;
const shouldPlayAsBackgroundMusic = isBgMusic === true;
var volume = shouldPlayAsBackgroundMusic ? this.bgMusicVolume : this.effectMusicVolume;
// if (cc.sys.isBrowser) {
// if(audioSource == this.brickSound){
// volume = 0.1;
// }
volume = 1;
cc.audioEngine.setEffectsVolume(1);
cc.audioEngine.setMusicVolume(1);
if (shouldPlayAsBackgroundMusic) {
const musicContext = cc.audioEngine.playMusic(audioSource, loop);
this.mAudioMap[audioSource.name] = musicContext;
return audioSource;
}
if (audioSource.name == "lose" || audioSource.name == "zhuan1" || audioSource.name == "zhuan2") {
cc.audioEngine.setEffectsVolume(0.5);
}
else {
cc.audioEngine.setEffectsVolume(1);
}
var context = cc.audioEngine.playEffect(audioSource, loop);
// if (callback) {
// cc.audioEngine.setFinishCallback(context, function () {
// if (this && callback) {
// callback.call(this);
// }
// }.bind(this));
// }
// cc.wwx.OutPut.log('play audio effect isBrowser: ' + context.src);
this.mAudioMap[audioSource.name] = context;
return audioSource;
// } else {
// return audioSource;
// }
}
save() {
// cc.wwx.Storage.setItem(cc.wwx.Storage.Key_Setting_Music_Volume, this.mMusicSwitch);
// cc.wwx.Storage.setItem(cc.wwx.Storage.Key_Setting_Effect_Volume, this.mEffectSwitch);
}
// static get Instance()
// {
// if (this._instance == null)
// {
// this._instance = new AudioManager();
// }
// return this._instance;
// }
preload() {
if (!(cc.sys.platform === cc.sys.WECHAT_GAME || cc.sys.platform === cc.sys.BYTEDANCE_GAME)) { return; }
if (cc.fx.GameTool.shouldDefaultBackgroundMusicOff()) {
cc.fx.GameConfig.GM_INFO.musicOpen = false;
}
const cacheableSounds = Object.keys(this.audioUrlMap).filter((name) => {
return this.isBackgroundMusicUsedByCurrentUI(name);
});
const allSounds = cacheableSounds.filter((name) => this.isAudioEnabled(name));
console.log('AudioManager 开始按方案 2 预加载音频,共', allSounds.length, '个');
this.preloadAudios(
allSounds,
(loaded, total) => {
// console.log('音频预加载进度:', loaded, '/', total);
},
() => {
console.log('所有音频预加载完成');
this.finalizeAudioCacheVersion(cacheableSounds);
},
// 当前场景对应的背景音乐加载完成后自动播放。
(name, clip) => {
if (this.isBackgroundMusicName(name)
&& name === this.getSceneBackgroundMusicName()
&& clip) {
if (cc.fx.GameConfig.GM_INFO && cc.fx.GameConfig.GM_INFO.musicOpen) {
console.log(name, '加载完成,音乐开关已开启,自动播放');
this.playSceneMusic();
} else {
console.log(name, '加载完成,但音乐开关未开启');
}
}
}
);
}
/**
* 预加载指定的音频列表
* @param names 音频名称数组
* @param onProgress 进度回调 (loaded, total)
* @param onComplete 完成回调
*/
preloadAudios(
names: string[],
onProgress?: (loaded: number, total: number) => void,
onComplete?: () => void,
onItemLoaded?: (name: string, clip: cc.AudioClip) => void
) {
if (!names || names.length === 0) {
onComplete && onComplete();
return;
}
let loaded = 0;
const total = names.length;
const onOneLoaded = (name: string, clip: cc.AudioClip) => {
loaded++;
onItemLoaded && onItemLoaded(name, clip);
onProgress && onProgress(loaded, total);
if (loaded === total) onComplete && onComplete();
};
names.forEach((name) => {
if (this.audioCache[name]) {
onOneLoaded(name, this.audioCache[name]);
return;
}
this.loadRemoteAudio(name, (clip) => {
onOneLoaded(name, clip);
});
});
}
getAudioMusicSwitch() {
return this.mMusicSwitch;
}
getAudioEffectSwitch() {
return this.mEffectSwitch;
}
trunAudioSound(on) {
this.switchMusic(on);
this.switchEffect(on)
}
switchMusic(on) {
if (this.mMusicSwitch != (on ? 1 : 0)) {
this.mMusicSwitch = 1 - this.mMusicSwitch;
// this.save();
}
if (on) {
this.playMusicGame();
}
else {
this.releaseBackgroundMusicFromMemory();
}
}
switchEffect(on) {
if (this.mEffectSwitch != (on ? 1 : 0)) {
this.mEffectSwitch = 1 - this.mEffectSwitch;
// this.save();
}
if (!on) {
this.releaseEffectsFromMemory();
}
}
onHide() {
try {
if (!this.isAndroidWechatGame()) {
return;
}
} catch (error) {
this.logLifecycleAudioError('onHide 平台判断', error);
return;
}
try {
const gameInfo = cc.fx
&& cc.fx.GameConfig
&& cc.fx.GameConfig.GM_INFO;
// Android 微信退后台或锁屏后,原音乐上下文可能无法正常 resume。
// 只要音乐开关开启,回前台时就按当时的场景和 AB 分组重新播放。
this.shouldRestartMusicOnShow = !!(gameInfo && gameInfo.musicOpen);
} catch (error) {
this.shouldRestartMusicOnShow = false;
this.logLifecycleAudioError('onHide 读取音乐开关', error);
}
try {
cc.audioEngine.pauseAll();
} catch (error) {
this.logLifecycleAudioError('onHide 暂停音频', error);
}
try {
this.stopMusic();
} catch (error) {
this.logLifecycleAudioError('onHide 停止背景音乐', error);
}
}
onShow() {
try {
if (!this.isAndroidWechatGame()) {
return;
}
} catch (error) {
this.logLifecycleAudioError('onShow 平台判断', error);
return;
}
try {
cc.audioEngine.resumeAll();
} catch (error) {
this.logLifecycleAudioError('onShow 恢复音频', error);
}
if (!this.shouldRestartMusicOnShow) {
return;
}
this.shouldRestartMusicOnShow = false;
if (this.restartMusicTimer != null) {
try {
clearTimeout(this.restartMusicTimer);
} catch (error) {
this.logLifecycleAudioError('onShow 清理旧定时器', error);
}
this.restartMusicTimer = null;
}
try {
// 等微信小游戏从锁屏/后台恢复音频环境后,再创建新的背景音乐播放。
this.restartMusicTimer = setTimeout(() => {
this.restartMusicTimer = null;
try {
const gameInfo = cc.fx
&& cc.fx.GameConfig
&& cc.fx.GameConfig.GM_INFO;
if (!gameInfo || !gameInfo.musicOpen || this.lowMemoryAudioMode) {
return;
}
const sceneName = this.getCurrentSceneName();
const backgroundMusicName = this.getSceneBackgroundMusicName(sceneName);
if (!backgroundMusicName) {
return;
}
console.log(
'Android 微信小游戏回到前台,重新播放场景背景音乐:',
sceneName,
backgroundMusicName
);
this.stopMusic();
this.playSceneMusic(sceneName);
} catch (error) {
this.logLifecycleAudioError('onShow 重新播放背景音乐', error);
}
}, 300);
} catch (error) {
this.restartMusicTimer = null;
this.logLifecycleAudioError('onShow 创建恢复定时器', error);
}
}
//播放音效
playEffect(name, callback) {
if (!cc.fx.GameConfig.GM_INFO.effectOpen) {
return;
}
if (this.isBackgroundMusicName(name)) {
console.warn('背景音乐必须通过 playSceneMusic/playMusic 播放:', name);
return;
}
// 1. 缓存中有当前启动已加载的 AudioClip
if (this.audioCache[name]) {
// console.log('____________缓存音频加载成功:', name, this.audioCache);
return this.play(this.audioCache[name], false, callback, false);
}
// console.log('____________最终走远程加载:', name);
// 2. 按当前版本从本地缓存或 CDN 加载
this.loadRemoteAudio(name, (clip) => {
if (clip) {
// console.log('____________远程音频加载成功:', name);
this.play(clip, false, callback, false);
} else {
console.log('音效播放失败:', name);
}
});
}
playMusic(key, callback, loop) {
console.log("音频开关________________________________________", cc.fx.GameConfig.GM_INFO.musicOpen);
if (!cc.fx.GameConfig.GM_INFO.musicOpen || this.lowMemoryAudioMode) {
return;
}
loop = typeof loop == 'undefined' || loop ? true : false;
const targetName = (key instanceof cc.AudioClip) ? key.name : key;
if (this.isBackgroundMusicName(targetName)) {
if (!this.isBackgroundMusicUsedByCurrentUI(targetName)) {
console.warn('方案 2 不使用该背景音乐:', targetName);
return;
}
const expectedName = this.getSceneBackgroundMusicName();
if (targetName !== expectedName) {
console.warn('当前场景禁止播放背景音乐:', targetName, '期望:', expectedName);
return;
}
}
// 如果请求播放的就是当前音乐,不重复中断和重启。
if (this.mMusicKey != null && this.mMusicKey === targetName
&& cc.audioEngine.isMusicPlaying && cc.audioEngine.isMusicPlaying()) {
console.log("当前已在播放的音乐,跳过 stop+restart,避免打断");
return;
}
// 场景切换时先停止旧背景音乐;请求编号用于拦截旧场景的异步加载回调。
this.stopMusic();
const requestId = ++this.musicRequestId;
const playClip = (clip: cc.AudioClip, name: string) => {
if (!clip || requestId !== this.musicRequestId) {
return;
}
if (this.isBackgroundMusicName(name)
&& name !== this.getSceneBackgroundMusicName()) {
return;
}
this.play(clip, loop, callback, true);
this.mMusicKey = name;
};
// key 可能是 AudioClip 实例,也可能是字符串名称
if (key instanceof cc.AudioClip) {
playClip(key, targetName);
console.log("开始播放音乐", key.name);
return;
}
// 字符串名称:尝试从缓存或远程加载
const name = key;
console.log("开始播放音乐的名称", name);
if (this.audioCache[name]) {
console.log("从缓存播放音乐", name);
playClip(this.audioCache[name], name);
} else {
this.loadRemoteAudio(name, (clip) => {
console.log("从远程加载音乐", name, clip);
if (clip) {
playClip(clip, name);
}
});
}
}
public playSceneMusic(sceneName?: string) {
const backgroundMusicName = this.getSceneBackgroundMusicName(sceneName);
if (!backgroundMusicName) {
this.stopMusic();
return;
}
this.playMusic(backgroundMusicName, null, true);
}
/**
* 兼容原有调用入口,根据当前场景选择方案 2 背景音乐。
*/
playMusicGame() {
this.playSceneMusic();
}
/**
* 停止背景音乐播放
*/
stopMusic() {
console.log("无论什么原因——————————————暂停了背景音乐播放", this.mMusicKey);
this.musicRequestId++;
this.mMusicKey = null;
cc.audioEngine.stopMusic();
}
/**
* 恢复被暂停的背景音乐播放
*/
resumeMusic() {
// 调用 cc.audioEngine 的 resumeMusic 方法恢复音乐播放
cc.audioEngine.resumeMusic();
}
/*
* 游戏开始音效
*
*/
playGameStart() {
}
/*
* 失败的游戏结束
*/
playGameOver() {
}
/*
* 成功的游戏结束
*/
playGameResultFailed() {
}
/*
* 成功的游戏结束
*/
playGameResultSuccess() {
}
/**
* 报警的音效
*/
/**
* 按钮
*/
playAudioButton() {
// return this.play(this.audioButtonClick, false,null,this.mEffectSwitch);
}
};
// export { AudioManager };