Android音频播放录制全攻略:API选型、参数配置与避坑指南

发布时间:2026/9/26 18:47:25
Android音频播放录制全攻略:API选型、参数配置与避坑指南 做Android音频这几年播放和录制相关的坑我基本踩过一遍。这篇是Android音频系列的第四篇重点讲Audio播放录制时怎么选API、怎么定参数以及那些没人写在文档里的细节。如果你正准备在App里加语音消息、做录音转文字或者想做一个音频编辑器这篇的内容基本覆盖了会用到的核心场景。代码层面我会把参数背后的逻辑也讲清楚方便你回头自己调。1. 播放方案怎么选MediaPlayer和AudioTrack到底用哪个1.1 先搞懂音频播放的完整链路很多人上来就直接用MediaPlayer觉得它简单这没错。但如果你不知道背后发生了什么遇到卡顿、延迟、杂音这类问题会完全没有排查思路。从应用层到底层Android音频播放大概是这么一条链路应用进程调用MediaPlayer或者AudioTrack → 经过Binder进入系统的AudioFlinger服务 → AudioFlinger把数据交给音频HAL硬件抽象层 → HAL驱动底层硬件扬声器、蓝牙、耳机。关键是MediaPlayer自带解码能力你给它一个MP3或者AAC文件它会自己解码成PCM数据再送去播放。而AudioTrack不认任何压缩格式它只接受裸的PCM数据。所以本质上是两种需求一种是你想把已有文件播出来另一种是你手里已经有PCM数据流比如从网络实时拉流、从AudioRecord采集出来、或者自己合成的声音想直接送进声卡。前者用MediaPlayer后者用AudioTrack。1.2 两种API的核心差异与适用场景我用一个表直接对比对比项MediaPlayerAudioTrack输入数据压缩音频文件/流MP3、AAC、FLAC等PCM原始数据是否内置解码器有没有延迟表现较高适合普通播放低适合实时性场景音频焦点处理需手动申请需手动申请音效/均衡器可通过AudioEffect叠加同样可以但更偏底层实际用途音乐播放器、铃声预览、视频配音语音实时对讲、音频可视化、播放AudioRecord采集的数据我个人的经验是判断标准很简单你拿到的数据源是什么格式。如果是文件或网络流且需要解码用MediaPlayer如果数据已经是PCM裸流或者需要极低延迟果断上AudioTrack。补充一个容易忽略的点AudioTrack可以配合AudioRecord实现低延迟的实时回采和回放这在K歌应用、助听器类应用里特别常见。MediaPlayer做不到这种细粒度的数据回调。1.3 声音从哪出来路由和音频设备选择播放之前一定要考虑声音从哪个设备出来不然就会出现“明明耳机没插声音从扬声器出来了”这类问题。Android从API 23开始有完整的AudioRouting接口可以指定播放设备。最常见的是切换听筒和扬声器// 获取当前路由信息 AudioRouting routing audioTrack.getRouting(); // 通过AudioDeviceInfo获取设备列表 AudioManager audioManager (AudioManager) getSystemService(Context.AUDIO_SERVICE); AudioDeviceInfo[] devices audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); for (AudioDeviceInfo device : devices) { if (device.getType() AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { // 指定输出到扬声器 audioTrack.setPreferredDevice(device); } }这里有个坑setPreferredDevice不是立即生效的而且有些设备会忽略这个偏好设置尤其是在蓝牙和有线耳机同时连接的时候。我一般是把可用设备罗列出来让用户自己选而不是强制用代码指定。另外涉及通话场景时还要注意setSpeakerphoneOn这类旧接口新版系统上部分已经废弃建议直接用AudioDeviceInfo做路由。2. 录制方案拆解MediaRecorder和AudioRecord的分工2.1 两条录制路线的底层逻辑录制的选择逻辑跟播放是对称的。MediaRecorder是把麦克风采集到的PCM数据直接编码成AAC或者MP3等格式输出是一个文件你用起来最省事。AudioRecord只负责采集PCM数据编码什么的完全不管拿到原始数据之后你可以自己处理、自己编码也可以直接做实时分析比如音量检测、语音识别。举个具体例子你要做一个录音App录完要保存成m4a文件分享出去用MediaRecorder两行代码就搞定。你要做一个实时语音分析工具检测说话时长、分贝大小那必须用AudioRecord因为MediaRecorder给你的就是封装好的文件拿不到中间数据。2.2 音频源AudioSource参数怎么选很多人在录音时直接填MediaRecorder.AudioSource.MIC实际上这个参数选择直接影响录音效果。Android支持的常用音源有音源常量说明适用场景MIC麦克风系统默认处理AGC、降噪通用录音VOICE_COMMUNICATION针对语音通信优化通常会启用回声消除和降噪语音通话、语音消息CAMCORDER和相机联动适合录像时收音视频录制UNPROCESSED原始麦克风信号不做任何处理API 24专业录音、算法调试VOICE_RECOGNITION针对语音识别优化会关闭某些音效语音转文字我的建议是做常规录音用MIC就行做语音通话类用VOICE_COMMUNICATION要拿原始数据做算法分析用UNPROCESSED。这里特别提醒UNPROCESSED并不保证一定生效有些设备不支持时会回落到MIC所以代码里要判断是否真的处于unprocessed状态否则你的算法数据可能一直在被降噪和AGC污染。2.3 录音参数配置的完整代码框架我以AudioRecord为例把一个最常见的录音初始化写出来private AudioRecord createAudioRecord() { int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_IN_MONO; int audioFormat AudioFormat.ENCODING_PCM_16BIT; int minBufferSize AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioRecord audioRecord new AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, minBufferSize * 2 // 这里乘2是关键后面会讲 ); return audioRecord; }读取数据用read方法可以一次读一块byte[] buffer new byte[4096]; int readSize audioRecord.read(buffer, 0, buffer.length); if (readSize 0) { // 处理buffer里的PCM数据 }这里read是阻塞式的如果buffer一直没填满它就会一直等着。所以录音采集必须在子线程跑不能在主线程读数据。3. 音频参数详解采样率、位深、声道、Buffer3.1 采样率和位深怎么定才不坑采样率的基础概念不复杂每秒采集多少次声音样本。44100Hz是CD标准48000Hz是现在安卓手机最常见的原生采样率。实际使用的时候关键是要知道你的设备原生支持哪些采样率如果设置一个设备不支持的采样率系统会做重采样这会引入额外开销和延迟。你可以用地道的方式查设备原生采样率// AudioTrack可以直接查询设备的native采样率 int nativeSampleRate AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);这个数值通常是48000但是有些设备是44100也有不少设备支持混合采样率。我实测过的大部分中高端手机48000Hz都是最稳的。位深PCM位宽也是一个大项。16bit是绝对主流手机硬件和系统都默认支持得最好。24bit和32bit浮点ENCODING_PCM_FLOAT主要在专业音频设备或者使用AAudio时有优势。普通App用16bit就够了非要追求高动态范围可以用float格式的AudioRecord/AudioTrack但要注意处理数据时的转换工作。3.2 声道数配置不只是mono和stereo的区别声道掩码这个参数经常被人忽略。AudioFormat里的CHANNEL_IN_MONO和CHANNEL_OUT_STEREO等二进制上其实是一个bit位图代表哪些物理声道被使用。播放立体声时如果你把数据源配成了单声道而播放器是立体声常见的做法是把单声道数据填充到两个声道声音不会丢但空间感会差很多。反过来立体声数据喂给单声道播放会发生混音合并音量可能会变高也可能出现相位抵消听起来很奇怪。录制时的注意点更多。大多数手机只有一个麦克风你配置CHANNEL_IN_STEREO去录制实际得到的往往是同一个单声道信号复制了两份。所以我做App时录音一般统一用单声道既省空间又避免兼容问题。3.3 Buffer Size计算与低延迟调优Buffer Size这里面的学问是音频开发里最容易被忽视的坑。直接用getMinBufferSize是最常见的做法但这个返回值真的是“最低可用值”不代表音质好。如果buffer太小录音或播放的频率稍有抖动就会丢数据表现为录音丢字、播放噼里啪啦。我个人的经验是在getMinBufferSize的基础上乘2到4稳定性会好很多。上面的代码里我乘了2实测下来对大多数场景已经够用。如果你在做低延迟实时音频就需要再小一些让你能在“尽量小但不会欠载”之间找到平衡。一个关键公式buffer对应的时长 buffer字节数 / (采样率 × 位深字节数 × 声道数)。比如采样率44100、16bit单声道一秒钟的数据量是44100 × 2 88200字节。如果buffer是4096字节那这个buffer大约只能存46毫秒的声音。一旦处理线程卡顿超过46毫秒播放就会断流。所以低延迟永远是在和稳定性做权衡。要延迟低就得buffer小要稳定不卡就得buffer大。做语音通话这类实时场景我建议先测你的目标机型用不同的buffer大小跑一遍找到不卡顿的最小值不要盲目照抄网上的参数。4. 完整实操写一个可用的录音并播放Demo4.1 权限配置与运行时授权录音必须申请RECORD_AUDIO权限这是危险权限Android 6.0以上必须动态申请。写到AndroidManifest里是第一步uses-permission android:nameandroid.permission.RECORD_AUDIO /如果你的App目标SDK是Android 12API 31以上涉及蓝牙录音等场景可能还需要BLUETOOTH_CONNECT权限。普通录音场景一个RECORD_AUDIO就够了。运行时申请代码不复杂但要注意用户拒绝了之后有“不再询问”的选项你要是只调一次就完事后续录音功能会直接失败。最好处理一下被拒绝后的引导逻辑if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_RECORD_AUDIO); }4.2 用AudioRecord采集PCM并保存我贴一段完整录音的代码做法没有封装简化方便你直接看懂private volatile boolean isRecording false; private AudioRecord audioRecord; private void startRecording() { int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_IN_MONO; int audioFormat AudioFormat.ENCODING_PCM_16BIT; int minBufferSize AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); int bufferSize Math.max(minBufferSize * 2, 4096); audioRecord new AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, bufferSize); isRecording true; File pcmFile new File(getExternalFilesDir(null), record.pcm); new Thread(() - { try (FileOutputStream fos new FileOutputStream(pcmFile)) { byte[] buffer new byte[bufferSize]; audioRecord.startRecording(); while (isRecording) { int readSize audioRecord.read(buffer, 0, buffer.length); if (readSize 0) { fos.write(buffer, 0, readSize); } } audioRecord.stop(); } catch (IOException e) { e.printStackTrace(); } finally { audioRecord.release(); audioRecord null; } }).start(); } private void stopRecording() { isRecording false; }这段代码的核心点读数据循环放在子线程read是阻塞式线程会等数据到了再写文件。注意这个startRecording并不是立刻返回数据而是启动采集后由后台线程持续写入文件。你要做实时处理的话把fos.write换成你自己的回调处理逻辑就行。4.3 用AudioTrack回放PCM数据对应上面的录音回放PCM文件的做法如下private void startPlayback() { File pcmFile new File(getExternalFilesDir(null), record.pcm); int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_OUT_MONO; int audioFormat AudioFormat.ENCODING_PCM_16BIT; int bufferSize AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioTrack audioTrack new AudioTrack.Builder() .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build()) .setAudioFormat(new AudioFormat.Builder() .setSampleRate(sampleRate) .setChannelMask(channelConfig) .setEncoding(audioFormat) .build()) .setBufferSizeInBytes(bufferSize * 2) .setTransferMode(AudioTrack.MODE_STREAM) .build(); new Thread(() - { try (FileInputStream fis new FileInputStream(pcmFile)) { audioTrack.play(); byte[] buffer new byte[bufferSize]; int readSize; while ((readSize fis.read(buffer)) 0) { audioTrack.write(buffer, 0, readSize); } audioTrack.stop(); } catch (IOException e) { e.printStackTrace(); } finally { audioTrack.release(); } }).start(); }这里用了AudioTrack.Builder而不是老式的构造函数推荐大家都改成Builder方式参数更清晰也更容易扩展。MODE_STREAM代表流式写入适合边读边播如果数据量不大也可以一次性写进MODE_STATIC模式播放延迟可以更低。4.4 播放录制参数一致性的关键点录制和播放联调时最容易犯的错是参数不一致。我见过很多人录音用44100、16bit单声道播放时却用48000、双声道结果声音变调或者只有一边响。保证一致性的最稳做法是把录音参数抽成常量或者在文件头记录参数。写个简单的PCM文件头自定义格式播放时先读头信息再初始化AudioTrack就不会出这类问题。5. 踩坑实录常见问题与排查速查表5.1 AudioRecord初始化失败参数组合不合法用老构造函数时如果采样率、声道、编码格式组合起来设备不支持会返回ERROR_BAD_VALUE。我碰到过一种比较隐蔽的情况录音源选了UNPROCESSED采样率设成96000某款中端机上直接初始化失败改成MIC就正常。遇到初始化失败排查路径建议按这个顺序来确认RECORD_AUDIO权限已授予没有授权就会返回ERROR或者直接SecurityException用getMinBufferSize返回合法值来确认组合是否被支持尝试降低采样率到44100或者48000尝试把音源从UNPROCESSED改成MIC检查是不是buffer size填了一个小于最小值的数5.2 播放卡顿欠载和过载播放卡顿通常分两类一类是write速度跟不上欠载、另一类是CPU负载过高过载。欠载的典型表现为播放时每隔一小段就“啪嗒”一声。这通常是你的buffer太小或者写数据的线程被别的任务抢占了。解决思路是增大buffer、提升线程优先级。过载则比较复杂常见的是应用里动画、网络请求占用太多CPU导致音频线程调度不及时。Android的音频回调线程虽然有一定优先级保障但如果你在回调里做了耗时操作比如写文件、加解密一样会翻车。我在实际项目里看到过有人在音频回调里直接做网络上传结果惨不忍睹。5.3 音频焦点处理不当音乐和录音互掐这个坑99%的人会遇到。你开始录音或者播放音乐了别的App同时在播放音乐两边互相打架。正确的做法是播放前申请音频焦点处理焦点丢失时的暂停逻辑AudioManager audioManager (AudioManager) getSystemService(Context.AUDIO_SERVICE); AudioFocusRequest request new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(playbackAttributes) .setOnAudioFocusChangeListener(focusChangeListener) .build(); int result audioManager.requestAudioFocus(request); // 焦点变化 private final AudioManager.OnAudioFocusChangeListener focusChangeListener new AudioManager.OnAudioFocusChangeListener() { Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_LOSS: // 长时间失去焦点暂停播放 break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // 短暂失去焦点比如来电暂停并恢复 break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // 可以降低音量继续播常见于导航播报 break; case AudioManager.AUDIOFOCUS_GAIN: // 恢复焦点重新播放 break; } } };我这里用了API 26的AudioFocusRequest老项目还在用requestAudioFocus旧接口的话建议迁过去。新的焦点框架里AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK这种情况较为常见很多App处理不好导致导航播报时音乐也在大声放很影响体验。5.4 路由切换与设备变更耳机拔插、蓝牙连接断开时系统会发出ACTION_AUDIO_BECOMING_NOISY广播。如果不处理拔掉耳机后声音会直接从扬声器爆出来特别是在地铁上特别尴尬。private final BroadcastReceiver becomingNoisyReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { // 暂停播放或者切换路由 pausePlayback(); } } };注册方式在Android 13及以上要留意动态广播的RECEIVER_NOT_EXPORTED标志。这种系统广播是系统发出的你注册的时候用RECEIVER_EXPORTED或者不去设置导出标志都行但如果你把导出设置成false部分系统广播会收不到。建议按官方推荐的方式使用ContextCompat.registerReceiver。5.5 高版本系统上的兼容性问题Android 12API 31开始很多音频相关的行为更严格了。比如在后台启动录音有明确限制App必须处于前台才能采集数据如果应用进入后台后还继续录音系统会直接杀掉进程。这个行为其实挺好防止App偷录但如果你想做后台录音的App需要提供前台服务并保持通知栏可见否则录音功能会被系统干掉。还有Android 14API 34之后对部分非SDK接口的访问限制更严格了。有些老项目会用反射去拿一些隐藏的音频接口在新系统上大概率会崩建议从一开始就不用这些私有API。5.6 底层参数调优高通平台的Audio AFE补充到这里给做系统层和底层音频开发的读者补充一点。在高通平台上音频前端的AFEAudio Front End和PDNPort Device Number配置会直接影响设备底层的采样率、时钟等。普通应用层开发者接触不到这一层但如果你在调试系统级音频问题或者做音效定制就需要查看设备树和混音器XML中的AFE配置确认某个端口绑定的采样率是多少否则会出现特定采样率下无声、杂音甚至硬件异常。这个领域比较依赖具体平台的文档和代码应用层开发者只需要了解有这一层存在即可出问题时可以把矛头指到系统调音配置上而不是怀疑是自己的参数设置错了。我个人在实际操作中的体会是音频开发最核心的能力不是“会用几个API”而是对参数有敬畏心。采样率、位深、声道、buffer大小每一个参数都是跟硬件和系统在对话改任何一个都可能引发蝴蝶效应。录音播放联调时先把参数锁定再谈优化。最后再分享一个小技巧开发阶段把每次录音和播放的采样率、buffer大小记到日志里复现问题时会发现排查效率能翻倍。这个习惯我保留了很多年基本所有音频疑难杂症的第一步排查都是“先看日志里的参数快照”。