useAudioPlayer.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. export function useAudioPlayer() {
  2. let audioContext = null;
  3. let audioQueue = [];
  4. let isPlaying = false;
  5. let currentTime = 0; // 当前播放时间(用于连续播放)
  6. const TARGET_SAMPLE_RATE = 16000; // 目标采样率(匹配后端)
  7. const CHANNELS = 1; // 单声道
  8. const BIT_DEPTH = 16; // 16位深
  9. const FADE_DURATION = 0.01; // 淡入淡出持续时间(秒)
  10. let actualSampleRate = TARGET_SAMPLE_RATE; // 实际使用的采样率
  11. // 初始化AudioContext
  12. const initAudioContext = () => {
  13. if (!audioContext) {
  14. try {
  15. // 尝试创建指定采样率的AudioContext
  16. audioContext = new (window.AudioContext || window.webkitAudioContext)({
  17. sampleRate: TARGET_SAMPLE_RATE
  18. });
  19. } catch (e) {
  20. // 如果浏览器不支持指定的采样率,使用默认采样率
  21. console.warn(`浏览器不支持${TARGET_SAMPLE_RATE}Hz采样率,使用默认采样率:`, e);
  22. audioContext = new (window.AudioContext || window.webkitAudioContext)();
  23. }
  24. actualSampleRate = audioContext.sampleRate;
  25. console.log(`使用的音频采样率: ${actualSampleRate}Hz`);
  26. currentTime = 0; // 重置播放时间
  27. }
  28. };
  29. // 添加淡入淡出效果,减少音频块过渡时的突变噪声
  30. const applyFadeInOut = (audioBuffer) => {
  31. const channelData = audioBuffer.getChannelData(0);
  32. const fadeSamples = Math.floor(FADE_DURATION * audioBuffer.sampleRate);
  33. // 淡入效果
  34. for (let i = 0; i < fadeSamples && i < channelData.length; i++) {
  35. channelData[i] *= (i / fadeSamples);
  36. }
  37. // 淡出效果
  38. for (let i = channelData.length - fadeSamples; i < channelData.length; i++) {
  39. if (i >= 0) {
  40. channelData[i] *= ((channelData.length - i) / fadeSamples);
  41. }
  42. }
  43. };
  44. // 创建低通滤波器,减少高频噪声
  45. const createLowPassFilter = () => {
  46. const filter = audioContext.createBiquadFilter();
  47. filter.type = 'lowpass';
  48. filter.frequency.value = 4000; // 4kHz截止频率
  49. filter.Q.value = 0.707; // 巴特沃斯滤波器
  50. filter.connect(audioContext.destination);
  51. return filter;
  52. };
  53. // 播放音频块(支持流式PCM)
  54. const playAudioChunk = async (base64Audio) => {
  55. initAudioContext();
  56. // 解码Base64音频数据
  57. const audioBytes = Uint8Array.from(atob(base64Audio), c => c.charCodeAt(0));
  58. audioQueue.push(audioBytes);
  59. if (!isPlaying) {
  60. processAudioQueue();
  61. }
  62. };
  63. // 处理音频队列(核心流式播放逻辑)
  64. const processAudioQueue = async () => {
  65. if (audioQueue.length === 0) {
  66. isPlaying = false;
  67. return;
  68. }
  69. isPlaying = true;
  70. const audioData = audioQueue.shift();
  71. try {
  72. // 1. 处理首个WAV分片(带文件头)
  73. if (currentTime === 0) {
  74. // 解码完整WAV文件(仅首次)
  75. const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
  76. applyFadeInOut(audioBuffer); // 添加淡入淡出
  77. playBuffer(audioBuffer);
  78. currentTime += audioBuffer.duration; // 更新播放时间
  79. }
  80. // 2. 处理后续PCM分片(无文件头)
  81. else {
  82. // 确保数据是16位PCM格式
  83. if (audioData.length % 2 !== 0) {
  84. // 如果数据长度不是偶数,可能存在问题,丢弃最后一个字节
  85. audioData = audioData.slice(0, audioData.length - 1);
  86. }
  87. // 将16位PCM字节转换为Float32Array(AudioContext要求格式)
  88. const float32Data = convertPCMToFloat32(audioData);
  89. // 如果实际采样率与目标采样率不一致,进行重采样
  90. const resampledData = actualSampleRate !== TARGET_SAMPLE_RATE
  91. ? resampleAudio(float32Data, TARGET_SAMPLE_RATE, actualSampleRate)
  92. : float32Data;
  93. // 创建音频缓冲区
  94. const audioBuffer = audioContext.createBuffer(CHANNELS, resampledData.length, actualSampleRate);
  95. audioBuffer.copyToChannel(resampledData, 0); // 复制到音频通道
  96. applyFadeInOut(audioBuffer); // 添加淡入淡出效果
  97. playBuffer(audioBuffer);
  98. currentTime += audioBuffer.duration; // 更新播放时间
  99. }
  100. } catch (error) {
  101. console.error("音频处理失败:", error);
  102. isPlaying = false;
  103. }
  104. };
  105. // 重采样音频数据以匹配AudioContext的采样率
  106. const resampleAudio = (inputData, inputSampleRate, outputSampleRate) => {
  107. const inputLength = inputData.length;
  108. const outputLength = Math.floor(inputLength * (outputSampleRate / inputSampleRate));
  109. const outputData = new Float32Array(outputLength);
  110. for (let i = 0; i < outputLength; i++) {
  111. const inputIndex = i * (inputSampleRate / outputSampleRate);
  112. const index1 = Math.floor(inputIndex);
  113. const index2 = Math.min(index1 + 1, inputLength - 1);
  114. const fraction = inputIndex - index1;
  115. // 线性插值
  116. outputData[i] = inputData[index1] * (1 - fraction) + inputData[index2] * fraction;
  117. }
  118. return outputData;
  119. };
  120. // 播放音频缓冲区并调度下一个分片
  121. const playBuffer = (audioBuffer) => {
  122. const source = audioContext.createBufferSource();
  123. source.buffer = audioBuffer;
  124. // 添加低通滤波器减少噪声
  125. const filter = createLowPassFilter();
  126. source.connect(filter);
  127. // 确保从正确的时间点开始播放,处理可能的微小偏差
  128. const scheduledTime = Math.max(currentTime, audioContext.currentTime);
  129. source.start(scheduledTime);
  130. // 播放结束后继续处理队列
  131. source.onended = processAudioQueue;
  132. };
  133. // 将16位PCM字节转换为Float32Array([-1.0, 1.0]范围)
  134. const convertPCMToFloat32 = (bytes) => {
  135. // 创建DataView确保正确处理小端字节序
  136. const dataView = new DataView(bytes.buffer);
  137. const float32Array = new Float32Array(bytes.length / 2); // 16位PCM,每2字节一个样本
  138. for (let i = 0; i < float32Array.length; i++) {
  139. // 使用getInt16方法并指定littleEndian=true来确保正确的字节序
  140. const int16 = dataView.getInt16(i * 2, true);
  141. float32Array[i] = int16 / 32768; // 16位PCM最大值为32767,除以32768可得到[-1, 1)范围
  142. }
  143. return float32Array;
  144. };
  145. // 停止播放并清理
  146. const stopPlayback = () => {
  147. if (audioContext) {
  148. // 不关闭AudioContext,只重置播放状态
  149. currentTime = 0; // 重置播放时间
  150. }
  151. audioQueue = [];
  152. isPlaying = false;
  153. };
  154. return {
  155. playAudioChunk,
  156. stopPlayback
  157. };
  158. }