news 2026/10/15 14:26:45

Android录音完整实现:MediaRecorder与AudioRecord避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android录音完整实现:MediaRecorder与AudioRecord避坑指南

简介:面向初学Android录音功能的开发者,资源完整演示了通过MediaRecorder采集麦克风音频、设置编码格式与输出路径,以及使用MediaPlayer播放录音文件的实现方式,可帮助快速掌握录音播放全流程的关键API与异常处理。压缩包共44个文件、约1.62MB,包含Java源码、XML布局与配置、编译生成的class与dex文件、可直接安装的APK,以及工程依赖的jar包和PNG图片资源,结构完整,适合导入Eclipse工程直接运行对照。目前已有2228人学习下载,可满足边看边练、理解录音权限与文件存储细节的进阶需求。核心价值在于可复用的完整工程模板,涵盖界面布局、录音开始停止与播放结束监听、6.0以上运行时权限申请等模块,省去从零搭建和排查环境问题的过程。

1. 录音功能不是 new 一个 MediaRecorder 就完事:这份代码帮你把路铺平

Android 录音这个需求,看起来就是「申请权限、创建 MediaRecorder、start、stop、release」五连招,但真在项目里做过的开发者都清楚,这里面藏着权限弹窗时机、生命周期打断、文件格式选择、播放不同步、模拟器假死一整套连环坑。尤其当你拿着「待机 30 分钟、电量 5% 时启动录音」这类需求去找方案时,网上的代码碎片根本不够用。这份录音完整代码覆盖了 MediaRecorder 与 AudioRecord 两条路线,从权限动态申请到录音、停止、恢复、释放、播放验证的完整闭环,还带规避低频翻车的防御性写法。适合两类人:一是功能模块需要快速落地的业务开发,二是想在录音这块把边界参数搞清楚的进阶者。读完你至少能少踩五个真实的坑。

2. MediaRecorder 与 AudioRecord:先选对路线,再动手不迟

2.1 两条路线的本质区别:封装与裸数据

MediaRecorder 是系统封好的录音器,你只管设置音频源、输出格式、编码格式,它自己处理编码、封装、文件写入。AudioRecord 则是把麦克风的 PCM 裸数据直接交到你手里,所有编码、压缩、成文件的操作都要自己做。简单说,前者是「傻瓜相机」,后者是「单反手动挡」。

选择依据就三条:

  • 只要最终存成一个文件、能播放就行,选 MediaRecorder,代码量小,稳定。
  • 要做实时波形显示、分贝检测、语音唤醒前级,或需要把音频数据做二次处理(降噪、变调、水印),选 AudioRecord。
  • 不要被「MediaRecorder 格式单一」这个旧印象带偏,它现在能输出 m4a、mp4、3gp,覆盖绝大多数国内 Android 机的播放场景。

资源包里两条路线都写了,我一般建议先看 MediaRecorder,复杂度低,适合项目主流程;AudioRecord 作为进阶理解 PCM 的补充。如果你最后做的是通话类 App,AudioRecord 那条路绕不开。

2.2 格式与编码的参数选型:m4a 还是 3gp

MediaRecorder 的 setOutputFormat 和 setAudioEncoder 必须配对,乱配直接抛异常。我常用的组合:

场景输出格式音频编码封装文件
通用录音(微信语音类)MPEG_4AAC.m4a
低版本兼容THREE_GPPAMR_NB.3gp
高音质采集MPEG_4AAC.m4a
# 注意:MPEG_4 配 AMR_NB 会直接崩,这是固定约束

m4a 已经是 Android 4.4 之后的默认安全选择,采样率 44100、单声道、比特率 128kbps 就能满足日常人声录音的质量。3gp 只在应对 API 16 以下的老设备时才值得考虑——那个年代的硬件解码器对 AAC 支持不完整。现在的项目 targetSdk 普遍 33 以上,直接 m4a,不做兼容换算。

