JS使用AudioContext實(shí)現(xiàn)音頻流實(shí)時(shí)播放
使用場景
在項(xiàng)目中開發(fā)中,遇到這樣的需求:有一段文字,需要通過后臺接口轉(zhuǎn)成語音傳到前端進(jìn)行播放。 因?yàn)槲淖质菍?shí)時(shí)生成的,為保證實(shí)時(shí)性,需要在生成文字的過程中,轉(zhuǎn)為一段一段的音頻流通過websocket傳遞到前端,前端拿到音頻流后立即開始播放,接收到后續(xù)的音頻流后追加到播放音頻里繼續(xù)播放,達(dá)到實(shí)時(shí)生成文字,實(shí)時(shí)轉(zhuǎn)換音頻流,前端實(shí)時(shí)播放的效果。
解決方案
剛接到這個(gè)需求時(shí),想到的解決方案是這樣的
- 前端接收到音頻數(shù)據(jù)后放入緩存數(shù)組
- 檢測緩存數(shù)組中是否存在音頻數(shù)據(jù)
- 存在音頻數(shù)據(jù)則將音頻數(shù)據(jù)轉(zhuǎn)為Audio的src播放出來
- Audio播放完畢后轉(zhuǎn)到第2步繼續(xù)檢測
實(shí)現(xiàn)后發(fā)現(xiàn)在音頻傳遞過程中,每段音頻和文字的斷句并不一樣,兩段音頻斷在一個(gè)字的音中間,但是Audio的音頻解析到播放需要消耗時(shí)間,導(dǎo)致播放時(shí)會(huì)有卡頓的感覺。 后來了解到Web Audio API
中的AudioContext
接口可以處理音頻流數(shù)據(jù)并播放,就有了下面的方案。
- 創(chuàng)建
AudioContext
/MediaSource
接口實(shí)例 MediaSource
實(shí)例打開后創(chuàng)建sourceBuffer
,并監(jiān)聽update
事件- 接收到音頻流數(shù)據(jù)后查看
sourceBuffer
是否空閑 - 如果
sourceBuffer
處于空閑狀態(tài),則將音頻流追加到sourceBuffer
內(nèi)并開始播放 - 如果
sourceBuffer
處于工作狀態(tài),則將音頻流放入緩存數(shù)組待用 sourceBuffer
監(jiān)聽到update
事件后表示sourceBuffer
空閑,則檢測緩存數(shù)據(jù)是否有音頻數(shù)據(jù),如有則執(zhí)行第4步
音頻實(shí)時(shí)播放類
// 音頻實(shí)時(shí)播放 class AudioPlayer { mediaSource: MediaSource // 媒體資源 audio: HTMLAudioElement // 音頻元素 audioContext: AudioContext // 音頻上下文 sourceBuffer?: SourceBuffer // 音頻數(shù)據(jù)緩沖區(qū) cacheBuffers: ArrayBuffer[] = [] // 音頻數(shù)據(jù)列表 pauseTimer: number | null = null // 暫停定時(shí)器 constructor() { const AudioContext = window.AudioContext this.audioContext = new AudioContext() this.mediaSource = new MediaSource() this.audio = new Audio() this.audio.src = URL.createObjectURL(this.mediaSource) this.audioContextConnect() this.listenMedisSource() } // 連接音頻上下文 private audioContextConnect() { const source = this.audioContext.createMediaElementSource(this.audio) source.connect(this.audioContext.destination) } // 監(jiān)聽媒體資源 private listenMedisSource() { this.mediaSource.addEventListener('sourceopen', () => { if (this.sourceBuffer) return this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mpeg') this.sourceBuffer.addEventListener('update', () => { if (this.cacheBuffers.length && !this.sourceBuffer?.updating) { const cacheBuffer = this.cacheBuffers.shift()! this.sourceBuffer?.appendBuffer(cacheBuffer) } this.pauseAudio() }) }) } // 暫停音頻 private pauseAudio() { const neePlayTime = this.sourceBuffer!.timestampOffset - this.audio.currentTime || 0 this.pauseTimer && clearTimeout(this.pauseTimer) // 播放完成5秒后還沒有新的音頻流過來,則暫停音頻播放 this.pauseTimer = setTimeout(() => this.audio.pause(), neePlayTime * 1000 + 5000) } private playAudio() { // 為防止下一段音頻流傳輸過來時(shí),上一段音頻已經(jīng)播放完畢,造成音頻卡頓現(xiàn)象, // 這里做了1秒的延時(shí),可根據(jù)實(shí)際情況修正 setTimeout(() => { if (this.audio.paused) { try { this.audio.play() } catch (e) { this.playAudio() } } }, 1000) } // 接收音頻數(shù)據(jù) public receiveAudioData(audioData: ArrayBuffer) { if (!audioData.byteLength) return if (this.sourceBuffer?.updating) { this.cacheBuffers.push(audioData) } else { this.sourceBuffer?.appendBuffer(audioData) } this.playAudio() } } export default AudioPlayer
WebSocket 封裝
如果websocket需要支持心跳、重連等機(jī)制可以查看WebSocket 心跳檢測,斷開重連,消息訂閱 js/ts
const BASE_URL = import.meta.env.VITE_WS_BASE_URL type ObserverType<T> = { type: string callback: (data: T) => void } class SocketConnect<T> { private url: string public ws: WebSocket | undefined //websocket實(shí)例 private observers: ObserverType<T>[] = [] //消息訂閱者列表 private waitingMessages: string[] = [] //待執(zhí)行命令列表 private openCb?: () => void constructor(url = '', openCb?: () => void) { this.url = BASE_URL + url if (openCb) this.openCb = openCb this.connect() } //websocket連接 connect() { this.ws = new WebSocket(this.url) this.ws.onopen = () => { this.openCb && this.openCb() // 發(fā)送所有等待發(fā)送的信息 const length = this.waitingMessages.length for (let i = 0; i < length; ++i) { const message = this.waitingMessages.shift() this.send(message) } } this.ws.onclose = (event) => { console.log('webSocket closed:', event) } this.ws.onerror = (error) => { console.log('webSocket error:', error) } this.ws.onmessage = (event: MessageEvent) => { this.observers.forEach((observer) => { observer.callback(event.data) }) } } //發(fā)送信息 send(message?: string) { if (message) { //發(fā)送信息時(shí)若websocket還未連接,則將信息放入待發(fā)送信息中等待連接成功后發(fā)送 if (this.onReady() !== WebSocket.OPEN) { this.waitingMessages.push(message) return this } this.ws && this.ws.send(message) } return this } //訂閱webSocket信息 observe(callback: (data: T) => void, type = 'all') { const observer = { type, callback } this.observers.push(observer) return observer } //取消訂閱信息 cancelObserve(cancelObserver: ObserverType<T>) { this.observers.forEach((observer, index) => { if (cancelObserver === observer) { this.observers.splice(index, 1) } }) } // 關(guān)閉websocket close() { this.ws && this.ws.close() } // websocket連接狀態(tài) onReady() { return this.ws && this.ws.readyState } } export default SocketConnect
工具函數(shù)
// 從十六進(jìn)制字符串轉(zhuǎn)換為字節(jié)數(shù)組 export function hexStringToByteArray(hexString: string): Uint8Array { const byteArray: number[] = [] for (let i = 0; i < hexString.length; i += 2) { byteArray.push(parseInt(hexString.substring(i, i + 2), 16)) } return new Uint8Array(byteArray) } // 從字節(jié)數(shù)組轉(zhuǎn)換為 ArrayBuffer export function byteArrayToArrayBuffer(byteArray: Uint8Array): ArrayBuffer { const arrayBuffer = new ArrayBuffer(byteArray.length) const uint8Array = new Uint8Array(arrayBuffer) uint8Array.set(byteArray) return arrayBuffer } // 從十六進(jìn)制字符串轉(zhuǎn)換為 ArrayBuffer export function hexStringToArrayBuffer(hexString: string): ArrayBuffer { return byteArrayToArrayBuffer(hexStringToByteArray(hexString)) }
函數(shù)調(diào)用
const ws = new SocketConnect<string>('/audio') const audioPlayer = new AudioPlayer() ws.observe((data) => { console.log('receivebytes:'+new Date().getTime()) // 接收到的16進(jìn)制字符串?dāng)?shù)據(jù)轉(zhuǎn)換為ArrayBuffer傳遞給audioPlay const arrayBuffer = hexStringToArrayBuffer(data) audioPlayer.receiveAudioData(arrayBuffer) })
以上就是JS使用AudioContext實(shí)現(xiàn)音頻流實(shí)時(shí)播放的詳細(xì)內(nèi)容,更多關(guān)于JS AudioContext音頻流播放的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
前端微信H5公眾號實(shí)現(xiàn)授權(quán)登錄的方法總結(jié)
這篇文章主要介紹了如何在微信公眾號中實(shí)現(xiàn)網(wǎng)頁授權(quán)功能,包括創(chuàng)建微信服務(wù)公眾號、配置重定向地址、調(diào)試和開發(fā)使用等步驟,文中通過圖文及代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-01-01js漢字排序問題 支持中英文混排,兼容各瀏覽器,包括CHROME
這套排序機(jī)制同時(shí)兼容了IE和ff 可以實(shí)現(xiàn)所有瀏覽器下排序的統(tǒng)一哦2011-12-12ES6中Array.includes()函數(shù)的用法
這篇文章主要介紹了ES6中Array.includes()函數(shù)的用法,需要的朋友可以參考下2017-09-09js仿蘋果iwatch外觀的計(jì)時(shí)器代碼分享
這篇文章主要介紹了JS+CSS3實(shí)現(xiàn)的類似于蘋果iwatch計(jì)時(shí)器特效,很實(shí)用的代碼,推薦給大家,有需要的小伙伴可以參考下。2015-08-08JavaScript 解析數(shù)學(xué)表達(dá)式的過程詳解
這篇文章主要介紹了JavaScript 解析數(shù)學(xué)表達(dá)式的過程詳解,本文以一個(gè)的解題思路,來分享如何解決問題,解決的過程,可以作為解決工作中一般問題的通用思路,對js解析表達(dá)式相關(guān)知識感興趣的朋友一起看看吧2022-06-06