音频采集与处理
系统介绍 Web 语音系统的音频采集与处理流程,涵盖声音数字化、浏览器音频采集、音频处理及实时传输等核心技术。

概述
在 Web 语音系统中,用户说出的声音并不能直接交给语音识别模型处理。现实中的声音首先需要经过麦克风拾取和模数转换形成数字音频,随后由浏览器获取麦克风产生的实时媒体流,并根据后续处理需求进一步完成音频数据读取、格式规范化和音频增强等操作。
从整体链路来看,浏览器中的音频输入通常经历以下过程:

其中,声音数字化解决的是现实声音如何转换为计算机能够处理的数字信号;浏览器音频采集解决的是网页如何在获得用户授权后访问麦克风,并取得持续产生的实时媒体流;音频数据处理则进一步从媒体流中读取实际的音频数据,并根据后续语音服务的要求进行编码与解码、重采样、声道转换和采样格式转换等格式规范化处理,同时还可以通过降噪、回声消除、自动增益控制等方式对音频进行增强,以提高输入音频的质量。完成这些处理后,音频数据即可通过 HTTP、WebSocket 或其他实时通信机制发送至服务端,供后续语音识别等环节使用。
声音数字化
声音进入 Web 应用之前,首先需要完成从物理声波到数字音频数据的转换。
现实环境中的声音本质上是空气压力随时间产生的连续变化。当用户说话时,声波推动麦克风内部的振膜振动,麦克风再通过相应的换能机制将这种机械振动转换为随时间变化的模拟电信号。
此时得到的信号仍然是连续的,计算机不能直接按照这种形式进行存储和运算。因此,模拟电信号还需要经过模数转换(Analog-to-Digital Conversion,ADC),将连续变化的信号转换为一系列离散的数字采样值。
数字化过程主要涉及两个步骤:采样(Sampling)和量化(Quantization)。

