import { ref, onUnmounted } from 'vue' import { $api } from '../common/globalFunction'; // 你的请求封装 import config from '@/config' // 开源 export function useAudioRecorder() { // --- 状态定义 --- const isRecording = ref(false) const isSocketConnected = ref(false) const recordingDuration = ref(0) const volumeLevel = ref(0) // 0-100 const recognizedText = ref('') // --- 内部变量 --- let socketTask = null let durationTimer = null // --- APP/小程序 变量 --- let recorderManager = null; // --- H5 变量 --- let audioContext = null; let scriptProcessor = null; let mediaStreamSource = null; let h5Stream = null; // --- 配置项 --- const RECORD_CONFIG = { duration: 600000, sampleRate: 16000, numberOfChannels: 1, format: 'pcm', frameSize: 4096 } /** * 获取 WebSocket 地址 (含 Token) */ const getWsUrl = async () => { let wsUrl = config.vioceBaseURl // 拼接 Token const token = uni.getStorageSync('token') || ''; if (token) { const separator = wsUrl.includes('?') ? '&' : '?'; wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(token)}`; } return wsUrl; } /** * 开始录音 (入口) */ const startRecording = async () => { if (isRecording.value) return try { recognizedText.value = '' volumeLevel.value = 0 // #ifdef H5 if (location.protocol !== 'https:' && location.hostname !== 'localhost') { uni.showToast({ title: 'H5录音需要HTTPS环境', icon: 'none' }); return; } // #endif const url = await getWsUrl() console.log('正在连接 ASR:', url) await connectSocket(url); } catch (err) { console.error('启动失败:', err); uni.showToast({ title: '启动失败: ' + (err.message || ''), icon: 'none' }); cleanup(); } } /** * 连接 WebSocket */ const connectSocket = (url) => { return new Promise((resolve, reject) => { socketTask = uni.connectSocket({ url: url, success: () => console.log('Socket 连接请求发送'), fail: (err) => reject(err) }); socketTask.onOpen((res) => { console.log('WebSocket 已连接'); isSocketConnected.value = true; // #ifdef H5 startH5Recording().then(() => resolve()).catch(err => { socketTask.close(); reject(err); }); // #endif // #ifndef H5 startAppRecording(); resolve(); // #endif }); socketTask.onMessage((res) => { // 接收文本结果 if (res.data) { recognizedText.value = res.data; } }); socketTask.onError((err) => { console.error('Socket 错误:', err); isSocketConnected.value = false; stopRecording(); }); socketTask.onClose(() => { isSocketConnected.value = false; console.log('Socket 已关闭'); }); }) } const startH5Recording = async () => { try { // 1. 获取麦克风流 const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); h5Stream = stream; // 2. 创建 AudioContext const AudioContext = window.AudioContext || window.webkitAudioContext; audioContext = new AudioContext({ sampleRate: 16000 }); mediaStreamSource = audioContext.createMediaStreamSource(stream); scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1); scriptProcessor.onaudioprocess = (event) => { if (!isSocketConnected.value || !socketTask) return; const inputData = event.inputBuffer.getChannelData(0); calculateVolume(inputData, true); const buffer = new ArrayBuffer(inputData.length * 2); const view = new DataView(buffer); for (let i = 0; i < inputData.length; i++) { let s = Math.max(-1, Math.min(1, inputData[i])); view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } socketTask.send({ data: buffer, fail: (e) => console.error('发送音频失败', e) }); }; mediaStreamSource.connect(scriptProcessor); scriptProcessor.connect(audioContext.destination); isRecording.value = true; recordingDuration.value = 0; durationTimer = setInterval(() => recordingDuration.value++, 1000); console.log('H5 录音已启动'); } catch (err) { console.error('H5 录音启动失败:', err); throw err; } } const stopH5Resources = () => { if (scriptProcessor) scriptProcessor.disconnect(); if (mediaStreamSource) mediaStreamSource.disconnect(); if (audioContext) audioContext.close(); if (h5Stream) h5Stream.getTracks().forEach(track => track.stop()); scriptProcessor = null; mediaStreamSource = null; audioContext = null; h5Stream = null; } const startAppRecording = () => { recorderManager = uni.getRecorderManager(); recorderManager.onFrameRecorded((res) => { const { frameBuffer } = res; calculateVolume(frameBuffer, false); if (isSocketConnected.value && socketTask) { socketTask.send({ data: frameBuffer }); } }); recorderManager.onStart(() => { console.log('APP 录音已开始'); isRecording.value = true; recordingDuration.value = 0; durationTimer = setInterval(() => recordingDuration.value++, 1000); }); recorderManager.onError((err) => { console.error('APP 录音报错:', err); cleanup(); }); recorderManager.start(RECORD_CONFIG); } const stopHardwareResource = () => { // APP/小程序停止 if (recorderManager) { recorderManager.stop(); } // H5停止 // #ifdef H5 if (scriptProcessor) scriptProcessor.disconnect(); if (mediaStreamSource) mediaStreamSource.disconnect(); if (audioContext) audioContext.close(); if (h5Stream) h5Stream.getTracks().forEach(track => track.stop()); scriptProcessor = null; mediaStreamSource = null; audioContext = null; h5Stream = null; // #endif } /** * 停止录音 (通用) */ const stopRecording = () => { // 停止 APP 录音 if (recorderManager) { recorderManager.stop(); } // 停止 H5 录音资源 // #ifdef H5 stopH5Resources(); // #endif // 关闭 Socket if (socketTask) { socketTask.close(); } cleanup(); } const cancelRecording = () => { if (!isRecording.value) return; console.log('取消录音 - 丢弃结果'); // 1. 停止硬件录音 stopHardwareResource(); // 2. 强制关闭 Socket if (socketTask) { socketTask.close(); } // 3. 关键:清空已识别的文本 recognizedText.value = ''; // 4. 清理资源 cleanup(); } /** * 清理状态 */ const cleanup = () => { clearInterval(durationTimer); isRecording.value = false; isSocketConnected.value = false; socketTask = null; recorderManager = null; volumeLevel.value = 0; } /** * 计算音量 (兼容 Float32 和 Int16/ArrayBuffer) */ const calculateVolume = (data, isFloat32) => { let sum = 0; let length = 0; if (isFloat32) { length = data.length; for (let i = 0; i < length; i += 10) { sum += Math.abs(data[i]); } volumeLevel.value = Math.min(100, Math.floor((sum / (length / 10)) * 100 * 3)); } else { const int16Data = new Int16Array(data); length = int16Data.length; for (let i = 0; i < length; i += 10) { sum += Math.abs(int16Data[i]); } const avg = sum / (length / 10); volumeLevel.value = Math.min(100, Math.floor((avg / 10000) * 100)); } } onUnmounted(() => { if (isRecording.value) { stopRecording(); } }) return { isRecording, isSocketConnected, recordingDuration, volumeLevel, recognizedText, startRecording, stopRecording, cancelRecording } }