JS使用AudioContext实现音频流实时播放

 更新时间:2024年01月10日 08:21:10   作者:hktk_wb  
这篇文章主要为大家详细介绍了JavaScript如何使用AudioContext实现音频流实时播放功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

使用场景

在项目中开发中,遇到这样的需求:有一段文字,需要通过后台接口转成语音传到前端进行播放。 因为文字是实时生成的,为保证实时性,需要在生成文字的过程中,转为一段一段的音频流通过websocket传递到前端,前端拿到音频流后立即开始播放,接收到后续的音频流后追加到播放音频里继续播放,达到实时生成文字,实时转换音频流,前端实时播放的效果。

解决方案

刚接到这个需求时,想到的解决方案是这样的

  • 前端接收到音频数据后放入缓存数组
  • 检测缓存数组中是否存在音频数据
  • 存在音频数据则将音频数据转为Audio的src播放出来
  • Audio播放完毕后转到第2步继续检测

实现后发现在音频传递过程中,每段音频和文字的断句并不一样,两段音频断在一个字的音中间,但是Audio的音频解析到播放需要消耗时间,导致播放时会有卡顿的感觉。 后来了解到Web Audio API中的AudioContext接口可以处理音频流数据并播放,就有了下面的方案。

  • 创建AudioContext/MediaSource接口实例
  • MediaSource实例打开后创建sourceBuffer,并监听update事件
  • 接收到音频流数据后查看sourceBuffer是否空闲
  • 如果sourceBuffer处于空闲状态,则将音频流追加到sourceBuffer内并开始播放
  • 如果sourceBuffer处于工作状态,则将音频流放入缓存数组待用
  • sourceBuffer监听到update事件后表示sourceBuffer空闲,则检测缓存数据是否有音频数据,如有则执行第4步

音频实时播放类

// 音频实时播放
class AudioPlayer {
  mediaSource: MediaSource // 媒体资源
  audio: HTMLAudioElement // 音频元素
  audioContext: AudioContext // 音频上下文
  sourceBuffer?: SourceBuffer // 音频数据缓冲区
  cacheBuffers: ArrayBuffer[] = [] // 音频数据列表
  pauseTimer: number | null = null // 暂停定时器

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

  // 监听媒体资源
  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() {
    // 为防止下一段音频流传输过来时,上一段音频已经播放完毕,造成音频卡顿现象,
    // 这里做了1秒的延时,可根据实际情况修正
    setTimeout(() => {
      if (this.audio.paused) {
        try {
          this.audio.play()
        } catch (e) {
          this.playAudio()
        }
      }
    }, 1000)
  }

  // 接收音频数据
  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需要支持心跳、重连等机制可以查看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实例
  private observers: ObserverType<T>[] = [] //消息订阅者列表
  private waitingMessages: string[] = [] //待执行命令列表
  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()
      // 发送所有等待发送的信息
      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)
      })
    }
  }

  //发送信息
  send(message?: string) {
    if (message) {
      //发送信息时若websocket还未连接,则将信息放入待发送信息中等待连接成功后发送
      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)
      }
    })
  }

  // 关闭websocket
  close() {
    this.ws && this.ws.close()
  }

  // websocket连接状态
  onReady() {
    return this.ws && this.ws.readyState
  }
}

export default SocketConnect

工具函数

// 从十六进制字符串转换为字节数组
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)
}

// 从字节数组转换为 ArrayBuffer
export function byteArrayToArrayBuffer(byteArray: Uint8Array): ArrayBuffer {
  const arrayBuffer = new ArrayBuffer(byteArray.length)
  const uint8Array = new Uint8Array(arrayBuffer)
  uint8Array.set(byteArray)
  return arrayBuffer
}

// 从十六进制字符串转换为 ArrayBuffer
export function hexStringToArrayBuffer(hexString: string): ArrayBuffer {
  return byteArrayToArrayBuffer(hexStringToByteArray(hexString))
}

函数调用

const ws = new SocketConnect<string>('/audio')

const audioPlayer = new AudioPlayer()

ws.observe((data) => {
    console.log('receivebytes:'+new Date().getTime())
    // 接收到的16进制字符串数据转换为ArrayBuffer传递给audioPlay
    const arrayBuffer = hexStringToArrayBuffer(data)
    audioPlayer.receiveAudioData(arrayBuffer)
})

以上就是JS使用AudioContext实现音频流实时播放的详细内容,更多关于JS AudioContext音频流播放的资料请关注脚本之家其它相关文章!

相关文章

  • 浅谈Sticky组件的改进实现

    浅谈Sticky组件的改进实现

    这篇文章主要介绍了Sticky组件的改进实现的相关资料,需要的朋友可以参考下
    2016-03-03
  • 前端JavaScript解决防盗链(Referer Check)图片加载问题的常用方法

    前端JavaScript解决防盗链(Referer Check)图片加载问题的常用方法

    防盗链(Referer Check)是服务器通过检查请求头中的 Referer 字段,来判断请求是否来自合法的来源,本文介绍了解决防盗链图片加载问题的几种常用方法,有需要的可以了解下
    2026-01-01
  • ElementUI的Dialog弹窗实现拖拽移动功能示例代码

    ElementUI的Dialog弹窗实现拖拽移动功能示例代码

    这篇文章主要介绍了ElementUI的Dialog弹窗实现拖拽移动功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07
  • JS解决IE使用JSON.stringify报JSON未定义错误的问题

    JS解决IE使用JSON.stringify报JSON未定义错误的问题

    在IE6,IE7,IE8这些老版本的浏览器中,原生并不支持JSON对象,这导致了在使用​​JSON.stringify​​时会遇到JSON未定义的错误,下面我们就来看看如何解决这一错误吧
    2025-06-06
  • JavaScript实现窗口抖动效果

    JavaScript实现窗口抖动效果

    抖动效果在各大网页上都常遇到,这篇文章主要介绍了JavaScript实现窗口抖动效果的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-10-10
  • javascript object array方法使用详解

    javascript object array方法使用详解

    在javascript开发中经常会使用到array中方法,本文将对其一一详细介绍,需要了解的朋友可以参考下
    2012-12-12
  • 微信小程序登录获取不到头像和昵称的详细解决办法

    微信小程序登录获取不到头像和昵称的详细解决办法

    相信好多小伙伴在使用getUserInfo获取小程序用户昵称和头像时却获取不到,下面这篇文章主要给大家介绍了关于微信小程序登录获取不到头像和昵称的详细解决办法,需要的朋友可以参考下
    2022-12-12
  • 如何在JavaScript中谨慎使用代码注释

    如何在JavaScript中谨慎使用代码注释

    这篇文章主要介绍了如何在JavaScript中谨慎使用代码注释,必要的注释可以阐明实现细节和设计意图,以此节约自己和别人的时间。 然而很多时候注释起的作用却适得其反,,需要的朋友可以参考下
    2019-06-06
  • 利用Javascript实现BMI计算器

    利用Javascript实现BMI计算器

    BMI指数计算器相信大家都用过,那用JavaScript怎么实现呢?其实很简单,这篇文章给出了实例代码,有需要的可以参考学习。
    2016-08-08
  • JS实现服务五星好评

    JS实现服务五星好评

    这篇文章主要为大家详细介绍了JS实现服务五星好评,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-09-09

最新评论