前言
现在为止也开发了许多杰理和中科蓝讯TWS蓝牙耳机、音响项目 SDK的案子,在调试案子时不断的向前辈们学习到了很多关于蓝牙音响、蓝牙TWS耳机专业的知识。想在这里做一个学习汇总,方便各位同行和对杰理芯片SDK感兴趣的小伙伴们学习;
本章详细讲解杰理可视化SDK开发-音频录制功能使用教程
今天遇到声学工程师要求给TWS耳机开启音频录制功能给他用于分析耳机双麦之间的差异;正好借这个机会出一个关于杰理可视化SDK开启音频录制功能的教程;
一、这个功能到底是干什么的?
用一句话概括:音频录制功能是一个"调试用数据抓取通道"------它把耳机麦克风实时采集到的原始声音数据,原封不动地导出到外部(上位机 / SD 卡 / 串口),供工程师在 PC 端做分析。
它最常见的三个用途:
| 用途 | 说明 |
|---|---|
| 双麦 ENC 降噪调试 | 把左、右两个麦克风的原始数据导出来,核对降噪算法输入是否正确 |
| 通话算法(CVP/AEC)调试 | 抓取通话过程中的麦克风与参考信号,回放分析回声、噪声 |
⚠️ 注意:这是工程调试功能 ,不是面向消费者的"录音机"功能。它默认是关闭的(
TCFG_AUDIO_DATA_EXPORT_DEFINE为 0),只有需要调试时才打开。
二、如何开启(改一个宏即可)

