跳到主要内容

基于 Web Audio API(AudioContext)+ WebSocket 的实时音频播放(解码 + 预调度)

本文基于 react/react-template-demo/src/examples/realtime-audiocontext.tsx 的实现,解释“实时音频片段怎么在浏览器里无缝播放”,并把关键 API 和面试答题框架整理成可背诵的版本。


0. 面试速答(30 秒 TL;DR)

  • WebSocket 持续接收音频片段(示例里是 sequence + base64 的 JSON)。
  • AudioContext.decodeAudioData() 把片段解码成 AudioBuffer(前提:片段本身是浏览器支持的编码格式数据)。
  • AudioBufferSourceNode.start(when)AudioContext.currentTime 做“未来时间点播放”,并维护一个 lastScheduledTime 作为播放时间线,避免网络抖动导致的卡顿/间隙。
  • 关键机制:Look-ahead 预调度(例如提前 0.5s 把未来要播的片段都 schedule 掉)。
  • 关键风险:片段格式不适合 decodeAudioDatabase64 带宽/CPU 浪费、以及 暂停/恢复时已调度节点的管理

1. 心智模型:Web Audio 是“音频引擎 + 时间轴调度器”

<audio> 不同,Web Audio API 的核心是:

  • AudioContext 提供一个高精度的“音频时间轴”(currentTime
  • 你把每段音频做成一次性的 AudioBufferSourceNode,然后调用 start(when) 把它“钉”在时间线上
  • 只要你保证片段在时间线上连续、不重叠,听起来就像一条实时流

2. 代码在做什么:逐段对齐 realtime-audiocontext.tsx

2.1 初始化 AudioContext(兼容 webkit)

示例使用:

  • new (window.AudioContext || window.webkitAudioContext)()
  • 组件卸载时 audioContext.close()

面试要点:很多浏览器会把新建的 AudioContext 默认置为 suspended,需要用户手势后 resume() 才能出声。

2.2 片段缓存:Map + nextSequence

数据结构:

  • audioSegmentsRef: Map<number, AudioSegment>
  • nextSequenceToPlayRef: number

思路:把乱序/抖动先“吃进去”,播放侧只认 nextSequenceToPlay,有洞就等/跳过(示例里 decode 失败会直接跳过)。

2.3 解码:decodeAudioData(注意:不是万能解码器)

decodeAudioData(segment.data.slice(0)) 的两个关键点:

  • decodeAudioData 期待的是浏览器支持的音频文件/片段数据(例如某些 mp3/aac/ogg/wav 等);如果你推的是原始 PCM 或者“非完整/非自包含”的片段,可能会失败。
  • .slice(0) 是为了拷贝一份 ArrayBuffer,避免实现差异导致的“解码过程消费/分离(detach)原 buffer”问题。

2.4 播放连续性:lastScheduledTime + lookAheadTime

这段是核心:

  • currentTime = audioContext.currentTime
  • lookAheadTime = 0.5:只要“已调度到的末尾时间”还没覆盖到 currentTime + 0.5,就继续调度更多片段
  • startTime = max(lastScheduledTime, currentTime):保证不会把片段排到“过去”,也保证片段之间紧密衔接
  • setLastScheduledTime(startTime + audioBuffer.duration):推进时间线

本质是一个“生产者(网络)→ 缓冲 → 时间轴调度”的模型:

  • 网络到得快:你会调度出一个更长的“未来缓冲”
  • 网络抖动:只要抖动不超过你预留的 lookAheadTime,听感仍然连续

2.5 定时调度:100ms tick

示例用 setInterval(100ms)

  • 更新 UI:currentPlaybackTime = audioContext.currentTime
  • 持续调用 scheduleNextSegment() 进行“补调度”

工程上也可以改成 requestAnimationFrame 或用更明确的“每次收到新片段就触发一次 + update 事件节流”。

2.6 暂停/恢复:resume/suspend

  • 播放:若 state === 'suspended',先 resume();再 setIsPlaying(true) 并立即调度
  • 暂停:sourceNode.stop() + audioContext.suspend()

注意:示例只保存了一个 sourceNodeRef(最后一次创建的 source),而实际可能已经调度了多个 future source;suspend() 会统一暂停输出,但“恢复后的时间线/已调度节点是否还符合预期”是常见坑,面试可主动提。


3. API 思路速查(面试高频)

3.1 Web Audio API

  • AudioContext.currentTime:音频引擎时间轴(调度基准)
  • AudioContext.decodeAudioData(arrayBuffer):把编码音频解码成 AudioBuffer
  • AudioContext.createBufferSource():创建一次性播放节点(不能复用)
  • source.buffer = audioBuffer:挂载解码结果
  • source.connect(audioContext.destination):连接到输出
  • source.start(when):在“未来某一刻”开始播放(无缝拼接的关键)
  • audioContext.resume()/suspend()/close():控制生命周期与用户手势限制

3.2 WebSocket

  • ws.onmessage:收数据
  • ws.binaryType = 'arraybuffer':二进制传输建议直接用 ArrayBuffer(示例里仍用 JSON+base64,偏演示用)
  • 断线重连:onclose -> setTimeout(reconnect)

4. 典型面试题 & 标准答法

Q1:如何用 AudioContext 实现“实时流式播放”且不卡顿?

答题模板:

  1. 把每个音频片段解码成 AudioBuffer
  2. AudioBufferSourceNode.start(when) 把片段预先调度到 AudioContext 的时间轴上
  3. 维护 lastScheduledTime,保证片段在时间轴上连续衔接
  4. 做 look-ahead(例如提前 0.5s)抵抗网络抖动
  5. 用序号/队列做乱序缓冲与缺片策略(等/跳/补)

Q2:这种方案和 MediaSource(MSE)有什么区别?

  • MSE:更适合“长时播放的编码媒体流”(fMP4/WebM 等),浏览器自己做缓冲与解码,整体更像播放器管线
  • Web Audio:更适合“要做音频处理/混音/特效/可视化”的场景,你自己掌控调度与节点图
  • 但 Web Audio 的 decodeAudioData 对“片段格式”要求更苛刻;要极低延迟/原始 PCM 连续输出,通常会升级为 AudioWorklet + 环形缓冲

Q3:为什么示例要用 base64?更好的传输方式是什么?

  • base64 方便演示(JSON 可直传),但会带来 ~33% 体积膨胀 + 编解码 CPU 开销
  • 生产更推荐:直接发二进制 ArrayBuffer,并像 realtime-mediasource.tsx 那样把 sequence 放在前几个字节或单独的头部字段

5. 易错点 / 坑(面试加分)

  • decodeAudioData 不等于“流式解码”:片段必须是可解码的格式/边界;否则会失败或产生爆音/间隙。
  • 调度与状态要用 ref 更稳:示例的 lastScheduledTime 用 state,异步更新可能导致调度抖动;工程上常用 useRef 存时间线指针。
  • 暂停要处理已调度的 future nodes:只 stop 一个引用不够;要么记录所有已调度节点统一 stop,要么在恢复时重建时间线。
  • 缺片策略要明确:一直等洞会导致缓存无限增长;要有超时丢弃/静音填充/请求补发策略。
  • 自动播放策略:必须用用户手势触发 resume()/start()(示例用按钮是正确交互)。

6. 速记要点(背诵版)

  • Web Audio 实时播放 = 解码成 AudioBuffer + start(when) 预调度
  • 无缝关键 = lastScheduledTime(时间线指针)+ lookAhead(抵抗抖动)
  • 乱序处理 = sequence + Map/队列
  • 大坑 = decodeAudioData 对片段格式敏感、base64 开销、暂停/恢复的已调度节点管理