|
|
@@ -3,59 +3,20 @@ export function useAudioPlayer() {
|
|
|
let audioQueue = [];
|
|
|
let isPlaying = false;
|
|
|
let currentTime = 0; // 当前播放时间(用于连续播放)
|
|
|
- const TARGET_SAMPLE_RATE = 16000; // 目标采样率(匹配后端)
|
|
|
+ const SAMPLE_RATE = 16000; // 匹配后端采样率
|
|
|
const CHANNELS = 1; // 单声道
|
|
|
const BIT_DEPTH = 16; // 16位深
|
|
|
- const FADE_DURATION = 0.01; // 淡入淡出持续时间(秒)
|
|
|
- let actualSampleRate = TARGET_SAMPLE_RATE; // 实际使用的采样率
|
|
|
|
|
|
// 初始化AudioContext
|
|
|
const initAudioContext = () => {
|
|
|
if (!audioContext) {
|
|
|
- try {
|
|
|
- // 尝试创建指定采样率的AudioContext
|
|
|
- audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
|
- sampleRate: TARGET_SAMPLE_RATE
|
|
|
- });
|
|
|
- } catch (e) {
|
|
|
- // 如果浏览器不支持指定的采样率,使用默认采样率
|
|
|
- console.warn(`浏览器不支持${TARGET_SAMPLE_RATE}Hz采样率,使用默认采样率:`, e);
|
|
|
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
- }
|
|
|
- actualSampleRate = audioContext.sampleRate;
|
|
|
- console.log(`使用的音频采样率: ${actualSampleRate}Hz`);
|
|
|
+ audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
|
+ sampleRate: SAMPLE_RATE
|
|
|
+ });
|
|
|
currentTime = 0; // 重置播放时间
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // 添加淡入淡出效果,减少音频块过渡时的突变噪声
|
|
|
- const applyFadeInOut = (audioBuffer) => {
|
|
|
- const channelData = audioBuffer.getChannelData(0);
|
|
|
- const fadeSamples = Math.floor(FADE_DURATION * audioBuffer.sampleRate);
|
|
|
-
|
|
|
- // 淡入效果
|
|
|
- for (let i = 0; i < fadeSamples && i < channelData.length; i++) {
|
|
|
- channelData[i] *= (i / fadeSamples);
|
|
|
- }
|
|
|
-
|
|
|
- // 淡出效果
|
|
|
- for (let i = channelData.length - fadeSamples; i < channelData.length; i++) {
|
|
|
- if (i >= 0) {
|
|
|
- channelData[i] *= ((channelData.length - i) / fadeSamples);
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 创建低通滤波器,减少高频噪声
|
|
|
- const createLowPassFilter = () => {
|
|
|
- const filter = audioContext.createBiquadFilter();
|
|
|
- filter.type = 'lowpass';
|
|
|
- filter.frequency.value = 4000; // 4kHz截止频率
|
|
|
- filter.Q.value = 0.707; // 巴特沃斯滤波器
|
|
|
- filter.connect(audioContext.destination);
|
|
|
- return filter;
|
|
|
- };
|
|
|
-
|
|
|
// 播放音频块(支持流式PCM)
|
|
|
const playAudioChunk = async (base64Audio) => {
|
|
|
initAudioContext();
|
|
|
@@ -84,96 +45,52 @@ export function useAudioPlayer() {
|
|
|
if (currentTime === 0) {
|
|
|
// 解码完整WAV文件(仅首次)
|
|
|
const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
|
|
|
- applyFadeInOut(audioBuffer); // 添加淡入淡出
|
|
|
playBuffer(audioBuffer);
|
|
|
currentTime += audioBuffer.duration; // 更新播放时间
|
|
|
}
|
|
|
// 2. 处理后续PCM分片(无文件头)
|
|
|
else {
|
|
|
- // 确保数据是16位PCM格式
|
|
|
- if (audioData.length % 2 !== 0) {
|
|
|
- // 如果数据长度不是偶数,可能存在问题,丢弃最后一个字节
|
|
|
- audioData = audioData.slice(0, audioData.length - 1);
|
|
|
- }
|
|
|
-
|
|
|
// 将16位PCM字节转换为Float32Array(AudioContext要求格式)
|
|
|
const float32Data = convertPCMToFloat32(audioData);
|
|
|
-
|
|
|
- // 如果实际采样率与目标采样率不一致,进行重采样
|
|
|
- const resampledData = actualSampleRate !== TARGET_SAMPLE_RATE
|
|
|
- ? resampleAudio(float32Data, TARGET_SAMPLE_RATE, actualSampleRate)
|
|
|
- : float32Data;
|
|
|
-
|
|
|
// 创建音频缓冲区
|
|
|
- const audioBuffer = audioContext.createBuffer(CHANNELS, resampledData.length, actualSampleRate);
|
|
|
- audioBuffer.copyToChannel(resampledData, 0); // 复制到音频通道
|
|
|
-
|
|
|
- applyFadeInOut(audioBuffer); // 添加淡入淡出效果
|
|
|
+ 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;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // 重采样音频数据以匹配AudioContext的采样率
|
|
|
- const resampleAudio = (inputData, inputSampleRate, outputSampleRate) => {
|
|
|
- const inputLength = inputData.length;
|
|
|
- const outputLength = Math.floor(inputLength * (outputSampleRate / inputSampleRate));
|
|
|
- const outputData = new Float32Array(outputLength);
|
|
|
-
|
|
|
- for (let i = 0; i < outputLength; i++) {
|
|
|
- const inputIndex = i * (inputSampleRate / outputSampleRate);
|
|
|
- const index1 = Math.floor(inputIndex);
|
|
|
- const index2 = Math.min(index1 + 1, inputLength - 1);
|
|
|
- const fraction = inputIndex - index1;
|
|
|
-
|
|
|
- // 线性插值
|
|
|
- outputData[i] = inputData[index1] * (1 - fraction) + inputData[index2] * fraction;
|
|
|
- }
|
|
|
-
|
|
|
- return outputData;
|
|
|
- };
|
|
|
-
|
|
|
// 播放音频缓冲区并调度下一个分片
|
|
|
const playBuffer = (audioBuffer) => {
|
|
|
const source = audioContext.createBufferSource();
|
|
|
source.buffer = audioBuffer;
|
|
|
-
|
|
|
- // 添加低通滤波器减少噪声
|
|
|
- const filter = createLowPassFilter();
|
|
|
- source.connect(filter);
|
|
|
-
|
|
|
- // 确保从正确的时间点开始播放,处理可能的微小偏差
|
|
|
- const scheduledTime = Math.max(currentTime, audioContext.currentTime);
|
|
|
- source.start(scheduledTime);
|
|
|
-
|
|
|
+ source.connect(audioContext.destination);
|
|
|
+ source.start(currentTime); // 从当前时间点开始播放
|
|
|
// 播放结束后继续处理队列
|
|
|
source.onended = processAudioQueue;
|
|
|
};
|
|
|
|
|
|
// 将16位PCM字节转换为Float32Array([-1.0, 1.0]范围)
|
|
|
const convertPCMToFloat32 = (bytes) => {
|
|
|
- // 创建DataView确保正确处理小端字节序
|
|
|
- const dataView = new DataView(bytes.buffer);
|
|
|
- const float32Array = new Float32Array(bytes.length / 2); // 16位PCM,每2字节一个样本
|
|
|
-
|
|
|
- for (let i = 0; i < float32Array.length; i++) {
|
|
|
- // 使用getInt16方法并指定littleEndian=true来确保正确的字节序
|
|
|
- const int16 = dataView.getInt16(i * 2, true);
|
|
|
- float32Array[i] = int16 / 32768; // 16位PCM最大值为32767,除以32768可得到[-1, 1)范围
|
|
|
+ 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,只重置播放状态
|
|
|
- currentTime = 0; // 重置播放时间
|
|
|
+ audioContext.close().then(() => {
|
|
|
+ audioContext = null;
|
|
|
+ currentTime = 0; // 重置播放时间
|
|
|
+ });
|
|
|
}
|
|
|
audioQueue = [];
|
|
|
isPlaying = false;
|