22、音频录制与处理:AudioRecord与AudioTrack、噪声抑制、回声消除、AGC

音频处理这块,说实话,是很多Android开发者的“盲区”。大家平时做UI、做网络请求都很熟练,但一碰到音频,就有点懵。我刚开始接触时也一样,总觉得音频不就是录个音、放个声嘛,能有多复杂?直到我在一个语音社交项目里被用户吐槽“声音像在水里说话”……嗯,从那以后,我才真正开始认真研究音频底层。

今天这一章,咱们就把音频录制与处理的核心技术掰开揉碎。说白了,就是两件事:怎么把声音存下来(录制),怎么把声音放出去(播放)。但中间还夹着三个“魔鬼”——噪声、回声、音量忽大忽小。别急,我一个一个给你讲透。

22.1 AudioRecord:从麦克风抓取原始数据

AudioRecord是Android提供的底层音频录制API。它不负责编码,只负责从麦克风拿到原始的PCM数据。你想想看,这就像你去菜市场买鱼,AudioRecord只负责把鱼从水里捞出来,至于怎么杀、怎么切、怎么做,那是你的事。

我个人习惯用AudioRecord做实时音频处理,比如语音聊天、实时变声。因为它延迟低,而且你能拿到最原始的数据,想怎么折腾都行。

基本使用流程

// 1. 定义参数
int sampleRate = 44100;          // 采样率,单位Hz
int channelConfig = AudioFormat.CHANNEL_IN_MONO;  // 单声道
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位采样
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

// 2. 创建AudioRecord实例
AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.MIC,  // 麦克风作为音源
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize
);

// 3. 开始录制
audioRecord.startRecording();

// 4. 循环读取数据
byte[] audioData = new byte[bufferSize];
boolean isRecording = true;
while (isRecording) {
    int bytesRead = audioRecord.read(audioData, 0, bufferSize);
    if (bytesRead > 0) {
        // 这里拿到原始PCM数据,可以写入文件或做实时处理
        processAudioData(audioData, bytesRead);
    }
}

// 5. 停止并释放
audioRecord.stop();
audioRecord.release();
注意:bufferSize不能随便设。我见过有人直接写1024,结果在某些手机上录出来的声音断断续续。一定要用getMinBufferSize()获取最小值,或者在此基础上乘以2、4来保证流畅。

音源选择有讲究

AudioRecord构造函数的第一个参数是音源。常用的有这几个:

音源常量 说明 适用场景
MediaRecorder.AudioSource.MIC 默认麦克风 普通录音
MediaRecorder.AudioSource.VOICE_COMMUNICATION 语音通信模式 VoIP、语音聊天
MediaRecorder.AudioSource.CAMCORDER 摄像模式 视频录制时同步录音
MediaRecorder.AudioSource.UNPROCESSED 原始未处理音频 需要自己处理噪声的场景

我曾经在一个对讲机App里踩过坑:用MIC音源录出来的声音,在嘈杂环境下根本没法听。后来换成VOICE_COMMUNICATION,效果好了很多。因为系统会在这个模式下自动开启一些降噪处理。

22.2 AudioTrack:把数据送到扬声器

AudioTrack是AudioRecord的“另一半”。它负责把PCM数据播放出来。说白了,AudioRecord是“输入”,AudioTrack是“输出”。

基本使用流程

// 1. 定义参数
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);

// 2. 创建AudioTrack实例
AudioTrack audioTrack = new AudioTrack(
    AudioManager.STREAM_MUSIC,  // 音频流类型
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize,
    AudioTrack.MODE_STREAM      // 流模式,适合实时播放
);

// 3. 开始播放
audioTrack.play();

// 4. 写入数据
byte[] audioData = getAudioDataFromSomewhere(); // 从网络或文件获取
audioTrack.write(audioData, 0, audioData.length);

// 5. 停止并释放
audioTrack.stop();
audioTrack.release();
小技巧:AudioTrack有两种模式:MODE_STATIC和MODE_STREAM。MODE_STATIC适合播放短音频(比如提示音),数据一次性写入;MODE_STREAM适合长音频或实时流,数据边写边播。做实时语音处理时,一定要用MODE_STREAM。

22.3 噪声抑制:让声音更干净

噪声抑制,说白了就是把背景里的“嗡嗡声”、“嘶嘶声”去掉。我最早做这个功能时,天真地以为用个低通滤波器就行了。结果发现,噪声的频段和语音频段经常重叠,一刀切会把说话声也切掉。

实际项目中,常用的噪声抑制方法有两种:

方法一:使用系统内置的噪声抑制

Android从4.0开始就提供了噪声抑制的API。用法很简单:

// 创建AudioRecord时,指定音源为VOICE_COMMUNICATION
AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.VOICE_COMMUNICATION,
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize
);

// 或者手动开启噪声抑制
if (AudioRecord.isNoiseSuppressionSupported()) {
    audioRecord.setNoiseSuppressionEnabled(true);
}

这个方法的好处是简单,系统帮你做了。但坏处是——你没法控制降噪的强度,而且不同手机的效果差异很大。我测试过几款手机,有的降噪后语音清晰度明显下降,有的则几乎没效果。

方法二:自己实现噪声门(Noise Gate)

噪声门的基本思路是:当音频信号的音量低于某个阈值时,直接静音。这样背景噪声就被“关在门外”了。