2.3 AudioRecord 的参数理解:缓冲区大小算错就无声

AudioRecord 绕不开一个概念:最小缓冲区大小。它是系统根据采样率、声道数、采样位深算出来的硬性要求,传小了直接初始化失败,这是新手的典型翻车点。常见参数设置:

int sampleRate = 44100; // 44.1kHz,大多数设备可支持 int channelConfig = AudioFormat.CHANNEL_IN_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioRecord recorder = new AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, bufferSize );

bufferSize 的返回值是系统建议的最小分配字节数,你可以在它基础上乘 2 作为实际缓冲,避免读数据时偶尔的饥饿。另一个边界:麦克风输入源统一用 MediaRecorder.AudioSource.MIC,别用 VOICE_CALL 或 CAMCORDER——一个是通话上行通道,一个是摄像头方向拾音,走错了录出来要么没声音,要么音质怪得要命,华为系手机上这个问题尤其明显。

3. 完整实现:从权限动态申请到生成一段可播放的音频文件

3.1 权限申请:不要进页面就弹窗

Android 6.0 之后录音权限是动态申请的,而且必须在启动录音前实时检查。很多开发者把权限申请放在 onCreate,一进页面就弹窗,用户顺手拒绝后整个录音功能变成黑匣子,点哪都没反应。正确做法是:用户真正按下录音按钮的那一刻再发起申请,申请通过后直接进入录音状态。

private boolean ensureRecordPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { return true; } ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_RECORD_AUDIO); return false; }

这套代码有两个细节值得说明:checkSelfPermission 每次 start 前都要执行,因为用户在系统设置里随时可能关掉录音权限,你只检查一次的话,后台回来后录音器会一直初始化失败;requestPermissions 是异步回调,不能指望返回值直接继续业务,所以 ensureRecordPermission 返回 false 时,启动录音动作应该直接终止,等 onRequestPermissionsResult 回调里再继续。

权限弹窗被拒绝后还有一层判断——如果用户勾选了「不再询问」,后续 requestPermissions 不会再弹窗,onRequestPermissionsResult 里直接收到 denied。此时必须兜底:要么跳系统设置引导,要么把录音按钮置灰展示文案,不能让用户觉得按钮坏了。

@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode != REQUEST_RECORD_AUDIO) { return; } if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startRecordInternal(); // 权限到位,真正启动 } else { boolean showRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO); if (!showRationale) { // 用户点了“不再询问”,引导去设置页 } } }

3.2 MediaRecorder 实现:带防御性判空与异常兜底

MediaRecorder 的状态机很敏感:初始化、start、stop、reset、release 每一步都要求前置状态正确,乱序调用直接 IllegalStateException。所以完整代码里每一步都做了状态的显式管理,不只在注释层面,而是定义了一个 int 常量记录当前状态。

