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();
音源选择有讲究
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();
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; // 低于阈值,静音
}
}
}
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模块
// 它经过了大量优化,效果比你自己写的要好得多
}
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;
}
}
22.6 知识体系总览
说了这么多,咱们用一张图来总结一下音频录制与处理的核心逻辑:
这张图展示了音频从麦克风到扬声器的完整链路。你想想看,每个环节都像一道关卡,过五关斩六将,最后才能输出清晰的声音。
好了,这一章的内容就到这里。音频处理是个实践性很强的工作,光看理论没用,一定要动手写代码、跑测试。我建议你从最简单的AudioRecord+AudioTrack开始,先实现一个“录音并播放”的Demo,然后再逐步加入噪声抑制、回声消除、AGC这些功能。每加一个功能,都去嘈杂环境下测试一下效果,这样你才能真正理解每个模块的作用。