前端音频处理架构

架构总览

graph TB
    subgraph capture [音频采集]
        MIC["麦克风\ngetUserMedia()"]
        CTX["AudioContext\n(16kHz)"]
        WL["AudioWorkletNode\ncapture-processor"]
        MIC --> CTX --> WL
    end

    subgraph transport [传输]
        WS["WebSocket\naudio_base64"]
    end

    subgraph playback [音频播放]
        AP["AudioPlayer"]
        RESAMP["重采样\n24kHz → 设备采样率"]
        BUF["AudioBufferSourceNode\n预调度播放"]
        AP --> RESAMP --> BUF
    end

    subgraph analysis [音频分析]
        LUFS["LUFS 测量\nITU-R BS.1770"]
        MIXER["MixerController\n自动增益"]
        LUFS --> MIXER
    end

    subgraph recording [录制]
        SREC["SessionRecorder\n立体声 WAV"]
        VREC["SessionVideoRecorder\n视频 + 音频"]
    end

    WL -->|"Float32 chunk (1s)"| WS
    WS -->|"Base64 audio (24kHz)"| AP
    AP --> SREC
    AP --> VREC
    BUF --> LUFS

capture-processor.js — AudioWorklet 音频采集

运行在 Web Audio 渲染线程的 AudioWorkletProcessor,实现低延迟音频捕获。

工作原理

process(inputs, outputs) {
    // 1. 直通到输出(用于 MediaStreamDestination)
    output.set(input);
    // 2. 累积到 _buffer
    _buffer = concat(_buffer, input);
    // 3. buffer 满一个 chunk 时发送
    while (_buffer.length >= _chunkSize) {
        const chunk = _buffer.slice(0, _chunkSize);
        _buffer = _buffer.slice(_chunkSize);
        port.postMessage({type: 'chunk', audio: chunk}, [chunk.buffer]);
    }
}

配置

参数 默认值 说明
chunkSize 16000 每个 chunk 的样本数
采样率 16000 Hz 由 AudioContext sampleRate 决定
chunk 时长 1 秒 chunkSize / sampleRate

MessagePort 通信

接收命令: - {command: 'start'} — 开始累积和发送 chunk - {command: 'stop'} — 停止,发送剩余 buffer(final: true

发送消息: - {type: 'chunk', audio: Float32Array} — 正常 chunk - {type: 'chunk', audio: Float32Array, final: true} — 最后一个 chunk

使用 Transferable 对象([chunk.buffer])实现零拷贝传输。


audio-player.js — AI 音频实时播放器

AudioPlayer 类管理从服务端接收的 AI 音频的实时无间隙播放。

完整 API

方法 说明
init() 初始化 AudioContext
beginTurn() 开始新的说话轮次(重置调度时间)
playChunk(base64Data, arrivalTime) 入队并调度一个音频 chunk
endTurn() 结束当前轮次
stopAll() 立即停止所有播放(强制监听时使用)
stop() 完整停止并清理
属性 说明
turnActive 是否处于说话轮次
playing 是否正在播放
gapCount 间隙总数
totalShiftMs 总漂移时间
lastAheadMs 最后的前置缓冲时间

播放流程

sequenceDiagram
    participant S as 服务端
    participant AP as AudioPlayer
    participant ACX as AudioContext

    S->>AP: playChunk(base64)
    AP->>AP: Base64 → Float32Array
    AP->>AP: 重采样 24kHz → 设备采样率
    AP->>AP: 创建 AudioBuffer

    alt 首个 chunk + 配置了延迟
        AP->>AP: setTimeout(playbackDelay)
        Note over AP: 等待更多 chunk 到达避免间隙
    end

    AP->>ACX: bufferSource.start(nextTime)
    AP->>AP: nextTime += buffer.duration

    Note over AP: 后续 chunk 紧密拼接
    S->>AP: playChunk(base64)
    AP->>ACX: bufferSource.start(nextTime)

间隙检测

nextTime < AudioContext.currentTime 时检测到间隙(buffer underrun):

gapMs = (currentTime - nextTime) * 1000
if gapMs > 10ms:
    gapCount++
    totalShiftMs += gapMs
    nextTime = currentTime + 小偏移    // 修正
    触发 onGap 回调

间隙通常由网络延迟或推理速度慢导致。

播放延迟

通过 getPlaybackDelayMs() 配置(默认 200ms,对应 config.json 中的 playback_delay_ms)。

回调


lufs.js — LUFS 响度测量

实现 ITU-R BS.1770 集成响度测量算法。

算法步骤

  1. K 加权滤波:两级 IIR 滤波器(高通 + 高频增益),模拟人耳频率感知
  2. 分块均方值:将信号分为 400ms 重叠块,计算每块的均方值
  3. 绝对门限:去除低于 -70 LUFS 的静音块
  4. 相对门限:计算剩余块的平均值,去除低于平均值 -10 dB 的块
  5. 集成响度:计算最终块的加权平均 → LUFS 值

用途


mixer-controller.js — 混音器控制

MixerController 提供双通道音频混音控制界面。

功能

功能 说明
实时 LUFS 计量 分别显示用户和 AI 音频的实时响度
自动增益 基于 LUFS 自动调整 AI 音频音量
独立音量控制 用户/AI 音量滑块独立调节
可拖动面板 浮动面板 UI,可拖拽到任意位置

duplex-utils.js — 工具函数

函数 说明
resampleAudio(input, srcRate, dstRate) 线性插值重采样
float32ToBase64(float32Array) Float32 数组 → Base64 字符串
base64ToFloat32(base64) Base64 字符串 → Float32 数组

重采样逻辑:计算采样率比值,按线性插值对每个目标样本点取两个最近源样本点的加权平均。


stereo-recorder-processor.js — 立体声处理器

AudioWorkletProcessor 实现,用于 SessionRecorder 的立体声录制: - 接收两路音频输入(用户 + AI) - 交织为立体声帧(左 = 用户,右 = AI) - 通过 MessagePort 发送立体声 PCM 数据


queue-chimes.js — 排队音效

使用 Web Audio API 合成排队状态音效: - 入队时:低音提示 - 排队完成时:高音提示 - 纯合成音,无外部音频文件依赖