| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- export function useAudioPlayer() {
- let audioContext = null;
- let audioQueue = [];
- let isPlaying = false;
- let currentTime = 0; // 当前播放时间(用于连续播放)
- const TARGET_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`);
- 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();
- // 解码Base64音频数据
- const audioBytes = Uint8Array.from(atob(base64Audio), c => c.charCodeAt(0));
- audioQueue.push(audioBytes);
- if (!isPlaying) {
- processAudioQueue();
- }
- };
- // 处理音频队列(核心流式播放逻辑)
- const processAudioQueue = async () => {
- if (audioQueue.length === 0) {
- isPlaying = false;
- return;
- }
- isPlaying = true;
- const audioData = audioQueue.shift();
- try {
- // 1. 处理首个WAV分片(带文件头)
- 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); // 添加淡入淡出效果
- playBuffer(audioBuffer);
- currentTime += audioBuffer.duration; // 更新播放时间
- }
- } catch (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.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)范围
- }
- return float32Array;
- };
- // 停止播放并清理
- const stopPlayback = () => {
- if (audioContext) {
- // 不关闭AudioContext,只重置播放状态
- currentTime = 0; // 重置播放时间
- }
- audioQueue = [];
- isPlaying = false;
- };
- return {
- playAudioChunk,
- stopPlayback
- };
- }
|