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技術詳解