亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

JS使用AudioContext實(shí)現(xiàn)音頻流實(shí)時(shí)播放

 更新時(shí)間:2024年01月10日 08:21:10   作者:hktk_wb  
這篇文章主要為大家詳細(xì)介紹了JavaScript如何使用AudioContext實(shí)現(xiàn)音頻流實(shí)時(shí)播放功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

使用場景

在項(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)文章

最新評論