整个功能的开关集中在一个宏上:TCFG_AUDIO_DATA_EXPORT_DEFINE。
2.1 找到配置位置
文件:apps/earphone/include/app_config.h(第 607~616 行)
cpp
/*Audio数据导出配置:通过蓝牙spp导出/sd写卡导出/uart写卡导出*/
#define AUDIO_DATA_EXPORT_VIA_UART 1
#define AUDIO_DATA_EXPORT_VIA_SPP 2
#define AUDIO_DATA_EXPORT_VIA_SD 3
#define TCFG_AUDIO_DATA_EXPORT_DEFINE AUDIO_DATA_EXPORT_VIA_SPP//DISABLE_THIS_MOUDLE
#if TCFG_DATA_EXPORT_NODE_ENABLE
#undef TCFG_AUDIO_DATA_EXPORT_DEFINE
#define TCFG_AUDIO_DATA_EXPORT_DEFINE AUDIO_DATA_EXPORT_VIA_UART
#endif
2.2 三种取值,任选其一
把 TCFG_AUDIO_DATA_EXPORT_DEFINE 改成下面三个值中的一个,就对应一种导出方式:
| 取值 | 含义 | 数据去向 |
|---|---|---|
AUDIO_DATA_EXPORT_VIA_UART(=1) |
串口导出 | 耳机 UART1 串口 → 串口工具 |
AUDIO_DATA_EXPORT_VIA_SPP(=2) |
蓝牙 SPP 导出 | 耳机 SPP → 上位机(JL Studio/测试工具) |
AUDIO_DATA_EXPORT_VIA_SD(=3) |
SD 卡写卡导出 | 耳机 → SD 卡文件 |
2.3 开启步骤(以 SPP 导出为例)
-
打开
apps/earphone/include/app_config.h; -
将
TCFG_AUDIO_DATA_EXPORT_DEFINE改为目标方式,例如:cpp#define TCFG_AUDIO_DATA_EXPORT_DEFINE AUDIO_DATA_EXPORT_VIA_SPP -
重新编译并烧录耳机固件;
-
若选 SPP 方式,用上位机(JL Studio 等)通过蓝牙 SPP 连接耳机,下发"开始录制"命令即可收到麦克风数据。
提示:
TCFG_DATA_EXPORT_NODE_ENABLE一旦为真,会强制 把导出方式切回 UART(见 2.1 的第二个#if),这一点在切换方式时需要留意。
三、三种导出方式深入对比
3.1 方式一:串口导出(UART,宏值 1)
数据流 :麦克风 → CVP/AEC 通话处理模块内部 → uartSendData() → UART1 → 串口调试工具。
入口(关键代码):
-
audio/common/audio_general.c:108-112------ 初始化阶段调用uartSendInit(),打开并初始化 UART1:cppint audio_general_init() { #if ((defined TCFG_AUDIO_DATA_EXPORT_DEFINE) && (TCFG_AUDIO_DATA_EXPORT_DEFINE == AUDIO_DATA_EXPORT_VIA_UART)) uartSendInit(); #endif ... } -
audio/CVP/audio_cvp.c:76-81------ 打开 AEC 串口数据导出开关:cpp#if ((defined TCFG_AUDIO_DATA_EXPORT_DEFINE) && (TCFG_AUDIO_DATA_EXPORT_DEFINE == AUDIO_DATA_EXPORT_VIA_UART)) /*AEC串口数据导出*/ const u8 CONST_AEC_EXPORT = 1; #else const u8 CONST_AEC_EXPORT = 0; #endif -
audio/common/uartPcmSender.c------ 发送实现:uartSendInit()初始化 UART1(波特率PCM_UART1_BAUDRATE、DMA),uartSendData()用 DMA 阻塞式发送。
特点:
- 不关闭 TWS/BLE/A2DP,也不创建音频采集任务(
audio_capture_init不被调用)。 - 数据直接由 AEC 模块内部导出到串口,链路最"轻"。
- 需要接串口线,适合桌面联调,不适合无线场景。
3.2 方式二:蓝牙 SPP 导出(宏值 2,最常用)
数据流:
cpp
麦克风 ADC 采集
→ adc_mic_output() 拆出 mic0/mic1(/mic2) 各路数据
→ 写入环形缓冲 mic0_cb / mic1_cb / mic2_cb
→ AudioCapture_Task 取出、按通道打包(4字节序号 + 640字节数据)
→ spp_data_export() → online_db 协议打包
→ SPP 蓝牙通道 → 上位机
入口(关键代码):
-
apps/earphone/mode/bt/earphone.c:472-480------ 开机流程里:SPP 方式先关闭蓝牙角色切换,再初始化音频采集:cpp#if TCFG_AUDIO_DATA_EXPORT_DEFINE #if (TCFG_AUDIO_DATA_EXPORT_DEFINE == AUDIO_DATA_EXPORT_VIA_SPP) lmp_hci_set_role_switch_supported(0); // 关闭角色切换 #endif #if ((TCFG_AUDIO_DATA_EXPORT_DEFINE == AUDIO_DATA_EXPORT_VIA_SPP) \ || (TCFG_AUDIO_DATA_EXPORT_DEFINE == AUDIO_DATA_EXPORT_VIA_SD)) audio_capture_init(); #endif #endif -
apps/earphone/include/app_config.h:842-857------ SPP 方式下强制关闭 TWS/BLE/A2DP、打开 SPP 与在线调试(详见第五节)。

audio/common/online_debug/audio_capture.c:376-406------data_export_init()里,SPP 方式调用aec_data_export_init(SPP_EXPORT_CH)。audio/CVP/audio_cvp_online.c:693-733------ 注册命令解析aec_online_parse(),处理上位机下发的:AEC_RECORD_COUNT(0x200):查询导出通道数AEC_RECORD_START:调用audio_capture_start()开始录音AEC_RECORD_STOP:调用audio_capture_stop()停止录音ONLINE_OP_QUERY_RECORD_PACKAGE_LENGTH:查询每包长度(回复 644)
audio/CVP/audio_cvp_online.c:758-780------spp_data_export():录音状态下把数据按通道用app_online_db_send_more()发出去。
特点:
- 由上位机控制启停 :耳机开机只做好采集准备,收到上位机
AEC_RECORD_START命令后才真正启动麦克风采集。 - 采样率 16 kHz(
AC_SAMPLE_RATE),支持双麦(2 路)或三麦(3 路)导出。 - 每包结构:4 字节序号 + 640 字节数据 = 644 字节 (
SPP_EXPORT_MTU)。 - 需要关闭 TWS/BLE/A2DP,是最常用的无线调试方式。
3.3 方式三:SD 卡写卡导出(宏值 3)
数据流:
cpp
麦克风 ADC 采集
→ adc_mic_output() → 环形缓冲
→ AudioCapture_Task 取出 → data_export_run() 按通道写入缓冲
→ DataExport_Task → force_write_sd() 直接写 SD 卡扇区
入口(关键代码):
-
audio/common/online_debug/audio_capture.c:818-832------ SD 方式下开机自动开始录音 (无需上位机):cppstatic void audio_capture_board_init() { led_init(); data_export_init(); ... #if (TCFG_AUDIO_DATA_EXPORT_DEFINE == AUDIO_DATA_EXPORT_VIA_SD) audio_capture_start(); // SD 导出:默认自动启动声音捕获 #endif dm.state = AC_STATE_INIT; } -
audio/common/online_debug/audio_capture.c:376-397------data_export_init()里,SD 方式:给 SD 卡供电(RT9193,JL_PORTB第 3 脚)→force_open_sd()打开 SD → 创建DataExport_Task写卡任务。 -
audio/common/online_debug/audio_capture.c:216-240------data_export_run()把数据写入写卡缓冲。 -
audio/common/online_debug/audio_capture.c:338-355------DataExport_Task用force_write_sd()按扇区写卡,起始扇区SD_START_BLOCK = 50000,每次写 512 字节(SD_PCM_BUF_SIZE)。
特点:
- 完全离线:不需要蓝牙连接、不需要上位机,开机即录。
- 数据直接写 SD 卡,适合现场长时间抓数据、事后分析。
- 不关闭 TWS/BLE(earphone.c 里 SD 分支只调用
audio_capture_init,不关角色切换)。
3.4 一张表看清三种方式
| 对比项 | UART(1) | SPP(2) | SD(3) |
|---|---|---|---|
| 数据去向 | UART1 串口 | 蓝牙 SPP → 上位机 | SD 卡 |
| 是否创建采集任务 | 否 | 是 | 是 |
| 是否关闭 TWS/BLE/A2DP | 否 | 是 | 否 |
| 是否需要上位机/蓝牙 | 需接串口 | 需 SPP 连接 | 不需要 |
| 录音启停 | 随 AEC 导出 | 上位机命令控制 | 开机自动 |
| 数据格式 | AEC 内部格式 | 4 字节序号 + 640 数据 | 512 字节/扇区(可加 12 字节头) |
| 典型场景 | 桌面串口联调 | 无线在线调音/调 ENC | 现场离线抓数 |
四、代码运行骨架(内部是怎么跑起来的)
本节以最常用的 SPP 导出 为主线,SD/UART 只标差异。所有函数与行号均在 audio/common/online_debug/audio_capture.c,除非特别说明。
4.1 开机初始化链路(从开机到"录音就绪")
cpp
开机 power on
│ apps/earphone/mode/bt/earphone.c:472-480
▼
[TCFG_AUDIO_DATA_EXPORT_DEFINE 生效]
├─ SPP 方式:lmp_hci_set_role_switch_supported(0) // 关角色切换
└─ SPP/SD 方式:audio_capture_init() // audio_capture.c:996
├─ os_sem_create(&dm.sem, 0) // 建信号量
└─ task_create(AudioCapture_Task, "aud_capture") // 建采集任务
│ audio_capture.c:947
▼
AudioCapture_Task
└─ audio_capture_board_init() // audio_capture.c:818
├─ led_init()
├─ data_export_init() // audio_capture.c:376
│ ├─ SD 方式:给卡供电(RT9193/PB3) + force_open_sd()
│ │ + task_create(DataExport_Task, "data_export")
│ └─ SPP 方式:aec_data_export_init(SPP_EXPORT_CH)
│ → app_online_db_register_handle(DB_PKT_TYPE_EXPORT, aec_online_parse)
└─ SD 方式:audio_capture_start() // 开机自动开始
▼
主循环(见 4.2/4.3)
SPP 的蓝牙通道初始化是另一条并行链路(在蓝牙协议栈初始化时完成):
cpp
APP_ONLINE_DEBUG = 1 // app_config.h:854
└─ BT_AI_SEL_PROTOCOL |= ONLINE_DEBUG_EN // app_config.h:879-886
└─ earphone.c:497 调用 multi_protocol_bt_init()
└─ multi_protocol_main.c:378-380
#if (BT_AI_SEL_PROTOCOL & ONLINE_DEBUG_EN)
online_spp_init(); // spp_online_db.c:113
├─ app_spp_hdl_alloc() // 分配 SPP 通道句柄
├─ app_spp_hdl_uuid_set() // 设服务 UUID
├─ app_spp_recieve_callback_register() // 收数据回调
└─ app_spp_state_callback_register() // 连接状态回调
#endif
4.2 SPP 方式:数据流 + 命令流
命令流(上位机 → 耳机),用于"开始/停止录音":
cpp
上位机 --SPP--> online_spp_recieve_cbk() // spp_online_db.c:100
└─ db_api->packet_handle() → aec_online_parse() // audio_cvp_online.c:693
├─ AEC_RECORD_COUNT(0x200) → 回复通道数
├─ AEC_RECORD_START → audio_capture_start() // 真正启动麦克风
├─ AEC_RECORD_STOP → audio_capture_stop()
└─ QUERY_RECORD_PACKAGE_LENGTH → 回复每包长度 644
audio_capture_start()(audio_capture.c:834)内部做三件事:
- 设置采样率
dm.sr = 16000,初始化 3 个环形缓冲mic0_cb / mic1_cb / mic2_cb; - 麦克风上电 +
audio_mic_init(16000):打开 ADC,把回调adc_mic_output挂到 ADC 输出上; - (定时器模式)创建
aec_export_timer定时器,周期SPP_SEND_INTERVAL(双麦 6ms / 三麦 4ms)。
数据流(耳机 → 上位机):
cpp
麦克风 → ADC 中断
└─ adc_mic_output() // audio_capture.c:432
├─ 双麦:把交织数据拆成 mic0 / mic1
├─ 三麦:拆成 mic0 / mic1 / mic2
└─ cbuf_write(mic0_cb / mic1_cb / mic2_cb)
└─ os_sem_post(&dm.sem)
定时器 aec_export_timer() // audio_capture.c:802,每 6ms/4ms
└─ spp_data_export_run() // audio_capture.c:720
├─ 按 ch_idx 轮询 0/1/2 三路
├─ 每路:cbuf_read 640 字节 → export_buf[4..643]
├─ export_buf[0..3] 写入 4 字节序号(seqn)
└─ spp_data_export(ch, export_buf, 644) // audio_cvp_online.c:758
└─ app_online_db_send_more(DB_PKT_TYPE_DAT_CH0/1/2, ...)
└─ online_db 协议打包 → app_spp_data_send() → SPP → 上位机
关于"定时器模式":
audio_capture.c:110定义SPP_SEND_BY_TIMER = 1(默认)。
- 为 1 时:由
aec_export_timer定时器驱动发送(上文流程);- 为 0 时:改为由 ADC 中断
os_sem_post唤醒AudioCapture_Task,在任务里直接调spp_data_export_run()(audio_capture.c:989)。
4.3 SD 方式:数据流
cpp
audio_capture_board_init() → audio_capture_start()(开机自动)
└─ 麦克风 ADC → adc_mic_output() → cbuf
AudioCapture_Task 主循环(audio_capture.c:956-968)
└─ 当 mic0_cb、mic1_cb 都凑满 512 字节(DM_RUN_SIZE):
├─ 读 mic0 → data_export_run(mic0, 512, ch0)
├─ 读 mic1 → data_export_run(mic1, 512, ch1)
├─ DualMic_NoiseReduce_run() 双麦降噪
└─ data_export_run(output, ..., ch2)
data_export_run()(audio_capture.c:216)→ 写入写卡缓冲
DataExport_Task(audio_capture.c:338)
└─ 缓冲满 512 字节 → force_write_sd(buf, 扇区, 1) 写卡
4.4 UART 方式:数据流
cpp
audio_general_init()(audio_general.c:108)
└─ uartSendInit()(uartPcmSender.c:49)初始化 UART1 + DMA
CVP/AEC 模块(CONST_AEC_EXPORT = 1,audio_cvp.c:78)
└─ uartSendData(buf, len)(uartPcmSender.c:20)
└─ DMA 拷贝 + uart_send_blocking() 串口发送
五、开启后耳机软件发生了什么变化
"改一个宏"背后,编译器通过条件编译改变了整个固件的功能形态 。下面按方式分别说明(均对应 apps/earphone/include/app_config.h,除非标注)。
5.1 SPP 方式(变化最大)
SPP 方式会大幅收窄蓝牙功能,因为它把耳机临时变成了一台"SPP 数据采集机":
| 变化项 | 值 | 出处 |
|---|---|---|
| 关闭 TWS(双耳互联) | TCFG_USER_TWS_ENABLE = 0 |
app_config.h:849 |
| 关闭 BLE | TCFG_USER_BLE_ENABLE = 0 |
app_config.h:850 |
| 关闭 A2DP(音乐) | TCFG_BT_SUPPORT_A2DP = 0 |
app_config.h:853 |
| 连接设备数改为 1 | TCFG_BD_NUM = 1 |
app_config.h:851 |
| 打开经典蓝牙 SPP | TCFG_BT_SUPPORT_SPP = 1 |
app_config.h:852 |
| 打开在线调试 | APP_ONLINE_DEBUG = 1 |
app_config.h:854 |
| 关闭角色切换 | lmp_hci_set_role_switch_supported(0) |
earphone.c:474 |
| 协议选择加入 ONLINE_DEBUG | `BT_AI_SEL_PROTOCOL | = ONLINE_DEBUG_EN` |
由此带来更深层的连锁反应:
-
新增在线调试协议层 :
APP_ONLINE_DEBUG = 1使online_db协议层被编译进来,负责把数据打包成 SPP 协议包。 -
SPP 通道注册 :
online_spp_init()分配专用 SPP 句柄并注册收发回调(spp_online_db.c:113)。 -
内存占用增加 :发送缓冲
DB_MEM_BUF_LEN从默认的768字节增加到768 * 15 = 11520字节(online_db_deal.c:26-30),因为录音数据量远大于普通调试。 -
新增两个任务 :
aud_capture(优先级 4)和data_export(优先级 5),在apps/earphone/app_main.c:88-91注册:cpp#if TCFG_AUDIO_DATA_EXPORT_DEFINE {"aud_capture", 4, 0, 512, 256 }, {"data_export", 5, 0, 512, 256 }, #endif -
麦克风持续采集:收到上位机"开始录音"命令后,ADC 以 16 kHz 采样率持续采集 2~3 路麦克风并发送。
一句话总结:SPP 方式 = 关闭 TWS/BLE/A2DP + 打开 SPP/在线调试 + 常驻两个采集任务 + 增加约 11KB 发送缓冲。这也是为什么它必须"独占"------TWS 双耳互联与 SPP 调试通道会争抢蓝牙资源。
5.2 UART 方式(变化最小)
- 不关闭 TWS/BLE/A2DP,不创建采集任务;
- 仅打开 UART1(占用
PCM_UART1_TX_PORT/RX_PORT引脚,波特率PCM_UART1_BAUDRATE),并启用 DMA 发送(uartPcmSender.c:56-71); - 锁定系统时钟到最高频率(
clock_lock("sys", clk_get_max_frequency()),uartPcmSender.c:74); - 打开 AEC 串口导出开关
CONST_AEC_EXPORT = 1(audio_cvp.c:78)。
5.3 SD 方式
- 不关闭 TWS/BLE/A2DP,但会创建采集任务与写卡任务;
- 开机即给 SD 卡供电(RT9193,PB3 引脚)并打开 SD 设备(audio_capture.c:380-383);
- 开机自动开始录音(audio_capture.c:827-829),无需上位机;
- 数据直接写 SD 卡扇区(起始扇区 50000)。
5.4 三种方式"软件形态"对比
| 变化项 | UART | SPP | SD |
|---|---|---|---|
| 关闭 TWS/BLE/A2DP | ❌ | ✅ | ❌ |
| 打开 SPP / 在线调试 | ❌ | ✅ | ❌ |
| 新增采集/导出任务 | ❌ | ✅(2 个) | ✅(2 个) |
| 麦克风持续采集 | 由 AEC 内部驱动 | 上位机命令驱动 | 开机自动 |
| 额外硬件/外设 | UART1 + DMA | 蓝牙 SPP | SD 卡 + 供电 |
| 内存增长 | 几乎无 | 约 +11KB 发送缓冲 | SD 写缓冲 |
六、关键源码文件索引
| 文件 | 作用 |
|---|---|
apps/earphone/include/app_config.h:607-616 |
导出方式选择宏(总开关) |
apps/earphone/include/app_config.h:618-626 |
SPP 导出的麦克风通道配置 |
apps/earphone/include/app_config.h:842-886 |
SPP 方式下 TWS/BLE/A2DP/在线调试配置 |
apps/earphone/mode/bt/earphone.c:472-480 |
开机入口:关角色切换 + 采集初始化 |
apps/earphone/app_main.c:88-91 |
采集/导出任务注册表 |
audio/common/online_debug/audio_capture.c |
采集任务、ADC 回调、SD 写卡、SPP 发送(核心) |
audio/CVP/audio_cvp_online.c:658-803 |
SPP 命令解析 + 数据发送 + 导出初始化 |
apps/common/third_party_profile/jieli/online_db/spp_online_db.c |
online_db 的 SPP 通道收发 |
apps/common/third_party_profile/jieli/online_db/online_db_deal.c |
online_db 协议层(打包/缓冲/应答) |
apps/common/third_party_profile/multi_protocol_main.c:378-380 |
online_spp_init() 调用点 |
audio/common/audio_general.c:108-112 |
UART 导出初始化入口 |
audio/CVP/audio_cvp.c:76-81 |
UART 导出:AEC 串口导出开关 |
audio/common/uartPcmSender.c |
七、蓝牙串口SPP音频录制功能调试
1、打开工具,选择"音频录制",然后"确定"。

2、进入蓝牙连接界面,操作如下(如果有两个蓝牙标准串口,一个不行,则换另一个)

3、 进入录制界面

制作不易!喜欢的小伙伴给个小赞赞!喜欢我的小伙伴点个关注!有不懂的地方和需要的资源随时问我哟!