2026/9/13 21:14:04

Android MIUI录音机源码解析与工程化改造指南

Android MIUI录音机源码解析与工程化改造指南 简介面向Android开发者的MIUI小米录音机完整工程源码压缩包适合对音频应用开发、Android系统定制感兴趣的中高级开发者。资源共78个文件其中51张png用于界面预览与操作说明15个xml涉及布局、资源与配置文件8个java文件承载录音、播放及生命周期等核心逻辑另有readme、NOTICE、.gitignore等辅助说明整体包仅880KB结构清晰便于按目录定位关键代码。源码围绕Android音频体系和MIUI特性展开可重点学习MediaRecorder与AudioRecord的参数配置与底层数据读取、RECORD_AUDIO权限申请流程、基于Service的后台录音及通知栏状态反馈、外部存储路径与文件命名策略也可参考其降噪、回声消除等音频处理思路和针对性能的优化手段理解一个录音应用从界面到底层的主要环节。已有173人浏览学习是阅读Android音频框架、复现MIUI录音交互与工程化组织的实用素材。1. Android MIUI 小米录音机源码先把它当成一份“图纸”看待做 Android 开发的人几乎都有机会碰到一个叫 “Android MIUI 小米录音机源码.rar” 的离线包。它不是一份能直接打开的工程而是一堆纸拼图AOSP 的 SoundRecorder 骨架、MIUI 定制的业务类、中文资源和声音处理 so 库有时连 smali 目录也存在。你拿它的目的通常是搞明白 MIUI 录音机怎么做到持续采集、降噪和转文字或者索性把它改成自己能发布的录音组件。实际处理这类包会按这样一个顺序展开先认包的类型再改造成可编译工程接着拆读录音主链路最后把核心 Service 抽成可复用模块并做验证。适合已有 Android 基础、打算进入系统应用和 ROM 方向的人。2. 解开 Android MIUI 录音机源码 rar 后的现场目录树、Manifest 与 smali 辨识拿到 rar 的第一步不是开 Android Studio而是先看解压之后的包里有没有能让人直接编译的工程脚手架。我把这一类包件的检验拆成三件事目录基准、真源码判定、系统应用改造点识别。2.1 标准 AOSP SoundRecorder 目录基准AOSP 的录音机源码本身很小只有一个packages/apps/SoundRecorder模块MIUI 的定制版也保留了这套骨架。解开 rar 后我会先找下面这些标记Recorder/ ├── Android.bp # soong 编译入口AOSP 工程都有 ├── AndroidManifest.xml ├── src/ │ ├── com/android/soundrecorder/ │ │ ├── SoundRecorder.java # 列表与状态管理入口 │ │ ├── RecordService.java # 真正持有 MediaRecorder/AudioRecord 的服务 │ │ └── RecorderParameters.java │ └── com/xiaomi/soundrecorder/ # MIUI 新增的定制包 ├── res/ │ ├── layout/ │ ├── values-zh-rCN/ │ └── drawable-*/ # MIUI 的 icon 和配色 └── libs/ └── armeabi-v7a/ └── miui_audio_engine.so # 音频后处理库非 MIUI 设备上容易加载失败这套结构不是某一份具体源码的标准答案而是 AOSP 基线工程的标准轮廓。你用grep去检查解压目录会发现绝大多数源码包在这个基准上会有增删例如src/改成单个目录、jni/取代libs/。但有一条规律不会变只要Android.bp和AndroidManifest.xml同时出现就能确定它属于系统 make/soong 工程不能直接喂给 gradle 编译。先用两条命令快速建立对包的整体判断# 在解压目录中确认主包名MIUI 定制版常从 com.android.soundrecorder 改为 com.miui.soundrecorder grep -R package --includeAndroidManifest.xml . | head -n 5 # 数一下模块数量判断这个包是否只包含录音机本身 find . -maxdepth 2 \( -name Android.bp -o -name Android.mk \) | wc -l第一条命令的结果决定你后面applicationId写什么。第二条命令说明包体的收拢程度如果只返回 1 到 3 个模块那大部分逻辑都收在应用自身如果返回十几条就要留意代码里是否引用了同仓库里其他系统组件的接口比如SystemUI、Telephony或者公用的framework代码。2.2 真源码与反编译重建的两个判定点网上流传的“源码.rar”名不副实的情况很多。不少包是拿系统安装包反编译后重建的 Java 文件特征非常明显。第一看synthetic和lambda$。反编译工具会把编译器生成的桥接方法原样保留下源码里则很少长这样class RecordService { // 反编译常见产物方法名直接暴露 lambda$ 前缀 public void lambda$startRecording$0() { onRecordingStopped(); } }在解压目录里批量搜一下能快速确认find src -name *.java | xargs grep -l lambda\$ | head -n 5如果结果超过 10 个文件基本可按反编译重建品对待。重建品不是不能改但改动后不能再做“替换一个类”的局部交付必须走完整的源码编译链路而且排查逻辑时要分辨哪些是编译器生成物工作量比真源码大不少。第二看res目录。反编译出来的values目录里会有大量数字资源 ID字符串以0x7f0b0001这种形式被引用因为资源索引被固定过。源码包里的资源 XML 是给人看的键值对strings.xml直接以 key 命名不带十六进制引用号。这也决定了你要不要保留 MIUI 的 UI 层反编译重建的 UI 改起来很痛苦通常只取RecordService和音频处理逻辑UI 用自己工程的layout重写。2.3 MIUI 改造的骨架落在哪识别完上面两点之后我会先立一张判断表记录这个包留给我的改造点。这张表也适合在团队交接时直接贴进 Wiki包内特征代表性表现对二次开发的影响Android.bp存在但无 gradle 文件soong 系统工程需要手动搭 gradle 骨架sharedUserIdandroid.uid.system系统应用签名第三方装机会报 INSTALL_FAILED_SHARED_USER_INCOMPATIBLEcom.miui.soundrecorder独立分包MIUI 定制入口主入口要确认是 MIUI Activity 还是 AOSP Activitylibs/miui_audio_engine.so降噪/后处理引擎非 MIUI 设备可能loadLibrary失败必须有回退逻辑大量lambda$与0x7f资源 ID反编译重建品能读但不宜局部小改建议只提取核心服务这一章的结论是从 rar 到“可改工程”卡点往往不在代码逻辑而在代码之外的构建约定。目录认清楚后面把工程搬进 Android Studio 时才能少走弯路。3. 把录音机源码搬进 Android Studio 编译gradle、Android SDK 与签名处理包里的 Java 源码本身不需要改需要改的是它外层的构建系统。soong 是给整机编译用的Android Studio 只认 gradle。所以第一个任务就是把Android.bp的项目结构转述成 gradle 能读懂的模块配置。3.1 从 Android.bp 翻成 gradle 的最小配置先准备 Android SDK。很多新手卡在 Android Studio 里 SDK 无法勾选最省事的做法是用命令行把需要的版本直接装齐sdkmanager platforms;android-34 build-tools;34.0.0 platform-tools装完 SDK 之后再处理工程骨架。我会新建一个名为app的模块把源码包里的src、res、assets原样映射进去// settings.gradle pluginManagement { repositories { google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // 如果包里带本地 jar/aar用 flatDir 指向 libs 目录 flatDir { dirs libs } } }// app/build.gradle plugins { id com.android.application } android { namespace com.android.soundrecorder // 与 AndroidManifest 里 package 保持一致 compileSdk 34 defaultConfig { applicationId com.android.soundrecorder minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 } sourceSets { main { manifest.srcFile AndroidManifest.xml java.srcDirs [src] res.srcDirs [res] assets.srcDirs [assets] // 若 so 从系统源码树带出放 libs/ 后由 jniLibs 自动打包 jniLibs.srcDirs [libs] } } lint { abortOnError false } } dependencies { // 源码里引用了系统隐藏 API 时用 compileOnly 放一个 stub jar compileOnly files(libs/app_framework_stub.jar) }这段配置里最关键的是namespace和java.srcDirs。namespace决定R类和 manifest 里裸类名的归属必须跟着AndroidManifest.xml的package走不能自己另起一个包名。java.srcDirs则是为了让 gradle 找到com/android/soundrecorder这个目录AOSP 习惯把代码直接放在src底下和 Android Studio 默认的src/main/java结构不同不改这个路径会“找不到符号”。3.2 编译前要处理的三处 Manifest 声明MIUI 的录音机在系统里是按系统应用跑的Manifest 里写了一些只为系统服务的东西。搬成普通应用时第一件事是去掉sharedUserIdmanifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.android.soundrecorder !-- 系统源码里经常有这一行第三方按普通应用安装必须去掉 android:sharedUserIdandroid.uid.system -- uses-permission android:nameandroid.permission.RECORD_AUDIO / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_MICROPHONE / application activity android:name.SoundRecorder android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity service android:name.RecordService android:foregroundServiceTypemicrophone android:exportedfalse / /application /manifest三处需要注意其一是sharedUserId带着它安装必报INSTALL_FAILED_SHARED_USER_INCOMPATIBLE因为普通签名无法匹配系统共享 UID。其二是RECORD_AUDIO是运行时权限需要在代码里用requestPermissions或 Activity Result API 主动申请。其三是 Android 10 后只要录音服务在后台持续跑就必须声明FOREGROUND_SERVICE_MICROPHONE并指定foregroundServiceTypemicrophone否则会抛MissingForegroundServiceTypeException。3.3 用 gradle 命令跑通首包配置完成后直接命令行编译比每次点 Android Studio 菜单更利于排错./gradlew assembleDebug adb install -r app/build/outputs/apk/debug/app-debug.apk # 确认包已安装且组件被系统正确识别 adb shell pm list packages | grep soundrecorderassembleDebug会使用默认 debug keystore 签名第三方手机上可以直接安装。如果源码里带platform权限调用例如录音时读取通话状态那 debug 签名下某些路径会在运行时抛SecurityException这是预期行为调试时先用日志确认走到哪一步而不是把platform签名当作默认选项硬接。系统级签名只应用于你自己的测试机命令不再展开但原理是apksigner拿platform.pk8和platform.x509.pem换签。4. 录音机源码的录音主体MediaRecorder 与 AudioRecord 的分工和落盘编译通过只是第一步。真正有价值的是源码里的录音链路怎么组织。MIUI 录音机的源码里通常有两条并行实现一条走MediaRecorder负责输出 AAC、AMR 这类封装好的文件另一条走AudioRecord用来拿原始 PCM、画波形和支持无损格式。4.1 源码中两条录音链路的分工选择依据不是“哪个更好”而是输出文件形态。录音界面如果只需要一个播放列表MediaRecorder足够如果需要看到实时音量曲线或保存 WAV/FLAC就必须用AudioRecord把 PCM 读出来自己写文件。源码里的常见写法是准备一个startCapture(boolean usePcmPipeline)入口两条分支在调用前就被决定private void startCapture(boolean usePcmPipeline) { if (!usePcmPipeline) { mMediaRecorder new MediaRecorder(); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mMediaRecorder.setAudioEncodingBitRate(128_000); mMediaRecorder.setAudioSamplingRate(48_000); // setOutputFile 可以接收 fd避免传路径字符串带来的权限坑 mMediaRecorder.setOutputFile(mCurrentFileDescriptor); mMediaRecorder.prepare(); mMediaRecorder.start(); } else { int minBufSize AudioRecord.getMinBufferSize( 48_000, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT); mAudioRecord new AudioRecord( MediaRecorder.AudioSource.MIC, 48_000, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, minBufSize * 2); mAudioRecord.startRecording(); // 后台线程里循环 read()把 PCM 写进 FileOutputStream new Thread(this::pumpPcmData).start(); } }参数上的关键点setAudioEncodingBitRate只影响编码文件的压缩质量不改变采样率setAudioSamplingRate与new AudioRecord的第二个参数必须一致否则可能出现只有MediaRecorder分支能稳定工作、AudioRecord分支出声异常。minBufSize * 2是缓冲区倍率常见工程上都取两到四倍来降低read()掉帧概率但倍数过大又会让开始录音后出音量图有明显的延迟需要按机型微调。4.2 音质参数表的四个必调项源码里最终决定文件大小和清晰度的其实是这组参数封装/编码采样率推荐码率每分钟约大小适用场景M4A/AAC48000 Hz128 kbps约 960 KB默认录音兼容性和体积均衡WAV/PCM48000 Hz1536 kbps约 11.5 MB波形编辑、后端音频分析FLAC48000 Hz无损约 5–8 MB高保真收藏牺牲体积和编码耗时AMR8000 Hz12.2 kbps约 90 KB语音备忘录听感偏窄改源码时最容易漏掉的是“编码器与容器不匹配”。把OutputFormat.MPEG_4和AudioEncoder.AAC搭配没问题但想输出 FLAC 时必须把容器改成OutputFormat.FLAC同时采样率保持 48000不少移植项目在这里直接得到损坏文件。AMR 则必须使用OutputFormat.AMR_NB与AudioEncoder.AMR_NB且采样率只支持 8000。4.3 Android 10 之后的 MediaStore 落盘老录音机源码里常直接写mRecorder.setOutputFile(/sdcard/rec.mp3)这在 Android 10 之后的设备上会失败因为应用没有直接写外部存储路径的通用权限。无论源码里原本怎么处理迁移到目标 34 时都应该改成 MediaStore 写入private Uri createMediaStoreEntry(String displayName) { ContentResolver resolver getContentResolver(); ContentValues values new ContentValues(); values.put(MediaStore.Audio.Media.DISPLAY_NAME, displayName); values.put(MediaStore.Audio.Media.MIME_TYPE, audio/mp4); values.put(MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_MUSIC /Recorder); return resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values); } // 录音开始前先拿到可写句柄 ParcelFileDescriptor pfd getContentResolver().openFileDescriptor(mUri, rw); mMediaRecorder.setOutputFile(pfd.getFileDescriptor());RELATIVE_PATH指定的是设备上对用户可见的目录DISPLAY_NAME决定文件名。用openFileDescriptor之后MediaRecorder 只认文件描述符不再关心 URI 来自哪个目录。停止录音后要让媒体库能发现文件只需要保持MediaStore.Audio.Media.EXTERNAL_CONTENT_URI这条记录存在不需要额外scanFile。Android 13 及以上还建议在 Manifest 里声明READ_MEDIA_AUDIO否则用户从文件管理器回看录音列表时可能看到“无权限”。5. 进阶改法通话录音、降噪与定点转写在源码层的落位把默认的麦克风录音链路跑通之后很多人想再把 MIUI 那套通话录音、降噪和转写能力也搬过来。这里要先泼一盆冷水源码包里能直接搬动的部分只有通用 API通话和转写大多要依赖系统权限或远端服务。5.1 通话录音的来源边界VOICE_CALL 只能系统应用触碰普通应用里写MediaRecorder.AudioSource.VOICE_CALL在prepare()阶段就会失败Logcat 能看到start failed: -38。原因是该音频源需要CAPTURE_AUDIO_OUTPUT这类 system/privileged 级别权限普通第三方签名申请不到。MIUI 录音机能做通话录音是因为它在 ROM 里走的是系统音频框架通道和这份源码里的应用层逻辑不是一个层级。如果只是做自己设备的工具应用一个可行的替代方案是 Android 10 的AudioPlaybackCapture它配合MediaProjection抓取正在播放的媒体流MediaProjection projection projectionManager.getMediaProjection(resultCode, data); AudioPlaybackCaptureConfiguration config new AudioPlaybackCaptureConfiguration.Builder(projection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA) .addMatchingUsage(AudioAttributes.USAGE_GAME) .build(); AudioRecord record new AudioRecord.Builder() .setAudioFormat(new AudioFormat.Builder() .setSampleRate(48_000) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .build()) .setAudioPlaybackCaptureConfig(config) .build();注意USAGE_MEDIA只能匹配媒体和游戏流匹配不到电话听筒要抓 VoIP 通话音频可尝试USAGE_VOICE_COMMUNICATION但这取决于应用是否按该类型播放。每次使用 MediaProjection 都要拉起用户授权并且 Android 14 上投影授权默认有时间限制。源码里如果原本直接调了VOICE_CALL移植该分支前先问一句“目标设备是否系统签名”不满足就宁可回退到麦克风录制。5.2 降噪的两个挂点与回退参数MIUI 录音机里的降噪并不都在 Java 层。常见做法是拿AudioRecord拿 PCM再送到libmiui_audio_engine.so做回声消除和噪声抑制迁移到非 MIUI 设备上这个 so 可能加载不成功。通用 API 层的降噪只有一个NoiseSuppressor ns NoiseSuppressor.create(mAudioRecord.getAudioSessionId()); if (ns ! null) { ns.setEnabled(true); } else { // null 表示当前设备/硬件不支持该效果必须回退到普通录音流程 Log.w(Recorder, NoiseSuppressor unavailable on this device); }要点是NoiseSuppressor基于 AudioSession必须在AudioRecord.startRecording()之后创建且依赖硬件 DSP 支持。它不等同于应用层的降噪算法不会有“AI 降噪”那种效果源码包里如果想做更强的降噪应把精力放在 PCM 环路里接 VAD 或第三方算法而不是指望系统这个词。遇到IllegalArgumentException时说明该录音会话已被释放需要在创建器和销毁逻辑里做同步保护。5.3 转写切片的字节和时间换算转写功能在源码里出现的形态通常是“边录边上转”即把 PCM 按时间切片交给识别服务同时把切片时间点绑到最终文件上。切片的换算不是看System.currentTimeMillis()而是按采样数算否则录音开始后设备休眠、线程调度波动都会让标注错位long baseTimeMs SystemClock.elapsedRealtime(); int sampleRate 48_000; int bytePerSample 2; // 16bit int channelCount 2; // stereo // 每读一次缓冲估算该缓冲对应的时长 long offsetFromBaseMs bytesRead * 1000L / (long) (sampleRate * bytePerSample * channelCount);这个换算公式在录音线程里很常见累计字节数除以每秒钟采样字节数得到相对起始点的毫秒偏移。再与baseTimeMs相加就是该段音频在国际标准时间轴上的参考位置。若把声道数或位深写错切片文本会随时间越偏越多前 10 秒看不出来录满一小时就会错位几十秒。源码里如果有已写好的WaveHeader工具类注意它只负责写 WAV 的头部块跟转写时间轴是两套逻辑不要混用。6. 一个可直接落地的技巧把 RecordService 抽成 RecorderKit不想维护整套 MIUI UI 的时候最务实的方式是把录音链路抽成一个独立模块。这里给出一个最小可用的RecorderKit它只做三件事提供开始录音、结束录音、把结果交给调用方。整个抽取过程基于前文已经跑通的 MediaStore 逻辑不再挂任何系统权限。public class RecorderKit { public interface Callback { void onFinished(String uri, long durationMs); } private MediaRecorder recorder; private Uri outputUri; private long startElapsedMs; public void start(Context context) throws IOException { startElapsedMs SystemClock.elapsedRealtime(); outputUri createMediaStoreEntry(context); ParcelFileDescriptor pfd context.getContentResolver() .openFileDescriptor(outputUri, rw); recorder new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); recorder.setAudioSamplingRate(48_000); recorder.setAudioEncodingBitRate(128_000); recorder.setOutputFile(pfd.getFileDescriptor()); recorder.prepare(); recorder.start(); } public long stop() { if (recorder null) { return 0; } recorder.stop(); recorder.release(); recorder null; return SystemClock.elapsedRealtime() - startElapsedMs; } }抽取后要验证的不只是“能录出文件”还要验证异常路径录制中切到后台、被系统杀掉、存储空间满三件事在系统录音机里都有完整兜底。测试命令可以直接走 adb不需要打开界面# 先授予录音权限再拉起一个带 RecorderKit 的测试页面 adb shell pm grant com.example.recorderkit android.permission.RECORD_AUDIO adb shell am start -n com.example.recorderkit/.MainActivity adb shell sleep 10 adb shell content query --uri content://media/external/audio/media \ --projection _display_name:duration:date_added \ --sort date_added DESC | head -n 5最后一条命令会按时间倒序列出最近插入的媒体记录如果看到 10 秒左右的 duration就说明 RecorderKit 的写入链路完整。接着再测一次“开始录音后立即锁屏”看foregroundServiceTypemicrophone是否真的生效Logcat 里若出现MediaRecorder start failed说明 service 进程在屏幕熄灭时被系统降级需要在onStartCommand里补startForeground()。这一步验证完从 rar 里拿到的录音机源码才算真正变成了能交出去的录音组件。本文还有配套的精品资源点击获取