采样
采样(Sampling)是在固定时间间隔上测量模拟信号的瞬时幅值,从而将时间上连续的信号转换为离散的采样点。
采样频率通常使用采样率(Sample Rate)表示,单位为 Hz。例如:
8 kHz → 每秒 8000 个采样点
16 kHz → 每秒 16000 个采样点
48 kHz → 每秒 48000 个采样点
采样率决定数字音频在时间维度上的采样密度,也影响能够表示的最高频率范围。
为了在理想条件下避免频谱混叠,采样率应高于信号中最高频率的两倍。如果采样率不足,高频信号可能被错误映射到较低频率范围,导致采样结果无法准确表示原始信号。
不同音频场景通常会采用不同的采样率。例如,传统电话语音常采用 8 kHz 左右的采样率,许多语音识别系统会使用 16 kHz 音频,而通用音频设备、浏览器音频系统和实时媒体场景中经常可以看到 44.1 kHz 或 48 kHz。
采样率并不是越高越好。更高的采样率能够保留更宽的频率范围,但同时也意味着更大的数据量和更高的计算、存储与传输开销。实际系统通常需要根据语音质量、设备能力和后续模型要求进行选择。
量化
完成采样后,系统已经在时间轴上获得了一系列离散采样点,但每个采样点的幅值仍然来源于连续范围。为了使用有限位数的二进制数据表示这些幅值,还需要进行量化(Quantization)。
量化会将采样得到的幅值映射到有限数量的离散等级,并使用数字记录对应结果。
简单来说:
采样 → 决定“什么时候记录”
量化 → 决定“每次记录的幅值用多高精度表示”
量化精度通常使用位深(Bit Depth)描述。例如:
- 8 bit 可以提供 256 个离散等级;
- 16 bit 可以提供 65536 个离散等级;
- 24 bit 可以提供更高的幅值分辨率和动态范围。
在常见的 16 位有符号整数 PCM 中,一个采样值通常使用 Int16 表示,其范围约为 -32768 ~ 32767。
由于实际幅值被映射到了有限的离散等级,量化后的数值与原始模拟幅值之间可能存在一定差异,这种差异通常称为量化误差。提高位深可以增加可用的量化等级,但也会增加单位时间内的数据量。
声道
除了采样率和位深,数字音频还需要关注声道数(Channel Count)。
单声道音频每个采样时刻只包含一组采样值,而双声道音频通常包含左右两个声道的数据:
单声道
L L L L L ...
双声道
L R L R L R ...
语音识别通常更关注说话内容本身,因此很多语音识别接口会使用单声道输入;录音、音乐和空间音频等场景则可能需要双声道或更多声道。
如果浏览器采集到的是多声道音频,而后续语音服务只接受单声道,就需要在进入服务之前进行声道合并或重新组织。
PCM
经过采样和量化后,声音已经被转换为按照时间顺序排列的一组数字采样值。最常见的表示方式之一是脉冲编码调制(Pulse Code Modulation,PCM)。
PCM 描述的是数字音频采样值如何表示,本质上是一系列按照时间顺序排列的采样数据。
例如,一段 16 位单声道 PCM 可以简单理解为:
102
340
512
420
-80
-310
-460
...
这些值描述声音振幅随时间的变化。
在实际接口中,经常还能看到pcm_s16le这样的格式名称,其中:
pcm → PCM 音频
s16 → 16 位有符号整数
le → Little Endian,小端字节序
因此,pcm_s16le 并不是一种“音频文件格式”,而是在描述 PCM 采样数据的具体表示方式。这也是理解后续音频格式问题的关键:PCM、WAV、MP3、Opus、WebM 并不处于同一个技术层次。
音频格式
在实际开发中,经常会看到 PCM、WAV、MP3、AAC、Opus、WebM、Ogg 等名称被统称为“音频格式”。这种说法在日常使用中没有太大问题,但如果从音频处理角度分析,它们实际上属于不同层次。
一个音频文件或音频数据流通常可以从三个层面理解:
音频采样数据
↓
编码方式(Codec)
↓
容器格式(Container)
PCM 更接近数字音频本身的采样表示。它通常没有压缩,可以直接用于信号处理和模型输入,但数据量较大。
音频编码(Audio Codec)负责按照某种算法对音频数据进行编码和压缩。例如 MP3、AAC、Opus、Vorbis 和 FLAC 都属于音频编码方式。编码后的数据通常需要先解码,才能恢复为 PCM 采样值并进行进一步信号处理。
容器格式(Container)负责组织音频轨道、编码信息、时间戳和其他元数据。WAV、WebM、Ogg、MP4 等属于容器或文件组织形式,它们内部可以保存使用不同编码方式处理后的音频。
因此,“WebM 和 Opus”并不是两个相互替代的格式。一个常见组合恰恰是:WebM 容器 + Opus 音频编码,对应的 MIME 类型可以写成:audio/webm;codecs=opus。
常见音频形式对比
| 名称 | 所属层次 | 压缩方式 | 主要特点 | Web 语音中的常见用途 |
|---|---|---|---|---|
| PCM | 采样数据表示 | 不压缩 | 数据结构简单,可直接进行音频处理 | ASR 输入、VAD、波形分析、实时处理 |
| WAV | 容器格式 | 取决于内部编码,常见为 PCM | 文件结构简单,常用于保存未压缩音频 | 录音文件、模型测试、离线识别 |
| MP3 | 有损音频编码 | 有损压缩 | 文件较小、播放兼容性高,但编码面向通用音频 | 音频存储和播放,不常作为实时 ASR 的首选传输格式 |
| AAC | 有损音频编码 | 有损压缩 | 常见于 MP4、M4A、ADTS 等媒体体系 | 音视频文件、媒体播放、部分语音接口 |
| Opus | 有损音频编码 | 有损压缩 | 面向交互式语音和音频,兼顾低延迟和压缩效率 | WebM/Ogg 音频、WebRTC、实时语音传输 |
| WebM | 容器格式 | 取决于内部编码 | 面向 Web,音频常使用 Opus 或 Vorbis | MediaRecorder 录音、实时上传 |
| Ogg | 容器格式 | 取决于内部编码 | 开放容器,可封装 Opus、Vorbis 等 | 音频文件和部分实时语音场景 |
| FLAC | 无损音频编码 | 无损压缩 | 能还原原始 PCM,体积通常小于未压缩 PCM | 高质量音频保存、离线处理 |
PCM 是音频采样数据的表示方式,而 WAV 是一种容器格式。WAV 文件中经常保存 PCM 数据,因此二者经常同时出现,但“PCM 音频”和“WAV 文件”并不能直接画等号。
原始 PCM 数据通常不带完整的文件头信息,接收方需要事先知道采样率、位深、声道数和字节序;WAV 则可以在文件头中保存其中的部分音频参数,使文件更容易被播放器和音频工具识别。
如果系统需要直接执行语音识别、VAD、音量计算和频谱分析等处理,PCM 更方便,因为不需要额外解码。
如果网络带宽更加敏感,或者浏览器可以直接产生 WebM/Opus 等编码音频,则可以先压缩再传输,由服务端解码后恢复为 PCM。Opus 本身就是为交互式语音和音频应用设计的编码方式,因此在实时通信场景中较为常见。
对于语音识别而言,真正重要的并不是文件扩展名,而是后续模型最终能够获得符合要求的音频采样数据。一个服务即使接收 MP3、AAC、WebM 或 WAV,内部通常也需要先完成相应的解封装、解码和格式转换,再进入后续声学处理流程。
浏览器音频采集
声音在麦克风和音频设备中完成数字化之后,Web 应用还需要通过浏览器获得这一路实时音频。
出于安全和隐私考虑,网页不能直接绕过浏览器访问本地麦克风。现代 Web 应用通常通过 Media Capture and Streams API 提供的 getUserMedia() 请求麦克风权限,并在用户允许后获得实时的 MediaStream。
获取麦克风
一个基本的麦克风采集过程可以写成:
async function getMicrophoneStream() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: { ideal: 1 },
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
return stream;
}
这里的 audio 用于描述应用期望获取的音频轨道。如果只写:audio: true,表示应用希望获取麦克风音频,并由浏览器选择默认采集配置。
如果传入约束对象,则可以进一步表达对声道数、回声消除、噪声抑制和自动增益控制等能力的要求。
需要注意的是,这些参数本质上属于应用向浏览器和设备提出的采集约束,而不是对底层硬件配置的绝对控制。实际生效结果还会受到浏览器、操作系统、音频驱动和麦克风设备能力的影响。
因此,不应该仅根据传入的约束判断最终音频格式,而应在获得轨道后检查其实际设置。
getUserMedia() 返回的是一个 MediaStream。
MediaStream 不是录制完成的音频文件,也不是一段可以直接读取的 PCM 字节数组,而是浏览器对实时媒体流及其轨道的抽象。
一个 MediaStream 可以同时包含音频和视频轨道。只请求麦克风时,通常可以通过:
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
const [audioTrack] = stream.getAudioTracks();获取其中的音频轨道。
通过 getSettings() 可以查看当前轨道实际采用的配置:
const settings = audioTrack.getSettings();
console.log(settings.sampleRate);
console.log(settings.channelCount);
console.log(settings.echoCancellation);
console.log(settings.noiseSuppression);这里需要区分:
getUserMedia() 中的 constraints
→ 应用希望得到什么配置
MediaStreamTrack.getSettings()
→ 当前轨道实际上采用了什么配置这一区别在音频格式规范化中非常重要。如果后续语音服务要求 16 kHz 单声道 PCM,但浏览器实际运行在 48 kHz,就不能仅仅因为调用 getUserMedia() 时填写了某个参数,就假设采集结果已经满足要求。
MediaStreamTrack 提供了对实时轨道的状态控制。
常见属性包括:
kind
→ 轨道类型,例如 audio
label
→ 媒体输入设备名称
enabled
→ 当前轨道是否启用
readyState
→ live 或 ended将:
audioTrack.enabled = false;通常只是让轨道输出静音数据,并不等于真正结束麦克风采集。
如果语音功能结束,需要调用:
function stopMicrophone(stream) {
stream.getTracks().forEach((track) => {
track.stop();
});
}stop() 会结束对应轨道,使其不再继续从媒体源获取数据。
因此,在设计录音按钮、语音助手或实时识别界面时,需要明确区分“暂时静音”和“真正释放麦克风”两个状态。
从 MediaStream 获取音频数据
获得 MediaStream 后,Web 应用只是建立了与麦克风之间的实时输入通道。
如果系统需要把音频保存、发送给服务端、显示波形或者直接执行实时音频算法,还需要进一步从媒体流中获得实际音频数据。
在 Web 中,最常见的两条处理路线是:
MediaStream
│
├── MediaRecorder
│ ↓
│ 编码 + 封装
│ ↓
│ Blob
│
└── Web Audio API
↓
AudioWorklet
↓
Float32Array
↓
实时音频采样值
两种方案的数据层级不同,也决定了后续能够进行的处理方式。
MediaRecorder:获取编码音频
MediaRecorder 是浏览器提供的媒体录制接口,可以直接以 MediaStream 为输入,由浏览器完成录制、编码和容器封装。
例如:
const recorder = new MediaRecorder(stream);
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
console.log(event.data);
}
};
recorder.start(500);
调用 start(timeslice) 后,浏览器会在录制过程中周期性触发 dataavailable 事件,并返回 Blob。
需要注意的是,timeslice 并不是严格实时的定时器。浏览器调度、系统负载等因素都可能使实际数据块间隔存在偏差,因此不能依赖它进行精确计时。
MediaRecorder 返回的数据不是原始 PCM,而通常已经经过:
录制
↓
音频编码
↓
容器封装
↓
Blob
具体使用什么编码和容器,可以通过 MIME 类型指定,并通过 MediaRecorder.isTypeSupported() 在运行时检查。
例如:
const mimeType = "audio/webm;codecs=opus";
if (MediaRecorder.isTypeSupported(mimeType)) {
const recorder = new MediaRecorder(stream, {
mimeType
});
}
在支持该组合的浏览器中,这表示希望获得:
WebM 容器
+
Opus 音频编码
如果浏览器和服务端都能够处理这种音频,那么整体链路会比较简单:
MediaStream
↓
MediaRecorder
↓
WebM / Opus
↓
Blob
↓
网络传输
↓
服务端解码
↓
PCM
↓
语音识别
这种方式把编码和封装工作交给浏览器完成,因此前端实现相对简单,并且能够显著减少与原始 PCM 相比的网络传输数据量。
Web Audio API:建立音频处理图
如果应用需要直接分析或处理实时采样值,则需要进入更底层的音频处理链路。
Web Audio API 使用 Audio Graph 的方式组织音频处理。应用可以创建 AudioContext,再将麦克风对应的 MediaStream 接入其中:
const audioContext = new AudioContext();
const source =
audioContext.createMediaStreamSource(stream);
这里的 source 是一个 MediaStreamAudioSourceNode,表示 Web Audio 音频图中的麦克风输入节点。
需要注意的是,如果媒体流的采样率与当前 AudioContext 的采样率不同,Web Audio 的处理链路会将输入重采样到 AudioContext.sampleRate。因此,进入 Web Audio 之后,应以 audioContext.sampleRate 作为当前音频处理图实际使用的采样率,而不是继续假设它等于麦克风设备的原始采样率。
AudioWorklet:读取实时采样值
仅仅把 MediaStream 接入 AudioContext,并不会自动把每个采样值交给业务代码。
如果需要持续读取实时音频,可以使用 AudioWorklet。
AudioWorklet 允许开发者创建自定义音频处理节点,在浏览器的音频渲染线程中处理连续的音频数据块。
一个最基本的处理器可以写成:
class AudioProcessor extends AudioWorkletProcessor {
process(inputs) {
const input = inputs[0];
const samples = input?.[0];
if (samples) {
this.port.postMessage(samples.slice());
}
return true;
}
}
registerProcessor(
"audio-processor",
AudioProcessor
);
这里:
inputs[0]
→ 第一路输入
inputs[0][0]
→ 第一路输入中的第一个声道
samples
→ 当前声道的一组 Float32Array 音频采样值
当前浏览器中的 AudioWorklet 通常以较小的渲染块持续调用 process()。业务代码不应该假设每次固定获得某一个长度,而应根据实际 samples.length 处理数据。
主线程首先加载处理器:
await audioContext.audioWorklet.addModule(
"/audio-processor.js"
);
然后创建 AudioWorkletNode:
const worklet = new AudioWorkletNode(
audioContext,
"audio-processor",
{
numberOfOutputs: 0
}
);
source.connect(worklet);
主线程可以通过 MessagePort 接收处理器发送的数据:
worklet.port.onmessage = (event) => {
const samples = event.data;
console.log(samples);
};
此时拿到的数据通常是:
Float32Array
↓
[-1, 1] 范围附近的浮点采样值
这种数据可以继续用于:
- 波形显示;
- 音量计算;
- 时域分析;
- 频谱分析;
- VAD 等实时算法;
- 声道处理;
- 重采样;
- PCM 格式转换;
- 实时发送给语音服务。
Float32 转换为 PCM Int16
很多语音服务会要求 16 位整数 PCM,而 Web Audio 内部通常以 32 位浮点采样值处理音频。
因此,可以将:
Float32
[-1, 1]
转换为:
Int16
[-32768, 32767]
例如:
function float32ToInt16(samples) {
const pcm = new Int16Array(samples.length);
for (let i = 0; i < samples.length; i++) {
const sample = Math.max(
-1,
Math.min(1, samples[i])
);
pcm[i] = sample < 0
? sample * 0x8000
: sample * 0x7fff;
}
return pcm;
}
这里完成的只是采样值表示方式转换:
Float32 PCM
↓
Int16 PCM
它并不会自动改变采样率。
如果当前 AudioContext 运行在 48 kHz,而后续语音服务要求:
16 kHz
单声道
PCM S16LE
那么仍然需要额外进行:
48 kHz
↓
重采样
↓
16 kHz
之后再按照接口要求组织和传输数据。
MediaRecorder 与 AudioWorklet 如何选择
两种方案并不存在绝对优劣,它们解决的是不同层次的问题。
| 对比项 | MediaRecorder | AudioWorklet |
|---|---|---|
| 输入 | MediaStream | Web Audio 音频图 |
| 输出 | Blob | Float32Array 等实时采样值 |
| 数据形态 | 编码并封装后的媒体数据 | 直接可处理的音频采样数据 |
| 编码控制 | 主要由浏览器负责 | 应用可以自行组织和转换 |
| 实现复杂度 | 较低 | 较高 |
| 网络数据量 | 使用压缩编码时较小 | 原始 PCM 通常较大 |
| 波形 / 频谱分析 | 不方便直接进行 | 适合 |
| 实时 VAD | 通常需要解码后处理 | 适合 |
| 直接生成 PCM | 不适合 | 适合 |
| 录音保存 | 适合 | 需要自行编码或封装 |
| 实时 ASR | 服务端支持编码格式时很方便 | 服务要求固定 PCM 时更灵活 |
因此,可以简单理解为:
MediaRecorder
→ 更偏“录制并获得编码音频”
AudioWorklet
→ 更偏“直接处理实时音频采样值”
如果服务端能够直接接收 WebM/Opus,使用 MediaRecorder 往往更简单;如果接口明确要求 PCM,或者浏览器端需要执行实时信号分析,就更适合使用 AudioWorklet。
音频格式规范化
浏览器能够获取音频,并不意味着这些数据已经可以直接交给语音模型。
不同浏览器、操作系统和音频设备可能产生不同的采样率、声道数和数据表示,而语音服务往往会规定自己的输入格式。
例如,一个语音服务可能要求:
sampleRate: 16000
channels: 1
format: pcm_s16le
而浏览器实际得到的音频可能是:
sampleRate: 48000
channels: 2
sampleFormat: Float32
因此,在进入语音服务之前通常需要执行音频格式规范化。
音频解码
如果输入来自:
WebM / Opus
MP4 / AAC
Ogg / Opus
MP3
那么在需要直接访问采样值时,首先必须将压缩媒体数据进行解封装和解码,恢复为 PCM。
这一过程可以在浏览器完成,也可以由服务端统一完成。
如果使用 MediaRecorder 直接将 WebM/Opus 发送给服务器,那么通常更适合让服务器负责解码;如果浏览器需要直接分析音频,则通常会选择 Web Audio 或其他能够获得 PCM 数据的处理路径。
重采样
重采样(Resampling)用于将音频从一个采样率转换到另一个采样率。
例如:
48 kHz
↓
重采样
↓
16 kHz
重采样并不是简单地“每隔三个点取一个值”。如果直接丢弃采样点,可能引入频谱混叠或失真。规范的重采样需要结合低通滤波和插值等处理。
在实际 Web 语音系统中,重采样可以由浏览器、音频处理库或服务端完成。对于需要兼容多种浏览器和设备的系统,将重采样集中在服务端往往更容易统一行为;对于端侧实时处理,也可以在浏览器中完成。
声道转换
如果语音服务要求单声道,而输入是双声道,则需要将多个声道合并为单声道。
最简单的双声道合并方式可以表示为:
Mono = (Left + Right) / 2
实际处理时还需要考虑幅度范围和削波问题。
对于只需要识别人声内容的 ASR 场景,单声道通常已经足够,并且可以降低传输和计算开销。
采样格式转换
采样格式决定一个采样值以什么数据类型保存,例如:
Float32 PCM
Int16 PCM
Int24 PCM
Int32 PCM
Web Audio 常使用 Float32,而许多语音服务会使用 Int16 PCM。因此,从浏览器实时采样到服务接口之间,经常存在:
Float32
↓
幅值缩放
↓
Int16
这样的转换过程。
字节序
当 PCM 以多字节整数形式传输时,还需要明确字节序(Endianness)。
例如:
pcm_s16le
中的 le 表示 Little Endian。
如果发送方和接收方对字节序理解不同,同一组二进制数据会被解释为完全不同的采样值。因此,对于直接传输裸 PCM 的协议,应明确约定:
采样率
声道数
位深
有符号 / 无符号
整数 / 浮点
字节序
音频分块
实时音频不会等用户完整说完后再一次性产生,而是持续到达。
因此,还需要将连续音频组织为适合处理和网络传输的数据块:
连续音频
↓
Chunk 1
Chunk 2
Chunk 3
Chunk 4
...
分块本身不会改变音频的采样率、编码方式或位深,它只是改变数据在时间和网络消息中的组织方式。
数据块过大,会增加等待下一块形成的时间;数据块过小,则会增加消息数量、协议开销和调度压力。实际系统需要结合语音服务协议、网络情况和延迟要求进行选择,而不是简单追求越小越好。
一个完整的规范化过程
如果浏览器通过 Web Audio 获得 48 kHz 双声道 Float32 数据,而服务要求 16 kHz 单声道 pcm_s16le,整个过程可以表示为:
48 kHz / Stereo / Float32
↓
声道合并
↓
48 kHz / Mono / Float32
↓
重采样
↓
16 kHz / Mono / Float32
↓
采样格式转换
↓
16 kHz / Mono / Int16
↓
小端字节序组织
↓
pcm_s16le
音频格式规范化的目标不是让所有音频都变成某一种固定格式,而是把不同来源的音频转换为后续语音服务真正要求的形式。
音频增强
现实环境中的麦克风输入通常并不只有目标语音。
背景噪声、扬声器回放、麦克风距离、设备增益和周围其他声音,都可能影响后续语音识别和交互效果。因此,在格式规范化之外,系统还可能进行一定的音频增强。
噪声抑制
噪声抑制(Noise Suppression)用于降低与目标语音无关的背景信号,例如:
- 空调和风扇;
- 道路交通;
- 设备运行声;
- 部分键盘和环境噪声。
浏览器可以在 getUserMedia() 中通过:
noiseSuppression: true
请求启用系统提供的噪声抑制能力。
需要注意的是,这只是一个媒体采集约束。最终是否启用以及实际处理效果取决于浏览器、操作系统和音频设备。
此外,噪声抑制并不能保证“只剩用户声音”。尤其是周围其他人的说话声本身也具有明显的语音结构,通常比稳定环境噪声更难分离。
声学回声消除
声学回声消除(Acoustic Echo Cancellation,AEC)用于处理扬声器播放内容重新进入麦克风的问题。
例如:
系统播放 TTS
↓
扬声器发声
↓
声音经过空间传播
↓
麦克风再次采集
↓
系统误以为这是新的用户语音
在语音助手、语音通话和边播放边监听的系统中,这种回声尤其重要。
浏览器可以通过:
echoCancellation: true
请求启用相应能力。
AEC 通常需要以系统正在播放的音频作为参考,估计其中重新进入麦克风的部分,并尽量从输入信号中抵消。
自动增益控制
用户距离麦克风的远近、说话音量和设备灵敏度都可能不同。
自动增益控制(Automatic Gain Control,AGC)会根据输入信号幅度动态调整增益,使语音保持在相对稳定的音量范围。
浏览器可以通过:
autoGainControl: true
请求启用这一能力。
AGC 并不是简单地把所有声音放大。输入本身较弱时,提高增益也可能同时放大背景噪声;输入过强时,如果处理不当还可能产生削波和失真。因此,音量稳定性和信号质量之间需要保持平衡。
降噪、回声消除、重采样、增益调整等处理都会改变输入信号。如果参数不合理,可能反而损失发音细节或引入新的失真。
对 Web 语音系统而言,音频处理的目标应当是:在合理的计算和传输成本下,将不同设备产生的音频转换为稳定、统一并符合后续语音服务要求的数据。
语音活动检测(VAD)有时也会出现在音频处理链路中,用于过滤静音数据或辅助判断用户是否正在说话。不过,VAD 更核心的作用是识别语音活动状态并参与话轮控制,因此将在后续“语音交互机制”中进一步介绍。
实时音频传输
完成音频采集和处理后,浏览器已经能够得到编码音频或 PCM 数据。
如果语音识别模型部署在服务端,这些数据还需要通过网络持续发送给语音服务。
对于实时语音系统而言,通信目标并不仅仅是:
把一段音频上传到服务器。
更重要的是:
在用户持续说话的过程中不断发送音频,同时让服务端能够持续处理并及时返回中间结果。
HTTP:适合完整音频和请求响应
最简单的语音服务可以采用普通 HTTP:
录制完整语音
↓
上传音频
↓
服务端识别
↓
返回最终结果
例如:
const formData = new FormData();
formData.append(
"audio",
audioBlob,
"speech.webm"
);
const response = await fetch("/api/asr", {
method: "POST",
body: formData
});
const result = await response.json();
console.log(result.text);
这种方式实现简单,适合:
- 录音文件识别;
- 非实时转写;
- 短语音提交;
- 异步语音任务。
它的问题在于,浏览器通常需要先形成一段完整音频,服务端再开始或完成后续处理,因此不适合追求低延迟的连续交互。
WebSocket:持续双向交换音频和结果
实时 ASR 和语音助手往往需要:
浏览器 ───── 音频数据 ─────> 服务端
浏览器 <──── 识别结果 ───── 服务端
也就是说,浏览器一边持续上传音频,服务端一边返回临时识别结果、最终识别结果和状态信息。
WebSocket 提供了持续的双向连接,因此非常适合这种通信方式:
浏览器 语音服务
│ │
│──── 建立连接 ────────────────>│
│ │
│──── 音频块 1 ───────────────>│
│──── 音频块 2 ───────────────>│
│<─── 临时识别结果 ─────────────│
│──── 音频块 3 ───────────────>│
│<─── 临时识别结果 ─────────────│
│──── 音频结束 ────────────────>│
│<─── 最终识别结果 ─────────────│
│ │
创建连接:
const socket = new WebSocket(
"wss://example.com/asr"
);
socket.onopen = () => {
console.log("连接已建立");
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log(message);
};
WebSocket 可以直接发送:
- 字符串;
Blob;ArrayBuffer;- TypedArray 等二进制数据。
因此,无论前面使用的是 MediaRecorder 还是 AudioWorklet,都可以接入 WebSocket。
MediaRecorder + WebSocket
如果浏览器通过 MediaRecorder 产生 WebM/Opus:
recorder.ondataavailable = (event) => {
if (
event.data.size > 0 &&
socket.readyState === WebSocket.OPEN
) {
socket.send(event.data);
}
};
整体链路就是:
MediaStream
↓
MediaRecorder
↓
WebM / Opus
↓
Blob
↓
WebSocket
↓
服务端解码
↓
PCM
↓
ASR
这种方式实现简单、网络数据量较小,但服务端必须能够正确处理对应容器和编码。
AudioWorklet + WebSocket
如果应用通过 AudioWorklet 获得了 PCM:
const pcm = float32ToInt16(samples);
if (socket.readyState === WebSocket.OPEN) {
socket.send(pcm.buffer);
}
整体链路变为:
MediaStream
↓
Web Audio API
↓
AudioWorklet
↓
Float32 PCM
↓
声道 / 重采样 / Int16 转换
↓
PCM S16LE
↓
WebSocket
↓
ASR
此时服务端不需要再对 MP3、AAC 或 Opus 进行解码,但网络中传输的是未压缩音频,数据量通常会更大。
控制消息与二进制音频分离
一个完整的实时语音协议通常不只有音频。
在开始发送之前,客户端可能需要告诉服务端:
{
"type": "start",
"sampleRate": 16000,
"channels": 1,
"format": "pcm_s16le"
}
随后发送二进制音频:
Binary
Binary
Binary
Binary
...
用户结束说话后,再发送:
{
"type": "end"
}
服务端则可以返回:
{
"type": "partial",
"text": "今天天气"
}
以及:
{
"type": "final",
"text": "今天天气怎么样"
}
这种设计将两类信息分开:
JSON
→ 描述会话、格式和状态
Binary
→ 传输真正的音频数据
相比将二进制音频先转为 Base64 再放进 JSON,直接发送二进制数据可以避免额外的编码和数据膨胀。
WebSocket 不决定音频格式
这里需要特别区分:
WebSocket
→ 解决“数据如何持续传输”
PCM / Opus / AAC
→ 解决“音频如何表示或编码”
WebM / Ogg
→ 解决“编码数据如何组织和封装”
因此:
WebSocket + PCM
和:
WebSocket + WebM / Opus
都是合理的组合。
WebSocket 本身并不知道你发送的是语音、图片还是其他二进制数据。具体音频格式必须由浏览器和服务端在应用层协议中提前约定。
流量控制
实时音频会持续产生。如果音频生成速度长期高于网络发送速度,浏览器中的待发送数据会不断积累。
标准 WebSocket API 没有自动为应用完成背压控制,因此可以通过:
socket.bufferedAmount
观察当前已经排队、但尚未实际发送到网络的数据量。
例如:
const MAX_BUFFERED_AMOUNT =
512 * 1024;
if (
socket.readyState === WebSocket.OPEN &&
socket.bufferedAmount < MAX_BUFFERED_AMOUNT
) {
socket.send(audioBuffer);
}
这里的阈值并不存在统一标准,应根据音频码率、网络质量和业务容忍度自行设计。
如果缓冲持续增长,系统还需要考虑:
- 暂停或降低发送速度;
- 丢弃允许丢弃的数据;
- 调整分块策略;
- 降低编码码率;
- 中断异常会话;
- 在服务端增加流量控制。
WebRTC 与实时音频
除了 WebSocket,Web 平台还提供了专门面向实时音视频通信的 WebRTC。
WebRTC 更关注:
- 实时媒体传输;
- 音视频轨道;
- 网络穿透;
- 拥塞控制;
- 抖动处理;
- 实时通信质量。
因此,如果业务本身是实时通话、语音房或音视频会议,WebRTC 往往比把媒体数据全部当作普通 WebSocket 二进制消息传输更合适。
对于“浏览器把麦克风音频持续发送给 ASR 服务”这种偏应用数据处理的场景,WebSocket 的模型通常更加直观。
二者可以简单区分为:
| 方式 | 更适合的场景 |
|---|---|
| HTTP | 文件识别、短语音、异步任务 |
| WebSocket | 实时 ASR、语音助手、持续双向消息 |
| WebRTC | 实时通话、音视频通信、媒体传输 |
| SSE | 服务端单向推送结果,不适合单独承担音频上行 |
HTTP 和 WebSocket 也并不是互斥关系。实际系统经常同时使用:
HTTP
→ 登录鉴权 / 获取配置 / 创建会话 / 查询历史
WebSocket
→ 实时音频 / 临时结果 / 最终结果 / 会话状态
这样可以分别发挥请求响应接口和持续双向连接的优势。
常见实现方案
综合前面的采集、处理和传输方式,Web 语音系统通常可以形成几种典型实现方案。
方案一:MediaRecorder + WebM/Opus
getUserMedia
↓
MediaStream
↓
MediaRecorder
↓
WebM / Opus
↓
WebSocket
↓
服务端解码
↓
ASR
这种方案的特点是:
- 浏览器端实现简单;
- 利用 Opus 压缩后网络数据量较小;
- 不需要前端自行处理 PCM;
- 服务端需要支持对应容器和编码;
- 浏览器端不方便直接执行底层音频算法。
适合:
- 实时语音识别;
- 语音输入;
- 服务端已经具备 WebM/Opus 解码能力的系统。
方案二:AudioWorklet + PCM
getUserMedia
↓
MediaStream
↓
AudioContext
↓
AudioWorklet
↓
Float32 PCM
↓
格式规范化
↓
PCM S16LE
↓
WebSocket
↓
ASR
这种方案的特点是:
- 可以直接访问实时采样数据;
- 便于波形、频谱、VAD 等实时处理;
- 可以精确控制发送给服务端的数据格式;
- 浏览器端实现更加复杂;
- 未压缩 PCM 的网络数据量更大。
适合:
- ASR 接口严格要求 PCM;
- 浏览器需要执行实时音频分析;
- 需要精确控制重采样、声道和采样格式;
- 端侧语音算法。
方案三:WebRTC 实时媒体链路
getUserMedia
↓
MediaStream
↓
RTCPeerConnection
↓
实时媒体传输
↓
媒体服务器 / 实时服务
适合:
- 实时通话;
- 语音房;
- 音视频会议;
- 需要成熟实时媒体传输能力的系统。
如果后续仍然需要 ASR,可以在媒体服务器或后端从实时音频轨道中提取音频,再交给识别模型处理。
如何选择
如果只是希望快速实现一个 Web 语音识别功能,可以优先判断服务端支持什么输入。
如果服务端支持:
WebM / Opus
可以优先考虑:
getUserMedia
+
MediaRecorder
+
WebSocket
如果服务端明确要求:
16 kHz
Mono
PCM S16LE
或者浏览器还需要执行:
波形
频谱
VAD
自定义音频算法
则更适合:
getUserMedia
+
Web Audio API
+
AudioWorklet
+
格式规范化
+
WebSocket
因此,浏览器音频方案的选择并不是单纯比较哪个 API 更“高级”,而应该首先确认:
- 后端或语音模型接受什么音频格式;
- 是否需要浏览器直接访问 PCM;
- 是否需要实时分析或修改音频;
- 网络带宽和延迟要求如何;
- 音频解码、重采样和增强应该放在浏览器还是服务端。
只有先确定后续语音服务需要什么数据,才能反过来决定浏览器应该以什么方式采集、处理和传输音频。
总结
Web 语音系统中的音频采集与处理,本质上是在完成一系列连续的数据转换:
现实声音
↓
模拟电信号
↓
ADC
↓
数字音频
↓
浏览器 MediaStream
↓
MediaRecorder 或 AudioWorklet
↓
编码音频或 PCM
↓
格式规范化
↓
实时传输
↓
语音服务
其中,声音数字化解决现实声音如何变成数字采样数据;getUserMedia() 和 MediaStream 负责将麦克风音频引入 Web 应用;MediaRecorder 更适合获得经过编码和封装的音频,而 AudioWorklet 更适合直接处理实时 PCM 采样值。
在理解音频格式时,需要特别区分 PCM、音频编码和容器格式。PCM 描述采样数据本身,MP3、AAC、Opus 等负责音频编码和压缩,而 WAV、WebM、Ogg 等负责组织和封装媒体数据。它们并不是同一层级的概念。
浏览器产生的音频还可能需要经过解码、重采样、声道转换、采样格式转换和音频增强,最终变成语音服务能够稳定处理的数据。对于实时语音场景,WebSocket 可以进一步承担持续的双向数据交换,使浏览器在不断上传音频的同时接收服务端返回的识别结果和状态信息。
完成这一阶段后,Web 语音系统已经获得可以供模型分析的数字音频数据。下一步,就是利用自动语音识别(Automatic Speech Recognition,ASR)将这些音频信息转换为对应的文本内容。
参考资料
相关阅读






