这是一套 4G Cat.1 嵌入式方案 :移芯 EC718PM 芯片(Cortex-M3 306 MHz、4 MB Flash / 4 MB RAM)+ NT26F6D0 模组 + ES8311 codec,跑 RTOS。固件开源在
tangeai/xiaotai-lierda,本文代码以examples/NT26FxDx_OpenKit/tirtc_pro/media/为根,涉及audio_device.c与audio_device.h。
一、一颗 codec 被五个功能抢
4G 对讲设备上有个复现率不低的毛病:AI 对话挂断,用户立刻再喊一次,界面显示已经在听,喇叭却一声不出。重新开机又好了。
排查方向通常不在音频算法上。挂断那一刻,旧会话的收尾还排在 worker 队列里;等它终于被执行时,新会话已经拿到这颗 codec 并配好了音量------那笔收尾把刚配好的音量又写成了 0。网络侧照常收发,看不出异常。
终端上同时能跑的东西有:AI 语音对话、微信与设备之间的音视频通话、摄像头实时查看与远程对讲、独立多人房间、本机录音回放。头文件的注释把这层适配层称作 shared ES8311 audio adapter,五个业务共用这一份 826 行的实现。板子上只有这一颗 ES8311------就是那颗音频编解码芯片,挂在 I2C0 上配寄存器,PCM 样本走 I2S0(芯片与 codec 之间传音频的串行总线),采集和播放各占一个方向,功放使能接在 GPIO11。
这类 Cat.1 平台上做音视频的团队不少。这套租约也不是纸面推演------它是在这颗芯片上被会话交接的缝隙逼出来的,下面每一步都对着真正在跑的固件。
仓库里 tirtc_lite 和 tirtc_pro 各有一份音频适配层,本文引用的是带屏那版 tirtc_pro;无屏那版另有一份实现,租约和掩码的设计是同一套,细节已经各自走了不同路子------比如同样的有效性判断,pro 里叫 lease_valid(),lite 里叫 demo_ai_audio_lease_valid_locked()。下面出现的行号、变量名、返回值只对 pro 成立。
顺带说清它和上一篇的分工:上一篇让旧音频数据在管道里失效,这篇让旧会话留下的 stop() 失去操作硬件的资格。
挡并发本身不难,一个 busy 标志就够了。麻烦出在收尾。一套完整的收尾是分批的:先停新输入、等 media worker 退出、调 stop()、最后 release()。这几步跨在不同任务上,中间隔着真实的调度延迟。用户挂断那一刻,收尾任务可能还在队列里排着,下一个会话已经 acquire 拿到硬件了。
stop() 收尾时不只关 DMA,它会先把音量写 0 再停硬件,顺序是"先静音、后停"。具体写哪几笔看构建怎么选:打开 AEC 时由 aec_stop() 调 aec_mute(),只写 codec 音量一笔(audio_aec.inc 里就是 Liot_AudioSetCodecVolume(0));关掉 AEC 时走下面这三行,软件音量和 codec 音量各写一个 0。两条分支在源码里用 #if DEMO_AI_AUDIO_AEC_ENABLE 并列,tirtc_app.mk 由 TIRTC_AUDIO_AEC_EN 决定走哪条(开 free guard 时还要求 AEC 必须为 y,否则直接报 error)。后面提到"两笔"的地方,只对关掉 AEC 的构建成立:
c
int sw = Liot_AudioSetVolume(0);
int codec = Liot_AudioSetCodecVolume(0);
int stopped = Liot_AudioStop();
这两笔(AEC 构建下是 aec_mute() 那一笔)如果落在新会话 prepare_session() 之后,会把新会话刚配好的音量又写成 0,还可能把它标记成已停止。症状是本地不出声。至于网络侧是不是照常收发、包序和计数有没有异常,取决于上层怎么传,光看这份适配层判断不了。同理,日志里有没有 error、后续 record 和 play 返回什么,也要看迟到那笔插在哪个位置,没有固定答案。
最容易撞上的形态是同一功能连续占用两次。AI 对话挂断,用户立刻再喊一次,前后两个会话的 owner 完全一样------owner 校验在这种场合帮不上忙,能区分的只有编号。
这里有个前提要先讲清楚:release() 成功时会把传进来的那个租约对象清零(源码里是 lease->owner = DEMO_AI_AUDIO_OWNER_NONE; lease->generation = 0;)。所以真正会出事的不是"同一份收尾没跑完",而是另有一份收尾,手里握着旧租约的副本------同一个对象在 release 之后自己就已经失效了,轮不到它迟到。
#mermaid-svg-QnqpxGoSN1OxI2RL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QnqpxGoSN1OxI2RL .error-icon{fill:#552222;}#mermaid-svg-QnqpxGoSN1OxI2RL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QnqpxGoSN1OxI2RL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QnqpxGoSN1OxI2RL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QnqpxGoSN1OxI2RL .marker.cross{stroke:#333333;}#mermaid-svg-QnqpxGoSN1OxI2RL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QnqpxGoSN1OxI2RL p{margin:0;}#mermaid-svg-QnqpxGoSN1OxI2RL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QnqpxGoSN1OxI2RL .cluster-label text{fill:#333;}#mermaid-svg-QnqpxGoSN1OxI2RL .cluster-label span{color:#333;}#mermaid-svg-QnqpxGoSN1OxI2RL .cluster-label span p{background-color:transparent;}#mermaid-svg-QnqpxGoSN1OxI2RL .label text,#mermaid-svg-QnqpxGoSN1OxI2RL span{fill:#333;color:#333;}#mermaid-svg-QnqpxGoSN1OxI2RL .node rect,#mermaid-svg-QnqpxGoSN1OxI2RL .node circle,#mermaid-svg-QnqpxGoSN1OxI2RL .node ellipse,#mermaid-svg-QnqpxGoSN1OxI2RL .node polygon,#mermaid-svg-QnqpxGoSN1OxI2RL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QnqpxGoSN1OxI2RL .rough-node .label text,#mermaid-svg-QnqpxGoSN1OxI2RL .node .label text,#mermaid-svg-QnqpxGoSN1OxI2RL .image-shape .label,#mermaid-svg-QnqpxGoSN1OxI2RL .icon-shape .label{text-anchor:middle;}#mermaid-svg-QnqpxGoSN1OxI2RL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QnqpxGoSN1OxI2RL .rough-node .label,#mermaid-svg-QnqpxGoSN1OxI2RL .node .label,#mermaid-svg-QnqpxGoSN1OxI2RL .image-shape .label,#mermaid-svg-QnqpxGoSN1OxI2RL .icon-shape .label{text-align:center;}#mermaid-svg-QnqpxGoSN1OxI2RL .node.clickable{cursor:pointer;}#mermaid-svg-QnqpxGoSN1OxI2RL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QnqpxGoSN1OxI2RL .arrowheadPath{fill:#333333;}#mermaid-svg-QnqpxGoSN1OxI2RL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QnqpxGoSN1OxI2RL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QnqpxGoSN1OxI2RL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QnqpxGoSN1OxI2RL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QnqpxGoSN1OxI2RL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QnqpxGoSN1OxI2RL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QnqpxGoSN1OxI2RL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QnqpxGoSN1OxI2RL .cluster text{fill:#333;}#mermaid-svg-QnqpxGoSN1OxI2RL .cluster span{color:#333;}#mermaid-svg-QnqpxGoSN1OxI2RL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QnqpxGoSN1OxI2RL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QnqpxGoSN1OxI2RL rect.text{fill:none;stroke-width:0;}#mermaid-svg-QnqpxGoSN1OxI2RL .icon-shape,#mermaid-svg-QnqpxGoSN1OxI2RL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QnqpxGoSN1OxI2RL .icon-shape p,#mermaid-svg-QnqpxGoSN1OxI2RL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QnqpxGoSN1OxI2RL .icon-shape .label rect,#mermaid-svg-QnqpxGoSN1OxI2RL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QnqpxGoSN1OxI2RL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QnqpxGoSN1OxI2RL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QnqpxGoSN1OxI2RL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 假设版 只比 owner
当前版 再比 generation
T1 AI 会话 1 acquire
owner=AI generation=5
T2 用户挂断
stop 后 release 成功
owner 归 NONE 传入的租约被清零
T3 另一份早前投递的收尾仍排在 worker 队列
手里是旧租约的副本 owner=AI generation=5
T4 AI 会话 2 acquire
owner=AI generation=6
T5 会话 2 prepare_session 完成
codec 音量已按新会话写好
T6 那份迟到的收尾终于执行
携带副本 owner=AI generation=5
这张票还成立吗
放行 掐掉的是会话 2 的声音
5 不等于 6
返回 INVALID_LEASE -30
图里"只比 owner"那条是假设的对照版本,不是当前实现的行为。当前实现走到 T6 时,lease_valid() 这个内部函数返回 false,公共的 stop() 经 enter() 把它翻译成 -30 交给调用方------内部校验是布尔值,返回码是外面那层给的。
二、布尔标志答不了"是不是同一次"
static bool s_audio_busy; 能回答有没有人占着,回答不了占着的是不是当初那一个。值从 false 变 true、再变 false、再变 true,用值本身判断的代码看不出中间发生过一次完整的交接。这类问题在并发里叫 ABA。
只用 owner 也一样,它能答出"现在归 AI",答不出"归的是哪一次 AI"。
把三种常见判据摆在一起,能力边界差得很清楚,卡点都在同一件事上:分不清第几次。
| 判据 | 能挡住 | 挡不住 |
|---|---|---|
| 一个 busy 标志 | 两个功能同时 acquire | 旧会话的收尾落到新会话头上 |
| 只有 owner | 跨功能误操作,比如微信的收尾关掉 AI | 同一功能前后两次会话的混淆 |
| owner + generation | 上面两种 | 绕过 API 直接改全局变量的调用方 |
所以票面上写两样:谁拿的,第几次发出。每发一次换一个编号,旧票就自然作废,不需要谁去遍历队列。stop() 里那两笔写之所以危险,是因为执行它的人手里也可能握着一张过期的票,而他自己看不出这一点。
三、8 个字节的票面
c
typedef struct
{
demo_ai_audio_owner_e owner;
uint32_t generation;
} demo_ai_audio_lease_t;
#define DEMO_AI_AUDIO_LEASE_INIT \
{ DEMO_AI_AUDIO_OWNER_NONE, 0U }
两个成员都是 4 字节宽:enum 在 ARM EABI 默认配置(不开 -fshort-enums)下按 int 存放,加上一个 uint32_t,结构体里没有填充字节,也就是 8 个字节。它全程不进堆------acquire 由调用方把对象地址传进来填,适配层自己不做动态分配。EC718PM 只有 4 MB RAM,音频线程的栈按 KB 计,8 个字节和一次 malloc 在这块芯片上是两回事。要是五个功能各只保存一份、每份按 8 字节算,加起来是 40 字节------这只是一种调用方写法下的算术,适配层既不管你复制几份,也不限制份数。
owner 的取值有五个:AI、微信、设备、直播、群房间。最后一个 DEMO_AI_AUDIO_OWNER_GROUP_ROOM 挂在 HWDEMO_GROUP_ROOM_EN 宏下,不带群房间的构建里这个枚举项连出现都不会出现。
generation 的 0 是保留值。这一条在 lease_valid() 里省掉了一整类判断:
c
static bool lease_valid(const demo_ai_audio_lease_t *lease)
{
return lease && lease->owner != DEMO_AI_AUDIO_OWNER_NONE &&
lease->owner == s_audio_owner && lease->generation != 0 &&
lease->generation == s_audio_generation;
}
五个条件串成一条与链,任一不成立就无效。DEMO_AI_AUDIO_LEASE_INIT 展开成全零,owner 是 NONE、generation 是 0,各挡一次。所以用这个宏初始化过的租约天然无效,不用额外写一个"是否初始化过"的标志位------注意这句话只对用这个宏初始化过的对象成立,一个没初始化过的结构体里是什么值,谁也说不准。
这五个条件得在一个打不断的窗口里读完,否则就有读到一半被打断的风险。调用点都套在 liot_rtos_enter_critical() 与 liot_rtos_exit_critical() 之间:纯粹的校验(demo_ai_audio_lease_is_valid())在临界区里只读字段,enter() 这类还会在同一个临界区里把掩码位改掉,SDK 的 I/O 一律放在临界区外面,也不在临界区里等任何东西。
四、发放靠一次自增,归还可以被拒
c
int demo_ai_audio_acquire(demo_ai_audio_owner_e owner, demo_ai_audio_lease_t *lease)
{
int result = 0;
if (!lease || owner <= DEMO_AI_AUDIO_OWNER_NONE || owner >= DEMO_AI_AUDIO_OWNER_COUNT)
return DEMO_AI_AUDIO_ERR_INVALID_LEASE;
liot_rtos_enter_critical();
if (s_audio_owner != DEMO_AI_AUDIO_OWNER_NONE) result = DEMO_AI_AUDIO_ERR_BUSY;
else {
if (++s_audio_generation == 0) ++s_audio_generation;
s_audio_owner = owner;
discard_capture_buffer();
s_discarded_capture_ms = 0;
lease->owner = owner;
lease->generation = s_audio_generation;
}
liot_rtos_exit_critical();
if (!result) TIRTC_LOG_DEBUG("[AI-AUDIO] acquired owner=%u generation=%lu\r\n",
(unsigned)owner, (unsigned long)lease->generation);
return result;
}
if (++s_audio_generation == 0) ++s_audio_generation; 这一行处理回绕。32 位计数器加到 0xFFFFFFFF 再加一变成 0,而 0 是保留的无效值,所以跳过一格发 1。回绕一次要多久:编号只在 acquire 成功时才推进,跳过 0 之后可用的非零编号是 2 的 32 次方减 1,即 4294967295 个。按每秒成功占用一次算,除以 86400 得 49710 天,再除以 365 是 136 年。余量够,代码里也就没有 generation 的回收或重置------但严格说它保证的是"136 年之内两张票编号不同",不是一个永不相交的编号。
发放成功时顺手做两件事。discard_capture_buffer() 把上一张票残留的采集缓存作废,s_discarded_capture_ms = 0 把丢弃时长记账归零,这样 discard_capture_timed() 返回的就是这一段会话自己的账。
归还这一步会失败,失败时所有权不交出去,这也是它和普通互斥锁差别最大的地方:
c
if (!lease_valid(lease)) result = DEMO_AI_AUDIO_ERR_INVALID_LEASE;
else if (s_active || (s_audio_ready && !s_stopped)) result = DEMO_AI_AUDIO_ERR_BUSY;
s_active 非零说明还有 record、play 或 control 在跑,s_audio_ready && !s_stopped 说明硬件还没停。这两种情况下 owner 都不释放,租约继续有效。这条约束把调用顺序钉死了,头文件里那段注释就是契约:Stop new inputs, wait for the media worker, stop(), then release().
顺序反了拿到的是 -31,跟 -30 是两件事。前者是"你还没收完,等会儿再来",后者是"这张票不是你的"。上层按返回码决定重试还是放弃。
#mermaid-svg-Wz9M6TYzXuVEgosP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wz9M6TYzXuVEgosP .error-icon{fill:#552222;}#mermaid-svg-Wz9M6TYzXuVEgosP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wz9M6TYzXuVEgosP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wz9M6TYzXuVEgosP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wz9M6TYzXuVEgosP .marker.cross{stroke:#333333;}#mermaid-svg-Wz9M6TYzXuVEgosP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wz9M6TYzXuVEgosP p{margin:0;}#mermaid-svg-Wz9M6TYzXuVEgosP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wz9M6TYzXuVEgosP .cluster-label text{fill:#333;}#mermaid-svg-Wz9M6TYzXuVEgosP .cluster-label span{color:#333;}#mermaid-svg-Wz9M6TYzXuVEgosP .cluster-label span p{background-color:transparent;}#mermaid-svg-Wz9M6TYzXuVEgosP .label text,#mermaid-svg-Wz9M6TYzXuVEgosP span{fill:#333;color:#333;}#mermaid-svg-Wz9M6TYzXuVEgosP .node rect,#mermaid-svg-Wz9M6TYzXuVEgosP .node circle,#mermaid-svg-Wz9M6TYzXuVEgosP .node ellipse,#mermaid-svg-Wz9M6TYzXuVEgosP .node polygon,#mermaid-svg-Wz9M6TYzXuVEgosP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wz9M6TYzXuVEgosP .rough-node .label text,#mermaid-svg-Wz9M6TYzXuVEgosP .node .label text,#mermaid-svg-Wz9M6TYzXuVEgosP .image-shape .label,#mermaid-svg-Wz9M6TYzXuVEgosP .icon-shape .label{text-anchor:middle;}#mermaid-svg-Wz9M6TYzXuVEgosP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Wz9M6TYzXuVEgosP .rough-node .label,#mermaid-svg-Wz9M6TYzXuVEgosP .node .label,#mermaid-svg-Wz9M6TYzXuVEgosP .image-shape .label,#mermaid-svg-Wz9M6TYzXuVEgosP .icon-shape .label{text-align:center;}#mermaid-svg-Wz9M6TYzXuVEgosP .node.clickable{cursor:pointer;}#mermaid-svg-Wz9M6TYzXuVEgosP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Wz9M6TYzXuVEgosP .arrowheadPath{fill:#333333;}#mermaid-svg-Wz9M6TYzXuVEgosP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wz9M6TYzXuVEgosP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wz9M6TYzXuVEgosP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wz9M6TYzXuVEgosP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Wz9M6TYzXuVEgosP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wz9M6TYzXuVEgosP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Wz9M6TYzXuVEgosP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wz9M6TYzXuVEgosP .cluster text{fill:#333;}#mermaid-svg-Wz9M6TYzXuVEgosP .cluster span{color:#333;}#mermaid-svg-Wz9M6TYzXuVEgosP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wz9M6TYzXuVEgosP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Wz9M6TYzXuVEgosP rect.text{fill:none;stroke-width:0;}#mermaid-svg-Wz9M6TYzXuVEgosP .icon-shape,#mermaid-svg-Wz9M6TYzXuVEgosP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wz9M6TYzXuVEgosP .icon-shape p,#mermaid-svg-Wz9M6TYzXuVEgosP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Wz9M6TYzXuVEgosP .icon-shape .label rect,#mermaid-svg-Wz9M6TYzXuVEgosP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wz9M6TYzXuVEgosP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Wz9M6TYzXuVEgosP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Wz9M6TYzXuVEgosP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} acquire 成功
generation 自增
init 与 prepare_session
stop 完成
s_stopped=1
release 成功
owner 归 NONE
未发放
owner=NONE
已发放
owner 置位
使用中
record 与 play 可并行
已停
租约仍握在手里
release 被拒 -31
s_active 非零或未 stop
五、三个位,只允许一种并行
租约管的是"谁有资格",同一 owner 内部的并发靠另一件东西:一个 8 位掩码。s_active 声明成 uint8_t,低 3 位有定义,剩下 5 位空着。
c
enum { OP_RECORD = 1, OP_PLAY = 2, OP_CONTROL = 4 };
static int enter(const demo_ai_audio_lease_t *lease, uint8_t op, bool needs_ready)
{
int result = 0;
liot_rtos_enter_critical();
if (!lease_valid(lease)) result = DEMO_AI_AUDIO_ERR_INVALID_LEASE;
else if ((op == OP_CONTROL && s_active) || (s_active & (op | OP_CONTROL)))
result = DEMO_AI_AUDIO_ERR_BUSY;
else if (needs_ready && (!s_audio_ready || s_stopped)) result = -1;
else s_active |= op;
liot_rtos_exit_critical();
return result;
}
关键在第二行判断。op | OP_CONTROL 把"同一类操作"和"控制操作"合成一个掩码,s_active 与它相与非零就返回 BUSY。三种请求逐个展开:
| 请求 | 掩码值 | 会被谁挡住 | 能与谁共存 |
|---|---|---|---|
OP_RECORD |
5,即 bit0 加 bit2 | 另一次 record、任何 control | play |
OP_PLAY |
6,即 bit1 加 bit2 | 另一次 play、任何 control | record |
OP_CONTROL |
4,即 control 位自身 | 任何已置位的操作 | 无 |
record 和 play 能同时挂在掩码上,这是 AI 对话"边播边听"在接口层的前提:下行还在放上一句回答时,上行已经可以在采,两边不用互相让路。
control 独占,因为它改的是两边共用的东西。init() 和 prepare_session() 共用同一条初始化路径:还没成功初始化过才去配 I2S 和 codec,已经有初始化状态时就按当前状态走------没停就只应用音量,停了就走 aec_start()(非 AEC 构建是 Pause 加 Resume)把会话拉起来,不是每次都推倒重配。另外 AEC 后端自己还有一把音量互斥锁(s_aec_levels_mutex)和后台的音量维护,所以不是所有写 codec 的场合都走公共的 OP_CONTROL。
音量这一层有个和主题直接相关的细节。第 0 档不止是把增益拧到最小:源码注释写的是 ADC attenuation alone is not a mute,这一档除写寄存器,还会把整帧 PCM 清零,s_mic_level 为 0 时那句 memset(pcm, 0, DEMO_AI_AUDIO_FRAME_BYTES) 省不掉。也就是说「静音」在这份代码里是靠显式清零实现的,不是靠把增益拧到底。再往上的 10 档各有曲线------普通 owner 的扬声器落在 30 到 60、麦克风数字音量落在 160 到 199,DEVICE 和群房间又走软件音量固定 10、codec 38 到 72 那套。具体分档跟租约没关系,提它只是因为改音量走的正是 control 那条独占路径。
#mermaid-svg-Hth7TN72bi5lFosZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Hth7TN72bi5lFosZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Hth7TN72bi5lFosZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Hth7TN72bi5lFosZ .error-icon{fill:#552222;}#mermaid-svg-Hth7TN72bi5lFosZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Hth7TN72bi5lFosZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Hth7TN72bi5lFosZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Hth7TN72bi5lFosZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Hth7TN72bi5lFosZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Hth7TN72bi5lFosZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Hth7TN72bi5lFosZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Hth7TN72bi5lFosZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Hth7TN72bi5lFosZ .marker.cross{stroke:#333333;}#mermaid-svg-Hth7TN72bi5lFosZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Hth7TN72bi5lFosZ p{margin:0;}#mermaid-svg-Hth7TN72bi5lFosZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Hth7TN72bi5lFosZ .cluster-label text{fill:#333;}#mermaid-svg-Hth7TN72bi5lFosZ .cluster-label span{color:#333;}#mermaid-svg-Hth7TN72bi5lFosZ .cluster-label span p{background-color:transparent;}#mermaid-svg-Hth7TN72bi5lFosZ .label text,#mermaid-svg-Hth7TN72bi5lFosZ span{fill:#333;color:#333;}#mermaid-svg-Hth7TN72bi5lFosZ .node rect,#mermaid-svg-Hth7TN72bi5lFosZ .node circle,#mermaid-svg-Hth7TN72bi5lFosZ .node ellipse,#mermaid-svg-Hth7TN72bi5lFosZ .node polygon,#mermaid-svg-Hth7TN72bi5lFosZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Hth7TN72bi5lFosZ .rough-node .label text,#mermaid-svg-Hth7TN72bi5lFosZ .node .label text,#mermaid-svg-Hth7TN72bi5lFosZ .image-shape .label,#mermaid-svg-Hth7TN72bi5lFosZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-Hth7TN72bi5lFosZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Hth7TN72bi5lFosZ .rough-node .label,#mermaid-svg-Hth7TN72bi5lFosZ .node .label,#mermaid-svg-Hth7TN72bi5lFosZ .image-shape .label,#mermaid-svg-Hth7TN72bi5lFosZ .icon-shape .label{text-align:center;}#mermaid-svg-Hth7TN72bi5lFosZ .node.clickable{cursor:pointer;}#mermaid-svg-Hth7TN72bi5lFosZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Hth7TN72bi5lFosZ .arrowheadPath{fill:#333333;}#mermaid-svg-Hth7TN72bi5lFosZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Hth7TN72bi5lFosZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Hth7TN72bi5lFosZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hth7TN72bi5lFosZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Hth7TN72bi5lFosZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hth7TN72bi5lFosZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Hth7TN72bi5lFosZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Hth7TN72bi5lFosZ .cluster text{fill:#333;}#mermaid-svg-Hth7TN72bi5lFosZ .cluster span{color:#333;}#mermaid-svg-Hth7TN72bi5lFosZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Hth7TN72bi5lFosZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Hth7TN72bi5lFosZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-Hth7TN72bi5lFosZ .icon-shape,#mermaid-svg-Hth7TN72bi5lFosZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hth7TN72bi5lFosZ .icon-shape p,#mermaid-svg-Hth7TN72bi5lFosZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Hth7TN72bi5lFosZ .icon-shape .label rect,#mermaid-svg-Hth7TN72bi5lFosZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hth7TN72bi5lFosZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Hth7TN72bi5lFosZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Hth7TN72bi5lFosZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 可共存
各用 I2S 一个方向
互斥
遮罩含 bit2
互斥
遮罩含 bit2
OP_RECORD
上行采集 bit0
OP_PLAY
下行播放 bit1
OP_CONTROL
音量与参数重配 bit2
一次 record_20ms() 进来要过三道门:先验票(enter() 里调用 lease_valid()),再看掩码看有没有别的 record 或 control 在跑,最后看 s_audio_ready 和 s_stopped。任何一道没过直接返回,一个字节都不读------传进来的 pcm 是空指针的话,连第一道都到不了,在函数开头就返回 -1。三道全过才进后端去取那 320 个样本、640 个字节,而后端自己还可能超时或报故障。
#mermaid-svg-h85HOsC454v6zWzW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h85HOsC454v6zWzW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h85HOsC454v6zWzW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h85HOsC454v6zWzW .error-icon{fill:#552222;}#mermaid-svg-h85HOsC454v6zWzW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h85HOsC454v6zWzW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h85HOsC454v6zWzW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h85HOsC454v6zWzW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h85HOsC454v6zWzW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h85HOsC454v6zWzW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h85HOsC454v6zWzW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h85HOsC454v6zWzW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h85HOsC454v6zWzW .marker.cross{stroke:#333333;}#mermaid-svg-h85HOsC454v6zWzW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h85HOsC454v6zWzW p{margin:0;}#mermaid-svg-h85HOsC454v6zWzW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-h85HOsC454v6zWzW .cluster-label text{fill:#333;}#mermaid-svg-h85HOsC454v6zWzW .cluster-label span{color:#333;}#mermaid-svg-h85HOsC454v6zWzW .cluster-label span p{background-color:transparent;}#mermaid-svg-h85HOsC454v6zWzW .label text,#mermaid-svg-h85HOsC454v6zWzW span{fill:#333;color:#333;}#mermaid-svg-h85HOsC454v6zWzW .node rect,#mermaid-svg-h85HOsC454v6zWzW .node circle,#mermaid-svg-h85HOsC454v6zWzW .node ellipse,#mermaid-svg-h85HOsC454v6zWzW .node polygon,#mermaid-svg-h85HOsC454v6zWzW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-h85HOsC454v6zWzW .rough-node .label text,#mermaid-svg-h85HOsC454v6zWzW .node .label text,#mermaid-svg-h85HOsC454v6zWzW .image-shape .label,#mermaid-svg-h85HOsC454v6zWzW .icon-shape .label{text-anchor:middle;}#mermaid-svg-h85HOsC454v6zWzW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-h85HOsC454v6zWzW .rough-node .label,#mermaid-svg-h85HOsC454v6zWzW .node .label,#mermaid-svg-h85HOsC454v6zWzW .image-shape .label,#mermaid-svg-h85HOsC454v6zWzW .icon-shape .label{text-align:center;}#mermaid-svg-h85HOsC454v6zWzW .node.clickable{cursor:pointer;}#mermaid-svg-h85HOsC454v6zWzW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-h85HOsC454v6zWzW .arrowheadPath{fill:#333333;}#mermaid-svg-h85HOsC454v6zWzW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-h85HOsC454v6zWzW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-h85HOsC454v6zWzW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h85HOsC454v6zWzW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-h85HOsC454v6zWzW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h85HOsC454v6zWzW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-h85HOsC454v6zWzW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-h85HOsC454v6zWzW .cluster text{fill:#333;}#mermaid-svg-h85HOsC454v6zWzW .cluster span{color:#333;}#mermaid-svg-h85HOsC454v6zWzW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-h85HOsC454v6zWzW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-h85HOsC454v6zWzW rect.text{fill:none;stroke-width:0;}#mermaid-svg-h85HOsC454v6zWzW .icon-shape,#mermaid-svg-h85HOsC454v6zWzW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h85HOsC454v6zWzW .icon-shape p,#mermaid-svg-h85HOsC454v6zWzW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-h85HOsC454v6zWzW .icon-shape .label rect,#mermaid-svg-h85HOsC454v6zWzW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h85HOsC454v6zWzW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-h85HOsC454v6zWzW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-h85HOsC454v6zWzW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不成立
成立
非零
为零
不成立
成立
demo_ai_audio_record_20ms
门 1
lease_valid 五条件
-30 INVALID_LEASE
旧票或未初始化
门 2
s_active 与 1 或 4 相与
-31 BUSY
采集或控制已在跑
门 3
s_audio_ready 且未 stopped
-1 硬件没起来
s_active 置 bit0
读一帧 320 样本
leave 清 bit0
这套三道门加五条件,就是 pro 版固件里正在跑的结构。AI 对话和微信通话走同一份音频适配层,掩码和租约都只有这一处实现。
六、租约管不到的那一半
租约管的是"有没有资格",管不了指针的生命周期。s_audio_config 声明成文件级 static,上面那行注释给了理由:
c
/* The real SDK retains this pointer, rather than copying the configuration. */
static Liot_AudHwConfig_t s_audio_config;
Liot_AudioInit(&s_audio_config) 之后,SDK 内部留着的是这个地址而不是一份拷贝,所以配置对象必须在 SDK 还在用它的整个期间都有效。这里用文件级 static 就是为了满足这一点。换成栈上的局部变量,风险取决于那个变量的作用域什么时候结束------真出问题的话,症状会出现在别的任务里,读到的也不再是你写进去的值,排查起来比 ABA 更费劲。至于具体什么时候崩、会不会崩,光看这段代码定不了。
错误码各有各的归属,不能只看数字大小猜。-30 是票无效,-31 是冲突:既可能是硬件还被别的 owner 占着,也可能是同一个租约内部的操作撞车(enter() 的掩码判断返回的也是 -31),-32 是非 AEC 构建里静音前奏已部分提交。-30 也不能一律读成"票过期了":AEC 的 aec_record() 在等待途中遇到 epoch 变化或 s_aec_fault 置真时,同样返回 -30,那两样跟租约过不过期是两回事。AEC 那一侧,-40 是超时------audio_aec.inc 里 AEC_ERR_TIMEOUT 就直接等于 DEMO_AI_AUDIO_ERR_CAPTURE_TIMEOUT,静音锁等待、aec_stop 的等待、采集帧等待都返回它,-41 是故障,-42 才是这一帧 PCM 一个样本都没被收下。同一个 -40,在 record 和 stop 两条路径上含义不同,得连调用的 API 一起看。
PLAY_BUSY 的语义在头文件里写得很明确:AEC copies the entire block into its bounded ring or returns PLAY_BUSY without accepting any prefix. 要么整块收下,要么一块都不收。这就给了调用方一个干净的重试条件:整块一个样本都没进去,原样再提交一次即可。对比之下 -32 只说明静音前奏已经有部分提交成功进了队列,到底进了多少不知道,这种情况要走正常的安全收尾,不能重试半截的前奏。
七、代价
适配层一次只认一个 owner,占用期间别的功能 acquire 一律返回 -31,它不提供抢占,也没有等待队列------来电怎么排队、要不要抢占,是调用方的事。微信来电进来时 AI 对话还占着硬件,能不能切过去取决于上层怎么处理这个 -31。
真要算等待时间的下限,得看停止那条路径:AEC 后端在 stop() 里先 aec_mute();worker 处理停止请求时若 s_aec_play_active 为真(注意这个标志在 vendor 播放调用之前就置真,不等于已经成功播过),会再睡 AEC_DRAIN_GRACE_MS 那 200 ms,然后按先 Liot_AudioRecordStreamStop() 再 Liot_AudioPlayStreamStop() 的顺序拆流。这 200 ms 的注释自己写明是 best effort,不保证 DMA 里的残料已经排空;注释里也记了反序停止在板上出现过 panic,不过那是注释的转述,我这边没有对应的日志。只采不播的会话不走这段。
租约由调用方持有,没有集中注册表。owner 传错在编译期抓不到:acquire() 开头那句 owner <= DEMO_AI_AUDIO_OWNER_NONE || owner >= DEMO_AI_AUDIO_OWNER_COUNT 能把 NONE 和越界值挡回去,但那是运行时的判断,枚举类型本身不提供这种保护,更挡不住"这段代码该用 LIVE 却写了 DEVICE"。抓后面这类错得靠运行日志,[AI-AUDIO] acquired owner=%u generation=%lu 每发一张票打一行(走的是 TIRTC_LOG_DEBUG,看不看得到取决于日志开关)。
release 可以被拒,意味着调用方不能"调了就当还了"。有效租约的 release 若因为 s_active 非零或还没 stop 而返回 -31,owner 会一直保留,后面每次 acquire 都还是 -31------除非调用方自己安排重试,否则这条路径就停在这儿了。
generation 只增不回收,所以它不能当"用过几次"的业务计数,要统计会话次数得另开一个计数器。它能保证的边界也说清楚:在编号还没绕回来、旧操作手里还是原来那份租约值、并且走的是受检接口的前提下,它能把前一次授权留下的操作挡回去。它不负责取消任务、清空别人的队列,也不管调用方那份对象是复制了几份、什么时候失效。
代码里还有个刻意的空缺:没有公开的 deinit。头文件写明的原因是普通 SDK 录音可能永远等不到返回,并发 deinit 不安全。代价是初始化只做一次:首次调用才配硬件,之后反复 init 和 prepare_session 只是按状态应用音量或把会话拉起来(AEC 构建里 stop 之后还会拆流再重建采集)。s_audio_ready 置位后不再清,来回翻的只有 s_stopped。
八、搬到别的单例硬件上
下面几条是从这份 826 行的 audio_device.c 里抠出来的。能不能搬到你的板子上,先看三件事:硬件是不是只有一个实例、收尾是不是分批的、SDK 会不会保留你传进去的指针。
- 判据要包含"第几次",不只是"是谁"。任何靠值本身判断状态的代码,都挡不住值绕一圈回来。
- 票面尽量小,由调用方持有。这里两个成员各 4 字节,适配层不替它动态分配、也不建注册表,代价是名单散落在各个调用方手里。
- 保留值要挑 0。generation 从 1 开始发,全零初始化的句柄天然无效,省掉一整类"是否初始化"的判断。
- 归还允许失败。收尾没做完就不该交出所有权;租约层的码和数据面的码分开定义,调用方才有机会只靠返回码做决策------但别指望按数值区间一刀切,同一个码在不同 API 上可能不是一回事。
- 掩码里每一位代表一条可以并行的路径,控制类操作要跟这些路径互斥。采放并行是实时通话的底线,配置类操作破坏的是两边共享的基准。
- SDK 只保留指针不拷贝参数时,配置对象的生命周期必须覆盖 SDK 使用它的整个期间。放静态存储是最省事的一种做法,但不是唯一解------关键是别让它在 SDK 还在读的时候失效。
- 要不要公开 deinit,取决于 SDK 的取消、阻塞和资源生命周期契约。这份适配层选择不公开,理由是普通 SDK 录音可能永远等不到返回、并发 deinit 不安全;代价是 AEC 的某些故障路径还会把 fault 锁存住,光靠 stop 加重新 prepare 不一定能恢复。
自己跑一遍
这篇讲的是单例音频硬件的租约与互斥,验证点在真机的会话切换上,日志里能一行行数出来。
| 你想做什么 | 需要什么 | 说明 |
|---|---|---|
看租约日志 owner= / generation= |
NT26F6D0 模组开发板 | 仓库 firmware/ 下带 lite 和 pro 两个目录,里面是 .binpkg 烧录包和 SHA256SUMS,先烧这个,不用自己编译 |
改 lease_valid() 做对照实验 |
同上 + 编译环境 | 仓库 tools/toolchain/ 下有 gcc-arm-none-eabi-windows.7z 和 Linux 分卷包,tools/msys64/ 是 Windows 下的构建环境 |
| 看平台侧的会话切换(附加入口) | 一台电脑 + Python 3 | tirtc-server-example 仓库 thing-connect/device-sim/device-sim-py/ 下的 device_sim_main.py --mac AA:BB:CC:DD:EE:FF |
第三行是附加入口,不计入下面的验证步骤:模拟器里没有 ES8311,也没有这份适配层,租约和掩码它一个都碰不到,只能看平台侧的协议行为。真要看租约,得上跑着这份固件的板子。
这篇的验证点落在租约本身,三步各有能观察到的现象:
- 跑通一次完整交接 。按固件实际支持的流程做一次 AI→微信切换,再反过来做一次 AI→AI 重连(挂断后立刻重新唤起)。每段都确认两件事:本地扬声器出声、远端能采到本地说话;同时把旧会话的
stop()、release()和新会话的acquire()、prepare_session()的返回码一起打出来。第 1 步验证的是功能正常,不等于租约机制被验证------后者靠第 2、3 步。 - 数日志里的票 。这两行都走
TIRTC_LOG_DEBUG,看不看得到取决于你的日志开关。状态上必须是旧租约 release 成功、新租约才可能 acquire 成功,但两行日志都在退出临界区之后才输出,顺序未必等于状态提交的顺序,别拿日志先后当因果。要看的是返回码和编号:一次交接后 generation 应该加一,而它推进的是全局成功 acquire 的次数,中间夹了别的会话就不止加一。 - 让一张旧票真的迟到一次 。这是唯一能直接观察 generation 的步骤。做法是:在旧会话里把租约对象另存一份副本 (
release()会把传进去的那份清零,直接用它会失效),等旧会话 stop、release 和新会话 acquire、prepare 都成功之后,拿这份副本去调一次公共的stop(),把返回码打出来。原版应当返回-30------owner 一样、generation 是 5 对 6。这一步不用特意等新会话停下来:enter()是先验票、再判掩码,票不对在第一道就返回 -30,掩码冲突与否根本轮不到。
然后把 lease_valid() 里 lease->generation == s_audio_generation 这个条件删掉只留 owner 比较,重新编译烧录,再跑同一遍。这次票能过第一道门了,但还要看第二道:新会话若有 record、play 或 control 正在跑,stop() 会撞在 OP_CONTROL 的互斥上返回 -31。所以对照版要挑一个新会话空闲的时机------比如在 prepare 之后、第一帧音频之前------它才能真的进到停止路径,去动新会话的状态。
第 3 步做完记得改回来。只比 owner 的对照版本在单次会话里跑不出差异,因为它要检验的本来就是跨授权批次的那一档保护。
这条路的局限 在硬件和网络上。模拟器里没有 ES8311,也没有 I2S DMA,租约和掩码都是固件里的东西,只在实机上跑得到。完整验证要打进一通真实呼叫,手边得有第二台能发起呼叫的设备,以及一个可用的服务端,自建的步骤在 tirtc-server-example 仓库的 thing-connect/deployment.md。另外本文的实验只覆盖租约交接这一段,弱信号下的断续要另结合网络和音频状态记录去定位,不能顺手归到链路层了事。
系列文章
- 上一篇:点了静音对端还在听:ESP32-S3 音频管道里那 630 ms,靠代次号无锁作废,讲 Wi-Fi 方案里三层代次如何让在途音频立刻失效
- 下一篇:Cat.1 上语音越说越卡?把发送缓冲卡在 12 KiB 主动丢帧,讲同一套 SDK 在 Wi-Fi 与 Cat.1 上为什么是两套上行预算
- 更早的两篇:用 319/320/321 个样本驯服时钟漂移 讲下行抖动缓冲与速率控制,AEC 回声消不掉时,把参考信号直接接回 ADC 讲用硬件参考换掉时延搜索
代码仓库 github.com/tangeai/xiaotai-lierda,路径 examples/NT26FxDx_OpenKit/tirtc_pro/media/audio_device.c 与 audio_device.h。国内访问慢可以用 Gitee 镜像 gitee.com/tange-ai/xiaotai-lierda。