57、多媒体之音频:MediaPlayer播放、SoundPool音效、AudioRecord录音
音频处理,是移动开发里绕不开的一块硬骨头。说实话,我刚入行那会儿,觉得播放个MP3能有多难?结果第一次做语音社交App,就被各种状态回调、生命周期管理、权限申请折腾得够呛。今天咱们就把Android音频三剑客——MediaPlayer、SoundPool、AudioRecord,一次性讲透。
核心知识点速览
- MediaPlayer:适合长音频、音乐播放,支持流媒体
- SoundPool:适合短促音效、游戏反馈,低延迟
- AudioRecord:原始PCM录音,适合语音处理、实时分析
一、MediaPlayer:播放音乐的正确姿势
MediaPlayer是个状态机,这一点我踩过坑。记得有次线上反馈说播放器点了没反应,查了半天发现是在错误状态调了start()。说白了,你得把它当个有限状态机来伺候。
1.1 基本使用流程
MediaPlayer mediaPlayer = new MediaPlayer();
try {
// 设置数据源(本地或网络)
mediaPlayer.setDataSource(context, Uri.parse("file:///sdcard/music.mp3"));
// 同步准备(简单场景)
mediaPlayer.prepare();
// 开始播放
mediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
我的习惯:网络音频一定要用异步准备,否则主线程卡住用户直接骂娘。
mediaPlayer.setOnPreparedListener(mp -> {
// 准备完成,可以播放了
mp.start();
});
mediaPlayer.prepareAsync(); // 异步准备,不阻塞UI
1.2 状态管理要点
MediaPlayer有6种核心状态:Idle、Initialized、Prepared、Started、Paused、Stopped。嗯,这里要注意——Released状态后不能再使用。
| 当前状态 | 可调用方法 | 下一个状态 |
|---|---|---|
| Idle | setDataSource() | Initialized |
| Initialized | prepare() / prepareAsync() | Prepared |
| Prepared | start() | Started |
| Started | pause() / stop() | Paused / Stopped |
| Paused | start() | Started |
| Stopped | prepare() / prepareAsync() | Prepared |
我曾经踩过的坑:在Activity的onDestroy里直接调mediaPlayer.release(),但没判断是否为空。结果用户快速旋转屏幕,空指针直接闪退。后来我统一用LifecycleObserver管理,省心多了。
1.3 生命周期最佳实践
public class AudioPlayerViewModel extends ViewModel {
private MediaPlayer mediaPlayer;
@Override
protected void onCleared() {
super.onCleared();
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
}
}
把MediaPlayer放到ViewModel里,配合Lifecycle,旋转屏幕不会中断播放。这个方案我在多个项目里验证过,稳得很。
二、SoundPool:音效播放的轻骑兵
SoundPool适合短促音效,比如按钮点击、游戏中的射击声。它把音频文件预加载到内存,播放延迟极低。你想想看,要是每次点击都new一个MediaPlayer,那延迟和内存开销谁也受不了。
2.1 初始化与加载
SoundPool soundPool;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes attrs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
soundPool = new SoundPool.Builder()
.setMaxStreams(5) // 同时播放的最大流数
.setAudioAttributes(attrs)
.build();
} else {
soundPool = new SoundPool(5, AudioManager.STREAM_MUSIC, 0);
}
// 加载音效,返回soundID
final int soundId = soundPool.load(context, R.raw.click_sound, 1);
关键参数:setMaxStreams(5)表示最多同时播放5个音效。如果超过,系统会停止优先级最低的那个。我在做射击游戏时设了8个,结果枪声和脚步声互相抢资源,后来改成4个就正常了。
2.2 播放与控制
// 播放音效
soundPool.play(
soundId, // 音效ID
1.0f, // 左声道音量 (0.0 - 1.0)
1.0f, // 右声道音量
1, // 优先级 (0=最低)
0, // 循环次数 (0=不循环, -1=无限循环)
1.0f // 播放速率 (0.5 - 2.0)
);
播放速率这个参数很有意思。调成2.0f,声音会变尖变快,适合加速效果;调成0.5f,声音变低沉,适合慢动作场景。我在一个跑酷游戏里就用这个参数做时间减速特效,玩家反馈很带感。
2.3 释放资源
@Override
protected void onDestroy() {
super.onDestroy();
if (soundPool != null) {
soundPool.release();
soundPool = null;
}
}
别忘了释放。SoundPool占用的内存虽然不大,但累积起来也会出问题。我见过一个App切了20次页面后音效全没了,就是没释放导致的。
三、AudioRecord:录音与原始音频处理
AudioRecord获取的是原始PCM数据,说白了就是未经压缩的音频裸数据。它不像MediaRecorder那样直接输出MP3文件,但胜在灵活——你可以做实时处理、音频分析、甚至自己写编码器。
3.1 权限与配置
注意:Android 6.0以上需要动态申请RECORD_AUDIO权限。我曾经在测试机上忘了处理权限回调,结果录音一直静默失败,排查了半天才发现是权限被拒了。
// 配置参数
int sampleRate = 44100; // 采样率,常用44100Hz
int channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16位PCM
// 计算缓冲区大小
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioRecord audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC, // 麦克风输入
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
3.2 开始录音与读取数据
audioRecord.startRecording();
byte[] audioData = new byte[bufferSize];
boolean isRecording = true;
// 在子线程中读取数据
new Thread(() -> {
while (isRecording) {
int bytesRead = audioRecord.read(audioData, 0, bufferSize);
if (bytesRead > 0) {
// 处理音频数据,比如保存到文件或实时分析
processAudioData(audioData, bytesRead);
}
}
}).start();
我的建议:read()方法是阻塞的,一定要放在子线程。如果在主线程调用,UI会直接卡死。另外,bufferSize不要设得太小,否则频繁读取会消耗大量CPU。
3.3 停止与释放
isRecording = false; // 停止循环
audioRecord.stop();
audioRecord.release();
audioRecord = null;
释放顺序很重要:先stop(),再release()。我见过有人直接调release(),结果底层资源没释放干净,下次录音时直接崩溃。
3.4 实战:保存为WAV文件
AudioRecord输出的是PCM裸数据,要保存为可播放的音频文件,需要自己加WAV头。说白了就是往PCM数据前面塞44个字节的文件头。
private void saveToWav(byte[] pcmData, int sampleRate, File file) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
// WAV文件头
int totalDataLen = pcmData.length + 36;
int channels = 1; // 单声道
int byteRate = sampleRate * channels * 2; // 16位 = 2字节
// 写RIFF头
writeString(fos, "RIFF");
writeInt(fos, totalDataLen);
writeString(fos, "WAVE");
// 写fmt块
writeString(fos, "fmt ");
writeInt(fos, 16); // 子块大小
writeShort(fos, (short)1); // PCM格式
writeShort(fos, (short)channels);
writeInt(fos, sampleRate);
writeInt(fos, byteRate);
writeShort(fos, (short)(channels * 2)); // 块对齐
writeShort(fos, (short)16); // 位深度
// 写data块
writeString(fos, "data");
writeInt(fos, pcmData.length);
fos.write(pcmData);
fos.close();
}
避坑指南:WAV头里的数据长度一定要算对,否则播放器会报错。我刚开始写的时候,totalDataLen少算了36字节,结果生成的WAV文件在Windows Media Player里能播,在手机自带播放器里就报格式错误。后来统一用标准计算方式才解决。
四、三者的选择策略
说了这么多,到底该用哪个?我总结了一个简单的判断逻辑:
- 播放音乐/长音频 → MediaPlayer。它支持流媒体、后台播放、多种格式,功能最全。
- 短促音效/游戏反馈 → SoundPool。低延迟、可同时播放多个、资源占用小。
- 录音/实时音频处理 → AudioRecord。获取原始PCM数据,灵活性最高。
当然,实际项目中经常组合使用。比如一个语音社交App:用AudioRecord录音,用MediaPlayer播放对方的声音,用SoundPool播放消息提示音。各司其职,互不干扰。
音频这块坑不少,但掌握了这三个核心组件,大部分场景都能应对。嗯,今天就先聊到这儿,下次咱们聊聊视频播放和CameraX。
公众号:蓝海资料掘金营,微信deep3321