| /** | |
|  * 统一音频管理器 | |
|  * 实现单实例音频管理,避免多个音频同时播放造成的冲突 | |
|  */ | |
| 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;
 |