3. 音频采集与预处理:麦克风权限获取、采样率设置、音频缓冲区管理、降噪与增益控制
音频采集是VAD算法的第一道关卡。说白了,麦克风拿不到干净的数据,后面算法再牛也白搭。我最早做语音项目时,就吃过采样率不匹配的亏——模型跑得飞起,结果识别率惨不忍睹。后来才发现,麦克风采集的是16kHz,模型却等的是8kHz。
这一章,咱们把音频采集和预处理的几个关键环节拆开揉碎。从权限获取到增益控制,每一步都有坑。
3.1 麦克风权限获取
移动端和桌面端,权限获取的方式完全不同。我建议你直接按平台分开处理。
3.1.1 Android 权限
Android 6.0 以上需要动态申请。你需要在 AndroidManifest.xml 里声明,然后在运行时调用 requestPermissions。
<uses-permission android:name="android.permission.RECORD_AUDIO" />
代码里这样写:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.RECORD_AUDIO},
REQUEST_CODE);
}
3.1.2 iOS 权限
iOS 需要在 Info.plist 里添加描述:
<key>NSMicrophoneUsageDescription</key>
<string>我们需要麦克风权限来采集语音</string>
然后调用 AVAudioSession 请求权限:
AVAudioSession.sharedInstance().requestRecordPermission { granted in
if granted {
// 开始采集
} else {
// 提示用户
}
}
3.1.3 Web 端权限
Web 端用 getUserMedia,注意必须走 HTTPS:
navigator.mediaDevices.getUserMedia({ audio: true })
.then(function(stream) {
// 拿到音频流
})
.catch(function(err) {
console.log('麦克风被拒绝: ' + err.message);
});
3.2 采样率设置
采样率决定了音频的时间分辨率。VAD 模型通常用 16kHz,因为人声的主要能量集中在 300Hz-3400Hz,16kHz 完全够用。
常见的采样率对比:
| 采样率 | 适用场景 | 优缺点 |
|---|---|---|
| 8kHz | 电话语音 | 数据量小,但高频信息丢失 |
| 16kHz | VAD、语音识别 | 平衡质量和数据量,推荐 |
| 44.1kHz | 音乐、高保真 | 数据量大,VAD 不需要 |
为什么 VAD 推荐 16kHz?因为大多数预训练模型都是基于 16kHz 训练的。你如果用 8kHz 的数据去跑 16kHz 的模型,效果会打折扣。反过来,44.1kHz 的数据下采样到 16kHz,反而会引入混叠噪声。
设置采样率的代码示例(Android AudioRecord):
int sampleRate = 16000;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
AudioRecord recorder = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize);
3.3 音频缓冲区管理
缓冲区管理是音频采集的难点。缓冲区太小,容易丢数据;缓冲区太大,延迟高。VAD 要求低延迟,所以缓冲区不能太大。
3.3.1 缓冲区大小怎么定?
我一般按 20ms-30ms 的时长来算。16kHz 采样率下,20ms 就是 320 个采样点。公式很简单:
bufferSize = sampleRate * durationInSeconds
// 20ms: 16000 * 0.02 = 320
// 30ms: 16000 * 0.03 = 480
实际开发中,我会用 getMinBufferSize 获取系统建议值,然后取最大值:
int desiredSize = 320; // 20ms
int minSize = AudioRecord.getMinBufferSize(16000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
int finalBufferSize = Math.max(desiredSize, minSize);
3.3.2 环形缓冲区
采集线程和 VAD 处理线程是异步的。我习惯用环形缓冲区(Ring Buffer)来解耦。这样采集线程只管写,处理线程只管读,互不阻塞。
一个简单的环形缓冲区实现:
public class RingBuffer {
private short[] buffer;
private int writeIndex = 0;
private int readIndex = 0;
private int size;
public RingBuffer(int size) {
this.size = size;
this.buffer = new short[size];
}
public synchronized void write(short[] data) {
for (short s : data) {
buffer[writeIndex] = s;
writeIndex = (writeIndex + 1) % size;
if (writeIndex == readIndex) {
// 缓冲区满了,覆盖旧数据
readIndex = (readIndex + 1) % size;
}
}
}
public synchronized int read(short[] out) {
int count = 0;
while (readIndex != writeIndex && count < out.length) {
out[count++] = buffer[readIndex];
readIndex = (readIndex + 1) % size;
}
return count;
}
}
3.4 降噪与增益控制
麦克风采集的原始信号,通常包含环境噪声。VAD 算法对信噪比敏感,噪声大会导致误检。所以预处理阶段,降噪和增益控制是标配。
3.4.1 降噪方法
我常用的降噪方法有三种:
- 谱减法: 简单粗暴,适合稳态噪声。但容易产生“音乐噪声”。
- 维纳滤波: 效果更好,但计算量大。移动端慎用。
- RNNoise: 基于深度学习的降噪,效果好,但需要模型文件。
对于 VAD 前端,我推荐用谱减法,因为它轻量、实时。代码示例:
// 简单的谱减法降噪
public void spectralSubtraction(short[] input, short[] output, float[] noiseProfile) {
int n = input.length;
float[] magnitude = new float[n];
float[] phase = new float[n];
// 计算幅度谱和相位谱
for (int i = 0; i < n; i++) {
magnitude[i] = Math.abs((float) input[i]);
phase[i] = (float) Math.atan2(0, input[i]);
}
// 减去噪声谱
for (int i = 0; i < n; i++) {
float sub = magnitude[i] - noiseProfile[i];
magnitude[i] = Math.max(sub, 0.0f);
}
// 重建信号
for (int i = 0; i < n; i++) {
output[i] = (short) (magnitude[i] * Math.cos(phase[i]));
}
}
3.4.2 增益控制
增益控制是为了让音频信号幅度保持在合理范围。太大会削波,太小会淹没在噪声里。
我习惯用自动增益控制(AGC),目标 RMS 值设为 -26dBFS。这是语音通信的常用标准。
public void applyAGC(short[] input, short[] output, float targetRMS) {
float sum = 0;
for (short s : input) {
sum += s * s;
}
float rms = (float) Math.sqrt(sum / input.length);
float gain = targetRMS / (rms + 1e-10f);
// 限制增益范围,防止削波
gain = Math.min(gain, 10.0f);
for (int i = 0; i < input.length; i++) {
int val = (int) (input[i] * gain);
output[i] = (short) Math.max(-32768, Math.min(32767, val));
}
}
3.5 本章知识体系
下面这张图,把音频采集与预处理的流程串起来了。你可以把它当作一个检查清单。
嗯,到这里,音频采集与预处理的核心内容就讲完了。权限获取、采样率、缓冲区、降噪和增益,每一步都直接影响 VAD 的效果。我建议你在实际项目中,先把这些基础打牢,再往上堆算法。