杰理 SDK 文档中心
首页
首页
  • 概述

    • SDK 概览与产品定位
    • 支持芯片平台与蓝牙认证
    • SDK 架构与目录分层
  • 快速开始

    • 环境搭建与编译工具链
    • 编译构建系统
    • 板级工程与配置
    • 烧录与固件升级工具
  • 应用工程

    • 应用选择与工程总览
    • SPP + BLE 数传应用框架
    • 透传与 AT 指令示例
    • BLE 广播/中心与定位示例
    • 2.4G 私有协议与 Dongle 示例
    • 云平台接入示例
    • HID 人机交互应用框架
    • HID 示例工程(键盘/鼠标/遥控器/手柄)
    • Bluetooth Mesh 应用框架
    • Mesh 模型与 Mesh DFU 固件升级
    • Mesh 音频编解码演示
  • 芯片平台与硬件抽象

    • 芯片平台总览与差异
    • 音频编解码与时钟管理
    • 外设驱动接口(ADC/IIC/SPI/PWM/LED/充电)
    • 芯片配置工具与下载支持
  • 蓝牙协议栈

    • 蓝牙控制器层(btctrler)
    • 蓝牙协议栈与 Profile(btstack)
    • 蓝牙模块选择与配置
  • 媒体与音频框架

    • 音频流框架
    • 音频编解码与 A2DP 媒体
    • 音频效果处理(EQ/频谱/变调/环绕/超低音)
    • 本地 TWS 与音频同步
  • 系统服务与运行时

    • 实时操作系统与任务调度
    • 消息事件机制
    • 电源管理与低功耗
    • 存储与配置系统
    • 设备驱动框架(USB/RTC)
  • 应用公共组件

    • 音频应用组件
    • 设备外设抽象(按键/触摸/传感器/存储)
    • 蓝牙公共模块与消息联动
    • 调试与配置组件
    • 杰理关键词唤醒(jl_kws)
  • 第三方协议与云平台接入

    • 杰理 RCSP 私有协议
    • 低功耗蓝牙 Mesh 方案(llsync_mesh)
    • Sig Mesh 方案
    • 涂鸦协议接入
    • 腾讯连连接入
    • 华为 HiLink 接入
  • 固件升级与维护

    • OTA 升级机制
    • 升级补丁与版本维护
    • 升级工具链(BLE OTA / USB Dongle OTA)
  • 文档与开发资源

    • 数据手册与架构文档
    • 协议与云平台开发文档
    • 常见问题与技术支持

音频流框架

音频流框架(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 结构沿节点链逐级向后传递。

框架的核心设计目标:

  1. 解耦:每个节点只关心自己的 data_handler,不关心数据来源与去向;节点通过 input/output 指针构成链表。
  2. 可组合:通过 audio_stream_add_list 一条语句即可将任意节点序列组成流水线。
  3. 同步与异步共存:节点通过 pass_by 标志声明自己是"同步透传"(复用上层 buf)还是"异步处理"(数据拷贝到自有 buf 再后传),框架据此决定数据传递方式。
  4. 支持一传多(分支):同一个输出节点可挂接多个后级节点,框架自动为每个分支复制数据,实现一路解码、多路输出的场景(如同时送 DAC 与录音)。
  5. 唤醒机制(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: 唤醒解码器继续产出下一帧

数据传递时序

解码启动后,数据流按以下顺序工作:

  1. 构建阶段:应用创建节点数组 entries[](首节点为解码器节点,末节点为 mixer 通道节点),调用 audio_stream_open 创建流,再调用 audio_stream_add_list 将节点按序串联——内部等价于逐个执行 audio_stream_add_tail,将新节点接到链尾并回填 input/output 指针。
  2. 产出阶段:audio_decoder_start 启动后,解码器在 data_handler 中把 PCM 数据封装为 audio_data_frame,从首节点开始沿链传递。
  3. 处理阶段:每个节点先执行 prob_handler 判断是否跳过,再执行 data_handler;若节点是异步模式(如 mixer 通道),数据被拷入节点自有缓冲,节点通过 data_process_len 向上游回报实际消费长度。
  4. 流控阶段:链尾节点(mixer 通道)消费完毕后触发 resume 回调(demo_frame_out_stream_resume → audio_decoder_resume),解码器据此继续解码,形成"推-拉结合"的背压流控。
  5. 停止阶段:数据帧中的 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_ACTIVE1检查数据流是否活动(见 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 的回调协议之上,新增一个处理模块只需三步:

  1. 实现节点回调:在自定义模块中定义 prob_handler、data_handler、data_clear(必要时含 ioctrl),模块结构体内嵌一个 struct audio_stream_entry(如 decoder.entry、mix_ch.entry 的用法)。
  2. 接入数据流:将模块的 entry 指针放入 entries[] 数组,调用 audio_stream_add_list 插入到目标位置;如需挂在已有节点之后且该节点已有后级,使用 audio_stream_add_entry 触发自动分支。
  3. 实现流控衔接:若模块是异步消费方,为它设置 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/动态音量相关页面。
Next
音频编解码与 A2DP 媒体