前端请求音频返回pcm流进行播放

业务场景是chat回答,点击播放则会将回答内容进行请求,返回音频数据流进行播放

实现方案,因为后端返回的是流式接口,但是流式接口我去截取后用自己完成的流式播放器方法进行播放会存在杂音,但是短句接口返回速度尚可,所以截取需要转音频的短句进行多次调用接口,返回的数据进行处理后存储下来,播放完上一段音频数据后即刻播放下一条。

注意返回的接口数据是pcm的base64编码格式

以下是代码片段

javascript 复制代码
if (type === "播放") {
      if (audioElement.value) {
        stopAudio();
      }
      currentIndex = 0;
      audioUrls = [];
      isPlay.forEach((item, index) => {
        isPlay[index] = false;
      });
      isPlay.push(true);
      // getSpeech(oldAnswer.replace(/[\n\t\s*]+/g, ""));
      let text = oldAnswer.replace(/[\n\t\s*]+/g, "");
      let arr = text.split("。").filter(Boolean);
      audioUrls = []; // 用于存储所有音频的 URL
      currentIndex = 0; // 当前应该播放的音频索引
      fetchAndPlayAudios(arr, isPlay.length - 1);
}
javascript 复制代码
let audioUrls = []; // 用于存储所有音频的 URL
let currentIndex = 0; // 当前应该播放的音频索引
let isPlay = [];
async function fetchAndPlayAudios(texts, isPlayIndex) {
  for (let index = 0; index < texts.length; index++) {
    console.log("index", index);
    if (!isPlay[isPlayIndex]) {
      break;
    }
    const res = await getSpeechAPI({
      input_text: texts[index],
      spk_id: "0",
    });
    // 解码 Base64 数据并存储 URL
    const audioUrl = pcmToAudioUrl(res);
    audioUrls.push(audioUrl);

    // 如果这是第一个音频,则立即播放它
    if (index === 0) {
      playNextAudio();
    }
    if (index == texts.length - 1) {
      isPlay[isPlayIndex] = false;
    }
  }
}
const audioElement = ref("");
function playNextAudio() {
  if (currentIndex < audioUrls.length) {
    audioElement.value = new Audio(audioUrls[currentIndex]);

    // 监听音频播放结束事件
    audioElement.value.addEventListener("ended", () => {
      audioElement.value.pause(); // 实际上在"ended"事件中,播放已经结束,但这行可以保留作为清晰性
      audioElement.value.currentTime = 0; // 重置播放位置
      currentIndex++; // 移动到下一个音频
      playNextAudio(); // 播放下一个音频(如果有的话)
    });

    audioElement.value.play();
  }
}

const stopAudio = () => {
  audioElement.value.pause(); // 实际上在"ended"事件中,播放已经结束,但这行可以保留作为清晰性
  audioElement.value.currentTime = 0; // 重置播放位置
};
javascript 复制代码
function pcmToAudioUrl(base64Data) {
  // console.log(base64Data)
  let pcmData = base64ToUint8Array(base64Data);
  // 创建WAV格式的Blob对象 (这是重点!直接创建blob数据是无法播放的!)
  const wavBlob = createWavBlob(pcmData);
  // 将URL设置为音频源即可
  return URL.createObjectURL(wavBlob);

  // base64编码的pcm16音频数据 转换为unit8格式数据
  function base64ToUint8Array(base64String) {
    const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
    const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }
  // 创建WAV格式的Blob对象 (调节采样率来微调语速和音调)
  function createWavBlob(pcmData) {
    const format = 1; // 格式代码(1表示PCM)
    const numChannels = 1; // 声道数量(单声道为1,立体声为2)
    const sampleRate = 26500; // 采样率(例如44100 Hz)
    const bitsPerSample = 16; // 每样本的位数(例如16位)
    const blockAlign = numChannels * (bitsPerSample / 8); // 对齐单位
    const byteRate = sampleRate * blockAlign; // 每秒的字节数
    const buffer = new ArrayBuffer(44 + pcmData.length); // WAV文件头部长度为44字节
    const view = new DataView(buffer);
    // 写入WAV文件头部信息
    writeString(view, 0, "RIFF"); // ChunkID
    view.setUint32(4, 36 + pcmData.length, true); // ChunkSize
    writeString(view, 8, "WAVE"); // Format
    writeString(view, 12, "fmt "); // Subchunk1ID
    view.setUint32(16, 16, true); // Subchunk1Size
    view.setUint16(20, format, true); // AudioFormat
    view.setUint16(22, numChannels, true); // NumChannels
    view.setUint32(24, sampleRate, true); // SampleRate
    view.setUint32(28, byteRate, true); // ByteRate
    view.setUint16(32, blockAlign, true); // BlockAlign
    view.setUint16(34, bitsPerSample, true); // BitsPerSample
    writeString(view, 36, "data"); // Subchunk2ID
    view.setUint32(40, pcmData.length, true); // Subchunk2Size
    // 将PCM数据写入buffer
    const pcmDataView = new Uint8Array(buffer, 44);
    pcmDataView.set(pcmData);
    return new Blob([view], { type: "audio/wav" });
  }
  // 写入字符串到DataView中的指定位置
  function writeString(view, offset, string) {
    for (let i = 0; i < string.length; i++) {
      view.setUint8(offset + i, string.charCodeAt(i));
    }
  }
}

业务场景:

相关推荐
开心工作室_kaic39 分钟前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā40 分钟前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年2 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder2 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727572 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
SoaringHeart3 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter
会发光的猪。3 小时前
css使用弹性盒,让每个子元素平均等分父元素的4/1大小
前端·javascript·vue.js
天下代码客3 小时前
【vue】vue中.sync修饰符如何使用--详细代码对比
前端·javascript·vue.js
猫爪笔记3 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
前端李易安4 小时前
Webpack 热更新(HMR)详解:原理与实现
前端·webpack·node.js