ソースを参照

1、每个音频端只有第一个有WAV,流播放

liyanbo 10 ヶ月 前
コミット
cee7af18f2
2 ファイル変更55 行追加24 行削除
  1. 47 16
      src/components/TTS/useAudioPlayer.js
  2. 8 8
      src/views/AIDevelop.vue

+ 47 - 16
src/components/TTS/useAudioPlayer.js

@@ -1,23 +1,24 @@
-import { ref } from 'vue';
-
 export function useAudioPlayer() {
     let audioContext = null;
     let audioQueue = [];
     let isPlaying = false;
+    let currentTime = 0; // 当前播放时间(用于连续播放)
+    const SAMPLE_RATE = 16000; // 匹配后端采样率
+    const CHANNELS = 1; // 单声道
+    const BIT_DEPTH = 16; // 16位深
 
     // 初始化AudioContext
     const initAudioContext = () => {
         if (!audioContext) {
             audioContext = new (window.AudioContext || window.webkitAudioContext)({
-                sampleRate: 24000 // 匹配TTS采样率
+                sampleRate: SAMPLE_RATE
             });
+            currentTime = 0; // 重置播放时间
         }
     };
 
-    // 播放音频块
+    // 播放音频块(支持流式PCM)
     const playAudioChunk = async (base64Audio) => {
-
-        // console.log('playAudioChunk=========', base64Audio);
         initAudioContext();
 
         // 解码Base64音频数据
@@ -29,7 +30,7 @@ export function useAudioPlayer() {
         }
     };
 
-    // 处理音频队列
+    // 处理音频队列(核心流式播放逻辑)
     const processAudioQueue = async () => {
         if (audioQueue.length === 0) {
             isPlaying = false;
@@ -40,25 +41,55 @@ export function useAudioPlayer() {
         const audioData = audioQueue.shift();
 
         try {
-            const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
-            const source = audioContext.createBufferSource();
-            source.buffer = audioBuffer;
-            source.connect(audioContext.destination);
-            source.start(0);
-
-            // 播放完成后继续处理队列
-            source.onended = processAudioQueue;
+            // 1. 处理首个WAV分片(带文件头)
+            if (currentTime === 0) {
+                // 解码完整WAV文件(仅首次)
+                const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
+                playBuffer(audioBuffer);
+                currentTime += audioBuffer.duration; // 更新播放时间
+            }
+            // 2. 处理后续PCM分片(无文件头)
+            else {
+                // 将16位PCM字节转换为Float32Array(AudioContext要求格式)
+                const float32Data = convertPCMToFloat32(audioData);
+                // 创建音频缓冲区
+                const audioBuffer = audioContext.createBuffer(CHANNELS, float32Data.length, SAMPLE_RATE);
+                audioBuffer.copyToChannel(float32Data, 0); // 复制到音频通道
+                playBuffer(audioBuffer);
+                currentTime += audioBuffer.duration; // 更新播放时间
+            }
         } catch (error) {
-            console.error('音频解码失败:', error);
+            console.error('音频处理失败:', error);
             isPlaying = false;
         }
     };
 
+    // 播放音频缓冲区并调度下一个分片
+    const playBuffer = (audioBuffer) => {
+        const source = audioContext.createBufferSource();
+        source.buffer = audioBuffer;
+        source.connect(audioContext.destination);
+        source.start(currentTime); // 从当前时间点开始播放
+        // 播放结束后继续处理队列
+        source.onended = processAudioQueue;
+    };
+
+    // 将16位PCM字节转换为Float32Array([-1.0, 1.0]范围)
+    const convertPCMToFloat32 = (bytes) => {
+        const int16Array = new Int16Array(bytes.buffer);
+        const float32Array = new Float32Array(int16Array.length);
+        for (let i = 0; i < int16Array.length; i++) {
+            float32Array[i] = int16Array[i] / 32768; // 16位PCM最大值为32767
+        }
+        return float32Array;
+    };
+
     // 停止播放并清理
     const stopPlayback = () => {
         if (audioContext) {
             audioContext.close().then(() => {
                 audioContext = null;
+                currentTime = 0; // 重置播放时间
             });
         }
         audioQueue = [];

+ 8 - 8
src/views/AIDevelop.vue

@@ -451,14 +451,14 @@ onMounted(async () => {
         }
 
         // 手动修改第一个课程为image类型用于测试
-        if (index === 0) {
-          courseTemp.courseContentType = 'ppt';
-          courseTemp.pptPath = 'http://59.110.91.129:8088/admin-api/infra/file/29/get/20250820/ppt_1755654972861.pptx';
-          // courseTemp.courseContentType = 'image';
-          // courseTemp.courseImagePath = 'http://59.110.91.129:8088/admin-api/infra/file/4/get/20250715/one_1752549934393.png,http://59.110.91.129:8088/admin-api/infra/file/29/get/20250722/666_1753151547130.png';
-          // // 可选:修改课程名称以便识别
-          courseTemp.courseName = '测试';
-        }
+        // if (index === 0) {
+        //   courseTemp.courseContentType = 'ppt';
+        //   courseTemp.pptPath = 'http://59.110.91.129:8088/admin-api/infra/file/29/get/20250820/ppt_1755654972861.pptx';
+        //   // courseTemp.courseContentType = 'image';
+        //   // courseTemp.courseImagePath = 'http://59.110.91.129:8088/admin-api/infra/file/4/get/20250715/one_1752549934393.png,http://59.110.91.129:8088/admin-api/infra/file/29/get/20250722/666_1753151547130.png';
+        //   // // 可选:修改课程名称以便识别
+        //   courseTemp.courseName = '测试';
+        // }
 
         if (topName === courseTemp.courseLabel) {
           let topMenu = menuItems.value[menuItems.value.length - 1]