MatchMaster/assets/Script/module/Music/AudioManager.ts
2026-06-17 15:43:40 +08:00

686 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.

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/<name>.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 };