|
|
- /**
- * 统一音频管理器
- * 实现单实例音频管理,避免多个音频同时播放造成的冲突
- */
- class AudioManager {
- constructor() {
- // 当前音频实例
- this.currentAudio = null;
- // 音频类型:'sentence' | 'word'
- this.currentAudioType = null;
- // 音频状态
- this.isPlaying = false;
- // 复用的音频实例池
- this.audioInstance = null;
- // 事件监听器
- this.listeners = {
- play: [],
- pause: [],
- ended: [],
- error: [],
- timeupdate: [],
- canplay: []
- };
- // 播放速度支持检测
- this.playbackRateSupported = true;
-
- // 统一音频配置
- this.globalPlaybackRate = 1.0; // 全局播放速度
- this.globalVoiceId = ''; // 全局音色ID
- this.speedOptions = [0.5, 0.8, 1.0, 1.25, 1.5, 2.0]; // 支持的播放速度选项
- }
-
- /**
- * 创建HTML5 Audio实例并包装为uni-app兼容接口
- */
- createHTML5Audio() {
- const audio = new Audio();
-
- // 包装为uni-app兼容的接口
- const wrappedAudio = {
- // 原生HTML5 Audio实例
- _nativeAudio: audio,
-
- // 基本属性
- get src() { return audio.src; },
- set src(value) { audio.src = value; },
-
- get duration() { return audio.duration || 0; },
- get currentTime() { return audio.currentTime || 0; },
-
- get paused() { return audio.paused; },
-
- // 支持倍速的关键属性
- get playbackRate() { return audio.playbackRate; },
- set playbackRate(value) {
- try {
- audio.playbackRate = value;
- console.log(`🎵 HTML5 Audio倍速设置成功: ${value}x`);
- } catch (error) {
- console.error('❌ HTML5 Audio倍速设置失败:', error);
- }
- },
-
- // 基本方法
- play() {
- return audio.play().catch(error => {
- console.error('HTML5 Audio播放失败:', error);
- });
- },
-
- pause() {
- audio.pause();
- },
-
- stop() {
- audio.pause();
- audio.currentTime = 0;
- },
-
- seek(time) {
- audio.currentTime = time;
- },
-
- destroy() {
- audio.pause();
- audio.src = '';
- audio.load();
- },
-
- // 事件绑定方法
- onCanplay(callback) {
- audio.addEventListener('canplay', callback);
- },
-
- onPlay(callback) {
- audio.addEventListener('play', callback);
- },
-
- onPause(callback) {
- audio.addEventListener('pause', callback);
- },
-
- onEnded(callback) {
- audio.addEventListener('ended', callback);
- },
-
- onTimeUpdate(callback) {
- audio.addEventListener('timeupdate', callback);
- },
-
- onError(callback) {
- // 包装错误事件,过滤掉非关键错误
- const wrappedCallback = (error) => {
- // 只在有src且音频正在播放时才传递错误事件
- if (audio.src && audio.src.trim() !== '' && !audio.paused) {
- callback(error);
- } else {
- console.log('🔇 HTML5 Audio错误(已忽略):', {
- hasSrc: !!audio.src,
- paused: audio.paused,
- errorType: error.type || 'unknown'
- });
- }
- };
- audio.addEventListener('error', wrappedCallback);
- },
-
- // 移除事件监听
- offCanplay(callback) {
- audio.removeEventListener('canplay', callback);
- },
-
- offPlay(callback) {
- audio.removeEventListener('play', callback);
- },
-
- offPause(callback) {
- audio.removeEventListener('pause', callback);
- },
-
- offEnded(callback) {
- audio.removeEventListener('ended', callback);
- },
-
- offTimeUpdate(callback) {
- audio.removeEventListener('timeupdate', callback);
- },
-
- offError(callback) {
- audio.removeEventListener('error', callback);
- }
- };
-
- return wrappedAudio;
- }
-
- /**
- * 创建音频实例
- * 优先使用HTML5 Audio(支持倍速),降级到uni.createInnerAudioContext
- * 复用已存在的音频实例以提高性能
- */
- createAudioInstance() {
- // 如果已有音频实例,直接复用
- if (this.audioInstance) {
- console.log('🔄 复用现有音频实例');
- return this.audioInstance;
- }
-
- // 在H5环境下优先使用HTML5 Audio
- // #ifdef H5
- try {
- this.audioInstance = this.createHTML5Audio();
- console.log('🎵 创建新的HTML5 Audio实例');
- } catch (error) {
- console.warn('⚠️ HTML5 Audio创建失败,降级到uni音频:', error);
- audio = uni.createInnerAudioContext();
- this.playbackRateSupported = false;
- }
- // #endif
-
- // 在非H5环境下使用uni音频
- // #ifndef H5
- this.audioInstance = uni.createInnerAudioContext();
- this.playbackRateSupported = false;
- console.log('🎵 创建新的uni音频实例');
- // #endif
-
- return this.audioInstance;
- }
-
- /**
- * 停止当前音频
- */
- stopCurrentAudio() {
- if (this.currentAudio) {
- console.log(`🛑 停止当前音频 (类型: ${this.currentAudioType})`);
-
- try {
- this.currentAudio.pause();
- // 不销毁音频实例,保留以供复用
- // this.currentAudio.destroy();
- } catch (error) {
- console.error('⚠️ 停止音频时出错:', error);
- }
-
- this.currentAudio = null;
- this.currentAudioType = null;
- this.isPlaying = false;
-
- // 触发暂停事件
- this.emit('pause');
- }
- }
-
- /**
- * 播放音频
- * @param {string} audioUrl - 音频URL
- * @param {string} audioType - 音频类型 ('sentence' | 'word')
- * @param {Object} options - 播放选项
- */
- async playAudio(audioUrl, audioType = 'word', options = {}) {
- try {
- console.log(`🎵 开始播放${audioType}音频:`, audioUrl);
-
- // 停止当前播放的音频
- this.stopCurrentAudio();
-
- // 创建新的音频实例
- const audio = this.createAudioInstance();
- audio.src = audioUrl;
-
- // 设置播放选项,优先使用全局播放速度
- const playbackRate = options.playbackRate || this.globalPlaybackRate;
- if (playbackRate && this.playbackRateSupported) {
- audio.playbackRate = playbackRate;
- console.log(`🎵 设置音频播放速度: ${playbackRate}x`);
- }
-
- // 绑定事件监听器
- this.bindAudioEvents(audio, audioType);
-
- // 保存当前音频实例
- this.currentAudio = audio;
- this.currentAudioType = audioType;
-
- // 延迟播放,确保音频实例完全准备好
- setTimeout(() => {
- if (this.currentAudio === audio) {
- try {
- audio.play();
- console.log(`✅ ${audioType}音频开始播放`);
- } catch (playError) {
- console.error('❌ 播放命令失败:', playError);
- this.emit('error', playError);
- }
- }
- }, 100);
-
- return audio;
- } catch (error) {
- console.error('❌ 播放音频异常:', error);
- this.emit('error', error);
- throw error;
- }
- }
-
- /**
- * 绑定音频事件监听器
- */
- bindAudioEvents(audio, audioType) {
- // 播放开始
- audio.onPlay(() => {
- console.log(`🎵 ${audioType}音频播放开始`);
- this.isPlaying = true;
- this.emit('play', { audioType, audio });
- });
-
- // 播放暂停
- audio.onPause(() => {
- console.log(`⏸️ ${audioType}音频播放暂停`);
- this.isPlaying = false;
- this.emit('pause', { audioType, audio });
- });
-
- // 播放结束
- audio.onEnded(() => {
- console.log(`🏁 ${audioType}音频播放结束`);
- this.isPlaying = false;
-
- // 不销毁音频实例,保留以供复用
- // 只清理当前播放状态
- if (this.currentAudio === audio) {
- this.currentAudio = null;
- this.currentAudioType = null;
- }
-
- this.emit('ended', { audioType, audio });
- });
-
- // 播放错误
- audio.onError((error) => {
- console.error(`❌ ${audioType}音频播放失败:`, error);
- this.isPlaying = false;
-
- // 发生错误时才销毁音频实例
- try {
- audio.destroy();
- } catch (destroyError) {
- console.error('⚠️ 销毁音频实例时出错:', destroyError);
- }
-
- if (this.currentAudio === audio) {
- this.currentAudio = null;
- this.currentAudioType = null;
- }
-
- // 清理复用的音频实例引用
- if (this.audioInstance === audio) {
- this.audioInstance = null;
- }
-
- this.emit('error', { error, audioType, audio });
- });
-
- // 时间更新
- audio.onTimeUpdate(() => {
- this.emit('timeupdate', {
- currentTime: audio.currentTime,
- duration: audio.duration,
- audioType,
- audio
- });
- });
-
- // 可以播放
- audio.onCanplay(() => {
- this.emit('canplay', { audioType, audio });
- });
- }
-
- /**
- * 暂停当前音频
- */
- pause() {
- if (this.currentAudio && this.isPlaying) {
- this.currentAudio.pause();
- }
- }
-
- /**
- * 恢复播放
- */
- resume() {
- if (this.currentAudio && !this.isPlaying) {
- this.currentAudio.play();
- }
- }
-
- /**
- * 设置播放速度
- */
- setPlaybackRate(rate) {
- if (this.currentAudio && this.playbackRateSupported) {
- this.currentAudio.playbackRate = rate;
- return true;
- }
- return false;
- }
-
- /**
- * 设置全局播放速度
- * @param {number} rate - 播放速度 (0.5 - 2.0)
- */
- setGlobalPlaybackRate(rate) {
- if (rate < 0.5 || rate > 2.0) {
- console.warn('⚠️ 播放速度超出支持范围 (0.5-2.0):', rate);
- return false;
- }
-
- this.globalPlaybackRate = rate;
- console.log(`🎵 设置全局播放速度: ${rate}x`);
-
- // 如果当前有音频在播放,立即应用新的播放速度
- if (this.currentAudio && this.playbackRateSupported) {
- try {
- this.currentAudio.playbackRate = rate;
- console.log(`✅ 当前音频播放速度已更新: ${rate}x`);
- } catch (error) {
- console.error('❌ 更新当前音频播放速度失败:', error);
- }
- }
-
- return true;
- }
-
- /**
- * 获取全局播放速度
- */
- getGlobalPlaybackRate() {
- return this.globalPlaybackRate;
- }
-
- /**
- * 设置全局音色ID
- * @param {string|number} voiceId - 音色ID
- */
- setGlobalVoiceId(voiceId) {
- this.globalVoiceId = String(voiceId);
- console.log(`🎵 设置全局音色ID: ${this.globalVoiceId}`);
- }
-
- /**
- * 获取全局音色ID
- */
- getGlobalVoiceId() {
- return this.globalVoiceId;
- }
-
- /**
- * 获取支持的播放速度选项
- */
- getSpeedOptions() {
- return [...this.speedOptions];
- }
-
- /**
- * 切换到下一个播放速度
- */
- togglePlaybackRate() {
- const currentIndex = this.speedOptions.indexOf(this.globalPlaybackRate);
- const nextIndex = (currentIndex + 1) % this.speedOptions.length;
- const nextRate = this.speedOptions[nextIndex];
-
- this.setGlobalPlaybackRate(nextRate);
- return nextRate;
- }
-
- /**
- * 获取当前播放状态
- */
- getPlaybackState() {
- return {
- isPlaying: this.isPlaying,
- currentAudioType: this.currentAudioType,
- hasAudio: !!this.currentAudio,
- playbackRateSupported: this.playbackRateSupported,
- currentTime: this.currentAudio ? this.currentAudio.currentTime : 0,
- duration: this.currentAudio ? this.currentAudio.duration : 0
- };
- }
-
- /**
- * 添加事件监听器
- */
- on(event, callback) {
- if (this.listeners[event]) {
- // 检查是否已经绑定过相同的回调函数,避免重复绑定
- if (this.listeners[event].indexOf(callback) === -1) {
- this.listeners[event].push(callback);
- console.log(`🎵 添加事件监听器: ${event}, 当前监听器数量: ${this.listeners[event].length}`);
- } else {
- console.warn(`⚠️ 事件监听器已存在,跳过重复绑定: ${event}`);
- }
- }
- }
-
- /**
- * 移除事件监听器
- */
- off(event, callback) {
- if (this.listeners[event]) {
- const index = this.listeners[event].indexOf(callback);
- if (index > -1) {
- this.listeners[event].splice(index, 1);
- }
- }
- }
-
- /**
- * 触发事件
- */
- emit(event, data) {
- if (this.listeners[event]) {
- this.listeners[event].forEach(callback => {
- try {
- callback(data);
- } catch (error) {
- console.error(`事件监听器执行错误 (${event}):`, error);
- }
- });
- }
- }
-
- /**
- * 销毁音频管理器
- */
- destroy() {
- this.stopCurrentAudio();
-
- // 销毁复用的音频实例
- // if (this.audioInstance) {
- // try {
- // this.audioInstance.destroy();
- // } catch (error) {
- // console.error('⚠️ 销毁音频实例时出错:', error);
- // }
- // this.audioInstance = null;
- // }
-
- this.listeners = {
- play: [],
- pause: [],
- ended: [],
- error: [],
- timeupdate: [],
- canplay: []
- };
- }
- }
-
- // 创建全局单例
- const audioManager = new AudioManager();
-
- export default audioManager;
|