当前位置: 首页 > article >正文

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

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

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

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

以下是代码片段

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);
}

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; // 重置播放位置
};
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));
    }
  }
}

业务场景:


http://www.kler.cn/news/323715.html

相关文章:

  • 大数据毕业设计选题推荐-豆瓣电子图书推荐系统-数据分析-Hive-Hadoop-Spark
  • 【Anti-UAV410】论文阅读
  • Miniforge详细安装教程(macOs和Windows)
  • 尚品汇-自动化部署-Jenkins的安装与环境配置(五十六)
  • SpringBoot gateway如何支持跨域?
  • Spring的IOC和DI入门案例分析和实现
  • AWS注册时常见错误处理
  • RabbitMQ——消息的可靠性处理
  • Docker-Compose:简化Docker容器编排的利器
  • [vulnhub] Prime 1
  • 从哪里下载高清解压视频素材?推荐五个优质素材资源网站
  • RtspServer:轻量级RTSP服务器和推流器
  • 使用 PowerShell 命令更改 RDP 远程桌面端口(无需修改防火墙设置)
  • 以太网交换安全:端口隔离
  • 【C语言】sigemptyset、sigaddset、pthread_sigmask
  • 人工智能在医疗健康领域的应用
  • 【AI】深度学习的数学--核心公式
  • 使用世界领先的 Qwen2.5-Math 开源模型当 AI 数学老师,让奥数解题辅导不在鸡飞狗跳(文末有福利)
  • Three.js后期处理与着色器
  • 有问题未解决(9.28)
  • Git的安装 + 基本操作
  • ubuntu中库文件安装的位置以及头文件的位置
  • 123法则与2B法则
  • Linux——k8s组件
  • Qt实现自定义的文件对话框CustomFileDialog
  • 【HarmonyOS】应用权限原理和封装
  • 免费视频无损压缩工具+预览视频生成工具
  • Dubbo 如何使用 Zookeeper 作为注册中心:原理、优势与实现详解
  • 校园二手交易平台的小程序+ssm(lw+演示+源码+运行)
  • Vue ElemetUI table实现双击修改编辑某个内容