音效算法(ANS、变调、变声、混响)
AD24N SDK 中 sound_effect_list 音效算法集合的核心 DSP 效果:ANS 自适应降噪、变调(含移频啸叫抑制)、变声与混响/回声,以及它们如何通过 sound 链机制接入录音与播放数据通路。
Purpose and Scope
本文档说明 AD24N SDK 中音效算法的实现方式、接入链路与配置方法,覆盖以下四类效果:
- ANS(Adaptive Noise Suppression):基于
NoiseSuppressLib库的自适应降噪,挂在录音/编码链路上。 - 变调(Pitch Shift):
vo_pitch变调器与pitch_howling移频变调(抑制啸叫),挂在扬声器/对讲链路上。 - 变声(Voice Changer):
voicechanger.c实现的音高/共振峰/语速/颤音综合变声效果。 - 混响/回声(Echo / Reverb):
echo_api.c回声效果。
同属 sound_effect_list 但不在本文范围内的音效(EQ、啸叫陷波、变速、能量检测)由各自的 Wiki 页面覆盖:pcm_eq(EQ)、notch_howling(陷波啸叫抑制)、speed(变速)、energe_detect(能量检测)。本文只引用它们所在的目录结构,不做展开。
Overview
AD24N 的音频处理采用 sound 链(sound chain) 设计:每一级音效都是一个 sound_out_obj / EFFECT_OBJ 节点,通过 link_*_sound() 函数把前级与后级串接起来,数据以短整型 PCM 块为单位流经各节点。效果算法普遍以 静态缓冲区 + 固定块长(如 128 点) 的方式运行,缓冲区通过 AT(.xxx_data) 指定到专用内存段(如 .ans_data、.voicechanger_data),避免运行期动态分配,从而适配 MCU 上无 MMU、内存受限的实时 DSP 场景。
四条主要接入路径:
- 录音/编码路径:
encoder_api.c在编码器启动时把 ANS 链接进 ADC 数据流,用于消除环境底噪。 - 扬声器路径:
speak_api.c按编译宏依次链接移频变调(啸叫抑制)、变声等效果,最后送入 DAC。 - 效果参数:部分效果(如变声)支持运行时通过
update_*_parm()重开算法实例来热更新参数。 - 编译开关:每个效果都有独立宏(
ANS_EN、VO_PITCH_EN、PITCHSHIFT_HOWLING_EN、HOWLING_EN、VO_CHANGER_EN等),未使能时相关代码与数据段全部不参与编译。
Architecture
flowchart TD
subgraph sg_RecPath["录音/编码链路"]
ADC["ADC 采集"] --> ENC["encoder_api.c 编码器"]
ENC -->|"link_ans_sound()"| ANS["ANS<br/>ans/ans_api.c"]
ANS --> OUT["编码输出"]
end
subgraph sg_SpkPath["扬声器/对讲链路"]
SRC["音频源(解码/ADC)"] --> PITCH["移频变调(啸叫抑制)<br/>pitch_howling/"]
PITCH --> VC["变声<br/>voice_change/voicechanger.c"]
VC --> ECHO["混响/回声<br/>echo/echo_api.c"]
ECHO --> DAC["DAC 输出"]
end
subgraph sg_EffectList["sound_effect_list 音效库"]
ANS
PITCH
VC
ECHO
EQ["pcm_eq(EQ)"]
NH["notch_howling"]
SPD["speed(变速)"]
ED["energe_detect"]
end
ADC -->|"ANS_EN 使能"| ANS
SRC -->|"HOWLING_EN 使能"| PITCH
架构说明:
- sound_effect_list 是统一的音效存放目录,每个子目录对应一类效果;每个效果模块通常暴露一个
link_xxx_sound()链接函数 + 一个xxx_api()初始化函数 + 一个xxx_run()处理函数,三者配合完成"接入链 → 初始化 → 逐块处理"。 - ANS 只挂在录音/编码链路(
encoder_api.c),因为它的输入是 ADC 采样率(8 kHz / 16 kHz),目标是降低录音信号底噪。 - 变调/变声/混响挂在扬声器链路(
speak_api.c),其中pitch_howling用移频方式把啸叫频率搬移出反馈回路,voicechanger改变人声特性,echo叠加回声营造空间感。 - 数据以 PCM 块为单位逐级流动,每级处理完通过
remain_output机制缓存未消费完的数据,保证下一级能以固定块长取数。
音效链机制:link 函数、EFFECT_OBJ 与 remain_output
所有音效共用的运行时骨架:
EFFECT_OBJ:效果对象,含run函数指针与sound(sound_out_obj)节点。初始化时把sound.p_obuf指向前级输出缓冲,并把*ppsound指向本节点,从而把自身插入链中。link_*_sound():调用xxx_api()创建效果;成功则置enable |= B_DEC_EFFECT并返回后级节点,失败则保持原链不变并打印init fail日志。remain_output/set_remain_len:块式效果的"剩余数据"管理。当本帧数据不足以填满固定块长(如 ANS 的 128 点)时,先输出上一帧的剩余数据;若上一帧仍未输出完则直接返回 0(背压),避免把不完整块交给后级。
这一骨架保证:每一级效果都按自己的固定块长消费输入、按自己的节奏吐数据,链路任意一级繁忙时自动背压,不会丢数据或错位。
ANS 自适应降噪实现详解
ANS 模块位于 sdk/app/bsp/common/sound_effect_list/ans/ans_api.c,核心算法封装在 NoiseSuppressLib(sdk/include_lib/ans/NoiseSuppressLib.h),本文件负责参数、缓冲区与数据流的桥接。
固定块长与内存布局
#define READSIZE 128 //每次run处理样点数
#define ANS_RUN_BUFFSIZE 5400
#define ANS_TMP_BUFSIZE 3604
#define ANS_NEAR_SIZE (READSIZE * 2)
static int ans_runbuf[ANS_RUN_BUFFSIZE / 4] AT(.ans_data);
static int ans_tmpbuf[ANS_TMP_BUFSIZE / 4] AT(.ans_data);
static short ans_output_buff[READSIZE] AT(.ans_data);
static remain_ops ans_remain_ops AT(.ans_data);
static EFFECT_OBJ ans_effect_obj AT(.ans_data);
Source: ans_api.c
设计意图:READSIZE = 128 是 ANS 算法库单次处理的最小样点块;所有中间缓冲(运行缓冲、临时缓冲、输出缓冲、剩余数据管理结构、效果对象)都声明为 static 并放入 .ans_data 段,启动时由链接脚本统一规划,运行时零动态分配。
逐块处理:ans_run()
int ans_run(void *hld, short *inbuf, int len)
{
u32 rlen = 0;
remain_ops *p_ans_remain_ops = &ans_remain_ops;
/* 0. output 上次剩余的数据 */
remain_output(&ans_effect_obj.sound, p_ans_remain_ops);
if (p_ans_remain_ops->remain_len) { //上次数据输出仍旧没输出完
return 0;
}
/* 1. 新一轮输入输出 */
memset(ans_output_buff, 0, sizeof(ans_output_buff)); //清空outdata
/* 2. input 数据 */
if (len < READSIZE * sizeof(short)) {
/* 本次输入数据不够128个点 */
return 0;
}
/* 3. 运算run */
NoiseSuppress_Process(ans_runbuf, ans_tmpbuf, inbuf, ans_output_buff, NULL, NULL, READSIZE);
/* 4. 设置需要 output 数据量 */
set_remain_len(p_ans_remain_ops, READSIZE * sizeof(short)); //设置output需要输出一包的数据量
/* 5. 输出 */
remain_output(&ans_effect_obj.sound, p_ans_remain_ops);
return READSIZE * sizeof(short); //成功返回读取byte长度
}
Source: ans_api.c
处理顺序(对应注释中的 0~5 步):
- 先尝试把上一帧未送出的数据交给后级;若
remain_len非零说明后级仍在消化,直接返回 0 形成背压。 - 清空输出缓冲,保证算法库不产生任何旁路残留。
- 输入不足 256 字节(128 个 short)时直接返回 0,等待攒够一帧。
- 调用
NoiseSuppress_Process()完成 128 点降噪,输入输出均为 short。 - 把 256 字节登记为待输出长度并立即输出,随后返回本次消耗的字节数(
READSIZE * sizeof(short))。
返回值语义:返回本次从输入中消费的字节数,上游据此推进数据指针;返回 0 表示本次没有消费。
初始化与参数:ans_api()
void *ans_api(void *obuf, void **ppsound, u32 sr)
{
const u32 ans_supprt_sr[2] = {8000, 16000};
if (sr != ans_supprt_sr[NS_IS_WIDEBAND]) {
log_error("ans not support curr sr %d\n", sr);
return NULL;
}
int tolbufsize = NoiseSuppress_QueryBufSize(NS_MODE, NS_IS_WIDEBAND);;
ASSERT(ANS_RUN_BUFFSIZE >= tolbufsize);
int maxtmpbufsize = NoiseSuppress_QueryTempBufSize(NS_MODE, NS_IS_WIDEBAND);
ASSERT(ANS_TMP_BUFSIZE >= maxtmpbufsize);
int ANS_AggressFactor = (int)(125 * 65536 / 100);/*范围:1~2,动态调整,越大越强(1.25f)*/
int ANS_MinSuppress = (int)(10 * 65536 / 100); /*范围:0~1,静态定死最小调整,越小越强(0.1f)*/
int ANS_NoiseLevel = (int)(1429 * 1024); /*范围:-100dB ~ -40dB (-75dB) (1429 = (10^(-75/20))*2^23)*/
NoiseSuppress_Init(ans_runbuf, ANS_AggressFactor, ANS_MinSuppress, NS_MODE, NS_IS_WIDEBAND, ANS_NoiseLevel);
return ans_phy(obuf, ppsound);
}
Source: ans_api.c
关键点:
- 采样率强约束:
NS_IS_WIDEBAND = 1表示宽带回声消除模式,仅支持 16 kHz;非 16 kHz 直接返回 NULL 并打错误日志(NS_MODE = 0选择窄带 8 kHz 模式需改此宏)。初始化失败时link_ans_sound会打印ans init fail并把链路保持原样。 - 参数以 Q 格式定点传递:
ANS_AggressFactor = 1.25(1.25×65536)、ANS_MinSuppress = 0.1、ANS_NoiseLevel = -75 dB转成 2^23 定点(1429×1024)。这些是算法库的内部定点约定,调整时需保持同一 Q 格式。 - 缓冲区大小运行时校验:
NoiseSuppress_QueryBufSize/QueryTempBufSize返回库所需字节数,用ASSERT保证静态数组足够大;若换库版本导致需求增大,会立即暴露问题而非运行期越界。
录音路径接入:encoder_api.c
#if (defined(ANS_EN) && (ANS_EN))
cbuf_init(&cbuf_ans, &ans_buff[0], sizeof(ans_buff)); //cbuf_ans 为link
p_curr_sound = link_ans_sound(p_curr_sound, &cbuf_ans, read_audio_adc_sr());
#endif
Source: encoder_api.c
编码器启动时若 ANS_EN 使能,先初始化 512 字节的环形缓冲 cbuf_ans(同样放 .ans_data),再把 ANS 效果链接进编码器输入链。read_audio_adc_sr() 提供实际 ADC 采样率,用于 ans_api 的 16 kHz 校验。链接函数本身:
void *link_ans_sound(void *p_sound_out, void *p_ans_obuf, u32 sr)
{
sound_out_obj *p_next_sound = 0;
sound_out_obj *p_curr_sound = p_sound_out;
p_curr_sound->effect = ans_api(p_ans_obuf, (void **)&p_next_sound, sr);
if (NULL != p_curr_sound->effect) {
p_curr_sound->enable |= B_DEC_EFFECT;
p_curr_sound = p_next_sound;
log_info("ans init succ\n");
} else {
log_info("ans init fail\n");
}
return p_curr_sound;
}
Source: ans_api.c
成功时置 B_DEC_EFFECT 标志(表示该节点带解码后处理效果),返回后级节点让链继续;失败时不破坏原链。文件末尾还导出了算法库需要的 STFT 窗函数表 STFT_Win_FixHalf_M256_D128[](256 点窗 / 128 点跳),由链接脚本放入常量区。
变调(Pitch Shift)
变调相关实现位于两个模块:
vo_pitch/vo_pitch_api.c(宏VO_PITCH_EN):人声/音乐变调效果,通过改变回放采样相位实现音调搬移。pitch_howling/howling_pitchshifter_api.c+pitch_howling_phy.c(宏PITCHSHIFT_HOWLING_EN/HOWLING_EN):移频抑制啸叫——把扬声器信号整体搬移一个微小频率偏移,破坏拾音反馈回路中的正反馈相位条件,从而抑制啸叫,同时人耳几乎无感。
扬声器路径中的接入点(speak_api.c):
#if defined(HOWLING_EN) && (HOWLING_EN) //移频抑制啸叫
p_curr_sound = link_pitchshift_howling_sound(p_curr_sound, &cbuf_ads_o, 0, adc_sr);
#endif
Source: speak_api.c
与 ANS 的 link_ans_sound 相同,link_pitchshift_howling_sound 接受"当前链节点 + 输出缓冲 + 采样率",成功后返回后级节点。PITCHSHIFT_HOWLING_EN 与 HOWLING_EN 分别控制编译与运行使能,注释 移频抑制啸叫 明确其用途。
变声(Voice Changer)实现详解
变声模块位于 sdk/app/bsp/common/sound_effect_list/voice_change/voicechanger.c,基于 voiceChanger_av_api.h 提供的 get_voiceChangerA_func_api() 算法接口,可同时控制音高(shiftv)、共振峰(formant_shift)、语速(speedv)、预设音效(effect_v),并叠加**颤音合成(VOICESYN)**参数。
默认参数与初始化
void *voice_changer_api(void *obuf, u32 sr, void **ppsound)
{
vc_parm.shiftv = 65;
vc_parm.formant_shift = 100;
vc_parm.speedv = 80;
vc_parm.effect_v = EFFECT_VC_AV_BIRD5;
vs_parm.randpercent = 100;
vs_parm.vibrate_lenCtrol = 30;
vs_parm.vibrate_rate_u = 0;
vs_parm.vibrate_rate_d = 100;
return voice_changer_phy(obuf, sr, &vc_parm, &vs_parm, ppsound);
}
Source: voicechanger.c
默认配置:音高 shiftv=65、共振峰 formant_shift=100(100% 不变形)、语速 speedv=80、预置音效 EFFECT_VC_AV_BIRD5(鸟鸣 5 号);颤音合成默认随机百分比 100%、颤音时长控制 30、上/下颤音速率 0/100。vc_parm 与 vs_parm 存放在 .voicechanger_data 段,2560 字(0x2010 字节)的工作缓冲 buflen 也固定在该段。
算法实例的打开与运行
void *voice_changer_phy(void *obuf, u32 sr, VOICECHANGER_AV_PARM *pvc_parm, VOICESYN_AV_PARM *pvs_parm, void **ppsound)
{
u32 need_buff_len;
VOICECHANGER_A_FUNC_API *ops;
ops = get_voiceChangerA_func_api();
need_buff_len = ops->need_buf(sr, pvc_parm);
if (need_buff_len > sizeof(buflen)) {
log_error("buff_len not enough, need 0x%x\n", need_buff_len);
return 0;
}
ops->open(&buflen[0], sr, pvc_parm, pvs_parm, (void *)&vc_pitch_io);
vc_sr = sr;
...
vchange_obj.p_si = &vchange_si;
vchange_obj.run = voice_changer_run;
vchange_obj.sound.p_obuf = obuf;
*ppsound = &vchange_obj.sound;
return &vchange_obj;
}
Source: voicechanger.c
要点:
- 缓冲区需求前置校验:
ops->need_buf(sr, parm)返回算法需要的字节数,超过静态buflen则报错退出,不初始化。 - IO 上下文绑定:
vc_pitch_io把算法输出绑定到vchange_obj.sound与sound_output,算法库通过该回调把处理结果推入 sound 链。 - 运行时对象
vchange_obj、vchange_si均为静态/全局,run函数voice_changer_run通过sound_in_obj中转调用ops->run(p_dbuf, inbuf, len)。
运行时热更新参数
void update_voice_changer_parm(VOICECHANGER_AV_PARM *new_vc_parm, VOICESYN_AV_PARM *new_vsyn_ctrol)
{
if ((NULL == new_vc_parm) || (NULL == new_vsyn_ctrol)) {
return;
}
VOICECHANGER_A_FUNC_API *ops;
ops = get_voiceChangerA_func_api();
OS_ENTER_CRITICAL();
ops->open(&buflen[0], vc_sr, new_vc_parm, new_vsyn_ctrol, NULL);
OS_EXIT_CRITICAL();
}
Source: voicechanger.c
运行时修改参数的方式是在临界区内用新参数重新调用 ops->open()(重开算法实例)。OS_ENTER_CRITICAL/OS_EXIT_CRITICAL 保证重开过程不会被音频中断打断,避免算法内部状态不一致;NULL 参数直接忽略。这是 MCU 场景下"无锁热更新 DSP 参数"的典型做法——不用队列、不用双缓冲,靠关中断换取原子性。
扬声器链路接入
void *link_voice_changer_sound(void *p_sound_out, void *p_dac_cbuf, void **pp_effect, u32 in_sr)
{
sound_out_obj *p_next_sound = 0;
sound_out_obj *p_curr_sound = p_sound_out;
p_curr_sound->effect = voice_changer_api(p_curr_sound->p_obuf, in_sr, (void **)&p_next_sound);
if (NULL != p_curr_sound->effect) {
if (NULL != pp_effect) {
*pp_effect = p_curr_sound->effect;
}
p_curr_sound->enable |= B_DEC_EFFECT;
p_curr_sound = p_next_sound;
p_curr_sound->p_obuf = p_dac_cbuf;
log_info("voice change init succ\n");
} else {
log_info("voice change init fail\n");
}
return p_curr_sound;
}
Source: voicechanger.c
与 link_ans_sound 相比多了两点:可把效果对象指针回传给调用方(pp_effect,供上层后续调 update_voice_changer_parm 使用),并把后级节点的输出缓冲改绑到 DAC 环形缓冲(p_dac_cbuf),实现"变声后直送扬声器"。文件顶部还定义了 VC_NG_THRES = 712(底噪较大的方案建议值),用于噪声门控阈值参考。
混响 / 回声(Reverb / Echo)
混响效果位于 sdk/app/bsp/common/sound_effect_list/echo/echo_api.c,对外头文件为 sdk/include_lib/audio/echo_api.h 与 sdk/include_lib/audio/reverb_api.h。其与 ANS、变声共用同一套音效链骨架(EFFECT_OBJ + link_*_sound() + remain_output),叠加在扬声器/对讲链路上,用于为语音增加回声/空间感。实现细节(延迟线长度、反馈系数、混合比例等参数)位于算法库二进制中,源码层面未提供参数表,配置需依赖厂商算法库文档。
Core Flow:数据流与关键时序
ANS 块处理时序
sequenceDiagram
participant E as encoder_api.c
participant A as ans_run()
participant R as remain_output
participant NS as NoiseSuppressLib
loop 每帧音频
E->>A: 输入 PCM 块(len)
A->>R: 先输出上一帧剩余数据
R-->>A: remain_len 检查
alt remain_len != 0
A-->>E: 返回 0(背压, 待后级消化)
else len < 256 字节
A-->>E: 返回 0(数据不足一帧)
else
A->>A: 清空 ans_output_buff
A->>NS: NoiseSuppress_Process(128 点)
NS-->>A: 降噪后 128 点
A->>R: set_remain_len(256)
R->>E: 输出 256 字节
A-->>E: 返回 256(已消费字节数)
end
end
变声参数热更新流程
flowchart TD
Start([上层应用/协议栈]) --> P["update_voice_changer_parm()"]
P --> CHK{"new_vc_parm 或<br/>new_vsyn_ctrol 为 NULL?"}
CHK -->|"是"| RET["直接返回(忽略)"]
CHK -->|"否"| OPS["get_voiceChangerA_func_api()"]
OPS --> CS["OS_ENTER_CRITICAL()<br/>关中断"]
CS --> OPEN["ops->open(重开算法实例, 新参数)"]
OPEN --> CE["OS_EXIT_CRITICAL()<br/>开中断"]
CE --> Done([完成, 新参数即刻生效])
扬声器链路整体顺序
flowchart LR
A["音频源"] --> B["移频变调<br/>(啸叫抑制)"]
B --> C["变声<br/>(voicechanger)"]
C --> D["混响/回声<br/>(echo)"]
D --> E["DAC"]
F["link_pitchshift_howling_sound"] --> G["link_voice_changer_sound"]
G --> H["link_echo_sound"]
Configuration Options
编译开关宏
| 宏 | 默认 | 作用域 | 说明 |
|---|---|---|---|
ANS_EN | 关 | 编译期 | 使能 ANS 降噪,并编译 ans_api.c、NoiseSuppressLib |
VO_PITCH_EN | 关 | 编译期 | 使能 vo_pitch 变调模块 |
PITCHSHIFT_HOWLING_EN | 关 | 编译期 | 编译移频啸叫抑制算法(howling_pitchshifter_api.c) |
HOWLING_EN | 关 | 运行期 | 在扬声器链中实际链接移频啸叫抑制 |
VO_CHANGER_EN | 关 | 编译期 | 使能变声模块(voicechanger.c 整体 #if VO_CHANGER_EN 包裹) |
ANS 运行参数(ans_api.c 内定义)
| 参数 | 值 | 格式 | 说明 |
|---|---|---|---|
NS_MODE | 0 | 枚举 | 算法模式(0 = 窄带) |
NS_IS_WIDEBAND | 1 | 枚举 | 1 对应 16 kHz;0 对应 8 kHz。决定支持的采样率 |
READSIZE | 128 | 样点 | 每次 run 处理块长 |
ANS_RUN_BUFFSIZE | 5400 | 字节 | 算法运行缓冲(静态,.ans_data) |
ANS_TMP_BUFSIZE | 3604 | 字节 | 算法临时缓冲(静态,.ans_data) |
ANS_AggressFactor | 125×65536/100 ≈ 1.25 | Q16 | 抑制强度,范围 1~2,越大越强 |
ANS_MinSuppress | 10×65536/100 ≈ 0.1 | Q16 | 最小抑制量,范围 0~1,越小越强 |
ANS_NoiseLevel | 1429×1024 ≈ -75 dB | Q23 | 噪声门限电平(-100dB ~ -40dB) |
变声参数(voicechanger.c 默认值)
| 参数 | 默认 | 说明 |
|---|---|---|
vc_parm.shiftv | 65 | 音高偏移(变调程度) |
vc_parm.formant_shift | 100 | 共振峰偏移(100 = 不变形) |
vc_parm.speedv | 80 | 语速 |
vc_parm.effect_v | EFFECT_VC_AV_BIRD5 | 预置音效(如鸟鸣等) |
vs_parm.randpercent | 100 | 颤音随机百分比 |
vs_parm.vibrate_lenCtrol | 30 | 颤音时长控制 |
vs_parm.vibrate_rate_u / vibrate_rate_d | 0 / 100 | 上/下颤音速率 |
VC_NG_THRES | 712 | 噪声门控阈值(底噪大的方案建议值) |
API Reference
ANS 模块(ans_api.c)
int ans_run(void *hld, short *inbuf, int len)
处理 128 点(256 字节)PCM 块的降噪。先输出上一帧剩余数据;输入不足一帧或上一帧未输出完时返回 0。
参数:
hld(void*): 效果对象句柄(当前实现未使用,保留接口兼容)inbuf(short*): 输入 PCM 数据len(int): 输入数据长度(字节)
返回: 本次消费的字节数(成功为 READSIZE * sizeof(short) = 256,否则 0)。
void *ans_api(void *obuf, void **ppsound, u32 sr)
初始化 ANS 效果。校验采样率(仅 16 kHz 通过当前配置)、查询并断言缓冲大小、以定点参数调用 NoiseSuppress_Init,最后调 ans_phy 构建效果节点。
参数:
obuf(void*): 前级输出缓冲ppsound(void**): 出参,返回后级 sound 节点sr(u32): 采样率,非 8000/16000 返回 NULL
返回: EFFECT_OBJ* 效果对象;失败返回 NULL 并打印 ans not support curr sr。
void *link_ans_sound(void *p_sound_out, void *p_ans_obuf, u32 sr)
把 ANS 链接进录音链。成功置 B_DEC_EFFECT 并返回后级节点;失败保持原链,返回原节点。
变声模块(voicechanger.c)
void *voice_changer_api(void *obuf, u32 sr, void **ppsound)
设置变声默认参数(shiftv=65、formant_shift=100、speedv=80、effect_v=鸟鸣5),调用 voice_changer_phy 完成初始化。返回 EFFECT_OBJ*。
void *voice_changer_phy(void *obuf, u32 sr, VOICECHANGER_AV_PARM *pvc_parm, VOICESYN_AV_PARM *pvs_parm, void **ppsound)
底层初始化:ops->need_buf(sr, parm) 校验静态缓冲 buflen(0x2010 字节)是否够用(不足打印 buff_len not enough 并返回 0),然后 ops->open() 打开算法实例并绑定 vc_pitch_io 输出回调,最后装配 vchange_obj(run = voice_changer_run)。
int voice_changer_run(void *hld, short *inbuf, int len)
经 sound_in_obj 中转调用算法库 ops->run(p_dbuf, inbuf, len),返回处理结果字节数。
void update_voice_changer_parm(VOICECHANGER_AV_PARM *new_vc_parm, VOICESYN_AV_PARM *new_vsyn_ctrol)
运行时热更新参数:NULL 参数直接返回;否则在 OS_ENTER_CRITICAL/OS_EXIT_CRITICAL 临界区内用新参数重开算法实例。
void *link_voice_changer_sound(void *p_sound_out, void *p_dac_cbuf, void **pp_effect, u32 in_sr)
把变声链接进扬声器链;成功时通过 pp_effect 回传效果对象、把后级 p_obuf 重绑到 DAC 缓冲,并置 B_DEC_EFFECT。
Failure Modes, Edge Cases & Concurrency
- 采样率不支持:
ans_api只接受 8 kHz/16 kHz(当前配置固定 16 kHz 宽带),其他采样率初始化返回 NULL,链路自动跳过该效果(ans init fail),系统继续以无降噪方式工作——失败降级而非崩溃。 - 静态缓冲不足:ANS 用
ASSERT(ANS_RUN_BUFFSIZE >= tolbufsize)硬校验;变声用运行时need_buf比较并返回错误码。换算法库版本时若缓冲区需求增大,会在初始化阶段立刻暴露。 - 块长不足/背压:
ans_run对len < 256与remain_len != 0均返回 0。这保证后级永远只收到完整块,代价是瞬时延迟可能增加一帧(约 8 ms @16 kHz)。 - 参数更新的并发安全:
update_voice_changer_parm依赖关中断(OS_ENTER_CRITICAL)保证"重开算法实例"与音频中断处理互斥。若在支持多核/多任务的平台上移植,需改为互斥锁或双缓冲方案。 - 重开即重置:变声参数更新通过
ops->open重开实例,意味着内部状态(颤音相位、时延缓冲)会清零,连续快速更新参数可能引入可闻的爆音/卡顿,上层应限频更新。 - 编译宏缺失时的行为:
ANS_EN/VO_CHANGER_EN未定义时相关文件整体不编译(#if defined(ANS_EN) && (ANS_EN)、#if VO_CHANGER_EN),link_*调用点也被同条件包裹,不存在空指针悬链问题。
Performance & Operational Notes
- 零动态内存:所有效果缓冲(
.ans_data、.voicechanger_data)在链接期布局,运行期无 malloc/free,适合 MCU 实时音频。 - 固定块长流水:每级 128 点/256 字节块处理 +
remain_output背压,CPU 占用稳定可预估;ANS 为 O(帧长) STFT 加窗运算,128 点窗口(STFT_Win_FixHalf_M256_D128)与跳长匹配。 - 内存段隔离:
.ans_data/.voicechanger_data与普通数据分离,便于链接脚本放置到 SRAM 特定区域,也便于排查内存占用。 - 日志埋点:初始化成功/失败均打印
log_info/log_error(如ans init succ/fail、voice change init succ/fail、need buff len 0x%x),调试时可直接从串口日志判断各效果是否挂载成功。 - 采样率联动:ANS 依赖
read_audio_adc_sr()的实际 ADC 采样率,变调/变声依赖adc_sr/in_sr;修改系统采样率配置时需同步核对各效果支持范围。
Extension Points
- 新增音效:在
sound_effect_list下新建目录,按既有模板实现xxx_api()(初始化 + 静态缓冲)、xxx_run()(块处理 + remain_output 背压)、link_xxx_sound()(链接入 +B_DEC_EFFECT),并在speak_api.c/encoder_api.c中用对应编译宏包裹调用点。 - 调参接口:变声已提供
update_voice_changer_parm()作为运行时调参入口,可被协议栈/App 调用;ANS 参数目前为编译期常量,如需运行时可调需仿照变声增加"临界区重开实例"接口。 - 链路顺序:扬声器链中移频 → 变声 → 回声的顺序由
speak_api.c中link_*调用次序决定,调整调用顺序即可改变效果叠加次序(需注意啸叫抑制必须位于反馈回路信号注入点之前才有效)。
Related Links
- ANS 实现 ans_api.c
- 变声实现 voicechanger.c
- 编码器接入 ANS(encoder_api.c)
- 扬声器接入移频啸叫抑制(speak_api.c)
- 混响头文件 reverb_api.h
- 回声头文件 echo_api.h
- ANS 算法库头文件 NoiseSuppressLib.h
- 相关兄弟页面:EQ(pcm_eq)、啸叫抑制(notch_howling)、变速(speed)、能量检测(energe_detect)