// 语音消息管理器 - 微信小程序专用 // 处理语音录制、播放、转换、上传等功能 const apiClient = require('./api-client.js'); const performanceMonitor = require('./performance-monitor.js'); /** * 语音消息管理器 * 功能: * 1. 语音录制和停止 * 2. 语音播放和暂停 * 3. 语音文件管理 * 4. 语音质量控制 * 5. 语音时长限制 * 6. 语音格式转换 */ class VoiceMessageManager { constructor() { this.isInitialized = false; // 录音配置 this.recordConfig = { duration: 60000, // 最大录音时长 60秒 sampleRate: 16000, // 采样率 numberOfChannels: 1, // 声道数 encodeBitRate: 48000, // 编码码率 format: 'mp3', // 录音格式 frameSize: 50, // 帧大小 minDuration: 1000, // 最小录音时长 1秒 maxDuration: 60000 // 最大录音时长 60秒 }; // 播放配置 this.playConfig = { autoplay: false, loop: false, volume: 1.0, playbackRate: 1.0 }; // 录音器实例 this.recorderManager = null; this.innerAudioContext = null; // 录音状态 this.recordingState = { isRecording: false, isPaused: false, startTime: null, duration: 0, tempFilePath: null, fileSize: 0 }; // 播放状态 this.playingState = { isPlaying: false, isPaused: false, currentTime: 0, duration: 0, currentVoiceId: null, playingMessageId: null }; // 语音文件缓存 this.voiceCache = new Map(); // 事件监听器 this.eventListeners = new Map(); // 权限状态 this.permissionGranted = false; this.init(); } // 初始化语音消息管理器 async init() { if (this.isInitialized) return; console.log('🎤 初始化语音消息管理器...'); try { // 初始化录音管理器 this.initRecorderManager(); // 初始化音频播放器 this.initAudioPlayer(); // 检查录音权限 await this.checkRecordPermission(); this.isInitialized = true; console.log('✅ 语音消息管理器初始化完成'); } catch (error) { console.error('❌ 语音消息管理器初始化失败:', error); } } // 初始化录音管理器 initRecorderManager() { this.recorderManager = wx.getRecorderManager(); // 录音开始事件 this.recorderManager.onStart(() => { console.log('🎤 录音开始'); this.recordingState.isRecording = true; this.recordingState.startTime = Date.now(); this.triggerEvent('recordStart'); }); // 录音暂停事件 this.recorderManager.onPause(() => { console.log('⏸️ 录音暂停'); this.recordingState.isPaused = true; this.triggerEvent('recordPause'); }); // 录音恢复事件 this.recorderManager.onResume(() => { console.log('▶️ 录音恢复'); this.recordingState.isPaused = false; this.triggerEvent('recordResume'); }); // 录音停止事件 this.recorderManager.onStop((res) => { console.log('⏹️ 录音停止:', res); this.recordingState.isRecording = false; this.recordingState.isPaused = false; this.recordingState.duration = res.duration; this.recordingState.tempFilePath = res.tempFilePath; this.recordingState.fileSize = res.fileSize; this.triggerEvent('recordStop', { duration: res.duration, tempFilePath: res.tempFilePath, fileSize: res.fileSize }); }); // 录音错误事件 this.recorderManager.onError((error) => { console.error('❌ 录音错误:', error); this.recordingState.isRecording = false; this.recordingState.isPaused = false; this.triggerEvent('recordError', error); }); // 录音帧数据事件(用于实时波形显示) this.recorderManager.onFrameRecorded((res) => { this.triggerEvent('recordFrame', { frameBuffer: res.frameBuffer, isLastFrame: res.isLastFrame }); }); } // 初始化音频播放器 initAudioPlayer() { this.innerAudioContext = wx.createInnerAudioContext(); // 播放开始事件 this.innerAudioContext.onPlay(() => { console.log('🔊 语音播放开始'); this.playingState.isPlaying = true; this.playingState.isPaused = false; this.triggerEvent('playStart'); }); // 播放暂停事件 this.innerAudioContext.onPause(() => { console.log('⏸️ 语音播放暂停'); this.playingState.isPaused = true; this.triggerEvent('playPause'); }); // 播放结束事件 this.innerAudioContext.onEnded(() => { console.log('⏹️ 语音播放结束'); this.playingState.isPlaying = false; this.playingState.isPaused = false; this.playingState.currentTime = 0; this.playingState.currentVoiceId = null; this.playingState.playingMessageId = null; this.triggerEvent('playEnd'); }); // 播放错误事件 this.innerAudioContext.onError((error) => { console.error('❌ 语音播放错误:', error); this.playingState.isPlaying = false; this.playingState.isPaused = false; this.triggerEvent('playError', error); }); // 播放进度更新事件 this.innerAudioContext.onTimeUpdate(() => { this.playingState.currentTime = this.innerAudioContext.currentTime; this.playingState.duration = this.innerAudioContext.duration; this.triggerEvent('playTimeUpdate', { currentTime: this.playingState.currentTime, duration: this.playingState.duration }); }); // 音频加载完成事件 this.innerAudioContext.onCanplay(() => { console.log('🎵 语音加载完成'); this.triggerEvent('playCanplay'); }); } // 🎤 ===== 录音功能 ===== // 开始录音 async startRecording(options = {}) { if (!this.isInitialized) { throw new Error('语音消息管理器未初始化'); } if (this.recordingState.isRecording) { throw new Error('正在录音中'); } // 检查录音权限 if (!this.permissionGranted) { const granted = await this.requestRecordPermission(); if (!granted) { throw new Error('录音权限被拒绝'); } } try { const recordOptions = { ...this.recordConfig, ...options }; console.log('🎤 开始录音,配置:', recordOptions); // 重置录音状态 this.recordingState = { isRecording: false, isPaused: false, startTime: null, duration: 0, tempFilePath: null, fileSize: 0 }; this.recorderManager.start(recordOptions); } catch (error) { console.error('❌ 开始录音失败:', error); throw error; } } // 停止录音 stopRecording() { if (!this.recordingState.isRecording) { throw new Error('当前没有在录音'); } try { console.log('⏹️ 停止录音'); this.recorderManager.stop(); } catch (error) { console.error('❌ 停止录音失败:', error); throw error; } } // 暂停录音 pauseRecording() { if (!this.recordingState.isRecording || this.recordingState.isPaused) { throw new Error('当前状态无法暂停录音'); } try { console.log('⏸️ 暂停录音'); this.recorderManager.pause(); } catch (error) { console.error('❌ 暂停录音失败:', error); throw error; } } // 恢复录音 resumeRecording() { if (!this.recordingState.isRecording || !this.recordingState.isPaused) { throw new Error('当前状态无法恢复录音'); } try { console.log('▶️ 恢复录音'); this.recorderManager.resume(); } catch (error) { console.error('❌ 恢复录音失败:', error); throw error; } } // 取消录音 cancelRecording() { if (!this.recordingState.isRecording) { return; } try { console.log('❌ 取消录音'); this.recorderManager.stop(); // 重置录音状态 this.recordingState = { isRecording: false, isPaused: false, startTime: null, duration: 0, tempFilePath: null, fileSize: 0 }; this.triggerEvent('recordCancel'); } catch (error) { console.error('❌ 取消录音失败:', error); } } // 🔊 ===== 播放功能 ===== // 播放语音消息 async playVoiceMessage(voiceUrl, messageId = null, options = {}) { if (!this.isInitialized) { throw new Error('语音消息管理器未初始化'); } try { // 如果正在播放其他语音,先停止 if (this.playingState.isPlaying) { this.stopPlaying(); } console.log('🔊 播放语音消息:', voiceUrl); // 设置播放配置 const playOptions = { ...this.playConfig, ...options }; this.innerAudioContext.src = voiceUrl; this.innerAudioContext.autoplay = playOptions.autoplay; this.innerAudioContext.loop = playOptions.loop; this.innerAudioContext.volume = playOptions.volume; this.innerAudioContext.playbackRate = playOptions.playbackRate; // 更新播放状态 this.playingState.currentVoiceId = voiceUrl; this.playingState.playingMessageId = messageId; // 开始播放 this.innerAudioContext.play(); } catch (error) { console.error('❌ 播放语音消息失败:', error); throw error; } } // 暂停播放 pausePlaying() { if (!this.playingState.isPlaying || this.playingState.isPaused) { return; } try { console.log('⏸️ 暂停播放'); this.innerAudioContext.pause(); } catch (error) { console.error('❌ 暂停播放失败:', error); } } // 恢复播放 resumePlaying() { if (!this.playingState.isPlaying || !this.playingState.isPaused) { return; } try { console.log('▶️ 恢复播放'); this.innerAudioContext.play(); } catch (error) { console.error('❌ 恢复播放失败:', error); } } // 停止播放 stopPlaying() { if (!this.playingState.isPlaying) { return; } try { console.log('⏹️ 停止播放'); this.innerAudioContext.stop(); // 重置播放状态 this.playingState.isPlaying = false; this.playingState.isPaused = false; this.playingState.currentTime = 0; this.playingState.currentVoiceId = null; this.playingState.playingMessageId = null; } catch (error) { console.error('❌ 停止播放失败:', error); } } // 设置播放进度 seekTo(time) { if (!this.playingState.isPlaying) { return; } try { this.innerAudioContext.seek(time); this.playingState.currentTime = time; } catch (error) { console.error('❌ 设置播放进度失败:', error); } } // 📁 ===== 文件管理 ===== // 上传语音文件 async uploadVoiceFile(tempFilePath, duration) { try { console.log('📤 上传语音文件:', tempFilePath); // 使用微信小程序的上传文件API const uploadResult = await new Promise((resolve, reject) => { wx.uploadFile({ url: `${apiClient.baseUrl}/api/v1/file/upload`, filePath: tempFilePath, name: 'file', formData: { file_type: 'audio', usage_type: 'message', duration: duration.toString() }, header: { 'Authorization': `Bearer ${apiClient.getToken()}` }, success: (res) => { try { const data = JSON.parse(res.data); resolve({ success: data.success || res.statusCode === 200, data: data.data || data, message: data.message }); } catch (error) { resolve({ success: res.statusCode === 200, data: { url: res.data }, message: '上传成功' }); } }, fail: reject }); }); if (uploadResult.success) { const fileUrl = uploadResult.data.url || uploadResult.data.file_url || uploadResult.data.fileUrl; console.log('✅ 语音文件上传成功:', fileUrl); return { success: true, url: fileUrl, duration: duration, size: this.recordingState.fileSize }; } else { throw new Error(uploadResult.message || '上传失败'); } } catch (error) { console.error('❌ 上传语音文件失败:', error); throw error; } } // 下载语音文件到本地 async downloadVoiceFile(voiceUrl) { try { // 检查缓存 if (this.voiceCache.has(voiceUrl)) { const cached = this.voiceCache.get(voiceUrl); if (this.isFileExists(cached.localPath)) { return cached.localPath; } else { this.voiceCache.delete(voiceUrl); } } console.log('📥 下载语音文件:', voiceUrl); const downloadResult = await new Promise((resolve, reject) => { wx.downloadFile({ url: voiceUrl, success: resolve, fail: reject }); }); if (downloadResult.statusCode === 200) { // 缓存文件路径 this.voiceCache.set(voiceUrl, { localPath: downloadResult.tempFilePath, downloadTime: Date.now() }); console.log('✅ 语音文件下载成功:', downloadResult.tempFilePath); return downloadResult.tempFilePath; } else { throw new Error(`下载失败,状态码: ${downloadResult.statusCode}`); } } catch (error) { console.error('❌ 下载语音文件失败:', error); throw error; } } // 检查文件是否存在 isFileExists(filePath) { try { const fileManager = wx.getFileSystemManager(); const stats = fileManager.statSync(filePath); return stats.isFile(); } catch (error) { return false; } } // 🔐 ===== 权限管理 ===== // 检查录音权限 async checkRecordPermission() { try { const setting = await new Promise((resolve, reject) => { wx.getSetting({ success: resolve, fail: reject }); }); this.permissionGranted = setting.authSetting['scope.record'] === true; console.log('🔐 录音权限状态:', this.permissionGranted); return this.permissionGranted; } catch (error) { console.error('❌ 检查录音权限失败:', error); return false; } } // 请求录音权限 async requestRecordPermission() { try { await new Promise((resolve, reject) => { wx.authorize({ scope: 'scope.record', success: resolve, fail: reject }); }); this.permissionGranted = true; console.log('✅ 录音权限获取成功'); return true; } catch (error) { console.error('❌ 录音权限获取失败:', error); this.permissionGranted = false; // 引导用户到设置页面 this.showPermissionGuide(); return false; } } // 显示权限引导 showPermissionGuide() { wx.showModal({ title: '需要录音权限', content: '使用语音消息功能需要录音权限,请在设置中开启', confirmText: '去设置', cancelText: '取消', success: (res) => { if (res.confirm) { wx.openSetting({ success: (settingRes) => { if (settingRes.authSetting['scope.record']) { this.permissionGranted = true; console.log('✅ 用户已开启录音权限'); } } }); } } }); } // 📊 ===== 状态管理 ===== // 获取录音状态 getRecordingState() { return { ...this.recordingState }; } // 获取播放状态 getPlayingState() { return { ...this.playingState }; } // 是否正在录音 isRecording() { return this.recordingState.isRecording; } // 是否正在播放 isPlaying() { return this.playingState.isPlaying; } // 获取当前播放的消息ID getCurrentPlayingMessageId() { return this.playingState.playingMessageId; } // 🎧 ===== 事件管理 ===== // 注册事件监听器 on(event, callback) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event).push(callback); } // 移除事件监听器 off(event, callback) { if (this.eventListeners.has(event)) { const listeners = this.eventListeners.get(event); const index = listeners.indexOf(callback); if (index > -1) { listeners.splice(index, 1); } } } // 触发事件 triggerEvent(event, data = null) { if (this.eventListeners.has(event)) { const listeners = this.eventListeners.get(event); listeners.forEach(callback => { try { callback(data); } catch (error) { console.error(`❌ 事件处理器错误 [${event}]:`, error); } }); } } // 🔧 ===== 工具方法 ===== // 格式化时长 formatDuration(duration) { const seconds = Math.floor(duration / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes > 0) { return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } else { return `${remainingSeconds}"`; } } // 获取语音文件大小描述 getFileSizeDescription(fileSize) { if (fileSize < 1024) { return `${fileSize}B`; } else if (fileSize < 1024 * 1024) { return `${(fileSize / 1024).toFixed(1)}KB`; } else { return `${(fileSize / (1024 * 1024)).toFixed(1)}MB`; } } // 清理缓存 clearCache() { this.voiceCache.clear(); console.log('🧹 语音文件缓存已清理'); } // 销毁管理器 destroy() { // 停止录音和播放 if (this.recordingState.isRecording) { this.cancelRecording(); } if (this.playingState.isPlaying) { this.stopPlaying(); } // 销毁音频上下文 if (this.innerAudioContext) { this.innerAudioContext.destroy(); this.innerAudioContext = null; } // 清理缓存和事件监听器 this.clearCache(); this.eventListeners.clear(); this.isInitialized = false; console.log('🎤 语音消息管理器已销毁'); } } // 创建全局实例 const voiceMessageManager = new VoiceMessageManager(); module.exports = voiceMessageManager;