ARTICLE DETAIL

资讯详情

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

杰理方案音频解码器启动与关闭:从open到close的完整流程与避坑指南

杰理方案音频解码器启动与关闭:从open到close的完整流程与避坑指南 做杰理方案这些年从AC690X一路调到AC701N被问得最多的还是解码器的启动和关闭。很多刚接触杰理SDK的人拿到的demo里就是一句audio_decoder_open加一句audio_decoder_close看着简单真放到项目里跑一遍问题就全出来了解码器打不开、打开后没声音、声音断断续续、close之后内存没释放、二次打开直接死机。这篇文章把启动解码和关闭解码这套流程从头到尾捋一遍讲清楚open和close背后到底发生了什么、参数怎么配、缓冲怎么给、关闭顺序怎么排以及我在实际项目里踩过的那些坑。这篇东西适合谁看正在调杰理AC69XX/AC70XX/AC79XX播放功能的嵌入式工程师尤其是从其他平台转过来、第一次碰杰理SDK的朋友。这类文章网上很少写透大多数只是贴个demo我争取把文档里没有的细节都补上。1. 先把解码这件事想清楚它在音频链路里处在哪个位置1.1 解码器不是读文件而是一条完整的数据管线很多同事第一次写播放功能时以为解码器就是一个把MP3文件转成声音的函数。其实不对。杰理芯片上的解码器严格说是解码服务它要从文件系统里读压缩数据进行格式解析把MP3、AAC、WAV、FLAC这些编码还原成PCM裸数据然后通过DMA送到DAC或者IIS接口最后经过功放输出到喇叭。这个链条里读数据、解析头、解帧、输出PCM每一环都可能出问题而启动解码和关闭解码就是管这条管线的开和关。理解这一点很重要因为open和close并不是简单的创建对象/销毁对象它们背后涉及解码库实例的申请每种编码格式的解码库占用的内存不同解码任务线程的创建与销毁杰理平台上的解码通常跑在独立任务里DMA通道和音频接口的占用释放采样率、声道数、位深等音频参数的配置和复位电源域的管理有些芯片在解码时需要把相关模块的电打开关闭后还要还原。拿着这个视角再去看启动和关闭很多玄学问题就不玄了。比如你close之后DAC没关干净进入低功耗时电流偏高这根本不是解码器的问题而是关闭流程里音频外设没有复位。后面我会重点讲。1.2 启动和关闭解码面向的是哪几个真实场景杰理平台上的启动解码和关闭解码不是只有播放MP3一个场景。我遇到过的真实需求大概有这几类场景A按下播放键从SD卡或U盘读一首歌开始播播完自动切下一首切歌时要把上一首的解码器关掉再重新开一个新的解码器。场景B蓝牙音频模式下手机推来的A2DP流需要解码电话进来时要暂停解码甚至关闭解码器腾出CPU和内存给通话挂断后再恢复。场景C语音提示音、按键音和主音乐之间要切换不同音频源共享一个解码器还是各开一个需要做决策。场景D低功耗待机前必须把所有解码任务关掉、把DAC通道释放否则进入不了浅睡或深睡。不同场景下启动和关闭的时机、参数、资源处理方式差别很大。比如场景C主音乐和提示音如果各开一个解码器内存压力会明显上来通常的做法是提示音使用独立小解码器或者干脆用预制的PCM/WAV数据直接灌到DAC不走通用解码。这些取舍后面聊。2. 启动解码接口调用背后的状态流转和参数门道2.1 open接口看着简单它内部替你做了哪些事以我在AC70XX和AC79XX项目里用过的杰理SDK为例启动解码的核心接口是类似这样的形式struct audio_decoder *dec audio_decoder_open(fops, task_id);fops是文件操作句柄task_id是解码任务ID。部分SDK版本里还要传一个audio_format或者解码回调新一点的SDK把它封装成了decoder_create加decoder_start两步调用但原理没有变。这个open一进来内部做的事情按顺序大致是校验传入的参数比如fops是否为NULL、task_id是否合法申请解码器句柄结构体这段内存一般是从系统内存池里动态分配的根据fops读取文件头判断文件类型ID3、RIFF、ADTS这些头确定要加载哪种格式的解码库解码库初始化包括码率、采样率、声道数的自动识别分配解码输入缓冲区和输出缓冲区这是内存消耗的大头启动解码任务创建任务用的栈空间也是一块不小的内存注册解码完成回调给上层通知EOS。也就是说一次open可能消耗几十KB甚至上百KB内存。在只有几百KB RAM可用的芯片上这非常可观。我后面说的解码器反复开关导致内存不足根源就在这里一次没释放两次没释放第三次之后系统内存池就告急了。2.2 参数配置的决定性作用启动解码时除了open调用本身最重要的是把输出参数配对。杰理的解码器输出直接对接音频通道常见的配置项包括解码格式MP3、WMA、AAC、FLAC、WAVPCM/ADPCM等。同一个open接口通过头信息自动识别但如果你知道固定格式可以强制指定省掉识别时间输出采样率44.1kHz还是48kHz这决定DAC和时钟配置错了会变调声道选择立体声还是强制单声道单声道输出可以省一半的缓冲和DAC功耗解码码率上限对CBR小码率文件可以配置固定码率减少CPU占用。曾经有个项目播放48kHz的WAV文件一切正常切到44.1kHz的MP3声音明显偏快、偏高。排查到最后是输出采样率被固定成了48kHz而MP3解码器按文件头识别出了44.1kHz但音频输出模块没有跟着切换。杰理平台上的做法一般是解码器识别出采样率后通过回调通知音频输出模块重新配置DAC你在驱动里要接好这个回调否则就出现上面这种能响但音调不对的问题。2.3 解码缓冲区的分配原则缓冲区怎么配是新手里最容易随便填、又最容易出问题的地方。先说输入缓冲。它用于存放从文件系统读出的压缩数据太小会导致频繁读卡、解码饥饿、声音卡顿。一般建议至少能容纳几百毫秒的压缩数据。再说输出缓冲。它是解码后PCM数据的暂存区太小会让DAC周期性地等不到数据表现为哒哒的断音。举个例子一个典型MP3128kbps每秒产生约16KB的压缩数据解码后44.1kHz双声道16bit PCM每秒约176.4KB。输出缓冲区如果按50ms算至少要8.8KB如果按100ms算就要17.6KB。在内存紧张时很多人把输出缓冲压到很低结果就是DAC欠载。我建议先按100ms到200ms的PCM输出缓冲起步然后把输入缓冲控制在64KB以内结合SDK的内存池大小逐步压。这个数值在不同SDK版本里体现为不同的宏定义比如DAC_BUFFER_SIZE、DECODE_BUF_SIZE之类各项目改法不一样但原则是先富余再优化。项目初期别抠得太狠等整个播放流程稳定了再一点点往下压每压一次都做48小时老化测试确认没有卡顿再收。3. 关闭解码释放资源比打开更需要细心3.1 close的正确调用方式关闭解码的接口一般是audio_decoder_close(dec)。看起来就是一行但有几点必须注意。第一时机。关闭解码的时机要选在解码线程安全的位置。杰理平台一般在音频任务上下文中执行close输入参数里带task_id就是为了让close往解码任务里发一个退出消息而不是直接从外部把任务杀掉。正确模型是外部发起关闭请求解码线程处理完当前帧后退出循环再把资源释放。这个优雅关闭和强杀任务的差别就是后面讲的死机问题的根源。第二顺序。先停输出再关解码还是先关解码再停输出我的经验是先让解码任务退出确保没有线程再往DMA缓冲区写数据再去关DAC通道、释放缓冲最后释放解码器句柄。如果顺序反过来DMA可能还在搬运一块已经被释放的内存系统会进hardfault。第三清理。关闭后要把句柄置NULL回调函数解除注册防止DMA中断里还调用到已经释放的回调。这一步很多人忽略导致的故障非常隐蔽。我自己就在一个项目的待机唤醒流程里栽过一回close之后没把回调解挂唤醒瞬间解码器回调被触发直接踩了野指针。3.2 关闭时的三种异常表现我整理一下close过程中常见的三种异常方便你对号入座异常现象大概率原因处理思路close之后立刻死机DMA还在搬运已释放的内存DAC先于解码任务被关严格按先停任务、再停外设的顺序调整close返回后再open失败报内存不足close没有走完整资源释放句柄和缓冲区泄漏用内存池水位统计排查定位泄漏点close之后系统能跑但进不了低功耗解码任务没真正退出、定时器未停、音频接口未释放检查任务句柄状态退出循环后把相关时钟全关了这三种问题我全在项目里见过每一种解决起来都不难难的是复现和定位。所以建议从一开始就在close路径上多打日志把申请资源栈地址和释放资源栈地址成对打出来对账就一目了然。3.3 启动和关闭必须成对出现这听上去像废话但在杰理平台上特别重要。因为很多代码路径是事件驱动的播放事件、切歌事件、暂停事件、待机事件每个事件都可能是不同文件写的很容易出现播放在A文件里open暂停在B文件里只做了状态置位没做close结果资源泄漏。我见过一种脏做法每个需要播放的地方都无条件open把旧句柄直接覆盖。旧解码器没有被close于是每次切歌都漏十几KB内存播几十首之后系统就开始卡顿、蓝牙断连最后复位。正确做法是维护一个播放状态机在任何新播放开始之前先确保旧解码器已close或者让open接口内部设计成先关旧、再开新。4. 一套可以抄作业的播放流程4.1 代码骨架给一个比较通用的播放流程基于杰理SDK的常见接口具体函数名以实际SDK版本为准。/* 播放一个文件 */ struct audio_decoder *dec NULL; struct audio_fops file_fops; file_fops.open my_file_open; file_fops.read my_file_read; file_fops.close my_file_close; file_fops.lseek my_file_lseek; file_fops.length my_file_length; /* 如果有旧解码器先关 */ if (dec ! NULL) { audio_decoder_close(dec); dec NULL; } /* 打开解码器 */ dec audio_decoder_open(file_fops, task_id); if (dec NULL) { /* 错误处理返回错误码 */ return -1; } /* 配置输出参数采样率、声道数、位深 */ audio_decoder_set_output_para(dec, 44100, 2, 16); /* 启动解码 */ audio_decoder_start(dec);这个骨架最核心的是open前后的成对逻辑先清旧状态再开新句柄。别小看这两行很多卡顿问题出在旧句柄没有清理干净。另外注意fops里的open/read/close不要直接对文件系统裸调中间最好加一层带锁的包装防止解码任务和主任务同时操作文件指针导致读取位置错乱。4.2 状态事件处理播放过程不是一路绿灯需要处理的事件至少包括播放中数据不足文件读取慢导致欠载解码错误格式不支持、文件损坏播放完成EOS用户主动停止每个事件都对应一个明确动作。比如EOS事件里要先把状态置为停止再close解码器然后根据播放列表决定下一首是自动播放还是停在停止状态。我习惯用一个枚举状态变量typedef enum { PLAY_IDLE, PLAY_OPENING, PLAY_PLAYING, PLAY_PAUSED, PLAY_STOPPING, } play_state_t;每次状态切换时打印日志后面调试问题会省很多事。千万别图省事只用一个bool表示在播放满足不了切歌和暂停同时处理的场景。4.3 带按键控制的示例写一个带按键控制的小例子。按键短按播放/暂停长按停止。void key_short_press(void) { if (play_state PLAY_PLAYING) { audio_decoder_pause(dec); play_state PLAY_PAUSED; } else if (play_state PLAY_PAUSED) { audio_decoder_resume(dec); play_state PLAY_PLAYING; } else { start_play(C:/music/test.mp3); } } void key_long_press(void) { if (dec ! NULL) { audio_decoder_close(dec); dec NULL; play_state PLAY_IDLE; } }注意暂停时不能close因为暂停要保留解码位置方便恢复停止时则必须close把资源释放出来。这两者的区别要记牢很多项目为了省事暂停直接close导致恢复播放时从头开始非常影响体验。5. 真实项目中踩过的坑和排查方法5.1 解码器反复开关导致的内存不足某车载蓝牙项目用户连续切歌二三十次系统开始卡顿、蓝牙断连最后复位。用内存统计看每切一次歌内存池少了约20KB每次close之后没有完全归位。后来定位到原因是文件句柄释放了、解码器句柄释放了但解码任务用的栈空间没有回收因为close内部等待任务退出用了超时机制超时后任务没退出栈空间就泄漏了。解决办法排查是哪个条件导致解码任务没退出延长等待超时时间并且打开SDK里的强制释放选项。后来我也总结出一个习惯在内存统计接口里定期打印各任务栈使用率和剩余堆内存项目阶段就能尽早发现问题。这个习惯帮我省了很多通宵查bug的时间。5.2 播放中直接close导致系统复位另一个项目工程师在按键处理里直接调用close结果一按停止键系统就复位。看复位日志是总线错误。原因是close发消息给解码任务但解码任务正在等待DMA中断同步按键处理函数没有先同步等待解码任务结束就从外部把DAC关了DMA还挂着。关闭时DMA下一笔传送直接踩到已释放的缓冲地址触发hardfault。正确流程是外部请求停止后先置一个stop_flag解码线程里探测到该标记就正常退出循环并释放资源外部只负责等待状态变为IDLE而不是直接关DAC。这一步处理干净这台机器的停止按键问题才彻底消失。5.3 多实例解码相互抢占问题有些项目需要在音乐播放中叠加提示音于是一口气开了两个解码器。两个解码器共享同一个DAC通道输出采样率和声道数不一致时DAC被反复切换配置结果音乐声音发闷、提示音炸音。我的做法是分层主音乐用通用解码器输出到主DAC通道提示音用短小的预编码WAV或几百字节的PCM不经解码器直接放进一个小DMA缓冲区发送到另一个输出通道。如果硬件只有一个DAC则提示音播放时先暂停主解码器输出用换源方式处理。优先级和通道分配必须在项目启动前定清楚不然后期改起来特痛苦。最后再分享一下我个人的操作习惯。我在杰理方案上做播放功能这几年反复验证下来启动解码和关闭解码这件事拼的从来不是open和close那两行代码而是资源管理是否严谨、状态机是否周全、释放顺序是否正确。刚上手的朋友可以先把demo跑通再往里面加暂停、切歌、低功耗这些逻辑每加一个功能就盯一下内存池水位和任务状态这样能少走很多弯路。写到这里下一篇可以聊聊杰理解码中的缓冲区和低功耗协同配置如果大家在实际项目中遇到这类问题欢迎带着具体现象来交流。
返回列表