const { ccclass, property } = cc._decorator; @ccclass export default class AudioManager extends cc.Component { static _instance: any; // 远程音频 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 } = {}; // 远程 URL 映射表 private audioUrlMap: { [key: string]: string } = {}; // 本地持久化缓存目录(微信小游戏) private localCacheDir: string = ''; /** * 获取本地缓存目录 * 微信小游戏使用 USER_DATA_PATH 下的 audio_cache 子目录 */ getLocalCacheDir(): string { if (this.localCacheDir) return 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/'; } return this.localCacheDir; } /** * 获取本地缓存文件路径 */ getLocalCachePath(name: string): string { return this.getLocalCacheDir() + 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: {}; bgMusicVolume: number; effectMusicVolume: number; mMusicSwitch: number; mEffectSwitch: number; brickSound: any; reward: boolean; finish: boolean; rewardCount: number; mMusicKey: any; 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.preload(); } 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', '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'; }); } /** * 根据名称获取远程音频 URL */ getRemoteUrl(name: string): string { if (name == null || name == "null") { return ""; } return this.audioUrlMap[name] || (this.CDN_AUDIO_BASE + name + '.mp3'); } /** * 加载远程音频 * 优先从缓存获取,没有则从 CDN 下载 */ loadRemoteAudio(name: string, callback?: (clip: cc.AudioClip) => void) { // 1. 已有缓存直接返回 if (this.audioCache[name]) { callback && callback(this.audioCache[name]); return; } // 2. 本地有 @property 注入的 AudioClip,优先使用 if (this[name] instanceof cc.AudioClip) { this.audioCache[name] = this[name]; callback && callback(this[name]); return; } // 3. 从远程 CDN 加载 const url = this.getRemoteUrl(name); // console.log('远程加载音频:', name, url); if (url == null || url == "") { callback && callback(null); return; } // 微信小游戏:先查本地缓存,没有再下载 if (cc.sys.platform === cc.sys.WECHAT_GAME) { // @ts-ignore if (typeof wx !== 'undefined' && wx.downloadFile) { this.loadRemoteAudioWithCache(name, url, callback); return; } } // 4. 其他平台直接 loadRemote cc.assetManager.loadRemote(url, (err, audioClip: cc.AudioClip) => { if (err) { console.error('远程音频加载失败:', name, err); callback && callback(null); return; } this.audioCache[name] = audioClip; callback && callback(audioClip); }); } /** * 微信小游戏:优先从本地缓存加载音频,没有则下载并保存到本地 * 缓存路径: wx.env.USER_DATA_PATH/audio_cache/.mp3 * 微信本地缓存总上限 50MB,超出 saveFile 会失败但不影响本次播放 */ loadRemoteAudioWithCache(name: string, url: string, callback?: (clip: cc.AudioClip) => void) { const localPath = this.getLocalCachePath(name); if (!localPath) { // 拿不到本地路径,回退到直接下载 this.downloadAndCacheAudio(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.downloadAndCacheAudio(name, url, localPath, callback); return; } this.audioCache[name] = audioClip; //console.log('____________本地缓存加载成功:', name, this.audioCache); callback && callback(audioClip); }); }, fail: () => { // 本地缓存未命中,从 CDN 下载 //console.log('____________本地缓存未命中,下载音频:', name, url); this.downloadAndCacheAudio(name, url, localPath, callback); } }); } /** * 下载音频,保存到本地后再从本地路径加载(避免 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; } this.audioCache[name] = audioClip; 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); this.audioCache[name] = audioClip; 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; } this.audioCache[name] = audioClip; 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; } fs.readdir({ dirPath: dir, success: (res: any) => { const files: string[] = res.files || []; let removed = 0; if (files.length === 0) { onComplete && onComplete(); return; } files.forEach((file) => { const filePath = dir + file; fs.unlink({ filePath: filePath, success: () => { removed++; if (removed === files.length) onComplete && onComplete(); }, fail: () => { removed++; if (removed === files.length) onComplete && onComplete(); } }); }); }, fail: () => { onComplete && onComplete(); } }); } play(audioSource, loop, callback, isBgMusic) { // if (isBgMusic && !this.mMusicSwitch) return; // if (!isBgMusic && !this.mEffectSwitch) return; var volume = isBgMusic ? 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 (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] = 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; } // 预加载所有音频 const allSounds = Object.keys(this.audioUrlMap); console.log('AudioManager 开始预加载所有音频,共', allSounds.length, '个'); this.preloadAudios( allSounds, (loaded, total) => { // console.log('音频预加载进度:', loaded, '/', total); }, () => { console.log('所有音频预加载完成'); }, // 单条加载完成回调:main_bgm 特殊处理 (name, clip) => { if (name === 'main_bgm' && clip) { // main_bgm 加载完成后,如果音乐开关是打开的,自动播放 if (cc.fx.GameConfig.GM_INFO && cc.fx.GameConfig.GM_INFO.musicOpen) { console.log('main_bgm 加载完成,音乐开关已开启,自动播放'); this.playMusic('main_bgm', null, true); } else { console.log('main_bgm 加载完成,但音乐开关未开启'); } } } ); } /** * 预加载指定的音频列表 * @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[name] instanceof cc.AudioClip || this.audioCache[name]) { onOneLoaded(name, this[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.stopMusic(); } } switchEffect(on) { if (this.mEffectSwitch != (on ? 1 : 0)) { this.mEffectSwitch = 1 - this.mEffectSwitch; // this.save(); } } onHide() { cc.audioEngine.pauseAll(); } onShow() { cc.audioEngine.resumeAll(); } //播放音效 playEffect(name, callback) { if (!cc.fx.GameConfig.GM_INFO.effectOpen) { return; } // 1. 本地有 AudioClip 直接播放 if (this[name] instanceof cc.AudioClip) { // console.log('____________本地有音频成功:', name); return this.play(this[name], false, callback, this.mEffectSwitch); } // 2. 缓存中有已加载的 AudioClip if (this.audioCache[name]) { // console.log('____________缓存音频加载成功:', name, this.audioCache); return this.play(this.audioCache[name], false, callback, this.mEffectSwitch); } // console.log('____________最终走远程加载:', name); // 3. 远程加载 this.loadRemoteAudio(name, (clip) => { if (clip) { // console.log('____________远程音频加载成功:', name); this.play(clip, false, callback, this.mEffectSwitch); } else { console.log('音效播放失败:', name); } }); } playMusic(key, callback, loop) { console.log("音频开关________________________________________", cc.fx.GameConfig.GM_INFO.musicOpen); if (!cc.fx.GameConfig.GM_INFO.musicOpen) { return; } loop = typeof loop == 'undefined' || loop ? true : false; // 如果请求播放的就是当前已在播放的音乐,跳过 stop+restart,避免打断 const targetName = (key instanceof cc.AudioClip) ? key.name : key; if (this.mMusicKey != null && this.mMusicKey === targetName && cc.audioEngine.isMusicPlaying && cc.audioEngine.isMusicPlaying()) { console.log("当前已在播放的音乐,跳过 stop+restart,避免打断"); return; } // this.stopMusic(); // key 可能是 AudioClip 实例,也可能是字符串名称 if (key instanceof cc.AudioClip) { this.mMusicKey = this.play(key, loop, callback, true); console.log("开始播放音乐", key.name); return; } // 字符串名称:尝试从缓存或远程加载 const name = key; console.log("开始播放音乐的名称", name); const playFromCache = () => { console.log("从缓存播放音乐", name); if (this.audioCache[name]) { this.mMusicKey = this.play(this.audioCache[name], loop, callback, true); } else { console.log('背景音乐播放失败:', name); } }; if (this.audioCache[name]) { console.log("从缓存播放音乐", name); playFromCache(); } else { this.loadRemoteAudio(name, (clip) => { console.log("从远程加载音乐", name, clip); if (clip) { this.mMusicKey = this.play(clip, loop, callback, true); } }); } } /** * 游戏背景音乐 * 仅当音乐开关开启、且当前没有在播放 main_bgm 时才真正播放 * 避免从 GameScene 退回 HomeScene 时被打断重启 */ playMusicGame() { if (!cc.fx.GameConfig.GM_INFO.musicOpen) { return; } // 已经在播背景音乐(musicId 不为 -1)就不再打断 if (this.mMusicKey != null && this.mMusicKey !== undefined && cc.audioEngine.isMusicPlaying && cc.audioEngine.isMusicPlaying()) { return; } console.log("开始播放音乐"); this.playMusic('main_bgm', null, true); } /** * 停止背景音乐播放 */ stopMusic() { console.log("无论什么原因——————————————暂停了背景音乐播放", this.mMusicKey); // cc.wwx.OutPut.log('stopMusic audio effect wx: ' + this.mMusicKey); cc.audioEngine.stopAll(); } /** * 恢复被暂停的背景音乐播放 */ resumeMusic() { // 调用 cc.audioEngine 的 resumeMusic 方法恢复音乐播放 cc.audioEngine.resumeMusic(); } /* * 游戏开始音效 * */ playGameStart() { } /* * 失败的游戏结束 */ playGameOver() { } /* * 成功的游戏结束 */ playGameResultFailed() { } /* * 成功的游戏结束 */ playGameResultSuccess() { } /** * 报警的音效 */ /** * 按钮 */ playAudioButton() { // return this.play(this.audioButtonClick, false,null,this.mEffectSwitch); } }; // export { AudioManager };