Files
2026-08-11 22:17:54 +08:00

352 lines
12 KiB
JavaScript
Raw Permalink 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.
/**
* AudioManager.js - 音频管理器(框架层)
*
* 功能:
* 1. 统一管理游戏音效和背景音乐的播放/停止
* 2. 封装底层音频API (Utl.playSound/stopSound/playMusic/stopMusic)
* 3. 提供根据性别选择语音的通用基础设施
*
* ⚠️ 框架设计原则:
* - 本文件不包含任何具体游戏的资源常量(不引用 SoundResources)
* - 不包含任何游戏专属的 ID 映射(如麻将牌值、动作类型)
* - 游戏专属逻辑在子游戏侧封装(如 GameAudio.js)
* - 静音开关与音量由平台层(Utl / 平台设置)统一处理,本文件不维护其状态
*
* 核心 API:
* - AudioManager.playSound(soundFile) // 播放任意音效文件
* - AudioManager.playVoice(baseId, sex) // 播放语音(自动切换男/女音)
* - AudioManager.playVoiceBySeat(seat, baseId) // 播放语音(从座位自动获取性别)
* - AudioManager.playMusic() // 播放背景音乐
*
* 性别判断:
* - sex === 2 为女性,女音文件 ID = 男音 baseId + FEMALE_VOICE_OFFSET (100)
* - 可通过 Utl.getSexBySeat(seat) 获取座位上玩家的性别
*
* 注意:
* - 背景音乐需要在 Game_Config.soundList 中配置
* - 游戏专属的 ID 映射(牌值/动作类型等)由子游戏侧 GameAudio.js 维护
* - 是否静音 / 音量大小由平台层 Utl.playSound / Utl.playMusic 内部按用户设置决定
*
* ============================================================================
* @guide 开发指南 - AudioManager 使用规范
* ============================================================================
*
* 【初始化(UIBootstrap 或游戏入口调用一次)】
* AudioManager.init();
*
* 【播放任意音效文件(游戏层传文件名)】
* AudioManager.playSound('00101.mp3');
* // 通常配合游戏侧常量使用:
* AudioManager.playSound(SoundResources.EFFECTS.DISCARD);
*
* 【通用性别语音播放(框架层 API,游戏层控制 ID)】
*
* gameabc 引擎语音文件命名规则:
* - 文件名统一为 5位数字 + ".mp3",如 "00151.mp3"
* - 男音使用基础 ID,女音 = 基础 ID + FEMALE_VOICE_OFFSET(默认 100)
* - 例:男音 baseId=151 → "00151.mp3",女音 → "00251.mp3"
*
* // 指定 baseId 和性别(ID 含义由游戏层定义):
* AudioManager.playVoice(151, sex); // 自动切换男/女音
* AudioManager.playVoiceBySeat(seat, 151); // 从座位自动获取性别再播放
*
* 【游戏侧扩展模式(在子游戏中创建 GameAudio.js)】
*
* // 子游戏自行维护 ID 映射,调用框架的 playVoice/playVoiceBySeat
* var ACTION_VOICE_BASE_ID = {
* 'peng': 152, 'gang': 153, 'hu': 154, 'chi': 151
* };
* GameAudio.playActionBySeat = function(seat, action) {
* var baseId = ACTION_VOICE_BASE_ID[action];
* if (baseId) AudioManager.playVoiceBySeat(seat, baseId);
* };
* // 调用:GameAudio.playActionBySeat(seat, 'peng');
*
* 【背景音乐】
* AudioManager.playMusic(); // 播放(需在 Game_Config.soundList 中配置)
* AudioManager.stopMusic();
*
* @version 4.0.0
* @date 2026-06-08
*/
var AudioManager = AudioManager || {};
(function() {
'use strict';
// ============================================================================
// 私有变量
// ============================================================================
var _initialized = false;
var _currentMusic = null;
var _playingSounds = {}; // { soundFile: playCount }
// ============================================================================
// 框架常量 - gameabc 引擎语音规则
// ============================================================================
/**
* 女性语音 ID 偏移量
* gameabc 引擎约定:女音文件 ID = 男音基础 ID + FEMALE_VOICE_OFFSET
*/
var FEMALE_VOICE_OFFSET = 100;
/**
* 女性性别值(来自 Utl.getSexBySeat 返回值约定)
*/
var SEX_FEMALE = 2;
// ============================================================================
// 私有方法
// ============================================================================
/**
* 检查 Utl API 是否可用
* @private
*/
function _isUtlAvailable() {
return typeof Utl !== 'undefined';
}
/**
* 格式化语音文件名
* gameabc 引擎语音文件统一为 5位数字 + .mp3
* @private
* @param {number} fileNum
* @returns {string} 如 "00151.mp3"
*/
function _formatSoundFile(fileNum) {
var s = String(fileNum);
while (s.length < 5) { s = '0' + s; }
return s + '.mp3';
}
function _isFemale(sex) {
return sex === SEX_FEMALE;
}
/**
* 通过座位号获取玩家性别
* @private
* @param {number} seat
* @returns {number} 性别值(2=女,其他=男)
*/
function _getSexBySeat(seat) {
if (_isUtlAvailable() && typeof Utl.getSexBySeat === 'function') {
return Utl.getSexBySeat(seat);
}
return 1;
}
/**
* 根据性别获取语音 ID 偏移量
* @private
* @param {number} sex
* @returns {number} 0(男)或 FEMALE_VOICE_OFFSET(女)
*/
function _getVoiceOffset(sex) {
return _isFemale(sex) ? FEMALE_VOICE_OFFSET : 0;
}
// ============================================================================
// 公共 API - 初始化
// ============================================================================
/**
* 初始化音频管理器
* 静音/音量由平台层处理,本管理器无需配置项。
*/
AudioManager.init = function() {
if (_initialized) {
console.warn('[AudioManager] Already initialized');
return;
}
_initialized = true;
// console.log('[AudioManager] Initialized (v4.0.0)');
};
// ============================================================================
// 公共 API - 音效播放
// ============================================================================
/**
* 播放音效
* @param {string} soundFile - 文件名,如 "00101.mp3" 或游戏侧常量
* @returns {boolean}
*/
AudioManager.playSound = function(soundFile) {
if (!soundFile) { return false; }
if (!_isUtlAvailable() || !Utl.playSound) {
console.warn('[AudioManager] Utl.playSound not available');
return false;
}
try {
Utl.playSound(soundFile);
_playingSounds[soundFile] = (_playingSounds[soundFile] || 0) + 1;
return true;
} catch (e) {
console.error('[AudioManager] playSound error:', soundFile, e);
return false;
}
};
/**
* 停止音效
* @param {string} soundFile
* @returns {boolean}
*/
AudioManager.stopSound = function(soundFile) {
if (!soundFile) { return false; }
if (!_isUtlAvailable() || !Utl.stopSound) {
console.warn('[AudioManager] Utl.stopSound not available');
return false;
}
try {
Utl.stopSound(soundFile);
if (_playingSounds[soundFile]) {
_playingSounds[soundFile]--;
if (_playingSounds[soundFile] <= 0) { delete _playingSounds[soundFile]; }
}
return true;
} catch (e) {
console.error('[AudioManager] stopSound error:', soundFile, e);
return false;
}
};
/** 停止所有音效 */
AudioManager.stopAllSounds = function() {
for (var f in _playingSounds) {
if (_playingSounds.hasOwnProperty(f)) { this.stopSound(f); }
}
_playingSounds = {};
};
// ============================================================================
// 公共 API - 性别语音(通用)
// ============================================================================
/**
* 根据 baseId 和性别播放语音
* 框架自动应用偏移:女音 = baseId + FEMALE_VOICE_OFFSET
* 游戏层负责维护 baseId 含义(哪个 ID 对应哪张牌/动作)
*
* @param {number} baseId - 男音基础 ID(如 151)
* @param {number} [sex=1] - 性别(2=女,其他=男)
* @returns {boolean}
*/
AudioManager.playVoice = function(baseId, sex) {
if (!baseId) { return false; }
return this.playSound(_formatSoundFile(baseId + _getVoiceOffset(sex)));
};
/**
* 从座位自动获取性别后播放语音(推荐用法)
* @param {number} seat - 座位号
* @param {number} baseId - 男音基础 ID
* @returns {boolean}
*/
AudioManager.playVoiceBySeat = function(seat, baseId) {
return this.playVoice(baseId, _getSexBySeat(seat));
};
/**
* 获取语音文件名(不播放,用于预加载或调试)
* @param {number} baseId
* @param {number} [sex=1]
* @returns {string} 如 "00151.mp3"
*/
AudioManager.getVoiceFile = function(baseId, sex) {
return _formatSoundFile(baseId + _getVoiceOffset(sex));
};
// ============================================================================
// 公共 API - 背景音乐
// ============================================================================
/**
* 播放背景音乐
* 注意:需在 Game_Config.soundList 中配置
* @returns {boolean}
*/
AudioManager.playMusic = function() {
if (!_isUtlAvailable() || !Utl.playMusic) {
console.warn('[AudioManager] Utl.playMusic not available');
return false;
}
try {
Utl.playMusic();
_currentMusic = true;
return true;
} catch (e) {
console.error('[AudioManager] playMusic error:', e);
return false;
}
};
/**
* 停止背景音乐
* @returns {boolean}
*/
AudioManager.stopMusic = function() {
if (!_isUtlAvailable() || !Utl.stopMusic) { return false; }
try {
Utl.stopMusic();
_currentMusic = null;
return true;
} catch (e) {
console.error('[AudioManager] stopMusic error:', e);
return false;
}
};
/** 暂停背景音乐(等同 stopMusic) */
AudioManager.pauseMusic = function() {
return this.stopMusic();
};
/** 恢复背景音乐 */
AudioManager.resumeMusic = function() {
if (_currentMusic) { return this.playMusic(); }
return false;
};
// ============================================================================
// 公共API - 状态
// ============================================================================
/**
* 获取音频管理器状态
* @returns {Object}
*/
AudioManager.getStatus = function() {
var playingCount = 0;
for (var key in _playingSounds) {
if (_playingSounds.hasOwnProperty(key)) {
playingCount += _playingSounds[key];
}
}
return {
initialized: _initialized,
playingSoundsCount: playingCount,
isMusicPlaying: !!_currentMusic,
version: '4.0.0'
};
};
/**
* 销毁音频管理器
*/
AudioManager.destroy = function() {
this.stopAllSounds();
this.stopMusic();
_playingSounds = {};
_currentMusic = null;
_initialized = false;
};
})();