1. 音频采集基础与AudioRecord概述
在Android平台上进行音频采集开发,AudioRecord类是最基础且核心的API。与MediaRecorder不同,AudioRecord提供了更底层的PCM数据访问能力,适合需要实时处理音频流的场景。我曾在一个语音识别项目中深度使用过这个类,当时为了优化延迟性能,几乎试遍了所有参数组合。
AudioRecord的工作流程可以类比为建立一个音频数据的"传送带":初始化时设定好采样率、声道等参数(相当于确定传送带的宽度和速度),然后启动采集线程不断从硬件缓冲区读取数据包(就像从传送带上取货)。这种机制给了开发者极大的灵活性,但也带来了复杂度。
关键提示:AudioRecord读取的是原始PCM数据,这意味着你需要自己处理音频格式转换、编码等后续操作。如果只是简单录音保存,MediaRecorder可能是更便捷的选择。
2. AudioRecord关键参数解析
2.1 音频源类型选择
Android定义了多种音频源常量,最常用的是:
MediaRecorder.AudioSource.MIC:内置麦克风MediaRecorder.AudioSource.VOICE_RECOGNITION:优化过的语音识别源MediaRecorder.AudioSource.VOICE_COMMUNICATION:适合VoIP场景
在智能音箱项目中,我们发现VOICE_RECOGNITION能显著降低环境噪音,但会引入约50ms的额外延迟。这个取舍需要根据场景决定。
2.2 采样率与配置实践
标准采样率包括:
- 8000Hz(电话质量)
- 16000Hz(语音识别常用)
- 44100Hz(CD质量)
- 48000Hz(高清音频)
java复制// 实际设备支持的采样率可能有限,需要检查
int[] sampleRates = {48000, 44100, 16000, 8000};
for (int rate : sampleRates) {
int bufferSize = AudioRecord.getMinBufferSize(
rate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
);
if (bufferSize > 0) {
// 找到可用采样率
break;
}
}
2.3 声道配置与缓冲区计算
声道配置主要有:
CHANNEL_IN_MONO(单声道)CHANNEL_IN_STEREO(立体声)
缓冲区大小计算公式:
code复制缓冲区大小(字节) = 采样时间(秒) × 采样率 × 每样本字节数 × 声道数
例如16000Hz、16bit、单声道、100ms缓冲:
code复制100ms = 0.1s
0.1 × 16000 × 2 × 1 = 3200字节
3. read方法深度解析
3.1 方法签名与重载
AudioRecord提供了三种read方法:
java复制public int read(byte[] audioData, int offsetInBytes, int sizeInBytes)
public int read(short[] audioData, int offsetInShorts, int sizeInShorts)
public int read(ByteBuffer audioBuffer, int sizeInBytes)
在实时语音处理中,我推荐使用short[]版本,因为:
- 16位PCM数据天然匹配short类型
- 避免byte[]到short[]的转换开销
- 直接操作数组比ByteBuffer更高效
3.2 返回值处理策略
返回值可能有三种情况:
- 正值:实际读取的数据量(单位与入参一致)
ERROR_INVALID_OPERATION:未正确初始化ERROR_BAD_VALUE:参数错误
典型处理模式:
java复制while (isRecording) {
int read = audioRecord.read(buffer, 0, buffer.length);
if (read > 0) {
// 处理有效数据
processAudio(buffer, read);
} else {
// 错误处理
handleError(read);
}
}
3.3 阻塞与非阻塞行为
read方法默认是阻塞的——当没有足够数据时会等待。这在实时系统中可能导致线程卡顿。解决方案:
- 确保缓冲区足够大(至少2倍的预期单次读取量)
- 使用独立的高优先级线程读取
- 定期检查数据可用性:
java复制if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
int avail = audioRecord.getBufferSizeInFrames() -
audioRecord.getBufferSizeInFrames();
if (avail >= requiredFrames) {
audioRecord.read(...);
}
}
4. 完整实现示例
4.1 初始化配置
java复制// 参数配置
int sampleRate = 16000;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
// 计算缓冲区大小
int minBufferSize = AudioRecord.getMinBufferSize(
sampleRate,
channelConfig,
audioFormat
);
int bufferSize = Math.max(minBufferSize * 4, 8192); // 经验值
// 创建实例
AudioRecord audioRecord = new AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
4.2 数据读取线程
java复制class AudioCaptureThread extends Thread {
private volatile boolean running = true;
private short[] buffer;
@Override
public void run() {
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_URGENT_AUDIO
);
buffer = new short[bufferSize / 2]; // 16bit=2bytes
audioRecord.startRecording();
while (running) {
int read = audioRecord.read(buffer, 0, buffer.length);
if (read > 0) {
// 示例:计算RMS音量
double sum = 0;
for (int i = 0; i < read; i++) {
sum += buffer[i] * buffer[i];
}
double rms = Math.sqrt(sum / read);
Log.d("Audio", "Current RMS: " + rms);
}
}
audioRecord.stop();
}
public void stopCapture() {
running = false;
}
}
4.3 资源释放模式
java复制@Override
protected void onDestroy() {
if (audioThread != null) {
audioThread.stopCapture();
try {
audioThread.join(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (audioRecord != null) {
if (audioRecord.getRecordingState() ==
AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop();
}
audioRecord.release();
}
super.onDestroy();
}
5. 性能优化与疑难解答
5.1 常见问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| read返回负值 | 未授予录音权限 | 检查Manifest和运行时权限 |
| 音频数据全是0 | 麦克风被其他应用占用 | 确保先stop再release |
| 周期性卡顿 | 缓冲区太小 | 增大bufferSize或优化读取频率 |
| 高频噪声 | 采样率不匹配 | 验证设备实际支持的采样率 |
| 延迟过大 | 处理耗时过长 | 分离采集和处理线程 |
5.2 低延迟优化技巧
- 线程优先级设置:
java复制android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_URGENT_AUDIO
);
- 缓冲区黄金法则:
- 单次读取量 ≈ 预期延迟 × 采样率 / 1000
- 例如10ms延迟@16kHz:160 samples/read
- 设备特性适配:
java复制// 检查低延迟支持
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
String prop = android.media.AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER;
int frames = Integer.parseInt(
audioManager.getProperty(prop)
);
int optimalBufferSize = frames * 2; // 16bit samples
}
5.3 音频数据处理建议
- 实时波形显示:
java复制// 下采样显示
float[] points = new float[100];
int step = buffer.length / 100;
for (int i = 0; i < 100; i++) {
points[i] = buffer[i * step] / 32768.0f; // 归一化
}
waveformView.updatePoints(points);
- VAD(语音活动检测):
java复制boolean isSpeech = false;
double energyThreshold = 500; // 经验值
double sum = 0;
for (short sample : buffer) {
sum += sample * sample;
}
if (sum / buffer.length > energyThreshold) {
isSpeech = true;
}
- 数据保存示例:
java复制void saveAsWav(short[] data, File file) throws IOException {
try (DataOutputStream out = new DataOutputStream(
new FileOutputStream(file))) {
// WAV头写入
writeWavHeader(out, data.length * 2);
// PCM数据写入
for (short s : data) {
out.writeShort(s);
}
}
}
6. 高级应用场景
6.1 回声消除配置
在视频会议项目中,我们这样集成AEC:
java复制// 创建时指定AEC音频源
AudioRecord record = new AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
// 需要同时配置AudioTrack
AudioTrack track = new AudioTrack(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build(),
new AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.build(),
bufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE
);
// 关键步骤:共享音频会话ID
int sessionId = track.getAudioSessionId();
record.setPreferredDevice(sessionId);
6.2 多路音频混合
处理多输入源时:
java复制// 初始化多个AudioRecord
AudioRecord mic1 = createRecorder(source1);
AudioRecord mic2 = createRecorder(source2);
// 读取线程
short[] mixBuffer = new short[bufferSize];
while (running) {
short[] buf1 = new short[bufferSize];
short[] buf2 = new short[bufferSize];
int read1 = mic1.read(buf1, 0, buf1.length);
int read2 = mic2.read(buf2, 0, buf2.length);
// 简单混合算法
int maxRead = Math.max(read1, read2);
for (int i = 0; i < maxRead; i++) {
int mixed = 0;
if (i < read1) mixed += buf1[i];
if (i < read2) mixed += buf2[i];
mixBuffer[i] = (short) Math.max(
Short.MIN_VALUE,
Math.min(Short.MAX_VALUE, mixed)
);
}
// 使用混合后数据
processMixedAudio(mixBuffer, maxRead);
}
6.3 音频预处理流水线
典型处理链实现:
java复制// 定义处理接口
interface AudioProcessor {
void process(short[] buffer, int length);
}
// 实现具体处理器
class NoiseSuppressor implements AudioProcessor {
private SpeexPreprocessor preprocessor;
public NoiseSuppressor(int sampleRate) {
preprocessor = new SpeexPreprocessor();
preprocessor.init(sampleRate);
}
@Override
public void process(short[] buffer, int length) {
preprocessor.process(buffer, length);
}
}
// 构建处理链
List<AudioProcessor> pipeline = Arrays.asList(
new NoiseSuppressor(sampleRate),
new GainController(1.5f),
new VoiceActivityDetector()
);
// 在读取线程中应用
while (running) {
int read = audioRecord.read(buffer, 0, buffer.length);
if (read > 0) {
for (AudioProcessor processor : pipeline) {
processor.process(buffer, read);
}
// 最终处理结果...
}
}
7. 兼容性处理与测试建议
7.1 设备兼容性矩阵
测试中发现的重要差异:
| 设备型号 | 特性支持 | 注意事项 |
|---|---|---|
| 华为P40 | 48kHz采样 | 需要额外配置音频参数 |
| 小米10 | 低延迟模式 | 需启用高性能模式 |
| 三星S21 | 多麦克风 | 需指定正确的音频源 |
| Pixel 5 | AEC优化 | 自动处理效果最佳 |
7.2 自动化测试方案
使用AudioRecord进行单元测试的要点:
java复制@Test
public void testAudioCapture() throws Exception {
// 模拟测试配置
int testDurationMs = 1000;
int sampleRate = 16000;
int bufferSize = sampleRate * 2 * testDurationMs / 1000;
// 创建测试录音机
AudioRecord record = new AudioRecord(...);
// 启动采集线程
AtomicInteger samplesRead = new AtomicInteger();
Thread testThread = new Thread(() -> {
short[] buffer = new short[bufferSize];
while (samplesRead.get() < sampleRate * testDurationMs / 1000) {
int read = record.read(buffer, 0, buffer.length);
if (read > 0) {
samplesRead.addAndGet(read);
}
}
});
// 执行测试
record.startRecording();
testThread.start();
testThread.join(testDurationMs + 500);
// 验证结果
assertTrue("应采集到足够样本",
samplesRead.get() >= sampleRate * testDurationMs / 1000 * 0.9);
}
7.3 性能监控指标
关键监控点实现:
java复制// 延迟计算
long lastReadTime = System.nanoTime();
while (running) {
int read = audioRecord.read(buffer, 0, buffer.length);
long now = System.nanoTime();
double latencyMs = (now - lastReadTime) / 1e6;
lastReadTime = now;
// 统计指标
stats.addLatency(latencyMs);
stats.addSampleCount(read);
// 超过阈值报警
if (latencyMs > 50) { // 50ms阈值
Log.w("Audio", "高延迟警告: " + latencyMs + "ms");
}
}
// 定期输出统计
Timer statsTimer = new Timer();
statsTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Log.i("AudioStats",
String.format("平均延迟: %.2fms, 吞吐量: %d samples/s",
stats.getAvgLatency(),
stats.getSamplesPerSecond()
)
);
}
}, 5000, 5000);
在语音直播项目中,这套监控系统帮助我们发现了某些机型上的缓冲区配置问题,将端到端延迟从200ms降低到了80ms以内。关键是要建立基线性能指标,持续监控异常波动。