音频流框架
音频流框架(Audio Stream)是 AC63 系列蓝牙 SoC SDK 中用于串联音频处理节点的轻量级数据管道机制,通过"节点链"将解码、混音、音效、EQ、DAC 输出等模块按顺序连接起来,实现音频数据的流水线式传递。
Purpose and Scope
本文档介绍 SDK 中音频流框架的完整实现机制,包括:
- 核心数据结构(
audio_stream、audio_stream_entry、audio_data_frame、audio_stream_group、audio_frame_copy) - 节点链的构建与数据传递流程
- 数据流群组(Group)与分支(Copy)机制的设计意图
- 全部公开 API 的说明
- 基于
apps/mesh/demo_dec_frame_play.c的真实使用示例
本页聚焦"音频流"管道机制本身。解码器、混音器(mixer)、EQ、DAC 等具体节点模块属于各自独立的能力,本文仅说明它们如何作为流节点接入本框架,不展开各模块内部实现。音频解码与编码相关内容请参见对应页面的解码器/编码器文档。
Overview
在嵌入式音频系统中,一段音频数据通常需要依次经过多个处理阶段:解码器输出 PCM → 混音(mixer)→ 音效处理(EQ、动态音量等)→ DAC 输出。传统做法是每个模块之间通过显式函数调用耦合,导致代码难以复用和组合。音频流框架将每个处理模块抽象为数据流节点(entry),多个节点依次串联形成一条数据流(stream),数据以统一的 audio_data_frame 结构沿节点链逐级向后传递。
框架的核心设计目标:
- 解耦:每个节点只关心自己的
data_handler,不关心数据来源与去向;节点通过input/output指针构成链表。 - 可组合:通过
audio_stream_add_list一条语句即可将任意节点序列组成流水线。 - 同步与异步共存:节点通过
pass_by标志声明自己是"同步透传"(复用上层 buf)还是"异步处理"(数据拷贝到自有 buf 再后传),框架据此决定数据传递方式。 - 支持一传多(分支):同一个输出节点可挂接多个后级节点,框架自动为每个分支复制数据,实现一路解码、多路输出的场景(如同时送 DAC 与录音)。
- 唤醒机制(resume):异步节点(如 mixer 通道)在数据消费完成后,通过
resume回调通知上游模块继续产生数据,形成背压式流控。
Architecture
flowchart TD
subgraph sg_Source["数据源(上游)"]
DEC["audio_decoder<br/>解码器节点"]
ENC["音频采集/编码节点"]
end
subgraph sg_Stream["音频流 audio_stream"]
E1["entry[0]<br/>decoder.entry"]
E2["entry[1]<br/>自定义处理节点"]
E3["entry[2]<br/>mix_ch.entry"]
E1 -->|"audio_data_frame<br/>data_handler 传递"| E2
E2 -->|"audio_data_frame"| E3
end
subgraph sg_Sink["数据汇(下游)"]
MIX["audio_mixer<br/>混音器"]
DAC["DAC 输出"]
end
subgraph sg_Control["唤醒/控制"]
RESUME["resume 回调<br/>(audio_decoder_resume)"]
IOCTL["节点 IOCTRL<br/>CHECK_ACTIVE"]
end
DEC --> E1
E3 --> MIX
MIX --> DAC
E3 -.->|"数据消费完毕"| RESUME
RESUME -.->|"通知上游继续产出"| DEC
IOCTL -.->|"轮询节点活动状态"| E1
如上图所示,一条音频流由多个 audio_stream_entry 节点按顺序串联而成。每个节点持有 data_handler 回调,负责从 input 节点接收 audio_data_frame,处理后通过 output 指针传给下一个节点。数据流本身通过 resume 回调与上游模块交互,形成"消费多少、生产多少"的流控闭环。
关键概念
| 概念 | 类型 | 说明 |
|---|---|---|
| 数据流节点 | struct audio_stream_entry | 流水线中的一个处理单元(解码、混音、EQ……) |
| 数据流 | struct audio_stream | 节点的容器,持有首节点指针与 resume 回调 |
| 数据帧 | struct audio_data_frame | 节点间传递的数据载体(通道、采样率、数据指针等) |
| 数据流群组 | struct audio_stream_group | 串联多条数据流的尾节点,实现一次 resume 驱动多条流 |
| 数据流分支 | struct audio_frame_copy | 支持一传多,自动为每个分支拷贝数据 |
核心数据结构
数据帧 audio_data_frame
节点之间传递的数据统一封装在 struct audio_data_frame 中,定义于 audio_stream.h:
struct audio_data_frame {
u8 channel; // 通道数
u16 stop : 1; // 数据流停止标志
u16 data_sync : 1; // 数据流同步标志
u16 no_subsequent : 1; // 1-不再执行后续的数据流;0-正常执行
u32 sample_rate; // 采样率
u16 offset; // 数据偏移
u16 data_len; // 数据长度
s16 *data; // 数据地址
};
Source: audio_stream.h
其中位域标志控制数据传递的边界行为:stop 表示整条流停止;no_subsequent 允许某个节点在满足条件时提前截断后续节点的处理(例如混音通道无数据时避免无效搬运);data_sync 用于数据同步场景。offset 与 data_len 配合支持"部分数据"传递——节点可只消费数据段的一部分,剩余部分留待下次处理。
数据流 audio_stream
struct audio_stream {
struct audio_stream_entry *entry; // 数据流节点
void *priv; // resume私有参数
void (*resume)(void *priv); // 激活回调接口
};
Source: audio_stream.h
audio_stream 是数据流的"句柄":entry 指向链表的第一个节点,resume 是模块唤醒数据流时的回调,priv 是该回调的私有参数。调用 audio_stream_open(priv, resume) 创建流时传入的 resume 通常指向解码器等上游模块的恢复函数(如 audio_decoder_resume),当链尾节点消费完数据后回调它,驱动上游继续产出数据。
数据流节点 audio_stream_entry
struct audio_stream_entry {
u8 pass_by; // 1-同步处理(即往后传的buf就是上层传入的buf);
// 0-异步处理(数据存到其他buf再往后传)
u8 remain; // 1-上次数据没输出完。0-上次数据输出完
u16 offset; // 同步处理时的数据偏移
struct audio_stream *stream; // 所属的数据流
struct audio_stream_entry *input; // 上一个节点
struct audio_stream_entry *output; // 下一个节点
struct audio_stream_entry *sibling; // 数据流群组中的数据流节点链表
struct audio_stream_group *group; // 数据流群组节点
struct audio_frame_copy *frame_copy; // 数据分支节点
int (*prob_handler)(struct audio_stream_entry *, struct audio_data_frame *in); // 预处理
int (*data_handler)(struct audio_stream_entry *, struct audio_data_frame *in,
struct audio_data_frame *out); // 数据处理
void (*data_process_len)(struct audio_stream_entry *, int len); // 后级返回使用的数据长度
void (*data_clear)(struct audio_stream_entry *); // 清除节点数据
int (*ioctrl)(struct audio_stream_entry *, int cmd, int *param); // 节点IOCTRL
};
Source: audio_stream.h
节点是框架的核心抽象,通过五个回调构成完整的处理协议:
prob_handler(预处理):在数据正式处理前被调用,通常用于判断节点是否需要处理本次数据(例如 mixer 通道被关闭时直接返回跳过)。data_handler(数据处理):核心处理函数,接收in帧,填充out帧(或复用in)。返回值约定"负数表示出错"。data_process_len(后级消费长度通知):当后级节点只消费了部分数据时,通过该回调把消费长度回传给前级,前级据此更新自己的offset/remain状态。data_clear(数据清除):节点内部缓冲清零,用于停止/复位场景。ioctrl(节点控制):向节点下发控制命令,框架目前定义AUDIO_STREAM_IOCTRL_CMD_CHECK_ACTIVE(1)用于检查数据流是否活动。
pass_by 标志决定了数据传递方式:同步节点(pass_by == 1)直接把上层传入的 buf 原样后传,零拷贝;异步节点(pass_by == 0)把数据拷入自身 buf 处理后再传,此时框架使用 offset 字段记录同步透传时的数据偏移。
数据流群组 audio_stream_group 与分支 audio_frame_copy
struct audio_stream_group {
struct audio_stream_entry *entry; // 数据流节点
};
struct audio_frame_copy {
struct list_head head; // 链表。用于连接各个分支
struct audio_data_frame frame; // 保存上层的传输内容
struct audio_stream_entry entry; // 连接上层的节点
};
Sources: audio_stream.h · audio_stream.h
群组用于串联多条数据流:把数据流 A 的尾节点和数据流 B 的尾节点都加入同一个 group,再把 group 挂到另一条数据流 X 的首节点上;当 X 被 resume 时,会依次 resume A 和 B。典型场景是:主播放流与音效处理流并行,均需在解码器被唤醒时同步推进。
分支用于一传多:当一个节点的 output 已连接某节点、又要挂接新节点时,框架自动生成 audio_frame_copy,为每个分支申请独立空间并拷贝数据,各自向后传递,互不影响。
核心流程
数据流构建流程
sequenceDiagram
participant APP as 应用层(demo_dec_frame_play)
participant OPEN as audio_stream_open
participant ADD as audio_stream_add_list
participant DEC as decoder.entry(节点0)
participant MIX as mix_ch.entry(节点N)
participant RESUME as resume回调
APP->>OPEN: audio_stream_open(dec, demo_frame_out_stream_resume)
OPEN-->>APP: 返回 audio_stream* 句柄
APP->>ADD: audio_stream_add_list(stream, entries[], entry_cnt)
ADD->>ADD: 依次 audio_stream_add_tail 链接各节点<br/>(input/output 指针串联)
ADD-->>APP: 节点链构建完成
Note over DEC,MIX: 解码开始后数据逐级传递
DEC->>DEC: data_handler 产出 PCM 帧
DEC->>MIX: 传递 audio_data_frame<br/>(data指针/len/采样率/通道)
MIX->>MIX: 混音处理后写入输出缓冲
MIX->>RESUME: 数据消费完毕,调用 resume(priv)
RESUME->>DEC: 唤醒解码器继续产出下一帧
数据传递时序
解码启动后,数据流按以下顺序工作:
- 构建阶段:应用创建节点数组
entries[](首节点为解码器节点,末节点为 mixer 通道节点),调用audio_stream_open创建流,再调用audio_stream_add_list将节点按序串联——内部等价于逐个执行audio_stream_add_tail,将新节点接到链尾并回填input/output指针。 - 产出阶段:
audio_decoder_start启动后,解码器在data_handler中把 PCM 数据封装为audio_data_frame,从首节点开始沿链传递。 - 处理阶段:每个节点先执行
prob_handler判断是否跳过,再执行data_handler;若节点是异步模式(如 mixer 通道),数据被拷入节点自有缓冲,节点通过data_process_len向上游回报实际消费长度。 - 流控阶段:链尾节点(mixer 通道)消费完毕后触发
resume回调(demo_frame_out_stream_resume→audio_decoder_resume),解码器据此继续解码,形成"推-拉结合"的背压流控。 - 停止阶段:数据帧中的
stop标志置位时整条流终止;data_clear用于复位各节点内部缓冲。
分支与群组的数据流
flowchart LR
subgraph sg_Main["主数据流 streamX"]
X0["entryX_0<br/>(group = &test_group)"]
X1["entryX_1"]
end
subgraph sg_StreamA["数据流 stream0"]
A0["entry0_0"] --> A1["entry0_n<br/>(group 成员)"]
end
subgraph sg_StreamB["数据流 stream1"]
B0["entry1_0"] --> B1["entry1_n<br/>(group 成员)"]
end
X0 -->|"resume 依次触发"| A1
X0 -->|"resume 依次触发"| B1
A1 --> A0
B1 --> B0
当 entryX_0.group = &test_group 且 entry0_n、entry1_n 已通过 audio_stream_group_add_entry 加入群组后,调用 streamX 的 resume 会依次驱动 stream0 与 stream1 的节点链推进(见 audio_stream.h 的注释说明)。
Usage Examples
示例一:创建数据流并串联解码器与混音器节点
以下代码摘自 mesh 演示工程的解码播放流程,展示了音频流框架的标准用法:先创建流,再用 audio_stream_add_list 一次性挂接所有节点。
// 数据流串联
struct audio_stream_entry *entries[8] = {NULL};
u8 entry_cnt = 0;
entries[entry_cnt++] = &dec->decoder.entry;
// 添加自定义数据流节点等
// 最后输出到mix数据流节点
entries[entry_cnt++] = &dec->mix_ch.entry;
// 创建数据流,把所有节点连接起来
dec->stream = audio_stream_open(dec, demo_frame_out_stream_resume);
audio_stream_add_list(dec->stream, entries, entry_cnt);
Source: demo_dec_frame_play.c
设计要点:
- 节点数组
entries[]中节点顺序即处理顺序:decoder.entry在前(解码输出),mix_ch.entry在后(混音汇入)。 audio_stream_open的第二个参数demo_frame_out_stream_resume是流的唤醒回调;在本例中 mixer 通道数据被消费后需要回调解码器继续喂数据。- 数组长度为 8,
entry_cnt为实际使用数;中间位置预留了"自定义数据流节点"的插入空间,便于扩展音效等处理。
示例二:解码器与 mixer 通道的联动配置
在使用 mixer 通道作为流末节点时,需要显式设置通道的 resume handler,使混音器在需要数据时反向唤醒解码器——这正是音频流 resume 机制与混音模块的衔接点:
audio_mixer_ch_open(&dec->mix_ch, &mixer);
audio_mixer_ch_set_resume_handler(&dec->mix_ch, (void *)&dec->decoder, (void (*)(void *))audio_decoder_resume);
audio_mixer_ch_set_sample_rate(&dec->mix_ch, fmt->sample_rate);
Source: demo_dec_frame_play.c
这里 audio_decoder_resume 即解码器的恢复函数,与 audio_stream_open 传入的流 resume 回调形成呼应:解码器既是数据流的"头"(产出数据),也是流控的"尾"(被唤醒继续产出)。
API Reference
以下 API 全部声明于 audio_stream.h。
struct audio_stream *audio_stream_open(void *priv, void (*resume)(void *priv))
创建一条新的数据流。
参数:
priv(void*):resume 回调的私有参数resume(void (*)(void*)):模块唤醒数据流时的回调函数
返回: 数据流句柄 struct audio_stream *。第一个节点通常为解码输出。
void audio_stream_add_first(struct audio_stream *stream, struct audio_stream_entry *entry)
添加数据流的第一个节点。第一个节点一般为解码输出。
参数:
stream:数据流句柄entry:数据流节点句柄
void audio_stream_add_head(struct audio_stream *stream, struct audio_stream_entry *entry)
将节点插入到数据流的 first 节点后面。
参数:
stream:数据流句柄entry:待插入的节点句柄
void audio_stream_add_tail(struct audio_stream *stream, struct audio_stream_entry *entry)
将节点放到数据流的最后。
参数:
stream:数据流句柄entry:待追加的节点句柄
void audio_stream_add_entry(struct audio_stream_entry *input, struct audio_stream_entry *output)
将 output 节点添加到 input 节点之后。若 input 后面已连接节点,将创建数据流分支处理(一传多)。
参数:
input:数据流节点句柄output:需要添加的数据流节点句柄
void audio_stream_add_list(struct audio_stream *stream, struct audio_stream_entry *entry[], int num)
将 entry 数组中节点按顺序添加到数据流中(内部依次追加到链尾)。
参数:
stream:数据流句柄entry[]:数据流节点数组num:节点总个数
void audio_stream_del_entry(struct audio_stream_entry *entry)
将节点从数据流中删除。
void audio_stream_del_list(struct audio_stream_entry *entry[], int num)
依次将数组中的节点删除。
群组 API
void audio_stream_group_add_entry(struct audio_stream_group *group, struct audio_stream_entry *entry):将节点加入群组。void audio_stream_group_del_entry(struct audio_stream_group *group, struct audio_stream_entry *entry):将节点从群组删除。int audio_stream_group_entry_num(struct audio_stream_group *group):获取群组中的节点个数。
数据流运行控制
audio_stream_run(entry, input):数据处理接口,数据流依次递归向后传输,直到最后一个节点或no_subsequent == 1;返回负数表示出错。audio_stream_resume(entry):恢复数据流,驱动上游继续产出数据。audio_stream_clear(entry):清除数据流中各节点的数据缓冲。audio_stream_ioctrl(entry, cmd, param):向数据流下发控制命令,框架预定义命令AUDIO_STREAM_IOCTRL_CMD_CHECK_ACTIVE (1)用于检查数据流是否活动。
IOCTRL 命令
| 命令 | 值 | 说明 |
|---|---|---|
AUDIO_STREAM_IOCTRL_CMD_CHECK_ACTIVE | 1 | 检查数据流是否活动(见 audio_stream.h) |
失败模式、边界情况与并发
no_subsequent截断:任一节点可在帧中置位no_subsequent终止后续节点处理。设计意图是让上游"无数据/无需输出"的节点(如静音通道)快速短路整条链,避免无效的数据搬运与拷贝开销。若节点未正确复位该标志,可能导致后续节点长时间收不到数据——排查时优先检查该位域。remain与部分消费:remain == 1表示上次数据未输出完,data_process_len负责把后级实际消费长度回传前级。若前级节点未实现/未正确调用data_process_len,会造成数据丢失或重复处理。- 分支(frame_copy)内存开销:每增加一个分支,框架都会为分支申请独立数据空间并拷贝数据。多路输出场景(如同时送 DAC、录音、TWS)会显著增加内存与带宽占用,需按实际分支数评估 RAM 预算。
- 群组循环引用风险:群组通过
sibling链表连接各流的尾节点。若误将同一条流的节点重复加入群组或形成环,resume 可能递归死循环;audio_stream_group_entry_num可用于校验节点数量。 - 异步节点与并发:mixer 通道等异步节点在中断/任务上下文消费数据,resume 回调会跨上下文触发上游。设计上依赖各节点自身的同步机制(如 mixer 的 no_wait 超时丢数策略),框架本身不提供锁;在自定义节点中访问共享缓冲时应沿用现有模块的中断保护约定。
- IOCTRL 活动检查:
AUDIO_STREAM_IOCTRL_CMD_CHECK_ACTIVE用于轮询节点是否仍处于活动状态,超时/异常场景下可据此判断数据流是否卡死,配合audio_stream_clear复位。
性能与运维考虑
- 零拷贝路径:
pass_by == 1的同步节点直接复用上层 buf,不产生拷贝;这是解码→混音直连场景下默认追求的高效路径。仅当节点必须改写数据或需要缓冲去抖时才应使用异步模式(pass_by == 0)。 - 背压流控:框架通过 resume 回调实现"消费驱动生产",天然具备背压能力——链尾消费多快,上游就生产多快,避免无界缓冲堆积。设计自定义节点时,应确保在
data_handler中及时调用后级的 resume 语义(或依赖链尾的 resume),否则会出现数据停顿。 - 内存预算:每个异步节点、每个分支(
audio_frame_copy)都会独立占用缓冲空间。在 RAM 受限的 AC63 平台上,新增处理节点前应核算其内部缓冲与分支拷贝开销,必要时用no_subsequent短路空闲路径。 - 时钟与功耗:解码播放过程中上层会主动提升系统时钟(如示例中的
clk_set("sys", 96 * 1000000L)),音频流的启停节奏直接影响系统功耗;流停止时应通过data_clear清理节点缓冲,避免残留数据导致下次启动异常。
扩展点
音频流框架的扩展能力完全建立在 audio_stream_entry 的回调协议之上,新增一个处理模块只需三步:
- 实现节点回调:在自定义模块中定义
prob_handler、data_handler、data_clear(必要时含ioctrl),模块结构体内嵌一个struct audio_stream_entry(如decoder.entry、mix_ch.entry的用法)。 - 接入数据流:将模块的 entry 指针放入
entries[]数组,调用audio_stream_add_list插入到目标位置;如需挂在已有节点之后且该节点已有后级,使用audio_stream_add_entry触发自动分支。 - 实现流控衔接:若模块是异步消费方,为它设置 resume 回调(参考
audio_mixer_ch_set_resume_handler(&dec->mix_ch, &dec->decoder, audio_decoder_resume)的配对方式);若模块是数据源头,将其作为流的 first 节点并在audio_stream_open时传入其恢复函数。
典型扩展场景:插入自定义音效节点(EQ/动态范围压缩)、插入录音分支(利用 frame_copy 一传多)、串联多条播放流(利用 audio_stream_group 群组同步 resume)。
测试与验证
SDK 中音频流框架的验证主要依赖两处:
- demo 工程:
apps/mesh/demo_dec_frame_play.c是框架的完整参考实现,覆盖"创建流 → add_list 串联 → 解码器/mixer 联动 → resume 流控"全流程,可作为新节点接入的模板。 - 音频测试用例:
apps/common/audio/demo/audio_decoder_test.c与audio_encoder_test.c提供解码/编码侧的独立验证路径,可用于确认流框架在无 mixer 场景下的最小链路行为。
由于框架本身为库代码(API 声明于 include_lib/media/audio_stream.h,实现在媒体库内部),节点级行为通过各模块自测与系统级播放测试覆盖;新增节点时应重点验证:数据不丢不重、采样率/通道信息沿链正确传递、停止与清除后无残留。
Related Links
- 音频流头文件(完整 API 声明)
- mesh 解码播放 demo(数据流串联参考实现)
- 音频基础定义(audio_base.h,流框架依赖)
- 音频解码器/编码器能力请参见对应解码器、编码器页面;EQ 与音量处理参见音频 EQ/动态音量相关页面。