private static final int STATE_IDLE = 0; private static final int STATE_RECORDING = 1; private int currentState = STATE_IDLE; private MediaRecorder mediaRecorder = null; private void startRecordInternal() { if (currentState == STATE_RECORDING) { return; } try { mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setAudioSamplingRate(44100); mediaRecorder.setAudioEncodingBitRate(128000); File recordFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "record_" + System.currentTimeMillis() + ".m4a"); mediaRecorder.setOutputFile(recordFile.getAbsolutePath()); mediaRecorder.prepare(); mediaRecorder.start(); currentState = STATE_RECORDING; } catch (Exception e) { releaseRecorder(); // 异常时释放干净,避免下一次 start 变成 IllegalStateException } } private void stopRecordInternal() { if (currentState == STATE_RECORDING) { try { mediaRecorder.stop(); // stop 在 start 后必须调用一次,否则 release 时可能崩 } catch (RuntimeException e) { // 没有有效数据时 stop 会抛异常,这里吃掉,靠文件长度兜底 } releaseRecorder(); currentState = STATE_IDLE; } } private void releaseRecorder() { if (mediaRecorder != null) { mediaRecorder.release(); mediaRecorder = null; } }

参数说几个关键点:setAudioSamplingRate 写 44100 是给 MediaRecorder 一个预期的采样率基准,部分机型不支持时系统会自动修正到最接近值,不影响后续播放;getExternalFilesDir 路径属于应用私有外部目录,不需要存储权限,这是很多旧代码还在申请存储权限的根本原因——他们用了 Environment.getExternalStorageDirectory()。stop 那个 catch RuntimeException 不是玄学,是 MediaRecorder 官方文档里明确写的:start 之后立刻 stop,或者录音时长过短,底层拿不到有效数据,stop 会崩,这个在低端机上出现频率很高。

3.3 AudioRecord 实现:裸数据写入与状态轮询

AudioRecord 的部分就奔着「可实时处理」去的。用一个线程循环读取缓冲区,读到多少写多少,全部完成之后再把 PCM 文件转成 WAV(这个步骤在最后一章展开)。这里直接看写入线程:

private AudioRecord audioRecord = null; private volatile boolean isRecording = false; private Thread recordThread = null; private void startAudioRecord() { int minBuffer = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(minBuffer, 2048)); isRecording = true; recordThread = new Thread(() -> { File pcmFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "record_" + System.currentTimeMillis() + ".pcm"); try (FileOutputStream fos = new FileOutputStream(pcmFile)) { byte[] buffer = new byte[minBuffer * 2]; while (isRecording) { int read = audioRecord.read(buffer, 0, buffer.length); if (read > 0) { fos.write(buffer, 0, read); } } } catch (IOException e) { isRecording = false; } }); recordThread.start(); } private void stopAudioRecord() { isRecording = false; if (audioRecord != null) { audioRecord.stop(); audioRecord.release(); audioRecord = null; } if (recordThread != null) { try { recordThread.join(1000); } catch (InterruptedException ignored) { } recordThread = null; } }

read(buffer, 0, buffer.length) 的返回值是实际读到的字节数,一定要用这个返回值作为写入长度,不能用 buffer.length 直接写——底层 DMA 可能没填满整个缓冲区,数据尾部全是 0,播出来就是一段沙沙声。写成 minBuffer * 2 是为了减少读空次数,但注意分配这个 byte 数组不要拿到新线程外面去,避免 GC 压力集中在录音线程上。stop 用 volatile boolean 标记位,配合 join 确保线程退出时 FileOutputStream 能正常关闭。如果你在这个基础上要叠加实时分贝检测,直接在 read 到数据之后加一个 RMS 计算就行。

3.4 播放验证:先把文件放出去听一遍

录完不播等于白录。MediaRecorder 产出的 m4a 用 MediaPlayer 播放即可:

