自己怎么开网站ae做动画教程网站
2026/2/9 5:42:58 网站建设 项目流程
自己怎么开网站,ae做动画教程网站,深圳网站建设制作开发公司,界首做网站Vue3 实时音频录制与转写 Composable 技术实现 前言 本文介绍如何基于 Vue3 Composition API 实现一个实时音频录制与转写的 Composable#xff0c;涉及 Web Audio API、WebSocket 实时通信、音频格式转换等技术。 技术栈 Vue3 Composition API: 组合式函数封装MediaRecorder …Vue3 实时音频录制与转写 Composable 技术实现前言本文介绍如何基于 Vue3 Composition API 实现一个实时音频录制与转写的 Composable涉及 Web Audio API、WebSocket 实时通信、音频格式转换等技术。技术栈Vue3 Composition API: 组合式函数封装MediaRecorder API: 浏览器音频录制Web Audio API: 音频流处理与格式转换WebSocket: 实时双向通信TypeScript: 类型安全核心功能实时音频录制支持暂停/继续/停止音频流实时处理与传输WebSocket 实时通信接收转写结果实时字幕逐句显示字幕定时保存机制技术架构┌─────────────────────────────────────────┐ │ useAudioRecorder Composable │ ├─────────────────────────────────────────┤ │ 1. 音频录制层 (MediaRecorder) │ │ 2. 音频处理层 (Web Audio API) │ │ 3. 实时通信层 (WebSocket) │ │ 4. 字幕处理层 (文本处理) │ │ 5. 数据持久化层 (定时保存) │ └─────────────────────────────────────────┘一、音频录制实现1.1 获取麦克风权限// 获取用户麦克风权限audioStreamawaitnavigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,// 回声消除noiseSuppression:true,// 降噪autoGainControl:true,// 自动增益控制},});技术要点getUserMedia返回MediaStream对象音频约束配置优化录音质量需要用户授权需要处理权限拒绝情况1.2 创建 MediaRecorder// 检测浏览器支持的音频格式constmimeTypeMediaRecorder.isTypeSupported(audio/webm;codecsopus)?audio/webm;codecsopus// 优先使用 Opus 编码:audio/webm;// 降级方案// 创建 MediaRecorder 实例mediaRecordernewMediaRecorder(audioStream,{mimeType});// 监听数据可用事件mediaRecorder.ondataavailable(event){if(event.data.size0){audioChunks.push(event.data);// 收集音频块}};// 开始录制每3秒触发一次 ondataavailablemediaRecorder.start(3000);技术要点MediaRecorder用于录制音频流start(timeslice)参数控制数据分片间隔音频数据以Blob格式存储二、音频处理与格式转换2.1 双音频流架构为了实现录音保存和实时转写并行需要创建两个独立的音频上下文// 音频流1用于录音保存原始采样率audioContextnewAudioContext();constanalyseraudioContext.createAnalyser();analyser.fftSize256;// 用于音频可视化audioContext.createMediaStreamSource(audioStream).connect(analyser);// 音频流2用于实时转写16000Hz采样率asrAudioContextnewAudioContext({sampleRate:16000});为什么需要双流录音保存需要高质量原始采样率实时转写需要标准采样率16000HzASR 标准两个流互不干扰独立处理2.2 音频格式转换Web Audio API 返回的是Float32Array范围 -1.0 到 1.0而 ASR 服务通常需要 16 位 PCM 格式范围 -32768 到 32767。/** * 将 Float32 音频转换为 16 位 PCM * param input Float32Array 音频数据 * returns Uint8Array 16位PCM数据 */constconvertTo16BitPCM(input:Float32Array):Uint8Array{constbuffernewArrayBuffer(input.length*2);// 16位 2字节constviewnewDataView(buffer);input.forEach((sample,i){// 限制范围到 [-1, 1]constvalueMath.max(-1,Math.min(1,sample));// 转换为16位整数// 负数: value * 0x8000 -32768// 正数: value * 0x7FFF 32767view.setInt16(i*2,value0?value*0x8000:value*0x7FFF,true// little-endian 字节序);});returnnewUint8Array(buffer);};转换原理Float32: 32位浮点数范围 [-1.0, 1.0]16位PCM: 16位整数范围 [-32768, 32767]使用线性映射PCM Float32 × 32767正数或Float32 × 32768负数2.3 音频分块处理使用ScriptProcessorNode处理音频流并按固定大小分块发送// 常量定义constPCM_CHUNK_SIZE6400;// 200ms音频数据// 计算16000Hz × 0.2s × 2字节 6400字节constASR_SAMPLE_RATE16000;// ASR标准采样率// 设置音频处理器constsetupAudioProcessor(){if(!wsClient?.isConnected()||!asrAudioContext||!audioStream)return;// 清空缓冲区audioBuffer[];// 创建音频源constasrSourceasrAudioContext.createMediaStreamSource(audioStream);// 创建脚本处理器已废弃但兼容性好// 参数bufferSize, numberOfInputChannels, numberOfOutputChannelsaudioProcessorasrAudioContext.createScriptProcessor(4096,1,1);// 连接音频节点asrSource.connect(audioProcessor);audioProcessor.connect(asrAudioContext.destination);// 处理音频数据audioProcessor.onaudioprocess(e){if(!wsClient?.isConnected())return;// 1. 获取输入音频数据Float32格式constinputDatae.inputBuffer.getChannelData(0);// 2. 转换为16位PCMconstpcmDataconvertTo16BitPCM(inputData);// 3. 累积到缓冲区audioBuffer.push(...pcmData);// 4. 达到200ms数据量时发送if(audioBuffer.lengthPCM_CHUNK_SIZE){constchunknewUint8Array(audioBuffer.slice(0,PCM_CHUNK_SIZE));// 5. 通过WebSocket发送二进制数据wsClient.send(chunk,false);// false: 不加入消息队列// 6. 移除已发送的数据audioBufferaudioBuffer.slice(PCM_CHUNK_SIZE);}};};分块策略块大小: 6400字节 200ms音频为什么200ms: 平衡实时性和网络效率太小网络请求频繁增加延迟太大实时性差用户体验不佳缓冲区管理: 使用数组累积达到阈值后发送并清空三、WebSocket 实时通信3.1 连接建立/** * 连接WebSocket接收实时转写结果 */constconnectWebSocketasync(recordId:string){try{constwsUrlwss://your-server.com/api/asr/realtime;consttokengetAuthToken();// 获取认证token// 使用封装的WebSocket客户端 你自己可以封装一个WebSocket的工具类wsClientcreateWebSocket({url:wsUrl,binaryType:arraybuffer,// 支持二进制数据heartbeatInterval:30000,// 30秒心跳reconnectInterval:3000,// 3秒重连间隔maxReconnectAttempts:10,// 最多重连10次headers:{Authorization:token?Bearer${token}:,},},{onOpen:(){console.log(WebSocket连接成功);},onMessage:(message){handleTranscriptionMessage(message);},onError:(event){console.error(WebSocket错误:,event);},onClose:(){console.log(WebSocket连接关闭);}});awaitwsClient.connect();}catch(error){console.error(WebSocket连接失败:,error);throwerror;}};技术要点使用 WSSWebSocket Secure保证传输安全配置心跳机制保持连接活跃自动重连机制处理网络波动Token 认证通过 URL 参数传递WebSocket 不支持自定义请求头3.2 消息处理/** * 处理转写消息 */consthandleTranscriptionMessage(message:WebSocketMessage){try{letresultmessage.data;// 1. 解析JSON消息if(typeofresultstring){// 错误检查if(result.includes(error)||result.includes(Error)||result.includes(timeout)){console.warn(收到错误消息:,result);return;}try{resultJSON.parse(result);}catch{return;// 解析失败忽略}}// 2. 检查错误字段if(result?.error){console.warn(WebSocket返回错误:,result.error);return;}// 3. 提取转写文本consttextresult?.result?.text||result?.text;if(text){// 更新完整字幕subtitles.valuetext;accumulatedText.valuetext;// 更新当前显示的字幕逐句显示updateCurrentSubtitle(text,currentSubtitle,lastDisplayedSentenceEnd);}}catch(error){console.error(处理转写消息失败:,error);}};四、实时字幕显示4.1 逐句显示逻辑实现智能的字幕逐句显示基于标点符号识别完整句子/** * 更新当前显示的字幕逐句显示 * param fullText 完整字幕文本 * param currentSubtitle 当前显示的字幕响应式引用 * param lastDisplayedSentenceEnd 已显示的最后一句的结束位置 */constupdateCurrentSubtitle(fullText:string,currentSubtitle:Refstring,lastDisplayedSentenceEnd:Refnumber){if(!fullText){currentSubtitle.value;return;}// 如果文本长度没有增加无需更新if(fullText.lengthlastDisplayedSentenceEnd.value){return;}// 获取新增的文本部分constnewTextfullText.substring(lastDisplayedSentenceEnd.value);// 尝试匹配完整句子以标点符号结尾constsentenceMatchnewText.match(/^[^。\n\.][。\n\.]/);if(sentenceMatch){// 找到完整句子constcompleteSentencesentenceMatch[0].trim();if(completeSentence){// 移除末尾多余的标点符号保留最后一个constdisplayTextcompleteSentence.replace(/[。\n\.]$/,(match)match[match.length-1]);currentSubtitle.valuedisplayText;// 更新已显示位置lastDisplayedSentenceEnd.valuelastDisplayedSentenceEnd.valuesentenceMatch[0].length;}}else{// 没有完整句子查找最后一个标点符号constlastPunctuationIndexMath.max(fullText.lastIndexOf(。),fullText.lastIndexOf(),fullText.lastIndexOf(),fullText.lastIndexOf(\n),fullText.lastIndexOf(.));if(lastPunctuationIndexlastDisplayedSentenceEnd.value){// 有新的标点符号显示标点符号之后的内容constafterLastPunctuationfullText.substring(lastPunctuationIndex1).trim();if(afterLastPunctuation){currentSubtitle.valueafterLastPunctuation;}}else{// 没有标点符号显示新文本部分constdisplayTextnewText.trim();if(displayText){currentSubtitle.valuedisplayText;}}}};显示策略完整句子优先: 识别以标点符号结尾的完整句子标点符号处理: 保留最后一个标点移除多余的增量更新: 只显示新增部分避免重复显示容错处理: 没有标点符号时显示新文本部分4.2 字幕状态管理// 响应式状态constsubtitlesref();// 完整字幕文本constcurrentSubtitleref();// 当前显示的字幕constaccumulatedTextref();// 累积文本constlastDisplayedSentenceEndref(0);// 已显示的最后一句的结束位置// 重置字幕状态constresetSubtitleState(){subtitles.value;currentSubtitle.value;accumulatedText.value;lastDisplayedSentenceEnd.value0;};六、完整生命周期管理6.1 开始录音conststartRecordingasync(){try{// 1. 初始化状态audioChunks[];resetSubtitleState();subtitleIdentification.valueuuidv4();// 2. 获取麦克风权限audioStreamawaitnavigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true,},});// 3. 创建音频上下文用于可视化audioContextnewAudioContext();constnewAnalyseraudioContext.createAnalyser();newAnalyser.fftSize256;audioContext.createMediaStreamSource(audioStream).connect(newAnalyser);analyser.valuenewAnalyser;// 4. 创建 MediaRecorderconstmimeTypeMediaRecorder.isTypeSupported(audio/webm;codecsopus)?audio/webm;codecsopus:audio/webm;mediaRecordernewMediaRecorder(audioStream,{mimeType});mediaRecorder.ondataavailable(event){if(event.data.size0){audioChunks.push(event.data);}};// 5. 连接 WebSockettry{awaitconnectWebSocket(currentRecordId.value);startSubtitleSaveTimer();// 6. 创建用于ASR的音频上下文if(wsClient?.isConnected()){asrAudioContextnewAudioContext({sampleRate:16000});setupAudioProcessor();}}catch(error){console.error(WebSocket连接失败继续录音但不显示实时字幕:,error);}// 7. 开始录音mediaRecorder.start(3000);isRecording.valuetrue;isPaused.valuefalse;recordingTime.value0;// 8. 开始计时conststartTimeDate.now();recordingTimersetInterval((){constelapsedMath.floor((Date.now()-startTime)/100);recordingTime.valueelapsed;},100);}catch(error){console.error(开始录音失败:,error);throwerror;}};6.2 暂停/继续录音/** * 暂停录音 */constpauseRecordingasync(){if(mediaRecorder?.state!recording)return;mediaRecorder.pause();isPaused.valuetrue;isRecording.valuefalse;// 停止计时器if(recordingTimer){clearInterval(recordingTimer);recordingTimernull;}// 停止音频处理器if(audioProcessor){audioProcessor.disconnect();audioProcessornull;}// 保存字幕并断开WebSocketif(subtitles.valuesubtitleIdentification.value){awaitsaveCurrentSubtitles();stopSubtitleSaveTimer();}if(wsClient){wsClient.disconnect();wsClientnull;}};/** * 继续录音 */constresumeRecordingasync(){if(!isPaused.value||!mediaRecorder)return;mediaRecorder.resume();isPaused.valuefalse;isRecording.valuetrue;// 重新连接WebSocketif(currentRecordId.value){try{awaitconnectWebSocket(currentRecordId.value);startSubtitleSaveTimer();// 重新设置音频处理器if(!asrAudioContextaudioStream){asrAudioContextnewAudioContext({sampleRate:16000});}setupAudioProcessor();}catch(error){console.error(WebSocket重连失败继续录音但不显示实时字幕:,error);}}// 恢复计时conststartTimeDate.now()-recordingTime.value*100;recordingTimersetInterval((){constelapsedMath.floor((Date.now()-startTime)/100);recordingTime.valueelapsed;},100);};6.3 停止录音与资源清理/** * 停止录音 */conststopRecordingasync(onStop?:(audioChunks:Blob[],recordId:string)Promisevoid){if(!mediaRecorder)return;isRecording.valuefalse;isPaused.valuefalse;// 停止计时器if(recordingTimer){clearInterval(recordingTimer);recordingTimernull;}// 设置停止事件处理mediaRecorder.onstopasync(){try{if(audioChunks.length0currentRecordId.valueonStop){awaitonStop(audioChunks,currentRecordId.value);}}finally{cleanup();}};// 停止录音并断开WebSocketmediaRecorder.stop();if(wsClient){wsClient.send(stop,false);// 延迟断开确保最后的消息能收到setTimeout((){if(wsClient){wsClient.disconnect();wsClientnull;}},500);}// 确保显示最后的内容if(subtitles.value){// 处理最后未完成的句子显示逻辑// ...}// 延迟重置状态确保用户能看到最后的内容setTimeout((){resetSubtitleState();},2000);};/** * 清理资源 */constcleanup(){// 停止音频流if(audioStream){audioStream.getTracks().forEach((track)track.stop());audioStreamnull;}// 关闭音频上下文if(audioContext){audioContext.close();audioContextnull;}if(asrAudioContext){asrAudioContext.close();asrAudioContextnull;}// 断开音频处理器if(audioProcessor){audioProcessor.disconnect();audioProcessornull;}analyser.valuenull;mediaRecordernull;// 清理WebSocketif(wsClient){wsClient.disconnect();wsClientnull;}// 停止字幕保存定时器stopSubtitleSaveTimer();// 清空音频块和缓冲区audioChunks[];audioBuffer[];};

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询