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

1217 lines
42 KiB
TypeScript

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 };