杰理 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)
  • 文档与开发资源

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

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 上的可行性,并提供一个可复用的编码器封装模板。

关键设计决策

  1. 双数据源设计:默认使用 ADC 麦克风(MIC_USE_MIC_CHANNEL = 1)实时采集;同时预留了 sys_hi_timer 定时器 + 正弦波表的模拟数据源(demo_frame_test_time_func,默认被注释),便于在没有 MIC 硬件环境下验证编码链路。
  2. 环形缓冲 + 信号量同步:采集端(中断回调或定时器)把 PCM 写入 pcm_in_cbuf 环形缓冲,编码任务侧 demo_enc_pcm_get 在缓冲数据不足时通过 pcm_frame_sem 信号量等待,实现生产-消费解耦。
  3. 编码格式可插拔:通过 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 参数选择。
  4. 编码期间提频:编码是计算密集操作,audio_demo_enc_open 会把系统时钟提升到 ENC_CLK(96MHz),关闭时恢复原时钟,平衡性能与功耗。
  5. 看门狗喂狗:编码输出回调中调用 wdt_clear(),防止长时间编码阻塞导致系统复位。

术语表

术语含义
ENC_DEMO_EN本演示文件的总编译开关
MESH_AUDIO_TEST板级配置中的 Mesh 音频测试使能,控制按键触发测试与 MIC 增益
demo_enc_hdl演示编码器句柄,封装编码器、环形缓冲、信号量与 MIC 资源
audio_encoderSDK 音频编码器抽象(audio_enc.h)
audio_encoder_task独立编码任务,驱动编码器运行
pcm_in_cbufPCM 输入环形缓冲,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.cMESH_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.hSDK 音频编码框架头文件(系统库)
api/mesh_config.hMesh 配置接口头文件,演示文件引用以便对接 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

各字段职责:

字段说明
encoderSDK 音频编码器实例,由 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_snmic_enc_PACK_ENABLE 使能时的打包类型与包序号(预留的分包发送字段)
adc_output / mic_ch / adc_bufMIC 通道资源:输出回调句柄、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

各格式设计要点:

编码格式关键参数设计意图
OPUSquality = 0 | ai_type,16kHz 采样quality 低 2 位选码率(0:16kbps / 1:32kbps / 2:64kbps),高 4 位 ai_type 透传 AI 场景标志;注释中说明可加 LOW_COMPLEX(bit4)降复杂度
SPEEXquality = 5,complexity = 2,16kHz面向语音的低码率编码,复杂度 2 平衡 CPU 开销
WAV(ADPCM)16kHz 双声道,bit_rate 为 blockSize(256~2048)ADPCM 通过 const_sel_adpcm_type 选择 IMA/MS 格式;Mesh 场景可改单声道进一步降码率
LC364kbps,采样率/帧长/声道取自 LC3_CODING_* 宏新一代低延迟音频编码,BLE Audio 同款,适合对延迟敏感的 Mesh 语音
SBC44.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_ENbool未定义(0)整个音频编解码演示的总开关,为 0 时 audio_codec_demo.c 全部代码被排除
MESH_AUDIO_TESTboolDISABLE板级 Mesh 音频测试开关;使能后按键可触发测试,并将 MIC 增益设为 14
TCFG_AUDIO_ENABLEbool板级决定音频子系统总开关,与 MESH_AUDIO_TEST 共同决定按键测试是否编译
MIC_USE_MIC_CHANNELbool1是否使用 ADC MIC 作为 PCM 数据源;为 0 时仅能用 timer 模拟灌数
TCFG_ENC_OPUS_ENABLEbool板级决定OPUS 编码器编译开关
TCFG_ENC_SPEEX_ENABLEbool板级决定SPEEX 编码器编译开关
TCFG_ENC_ADPCM_ENABLEbool板级决定ADPCM(WAV) 编码器编译开关
TCFG_ENC_LC3_ENABLEbool板级决定LC3 编码器编译开关
TCFG_ENC_SBC_ENABLEbool板级决定SBC 编码器编译开关
TCFG_ENC_MSBC_ENABLEbool板级决定MSBC 编码器编译开关
mic_enc_PACK_ENABLEbool未定义(0)使能后启用 cp_type / packet_head_sn 打包字段,供编码帧分包发送使用

内部常量(audio_codec_demo.c 顶部定义)

常量值说明
ENC_BUF_NUM2ADC 采集双缓冲个数
ENC_ADC_IRQ_POINTS320ADC 单次中断采样点数
ENC_ADC_BUFS_SIZE640ADC 缓冲总采样点数(2 * 320)
ENC_IN_SIZE640单帧 PCM 输入字节数(320 个 s16 采样)
ENC_OUT_SIZE320编码输出帧缓冲字节数
ENC_CLK96MHz编码期间 sys 时钟目标频率
HIGHT_COMPLEX / LOW_COMPLEX0 / BIT(4)OPUS 复杂度标志位(LOW_COMPLEX 用于注释中的降复杂度选项)
ENC_IN_SIZE * 63840pcm_in_cbuf 环形缓冲容量(6 帧 PCM)

编码格式参数汇总

编码格式采样率关键参数依赖宏
OPUS16000quality = 0 | ai_type(bitrate: 0/1/2 档)TCFG_ENC_OPUS_ENABLE
SPEEX16000quality = 5, complexity = 2TCFG_ENC_SPEEX_ENABLE
WAV(ADPCM)16000bit_rate = 1024(blockSize), channel = 2TCFG_ENC_ADPCM_ENABLE
LC3LC3_CODING_SAMPLERATEbit_rate = 64000, frame_len/channel 取 LC3_CODING_* 宏TCFG_ENC_LC3_ENABLE
SBC44100(SBC_FREQ_44100)16 blocks / 8 subbands / STEREO / LE / bitpool 53,frame_len = 640TCFG_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_handlerADC 中断上下文生产 pcm_in_cbuf,audio_encoder_resume 唤醒任务,投递信号量
demo_frame_test_time_funcsys_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 属可接受范围。

扩展点

  1. demo_output 回调(Mesh 发送):audio_demo_enc_open 首参注册的编码输出回调是本演示与 Mesh 网络的唯一耦合点。可在回调中把编码帧封装为 Mesh Vendor Model 消息(参考 api/mesh_config.h 与 SIG Mesh 模型接口)发送,实现 Mesh 语音对讲/广播。
  2. mic_enc_PACK_ENABLE 分包字段:使能后 demo_enc_hdl 携带 cp_type 与 packet_head_sn,编码帧可带类型与序号分包发送,接收端据此重组;当前源文件仅预留字段,未实现具体打包逻辑。
  3. 数据源替换:MIC_USE_MIC_CHANNEL 置 0 并打开 timer 模拟灌数(sys_hi_timer_add(NULL, demo_frame_test_time_func, 40)),可脱离 MIC 硬件验证编码链路;也可替换为其他数据源(如文件系统读 PCM)。
  4. 编码格式扩展:按 audio_fmt 结构新增 case 分支并打开对应 TCFG_ENC_*_ENABLE 即可接入新编码器;demo_enc_pcm_get 的 frame_len 与环形缓冲容量(ENC_IN_SIZE * 6)需与新格式帧长匹配。
  5. 关闭后任务复用: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)的系统解码配置进一步确认。

Prev
Mesh 模型与 Mesh DFU 固件升级