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);
}
注意: 用户拒绝后,不要反复弹窗。我曾经见过一个App,用户拒绝三次还继续弹,直接被用户卸载了。建议引导用户去设置里手动开启。

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);
核心原则: 采集端和模型端的采样率必须一致。如果不一致,一定要做重采样。我建议在采集端就固定为 16kHz,省去后续麻烦。

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;
    }
}
注意: 环形缓冲区一定要处理好读写冲突。我早期项目里因为没加同步,导致 VAD 偶尔检测不到语音,排查了两天才发现是数据被覆盖了。

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]));
    }
}
经验之谈: 噪声谱需要在静音段估计。我通常在前 200ms 采集噪声样本,然后更新噪声谱。如果环境变化,需要重新估计。

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));
    }
}
注意: 增益不要太大。我曾经遇到过增益设到 20,结果安静环境下底噪被放大,VAD 一直误判为有语音。建议增益上限控制在 10 以内。

3.5 本章知识体系

下面这张图,把音频采集与预处理的流程串起来了。你可以把它当作一个检查清单。

音频采集与预处理流程 麦克风权限获取 采样率设置(16kHz) 音频缓冲区管理 降噪(谱减法) 增益控制(AGC) 预处理后的音频数据 说明: 降噪和增益控制是并行处理 顺序可以互换,但建议先降噪后增益

嗯,到这里,音频采集与预处理的核心内容就讲完了。权限获取、采样率、缓冲区、降噪和增益,每一步都直接影响 VAD 的效果。我建议你在实际项目中,先把这些基础打牢,再往上堆算法。