有同行在调国产蓝牙音频SoC的SDK时问我:代码里DEC和ENC到底是什么意思?方向总是搞反,配置也容易漏。这个问题看起来基础,但确实卡住过不少人。简单说,DEC就是Decoder(解码器),负责把压缩格式还原成PCM;ENC就是Encoder(编码器),负责把PCM压缩成目标格式。放在蓝牙音频SDK里,解码器管音乐播放、通话下行这条链路,编码器管麦克风采集、通话上行这条链路。
这篇内容就围绕这套音频SDK展开,说说DEC和ENC在源码里的落点、典型使用场景、参数配置逻辑,以及调音质调延迟时绕不开的坑。适合正在读SDK音频代码、做蓝牙音箱/耳机/车载方案的开发朋友,刚接触SDK的也能看明白。厂商品牌就不具体点了,反正你打开手头那套工程,目录结构和思路基本都能对上。
1. DEC和ENC到底是什么:先理清音频链路的方向
1.1 名字拆解:Decoder和Encoder各管哪一段
DEC的全称是Decoder,也就是解码器。它的输入是SBC、AAC、mSBC、ADPCM这类有损或无损压缩格式的比特流,输出是线性PCM数据,通常是16bit、48kHz或者44.1kHz的裸流。解码器不关心声音好不好听,它只负责“还原”,把编码时丢掉的信息之外的部分尽量恢复出来。
ENC的全称是Encoder,也就是编码器。方向正好反过来:输入是PCM裸流,输出是压缩后的码流。为什么要把PCM压缩?因为蓝牙空口带宽有限,PCM 16bit 48kHz单声道就是768kbps,双声道直接1.5Mbps,蓝牙经典链路根本扛不住;通话场景的CVSD和mSBC也都是为窄带/宽带语音设计的编码格式。编码器存在的意义就是降低数据量,让数据能在有限的信道里传出去,或者能塞进存储介质。
很多人会把DEC等同于“播放”,把ENC等同于“录音”,大方向没错,但不严谨。播放本地MP3、WAV文件时,确实要经过解码器;但如果你在录音时选择存储成ADPCM格式,那么录音链路里一样有编码器。所以准确的理解是:凡是把压缩数据变成PCM,走的就是DEC;凡是把PCM变成压缩数据,走的就是ENC。
1.2 为什么一套SDK里同时需要这两个模块
蓝牙音频设备一般有三种工作状态:听音乐、打电话、本地录音/播放提示音。听音乐走A2DP协议,链路里的音频数据是压缩的,所以设备端必须解码,这里用到DEC。打电话走HFP协议,通话下行(对方说话)是压缩数据,需要解码后送到喇叭;通话上行(本地麦克风)要把PCM编码成协议要求的格式再发出去,这里同时用到DEC和ENC。
也就是说,一个完整的通话通话业务,DEC和ENC是成对出现的。哪怕你看SDK代码时发现配置项里只开了“HFP”,实际跑起来也是下行一个解码器实例、上行一个编码器实例同时在干活。
本地录音的场景更直白:麦克风采集进ADC,得到PCM,如果直接存WAV文件,不需要编码器;但很多嵌入式设备为了省Flash空间,会把录音存成ADPCM或者其它压缩格式,这时候就必须挂ENC。反过来播放这个文件,再挂DEC。所以说这两个模块看似简单,却是整个音频音控链路的地基。
1.3 上下行链路:DEC和ENC在数据流中的位置
为了更直观,我一般习惯把蓝牙音频设备画成两条流水线:
下行(音频进来):蓝牙协议栈收到压缩流 -> 进解码器DEC -> 输出PCM -> 进DSP做音效(EQ、动态增益等)-> 到DAC -> 功放 -> 喇叭。
上行(声音出去):麦克风 -> 模拟信号 -> ADC -> 得到PCM -> 进DSP做AEC回声消除和降噪 -> 进编码器ENC -> 压缩流 -> 蓝牙协议栈发送出去。
记住这个方向之后,再回头去读SDK代码就会轻松很多。你在源码里看到“dec”开头的结构体或文件,就在下行链路里找;看到“enc”开头的,就在上行链路里找。方向对了,后面排查问题才不迷路。
2. 代码里怎么找DEC和ENC:SDK音频框架的常见布局
2.1 音频框架分层:接口层、逻辑层、驱动层
这套SDK的音频部分一般不会是一个巨大的单文件,而是分成三层:最上层是给应用调用的API,比如打开解码器、启动编码器;中间是逻辑控制层,负责状态机、缓冲管理、格式协商;最底层是具体算法的封装,比如SBC解码、mSBC编码、ADPCM编解码。
把这三层想成一个餐厅:API层是服务员,你点菜只要和服务员说;控制层是厨房调度,决定哪个灶台炒哪道菜、什么时候出菜;算法层是炒菜师傅,SBC师傅只管SBC,AAC师傅只管AAC。你排查问题的时候,先看服务员有没有把单子传给调度,再看调度有没有叫对师傅,最后才去怀疑师傅手艺。
实际在代码里逛一圈,你大概率会看到类似这样的结构:
audio/ ├── dec/ │ ├── dec_sbc.c │ ├── dec_aac.c │ └── dec_msbc.c ├── enc/ │ ├── enc_sbc.c │ ├── enc_msbc.c │ └── enc_cvsd.c ├── algo/ │ ├── aec.c │ └── anc.c ├── stream/ │ ├── audio_stream.c │ └── audio_buffer.c └── api/ └── audio_play.c不同版本目录名可能叫audio或media或player,但模块拆分的思路大差不差。你只需要认准:dec和enc通常是独立目录或独立文件,算法层不会跟逻辑层混在一起。如果工程里没有dec目录,也别慌,可能被统编进了av_engine或plaer目录,用“dec_”和“enc_”做关键字全文搜索就能找到。
2.2 DEC相关的源码和配置项:初看一头雾水,抓关键字就行
开始读代码时不要一头扎进算法实现,先看导出接口。一般解码器会暴露类似aud_dec_open、aud_dec_close、aud_dec_process这样的API。这些API名字不固定,但思路是一致的:创建解码器实例、喂数据、取PCM数据。
以听歌场景为例,工程里通常会有一处宏开关,比如:
#define AUDIO_DEC_SBC 1 // 支持SBC解码 #define AUDIO_DEC_AAC 1 // 支持AAC解码这些宏决定了最终编译进固件的解码器类型。A2DP连接的时候,设备会和手机协商codec,协商结果如果没被编进固件,解码器就打不开,声音自然出不来。所以我每次拿到一个新SDK版本,第一件事就是把宏清单拉出来看看默认开了哪几个解码器。
配置完宏之后,SDK运行时会创建解码器实例,这个实例一般是一个大结构体,里面包括输入缓冲区指针、输出PCM缓冲、采样率、声道数、位深、以及当前状态。调试时打印这个结构体的状态字段,能看到解码器究竟是处于“空闲”还是“运行”或者“异常”。
2.3 ENC相关的源码和配置项:不在A2DP里,主要藏在通话和录音里
如果说DEC是台前明星,ENC就是幕后工作人员,很多工程师把它忽略掉。通话没上行声音、录音全是杂音,八成是ENC没配对。
通话上行用到的编码器宏一般是:
#define AUDIO_ENC_MSBC 1 // 宽带通话,采样率16kHz #define AUDIO_ENC_CVSD 1 // 窄带通话,采样率8kHz这两个编码器和HFP协议绑定。HFP 1.6以上基本都支持mSBC,老一点的对端才走CVSD。宏不全,或者打开方式不对,就会出现能听到对方说话、对方听不到你的情况。
本地录音场景里还常见ADPCM编码:
#define AUDIO_ENC_ADPCM 1录音时你选择保存为ADPCM文件,底层就是调用ENC模块做压缩。注意:ADPCM编码器的采样率、通道数必须和ADC配置一致,不一致录出来的就是变调甚至爆音。
2.4 开关日志定位DEC/ENC状态:做一个“会看代码的调试者”
SDK通常都有日志开关,按模块划分。调试音频问题时,把DEC和ENC模块的日志打开很有用。比如:
#define DBG_AUDIO_DEC 1 #define DBG_AUDIO_ENC 1日志打开后,串口会打印解码器/编码器的打开时间、参数信息、异常退出原因。一次完整的蓝牙连接,日志里能看到类似“dec open sbc 44.1k stereo”“enc open msbc 16k mono”的信息。如果在日志里只看到DEC没看到ENC,说明通话上行没被正确启用;反之亦然。
实战中我曾经遇到过一个问题:播放歌曲声音正常,但切歌偶尔会有几秒静音。日志显示“dec open failed”,原因是被上一首歌曲的实例占着资源,新的实例开不出来。这时候光是改播放应用层的逻辑没用,需要在切歌时先把上一个解码实例彻底释放,再创建新的实例。这就是看日志能少走弯路的原因。
3. 实际应用场景:到底什么时候用DEC,什么时候用ENC
3.1 音乐播放场景:A2DP Sink的DEC路径
做蓝牙音箱、蓝牙耳机时,设备工作在最常见的A2DP Sink模式,也就是听歌模式。手机的音频数据经过蓝牙传输过来,在SDK底层首先会被蓝牙协议栈拆包,组装成完整的数据帧,然后交给DEC模块解码。
这里有一个容易忽略的点:A2DP除了SBC,还可能协商AAC、LDAC等格式。你只把SBC解码编进固件,手机愿意发AAC也没用,协商结果最终会回退到SBC。但要注意,SBC在不同的BT协议栈版本里,默认参数可能不一样,有些默认是44.1kHz双声道,有些则是48kHz。如果你的DEC配置写死44.1kHz,遇到48kHz的SBC流,解码器也会打开失败或声音发闷。
在SDK里,A2DP音乐播放的DEC配置往往不是静态写死的,而是在协商完成后动态设置的。源码里会有一个格式协商回调,里面调用了解码器参数设置接口。你不需要完整看懂每个参数,但需要知道:协商回调里的采样率字段、声道字段和解码器最终打开的配置是强相关的。
3.2 通话场景:HFP里的双向编解码
通话场景和听歌最大的区别在于:听歌只要DEC,而通话要DEC加ENC同时工作。
下行方向,蓝牙收到对方的声音压缩流,比如mSBC,进入DEC,解码成16kHz单声道PCM,送到DAC输出到听筒或喇叭。上行方向,本地麦克风采集的声音经过ADC变成PCM,经过AEC回声消除和降噪之后,进入ENC编码成mSBC码流,再通过HFP通道发出去。
我第一次调通话音量时踩过一个坑:下行本机音量大不大,不取决于DAC输出功率,而取决于DEC输出的数字增益配得合不合理。有些SDK的解码器实例里带一个数字音量字段,默认可能是0dB,你觉得声音小,使劲调功放增益,结果削顶失真了。后来把DEC输出的数字增益调到+6dB,功放保持线性区,整体效果反而好很多。
3.3 录音和本地播放:最容易忽视的ENC/DEC组合
本地录音看起来不比蓝牙复杂,但很多人会在格式选择上翻车。想存成WAV文件,录音链路不需要ENC,因为WAV就是PCM加文件头;想保存成ADPCM,ENC就必须出场。
SDK里的录音模块通常会有两套路径:一套是裸PCM直通保存,另一套是编码后保存。对应地在界面上可能叫“WAV录音”和“ADPCM录音”。你这个项目如果是要做语音备忘录,建议直接WAV,省心。但如果Flash空间紧张,一个10分钟录音用WAV要几十MB,ADPCM能压到四分之一甚至更小,那就必须用ENC。
在编码格式选择上,ADPCM有好几个变种,IMA-ADPCM和G.726不是一回事。SDK里一般会固定支持一种,你要在录音头文件里写入正确的编码标识,否则自己写的播放器能播,电脑上可能打不开。别问我为什么知道,我拿电脑播放器折腾了两个小时。
喇叭播放提示音看起来不需要解码器,但如果你播放的是一个MP3格式的开机音,SDK就会复用DEC模块。有些SDK为了省RAM,为提示音单独做了一个轻量播放器,只支持WAV。这种情况下,你如果把MP3直接丢给轻量播放器,就会听到“嘶嘶”的噪音或直接无声。
3.4 提示音和UI音效:能不能绕过编解码器
很多开发者在做按键音、电量提示时,喜欢直接放小文件。在资源紧张的项目里,能省则省,所以弄清楚提示音路径是不是必须走DEC非常重要。
在多数这套SDK的实现中,提示音播放有两种方式:第一种是直接把PCM数据放到Flash里,播放时从Flash搬数据到DMA,根本不经过编解码器,这是最省CPU的方案。第二种是放一个压缩格式文件,每次播放都要现场解码,CPU占用就高一些,但Flash占用少。
CPU资源宽松的用第二种,反正解码个几十毫秒的小文件没什么感觉;如果CPU紧张,比如正在跑AEC、降噪这些算法的时候,再叠加解码器的耗时,可能造成其他任务被抢占。项目中期如果发现系统有偶发卡顿,回顾一下提示音是不是也用了DEC、是不是太频繁,经常能找到根源。
4. 参数配置和调优:采样率、比特率、缓存延迟的门道
4.1 采样率和位深的匹配:所有参数必须一条链对齐
音频系统最忌讳“以为对齐了”。举个例子:通话场景上行是16kHz mSBC编码,麦克风的ADC采样率就必须配成16kHz,如果ADC还是默认48kHz,编码器拿到48kHz的PCM去按16kHz处理,出来的声音就像磁带被匀速拉长了一样,又闷又慢。
下行也一样:解码器输出48kHz PCM,后级DAC就必须工作在48kHz,如果DAC配置成44.1kHz,声音会整体偏快。在实际SDK调试里,这种问题最常见的表现是“声音重采样没使能”。有些SDK具备自动重采样能力,但需要你在配置里把重采样开关打开,否则即使源和目标格式不同,它也不会自动转换。
我的建议是拿到一个项目后,先把整条链路的采样率表格画出来,从蓝牙协议协商值一直画到DAC和ADC。中间任何一档变了,就要检查那里有没有重采样点。这样能减少大量无效调试时间。
4.2 编码格式选择:SBC、AAC、mSBC、CVSD背后的权衡
解码/编码格式不能只看支不支持,还得看场景的带宽和时延约束。
SBC是A2DP的强制格式,所有蓝牙设备必须支持。SBC的音质上限不算高但可接受,胜在兼容性好。AAC码率更高、音质更好,但解码复杂度也高,对芯片主频和内存都有要求。小内存方案跑AAC容易出现解码超时,表现就是低频卡顿。
mSBC和CVSD用于HFP通话。mSBC是16kHz宽带语音,清晰度明显好于8kHz的CVSD。只要对端设备支持mSBC,就优先选mSBC,但要注意mSBC的编码延迟比CVSD略高,也只有1-2ms级别,不影响正常通话体验。
记住一个原则:功能上先保证“必须有”,再去选“最好”。SDK的宏默认值一般比较保守,比如只开SBC和CVSD,音质肯定一般,但不会出错。你在选配的时候要考虑自己的产品定位,不是所有功能全开就好。
4.3 缓存和延迟的平衡:缓冲区大不一定稳
蓝牙音频什么时候出现卡顿?通常是数据来不及取或者来不及送。SDK里的DEC/ENC实例一般都会带缓冲,这个缓冲大小是一个可以调的参数。
缓冲设大了,抗干扰能力会强一些,因为短暂的链路拥堵还能靠缓冲撑过去。但代价是延迟变大,看视频或玩游戏时音画不同步,影响体验。缓冲设小了,延迟低,但容易因为一次丢包或者DMA偶发异常就断流。
调这个参数不能拍脑袋。我一般会先测一下最差情况下的链路中断时长,用串口日志看丢包间隔,再把buffer time设成丢包间隔的2倍。比如实测蓝牙偶尔丢包会造成15ms的空洞,那缓冲区至少留30ms余量。这样既不浪费内存,也基本听不出卡顿。
4.4 AEC、降噪和编解码器的位置关系:谁先谁后很重要
通话场景光有ENC和DEC还不够,AEC(声学回声消除)的挂载位置直接影响通话质量。
正确的链路是:麦克风PCM先做AEC和降噪,再送ENC编码。如果顺序反了,把编码后的压缩数据再去做AEC,不仅效果差,还会引入额外延迟。排查通话有回声时,先确认AEC处理的数据源是不是原始PCM,而不是解码后的PCM。
很多SDK会把语音链路封装成一条固定的“语音处理链”,类似源-滤波器-输出。你不需要自己去拼流程图,但需要看懂哪个节点是AEC,哪个节点是ENC。一般节点名称都会带aec、ns、enc之类标识,顺序对得上就不用动。
另外还有一点,AEC需要参考信号,也就是要拿到喇叭正在播的参考数据。如果你在调试时,扬声器播放声音走的是另一条通路,没有把参考信号回灌给AEC模块,回声消除就会失效,表现出来就是对方能听到自己说话的回声。这个问题排查起来容易钻牛角尖,其实只要确认参考信号接没接。
5. 常见问题和排查技巧
5.1 播放卡顿,声音断断续续
遇到这种问题,先分场景:是蓝牙听歌卡,还是本地播放卡。蓝牙听歌卡,优先怀疑蓝牙带宽和抗丢包能力,再怀疑DEC处理速度不够。本地播放卡,基本就是解码性能或DMA搬运不及时的问题。
排查思路这样走:先看日志里DEC有没有频繁报“buffer underrun”,有就说明数据供给不及。此时把蓝牙的丢包重传参数调大或增大输入缓冲。再看CPU占用,如果解码线程CPU占用率接近上限,就考虑降比特率,或者换低复杂度解码配置。SBC有一个bitpool参数,调低一点,CPU占用立刻下降,代价是音质稍差。
我印象里有一次卡顿特别蹊跷:歌曲前半段流畅,后半段开始卡。查了很久,发现是内存碎片导致后半段解码器的临时缓冲申请失败,回退到了慢速路径。解决方案不是调解码参数,而是把解码器缓冲改成启动时一次性分配,运行中不再申请内存。这个经验告诉我,卡顿不一定都是编解码的问题,也要看内存管理策略。
5.2 通话有回声,对方能听到自己
除了前面说的AEC参考信号问题,还有一个常见原因是喇叭声音过大导致声学回声超过AEC处理范围。AEC对回声的压制比例是有限的,比如30dB,你功放输出增益太高,残余回声仍然能被对方听到。这时候调低功放增益或者加大喇叭和麦克风的物理隔离,比在算法里死磕更有用。
另一个隐藏坑是:AEC一般只保证单声道回声消除,如果你的设备上了双Mic,其中一个Mic采集到的噪声和回声没有被算法覆盖,对方还是会听到嘈杂的声音。遇到双Mic通话项目,一定要确认两个Mic的数据都进同一套语音处理链,而不是各走各的。
5.3 录音文件播放异常:放出来是噪声或变调
录音文件播放异常,多半是编码参数和文件头不一致,或者录制时格式和播放时格式对不上。我处理过一例:录音时默认配置是16kHz单声道,但播放模块按照44.1kHz双声道去播放,出来的声音就完全没法听。
解决的关键是:文件里要存参数,播放时读参数。所以录音头必须写准确,播放器必须有兜底判断。如果文件头丢了或者写错,播放器怎么猜都没用。
还有一种是编码器本身的bug,比如某些采样率下的ADPCM编码表溢出。这种问题需要换数据源做对比测试:用固定正弦波输入录音,再分析输出数据的频谱,如果频谱上出现非预期的谐波,就说明编码器实现可能有边界问题,考虑换一种编码格式绕过去。
5.4 内存占用超标或打开编解码器失败
SDK默认配置往往把各种编解码器的缓冲区都开得很大,如果你项目的内存只有一二百KB,不多加小心就会失败。打开DEC/ENC失败日志通常会提示“mem alloc fail”。
对策有三条。一是裁剪不用的格式,SBC、AAC、mSBC、ADPCM全开,内存自然就爆了,根据产品用到的场景,只保留必要的编解码器。二是调小单个实例的缓冲,比如解码器默认配置4个缓冲块,改成2个,内存少一半,代价是抗抖动能力下降。三是检查是不是每次播放都创建实例、停播才释放,如果反复创建释放,内存碎片叠加,时间一长也会分配失败,建议播放前统一管理解码器实例的初始化和反初始化。
另外提一句,在低功耗蓝牙方案里,编解码器模块的电源管理也要看。SDK有时候会在没有音频任务时对DEC/ENC做clk gate,如果唤醒条件设置得不合适,第一次打开解码器时会有明显延迟。表现为“第一个声音总是偏慢”。解决办法是预先初始化解码器模块,或者把唤醒中断优先级提高。
最后再分享一点小经验:不管DEC还是ENC,都是SDK里最接近数字音频本质的模块,参数看起来多,但逻辑很线性。方向搞清楚、链路画出来、日志打开,大部分问题都能按图索骥。如果真遇到编解码器内部算出来的数据不对,那就要怀疑算法库版本,换个版本或者咨询原厂FAE,比自己啃汇编快得多。