|
@@ -1,72 +1,103 @@
|
|
|
-import { ref } from 'vue';
|
|
|
|
|
-
|
|
|
|
|
export function useAudioPlayer() {
|
|
export function useAudioPlayer() {
|
|
|
- let audioContext = null;
|
|
|
|
|
- let audioQueue = [];
|
|
|
|
|
- let isPlaying = false;
|
|
|
|
|
|
|
+ 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采样率
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ // 初始化AudioContext
|
|
|
|
|
+ const initAudioContext = () => {
|
|
|
|
|
+ if (!audioContext) {
|
|
|
|
|
+ audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
|
|
|
+ sampleRate: SAMPLE_RATE
|
|
|
|
|
+ });
|
|
|
|
|
+ currentTime = 0; // 重置播放时间
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 播放音频块
|
|
|
|
|
- const playAudioChunk = async (base64Audio) => {
|
|
|
|
|
|
|
+ // 播放音频块(支持流式PCM)
|
|
|
|
|
+ const playAudioChunk = async (base64Audio) => {
|
|
|
|
|
+ initAudioContext();
|
|
|
|
|
|
|
|
- // console.log('playAudioChunk=========', base64Audio);
|
|
|
|
|
- initAudioContext();
|
|
|
|
|
|
|
+ // 解码Base64音频数据
|
|
|
|
|
+ const audioBytes = Uint8Array.from(atob(base64Audio), c => c.charCodeAt(0));
|
|
|
|
|
+ audioQueue.push(audioBytes);
|
|
|
|
|
|
|
|
- // 解码Base64音频数据
|
|
|
|
|
- const audioBytes = Uint8Array.from(atob(base64Audio), c => c.charCodeAt(0));
|
|
|
|
|
- audioQueue.push(audioBytes);
|
|
|
|
|
|
|
+ if (!isPlaying) {
|
|
|
|
|
+ processAudioQueue();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- if (!isPlaying) {
|
|
|
|
|
- processAudioQueue();
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ // 处理音频队列(核心流式播放逻辑)
|
|
|
|
|
+ const processAudioQueue = async () => {
|
|
|
|
|
+ if (audioQueue.length === 0) {
|
|
|
|
|
+ isPlaying = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 处理音频队列
|
|
|
|
|
- const processAudioQueue = async () => {
|
|
|
|
|
- if (audioQueue.length === 0) {
|
|
|
|
|
- isPlaying = false;
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ isPlaying = true;
|
|
|
|
|
+ const audioData = audioQueue.shift();
|
|
|
|
|
|
|
|
- isPlaying = true;
|
|
|
|
|
- const audioData = audioQueue.shift();
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 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);
|
|
|
|
|
+ isPlaying = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
|
|
|
|
|
- const source = audioContext.createBufferSource();
|
|
|
|
|
- source.buffer = audioBuffer;
|
|
|
|
|
- source.connect(audioContext.destination);
|
|
|
|
|
- source.start(0);
|
|
|
|
|
|
|
+ // 播放音频缓冲区并调度下一个分片
|
|
|
|
|
+ const playBuffer = (audioBuffer) => {
|
|
|
|
|
+ const source = audioContext.createBufferSource();
|
|
|
|
|
+ source.buffer = audioBuffer;
|
|
|
|
|
+ source.connect(audioContext.destination);
|
|
|
|
|
+ source.start(currentTime); // 从当前时间点开始播放
|
|
|
|
|
+ // 播放结束后继续处理队列
|
|
|
|
|
+ source.onended = processAudioQueue;
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 播放完成后继续处理队列
|
|
|
|
|
- source.onended = processAudioQueue;
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('音频解码失败:', error);
|
|
|
|
|
- isPlaying = false;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ // 将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;
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- audioQueue = [];
|
|
|
|
|
- isPlaying = false;
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ // 停止播放并清理
|
|
|
|
|
+ const stopPlayback = () => {
|
|
|
|
|
+ if (audioContext) {
|
|
|
|
|
+ audioContext.close().then(() => {
|
|
|
|
|
+ audioContext = null;
|
|
|
|
|
+ currentTime = 0; // 重置播放时间
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ audioQueue = [];
|
|
|
|
|
+ isPlaying = false;
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- return {
|
|
|
|
|
- playAudioChunk,
|
|
|
|
|
- stopPlayback
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ playAudioChunk,
|
|
|
|
|
+ stopPlayback
|
|
|
|
|
+ };
|
|
|
|
|
+}
|