Android音频播放录制参数全解析:AudioTrack与AudioRecord避坑指南
《Android音频系列》写到第4篇终于到了正面硬刚“Audio播放录制”和“参数”这两块骨头的时候了。播放录制说白了就是你把一段PCM数据塞给系统或者从麦克风把PCM数据取回来但中间隔着采样率、位深、声道、buffer这一堆参数任何一个不对出来的声音要么卡顿、要么有杂音、要么干脆没声。这篇我按自己排查线上问题的思路把播放和录制常用到的参数、配置逻辑和踩过的坑一次讲清楚。不管你做的是播放器、录音机、实时语音还是音频处理App这篇应该都能对得上号新手看也没门槛我会把每个参数为什么这么配、底层做了什么尽量说明白。1. 先理清播放录制的技术路线再谈参数1.1 播放和录制的两条主路线Android音频播放录制表面上都是操作PCM数据但底层实现和适用场景完全不同。播放侧绝大多数人第一反应是MediaPlayer它确实省心传个文件路径或网络地址就能响内部把音频解码、音效处理、时间同步全包了。但MediaPlayer不适合做低延迟场景也不适合处理实时生成的PCM数据你没法逐帧控制它喂数据也没法精确知道底层播放到哪个位置。真正需要精细控制播放时要选AudioTrack。它本身就是PCM数据的“搬运工”你在Java层或者Native层把一个short数组写进去它负责把数据送到AudioFlinger混合再交给音频HAL输出。同一条RoadMediaPlayer是自动挡AudioTrack是手动挡——开起来累但你能控制每一次换挡时机。录制侧相对统一核心就是AudioRecord你指定麦克风作为音源、指定采样格式和buffer大小然后不断read出来。AudioRecord拿回来的是最原始的PCM或者经过前端处理降噪、回声消除的PCM具体拿到什么取决于你选的audioSource参数后面会展开。这里说一个重要经验如果你做的是Unity里的Native Audio插件或者高性能音频引擎最终还是要走AAudio/OpenSL ES这一套原生接口但参数逻辑和Java层的AudioTrack完全一致。理解了AudioTrack的参数Native层无非是换了个API壳子。1.2 为什么说参数是音频开发的灵魂很多刚接触音频的人会问为什么同样一段声音我直接从AudioRecord读数据再交给AudioTrack播放听起来卡顿和变调答案几乎都在参数匹配上。Android音频系统看似是“你传什么我放什么”但底层AudioFlinger有自己的混音周期和输出设备。如果你设置的采样率、位深、声道数和底层硬件输出不一致系统会默默做一次采样率转换和通道混音。这个转换是有代价的CPU开销变大、延迟变高、极端情况下你写入的节奏和底层的拉取周期对不上就会出现断断续续的underrun。所以参数选型从来不是“都能响就行”。音频这块参数即质量参数即性能。我通常把参数分成两类一类是描述PCM数据本身“长什么样”的参数比如采样率、位深、声道数另一类是描述系统“怎么处理数据”的参数比如streamType、usage、bufferSize、transferMode。这两类搞混了调试的时候会走很多弯路。2. 播放侧方案选型MediaPlayer和AudioTrack怎么取舍2.1 MediaPlayer适合播文件和流媒体但别碰低延迟先说明一个容易踩的坑很多人在要做“实时音效播放”时第一个想到的是MediaPlayer.setAudioAttributes、setPlaybackParams然后往里塞一个动态生成的音频文件。这条路基本是死路。一是MediaPlayer的解码、渲染链路是完整的流程它不是PCM流的直接通道二是它的播放进度是解码器自己推进的你没法逐帧注入数据。延迟动辄几百毫秒而且一旦出现数据供给不及时它不会简单“卡一下播空buffer”而是直接表现为播放器内部状态错乱。MediaPlayer正确的使用场景是离线音频文件mp3、flac、wav、HTTP/HTTPS流媒体、系统预置资源。它的samplerate、channel、bitrate这些参数自动从容器和编码格式里读出来不需要你手动指定。此时如果你非要去“设置采样率”反而画蛇添足。除非你想做变速变调才需要接触PlaybackParams里的speed和pitch参数那是Post-processing层面的和PCM传输层完全两码事。2.2 AudioTrack适合自产数据MODE_STATIC和MODE_STREAM怎么选AudioTrack有两个工作模式说实话很多人用了很久也没搞清这俩模式的区别导致项目里乱切。MODE_STATIC适合一次性或短小的音频数据比如按键音、提示音。你把完整PCM一次性write进去然后play播放过程中数据不再变化。这种模式底层可以预载数据内存效率高但如果你在播放中反复write覆盖数据不同版本Android行为会有差异容易出诡异问题。MODE_STREAM适合流式数据比如实时通话、音频解码输出、边生成边播放的长音频。你需要在播放的同时持续调用write喂数据。实际开发中如果是做一个实时音频处理App基本无脑选MODE_STREAM。我见过有人用MODE_STATIC去播放短促的语音包但把一次write的数据量做得特别大然后等play结束后再重新write表面能用一旦音频底层周期被拉长就出现播完一段后卡一下的情况。静态模式适合“写一次放着响”别拿它装长音频。另外AudioTrack还有一个隐藏参数performanceModeAPI 26后可以通过AudioTrack.Builder设置成PERFORMANCE_MODE_LOW_LATENCY或者PERFORMANCE_MODE_NONE。想要低延迟就选LOW_LATENCY。但注意这个mode能否真正生效取决于系统当前输出设备是否支持fast mixer path。如果你插了蓝牙耳机很多设备会退回到普通路径此时低延迟模式只是个“建议”。这块我会在第5节实战中再说。2.3 别拍脑袋定采样率先问系统要答案写AudioTrack之前建议先向系统要一组真实运行参数。Android提供了AudioManager.getProperty方法能直接拿到当前系统音频输出实际的采样率和帧大小AudioManager am (AudioManager) getSystemService(Context.AUDIO_SERVICE); String sampleRateStr am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); String framesPerBufferStr am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); int sampleRate Integer.parseInt(sampleRateStr); int framesPerBuffer Integer.parseInt(framesPerBufferStr);这组数据是从AudioPolicyManager和音频HAL那边拿到的设备真实运行参数而不是你App自己的设定。很多高性能音频库比如OpenSL、AAudio在初始化时都会先取这组值然后按这组值创建自己的播放器或录音器。这样做的好处是避免音频链路做采样率转换最大程度降低延迟和CPU消耗。你可能会问是不是必须用这个值不一定取决于你的项目目标。如果只是放一个普通的背景音乐用44.1kHz还是48kHz体感差异很小。但如果要做音视频同步、实时耳返、或者把底层音频帧与UI刷新节奏对齐那么严格使用系统返回的采样率和framesPerBuffer是写高质量音频App的重要前提。3. AudioTrack核心参数逐个拆解3.1 采样率数据交换的“语言频率”采样率描述了PCM数据在一秒内采集/播放了多少个采样点。Android框架层合法值范围通常在8000~192000Hz常见的有8000、11025、16000、22050、32000、44100、48000、96000、192000。但注意合法不等于硬件支持。大多数手机的ADC/DAC原始采样率固定在48kHz或44.1kHz系统通过AudioFlinger的采样率转换模块把App传入的任意采样率转成设备原生采样率。采样率不匹配最典型的症状有两个一是变调因为源数据和设备采样率相差过大SRC算法虽然能转但如果AudioTrack内部直接把缓冲索引换算成时间偶尔会有音调微小偏移二是CPU占用变高SRC本身是计算密集型的。做低延迟应用时强烈建议直接用系统输出的采样率避免SRC。我做实时音频时会先记录AudioManager返回的native采样率再把这个采样率同时用于AudioRecord和AudioTrack。因为AudioRecord的采样率和AudioTrack的采样率保持一致就等于把链路里“SRC”的次数降到最低——除非麦克风硬件原生采样率和扬声器硬件原生采样率本身不同这种情况在手机上其实非常少。3.2 位深与声道同一个采样点的“体积”位深影响PCM每个采样点的取值范围。ENCODING_PCM_8BIT每个采样点一个字节有符号动态范围太小常见于极低端设备模拟不推荐。ENCODING_PCM_16BIT最通用每个采样点两个字节取值范围-32768~32767几乎所有App默认。ENCODING_PCM_FLOATAPI 21以后支持每个采样点4字节float取值范围-1.0f~1.0f处理时不用操心整型溢出适合加效果器、混音。实际输出时底层还是会转成硬件支持的格式。声道参数比较容易搞混。Android里有两种表示channelMask如CHANNEL_OUT_STEREO和channelIndexMask如指定第几路通道。绝大多数场景用channelMask就够了。立体声CHANNEL_OUT_STEREO代表两声道单声道CHANNEL_OUT_MONO代表单声道。注意如果你写入的数据是交织立体声却把AudioTrack配成MONO声音会听起来“变薄”甚至丢失部分内容反过来Mon数据配成Stereo则只有一边有声音听感是中空且音量变小。一个我经常提醒记者的点位深和声道决定了你每秒数据量公式是字节每秒 采样率 × 位深(字节) × 声道数例如48000Hz、16bit、立体声每秒约48000×2×2192000字节。这个数字在计算buffer大小、预估传输带宽时很有用。有一次线上问题就是后台把48kHz单声道数据强行当成16bit立体声去写字节数翻倍音频速度变快一倍、音调也明显变了排查时就是从这公式反推出来的。3.3 bufferSizeInBytes播放会不会卡的全在这bufferSizeInBytes是AudioTrack内部环形缓冲区的字节数。系统底层以固定周期从这块缓冲区拉数据到AudioFlinger混音器如果缓冲区空了底层拉不到数据就会出现underrun表现是播放中断、卡顿、甚至一声“噗”。最常用是直接查静态方法int minBuffer AudioTrack.getMinBufferSize(sampleRate, channelMask, encoding);但这里有个坑getMinBufferSize返回的只是“最小可工作缓冲区”并不代表“高质量缓冲区”。很多初学者直接用这个值创建AudioTrack结果一遇上系统负载抖动就卡。底层混音线程从缓冲区取数据的频率是不均匀的如果缓冲区刚好贴着硬件最小值应用端稍微在write上慢一点立刻underrun。我的经验是如果做实时性要求高的播放bufferSize取getMinBufferSize的2倍起步如果做普通音频播放可以按“期望延迟100ms~200ms”来算即期望字节数 (采样率 × 位深字节 × 声道数 / 1000) × 期望延迟ms比如48kHz/16bit/立体声想要150ms缓冲约192×15028800字节加上getMinBufferSize做下限取两者较大值。另外补充一个细节AudioTrack的write每次不是无限大单次写入数据量应控制在合理范围一般不超过一秒的数据量否则某些版本会有异常返回。我惯常的做法是单次写2048~8192字节循环写保证播放时数据供给节奏平滑。3.4 从StreamType到AudioAttributes播放参数之外的“身份标识”老项目里经常看到setStreamType(AudioManager.STREAM_MUSIC)这是Android早年写法API 21后推荐改用AudioAttributes它把“用途”和“内容类型”拆开底层音频策略能根据它的值做更细的管理比如音量分组、焦点处理、设备路由。AudioAttributes attributes new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build();不同场景下用法建议直接看这个表场景usagecontentType说明游戏背景音/音乐USAGE_MEDIACONTENT_TYPE_MUSIC常规走音乐音量流语音聊天/通话USAGE_VOICE_COMMUNICATIONCONTENT_TYPE_SPEECH可能会触发回声消除和通话音效系统提示音USAGE_ASSISTANCE_SONIFICATIONCONTENT_TYPE_SONIFICATION走通知/铃声音量导航语音USAGE_ASSISTANCE_NAVIGATION_GUIDANCECONTENT_TYPE_SPEECH走导航音量和音乐流混音策略不同录音回放/剪辑USAGE_MEDIACONTENT_TYPE_UNKNOWN无特殊处理很多掉声音的bug其实是usage选错了比如导航提示被当成音乐流和音乐流抢焦点导致一边音量下降。AudioAttributes对底层音频路由的影响远比你想象的大参数不是“填了就行”要按产品场景来。同样的AudiorRecord录制时也要注意audioSource它决定录音数据处理链路的激活状态。4. AudioRecord录制参数详解与Read机制4.1 音源source怎么选决定你拿到的是“原始声”还是“处理声”AudioRecord构造时有一个MediaRecorder.AudioSource参数这是很多人忽略的高阶敏感点。常见的source有MIC通用麦克风会经过平台默认的音频前端处理不同手机差异不小。VOICE_RECOGNITION为语音识别设计的源Android在部分平台会关闭回声消除和AGC帮你拿到“更原始”的输入但不同厂商策略不完全一样。VOICE_COMMUNICATION为通话优化会启用回声消除、噪声抑制和自动增益适合VoIP但拿到的数据已经“处理过”。CAMCORDER摄像头配套录音适合做相机类App。UNPROCESSEDAPI 24新增表示不经过任何前端处理和效果器只返回硬件AD采样后的数据。但它不是所有设备都支持使用前要通过AudioManager.getProperty或检查设备能力。选择逻辑很简单做录音机、需要原声优先试UNPROCESSED做语音通话App选VOICE_COMMUNICATION做ASR语音识别选VOICE_RECOGNITION拿不准就用MIC。但注意MIC这个源底层经常开启AGC自动增益当环境嘈杂时音量会被自动拉高对想分析原始声压的App是个干扰。4.2 录音参数三角采样率、channelMask、bufferSizeAudioRecord的参数和被录制的源设备相关采样率可以试试48000或44100但部分源设备比如UNPROCESSED只支持设备原生采样率你请求的如果不在支持列表里某些老设备会创建成功但read返回错误。channelMask方面录音一般建议用CHANNEL_IN_MONO。除非你明确知道需要立体声否则别轻易上CHANNEL_IN_STEREO。因为手机麦克风阵列在立体声模式下可能输出两个声道但这不代表真正的空间拾音中间的相位问题反而会让波形分析难做。而且同一设备上不同channelMask对应的硬件buffer周期不同有时候立体声的getMinBufferSize比单声道大不少延迟也更高。最低buffer大小照样用getMinBufferSizeint minRecBuf AudioRecord.getMinBufferSize(sampleRate, CHANNEL_IN_MONO, ENCODING_PCM_16BIT);录音的buffer如果太小read不及时拉走底层缓冲区写满新的PCM数据无处放就出现overrun表现是录到的数据出现随机丢块、音频时间轴跳变。我一般会把录音buffer设为minRecBuf的2倍或4倍给录音线程足够的调度余量。4.3 read阻塞与非阻塞线程模型怎么搭AudioRecord的read有好几个重载可以读byte[]、short[]、ByteBuffer。其中read(byte[], int, int)是阻塞的如果底层数据不够线程会一直等着另一个read(byte[], int, int, int)多了一个mode参数可传READ_BLOCKING或READ_NON_BLOCKING。阻塞式最适合写一个专门的录音线程死循环里调用read拿到多少就处理多少。这种方式代码简单不容易漏数据。非阻塞方式则适合把录音读放到游戏主循环或事件驱动模型里但你需要自行聚合数据块并处理返回0的情况。一个操作建议用short[]读数据要比用byte[]读数据在后续信号处理上更方便因为16bit PCM的每两个字节才是一个采样点直接用byte[]还得自己拼short、处理大小端。尤其在Android平台上PCM是Little Endian如果手动拼接一个不小心就高位低位弄反了波形看起来全是破音。用short[]之后你可以直接喂给FFT、音量检测、降噪算法性能也不会差。short[] buffer new short[readSize / 2]; // readSize是字节数 int read audioRecord.read(buffer, 0, buffer.length, AudioRecord.READ_BLOCKING); if (read 0) { // 对buffer[0..read)进行处理 }read的返回值是读取的short数量short[]版本或字节数byte[]版本不同重载的返回值单位不同非常容易搞混一定要看方法签名。如果返回负值比如-1表示非法操作-2表示参数错误-4表示没有初始化-6表示内部错误。排查阶段遇到这些负数优先去看调用顺序和参数是否合法。5. 实战打造一套低延迟播放录音的Demo骨架5.1 权限和环境准备老规矩先声明权限。录音必须声明RECORD_AUDIOAndroid 6还需要动态申请。播放普通PCM不需要特殊权限但如果要播放到通话上行的语音流那就涉及MODIFY_AUDIO_SETTINGS和系统级权限普通App基本拿不到这里不展开。代码里检查权限的方式就不重复贴了标准的checkSelfPermission那一套。环境上直接用Android Studio新建工程即可如果你的老工程是从旧项目移植过来的注意ndkVersion和Gradle版本要匹配特别是涉及Native音频层时so库和头文件编译环境版本不一致最容易出现初始化失败或者AudioTrack无法连接的怪问题。5.2 播放器实现流式写入和停止不炸音低延迟播放器的核心是尽早建立AudioTrack并保持稳定的写周期。下面是一个最小可用实现的关键片段int sampleRate 48000; int channelMask AudioFormat.CHANNEL_OUT_STEREO; int encoding AudioFormat.ENCODING_PCM_16BIT; int minBuf AudioTrack.getMinBufferSize(sampleRate, channelMask, encoding); int bufferSize Math.max(minBuf, 1024 * 4); AudioTrack track new AudioTrack.Builder() .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()) .setAudioFormat(new AudioFormat.Builder() .setEncoding(encoding) .setSampleRate(sampleRate) .setChannelMask(channelMask) .build()) .setBufferSizeInBytes(bufferSize) .setTransferMode(AudioTrack.MODE_STREAM) .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) .build(); track.play(); // 播放线程里循环 byte[] data loadNextPcmChunk(); // 从文件或网络读取 int written track.write(data, 0, data.length); // 如果written data.length说明底层缓冲区满了要稍等再写这里停播时有个细节容易踩坑直接stop()可能导致底层缓冲区里的残余数据来不及播放就被清掉有时会留下一声“咔”的爆音。我实践下来的处理是先正常把剩余数据write完短促地做一个音量渐出大概5ms到10ms的线性衰减然后暂停写入再调用track.pause()再flush()最后release()。如果只是临时暂停不需要release用pause就够别频繁创建AudioTrack创建本身是有开销的。关于播放线程的节奏可以用一种“水位线”思路写完后检查track.getPlaybackHeadPosition()的播放进度如果进度比写入量慢很多说明底层吃不下Sleep一小段时间再继续写如果进度追着写入量跑就要加快写入。网上很多实时播放卡顿的问题就是打死不Sleep、无脑循环写结果要么内存越来越小要么写线程把CPU占满。5.3 录音实现循环读取与实时音量反馈录音侧核心是一个独立的AudioRecord实例。初始化代码如下int sampleRate 48000; int channelIn AudioFormat.CHANNEL_IN_MONO; int encoding AudioFormat.ENCODING_PCM_16BIT; int minRecBuf AudioRecord.getMinBufferSize(sampleRate, channelIn, encoding); int recBufSize Math.max(minRecBuf * 2, 2048); AudioRecord recorder new AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION) .setAudioFormat(new AudioFormat.Builder() .setEncoding(encoding) .setSampleRate(sampleRate) .setChannelMask(channelIn) .build()) .setBufferSizeInBytes(recBufSize) .build(); recorder.startRecording(); // 录音线程循环 short[] buf new short[recBufSize / 2]; int ret recorder.read(buf, 0, buf.length, AudioRecord.READ_BLOCKING); if (ret 0) { // 计算分贝或丢给播放器做实时监听 }如果你此时需要把录到的声音“实时放出来”耳返一个比较土但稳定的做法是录音线程把读到的short[]经过处理以后丢到一个有界队列里播放线程再从队列里取出来write给AudioTrack。直接在read回调用track.write会把两个线程的节奏耦合在一起一旦其中一个开始抖动另一个也跟着抖声音会出现回声般的不稳定。录音结束后停止顺序也有讲究先调用recorder.stop()再调用recorder.release()。某些设备如果在你还在read阻塞时直接release会产生一个较长卡顿的阻塞异常所以停止录音前可以先设置一个volatile标志位让read返回后跳出循环再停止释放。5.4 给一个参数配置参考表下面这份表是我在平时项目中比较常用的参数起点大家可以按实际场景调整。注意不是绝对标准但能省掉大部分“能响但有问题”的初装成本。场景采样率位深声道buffer大小参考通用音乐播放44100/48000随源16bitstereogetMinBufferSize×2实时耳返48000系统native16bitmonogetMinBufferSize×2起游戏音效4800016bitmono/stereo按5~20ms延迟预算计算语音识别录音16000/48000均可用16bitmonominRecBuf×2通话降噪处理48000系统native16bitmonominRecBuf×2到×4表格里的buffer大小记住一个原则想要低延迟用“够用不卡”的最小值想要稳用“宽容routing调度抖动”的大值。这两者天然矛盾做产品的人必须接受这个tradeoff不存在“又低延迟又不卡”的万能参数。6. 常见问题与排查技巧实录6.1 播着播着卡一下先看underrun指标音频播放卡顿最常见的原因是underrun也就是底层缓冲区断粮。Android在API 24以后AudioTrack提供了getUnderrunCount()方法直接返回播放过程中发生underrun的次数。如果你的App播放时感觉像“打嗝”在界面上Debug环境下打印这个数如果数字持续增长基本坐实了。修复思路主要是三点一是把bufferSize调大二是检查写入线程是否调度不够比如线程优先级被压低或者写线程和UI线程锁竞争三是确认有没有别的地方在抢占CPU导致写线程多次错过调度窗口。我遇到过一种很隐蔽的情况App开了极高频的Choreographer动画刷新导致主线程频繁GC音频写线程和主线程共用一个HandlerThread结果写线程时不时饥饿。后来把音频写线程独立出来并设置较高优先级后大幅改善。6.2 录音对不上/杂音大overrun和设备增益在作怪录音数据时间轴跳跃或者波形出现间歇性“断点”基本是overrun底层录音buffer溢出PCM数据被丢弃。解决方法是调大录音buffer同时检查录音线程处理逻辑的性能不要每次都同步写文件或做重量级算法。如果需要实时算法处理尽量用“采集线程处理线程”两层结构别让重型算子阻塞read循环。杂音大问题则要先判断是环境杂音还是设备引入的底噪。可以录一段静音环境数据计算RMS值。如果RMS异常高试试切换audioSource比如从MIC切到UNPROCESSED看底噪是否明显下降。很多中低端机器MIC路径上的AGC会放大底噪用VOICE_RECOGNITION或UNPROCESSED会“干净”不少。PC端HD Audio驱动底噪问题也常有类似的“增益级放大”现象思路一致都是从增益链路入手而不是降噪后处理。6.3 天天见到的IllegalArgumentException从哪来的AudioTrack和AudioRecord的构造方法有一堆校验逻辑参数不在合法区间直接抛IllegalArgumentException。我自己踩过的几种来源采样率不在设备支持范围内尤其UNPROCESSED源录音时个别设备对采样率容忍度很低。bufferSizeInBytes小于getMinBufferSize有的设备getMinBufferSize返回比较大的值直接写1024就炸。channelMask用错比如用了CHANNEL_IN_STEREO却传了AudioFormat.CHANNEL_OUT_STEREO或者用了CHANNEL_OUT_MONO这种常量本身就不存在。AudioAttributes和AudioFormat混用不同版本API在低版本设备上调用高版本常量。排查思路很简单把每个参数都打日志然后和getMinBufferSize、系统native采样率比较。不要相信“我去年写的代码参数没问题”Android碎片化下不同厂商对参数的容忍度差异非常大必须运行时校准。6.4 设备切换与audio服务异常看不见的“参数变更”插拔耳机、连接蓝牙设备、切换到听筒等动作会改变音频路由和默认声道/采样率。Android的音频策略会自动重配AudioTrack底层路径但你的AudioTrack是之前创建好的底层重新配置期间经常出现几帧数据丢失或者短暂卡顿。处理方案是监听AudioDeviceCallback或者注册ACTION_AUDIO_BECOMING_NOISY广播在设备变化时重建AudioTrack或暂停写入避免在切换瞬间继续write造成异常。还有一类比较头疼的问题系统音频服务异常。表现为app端调用AudioTrack初始化时返回错误Logcat里能看到audio server相关的RPC连接失败或者明显的audioserver重启日志。这种问题一般不是你App代码的锅大多是系统audioserver崩溃或死锁。遇到时先抓取dumpsys audio信息看看是不是系统侧混音线程卡死。App层面能做的有限最多是捕获异常后延迟重试别无限重试加剧问题。如果你只是要快速恢复可以尝试杀掉自己App里音频相关线程再重建或者提示用户重启设备。音频这块调参数没有一劳永逸的万能解但理解参数背后的数据流方向和大小是万变不离其宗的基础。我自己的体会是每一次看似古怪的音频bug最后都能落到“某个参数和设备真实状态不匹配”上。先把采样率和位深固定在系统能力范围内再围绕buffer做性能取舍比从网上抄个参数然后瞎试要高效得多。如果你正被某个音频问题折腾不妨先把关键参数打印出来对照这篇的思路逐项检查大概率能省下一个下午。