對接扣子雙向流式 TTS Demo

Web端對接Demo

<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>TTS 測試</title>
</head><body><h1>TTS 測試頁面</h1><textarea id="textInput" rows="4" cols="50">真正的成長,是學會接受自己的不完美。</textarea><br><button onclick="sendText()">發送文本</button><script>class PCMAudioPlayer {constructor(sampleRate) {this.sampleRate = sampleRate;this.audioContext = null;this.audioQueue = [];this.isPlaying = false;this.currentSource = null;const bufferThreshold = 2;}connect() {if (!this.audioContext) {this.audioContext = new (window.AudioContext || window.webkitAudioContext)();}}pushPCM(arrayBuffer) {this.audioQueue.push(arrayBuffer);this._playNextAudio();}/*** 將arrayBuffer轉為audioBuffer*/_bufferPCMData(pcmData) {const sampleRate = this.sampleRate; // 設置為 PCM 數據的采樣率const length = pcmData.byteLength / 2; // 假設 PCM 數據為 16 位,需除以 2const audioBuffer = this.audioContext.createBuffer(1, length, sampleRate);const channelData = audioBuffer.getChannelData(0);const int16Array = new Int16Array(pcmData); // 將 PCM 數據轉換為 Int16Arrayfor (let i = 0; i < length; i++) {// 將 16 位 PCM 轉換為浮點數 (-1.0 到 1.0)channelData[i] = int16Array[i] / 32768; // 16 位數據轉換范圍}let audioLength = length / sampleRate * 1000;console.log(`prepare audio: ${length} samples, ${audioLength} ms`)return audioBuffer;}async _playAudio(arrayBuffer) {if (this.audioContext.state === 'suspended') {await this.audioContext.resume();}const audioBuffer = this._bufferPCMData(arrayBuffer);this.currentSource = this.audioContext.createBufferSource();this.currentSource.buffer = audioBuffer;this.currentSource.connect(this.audioContext.destination);this.currentSource.onended = () => {console.log('Audio playback ended.');this.isPlaying = false;this.currentSource = null;this._playNextAudio(); // Play the next audio in the queue};this.currentSource.start();this.isPlaying = true;}_playNextAudio() {if (this.audioQueue.length > 0 && !this.isPlaying) {// 計算總的字節長度const totalLength = this.audioQueue.reduce((acc, buffer) => acc + buffer.byteLength, 0);const combinedBuffer = new Uint8Array(totalLength);let offset = 0;// 將所有 audioQueue 中的 buffer 拼接到一個新的 Uint8Array 中for (const buffer of this.audioQueue) {combinedBuffer.set(new Uint8Array(buffer), offset);offset += buffer.byteLength;}// 清空 audioQueue,因為我們已經拼接完所有數據this.audioQueue = [];// 發送拼接的 audio 數據給 playAudiothis._playAudio(combinedBuffer.buffer);}}stop() {if (this.currentSource) {this.currentSource.stop(); // 停止當前音頻播放this.currentSource = null; // 清除音頻源引用this.isPlaying = false; // 更新播放狀態}this.audioQueue = []; // 清空音頻隊列console.log('Playback stopped and queue cleared.');}}let player = new PCMAudioPlayer(24000);player.connect()player.stop()// WebSocket URL 根據實際API文檔填寫const socket = new WebSocket('wss://ws.coze.cn/v1/audio/speech?authorization=Bearer czs_l8r6XWz7Ogvh8diyHEyls4fnnsV4zPALaZQ019nI8yD8hB4wyDfmNeufVf3kckb6H');socket.onmessage = function (event) {try {const message = JSON.parse(event.data);if (message.event_type === 'speech.audio.update') {const audioData = atob(message.data.delta);console.log('audioData type ', typeof audioData);const arrayBuffer = Uint8Array.from(audioData, c => c.charCodeAt(0)).buffer;player.pushPCM(arrayBuffer)}} catch (error) {console.error('解析消息失敗:', error);}};function sendText() {const textInput = document.getElementById('textInput').value;if (textInput) {// 發送文本到WebSocket服務器let append = {"id": "event_id","event_type": "input_text_buffer.append","data": {"delta": textInput}}socket.send(JSON.stringify(append));let submitData = {"id": "event_id","event_type": "input_text_buffer.complete"}socket.send(JSON.stringify(submitData));} else {alert('請輸入要轉換為語音的文本');}}</script>
</body></html>

PCMAudioPlayer

上面 demo 中的 PCMAudioPlayer 源碼來自于阿里云TTS文檔,在coze上沒有找到怎么播放音頻的demo, 想到了阿里云在文檔方面做得比較好,結果真有。

下面是我用 AI 模型增加了一些代碼注釋,方便理解:

class PCMAudioPlayer {constructor(sampleRate) {this.sampleRate = sampleRate;      // 音頻采樣率(單位:Hz),需與PCM數據實際采樣率一致this.audioContext = null;          // Web Audio API上下文實例this.audioQueue = [];              // 存儲待播放的PCM數據緩沖區隊列this.isPlaying = false;            // 標識當前是否正在播放音頻this.currentSource = null;         // 當前播放的音頻源節點const bufferThreshold = 2;         // 未使用的緩沖區閾值(代碼中未實現邏輯)}// 初始化或恢復Web Audio上下文connect() {if (!this.audioContext) {// 創建音頻上下文,兼容舊版webkit前綴this.audioContext = new (window.AudioContext || window.webkitAudioContext)();}}// 將PCM數據推入隊列并嘗試播放pushPCM(arrayBuffer) {this.audioQueue.push(arrayBuffer);this._playNextAudio();  // 觸發播放邏輯}/*** 將16位有符號PCM數據轉換為Web Audio兼容的AudioBuffer* @param {ArrayBuffer} pcmData - 原始16位PCM數據* @returns {AudioBuffer} - 標準化音頻緩沖區對象*/_bufferPCMData(pcmData) {const sampleRate = this.sampleRate;const length = pcmData.byteLength / 2; // 計算采樣點數(16位=2字節)const audioBuffer = this.audioContext.createBuffer(1, length, sampleRate); // 創建單聲道緩沖區const channelData = audioBuffer.getChannelData(0);const int16Array = new Int16Array(pcmData);// 將16位有符號整數(-32768~32767)歸一化為浮點數(-1.0~1.0)for (let i = 0; i < length; i++) {channelData[i] = int16Array[i] / 32768;  // 32768=2^15(16位有符號最大值)}console.log(`準備音頻:${length}個采樣點,時長${length/sampleRate*1000}ms`);return audioBuffer;}// 播放單個音頻緩沖區async _playAudio(arrayBuffer) {if (this.audioContext.state === 'suspended') {await this.audioContext.resume();  // 恢復掛起的音頻上下文}const audioBuffer = this._bufferPCMData(arrayBuffer);this.currentSource = this.audioContext.createBufferSource();this.currentSource.buffer = audioBuffer;this.currentSource.connect(this.audioContext.destination);  // 連接到輸出設備// 播放結束事件處理this.currentSource.onended = () => {console.log('音頻播放結束');this.isPlaying = false;this.currentSource = null;this._playNextAudio();  // 播放下一個緩沖};this.currentSource.start();  // 啟動播放this.isPlaying = true;}// 處理音頻隊列播放邏輯_playNextAudio() {if (this.audioQueue.length > 0 && !this.isPlaying) {// 合并隊列中所有緩沖區(可能影響實時性,適用于非流式場景)const totalLength = this.audioQueue.reduce((acc, buf) => acc + buf.byteLength, 0);const combinedBuffer = new Uint8Array(totalLength);let offset = 0;this.audioQueue.forEach(buffer => {combinedBuffer.set(new Uint8Array(buffer), offset);offset += buffer.byteLength;});this.audioQueue = [];  // 清空隊列this._playAudio(combinedBuffer.buffer);  // 播放合并后的數據}}// 立即停止播放并清空隊列stop() {if (this.currentSource) {this.currentSource.stop();  // 中止當前音頻源this.currentSource = null;this.isPlaying = false;}this.audioQueue = [];console.log('播放已停止,隊列已清空');}
}

PCM技術詳解

參考音頻基礎知識及PCM技術詳解

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/71630.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/71630.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/71630.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

科普:“git“與“github“

Git與GitHub的關系可以理解為&#xff1a;Git是一種軟件工具&#xff0c;而GitHub則是一個在線平臺&#xff0c;它們是“一家子”。二者的關聯最直接體現在你通過Git在GitHub倉庫中clone軟件包到你的機器中來。 具體來說&#xff1a; 一、Git 定義&#xff1a;Git是一個開源的…

jsherp importItemExcel接口存在SQL注入

一、漏洞簡介 很多人說管伊佳ERP&#xff08;原名&#xff1a;華夏ERP&#xff0c;英文名&#xff1a;jshERP&#xff09;是目前人氣領先的國產ERP系統雖然目前只有進銷存財務生產的功能&#xff0c;但后面將會推出ERP的全部功能&#xff0c;有興趣請幫點一下 二、漏洞影響 …

【目標檢測】【BiFPN】EfficientDet:Scalable and Efficient Object Detection

EfficientDet&#xff1a;可擴展且高效的目標檢測 0.論文摘要 模型效率在計算機視覺中變得越來越重要。在本文中&#xff0c;我們系統地研究了用于目標檢測的神經網絡架構設計選擇&#xff0c;并提出了幾項關鍵優化以提高效率。首先&#xff0c;我們提出了一種加權雙向特征金…

拖動線條改變區域大小

瀏覽網頁時&#xff0c;經常看到這樣一個功能&#xff0c;可以通過拖拽線條&#xff0c;改變左右區域大小 在管理后臺中更為常見&#xff0c;菜單的寬度如果固定死&#xff0c;而后續新增的菜單名稱又不固定&#xff0c;所以很可能導致換行&#xff0c;樣式不太美觀&#xff0c…

輸入框元素覆蓋沖突

后端響應中的 "trainingKbGroupName": "基礎死型" 通過searchForm2.initFormData(rowData[0]);操作會把基礎死型四個字填充到<div class"col-sm-5 form-group"> <label class"col-sm-3 control-label">知識點分組名稱<…

【LLM】Llama 3 論文精讀

導言 Llama 3.5系列模型的發布&#xff1a; Llama 3.5系列模型是開源的&#xff0c;最大模型參數為405B&#xff08;[[稠密Transformer架構]]&#xff0c;而不是MOE 架構&#xff09;&#xff0c;上下文窗口長度為128K。模型支持多語言和工具使用&#xff0c;并且在某些評估中已…

selenium環境搭建

1. 安裝selenium pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple/如遇以下報錯 Getting requirements to build wheel ... errorerror: subprocess-exited-with-error Getting requirements to build wheel did not run successfully.│ exit code: 1╰─…

My first Android application

界面元素組成&#xff1a; 功能代碼&#xff1a; /*實現功能&#xff1a;當輸入內容后&#xff0c;歡迎文本發生相應改變&#xff0c;并清除掉文本域內容當未輸入任何內容時&#xff0c;彈出提示文本以警告用戶*/val greetingText findViewById<TextView>(R.id.printer)…

js版本ES6、ES7、ES8、ES9、ES10、ES11、ES12、ES13、ES14[2023]新特性

ES全稱ECMAScript,ECMAScript是ECMA制定的標準化腳本語言,本文講述Javascript[ECMAScript]版本ES6、ES7、ES8、ES9、ES10、ES11、ES12、ES13、ES14[2023]的新特性,幫助朋友們更好的熟悉和使用Javascript ES5 1.嚴格模式 use strict2.Object getPrototypeOf,返回一個對象的原…

Redis數據結構-String字符串

1.String字符串 字符串類型是Redis中最基礎的數據結構&#xff0c;關于數據結構與要特別注意的是&#xff1a;首先Redis中所有的鍵的類型都是字符串類型&#xff0c;而且其他集中數據結構也都是在字符串類似基礎上進行構建&#xff0c;例如列表和集合的元素類型是字符串類型&a…

cline通過硅基流動平臺接入DeepSeek-R1模型接入指南

為幫助您更高效、安全地通過硅基流動平臺接入DeepSeek-R1模型&#xff0c;以下為優化后的接入方案&#xff1a; DeepSeek-R1硅基流動平臺接入指南 &#x1f4cc; 核心優勢 成本低廉&#xff1a;注冊即送2000萬Tokens&#xff08;價值約14元&#xff09;高可用性&#xff1a;規…

Java多線程三:補充知識

精心整理了最新的面試資料&#xff0c;有需要的可以自行獲取 點擊前往百度網盤獲取 點擊前往夸克網盤獲取 Lambda表達式 簡介&#xff1a; 希臘字母表中排序第十一位的字母&#xff0c;英語名稱為Lambda避免匿名內部類定義過多其實質屬于函數式編程的概念 為什么要使用lam…

裝修流程圖: 裝修前準備 → 設計階段 → 施工階段 → 安裝階段 → 收尾階段 → 入住

文章目錄 引言I 毛坯房裝修的全流程**1. 裝修前準備****1.1 確定裝修預算****1.2 選擇裝修方式****1.3 選擇裝修公司****1.4 辦理裝修手續****2. 設計階段****2.1 量房****2.2 設計方案****2.3 確認方案****3. 施工階段****3.1 主體拆改****3.2 水電改造****3.3 防水工程****3.…

Embedding方法:從Word2Vec到ltem2Vec

引言 在推薦系統領域&#xff0c;如何有效表征物品特征始終是核心挑戰。傳統協同過濾方法受限于稀疏性問題&#xff0c;直到2016年微軟研究院提出的Item2Vec方法&#xff0c;將自然語言處理中的Word2Vec技術創造性應用于物品表征學習&#xff0c;開啟了嵌入學習的新紀元。 It…

Udp發送和接收數據(python和QT)

服務端代碼 (python) import socketdef udp_server(host0.0.0.0, port12345):# 創建一個UDP套接字sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# 綁定服務器的IP地址和端口號sock.bind((host, port))print(f"UDP服務器已啟動&#xff0c;監聽端口 {port}...&…

VBA腳本將DeepSeek嵌入Word中教程

一、獲取API-Key 目前我們可以直接只用官網的API來實現&#xff0c;申請這一步是關鍵 也可以直接訪問官網的API平臺&#xff1a;https://platform.deepseek.com/ &#xff0c;沒注冊的注冊完登錄一下&#xff0c;我們點擊到左側菜單的“APIKeys”按鈕&#xff0c;然后點擊右側…

DeepSeek接入Siri(已升級支持蘋果手表)完整版硅基流動DeepSeek-R1部署

DeepSeek接入Siri&#xff08;已升級支持蘋果手表&#xff09;完整版硅基流動DeepSeek-R1部署 **DeepSeek** 是一款專注于深度學習和人工智能的工具或平臺&#xff0c;通常與人工智能、機器學習、自動化分析等領域有關。它的主要功能可能包括&#xff1a;深度學習模型搜索&…

網站搭建基本流程

需求分析&#xff1a; 實現網站搭建的過程&#xff1a;首先進行網站的需求性分析 網站可分為前臺系統和后臺系統&#xff0c;由不同的功能拆分為不同的模塊 如下是一個電商網站可以拆分出的模塊&#xff1a; 在編寫代碼前&#xff0c;我們要先對網站進行架構&#xff0c;通過…

解決elementUi el-select 響應式不生效的問題

情況一,字段類型不匹配 考慮option的value值的字段類型是否和api返回的字段類型一致&#xff0c;如果一個為字符串一個為數字類型是無法匹配上的 <template> <div><el-select v-model"value" size"large"style"width: 240px"&…

QT實戰-基于QWidget實現的異形tip窗口

本文主要介紹了qt中,基于QWidget實現異形tip窗口的幾種實現方式,話不多說,先上圖, 1.使用QPainter和QPainterPath實現 代碼:tipwnd1.h #ifndef TIPWND1_H #define TIPWND1_H#include <QWidget>class TipWnd1 : public QWidget {Q_OBJECTQ_PROPERTY(QColor my_border…