十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android音频开发基础:采样原理、系统架构与核心API全解析

Android音频开发基础:采样原理、系统架构与核心API全解析 做Android音频开发不是光会调几个API就能交差的。我见过太多同行拿到需求就直奔MediaPlayer结果一遇到延迟、卡顿、杂音、无声问题就傻眼最后只能怀疑是手机坏了。实际上Android音频这套体系从应用层到硬件层离得很远中间有引擎、策略、混音、路由好几道关卡任何一个环节没弄清线上就会出幺蛾子。这个系列我打算按“基础概念 → 系统架构 → API实践 → 格式处理 → 故障排查”的顺序来写第一篇先把地基打牢。今天这篇会重点讲清楚音频数字化的基本原理、Android系统对音频的整体分层结构以及我们开发时最常用的几个组件到底各自扮演什么角色。适合刚接触Android音频开发的人也适合做了几年应用开发但一直没系统梳理过音频链路的同学。看完这篇你会建立一张比较完整的“心智地图”后面聊AudioTrack源码、音效处理、低延迟方案时才不会飘。1. 音频数字化的核心概念拆解1.1 从模拟信号到数字信号采样、量化、编码物理世界的声音本质是空气震动产生的模拟波而手机只能处理0和1。把这根连续波变成二进制串要经历三个关键动作采样、量化、编码。采样是每隔一个固定时间间隔取一次波形幅值。取的间隙越密还原出来的波形就越接近原始形状。量化是把采样得到的幅值映射到有限的数字阶梯上阶梯越多精度越高。编码则是把这串数字按特定规则排列成计算机能识别的数据块。三者组合在一起就得到了我们常说的PCM数据Pulse Code Modulation脉冲编码调制。这里有个容易混淆的点很多人把采样率和码率混为一谈。采样率决定了时间维度上记录了多密位深度决定了振幅维度上分得多细码率则是两者结合声道数之后每秒产生的数据量。公式很简单码率bps 采样率Hz × 位深度bit × 声道数这组参数直接影响文件大小和音质也是我们写代码时配置AudioTrack、MediaCodec必填的几个字段。后面我会专门用一节来做具体计算演示。1.2 核心参数逐个看采样率、位深度、声道数采样率方面最绕不开的是44100Hz和48000Hz。为什么音乐CD偏偏选44100因为这个数字刚好是奈奎斯特采样定理要求的两倍余量——人耳听力上限约20kHz要无失真还原20kHz以内的频率采样率至少要40kHz以上44.1kHz是从模拟录像带时代就定下的行业标准。Android的AudioTrack原生支持列表里44100Hz被标为“始终可用”这意味着无论什么设备你传44100Hz的PCM数据它都能吃下哪怕底层硬件只支持48000Hz系统也会自动重采样。48000Hz则是现代数字视频和大部分安卓手机硬件原生采用的标准频率。选哪个如果只是播放普通音乐44100Hz兼容性最稳如果做视频配音尽量跟视频帧率体系对齐用48000Hz避免后期二次采样引入损失。位深度最常见的是16bit和24bit。16bit能表达65536级振幅动态范围理论可以到96dB足够覆盖绝大多数播放场景。24bit动态范围拉到144dB多出来的那部分主要是给录音、混音、降噪留处理余量的播放端24bit并没有想象中那么神。但如果你要做音乐类App建议还是用24bit接入因为上游FFmpeg解码出的可能本身就是24bit或者32bit float你硬转16bit就提前丢掉了动态细节。有一种例外网络不好或低端设备播放时转成16bit能明显降低CPU负载和内存占用这属于性能换音质要权衡。声道数不用太纠结常见的就是单声道和双声道。单声道数据量减半、处理简单适合人声识别、语音通话双声道是立体声主力媒体播放和游戏音效基本都走这个。5.1、7.1在这种基础篇里先不展开Android系统会在AudioTrack层把这些多声道数据混音、downmix成设备支持的格式应用层感知不到。1.3 奈奎斯特定理为什么是音质的“天花板”聊数字音频不讲奈奎斯特采样定理等于没聊。它的核心结论是当采样率大于信号最高频率两倍时采样后的离散序列可以唯一地重建出原始连续信号低于这个阈值就会发生混叠。所谓混叠就是高频信号被错误地折叠成低频产生一种刺耳的伪信号。放到Android开发里这句话意味着你录进来的音频如果原始声音里有超过采样率一半的高频成分采集芯片根本挡不住最终只会变成难听的失真不是“忽略掉”这么轻描淡写。所以专业的录音软件往往会在硬件前端做低通滤波器。而在播放端如果你硬把一个22050Hz的原始音频数据以11025Hz的采样率丢给播放器声波整个会折叠错位听着像有人掐住了扬声器。排查这类问题时优先确认采样率配置是否匹配源数据别急着换耳机。2. Android音频系统架构全景2.1 从应用代码到扬声器音频链路要经过几层很多教程喜欢直接扔一张分层架构图但对没做过底层开发的人而言这张图就是绕来绕去的矩形框。我换个思路用“点外卖”来类比。你在外卖App上点单这是应用层。订单系统接到单这是Framework服务层。餐厅后厨开始按订单备菜这是Native层对应AudioFlinger和AudioPolicyService。骑手按路线配送这是策略路由和混音。最后外卖送到你手上吃掉这是设备硬件输出。Android的音频链路大致也是这个路径应用层通过Java API发起请求Binder跨进程把这些请求交给system_server里的音频服务和Native层的AudioFlinger。AudioFlinger是系统音频总管家负责混音、音效处理、向硬件设备输出。AudioPolicyService则管策略决定当前应该把声音送到听筒、扬声器、蓝牙还是耳机以及哪个App能出声、谁应该被抢占。两者再往下走通过HAL层硬件抽象层把指令翻译成不同厂商硬件能理解的操作。如果你想查“为什么我的播放没有声音”把上面这条链路每个环节过一遍基本就能定位到问题。在应用层代码逻辑是否正确在框架层AudioFlinger有没有收到数据在策略层是不是被焦点抢占或者路由切走在HAL层是不是硬件初始化失败。这是音频问题通用的排查思维。2.2 音频焦点多个App同时出声时谁说了算智能手机不是声卡只有一个“物理喇叭”。当微信语音、音乐App、游戏音效同时嘶吼系统必须有个仲裁机制这就是音频焦点机制。Android里的音频焦点可以简单理解为一把锁。App要通过AudioManager请求焦点拿到焦点之后才能顺畅播放后续其他App请求焦点你可能会被暂停、降低音量或者默默失去焦点。具体表现取决于你请求时用的策略比如AudioManager.AUDIOFOCUS_GAIN需要长时间独占焦点适合音乐播放获得焦点后应继续播放失去焦点后要暂停。AUDIOFOCUS_GAIN_TRANSIENT短时间获取焦点适合提示音来电时音乐应该暂停铃声播完再恢复。AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK允许声音“打个盹”音乐可以继续放但要降低音量比如导航播报时。AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK这是被动通知告诉你别人正在用声音你该调低音量。工程上最容易踩坑的不是焦点请求本身而是没有正确处理焦点丢失后的恢复时机。很多App拿到LOSS之后就永久沉默了根本没有监听回调去恢复播放。实际落地时我会建议在回调里保住当前播放位置和状态等重新拿到GAIN时无缝续播而不是重建整个播放器。2.3 音频路由声音为什么会从喇叭飘到听筒音频路由是系统根据当前设备状态自动调整输出设备的机制。插上耳机就走耳机拿起来贴近耳朵可能走听筒连上蓝牙就走蓝牙这些都是AudioPolicyService在背后实时算出来的。开发音频App时至少要知道两个硬道理。第一别强改路由除非你有明确的产品理由。比如语音通话类App用户插着耳机时你硬把声音送到扬声器会非常突兀。第二监听路由变化是音视频会话类App的必修课。耳机拔出、蓝牙断开这类事件会触发ACTION_AUDIO_BECOMING_NOISY广播你不处理就会外放用户在地铁里被巨大的外放怼一脸这就是实实在在的差评来源。一般做法是注册这个广播的Receiver在回调里暂停播放器同时提示用户“音频设备已断开”。等用户重新插上耳机或者明确选择外放再恢复播放。这个逻辑看起来简单但很多团队就是忘了做测试环境永远安静一上线就炸。3. 核心API选型与实操要点3.1 MediaPlayer、SoundPool、AudioTrack到底怎么选Android官方提供了不止一套播放原语当初它们的存在是有明确分工的。我这里直接给一张对比表后面逐个解释背后的原因。API适用场景延迟表现数据输入形式底层机制MediaPlayer完整音乐、流媒体、本地文件较高数百ms可接受文件路径、URI、网络URLMediaCodec/OMX解码后走AudioTrackSoundPool短促音效、游戏打击感、高频次播放很低适合快速触发预解码后的PCM资源内置AudioTrack管理池AudioTrack低层PCM播放、实时音频流处理低可控PCM字节流native层直接写AudioFlingerAudioRecord采集PCM录音、识音、对讲低可控PCM字节流输出native层从AudioFlinger读数据我遇到很多需求明明只是播放10秒的提示音有人直接丢给MediaPlayer实例化结果冷启动延迟大得离谱。MediaPlayer为了支持流媒体内部维护了完整的解码器管线、状态机和缓冲机制如果你只是播放一个短音效这套东西纯属杀鸡用牛刀。SoundPool的设计恰恰相反它在一开始就把音频解码成原始PCM放进内存触发时直接把内存数据喂给AudioTrack延迟可以压到几十毫秒甚至更低。游戏里刀剑挥砍的反馈音基本都用它。如果你要播放的是自己动态生成的PCM数据比如实时从麦克风采集、处理后播回去MediaPlayer根本帮不上忙因为它只接受文件或流不接受裸数据。这时候就只能用AudioTrack。这是一种“原材料通道”给多少PCM它就播多少PCM不做任何解码所以延迟最低控制力最强。代价是你要自己管好数据的生产节奏和缓冲。3.2 AudioTrack详解参数怎么填、模式怎么选AudioTrack的构造函数有新旧两套老的用AudioManager.STREAM_MUSIC之类指定音频流类型新的则推荐用AudioAttributes.Builder来定义用途。流类型是传统老接口AudioAttributes从API 21开始才是正路新代码尽量用它后续适配系统音效策略会更规范。创建AudioTrack时要填的核心参数有sampleRateInHz向AudioFlinger申请的采样率填入PCM数据必须与此匹配。channelConfigCHANNEL_OUT_MONO或CHANNEL_OUT_STEREO。audioFormatENCODING_PCM_16BIT或ENCODING_PCM_FLOAT取决于你生成的PCM格式。bufferSizeInBytes音频数据缓冲区大小低于这个值系统会提示你。modeMODE_STATIC还是MODE_STREAM。MODE_STATIC是一次性把数据全部交给系统系统自己拷贝管理适合音效、铃声这类短数据。MODE_STREAM是你持续通过write()把数据灌进内部队列系统按需消费适合长播放、实时流。bufferSizeInBytes是最容易忽略的点。填得太小容易导致底层数据耗尽播放出现卡顿喷音填得太大则白白增加内存和延迟。官方给了一条获取最小值的路径AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)这个值代表AudioFlinger保证流畅工作的缓冲区下限。稳妥的做法是拿它乘2或乘3给驱动留出余量。实测下来大部分设备用最小值的2倍就有非常好的稳定性3倍以上基本是白费内存。3.3 AudioRecord与SoundPool使用心得录音方向AudioRecord是唯一正确的姿势。它跟AudioTrack是对称的一个读PCM一个写PCM。创建时同样要指定采样率、声道、编码格式和缓冲区大小然后通过startRecording()开启数据流用read()拉数据。这里有一个很关键的细节read()是阻塞的如果你在主线程调用一旦底层数据没准备好界面就会卡死。所以录音逻辑必须放到独立线程或者用非阻塞方式配置好超时机制。另外读取到的数据往往不是整数倍的buffer size你的消费逻辑要做好“攒数据”处理别一看到非满就丢弃。SoundPool的使用更顺手。从API 21开始建议用SoundPool.Builder创建设置maxStreams限制同时播放的stream数量。这个数值不是随便拍的它是底层混音的并发槽位数。设得过大多个声音叠加后系统自动混音CPU消耗上升低端机可能出现毛刺设得太小后面的请求会被丢弃游戏里的连续打击感直接消失。一般建议4到8之间根据你的游戏节奏来。加载方面可以用setOnLoadCompleteListener确认文件加载完成再触发播放否则会有一段“空窗”听起来像卡了。4. 音频数据格式与文件容器操控4.1 PCM裸流、WAV封装、有损压缩三者的关系要处理音频文件就得先分清三件事PCM裸流、WAV封装、有损压缩。PCM裸流是最纯粹的数字音频数据没有任何文件头只有连续的波形样本。它可以直接被AudioTrack播放但没法直接拖到播放器里。WAV则是在PCM前面加了个小头——RIFF头记录采样率、位深度、声道数等元信息本质上还是PCM所以体积巨大但解析简单。MP3、AAC、Opus这类则是有损压缩格式它们把PCM变换到频域丢掉人类不易察觉的细节换取极高的压缩比代价是解码后的数据不再完全等于原始波形。开发时理解这层关系非常有用。本地文件播放基本走MediaPlayer无需关心底层但一旦你要做实时处理比如音量增益、降噪、变速就会面临解码、处理、重编码、封装这一整套流程每一步都绕不开PCM这个“通用交换格式”。4.2 WAV文件头解析手动读一个WAV需要哪些字段我经常需要自己做一个小工具把收到的遗弃文件头信息的PCM数据封装成WAV再检查。WAV头长这样前4字节RIFF标识4字节文件长度从下个字段开始到文件末尾的总字节数4字节WAVE标识4字节fmt 子块标识4字节子块大小PCM时为162字节音频格式1表示PCM2字节声道数4字节采样率4字节字节率采样率×块对齐2字节块对齐声道数×位深/82字节位深度4字节data标识4字节数据长度如果你在Android上拿到一个WAV但播放器说不支持打开头信息一看很可能就不是PCM编码而是IEEE float或A-law之类的扩展格式。这时候直接按16bit去解析必然是乱码得先识别“音频格式”字段。4.3 无损与有损格式对Android播放链路的影响播放MP3时MediaPlayer内部会把MP3解成PCM再进AudioTrack所以你在上层只能影响解码源控制不了解码方式。但如果你用第三方解码器比如FFmpeg解出的是AVSampleFormat里面可能是32bit float或16bit int你必须按这个类型把它转成PCM才塞给AudioTrack否则音量会异常或全是杂音。另外值得一提的选择是设备对采样率的支持差异。Android原生支持的采样率列表里44100永远是稳定的48000在绝大多数设备也没问题。但22050、16000这些低采样率在部分设备上会导致重采样成本增加尤其是通话类App我见过有的机器重采样后收音发闷。建议语音识别类场景直接上16000或48000不用48000就用16000别用奇怪的中间值。5. 实操用AudioTrack生成并播放一段正弦波5.1 动手前的准备最小工程需要什么我们要做一个最简单的实验生成一段440Hz的正弦波用AudioTrack播放出去。这段代码能验证整条播放链路是否畅通也是调试AudioFlinger问题时的“探针工具”。先用Android Studio建一个空工程。如果你用的还是老版本建议升级到新版本如果编译时SDK缺失去SDK Manager把对应版本的Platform装好就行。代码只需要两个类MainActivity和一个用于生成PCM数据的工具方法或者干脆全写在MainActivity里。5.2 生成正弦波PCM数据与参数计算正弦波数据怎么来从原理上讲每个采样点按这个公式算sample Amplitude × sin(2π × frequency × t)其中t是当前时间点。我们按44100Hz采样频率生成1秒的440Hz正弦波需要44100个采样点每个点是16bit有符号。如果播放的是双声道需要每两个采样点交叠排列左声道、右声道、左声道、右声道。代码实现时可以用ByteBuffer主动排布提高效率。生成之前先算好buffer大小1秒 × 44100 × 2字节 × 2声道 176400字节。这个数字就是我前面提到的PCM数据量一定要用Int类型来装载用short直接乘会把结果溢出成负数测试时不报错但播放出来的声音会呈现“扭动”感。正弦波本身的音量需要注意。一般设幅度在峰值的一半比如32767 * 0.8 ≈ 26214太大会削波听起来像破喇叭太小会因为环境噪声几乎听不清。5.3 完整播放流程从创建到释放的不踩坑姿势创建AudioTrack时用AudioAttributes.Builder指定用途是USAGE_MEDIA内容类型是CONTENT_TYPE_MUSIC。再把之前算好的最小缓冲值乘2作为bufferSizeInBytes模式用MODE_STATIC因为数据是一次性写满的。代码骨架大概是这个样子int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_OUT_MONO; int audioFormat AudioFormat.ENCODING_PCM_16BIT; int minBuf AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); int bufferSize Math.max(minBuf * 2, 176400); AudioTrack track new AudioTrack.Builder() .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()) .setAudioFormat(new AudioFormat.Builder() .setSampleRate(sampleRate) .setChannelMask(channelConfig) .setEncoding(audioFormat) .build()) .setBufferSizeInBytes(bufferSize) .setTransferMode(AudioTrack.MODE_STATIC) .build(); byte[] pcmData generateTone(440, 1, sampleRate); track.write(pcmData, 0, pcmData.length); track.play();记得在onPause中暂停播放onDestroy中释放track调用release()时系统会释放底层native资源不释放就会出现闪烁的“设备占用”问题尤其是反复进入页面时。6. 常见问题与排查技巧实录6.1 音频问题排查速查表遇到问题时我习惯按表格从头到尾过一遍效率比人肉试高得多。现象可能原因排查路径完全无声焦点被抢占、路由切换到听筒、音量被调为0查AudioManager焦点状态、查当前路由设备、查音量曲线声音断续、卡顿bufferSize不足、write不及时调大缓冲区、用后台线程持续写破音、杂音PCM格式不匹配、采样率被重采样、位深不对核对数据编码类型、确认AudioTrack参数是否与数据一致音量偏小或偏大解码格式是float但按16bit写入统一转成16bit或者用ENCODING_PCM_FLOAT播放延迟明显bufferSize过大、链路环节太多缩小缓冲区、确认没有MediaPlayer层多余解码6.2 焦点、路由、音量三层隐性坑位逐个排查焦点问题隐蔽性很高。我接过一个项目播放明明正常但一进App就被系统压低音量。排查后发现是另一个SDK在后台偷偷请求了AUDIOFOCUS_GAIN并且没有释放。这种问题用dumpsys audio查焦点持有者能快速定位。路由问题第二隐蔽。很多设备上你在接电话时系统会把媒体路由自动切到听筒等通话结束可能不会切回来。这时即使你的播放逻辑没问题声音也只在听筒里音量条怎么调都像蚊子叫。排查方法是调用AudioManager的getDevices(GET_DEVICES_OUTPUTS)看当前激活的输出设备如果输出设备列表里有TYPE_BUILTIN_EARPIECE而用户并没有贴近耳朵基本就是路由被卡住了。音量问题最直白也最容易忽略。Android的每个stream type都有独立的音量曲线媒体音量低不等于铃声音量低。别在代码里假设“只要设置STREAM_MUSIC音量就能够满足所有播放”AudioAttributes的USAGE不同走的音量分组也不同。6.3 低延迟方案与全局音效处理经验谈低延迟是游戏、K歌、乐器App的命门。Android官方从API 26开始全力推AAudio它绕开了部分Java层和策略限制native层直接跟AudioFlinger打交道延迟可以压缩到很低。如果你的App主要播放高实时性声音建议直接把音频处理放到JNI层用AAudio。但要注意AAudio对设备的依赖依然很强你必须做运行时feature检测别在低级设备上硬上。全局音效处理则是另一套思路。系统提供了AudioEffect框架可以对全局输出加均衡器、重低音、环绕声等。但也只能做系统级效果没有办法做自定义DSP想更深层控制必须自己接FFmpeg或移植算法到native层做PCM改造再交给AudioTrack播放。这条路复杂但能做出来的东西上限高得多。7. 后续系列规划与个人实践分享到这里Android音频的基础链路基本打通了。下一篇我会深入AudioTrack源码讲清楚write()内部到底把数据送去了哪里以及底层混音和音效是怎么生效的。后面还会单独写一篇关于WAV、PCM文件读写和AudioRecord录音的完整案例再往后可能会聊FFmpeg集成和实时变声。最后分享一点实际做项目时悟到的建议做音频开发别把所有赌注押在模拟器上。模拟器只负责跑通流程对音频硬件的模拟极不完整延迟、路由、焦点这些跟设备强相关的行为必须拿真机验证。而且测试机型不要只拿自己的旗舰机找一台低端机、一台带蓝牙耳机的机器把场景组合都过一遍很多线上问题根本不会带出门。音频开发的知识环环相扣今天这篇是铺垫也是所有后续内容的基石。把这层地基打牢下一篇讲源码时你就能跟上节奏了。
返回列表