尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android录音开发避坑指南:MediaRecorder与AudioRecord选型及适配实战

Android录音开发避坑指南:MediaRecorder与AudioRecord选型及适配实战 简介压缩包内是一套完整的安卓录音机示例项目面向正在学习Java与安卓音频开发的初学者重点演示MediaRecorder类的实际用法。项目中不仅包含录音权限声明与动态申请、麦克风音频源选取、输出格式与音频编码器配置还涉及录音文件存储路径设置、录制状态监听、错误处理、线程管理以及比特率与采样率调节等知识点完整覆盖开发录音功能的核心链路。压缩包共41个文件以3个Java源文件、15个XML布局及配置、3个Gradle构建脚本为主辅以PNG图标和项目辅助文件整体体积仅146KB工程结构清晰从构建脚本到页面布局再到业务代码均有对应模块便于快速定位与学习。已有313人学习下载适合希望借助完整可运行工程理解录音机实现原理并在此基础上扩展功能的读者。 做录音功能这几年我踩得最深的一个坑不是麦克风权限而是Android碎片化本身。同样是MediaRecorder同一套代码在Pixel上一切正常换到某国产ROM上录出来的文件要么是0字节要么播放时只有前3秒有声音。这篇我把录音机从选型到上线的完整链路拆开讲包括权限、编码、存储、生命周期这些环节里最容易出问题的地方以及我实测下来的处理方案。1. 录音链路的第一道分岔口MediaRecorder还是AudioRecord1.1 MediaRecorder看起来很美好但它替你做了太多决定MediaRecorder是Android官方封装好的高阶录音API从麦克风取数据、编码、写入文件一条龙全包。对大多数场景来说它的确够用代码量少出错概率也低val recorder MediaRecorder() recorder.setAudioSource(MediaRecorder.AudioSource.MIC) recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) recorder.setAudioSamplingRate(44100) recorder.setAudioEncodingBitRate(128000) recorder.setOutputFile(filePath) recorder.prepare() recorder.start()但问题恰恰出在“替你做了太多决定”上。它内部有一套默认参数和行为逻辑一旦机型对某些参数支持不好它不会报错而是悄悄给你降级或产出异常文件。我遇到过最典型的情况部分低端机型对sET_AUDIO_SAMPLING_RATE(48000)支持不完整录出来的音频在播放时明显变调而MediaRecorder并不抛异常只有录制完去MediaPlayer播放时才暴露。1.2 AudioRecord可控性更强但工程量也大不少AudioRecord是更底层的API它只负责从麦克风读取原始PCM数据编码和封装全部由你自己处理。它的优势是你可以拿到每一帧音频数据自己做降噪、AGC自动增益控制、静音检测也可以在录音过程中实时做音量可视化。代价同样明显。你要自己维护一个写入线程用AudioTrack做回放时还要对齐参数如果要做AAC编码还得引入MediaCodec或者硬编码器工程量不是一个量级。我做过一个需要实时显示波形并支持暂停恢复的录音机最后选了AudioRecordMediaCodec的方案代码量比MediaRecorder多了三倍不止。1.3 我的选型建议按需求形态决定这里没有银弹我整理了一个判断标准基本能覆盖大多数需求场景需求特征推荐方案原因简单录音、后台录制、文件上传MediaRecorder系统封装完整省心需要暂停/恢复、实时波形、变声处理AudioRecord MediaCodec数据可控可做中间处理需要边录边传流媒体AudioRecord 自定义编码线程帧级别控制可分段推送需要通话录音系统级系统API / 辅助功能方案普通方案拿不到通话音频流我自己的主力录音应用选的是MediaRecorder但把“暂停恢复”做成了重新起一段文件然后内部拼接的方式。原因很简单项目上线时间紧MediaRecorder没有原生暂停接口API 24以下而自己用AudioRecord重写整个录制模块测试周期至少翻一倍。对商业项目来说稳定性和交付时间比架构上的纯粹更重要。2. 录音权限适配从Android 6到Android 14每个版本都在给你挖坑2.1 运行时权限只是第一道门槛Android 6.0引入运行时权限后录音权限RECORD_AUDIO需要在运行时动态申请。很多新手在这里只处理了“用户点击允许”的分支忽略了“用户勾选了不再询问”的分支。一个健壮的逻辑应该是首次申请失败引导用户去设置页手动开启判断shouldShowRequestPermissionRationale()如果返回true说明用户拒绝过但没勾选“不再询问”这时候弹自定义对话框解释用途再申请一次如果返回false且权限未授予说明用户已经选择了不再询问直接跳转Settings.ACTION_APPLICATION_DETAILS_SETTINGS。跳转设置页不是终点还要在onResume里面重新检测权限状态因为用户可能从设置页返回但没有开启权限这时候如果直接开始录音MediaRecorder会抛出RuntimeException而且是瞬间崩溃级别的。2.2 前台服务类型Android 14把这条路也堵了一半Android 14API 34要求如果你的应用 targetSdk 34 且要在后台使用麦克风必须声明前台服务类型microphone并在AndroidManifest.xml里加上权限uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_MICROPHONE / uses-permission android:nameandroid.permission.RECORD_AUDIO / service android:name.RecorderService android:foregroundServiceTypemicrophone android:exportedfalse /同时启动前台服务时startForeground()必须传入ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE。这里有个很多人忽略的细节如果应用切到后台但前台服务已经启动了Android 14仍然允许继续录音但会在通知栏持续显示麦克风指示器。这是系统行为不是你写错了别慌。2.3 一个被忽视的权限MODIFY_AUDIO_SETTINGS很多人录音时发现音量不对或者录出来声音小得离谱其实很可能不是硬件问题而是没有申请MODIFY_AUDIO_SETTINGS权限。这个权限用来调整音频参数比如设置AudioManager.STREAM_MUSIC的音量、切换扬声器和听筒对于处理录制过程中的音频焦点、回声消除都很关键。val audioManager getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager.setParameters(noise_suppressionon) audioManager.setParameters(echo_cancellationon)没有这个权限某些机型上setParameters调用会静默失败但你也不知道它失败了直到用户反馈录音有回声。这类问题在模拟器上永远复现不出来只有真机才暴露。3. 编码格式与参数组合录出来能播但不代表参数是对的3.1 容器格式3gp、m4a、mp3我最后选了这个MediaRecorder支持的输出格式常见有MPEG_4、THREE_GPP、AMR_NB、WEBM。如果你追求通用性和音质平衡MPEG_4封装的AAC音频是最好的选择几乎全平台可播放文件体积也合理。THREE_GPP3gp是老设备时代的产物文件更小但音质受限。AMR_NB只适合语音短消息采样频率只有8kHz放音乐或者环境音基本没法听。我个人最终选了MPEG_4 AAC采样率44.1kHz比特率128kbps。这个组合在所有测试机型覆盖Android 8到Android 14包括低端机和部分定制ROM上兼容性最佳。3.2 采样率、比特率、声道数到底怎么配参数不是越高越好高参数带来的收益远小于兼容性风险。以下是我实测过的汇总采样率比特率声道适用场景实测问题44100128kbps立体声通用录音兼容性最好48000192kbps立体声音乐录制部分机型音调异常4410064kbps单声道语音/讲座体积小人声够用1600032kbps单声道语音识别识别率高但音质差需要注意一个坑如果你设置的是立体声但手机只有单麦克风部分机型会直接崩溃或者产生损坏文件。所以稳妥做法是在初始化MediaRecorder前检测设备是否支持多麦克风或者干脆默认单声道。我的做法是做一个设置项默认单声道用户手动开启“高质量立体声”避免默认状态翻车。3.3 一个容易被忽略的参数AudioSourceMediaRecorder.AudioSource.MIC是最常用的但如果你在录制过程中电话打进来或者语音助手被唤醒MIC源会被系统打断录音就断了。一些场景下可以改用VOICE_RECOGNITION它能绕过某些系统级的音效处理让录音内容更符合语音识别的输入要求。不过VOICE_RECOGNITION在部分机型上不支持需要做降级val source if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { MediaRecorder.AudioSource.VOICE_RECOGNITION } else { MediaRecorder.AudioSource.MIC }另一个选择是UNPROCESSEDAPI 24它表示原始未处理的音频流做专业录音或者后续做音频处理时更合适但同样存在机型兼容问题需要运行时查询AudioManager.getProperty(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED)。4. 文件保存策略Scoped Storage进化到Android 14存储方案必须按年限重写4.1 为什么要绕过MediaStore直接写文件早期Android版本应用可以直接写/sdcard/下的任意路径那时候录音文件保存非常简单Environment.getExternalStorageDirectory()一把梭。Android 10强制分区存储后直接写公共目录已经不行了你有两个选择写到应用专属目录无需权限或者通过MediaStore写入公共音频目录。我的建议是录音文件优先写到应用专属目录除非你的产品定位是录音文件管理器用户有明确的导出需求。原因有二一是省去适配分区存储的麻烦二是应用卸载时文件自动清理不会给用户留垃圾。对于需要导出的场景用MediaStore再拷贝一份即可。4.2 MediaStore音频目录的坑我踩了三次第一次做MediaStore导出时我直接往MediaStore.Audio.Media.EXTERNAL_CONTENT_URI插入数据然后拿到content://Uri开始写本地测试没问题。上架后用户反馈录音文件找不到查了半天才发现问题部分机型尤其是定制ROM在应用卸载重装后MediaStore的条目还在但实际文件已经被系统清掉了导致播放时直接报错。正确的做法是插入MediaStore前先判断RELATIVE_PATH是否合法并检查目标目录是否存在。Android 10推荐写法val values ContentValues().apply { put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) put(MediaStore.Audio.Media.MIME_TYPE, audio/mp4) put(MediaStore.Audio.Media.RELATIVE_PATH, Music/MyRecorder) } val uri contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)另外RELATIVE_PATH必须包含主目录名如Music/不能直接写MyRecorder否则某些机型直接抛IllegalArgumentException。4.3 文件命名和时间戳的细节处理录音文件命名不要用yyyyMMdd_HHmmss这种简单格式就直接上我遇到过一个极端的用户他在同一秒内录了三次音三个文件全被覆盖了因为他手速太快加上系统时间被自动校正后往回跳了一秒。稳妥做法是加一个毫秒级时间戳或者用System.currentTimeMillis()加随机后缀val timestamp SimpleDateFormat(yyyyMMdd_HHmmss_SSS, Locale.US).format(Date()) val fileName rec_${timestamp}_${(1000..9999).random()}.m4a这个改法虽然很小但能避免一类数据丢失的严重投诉。5. 录音中段的争夺战AudioFocus和生命周期谁抢走了我的麦克风5.1 不做AudioFocus处理录音会被电话打断到死启动录音前应该请求音频焦点AudioFocus否则来电、导航语音、其他应用播放音乐都会抢走系统的音频焦点你的录制会莫名停止。虽然RECORD_AUDIO权限不代表焦点但焦点被抢占时某些机型会同时切断录音输入。处理方式val audioManager getSystemService(Context.AUDIO_SERVICE) as AudioManager val focusRequest AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run { setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build() ) setAcceptsDelayedFocusGain(true) build() } val result audioManager.requestAudioFocus(focusRequest) if (result ! AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // 提示用户当前有高优先级音频正在使用建议稍后再录 }注意录音场景请求焦点时不要用AUDIOFOCUS_GAIN_TRANSIENT因为录音通常是一个持续行为瞬态焦点会在音频播放结束后立刻释放导致你录到一半焦点就被系统收回。5.2 从后台回到前台时的录音状态检查很多应用在切后台时录音正常但从后台回前台时用户会误触了通知栏的“停止”按钮或者系统在低内存时杀掉了录制服务。因此onResume里要做一次状态同步检查MediaRecorder是否还在录制如果不在更新UI状态并弹Toast告知用户“录音已被系统中断”。这个看似简单的检查能避免大量“为什么我录了一半没有保存”的投诉。5.3 监听耳机插拔和音频设备切换如果用户插着耳机录音拔掉耳机的那一刻部分机型的录音源会自动切换导致后续录制的音频声音突然变小甚至无声。处理方法是注册ACTION_HEADSET_PLUG广播在耳机拔出时提示用户并且如果要继续录音重新start一次录制。这一块我在日志里见过太多类似case了属于“你不主动处理系统不会帮你处理”的类型。6. 真机排错实战为什么我的录音中断了、没声音了、文件坏了6.1 最经典的崩溃start() 抛 RuntimeExceptionMediaRecorder.start()抛RuntimeException的原因通常是上一次录音没有正确stop()和release()资源被占用了。一个容易踩的坑是录制过程中用户点了停止你的代码在stop()之前没有判断isRecording状态结果重复调用了stop()直接崩掉。标准做法是加一个isRecording标志位并在stop()外面包一层fun stopRecording() { if (!isRecording) return isRecording false try { recorder?.stop() } catch (e: RuntimeException) { // 捕获stop异常保证release一定执行 e.printStackTrace() } finally { recorder?.release() recorder null } }stop()抛RuntimeException很常见尤其是录制时长为0时它内部状态不对就会抛。一旦捕获异常文件通常是坏的需要主动删掉防止用户播放一个空白文件导致误判“应用bug”。6.2 文件时长对不上或无法播放这种情况90%是编码器没有正常结束。MediaRecorder.stop()之前调用了reset()导致编码器没有写入moov原子MP4的索引信息文件自然无法拖动进度条或直接被播放器判定为损坏。正确顺序永远是stop()-release()- 然后才做文件操作。另外如果输出路径是content://Uri不要对得到的Uri直接拼接路径要用ContentResolver.openFileDescriptor()获取FileDescriptor再交给MediaRecorder.setOutputFile(FileDescriptor)。我见过有人把content://转成/storage/emulated/0/...再写这在部分国产ROM上会直接抛FileNotFoundException。6.3 部分机型录音声音特别小排查链路这个问题的本质是麦克风增益策略不同。有的机型默认开启了AGC自动增益控制有的没有。排查时可以抓dumpsys media.audio_flinger看音频流参数但实际修复路径通常是下面几条确认没有路径写死音量AudioManager.setStreamVolume()不会影响录音增益但有些定制ROM有“录音音量增强”的系统设置项需要用户手动开启检查是否用了VOICE_RECOGNITION源部分机型给这个源做了降噪处理声音会更小更干试一下MediaRecorder.setAudioEncodingBitRate()调高比特率有用户反馈从64kbps提到128kbps后低端机录音“听起来声音变大了”——其实不是音量变大了而是低频保留了更多听感上更饱满。如果APP内做了实时音量可视化还可以顺手加一个“录制电平过低”的提醒逻辑用MediaRecorder.getMaxAmplitude()在录音过程中轮询连续几秒峰值都低于一个阈值就提示用户“当前录音环境可能过静或麦克风被遮挡”。这个值不是线性的大概阈值取20000-32767范围比较合理实测在不同机型上都还算灵敏。6.4 后台录制被系统干掉怎么处理即使用了前台服务低内存时系统也可能降级杀死你的进程。Android 12开始引入了foregroundServiceType限制如果你声明了microphone类型那么在前台服务运行期间系统会持续显示麦克风指示器有助于降低被杀概率。但还是建议在onTaskRemoved里判断一下如果用户从最近任务划掉了应用但录音还在进行应该弹一个通知让用户确认是否停止千万别静默杀掉前台服务否则后台录着录着突然断了用户完全无感知。这里有个我在生产环境踩过的坑startForeground()必须在Service.onStartCommand()之后短时间内调用否则会抛ForegroundServiceDidNotStartInTimeException。如果你在onCreate里做了耗时操作再去startForeground很容易超过系统限制的5秒窗口。7. 收尾前再补一句录音权限的合规提示不能省这部分容易被忽略。如果你的应用需要上架应用商店安卓的隐私政策会检查是否有动态权限申请弹窗同时要求“在隐私政策中明确说明录音数据用途”。我的建议是录音前用一次性弹窗先说明“录音仅用于本地保存不会上传”再做系统权限申请。这个前置说明能提高权限通过率也能减少因权限被拒导致的录制失败。记住前置说明的文案一定不能说谎如果后期产品增加了语音上传功能务必同时更新隐私政策和弹窗文案。做录音功能不难难的是把每一个边界情况都按死。上面这些内容是我从自己项目中一条条排雷排出来的按这个顺序走一遍能挡掉80%以上的录音问题。剩下的20%留给你的测试团队在真机上折腾吧。本文还有配套的精品资源点击获取
返回列表