private void playRecordedFile(String filePath) { MediaPlayer player = new MediaPlayer(); try { player.setDataSource(filePath); player.setOnCompletionListener(mp -> mp.release()); player.setOnErrorListener((mp, what, extra) -> { mp.release(); return true; }); player.prepare(); player.start(); } catch (IOException e) { // 文件已经被删除或者格式损坏时走到这里 } }

这段播放器代码的细节在 prepare 之后自己 start,而不是 setOnPreparedListener 里 start。因为 setDataSource 配本地文件时,prepare 是同步的,直接 start 没问题;如果数据源换成网络地址,就必须走异步 prepare。另一个值得注意的:MediaPlayer 的 release 放在 onCompletion 和 onError 两个回调里做,防止用户播一半退出导致资源泄漏。

AudioRecord 生成的 PCM 文件不能直接丢给 MediaPlayer,它没有文件头,播放器不知道采样率、声道数、位深。要么转成 WAV 再播,要么用 AudioTrack 边读边放。完整代码里选了 WAV 转换,理由很简单——转换后的文件能交出去给别的 App 处理,AudioTrack 只能自己本地放。

4. 录音状态机与 Activity 生命周期:onPause 不等于 stop

4.1 录音动作与页面状态的耦合:后台、来电、锁屏

录音功能最容易翻车的就是「用户把 App 切后台了」。Activity 进入 onPause 不代表录音要停——微信语音后台录制是常规操作,但是来电了、系统要杀进程了,你的 MediaRecorder 还挂在一条可能被回收的 Activity 链路上,这就危险。

正确耦合方式不是「onPause 就 stop」,而是分层处理:录音状态机由业务层控制(开始、暂停、停止),Activity 只在「用户明确取消」「系统即将销毁」两个时机介入。onStop 里不应该碰录音器,但 onDestroy 里必须做资源兜底释放,防止进程被杀时录音文件损坏。

表:不同生命周期节点的推荐动作

生命周期操作理由
onPause不做处理App 仅失去焦点,录音可以继续
onStop不处理录音器后台录音业务在服务中运行
onDestroyrelease 录音器防止 MediaRecorder 挂后台泄漏
onConfigurationChanged保持录音状态旋转屏幕不应重启录音器

4.2 暂停与恢复:MediaRecorder 没有 pause,自己补状态

MediaRecorder 从 API 24 才提供 pause,但实际设备兼容性参差。如果你想在较低版本上做暂停,常见的做法是 stop 并记录当前时间,恢复时开一个新文件——这个方案的问题在文件碎片化。另一个做法是继续录,但标记时间段,事后裁剪。完整代码里我采用「记录段内时间戳」策略:暂停后不真正 stop,而是通过人为跳过写入窗口来制造暂停效果。

这段逻辑依赖 AudioRecord 路线才能实现:

// 暂停点:只是把写入标记位翻 false,不释放 AudioRecord isPaused = true; // 恢复点:翻转标记,继续写 isPaused = false;

这样做的优势是录制线程和硬件资源都不需要重建,代价是没有真正的「暂停语义」,AudioRecord 底层仍在产生数据,只是你的线程不写了。如果业务要求暂停期间完全不采集(省电、省内存),那只能 stop 后重启。注意:MediaRecorder 没有 pause 时,切后台再回前台声音会缺——设备不在前台,系统可能自动降采样或静音,这不是你的 bug,是厂商策略,处理方式是给用户一个明确的对录音状态不感知的提示。

4.3 录音时长上限与文件大小上限

录音录到天荒地老不是好事。系统也好、播放器也好,对单个音频文件时长和体积都有隐性上限。MediaRecorder 自带两个 setter:setMaxDuration 与 setMaxFileSize。单位分别是毫秒和字节。

mediaRecorder.setMaxDuration(60 * 60 * 1000); // 最长 1 小时 mediaRecorder.setMaxFileSize(50 * 1024 * 1024); // 最大 50MB

到了上限之后,MediaRecorder 会回调 OnInfoListener,info 参数是 MEDIA_RECORDER_INFO_MAX_DURATION_REACHED 或 MAX_FILESIZE_REACHED。很多人不知道这个回调要自己实现才生效:

mediaRecorder.setOnInfoListener((mr, what, extra) -> { if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { // 时间到,走正常 stop 流程 } });

不处理这个回调的后果是:录音到了上限后底层自动停,但你的 App 还认为它在录,界面上秒表一直走,点停止的时候 stop 方法崩异常。完整的代码包里在这儿加了状态同步,我实测过,不处理的信息回调是录音功能线上崩溃的前三名来源之一。

5. 避坑排查:四条最常见的录音翻车记录与现场解法

5.1 模拟器上永远录不出声音,真机一测就正常

现象:模拟器上点击录音,Application 不报错,MediaRecorder start 正常,但生成的 m4a 文件播放出来全是静音或在 2 秒处戛然而止。

原因:Android 模拟器对麦克风虚拟化的实现很弱,多数模拟器默认根本没有音频输入设备。MediaRecorder 初始化时不会感知这个缺失,它只是拿到一个空数据流。另外模拟器的 getMaxAmplitude 经常返回 0,这会让基于音量检测判断「是否有人在说话」的逻辑直接死掉。

解决:开发阶段一律真机联调,模拟器只用来验证 UI 布局、权限流程、按钮禁用逻辑。如果你必须在模拟器上跑通流程,把录出来的文件放到 PC 上检查文件头是否完整(WAV 文件头 44 字节,m4a 能正常打开),但不要指望模拟器里的录音效果有任何参考价值。这个坑的教训是:凡是「录音没声音」的 bug,第一反应先排除模拟器,再查代码。

5.2 录音到一半退出页面,文件是 0 字节

现象:用户在录音过程中直接按返回键退出 Activity,下次进来看之前录的文件,大小是 0 字节,有时连 0 字节都没有,文件索性不存在。

原因:MediaRecorder 在 start 之后并没有把可播放数据真正写入文件,底层编码器在 stop 或 release 时才 finalize 文件。你如果在 start 后立刻杀页面,底层来不及做 finalize,文件就是空的。另一种情况是 stop 抛了 RuntimeException(录音太短)但没有被捕获,代码直接崩了,后续 release 也没执行。

解决:stop 之前做一次「是否有有效数据」的检查,最直观的是看文件长度:

private boolean isFileValid(File file) { return file != null && file.exists() && file.length() > 0; } // 在 stop 之后马上验证 File f = new File(currentFilePath); if (!isFileValid(f)) { // 删除无效文件,避免 0 字节垃圾堆积 f.delete(); }

另外,MediaRecorder 的 stop 必须包在 try-catch 里,原因前面说过,这里再强调一遍:start 之后立即 stop、录音时长小于 1 秒、或者底层拿到数据异常,任何一条触发 RuntimeException 都会让录音功能直接崩溃。代码里已经写了这个 catch,你一定要理解它在保护什么——它保护的不是「代码优雅」,而是「用户快速点开始再快速点结束」这个高频操作。

5.3 录完立刻播放,开头一秒是空白

现象:录音完成后马上调用播放,前面约 0.5~1 秒是静音,之后再正常出声。如果等 1 秒后再放,又好了。

原因:MediaRecorder 在 stop 之后还会有一部分缓冲数据没有完全 flush 到文件。播放器读取时,文件头已经存在,但前几个数据包在底层延时写入。这时你立刻 seek 到 0 开始播放,拿到的是不完整的音轨段。

解决:在 UI 上不要紧贴 stop 之后立刻自动播放。最稳妥的方式是 stop 之后先读一次文件长度,再延时 200~300ms 后调用播放:

private void stopAndPlayLater() { stopRecordInternal(); File f = new File(currentFilePath); if (f.length() > 0) { new Handler(Looper.getMainLooper()).postDelayed( () -> playRecordedFile(f.getAbsolutePath()), 300); } }

这个 300ms 不是玄学,是给底层文件系统足够的时间把 page cache 刷进磁盘。真机上如果文件存储在外置 SD 卡,这个延时要更保守一些。检查播放器返回的错误码:如果你在 stop 后马上 prepare 失败了,也别急着报错,先跑一遍上面的延时流程,很大概率能救回来。

5.4 外接耳机拔掉后,录音直接变成静音

现象:用户戴有线耳机录音正常,中途把耳机插头拔掉,录音仍然显示在走,但录出来无声。有的机型还会直接出现喇叭外放那种刺耳的反馈啸叫。

原因:部分设备的音频路由策略在耳机拔出时重置了音频输入源状态,MediaRecorder 拿不到麦克风数据。更常见的是你设置了错误的 AudioSource——VOICE_CALL 需要有通话状态的上下文,拔出耳机后系统把它切回非通话通道,数据流就断了。

解决:音频源固定用 MIC,且监听耳机的插拔事件,在拔出后重启录音器。

private void restartRecordingAfterHeadsetRemoved() { stopRecordInternal(); startRecordInternal(); }

重启代价不高,但能保住后续录音数据。监听可用 AudioManager 的 registerAudioDeviceCallback,或者退一步在广播里做——你的完整代码包里提供的是 AudioDeviceCallback 版本,这样不用在 manifest 里额外声明任何奇怪的广播过滤。重启录音器时如果有已录时长,注意做时间补偿显示,不然用户会以为录了 10 秒,实际文件里只有 8 秒。

6. 进阶验证:PCM 转 WAV、波形检查与格式兼容性自查

6.1 PCM 裸数据转 WAV:44 字节文件头手写指南

AudioRecord 导出的 PCM 文件在没有文件头之前,任何播放器都不认。转 WAV 本质就是手动拼一个 44 字节的文件头,然后把它和 PCM 数据合并输出。

private void pcmToWav(String pcmPath, String wavPath, int sampleRate, int channels, int bitDepth) throws IOException { FileInputStream fis = new FileInputStream(pcmPath); FileOutputStream fos = new FileOutputStream(wavPath); int dataSize = (int) fis.getChannel().size(); int byteRate = sampleRate * channels * bitDepth / 8; int blockAlign = channels * bitDepth / 8; byte[] header = new byte[44]; header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; writeInt(header, 4, 36 + dataSize); header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; writeInt(header, 16, 16); writeShort(header, 20, 1); writeShort(header, 22, channels); writeInt(header, 24, sampleRate); writeInt(header, 28, byteRate); writeShort(header, 32, blockAlign); writeShort(header, 34, bitDepth); header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; writeInt(header, 40, dataSize); fos.write(header); byte[] buffer = new byte[4096]; int len; while ((len = fis.read(buffer)) != -1) { fos.write(buffer, 0, len); } fis.close(); fos.close(); }

writeInt 与 writeShort 是按小端序写入,因为 WAV 文件头规定多字节整型用 little-endian 存储。这里的 dataSize 拿的是文件流的总字节数,换算成时长就是 dataSize / byteRate 秒。bitDepth 取 16,这是 AudioRecord 常见的 PCM 16bit 编码;如果你用的是 8bit,header 里的 blockAlign 和 byteRate 都要跟着变,其他不用改。这个转换脚本建议封装成一个工具方法,放在项目的音频工具类里,将来音频数据做本地 DSP 处理时能反复复用。

6.2 波形与音量检查:用 getMaxAmplitude 验证录音是否真的在工作

录音开发中「到底录到声音了没」不能靠耳朵听,得靠数据。MediaRecorder 提供了一个轻量的检查接口:getMaxAmplitude,返回最近一段时间内采集到的最大振幅值(0~32767 之间)。如果你录音正常,有人说话时这个值会在几百到几千浮动,安静环境接近 0。如果在录音过程中它始终是 0,基本可以断定麦克风数据没进来。

private void startAmplitudeMonitor() { Runnable check = new Runnable() { @Override public void run() { if (currentState == STATE_RECORDING && mediaRecorder != null) { int amp = mediaRecorder.getMaxAmplitude(); // 设置音量显示或者做低音量提示 uiHandler.postDelayed(this, 200); } } }; uiHandler.postDelayed(check, 200); }

轮询周期我用 200ms,这个频率既能覆盖人声的 envelope,又不至于对主线程造成压力。你可以在界面上把它映射成一个实时音量条,让用户直观看到录音正在工作。这里有个常见误用:getMaxAmplitude 是一个「最大值」采样器,它返回的是从上次调用到这次的区间峰值,不是瞬时值。你隔着 2 秒再调第一返回值很可能大得异常,这不是 bug,是这个 API 的特性。所以第一次调用时最好先调一次丢弃旧数据,后续的值才是这段时间内的真实峰值。

6.3 格式兼容性自查:发布前把 m4a、3gp、wav 三种格式都跑一遍

录音文件最终要给用户下载、发给微信、上传服务端,格式兼容性决定了这条路通不通。

格式播放器兼容文件大小场景
m4a主流全兼容适中默认选择
3gp桌面播放器部分不支持较小客服系统
wav全兼容但体积巨大大需要二次编辑

发布前的自查清单有三个动作:录一次 15 秒以上音频,分别用系统播放器和第三方播放器(比如某主流视频 App 的文件预览)打开确认可播;把文件通过系统分享发送到 PC 上检查编码信息;对录音开启「飞行模式」再测一次,确保断网状态下录音不崩溃。这三步做完,录音模块基本稳了。有个坑是部分国产 ROM 的系统相册对 m4a 的音频轨道显示成「未知文件」,这是 ROM 的识别问题,不是你的文件坏了,不用慌。

从那次做一个语音笔记功能、连续在测试机上折腾了一周多之后,我养成了一个习惯:每次提交录音相关的代码,都要强制走一遍「真机录音→立即停止→拷贝文件→播放器验证→检查文件长度→换一台设备再测权限拒绝流程」,五步缺一不可。这个流程不是刻意追求仪式感,是那些年后台报错堆里活生生踩出来的教训——录音这功能看起来简单,但状态机、缓冲、硬件路由哪一环掉链子都够你查上半天。希望这份完整代码和这些排查思路,能帮你把录音这条路铺得更平一点。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 14:26:43

SpringBoot+Android旅游酒店预订系统开发实践

1. 项目概述与核心价值 这个基于SpringBootAndroid的旅游景点酒店预订管理系统,本质上是一个典型的OMO(Online-Merge-Offline)解决方案。我在2018年参与过类似商业项目的架构设计,发现这类系统最核心的价值在于打通了旅游场景中的…

作者头像 李华
网站建设 2026/10/15 14:24:44

SpringBoot+Vue考勤管理系统开发实践

1. 项目概述考勤管理是企业日常运营中不可或缺的一环,传统的人工考勤方式效率低下且容易出错。这个基于SpringBoot的考勤管理系统正是为了解决这些问题而设计的。系统采用B/S架构,实现了员工信息管理、考勤记录、请假审批、数据统计等核心功能模块。我在…

作者头像 李华
网站建设 2026/10/15 14:24:35

深度拆解 PiDeck 工作原理:Electron 外壳如何启动与管理多个 pi RPC 进程

人工智能AI 应用代码智能体桌面应用开发工具dsh-plugin 【免费下载链接】PiDeck PiDeck 是一个开源的桌面工作台,用于在本地项目目录中统一管理 pi Agent 会话,并支持导入 Codex、Claude 本地会话以便统一浏览和恢复。支持多项目工作区、会话历史、Git 集…

作者头像 李华
网站建设 2026/10/15 14:24:22

导弹六自由度仿真模型:MATLAB/Simulink实战解析

1. 项目概述:导弹六自由度仿真模型的价值与意义导弹六自由度(6-DOF)仿真模型是航空航天领域最经典的动力学仿真案例之一。这个模型完整模拟了导弹在三维空间中的三个平移自由度和三个旋转自由度运动特性,涵盖了从控制系统设计到弹…

作者头像 李华
网站建设 2026/10/15 14:24:19

视频孪生产品哪家好?2026年最新避坑选型清单

视频孪生在2026年已经不算新概念了,但选型这件事反而比三年前更难。三年前市面上能叫得出名字的产品也就那么几家,现在一搜,做数字孪生的、做安防的、做物联网平台的,都说自己能做视频孪生。我们把市面上主流的几类方案都摸过一遍…

作者头像 李华
网站建设 2026/10/15 14:24:15

SpringBoot游泳用品电商系统设计与实践

1. 游泳用品电商系统设计背景去年夏天帮朋友改造他的游泳装备实体店时,发现传统零售模式存在三个致命伤:库存更新滞后导致热销泳镜断货三个月,手工记账造成促销价格录入错误,线下客流受天气影响波动极大。这促使我开始设计这套基于…

作者头像 李华