1. 音频系统架构到底在解决什么问题
音频系统架构这个词,乍一听像是那种写在芯片手册目录里的章节名,离日常开发很远。但只要你做过带声音的项目——不管是手机App里的播放器、车载语音助手、游戏引擎里的音效系统,还是智能音箱的语音交互——你迟早会撞上它。撞上的方式通常很具体:为什么蓝牙耳机和有线耳机切换时会有半秒的爆音?为什么录出来的声音总是比画面慢一拍?为什么同时播放导航播报和音乐时,音乐要自动 duck 下去?这些问题表面上是“调一下参数”,根子上全是架构问题。
我先把话说直白一点:音频系统架构,就是一套把声音从“产生”送到“听到”的完整流水线设计。它要回答的核心问题只有几个——声音数据从哪来、经过哪些处理环节、由谁决定什么时候送到哪个设备、多个声音同时存在时怎么混、延迟和音质怎么权衡。这套流水线在操作系统里通常横跨应用层、框架层、硬件抽象层和驱动层,每一层都有自己的职责边界和数据结构。
这一章之所以值得单独拿出来讲,是因为音频和视频、图像有一个本质区别:音频是流式的、连续的、对时间极度敏感的。视频掉一帧你可能看不出来,音频断一个采样点就是“咔”的一声。这意味着音频架构里几乎每一个设计决策,都是在和“实时性”做斗争。缓冲区开大了延迟高,开小了容易 underrun;重采样放在上层省驱动事但费CPU,放在下层省CPU但灵活性差。这些取舍没有标准答案,只有场景答案。
这篇文章适合谁看?如果你正在做音视频相关的开发,或者你在做IoT设备、车载系统、游戏、直播这类涉及声音的产品,又或者你只是好奇“手机里那套声音系统到底怎么跑起来的”,那这篇内容应该能给你一条清晰的脉络。我会尽量把抽象的分层讲成具体的流水线,把参数讲成可以算的账,把踩过的坑讲成你能直接避开的经验。
2. 音频系统的分层设计与核心思路拆解
2.1 为什么音频系统一定要分层
先想一个问题:如果让你从零设计一个操作系统的音频子系统,你会怎么切分职责?最朴素的做法是让App直接打开声卡设备写数据。这在单任务、单应用的嵌入式环境里确实能跑,但一旦系统里同时有音乐播放器、系统提示音、通话、录音四个声音源,这套做法立刻崩溃——谁来仲裁设备占用?谁来混音?谁来处理采样率不一致?
所以分层不是为了好看,是为了隔离变化。音频系统里变化最频繁的东西有三个:应用的需求(今天要低延迟,明天要省电)、硬件的形态(今天用内置Codec,明天接蓝牙)、以及信号处理链路(今天加个均衡器,明天加个降噪)。分层让这三类变化各自在自己的层里发生,不互相污染。
一个典型的音频系统从上到下大致是这样切的:
| 层级 | 职责 | 典型组件 |
|---|---|---|
| 应用层 | 发起播放/录制、选择设备、设置音量 | 播放器、语音SDK |
| 框架层 | 流管理、混音、路由、策略 | AudioFlinger、AudioServer |
| 硬件抽象层 | 屏蔽具体硬件差异 | HAL、AudioDevice |
| 驱动层 | 操作寄存器、DMA搬运 | ALSA、Codec驱动 |
| 硬件层 | 数模转换、功放 | DAC、ADC、PA |
这张表看着规整,但真正干活的时候,边界经常是模糊的。比如重采样到底算框架层还是HAL层的事?不同系统给的答案不一样。Android早期把重采样放在框架层,后来为了省电把一部分下推到HAL。这个迁移背后的逻辑很简单:谁离硬件近,谁做重采样就更省电,但灵活性更差。因为硬件往往有固定的采样率支持列表,框架层做重采样可以适配任意App请求的采样率,而HAL层做就只能支持硬件原生那几个。
2.2 数据流模型:Push还是Pull
音频架构里有一个特别容易被忽略但影响深远的设计选择:数据是“推”进去的还是“拉”出来的。
Push模型是应用主动把音频数据写进缓冲区,写完就返回,硬件慢慢消费。Pull模型是硬件或框架按固定节奏来“要”数据,应用被动提供。这两种模型决定了整个系统的时序特征。
Push模型实现简单,应用想写多少写多少,但问题是应用写太快会撑爆缓冲区,写太慢会 underrun。所以Push模型下,框架必须提供阻塞机制——缓冲区满了就阻塞写入,空了就阻塞读取。这个阻塞点就是延迟的来源之一。
Pull模型更符合音频的等时特性,硬件每到一个采样周期就来取一次数据,节奏天然稳定。但Pull模型对应用的要求更高,应用必须能随时响应“要数据”的请求,不能卡顿。
实际系统里往往是混合的。以常见的移动端音频框架为例,应用侧通常是Push(write调用),框架内部到HAL之间往往是Pull(callback或mmap)。这个转换点就是缓冲区,也是整个架构里最需要精心设计的地方。
提示:如果你在排查音频延迟问题,第一件事就是搞清楚你的数据流在哪个环节从Push变成了Pull,那个转换点的缓冲区大小基本决定了你的最小延迟。
2.3 缓冲区大小的账怎么算
缓冲区大小是音频架构里最经典的权衡。我用一个具体例子帮你把账算清楚。
假设采样率是48000Hz,位深16bit,双声道。那么每毫秒的音频数据量是:
48000 samples/s × 2 bytes × 2 channels = 192000 bytes/s 192000 / 1000 = 192 bytes/ms如果缓冲区设成1920字节,那就是10ms的缓冲。这意味着什么?意味着从应用写入数据到硬件真正播出来,中间至少有10ms的延迟(实际上还要加上处理链路和硬件本身的延迟)。同时,这个缓冲区能容忍应用最多10ms的调度抖动——如果应用超过10ms没写入新数据,就会 underrun,听到断音。
所以缓冲区大小的本质是:延迟 vs 抗抖动能力的权衡。缓冲区越大,越不容易断音,但延迟越高。专业音频设备追求低延迟,缓冲区可能只有几毫秒;而省电优先的场景,缓冲区可能开到几十甚至上百毫秒。
这里有个经验值可以参考:普通音乐播放,100ms到200ms的缓冲完全够用,用户感知不到;实时语音通话,端到端延迟要控制在150ms以内,留给缓冲的预算可能只有20ms到40ms;而乐器演奏这种场景,要求端到端低于10ms,缓冲区得压到个位数毫秒,这时候就得靠实时内核和mmap零拷贝来撑。
2.4 混音与路由:多路声音怎么共存
单路音频的架构其实不难,难的是多路。系统里同时有音乐、提示音、通话、录音的时候,架构必须回答:这些流怎么混?混的时候各自音量多少?送到哪个设备?
混音的核心是采样级对齐。两路音频要混在一起,必须采样率一致、位深一致、通道数一致、而且时间戳对齐。任何一项不一致,都得先做转换。转换本身不难,难的是转换会引入延迟和音质损失。所以好的架构会尽量让同一混音组里的流保持格式一致,减少转换次数。
路由则是另一套逻辑。路由决定“这个流的声音从哪个设备出来”。这里有个关键概念叫音频策略,它是一组规则,规定了什么类型的流在什么场景下走什么设备。比如通话流必须走听筒或蓝牙耳机,媒体流走扬声器,提示音走当前活跃的输出设备。策略的复杂度在于场景是动态变化的——插拔耳机、连接蓝牙、来电打断,每一个事件都可能触发路由重算。
我见过不少项目在路由上翻车,根本原因往往是策略写得太死。比如硬编码“媒体流走扬声器”,结果插上耳机后声音还是从扬声器出来。正确的做法是把路由决策抽象成“设备优先级列表 + 场景约束”,让框架根据当前设备状态动态选择。
3. 核心细节解析与实操要点
3.1 采样率转换:最容易被低估的性能杀手
采样率转换(SRC)在音频架构里是个隐形的大头。很多系统卡顿、发热、耗电,最后追查下来都是SRC在偷偷吃CPU。
为什么会有SRC?因为音频数据的生产者和消费者往往采样率不一致。App可能用44100Hz(CD标准),而硬件Codec只支持48000Hz。这中间的转换必须有人做。
SRC的算法质量差异极大。最粗暴的是线性插值,速度快但音质差,高频会有明显失真;好一点的是多相滤波器,音质好但计算量大几倍;专业级会用sinc插值,音质接近完美但CPU开销惊人。架构设计时必须在“谁来承担这个开销”上做决策。
我的经验是:如果硬件支持多种采样率,优先让硬件做SRC。硬件SRC通常有专门的DSP单元,几乎不占CPU。只有当硬件不支持目标采样率时,才在软件层做。软件层做的时候,尽量放在HAL层而不是框架层,因为HAL层离硬件近,可以做更激进的优化(比如用定点运算代替浮点)。
还有一个坑:重采样会改变数据长度。44100Hz转48000Hz,1000个采样点进去,出来是1088个左右。如果你的缓冲区管理是按固定长度算的,这里就会出问题。正确的做法是按时间算而不是按采样点数算,让重采样器自己管理内部状态。
3.2 时钟同步:数字音频的隐形地基
音频系统里有一个哲学级的问题:时间到底是什么。播放端有自己的时钟(DAC的晶振),录制端有自己的时钟(ADC的晶振),蓝牙耳机有它自己的时钟。这些时钟哪怕标称都是48000Hz,实际频率也会有微小差异,通常是几十到几百ppm。
这个微小差异累积起来就是灾难。假设播放端比录制端快100ppm,录一小时就会差出360ms,相当于音画不同步了三分之一秒。所以音频架构必须有一套时钟同步机制。
常见的同步策略有三种:
- 主从同步:以某一个时钟为基准,其他时钟通过调整采样率来跟随。比如以DAC时钟为主,ADC的数据通过重采样来匹配DAC的节奏。
- 时间戳对齐:每个音频缓冲区都带时间戳,框架根据时间戳决定什么时候播放。这种方式灵活但依赖时间戳的准确性。
- 异步缓冲:允许两端时钟独立,中间用足够大的缓冲区吸收差异,定期做微调(比如偶尔丢一个采样点或插一个采样点)。
移动端系统通常用第二种加第三种。蓝牙音频因为链路本身不稳定,几乎全靠异步缓冲,这也是蓝牙音频延迟普遍偏高的根本原因。
注意:如果你在做录音和播放同时进行的应用(比如K歌、通话),时钟同步没做好,最典型的症状就是录出来的声音和伴奏越走越偏,时间越长偏得越多。
3.3 低延迟路径的设计取舍
低延迟是音频架构里最贵的需求。每降低一点延迟,都要付出相应的代价。我把常见的低延迟手段和代价列一下:
| 手段 | 延迟收益 | 代价 |
|---|---|---|
| 减小缓冲区 | 直接降低 | 抗抖动能力下降,易断音 |
| mmap零拷贝 | 省一次拷贝 | 实现复杂,调试困难 |
| 实时线程优先级 | 减少调度延迟 | 可能影响系统其他任务 |
| 快速混音路径 | 绕过部分处理 | 功能受限,格式要求严格 |
| 硬件直通 | 几乎无软件延迟 | 只能单流,无法混音 |
实际项目里,低延迟路径通常是单独开一条“快速通道”,只有满足特定格式要求的流才能走。比如要求采样率必须是48000Hz、位深必须是16bit、通道数必须是2,满足这些条件才允许走快速路径,否则退回普通路径。这样既保证了常见场景的低延迟,又不用为所有格式都做优化。
这里有个实操心得:低延迟调试不要只看平均值,要看最坏情况。音频断音往往不是平均延迟高,而是某一帧突然延迟飙升。用示波器或者高精度计时器抓最坏情况,比看平均值有用得多。
3.4 电源管理与音频的冲突
音频系统有个特殊之处:它经常需要在系统“休眠”的时候继续工作。比如息屏听歌、待机接电话。这就要求音频架构和电源管理深度耦合。
冲突点在于:省电要求关闭尽可能多的硬件模块,而音频要求DAC、功放、时钟持续工作。解决方案通常是给音频子系统单独的电源域,让它能在主系统休眠时独立运行。但这又带来新问题——音频子系统独立运行的时候,谁来处理路由变化?谁来响应音量调节?
我见过的一个典型设计是:音频子系统维护一个“最小运行集”,只包含当前活跃流必需的处理环节,其他全部挂起。比如只播音乐的时候,录音链路、提示音混音器全部关掉。这个最小集的切换逻辑,是音频架构里最容易出bug的地方之一,因为切换时机往往和用户操作、系统事件交织在一起。
4. 实操过程与核心环节实现
4.1 从一次播放请求看完整链路
我拿一次最普通的“点击播放”来把整条链路走一遍,这样你能看到每一层到底做了什么。
第一步,应用层调用播放接口,传入音频文件路径或数据流。应用层通常还会指定一些参数:流类型(媒体/通话/提示音)、期望的采样率、通道数。这些参数会打包成一个请求发给框架层。
第二步,框架层收到请求后,先做策略检查。根据流类型和当前场景,决定这个流应该走什么设备、用什么音量曲线、是否需要和其他流混音。这一步会查路由表,可能还会触发设备切换(比如从扬声器切到蓝牙)。
第三步,框架层创建一条音频流轨道,分配缓冲区。缓冲区的格式要和目标设备的能力匹配,如果不匹配就插入重采样或格式转换模块。这时候框架会计算一个初始缓冲区大小,通常基于设备的最小缓冲和当前系统负载。
第四步,应用开始往缓冲区写数据。框架层从缓冲区取数据,做混音(如果有其他流)、做音量调节、做必要的音效处理,然后送到HAL层。
第五步,HAL层把数据转换成硬件能理解的格式,通过DMA送到Codec。Codec做数模转换,把数字信号变成模拟信号,送到功放,最后到扬声器。
第六步,硬件每消费完一个缓冲区,就通过中断通知驱动,驱动再通知HAL,HAL再通知框架,框架再唤醒应用写下一批数据。这个通知链路的效率,直接决定了系统的抗抖动能力。
这条链路里,任何一环出问题都会表现为“声音不对”。所以排查音频问题,核心方法就是在每一层加日志或探针,看数据在哪一层变形了。
4.2 缓冲区参数的实操计算
我拿一个真实场景帮你把缓冲区参数算一遍。假设要做实时语音通话,目标端到端延迟150ms以内。
先拆解延迟预算:
- 采集端硬件延迟:约10ms
- 采集端缓冲:假设20ms
- 编码:约5ms
- 网络传输:约50ms(这部分不可控,先按50算)
- 解码:约5ms
- 播放端缓冲:假设20ms
- 播放端硬件延迟:约10ms
加起来是120ms,留30ms余量。所以采集和播放的缓冲各20ms是合理的。
20ms对应的缓冲区大小,按48000Hz、16bit、单声道算:
48000 × 0.02 × 2 = 1920 bytes但实际不能只开一个缓冲区,通常要开双缓冲或环形缓冲。双缓冲的话每个1920字节,总共3840字节。环形缓冲的话,通常会开到2到4倍,也就是4KB到8KB,留出调度抖动的余量。
这里有个细节:缓冲区大小最好是2的幂次。因为很多DMA和内存对齐操作对2的幂次更友好,能减少边界处理的复杂度。1920不是2的幂,实际项目里可能会取2048,对应约21.3ms,也在预算内。
4.3 混音器的实现要点
混音器的核心逻辑看起来很简单——把两路采样点相加。但实际实现里有一堆细节。
首先是溢出处理。两个16bit采样点相加,结果可能超出16bit范围。正确做法是先转成32bit做加法,加完再做饱和处理(clamp到16bit范围),而不是简单截断。截断会产生刺耳的失真,饱和处理虽然也有失真但听感好得多。
其次是音量缩放。混音前每一路都要按自己的音量缩放。缩放系数通常是浮点数,但浮点运算在嵌入式环境里慢,所以很多实现用定点数。定点数的精度选择很关键,16bit定点做音量缩放,小音量时会有明显量化噪声。经验值是至少用24bit定点,或者干脆用浮点。
第三是混音顺序。多路混音时,先混哪些后混哪些会影响最终结果,因为饱和处理是非线性的。通常的做法是先把所有流缩放到一个统一的内部格式(比如32bit浮点),全部加完后再统一做饱和和输出格式转换。
第四是静音检测。如果某一路全是静音,可以跳过混音直接输出另一路,省CPU。但静音检测本身也有开销,所以要权衡。我的经验是,对于长时间静音的流(比如后台暂停的音乐),直接在路由层把它从混音组里摘掉,比在混音器里做静音检测更划算。
4.4 设备切换的平滑处理
设备切换是音频架构里最容易出爆音的地方。插拔耳机、连接蓝牙、切换扬声器,每一次切换都涉及数据流的重新路由,处理不好就是“啪”的一声。
爆音的根源是信号不连续。原来的数据流突然断了,新的数据流突然接上,如果两边的相位、幅度不连续,就会产生突变,听起来就是爆音。
平滑处理的核心思路是淡出淡入。切换前先把当前流在几毫秒内淡出到零,切换后再从零淡入。这个淡入淡出通常由硬件或HAL层的增益控制来做,因为软件层做会有延迟。
淡出淡入的时间也有讲究。太短了还是有爆音,太长了用户会觉得声音“断了一下”。经验值是5ms到20ms之间,具体看场景。音乐播放可以用长一点,通话要用短一点。
还有一个进阶技巧:交叉淡化。切换时不是先淡出再淡入,而是两路同时存在,一路淡出一路淡入,重叠一部分。这样听感上更平滑,但实现复杂度高,需要两路同时能输出。一般在专业音频设备里才这么做。
5. 常见问题与排查技巧实录
5.1 音频问题排查的通用思路
音频问题排查有个基本方法论:先定位是哪个环节,再定位是什么类型的问题。
定位环节的方法是分段隔离。在应用层录一份数据,在框架层输出录一份,在HAL层输出录一份,在硬件输出录一份。四份数据对比,就能看出问题出在哪一段。如果四份都一样,那问题在硬件或听感侧;如果某两份之间变了,问题就在那两层之间。
定位问题类型的方法是看症状。断音、爆音、延迟、失真、不同步,每种症状对应的原因范围不一样。我把常见症状和可能原因整理成一张速查表:
| 症状 | 常见原因 | 排查方向 |
|---|---|---|
| 周期性断音 | 缓冲区 underrun | 看应用写入是否及时,缓冲区是否太小 |
| 随机爆音 | 时钟不同步或切换不平滑 | 检查时钟源,检查设备切换逻辑 |
| 延迟逐渐增大 | 时钟漂移累积 | 检查时钟同步机制,检查缓冲区是否只增不减 |
| 高频失真 | 重采样质量差或溢出 | 检查SRC算法,检查混音溢出处理 |
| 单侧无声 | 通道映射错误 | 检查通道配置和路由 |
| 音量忽大忽小 | 增益控制冲突 | 检查是否有多个模块同时调增益 |
5.2 几个我踩过的坑
第一个坑是缓冲区只增不减。早期做的一个项目,为了抗抖动,缓冲区设计成动态增长——检测到 underrun 就加大缓冲区。结果跑久了缓冲区越来越大,延迟越来越高。后来改成双向调整,underrun 时加大,稳定运行一段时间后缓慢减小,才解决。这个经验告诉我,任何动态调整机制都必须有回退路径。
第二个坑是重采样器的状态没重置。设备切换时采样率变了,重采样器需要重置内部状态。如果忘了重置,切换后的前几个采样点会用旧状态计算,产生爆音。这个bug特别隐蔽,因为只在切换瞬间出现,平时测不出来。
第三个坑是音量曲线不连续。系统音量和应用音量是两级增益,如果两级的曲线没对齐,在某些组合下会出现音量跳变。比如系统音量50%加应用音量50%,理论上应该是25%,但如果两级曲线都是非线性的,实际结果可能差很多。解决办法是统一用一套音量曲线,或者至少保证两级曲线的端点对齐。
第四个坑是线程优先级反转。音频线程需要高优先级,但如果它等待一个低优先级线程持有的锁,就会被拖慢。这个在混音器里特别常见,因为混音器要访问共享的流列表。解决办法是用无锁数据结构,或者至少保证锁的持有时间极短。
5.3 调试工具与手段
音频调试光靠看日志不够,得用专门的工具。
最基础的是环回测试。把输出直接接到输入,播放一段已知信号,录回来对比。这个方法能测出整条链路的延迟和失真。测试信号通常用扫频信号或脉冲信号,前者测频响,后者测延迟。
进阶一点的是示波器观测。在DAC输出端接示波器,直接看模拟波形。这个方法最直观,能看到爆音、削波、直流偏置这些软件层看不到的问题。缺点是只能看模拟端,数字端的问题还得靠逻辑分析仪。
软件层面,时间戳追踪是最有用的手段。在每个关键节点打时间戳,最后把时间戳串起来看,就能知道延迟花在哪了。我习惯在应用写入、框架取出、HAL提交、硬件中断这四个点打时间戳,基本能覆盖整条链路。
还有一个技巧是用已知信号做端到端验证。比如播放一个1kHz的正弦波,录回来做FFT,看频谱是否干净。如果出现了额外的谐波,说明链路里有非线性失真;如果频率偏了,说明时钟有问题。这个方法简单但极其有效,我每次做新平台都会先跑一遍。
5.4 性能与功耗的平衡技巧
音频系统的性能和功耗平衡,核心是按需分配。不是所有流都需要最高质量的处理,也不是所有场景都需要最低延迟。
我的做法是把音频流分成几档:通话档追求低延迟,用最简处理链路;音乐档追求音质,可以用复杂一点的处理;提示音档追求省电,能用硬件直通就用硬件直通。每一档对应一套处理参数和缓冲区策略,框架根据流类型自动选择。
另一个技巧是动态降级。系统负载高的时候,自动降低音频处理质量——比如关掉音效、降低重采样质量、加大缓冲区。这个机制要做得隐蔽,用户不能感知到明显变化。比如重采样质量从sinc降到多相滤波,普通人根本听不出来,但CPU能省一半。
还有一个容易被忽略的点是空闲检测。没有音频播放的时候,整个音频子系统应该能进入低功耗状态。但很多实现里,框架层还在空转,定时器还在跑,白白耗电。正确的做法是让框架在没有活跃流的时候完全挂起,靠事件驱动唤醒。
6. 架构演进与场景适配的思考
6.1 不同场景对架构的不同要求
音频系统架构没有万能方案,因为不同场景的要求差异太大。我拿几个典型场景对比一下:
手机场景追求的是平衡。既要低延迟通话,又要省电音乐播放,还要支持各种蓝牙设备。所以手机音频架构通常是最复杂的,分层最多,策略最丰富。
车载场景追求的是可靠。车里可能有十几个扬声器,还有主动降噪、回声消除、多区音频。车载架构的特点是通道多、处理链路长、对故障容忍度高。一个扬声器坏了不能影响其他扬声器。
专业音频场景追求的是极致低延迟和高保真。录音棚里的话筒到监听耳机,延迟要低到人耳感知不到。这类架构通常绕过操作系统的通用音频栈,直接走专用驱动和硬件。
嵌入式场景追求的是简单和确定。资源有限,不能有复杂的动态策略。通常是静态配置,启动时确定好链路,运行时不改。
理解这些差异很重要,因为它决定了你在做架构决策时的优先级。手机架构里合理的取舍,放到车载里可能就是错的。
6.2 新趋势对架构的影响
这几年音频架构有几个明显的变化趋势。
第一个是空间音频的普及。空间音频要求对每个声音对象做三维定位和头部相关传输函数(HRTF)处理,这对架构的计算能力和数据流设计都提出了新要求。传统的声道式混音器要改成对象式混音器,每个对象独立处理后再渲染到扬声器。
第二个是AI降噪和AI音效的兴起。这些算法通常计算量大,放在主CPU上跑不现实,所以架构上要把它们卸载到DSP或NPU。这就引入了新的数据流路径——音频数据要送到加速器处理再送回来,中间的同步和缓冲又成了新问题。
第三个是无线音频的多样化。除了蓝牙,还有各种私有协议和低延迟无线方案。每种方案的延迟特性、带宽特性、可靠性都不一样,架构要能适配这些差异。趋势是把无线协议抽象成统一的设备接口,让上层不用关心底层是什么无线技术。
6.3 我个人的架构设计原则
做了这么多音频项目,我总结了几条自己的原则。
第一条:数据流要单向且清晰。音频数据从源到宿应该是一条直线,中间可以有处理节点,但不应该有环路。环路是bug的温床,也是延迟的隐形来源。
第二条:控制流和数据流分离。控制流(路由决策、参数设置)可以慢,可以复杂;数据流(音频采样)必须快,必须简单。两者混在一起,数据流就会被控制流拖慢。
第三条:每一层都要能独立测试。好的架构应该允许你单独测试HAL层、单独测试混音器、单独测试重采样器。如果一层出问题必须整条链路一起调,那架构就有问题。
第四条:默认配置要保守,优化要显式。默认用大缓冲区、高质量重采样、完整处理链路,保证基本可用。低延迟、省电这些优化,让应用显式请求,框架按需开启。这样不会为了少数场景牺牲多数场景的稳定性。
第五条:日志和探针要内建。音频问题难查,所以架构设计时就要把关键节点的日志和探针考虑进去。不要等出了问题再加,那时候改动成本高得多。
这套原则不是教条,是我踩了足够多坑之后形成的直觉。每次做新项目,我都会拿这几条对照一下,看看哪里可能出问题。事实证明,大部分音频架构的坑,都能在这几条原则里找到影子。
最后分享一个我常用的验证方法:拿一段包含人声、音乐、低频、高频的复杂音频,在目标系统上完整播放并录制,然后做客观分析(频谱、失真度、延迟)加主观试听。这个方法能一次性暴露大部分架构问题,比零散地测单项指标高效得多。音频这东西,最终是给人耳听的,客观指标再漂亮,听感不对就是不对。