在介绍了 AI 音频生成工具之后,关键是如何在 Cocos Creator 中高效管理这些音频。下面给出一个完整的音频管理架构:
5.1 音频管理器(AudioManager)
import { _decorator, Component, Node, AudioSource, resources, AudioClip, AudioClipSource, instantiate, Enum, sys, game } from'cc';const { ccclass, property } = _decorator;/*** 游戏音频类型枚举
* 区分BGM和SFX,支持独立音量控制
*/
exportenumAudioType { BGM = 0, // 背景音乐 SFX = 1, // 音效 VOICE = 2, // 语音(NPC对话等) AMBIENT = 3, // 环境音(雨声、风声等)}export interfaceAudioConfig { path: string; type: AudioType; volume?: number; loop?: boolean; fadeIn?: number; fadeOut?: number; }/*** AI生成音频的元数据
* 记录音频的来源信息,便于管理
*/
export interfaceAIAudioMeta { id: string; source: 'elevenlabs' | 'suno' | 'thinksound' | 'manual'; prompt: string; generatedAt: number; duration: number; }/*** 游戏音频管理器
* 统一管理 BGM、SFX、语音、环境音的播放
* 支持 AI 生成音频的缓存和程序化播放
*/
@ccclass('AudioManager')exportclassAudioManagerextendsComponent { private static _instance: AudioManager = null; public staticget instance(): AudioManager { return this._instance; } // 四类音频的独立 AudioSource 节点 @property(Node) bgmNode: Node = null; @property(Node) sfxNode: Node = null; @property(Node) voiceNode: Node = null; @property(Node) ambientNode: Node = null; private _bgmSource: AudioSource = null; private _sfxSource: AudioSource = null; private _voiceSource: AudioSource = null; private _ambientSource: AudioSource = null; // 音频缓存池private _clipCache: Map<string, AudioClip> = newMap(); private _aiMetaMap: Map<string, AIAudioMeta> = newMap(); // Fade 动画控制private _fadeTweens: Map<string, any> = newMap(); onLoad() { AudioManager._instance = this; // 获取各类型的 AudioSourcethis._bgmSource = this.bgmNode.getComponent(AudioSource); this._sfxSource = this.sfxNode.getComponent(AudioSource); this._voiceSource = this.voiceNode.getComponent(AudioSource); this._ambientSource = this.ambientNode.getComponent(AudioSource); } // ========== BGM 管理 ==========/** * 播放BGM(带淡入效果)
* @param path 音频资源路径
* @param fadeIn 淡入时长(秒),默认0.5秒
*/
asyncplayBGM(path: string, fadeIn: number = 0.5): Promise<void> { const clip = await this._loadClip(path); if (!clip) return; const source = this._bgmSource; // 淡出当前BGMif (source.playing) { await this._fadeOut(source, 0.3); } // 设置新BGM source.clip = clip; source.loop = true; source.volume = 0; source.play(); // 淡入新BGMif (fadeIn > 0) { await this._fadeIn(source, fadeIn, 1.0); } else { source.volume = 1.0; } console.log(`[Audio] BGM: ${path}`); } pauseBGM(): void { this._bgmSource?.pause(); } resumeBGM(): void { this._bgmSource?.play(); } // ========== SFX 管理 ==========/** * 播放音效(OneShot模式,不中断其他音效)
*/
asyncplaySFX( path: string, volumeScale: number = 1.0 ): Promise<void> { const clip = await this._loadClip(path); if (!clip) return; this._sfxSource.playOneShot(clip, volumeScale); } // ========== 语音管理(TTS集成) ==========/** * 播放AI生成的语音(NPC对话等)
* 支持回调,语音播完后触发字幕消失
*/
asyncplayVoice( path: string, onComplete?: () => void, volume: number = 1.0 ): Promise<void> { const clip = await this._loadClip(path); if (!clip) return; const source = this._voiceSource; source.clip = clip; source.loop = false; source.volume = volume; source.play(); // 监听播放完成const checkDone = () => { if (!source.playing) { onComplete?.(); return; } this.scheduleOnce(checkDone, 0.1); }; this.scheduleOnce(checkDone, 0.1); } // ========== 环境音管理 ==========asyncplayAmbient( path: string, fadeIn: number = 2.0, volume: number = 0.5 ): Promise<void> { const clip = await this._loadClip(path); if (!clip) return; const source = this._ambientSource; source.clip = clip; source.loop = true; source.volume = 0; source.play(); await this._fadeIn(source, fadeIn, volume); } // ========== 音量控制 ==========setBGMVolume(vol: number) { this._bgmSource.volume = vol; } setSFXVolume(vol: number) { this._sfxSource.volume = vol; } setVoiceVolume(vol: number) { this._voiceSource.volume = vol; } setAmbientVolume(vol: number) { this._ambientSource.volume = vol; } setMasterVolume(vol: number) { [this._bgmSource, this._sfxSource, this._voiceSource, this._ambientSource ].forEach(s => { if (s) s.volume = vol; }); } // ========== 内部方法 ==========private async_loadClip(path: string): Promise<AudioClip> { if (this._clipCache.has(path)) { return this._clipCache.get(path)!; } return newPromise((resolve) => { resources.load(path, AudioClip, (err, clip) => { if (err) { console.error(`[Audio] 加载失败: ${path}`, err); resolve(null); return; } this._clipCache.set(path, clip); resolve(clip); }); }); } private_fadeIn( source: AudioSource, duration: number, targetVol: number ): Promise<void> { return newPromise(resolve => { const startTime = Date.now(); const startVol = source.volume; const update = () => { const elapsed = (Date.now() - startTime) / 1000; const progress = Math.min(elapsed / duration, 1); // 使用缓动函数让淡入更自然const eased = progress * progress * (3 - 2 * progress); source.volume = startVol + (targetVol - startVol) * eased; if (progress < 1) { this.scheduleOnce(update, 0.02); } else { resolve(); } }; update(); }); } private_fadeOut( source: AudioSource, duration: number ): Promise<void> { return newPromise(resolve => { const startTime = Date.now(); const startVol = source.volume; const update = () => { const elapsed = (Date.now() - startTime) / 1000; const progress = Math.min(elapsed / duration, 1); source.volume = startVol * (1 - progress); if (progress < 1) { this.scheduleOnce(update, 0.02); } else { source.stop(); resolve(); } }; update(); }); } }
5.2 TTS语音预生成管理器
import { _decorator, Component, JsonAsset } from'cc';import { AIAudioMeta } from'./AudioManager';/*** TTS语音预生成管理器
* 管理AI生成的NPC对话语音,支持批量导出和按需加载
*/
exportclassTTSManager { private _voiceMap: Map<string, string> = newMap(); private _metaList: AIAudioMeta[] = []; /** * 初始化语音映射表
* NPC_ID + 对话ID => 音频文件路径
*/
initFromConfig(config: Array<{ npcId: string; dialogueId: string; text: string; voiceId: string; audioPath: string; }>): void { for (const item of config) { const key = `${item.npcId}_${item.dialogueId}`; this._voiceMap.set(key, item.audioPath); this._metaList.push({ id: key, source: 'elevenlabs', prompt: item.text, generatedAt: Date.now(), duration: 0, // 预生成时填入实际时长 }); } console.log(`[TTS] 加载 ${config.length} 条语音映射`); } getVoicePath(npcId: string, dialogueId: string): string | null { return this._voiceMap.get(`${npcId}_${dialogueId}`) || null; } /** * 导出语音元数据(用于编辑器和审核)
*/
exportMeta(): AIAudioMeta[] { return [...this._metaList]; } /** * 批量生成TTS语音的配置导出
* 输出给ElevenLabs API批量处理
*/
exportBatchConfig(): Array<{ text: string; voice_id: string; output_filename: string; }> { return this._metaList.map(meta => ({ text: meta.prompt, voice_id: 'default', output_filename: `voice_${meta.id}.mp3`, })); } }
传统游戏的BGM是固定的——进入Boss战换一首,回城镇换一首。自适应音乐(Adaptive Music)则更进一步:音乐实时响应游戏状态的变化。
6.1 自适应音乐的核心思路
6.2 完整实现:AdaptiveMusicSystem
import { _decorator, Component, AudioSource, AudioClip, resources } from'cc';const { ccclass, property } = _decorator;/*** 游戏状态枚举
* 每个状态对应不同的音乐参数
*/
exportenumGameMusicState { MENU = 'menu', EXPLORE = 'explore', COMBAT = 'combat', BOSS = 'boss', STEALTH = 'stealth', DIALOGUE = 'dialogue', VICTORY = 'victory', DEFEAT = 'defeat', }/*** 音乐参数配置
* 定义每个游戏状态对应的音乐特征
*/
export interfaceMusicStateConfig { state: GameMusicState; bgmPath: string; volume: number; // 0.0 ~ 1.0 pitch: number; // 变速(0.5~2.0) transitionTime: number; // 过渡时间(秒) priority: number; // 优先级(数字越大越优先)// AI生成音乐时的Prompt建议 aiPrompt?: string; }/*** 自适应音乐系统
* 根据游戏状态自动切换BGM,支持平滑过渡
*/
@ccclass('AdaptiveMusicSystem')exportclassAdaptiveMusicSystemextendsComponent { @property(AudioSource) bgmSource: AudioSource = null; @property(AudioSource) layerSource: AudioSource = null; // 当前音乐状态private _currentState: GameMusicState = GameMusicState.MENU; private _isTransitioning: boolean = false; private _clipCache: Map<string, AudioClip> = newMap(); // 事件订阅private _stateListeners: Map<string, () => void> = newMap(); // ========== 状态配置(AI生成音乐Prompt集成) ==========private _stateConfigs: Map<GameMusicState, MusicStateConfig> = newMap([ [GameMusicState.MENU, { state: GameMusicState.MENU, bgmPath: 'audio/bgm/menu', volume: 0.6, pitch: 1.0, transitionTime: 2.0, priority: 0, aiPrompt: 'peaceful menu music, fantasy RPG, instrumental, loopable', }], [GameMusicState.EXPLORE, { state: GameMusicState.EXPLORE, bgmPath: 'audio/bgm/explore', volume: 0.5, pitch: 1.0, transitionTime: 1.5, priority: 1, aiPrompt: 'adventurous exploration music, light orchestra, loopable', }], [GameMusicState.COMBAT, { state: GameMusicState.COMBAT, bgmPath: 'audio/bgm/combat', volume: 0.8, pitch: 1.1, transitionTime: 0.5, priority: 5, aiPrompt: 'intense battle music, fast drums, epic, loopable', }], [GameMusicState.BOSS, { state: GameMusicState.BOSS, bgmPath: 'audio/bgm/boss', volume: 0.9, pitch: 1.0, transitionTime: 0.3, priority: 10, aiPrompt: 'epic boss battle, full orchestra, dramatic, rising tension', }], [GameMusicState.STEALTH, { state: GameMusicState.STEALTH, bgmPath: 'audio/bgm/stealth', volume: 0.4, pitch: 0.9, transitionTime: 1.0, priority: 3, aiPrompt: 'sneaky stealth music, low tension, mysterious, loopable', }], [GameMusicState.DIALOGUE, { state: GameMusicState.DIALOGUE, bgmPath: 'audio/bgm/dialogue', volume: 0.3, pitch: 1.0, transitionTime: 0.8, priority: 7, aiPrompt: 'gentle dialogue background, minimal, emotional, loopable', }], ]); // ========== 核心方法 ==========/** * 切换音乐状态
* 支持优先级判断和平滑过渡
*/
asyncchangeState( newState: GameMusicState, force: boolean = false ): Promise<boolean> { const newConfig = this._stateConfigs.get(newState); const currentConfig = this._stateConfigs.get(this._currentState); // 优先级判断:新状态优先级必须 >= 当前状态if (!force && currentConfig && newConfig.priority < currentConfig.priority) { console.log(`[Music] 状态被忽略: ${newState} (优先级不足)`); return false; } if (this._isTransitioning) { console.warn('[Music] 正在过渡中,忽略状态切换'); return false; } this._isTransitioning = true; console.log(`[Music] 切换: ${this._currentState} → ${newState}`); try { const clip = await this._loadClip(newConfig.bgmPath); if (!clip) return false; const source = this.bgmSource; // 如果是同一首BGM,只调整参数if (source.clip === clip) { await this._smoothTransition( source, newConfig.volume, newConfig.pitch, newConfig.transitionTime ); } else { // 交叉淡入淡出await this._crossFade( source, clip, newConfig.volume, newConfig.pitch, newConfig.transitionTime ); } this._currentState = newState; return true; } finally { this._isTransitioning = false; } } /** * 获取当前状态的所有AI生成Prompt
* 可批量导出给Suno AI生成BGM
*/
exportAIPrompts(): Array<{ state: string; prompt: string }> { return Array.from(this._stateConfigs.values()) .filter(c => c.aiPrompt) .map(c => ({ state: c.state, prompt: c.aiPrompt + ', instrumental only, 2 minutes', })); } // ========== 过渡效果实现 ==========private async_crossFade( source: AudioSource, newClip: AudioClip, targetVol: number, targetPitch: number, duration: number ): Promise<void> { const startVol = source.volume; const startTime = Date.now(); return newPromise(resolve => { const fadeDuration = duration * 1000; const update = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / fadeDuration, 1); const eased = progress * progress * (3 - 2 * progress); // 在中点切换音频if (progress >= 0.5 && source.clip !== newClip) { source.clip = newClip; source.loop = true; source.play(); } // 音量先降后升(V形过渡)const vol = progress < 0.5 ? startVol * (1 - eased * 2) : targetVol * eased * 2; source.volume = Math.max(0, Math.min(1, vol)); if (progress < 1) { this.scheduleOnce(update, 0.016); } else { source.volume = targetVol; resolve(); } }; update(); }); } private async_smoothTransition( source: AudioSource, targetVol: number, targetPitch: number, duration: number ): Promise<void> { const startVol = source.volume; const startTime = Date.now(); return newPromise(resolve => { const update = () => { const elapsed = (Date.now() - startTime) / 1000; const progress = Math.min(elapsed / duration, 1); const eased = progress * progress * (3 - 2 * progress); source.volume = startVol + (targetVol - startVol) * eased; if (progress < 1) { this.scheduleOnce(update, 0.016); } else { resolve(); } }; update(); }); } private async_loadClip(path: string): Promise<AudioClip> { if (this._clipCache.has(path)) { return this._clipCache.get(path)!; } return newPromise(resolve => { resources.load(path, AudioClip, (err, clip) => { if (!err && clip) { this._clipCache.set(path, clip); resolve(clip); } else { console.error(`[Music] 加载BGM失败: ${path}`); resolve(null); } }); }); } }
6.3 游戏事件驱动的音乐切换
import { _decorator, Component, EventTarget } from'cc';import { AdaptiveMusicSystem, GameMusicState } from'./AdaptiveMusicSystem';/*** 游戏事件 -> 音乐状态 映射
* 自动监听游戏事件,驱动音乐系统切换状态
*/
exportclassMusicEventBinder { // 事件到音乐状态的映射private static readonly EVENT_MAP: Record<string, GameMusicState> = { 'game:start': GameMusicState.MENU, 'game:explore': GameMusicState.EXPLORE, 'combat:start': GameMusicState.COMBAT, 'combat:boss': GameMusicState.BOSS, 'combat:end': GameMusicState.EXPLORE, 'stealth:enter': GameMusicState.STEALTH, 'stealth:exit': GameMusicState.EXPLORE, 'dialogue:start': GameMusicState.DIALOGUE, 'dialogue:end': GameMusicState.EXPLORE, 'game:victory': GameMusicState.VICTORY, 'game:defeat': GameMusicState.DEFEAT, }; staticbind( eventTarget: EventTarget, musicSystem: AdaptiveMusicSystem ): void { for (const [event, state] of Object.entries(this.EVENT_MAP)) { eventTarget.on(event, () => { musicSystem.changeState(state); }); } console.log('[Music] 事件绑定完成'); } }