Mesh 音频编解码演示
Mesh 音频编解码演示(Audio Codec Demo)是 AC63xx 系列 SDK 中用于验证 SIG Mesh 节点音频采集、编码与输出的示例工程,其核心实现位于 apps/mesh/audio_codec_demo.c,演示了从麦克风(或正弦波模拟数据源)采集 PCM 数据、经音频编码任务编码为 OPUS / SPEEX / ADPCM / LC3 / SBC / MSBC 等格式、并通过输出回调把编码帧交给上层(可进一步经 Mesh 网络发送)的完整链路。
Purpose and Scope
本页面说明 Mesh 音频编解码演示的编码端实现机制:
- 演示入口与宏开关(
ENC_DEMO_EN、MESH_AUDIO_TEST、MIC_USE_MIC_CHANNEL) - 数据结构
demo_enc_hdl与音频编码任务(audio_encoder/audio_encoder_task)的装配方式 - PCM 数据源:ADC 麦克风采集与 44.1kHz 正弦波表模拟(timer 灌数)
- 六种编码格式(OPUS / SPEEX / ADPCM / LC3 / SBC / MSBC)的
audio_fmt参数配置 - 编码帧输出回调(
demo_output)——这是把编码数据接入 Mesh 网络发送的扩展点
以下主题属于相邻页面,不在本页展开:Mesh 协议栈本身(SIG Mesh 模型、配网、消息收发)参见 Mesh 协议相关页面;generic_onoff_server.c 中通用的 OnOff 模型按键测试参见通用模型示例页面;Mesh DFU 升级参见 Mesh DFU 页面。本页仅聚焦"音频采集 → 编码 → 输出"这条编解码演示链路,且需说明:当前源文件中实现的是编码端演示,解码端演示未见独立源文件,相关解码能力由系统 audio_decoder 框架提供。
Overview
设计目标
在资源受限的嵌入式蓝牙 SoC(AC63xx 系列)上,Mesh 节点除灯控、传感等低速数据应用外,还可能承载语音/音频类应用(如对讲、广播)。受限于 BLE 带宽,原始 PCM 数据无法直接经 Mesh 传输,必须先在节点本地完成音频编码压缩,再通过 Mesh 消息将编码帧分发给其他节点。本演示正是为了验证这一"采集-编码-输出"链路在 SDK 上的可行性,并提供一个可复用的编码器封装模板。
关键设计决策
- 双数据源设计:默认使用 ADC 麦克风(
MIC_USE_MIC_CHANNEL = 1)实时采集;同时预留了sys_hi_timer定时器 + 正弦波表的模拟数据源(demo_frame_test_time_func,默认被注释),便于在没有 MIC 硬件环境下验证编码链路。 - 环形缓冲 + 信号量同步:采集端(中断回调或定时器)把 PCM 写入
pcm_in_cbuf环形缓冲,编码任务侧demo_enc_pcm_get在缓冲数据不足时通过pcm_frame_sem信号量等待,实现生产-消费解耦。 - 编码格式可插拔:通过
TCFG_ENC_OPUS_ENABLE、TCFG_ENC_SPEEX_ENABLE、TCFG_ENC_ADPCM_ENABLE、TCFG_ENC_LC3_ENABLE、TCFG_ENC_SBC_ENABLE、TCFG_ENC_MSBC_ENABLE编译开关选择可用编码器,运行时由code_type参数选择。 - 编码期间提频:编码是计算密集操作,
audio_demo_enc_open会把系统时钟提升到ENC_CLK(96MHz),关闭时恢复原时钟,平衡性能与功耗。 - 看门狗喂狗:编码输出回调中调用
wdt_clear(),防止长时间编码阻塞导致系统复位。
术语表
| 术语 | 含义 |
|---|---|
ENC_DEMO_EN | 本演示文件的总编译开关 |
MESH_AUDIO_TEST | 板级配置中的 Mesh 音频测试使能,控制按键触发测试与 MIC 增益 |
demo_enc_hdl | 演示编码器句柄,封装编码器、环形缓冲、信号量与 MIC 资源 |
audio_encoder | SDK 音频编码器抽象(audio_enc.h) |
audio_encoder_task | 独立编码任务,驱动编码器运行 |
pcm_in_cbuf | PCM 输入环形缓冲,ENC_IN_SIZE * 6 字节(6 帧) |
demo_output | 编码完成后的输出回调,由调用方注册,可接入 Mesh 发送 |
Architecture
flowchart TD
subgraph sg_Source["数据源层"]
MIC["ADC 麦克风<br/>audio_adc_mic_open"]
TIMER["sys_hi_timer 定时器<br/>demo_frame_test_time_func"]
SINE["sin44K 正弦波表<br/>read_44k_sine_data"]
end
subgraph sg_Buffer["PCM 缓冲层"]
CBUF["pcm_in_cbuf 环形缓冲<br/>ENC_IN_SIZE * 6 字节"]
SEM["pcm_frame_sem 信号量"]
end
subgraph sg_Encoder["编码层"]
ENC_TASK["encode_task<br/>audio_encoder_task"]
ENC["audio_encoder<br/>demo_enc_input / demo_enc_handler"]
FMT["audio_fmt 格式参数<br/>OPUS/SPEEX/ADPCM/LC3/SBC/MSBC"]
end
subgraph sg_Output["输出层"]
OUT_CB["demo_enc_output_handler<br/>wdt_clear + put_buf"]
DEMO_OUT["demo_output 回调<br/>调用方注册"]
MESH["Mesh 网络发送<br/>扩展点"]
end
subgraph sg_Clock["时钟管理"]
CLK["sys 时钟提升至 ENC_CLK 96MHz<br/>clk_set / clk_get"]
end
MIC -->|"ADC 中断回调 adc_mic_output_handler"| CBUF
TIMER -->|"cbuf_write 320B/次"| CBUF
SINE --> TIMER
CBUF -->|"demo_enc_pcm_get fget"| ENC
SEM -->|"os_sem_pend 等待数据"| ENC
ENC -->|"读取 fmt 参数"| FMT
ENC_TASK -->|"驱动"| ENC
ENC -->|"enc_output 回调"| OUT_CB
OUT_CB -->|"调用"| DEMO_OUT
DEMO_OUT -->|"编码帧"| MESH
CLK -->|"提频保障编码性能"| ENC
架构说明:
- 数据源层:默认启用 MIC 通道(
MIC_USE_MIC_CHANNEL),adc_mic_output_handler在 ADC 中断中把采集到的s16数据写入环形缓冲并唤醒编码器;模拟模式下定时器每 40ms 从sin44K正弦波表读出 320 字节写入缓冲(该路径默认注释)。 - PCM 缓冲层:
demo_enc_pcm_get是编码器的fget回调,当缓冲不足一帧时os_sem_pend等待(超时 5ms),数据就绪后cbuf_read读出frame_len字节。 - 编码层:
audio_encoder_task(线程名"audio_enc")驱动编码器,audio_encoder_set_fmt下发格式参数;输出帧缓冲为 320 字节。 - 输出层:编码完成经
demo_enc_output_handler回调,打印帧长、put_buf输出前 16 字节调试信息,并调用上层注册的demo_output回调——这是把编码帧打包成 Mesh Vendor Model 消息发送的扩展点。 - 时钟管理:开启编码时若当前
sys时钟低于 96MHz 则提升,关闭时恢复,保证编码实时性同时避免常驻高频。
实现文件关系
| 文件 | 作用 |
|---|---|
apps/mesh/audio_codec_demo.c | 本演示的全部实现:编码器句柄、数据源、格式配置、开关函数 |
apps/mesh/examples/generic_onoff_server.c | MESH_AUDIO_TEST 使能时按键触发 Audio Test Demo 的调用点 |
apps/mesh/board/bd19/board_ac6321a_demo_cfg.h | 板级配置示例:MESH_AUDIO_TEST 默认 DISABLE |
asm/audio_src.h / audio_enc.h / sbc_enc.h | SDK 音频编码框架头文件(系统库) |
api/mesh_config.h | Mesh 配置接口头文件,演示文件引用以便对接 Mesh 发送 |
核心实现
编译开关与启用条件
整个演示文件被 #if ENC_DEMO_EN 包裹,ENC_DEMO_EN 未定义或为 0 时整个编码演示被编译排除:
#if ENC_DEMO_EN
...
#endif
来源:apps/mesh/audio_codec_demo.c
与之配套的板级宏 MESH_AUDIO_TEST 在示例板配置中默认关闭,需在板级 *_demo_cfg.h 中打开:
/* Mesh Audio Test */
#define MESH_AUDIO_TEST DISABLE
来源:apps/mesh/board/bd19/board_ac6321a_demo_cfg.h
各编码器由 TCFG_ENC_*_ENABLE 宏控制是否参与编译,演示运行时再按 code_type 参数选择。若所选编码器未使能,audio_encoder_open 后 encoder.enc_priv 为空,函数返回 -EINVAL 并打印 "encoder err, maybe coding(0x%x) disable"。
数据结构 demo_enc_hdl
struct demo_enc_hdl {
struct audio_encoder encoder;
OS_SEM pcm_frame_sem;
u8 output_frame[ENC_OUT_SIZE];
u8 pcm_frame[ENC_IN_SIZE];
u8 frame_size;
u8 in_cbuf_buf[ENC_IN_SIZE * 6];
cbuffer_t pcm_in_cbuf;
u32 clk_before;
#if mic_enc_PACK_ENABLE
u16 cp_type;
u16 packet_head_sn;
#endif
#if MIC_USE_MIC_CHANNEL
struct audio_adc_output_hdl adc_output;
struct adc_mic_ch mic_ch;
s16 adc_buf[ENC_ADC_BUFS_SIZE]; //align 4Bytes
#endif
int (*demo_output)(void *priv, void *buf, int len);
};
来源:apps/mesh/audio_codec_demo.c
各字段职责:
| 字段 | 说明 |
|---|---|
encoder | SDK 音频编码器实例,由 audio_encoder_open 初始化 |
pcm_frame_sem | 生产/消费同步信号量,缓冲不足时编码侧挂起 |
output_frame[320] | 编码输出帧缓冲(ENC_OUT_SIZE = 320 字节) |
pcm_frame[640] | 单帧 PCM 输入缓冲(ENC_IN_SIZE = 640 字节 = 320 个 s16 采样) |
in_cbuf_buf[640*6] | 环形缓冲底层存储,容量 6 帧 PCM,平滑 ADC 中断与编码消费的速率差 |
pcm_in_cbuf | 环形缓冲句柄 |
clk_before | 编码前 sys 时钟,关闭时恢复 |
cp_type / packet_head_sn | mic_enc_PACK_ENABLE 使能时的打包类型与包序号(预留的分包发送字段) |
adc_output / mic_ch / adc_buf | MIC 通道资源:输出回调句柄、MIC 通道、320*2 点双缓冲 |
demo_output | 编码输出回调函数指针,由调用方通过 audio_demo_enc_open 首参注册 |
全局实例 demo_enc 为静态指针,编码期间非空,关闭后被释放置空;多处回调先判空再使用,避免悬垂指针。
PCM 数据生产(采集侧)
MIC 路径(默认启用): ADC 中断回调把采集数据写入环形缓冲,随后唤醒编码任务:
static void adc_mic_output_handler(void *priv, s16 *data, int len)
{
if (demo_enc) {
u16 wlen = cbuf_write(&demo_enc->pcm_in_cbuf, data, len);
if (wlen != len) {
putchar('@');
}
audio_encoder_resume(&demo_enc->encoder);
demo_enc_resume();
}
}
来源:apps/mesh/audio_codec_demo.c
设计意图:audio_encoder_resume 唤醒编码器立即取数,demo_enc_resume 释放信号量;当环形缓冲满导致 cbuf_write 写入字节数不足时打印 '@' 作水位告警——这通常意味着编码吞吐跟不上采集速率,需要提高 ENC_CLK 或检查编码格式配置。
模拟路径(默认注释): 定时器回调通过正弦波表生成 PCM,便于无 MIC 环境验证:
static void demo_frame_test_time_func(void *param)
{
u32 len = 320; //每次写的字节数
frame_bytes = read_44k_sine_data(pcm_frames, len, frames_offset, 1);
frames_offset += (frame_bytes >> 1) / 1; //固定传320
if (demo_enc) {
u16 wlen = cbuf_write(&demo_enc->pcm_in_cbuf, pcm_frames, len);
if (wlen != len) {
putchar('@');
}
audio_encoder_resume(&demo_enc->encoder);
demo_enc_resume();
}
}
来源:apps/mesh/audio_codec_demo.c
read_44k_sine_data 从 88 字节的 sin44K 表按偏移循环读出采样并扩展声道,返回实际写入字节数:
int read_44k_sine_data(void *buf, int bytes, int offset, u8 channel)
{
s16 *sine = (s16 *)sin44K;
s16 *data = (s16 *)buf;
int frame_len = (bytes >> 1) / channel;
int sin44k_frame_len = sizeof(sin44K) / 2;
int i, j;
offset = offset % sin44k_frame_len;
for (i = 0; i < frame_len; i++) {
for (j = 0; j < channel; j++) {
*data++ = sine[offset];
}
if (++offset >= sin44k_frame_len) {
offset = 0;
}
}
return i * 2 * channel;
}
来源:apps/mesh/audio_codec_demo.c
PCM 数据消费(编码侧)
编码器通过 fget 回调取数。数据不足时挂起信号量等待生产,超时 5ms:
static int demo_enc_pcm_get(struct audio_encoder *encoder, s16 **frame, u16 frame_len)
{
int pcm_len = 0;
if (demo_enc == NULL) {
r_printf("demo_enc NULL\n");
return 0;
}
if ((&demo_enc->pcm_in_cbuf)->data_len < frame_len) {
os_sem_set(&demo_enc->pcm_frame_sem, 0);
os_sem_pend(&demo_enc->pcm_frame_sem, 5);
if (demo_enc == NULL) {
printf("demo_enc is NULL\n");
return 0;
}
}
pcm_len = cbuf_read(&demo_enc->pcm_in_cbuf, demo_enc->pcm_frame, frame_len);
if (pcm_len != frame_len) {
putchar('L');
}
*frame = demo_enc->pcm_frame;
return pcm_len;
}
来源:apps/mesh/audio_codec_demo.c
要点:
demo_enc_pcm_get与demo_enc_pcm_put(空实现)组成demo_enc_input回调集,通过audio_encoder_open(&demo_enc->encoder, &demo_enc_input, encode_task)注入编码器。- 等待前先
os_sem_set(..., 0)清零信号量再os_sem_pend(..., 5),保证只等待一次新数据到达,且 5ms 超时防止编码任务永久阻塞。 cbuf_read返回字节数不足frame_len时打印'L',表示缓冲水位不足(可能出现于超时退出后)。
编码输出
static int demo_enc_output_handler(struct audio_encoder *encoder, u8 *frame, int len)
{
if (encoder == NULL) {
r_printf("encoder NULL");
}
wdt_clear();
printf("demo frame len:%d \n", len);
if (demo_enc && demo_enc->demo_output) {
demo_enc->demo_output(NULL, frame, len);
}
put_buf(frame, 16);
return len;
}
来源:apps/mesh/audio_codec_demo.c
设计意图:wdt_clear() 确保持续编码时看门狗不被触发;put_buf(frame, 16) 输出帧头 16 字节便于串口抓包比对编码格式;demo_output 回调即 Mesh 发送扩展点——把编码帧填入 Mesh Vendor Model 的 mesh_model_send 消息即可完成"采集-编码-入网"链路。
编码格式参数配置
audio_demo_enc_open 按 code_type 填充 audio_fmt fmt:
int audio_demo_enc_open(int (*demo_output)(void *priv, void *buf, int len), u32 code_type, u8 ai_type)
{
int err;
struct audio_fmt fmt = {0};
switch (code_type) {
#if TCFG_ENC_OPUS_ENABLE
case AUDIO_CODING_OPUS:
fmt.quality = 0 | ai_type/*| LOW_COMPLEX*/;
fmt.sample_rate = 16000;
fmt.coding_type = AUDIO_CODING_OPUS;
break;
#endif
#if TCFG_ENC_SPEEX_ENABLE
case AUDIO_CODING_SPEEX:
fmt.quality = 5;
fmt.complexity = 2;
fmt.sample_rate = 16000;
fmt.coding_type = AUDIO_CODING_SPEEX;
break;
#endif
#if TCFG_ENC_ADPCM_ENABLE
case AUDIO_CODING_WAV: //adpcm 编码
fmt.sample_rate = 16000;
fmt.bit_rate = 1024; //blockSize,可配成256/512/1024/2048
fmt.channel = 2;
fmt.coding_type = AUDIO_CODING_WAV;
break;
#endif
#if TCFG_ENC_LC3_ENABLE
case AUDIO_CODING_LC3:
fmt.bit_rate = 64000;
fmt.sample_rate = LC3_CODING_SAMPLERATE;
fmt.frame_len = LC3_CODING_FRAME_LEN;
fmt.channel = LC3_CODING_CHANNEL;
fmt.coding_type = AUDIO_CODING_LC3;
break;
#endif
#if TCFG_ENC_SBC_ENABLE
case AUDIO_CODING_SBC:
sbc_t sbc_enc_parm = {
.frequency = SBC_FREQ_44100,
.blocks = SBC_BLK_16,
.subbands = SBC_SB_8,
.mode = SBC_MODE_STEREO,
.allocation = 0,
.endian = SBC_LE,
.bitpool = 53
};
fmt.priv = (void *)(&sbc_enc_parm);
fmt.frame_len = 640;
fmt.coding_type = AUDIO_CODING_SBC;
break;
#endif
#if TCFG_ENC_MSBC_ENABLE
case AUDIO_CODING_MSBC: //msbc没有外部配置的参数
fmt.coding_type = AUDIO_CODING_MSBC;
break;
#endif
default:
printf("do not support this type !!!\n");
return -1;
}
...
}
来源:apps/mesh/audio_codec_demo.c
各格式设计要点:
| 编码格式 | 关键参数 | 设计意图 |
|---|---|---|
| OPUS | quality = 0 | ai_type,16kHz 采样 | quality 低 2 位选码率(0:16kbps / 1:32kbps / 2:64kbps),高 4 位 ai_type 透传 AI 场景标志;注释中说明可加 LOW_COMPLEX(bit4)降复杂度 |
| SPEEX | quality = 5,complexity = 2,16kHz | 面向语音的低码率编码,复杂度 2 平衡 CPU 开销 |
| WAV(ADPCM) | 16kHz 双声道,bit_rate 为 blockSize(256~2048) | ADPCM 通过 const_sel_adpcm_type 选择 IMA/MS 格式;Mesh 场景可改单声道进一步降码率 |
| LC3 | 64kbps,采样率/帧长/声道取自 LC3_CODING_* 宏 | 新一代低延迟音频编码,BLE Audio 同款,适合对延迟敏感的 Mesh 语音 |
| SBC | 44.1kHz,16 block,8 subband,立体声,bitpool 53 | 经典蓝牙 A2DP 同款参数,frame_len=640 保证一次取数足够编码一帧 |
| MSBC | 无外部参数 | 免提(HFP)同款 16kHz 宽带语音编码,参数固定 |
code_type 无匹配分支时打印 "do not support this type !!!" 并返回 -1,属预期错误路径。
核心流程
编码器启动流程
audio_demo_enc_open 完成"资源分配 → 编码器装配 → 数据源启动"三步:
if (!encode_task) {
encode_task = zalloc(sizeof(*encode_task));
audio_encoder_task_create(encode_task, "audio_enc");
}
if (!demo_enc) {
demo_enc = zalloc(sizeof(*demo_enc));
memset(demo_enc, 0x00, sizeof(*demo_enc));
}
demo_enc_output_func_register(demo_output);
cbuf_init(&demo_enc->pcm_in_cbuf, demo_enc->in_cbuf_buf, ENC_IN_SIZE * 6);
os_sem_create(&demo_enc->pcm_frame_sem, 0);
audio_encoder_open(&demo_enc->encoder, &demo_enc_input, encode_task);
audio_encoder_set_handler(&demo_enc->encoder, &demo_enc_handler);
audio_encoder_set_fmt(&demo_enc->encoder, &fmt);
audio_encoder_set_event_handler(&demo_enc->encoder, demo_enc_event_handler, 0);
audio_encoder_set_output_buffs(&demo_enc->encoder, demo_enc->output_frame,
sizeof(demo_enc->output_frame), 1);
if (!demo_enc->encoder.enc_priv) {
log_e("encoder err, maybe coding(0x%x) disable \n", fmt.coding_type);
err = -EINVAL;
goto __err;
}
int start_err = audio_encoder_start(&demo_enc->encoder);
...
#if MIC_USE_MIC_CHANNEL
demo_enc->clk_before = clk_get("sys");
if (demo_enc->clk_before < ENC_CLK) {
clk_set("sys", ENC_CLK);
}
audio_mic_pwr_ctl(MIC_PWR_ON);
audio_adc_mic_open(&demo_enc->mic_ch, AUDIO_ADC_MIC_CH, &adc_hdl);
audio_adc_mic_set_sample_rate(&demo_enc->mic_ch, fmt.sample_rate);
audio_adc_mic_set_gain(&demo_enc->mic_ch, app_var.aec_mic_gain);
audio_adc_mic_set_buffs(&demo_enc->mic_ch, demo_enc->adc_buf,
ENC_ADC_IRQ_POINTS * 2, ENC_BUF_NUM);
demo_enc->adc_output.handler = adc_mic_output_handler;
audio_adc_add_output_handler(&adc_hdl, &demo_enc->adc_output);
audio_adc_mic_start(&demo_enc->mic_ch);
#endif
来源:apps/mesh/audio_codec_demo.c
sequenceDiagram
participant App as 应用层 (generic_onoff_server / 自定义)
participant Enc as audio_demo_enc_open
participant Task as encode_task<br/>(audio_enc)
participant Encoder as audio_encoder
participant ADC as ADC MIC
participant CBUF as pcm_in_cbuf
participant OUT as demo_output 回调
App->>Enc: audio_demo_enc_open(output_cb, code_type, ai_type)
Enc->>Enc: 按 code_type 填充 audio_fmt
Enc->>Task: audio_encoder_task_create("audio_enc")
Enc->>Encoder: audio_encoder_open + set_fmt + set_handler
Enc->>Encoder: audio_encoder_start
Enc->>ADC: 提升 sys 时钟至 96MHz
Enc->>ADC: audio_mic_pwr_ctl(MIC_PWR_ON)
Enc->>ADC: audio_adc_mic_open / set_buffs / start
ADC-->>CBUF: 中断回调 adc_mic_output_handler 写 PCM
CBUF-->>Encoder: fget 取数 (demo_enc_pcm_get)
Encoder->>Encoder: 编码 (OPUS/SPEEX/ADPCM/LC3/SBC/MSBC)
Encoder-->>OUT: enc_output 回调 (demo_enc_output_handler)
OUT-->>App: demo_output 回调收到编码帧
Note over OUT,App: 扩展点: 在此打包 Mesh Vendor 消息发送
编码循环状态流转
stateDiagram-v2
[*] --> IDLE
IDLE --> STARTING: audio_demo_enc_open
STARTING --> RUNNING: audio_encoder_start + mic start
RUNNING --> WAITING: cbuf 数据不足一帧
WAITING --> RUNNING: os_sem_pend 收到信号量(≤5ms)
RUNNING --> OUTPUT: 编码完成 enc_output
OUTPUT --> RUNNING: demo_output 回调返回
RUNNING --> STOPPING: audio_demo_enc_close
STOPPING --> IDLE: 释放 demo_enc
IDLE --> [*]
关闭流程
int audio_demo_enc_close()
{
if (!demo_enc) {
return -1;
}
printf("audio_demo_enc_close\n");
#if MIC_USE_MIC_CHANNEL
clk_set("sys", demo_enc->clk_before);
audio_adc_mic_close(&demo_enc->mic_ch);
audio_adc_del_output_handler(&adc_hdl, &demo_enc->adc_output);
#endif
demo_enc_resume();
audio_encoder_close(&demo_enc->encoder);
free(demo_enc);
demo_enc = NULL;
printf("audio_demo_enc_close end\n");
return 0;
}
来源:apps/mesh/audio_codec_demo.c
关闭顺序体现了资源依赖关系:先恢复时钟(编码器不再需要高频)、关闭 MIC 与注销中断回调(停止生产)、demo_enc_resume 唤醒可能阻塞在信号量上的编码任务(防止 audio_encoder_close 死锁)、最后关闭编码器并释放句柄。encode_task 保留不删除(注释代码),避免重复创建任务的开销。
使用示例
示例 1:注册编码输出回调(Mesh 发送扩展点)
调用方传入的 demo_output 回调会在每一帧编码完成后被调用,frame/len 即编码后的压缩数据,可直接封装为 Mesh 消息:
static int mesh_audio_output(void *priv, void *buf, int len)
{
/* 在此处把编码帧通过 Mesh Vendor Model 发送 */
/* mesh_model_send(...) */
return len;
}
来源:apps/mesh/audio_codec_demo.c(demo_output 函数指针类型与 demo_enc_output_func_register 注册机制)
示例 2:开启 MIC 编码(默认路径)
int audio_demo_enc_open(int (*demo_output)(void *priv, void *buf, int len), u32 code_type, u8 ai_type)
demo_output:编码输出回调,必填;code_type:AUDIO_CODING_OPUS/AUDIO_CODING_SPEEX/AUDIO_CODING_WAV/AUDIO_CODING_LC3/AUDIO_CODING_SBC/AUDIO_CODING_MSBC;ai_type:AI 场景标志,仅 OPUS 分支有效(透传到fmt.quality高 4 位)。
来源:apps/mesh/audio_codec_demo.c
示例 3:按键触发测试(板级集成方式)
在 generic_onoff_server.c 的按键处理中,MESH_AUDIO_TEST 使能时按下 TCFG_ADKEY_VALUE0 触发 Audio Test Demo:
#if (TCFG_AUDIO_ENABLE && MESH_AUDIO_TEST)
if (key_status == KEY_EVENT_CLICK && key_number == TCFG_ADKEY_VALUE0) {
/*Audio Test Demo*/
来源:apps/mesh/examples/generic_onoff_server.c
同时 audio_demo_enc_open 在 MESH_AUDIO_TEST 使能时会把 MIC 增益设为 14(app_var.aec_mic_gain = 14),保证采集幅度适中:
#if (TCFG_AUDIO_ENABLE && MESH_AUDIO_TEST)
app_var.aec_mic_gain = 14;
#endif
来源:apps/mesh/audio_codec_demo.c
配置选项
编译期宏
| 宏 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ENC_DEMO_EN | bool | 未定义(0) | 整个音频编解码演示的总开关,为 0 时 audio_codec_demo.c 全部代码被排除 |
MESH_AUDIO_TEST | bool | DISABLE | 板级 Mesh 音频测试开关;使能后按键可触发测试,并将 MIC 增益设为 14 |
TCFG_AUDIO_ENABLE | bool | 板级决定 | 音频子系统总开关,与 MESH_AUDIO_TEST 共同决定按键测试是否编译 |
MIC_USE_MIC_CHANNEL | bool | 1 | 是否使用 ADC MIC 作为 PCM 数据源;为 0 时仅能用 timer 模拟灌数 |
TCFG_ENC_OPUS_ENABLE | bool | 板级决定 | OPUS 编码器编译开关 |
TCFG_ENC_SPEEX_ENABLE | bool | 板级决定 | SPEEX 编码器编译开关 |
TCFG_ENC_ADPCM_ENABLE | bool | 板级决定 | ADPCM(WAV) 编码器编译开关 |
TCFG_ENC_LC3_ENABLE | bool | 板级决定 | LC3 编码器编译开关 |
TCFG_ENC_SBC_ENABLE | bool | 板级决定 | SBC 编码器编译开关 |
TCFG_ENC_MSBC_ENABLE | bool | 板级决定 | MSBC 编码器编译开关 |
mic_enc_PACK_ENABLE | bool | 未定义(0) | 使能后启用 cp_type / packet_head_sn 打包字段,供编码帧分包发送使用 |
内部常量(audio_codec_demo.c 顶部定义)
| 常量 | 值 | 说明 |
|---|---|---|
ENC_BUF_NUM | 2 | ADC 采集双缓冲个数 |
ENC_ADC_IRQ_POINTS | 320 | ADC 单次中断采样点数 |
ENC_ADC_BUFS_SIZE | 640 | ADC 缓冲总采样点数(2 * 320) |
ENC_IN_SIZE | 640 | 单帧 PCM 输入字节数(320 个 s16 采样) |
ENC_OUT_SIZE | 320 | 编码输出帧缓冲字节数 |
ENC_CLK | 96MHz | 编码期间 sys 时钟目标频率 |
HIGHT_COMPLEX / LOW_COMPLEX | 0 / BIT(4) | OPUS 复杂度标志位(LOW_COMPLEX 用于注释中的降复杂度选项) |
ENC_IN_SIZE * 6 | 3840 | pcm_in_cbuf 环形缓冲容量(6 帧 PCM) |
编码格式参数汇总
| 编码格式 | 采样率 | 关键参数 | 依赖宏 |
|---|---|---|---|
| OPUS | 16000 | quality = 0 | ai_type(bitrate: 0/1/2 档) | TCFG_ENC_OPUS_ENABLE |
| SPEEX | 16000 | quality = 5, complexity = 2 | TCFG_ENC_SPEEX_ENABLE |
| WAV(ADPCM) | 16000 | bit_rate = 1024(blockSize), channel = 2 | TCFG_ENC_ADPCM_ENABLE |
| LC3 | LC3_CODING_SAMPLERATE | bit_rate = 64000, frame_len/channel 取 LC3_CODING_* 宏 | TCFG_ENC_LC3_ENABLE |
| SBC | 44100(SBC_FREQ_44100) | 16 blocks / 8 subbands / STEREO / LE / bitpool 53,frame_len = 640 | TCFG_ENC_SBC_ENABLE |
| MSBC | 固定(无外部参数) | — | TCFG_ENC_MSBC_ENABLE |
API 参考
int audio_demo_enc_open(int (*demo_output)(void *priv, void *buf, int len), u32 code_type, u8 ai_type)
打开并启动音频编码演示链路:创建编码任务(单例)、分配并初始化 demo_enc 句柄、装配编码器与格式、启动 MIC 采集(MIC_USE_MIC_CHANNEL 为 1 时)并提升系统时钟。
参数:
demo_output(函数指针):编码完成回调,priv为调用方私有数据(当前传NULL),buf/len为编码帧数据与长度。此回调即 Mesh 发送扩展点。code_type(u32):编码格式,取AUDIO_CODING_OPUS/AUDIO_CODING_SPEEX/AUDIO_CODING_WAV/AUDIO_CODING_LC3/AUDIO_CODING_SBC/AUDIO_CODING_MSBC。ai_type(u8):AI 场景标志,仅 OPUS 分支有效,透传到fmt.quality高 4 位。
返回:
0:成功(printf("demo_enc_open ok %d", start_err))。-1:code_type无匹配分支。-EINVAL:编码器使能缺失(encoder.enc_priv为空)。
Throws / 错误输出:
- 内存分配失败:
printf("encode_task NULL !!!")/printf("demo_enc NULL !!!")后继续执行(未显式返回错误,仅日志告警)。
int audio_demo_enc_close(void)
关闭编码链路:恢复系统时钟、关闭 MIC 并注销输出回调、唤醒编码任务、关闭编码器、释放 demo_enc。
返回:
0:成功。-1:demo_enc为空(重复关闭或未打开)。
void demo_enc_resume(void)
向 pcm_frame_sem 投递一个信号量,唤醒可能阻塞在 demo_enc_pcm_get 中的编码任务。由数据生产方(ADC 中断回调、timer 回调)及 audio_demo_enc_close 调用,后者用于防止关闭时任务悬挂。
int read_44k_sine_data(void *buf, int bytes, int offset, u8 channel)
从 44.1kHz 正弦波表生成 PCM 数据。
参数: buf 输出缓冲;bytes 请求字节数;offset 表内偏移(自动取模回绕);channel 声道数(每采样复制到各声道)。
返回: 实际写入的字节数(i * 2 * channel)。
失败模式、边界情况与并发
缓冲水位告警('@' / 'L' 字符输出)
'@'(生产侧):cbuf_write写入字节数不足请求长度,说明环形缓冲已满,编码消费速度低于采集速度。排查方向:检查ENC_CLK是否生效、所选编码格式复杂度是否过高(如 OPUS 未开LOW_COMPLEX、SBC bitpool 偏大)、采样率是否匹配。'L'(消费侧):cbuf_read读出的字节数不足frame_len,说明缓冲欠载,通常发生在信号量 5ms 超时退出后。欠载会导致编码帧不完整,需确保数据源稳定灌数。
编码器未使能
audio_encoder_set_fmt 后若对应 TCFG_ENC_*_ENABLE 未打开,encoder.enc_priv 为空,函数打印 "encoder err, maybe coding(0x%x) disable" 并返回 -EINVAL,随后走 __err 标签:关闭编码器、关中断后释放 demo_enc 并置空。注意 __err 路径在 local_irq_disable 保护下 free,避免与中断回调并发访问。
空指针防护
- 所有回调(
demo_enc_pcm_get、adc_mic_output_handler、demo_enc_output_handler)先判demo_enc是否为空,关闭流程将demo_enc置NULL后回调直接返回,防止悬垂指针。 demo_enc_pcm_get在信号量等待返回后再次判空——因为等待期间audio_demo_enc_close可能已释放句柄(关闭流程通过demo_enc_resume主动唤醒等待者),这是典型的"等待后重查"防竞态模式。
并发模型
| 上下文 | 线程/中断 | 与 demo_enc 的关系 |
|---|---|---|
demo_enc_pcm_get | 编码任务线程 "audio_enc" | 消费 pcm_in_cbuf,等待信号量 |
adc_mic_output_handler | ADC 中断上下文 | 生产 pcm_in_cbuf,audio_encoder_resume 唤醒任务,投递信号量 |
demo_frame_test_time_func | sys_hi_timer 定时器回调 | 生产 pcm_in_cbuf(默认注释) |
demo_enc_output_handler | 编码任务线程 | 回调 demo_output,wdt_clear |
audio_demo_enc_open/close | 应用线程 | 分配/释放 demo_enc,close 先唤醒再释放 |
环形缓冲 + 信号量实现单生产者单消费者解耦;demo_enc 指针的生命周期由应用线程管理,中断与任务侧通过判空与唤醒顺序规避竞态。
时钟与功耗边界
编码期间 sys 时钟被强制提升至 96MHz(clk_set("sys", ENC_CLK)),仅当当前时钟低于目标时提升;关闭时恢复 clk_before。若中途异常退出(未调用 audio_demo_enc_close),时钟会保持高频,属需注意的边界场景。
关闭时序
audio_demo_enc_close 若在编码任务阻塞于 os_sem_pend 时直接关闭编码器,可能造成任务悬挂或访问已释放缓冲;因此先 demo_enc_resume() 唤醒,再 audio_encoder_close。若 demo_enc 已为空则直接返回 -1(幂等)。
性能与运维注意事项
- 编码实时性:编码为计算密集操作,务必确认
ENC_CLK(96MHz)在编码期间生效;同时编码输出回调中wdt_clear()避免长时间编码触发看门狗复位。 - 码率评估:Mesh 场景下 BLE 有效吞吐有限(GATT/Adv 承载),建议优先选用低码率格式:SPEEX(约 8~16kbps)、OPUS 16kbps 档、MSBC(约 64kbps 宽带语音)、ADPCM 单声道。SBC 立体声 bitpool 53 码率较高,实测中需评估是否适合 Mesh 分发。
- 帧率对齐:ADC 每次中断 320 点(16kHz 下约 20ms),编码器按
frame_len取数;OPUS/SPEEX 16kHz 一帧 20ms 可与之对齐,SBC 需 640 字节源数据(44.1kHz 下约 7.25ms),注意取数节奏差异。 - 串口调试:编码输出会打印
"demo frame len:%d"并put_buf前 16 字节,可抓取比对帧头确认编码格式与参数;'@'/'L'字符为水位告警,量产固件建议关闭此类打印。 - 内存占用:
demo_enc句柄约含 640×6 字节环形缓冲 + 640 字节输入帧 + 320 字节输出帧 + ADC 缓冲(320×2×2 字节),加上编码器内部状态,总内存开销在数百字节到 1KB 级,对 AC63xx 属可接受范围。
扩展点
demo_output回调(Mesh 发送):audio_demo_enc_open首参注册的编码输出回调是本演示与 Mesh 网络的唯一耦合点。可在回调中把编码帧封装为 Mesh Vendor Model 消息(参考api/mesh_config.h与 SIG Mesh 模型接口)发送,实现 Mesh 语音对讲/广播。mic_enc_PACK_ENABLE分包字段:使能后demo_enc_hdl携带cp_type与packet_head_sn,编码帧可带类型与序号分包发送,接收端据此重组;当前源文件仅预留字段,未实现具体打包逻辑。- 数据源替换:
MIC_USE_MIC_CHANNEL置 0 并打开 timer 模拟灌数(sys_hi_timer_add(NULL, demo_frame_test_time_func, 40)),可脱离 MIC 硬件验证编码链路;也可替换为其他数据源(如文件系统读 PCM)。 - 编码格式扩展:按
audio_fmt结构新增case分支并打开对应TCFG_ENC_*_ENABLE即可接入新编码器;demo_enc_pcm_get的frame_len与环形缓冲容量(ENC_IN_SIZE * 6)需与新格式帧长匹配。 - 关闭后任务复用:
encode_task在audio_demo_enc_close中保留不删除(代码已注释删除逻辑),便于编码链路快速重启。
测试覆盖情况
源文件中未发现独立单元测试;验证方式为板级集成测试:
MESH_AUDIO_TEST使能后,在generic_onoff_server.c按键处理中按下TCFG_ADKEY_VALUE0(单击)触发 Audio Test Demo(见 generic_onoff_server.c#L409-L411)。- 验证手段为串口日志:
"demo_enc_open ok"、"demo frame len:%d"、put_buf帧头数据;'@'/'L'字符提示缓冲水位异常。 - 各板级示例(bd19/AC632N 等)默认
MESH_AUDIO_TEST = DISABLE,需按板修改板级配置后编译验证。
相关链接
- 音频编解码演示源文件 audio_codec_demo.c
- generic_onoff_server.c 按键触发音频测试
- 板级配置示例 board_ac6321a_demo_cfg.h(MESH_AUDIO_TEST)
- SIG Mesh 模型接口 sig_mesh_api.h
- Mesh 配置接口 mesh_config.h
- Mesh 应用主入口 app_mesh.c
说明:本页覆盖 Mesh 音频编码端演示;解码端由系统
audio_decoder框架承载,SDK 源文件中未发现独立的 Mesh 音频解码演示文件,相关解码参数需结合目标编码格式(如 LC3/SBC)的系统解码配置进一步确认。