// 简单的噪声门实现
public void processNoiseGate(short[] audioData, int threshold) {
    for (int i = 0; i < audioData.length; i++) {
        if (Math.abs(audioData[i]) < threshold) {
            audioData[i] = 0;  // 低于阈值,静音
        }
    }
}
注意:噪声门虽然简单,但阈值设不好会出问题。阈值太高,说话声会被切掉,听起来断断续续;阈值太低,噪声又去不掉。我建议你先录制一段环境噪声,计算它的平均振幅,然后把这个值乘以1.5~2作为阈值。

22.4 回声消除:别让声音“绕圈圈”

回声消除,是语音通信里最头疼的问题之一。你想想看,A说话,B听到后通过扬声器放出来,这个声音又被B的麦克风录到,传回给A……A就听到了自己的回声。

Android提供了AcousticEchoCanceler类来做这件事:

// 检查是否支持回声消除
if (AcousticEchoCanceler.isAvailable()) {
    // 创建AudioRecord后,获取它的AudioSessionId
    int audioSessionId = audioRecord.getAudioSessionId();
    
    // 创建回声消除器
    AcousticEchoCanceler echoCanceler = AcousticEchoCanceler.create(audioSessionId);
    
    // 启用
    echoCanceler.setEnabled(true);
}

但这里有个坑:AcousticEchoCanceler是硬件相关的,不是所有手机都支持。而且即使支持,效果也参差不齐。我曾经在一个项目里,用某款国产手机测试,回声消除开了跟没开一样……

如果系统自带的不好用,那就得自己实现。常见的算法有NLMS(归一化最小均方)和频域自适应滤波。这里给个简单的NLMS实现思路:

// NLMS回声消除核心逻辑(伪代码)
public void processAEC(short[] micData, short[] speakerData) {
    // 1. 估计回声路径(自适应滤波器)
    // 2. 根据扬声器信号,生成估计的回声信号
    // 3. 从麦克风信号中减去估计的回声
    // 4. 更新滤波器系数
    
    // 实际项目中,建议直接用WebRTC的AEC模块
    // 它经过了大量优化,效果比你自己写的要好得多
}
建议:如果项目时间紧,直接用WebRTC的AEC模块。它支持Android,而且效果经过了大量验证。我自己在几个项目里都用过,基本能满足需求。

22.5 AGC:自动音量控制

AGC(Automatic Gain Control),自动增益控制。它的作用是:当说话人离麦克风远时,自动把音量调大;离得近时,自动调小。这样对方听到的音量就比较稳定。

Android同样提供了AutomaticGainControl类:

if (AutomaticGainControl.isAvailable()) {
    int audioSessionId = audioRecord.getAudioSessionId();
    AutomaticGainControl agc = AutomaticGainControl.create(audioSessionId);
    agc.setEnabled(true);
}

但说实话,系统自带的AGC效果一般。它反应比较慢,而且有时候会把背景噪声也一起放大。我更喜欢自己实现一个简单的AGC:

// 简单的AGC实现
public class SimpleAGC {
    private float targetLevel = 0.3f;  // 目标音量(归一化后)
    private float currentGain = 1.0f;
    private float attackTime = 0.1f;   // 增益增加速度
    private float releaseTime = 0.5f;  // 增益减小速度
    
    public void process(short[] audioData) {
        // 1. 计算当前帧的音量
        float currentLevel = calculateRMS(audioData);
        
        // 2. 计算需要的增益
        float targetGain = targetLevel / (currentLevel + 0.0001f);
        
        // 3. 平滑调整增益(防止突变)
        if (targetGain > currentGain) {
            currentGain += (targetGain - currentGain) * attackTime;
        } else {
            currentGain += (targetGain - currentGain) * releaseTime;
        }
        
        // 4. 限制增益范围
        currentGain = Math.max(0.5f, Math.min(2.0f, currentGain));
        
        // 5. 应用增益
        for (int i = 0; i < audioData.length; i++) {
            audioData[i] = (short)(audioData[i] * currentGain);
        }
    }
    
    private float calculateRMS(short[] data) {
        float sum = 0;
        for (short s : data) {
            sum += s * s;
        }
        return (float)Math.sqrt(sum / data.length) / 32768f;
    }
}
核心要点:AGC的关键在于“平滑”。如果增益变化太快,声音会忽大忽小,听起来很不自然。attackTime和releaseTime就是控制这个平滑速度的。attackTime要小一点(快速响应),releaseTime要大一点(缓慢恢复),这样效果最好。

22.6 知识体系总览

说了这么多,咱们用一张图来总结一下音频录制与处理的核心逻辑:

音频录制与处理核心流程 麦克风 AudioRecord 噪声抑制 回声消除 AGC AudioTrack 扬声器 数据流向:原始PCM → 降噪 → 去回声 → 音量均衡 → 播放 注意:实际项目中,这三个处理模块的顺序可以调整 比如先做AGC再做噪声抑制,效果可能更好 💡 建议:先用系统API快速验证,效果不满意再自己实现

这张图展示了音频从麦克风到扬声器的完整链路。你想想看,每个环节都像一道关卡,过五关斩六将,最后才能输出清晰的声音。

好了,这一章的内容就到这里。音频处理是个实践性很强的工作,光看理论没用,一定要动手写代码、跑测试。我建议你从最简单的AudioRecord+AudioTrack开始,先实现一个“录音并播放”的Demo,然后再逐步加入噪声抑制、回声消除、AGC这些功能。每加一个功能,都去嘈杂环境下测试一下效果,这样你才能真正理解每个模块的作用。


公众号:蓝海资料掘金营,微信deep3321