test(audiod): r1 P2-1/P2-2 — 补 rapid-toggle 回归用例 + BT 音量语义文档(实现零改动)

评审 r1 的 `p2-2items`,两项都是「缺用例 / 缺一句文档」,不改任何已交付行为。

P2-1(连续拖滑块/ramp 中途反向缺回归):
- test_vol_gain 新增 G4b:块长 100(< 512 斜坡)× 60 次 100↔10 反向,实测 59 次落在
  斜坡中段;断言增益全程 ∈ [q15(10)=4, 32768] 且从不放大、块内逐样本只朝目标单调走、
  相邻样本跳变 ≤64 LSB(硬跳变 ~16380)、停手后 cur=4/left=0/bump_left=0。
- test_audiod_host 新增 T20(daemon 侧等价集成用例):经 IPC 连发 60 次 vol,
  每次只喂 100 帧(400B),断言 60 次全 ACK、underrun/dropped=0、
  落盘 PCM 无硬跳变(max_delta 1020 = 正弦自身斜率)、尾部 RMS 1.49 比头部
  6376.58 低 72dB(10% 真的生效)。

P2-2(文档):vol_gain.h 顶部新增「App/SDK 侧契约」段:BT 下 set_volume 同样生效
(走软件增益 + 持久化),AVRCP 钉 127 只为耳机功放满量程而非「音量交给耳机」,
pct 只在 [0,100] 有意义(<=0 真静音、>100 按满量程截断),16bit 写路径的
量化下限说明;vol_gain_dac_att_reg 注释补 pct>100 行为。build_audiod.sh
头部补 M55 说明(必须一起编 vol_gain.c、旧 SDK 说法已过期)。

验证:test_audiod_host 134/134 PASS、test_vol_gain 36/36 PASS、test_bt_dev PASS;
mipsel 交叉编译 0 warning,产物 md5 6cef4c54a1cb8e0c55f4ad5b6c4a323e(与 r1 完全一致,
证明二进制未变)。
This commit is contained in:
AskaEth committed 2026-11-12 08:07:37 +08:00
1 parent e37c622597
commit 2c0ec2ba15
4 files changed
+151 -3

No files matched your search

+6
View File
@@ -22,6 +22,12 @@
# libasound 从不加载),改为 snd_pcm_open("bluealsa:DEV=<mac>,PROFILE=a2dp,CODEC=<token>") # libasound 从不加载),改为 snd_pcm_open("bluealsa:DEV=<mac>,PROFILE=a2dp,CODEC=<token>")
# 内联参数(对应 /etc/alsa/conf.d/20-bluealsa.conf 的 pcm.bluealsa @args),失败必回落 DAC。 # 内联参数(对应 /etc/alsa/conf.d/20-bluealsa.conf 的 pcm.bluealsa @args),失败必回落 DAC。
# #
# M55 蓝牙音量:BT 下 AVRCP 钉在 127(耳机功放满量程),用户音量由 vol_gain.c 的**软件增益**在
# 写路径执行(与 DAC 同一条曲线、全整数)。故 audiod 目标必须一起编 vol_gain.c(下方已带);
# host 目标另外单独编 test_vol_gain.c(纯函数回归:曲线/缩放/斜坡/toggle storm)。
# ⚠️ `meow_audio_set_volume` 在 BT 下同样生效(写增益 + 持久化),SDK 文档里「BT 时音量由 AVRCP
# 侧控」的旧说法已过期——见 vol_gain.h 顶部的 App/SDK 契约段。
#
# 环境变量覆盖:ROOT / REPO 自动推导,ZIG / CC / LIBBT / ASOUND_SRC / CC_HOST 可显式指定。 # 环境变量覆盖:ROOT / REPO 自动推导,ZIG / CC / LIBBT / ASOUND_SRC / CC_HOST 可显式指定。
set -e set -e
cd "$(dirname "$0")" cd "$(dirname "$0")"
+87 -2
View File
@@ -347,8 +347,9 @@ static int wait_wav_open(const char *path, int tmo_ms)
} }
/* S16 立体声 WAV 的 data 段统计(M55 音量阶梯的**客观量化**:不靠耳朵)。 /* S16 立体声 WAV 的 data 段统计(M55 音量阶梯的**客观量化**:不靠耳朵)。
* 返回 ok=1 时填 rms / peak / maxdelta(相邻样本最大跳变,用来证明改音量没有硬跳变爆音)。 */ * 返回 ok=1 时填 rms / peak / maxdelta(相邻样本最大跳变,用来证明改音量没有硬跳变爆音),
struct wavstat { int ok; long nsamp; double rms; int peak; int maxdelta; }; * tail_rms 只统计最后 4096 个样本(T20「连续改音量后是否稳定在新音量」用)。 */
struct wavstat { int ok; long nsamp; double rms; int peak; int maxdelta; double tail_rms; double head_rms; };
static struct wavstat wav_s16_stats(const char *path) static struct wavstat wav_s16_stats(const char *path)
{ {
struct wavstat s; struct wavstat s;
@@ -382,6 +383,14 @@ static struct wavstat wav_s16_stats(const char *path)
s.rms = sqrt((double)sumsq / (double)rd); s.rms = sqrt((double)sumsq / (double)rd);
s.peak = peak; s.peak = peak;
s.maxdelta = maxdelta; s.maxdelta = maxdelta;
{ /* 尾部/头部 4096 样本的 RMS(连续改音量后是否稳定在新音量、停手前后差多少) */
size_t take = rd < 4096 ? rd : 4096;
long long tq = 0, hq = 0;
for (size_t i = rd - take; i < rd; i++) tq += (long long)b[i] * b[i];
for (size_t i = 0; i < take; i++) hq += (long long)b[i] * b[i];
s.tail_rms = take ? sqrt((double)tq / (double)take) : 0.0;
s.head_rms = take ? sqrt((double)hq / (double)take) : 0.0;
}
free(b); free(b);
return s; return s;
} }
@@ -1360,6 +1369,82 @@ int main(void)
unlink(conf19); unlink(conf19);
} }
/* ---- T20:连续拖滑块(rapid toggle)的**集成**回归(评审 r1 P2-1 的 daemon 侧等价用例)。
* 状态机级断言(cur/left/bump_left、mid-ramp 反向计数)在 test_vol_gain 的 G4b——那里能直接
* 看内部字段;这里验真实写路径:通过 IPC 在 vol 100↔10 之间连发 60 次,每次只喂 100 帧
* (400B < 512 样本斜坡),让多数改动落在斜坡中段,然后看落盘的 PCM 有没有硬跳变/丢帧。 */
printf("[T20 rapid volume toggle (slider drag) through the write path]\n");
{
unlink(TSOCK); unlink(TWAV);
const char *conf20 = "/tmp/meow_test_settings20.conf";
FILE *f20 = fopen(conf20, "w");
if (f20) { fprintf(f20, "# meow-settings 1\naudio.volume = 100\naudio.output_bt = 1\n"); fclose(f20); }
g_audiod_pid = start_audiod_settings("file", conf20);
usleep(350000);
struct cli c20;
if (cli_connect(&c20) != 0) { check(0, "T20 connect"); }
else {
m_audio_open_ack_t a20;
if (do_open(&c20, 44100, 2, AUDIO_FMT_S16, &a20) != 0 || a20.err != MEOW_OK) {
check(0, "T20 open");
} else {
enum { NFR20 = 12000 }; /* 6000 帧风暴 + 6000 帧停手后(尾部窗口离过渡区很远) */
static uint8_t t20[NFR20 * 4];
static int16_t s20[NFR20 * 2];
for (int i = 0; i < NFR20 * 2; i++) { /* 相位连续:跳变只可能来自增益 */
double ph = 2.0 * M_PI * 440.0 * (double)(i / 2) / 44100.0;
s20[i] = (int16_t)(sin(ph) * 0.5 * 32767.0);
}
memcpy(t20, s20, sizeof t20);
size_t off = 0;
int ack_ok = 1;
for (int k = 0; k < 60; k++) {
int32_t pct = (k & 1) ? 100 : 10;
if (sf(c20.fd, MSG_AUDIO_VOLUME, (uint32_t)(100 + k), &pct, sizeof pct) == 0) {
uint8_t ab[64]; uint16_t at;
if (rf(c20.fd, &at, ab, sizeof ab) < 0 || at != MSG_ACK) ack_ok = 0;
} else ack_ok = 0;
do_write(&c20, t20 + off, 400); /* 100 帧:短于 512 样本斜坡 */
off += 400;
usleep(3000);
}
check(ack_ok, "60 rapid vol set all ACKed (async volume path keeps up)");
int32_t pct10 = 10; /* 停手:稳定到 10% */
if (sf(c20.fd, MSG_AUDIO_VOLUME, 199, &pct10, sizeof pct10) == 0) {
uint8_t ab[64]; uint16_t at; rf(c20.fd, &at, ab, sizeof ab);
}
usleep(80000);
for (; off < sizeof t20; off += 4096) { /* 剩余:在 10% 下播完 */
size_t left = sizeof t20 - off;
do_write(&c20, t20 + off, (uint32_t)(left < 4096 ? left : 4096));
}
char q20[1024];
do_query(&c20, q20, sizeof q20);
check(strstr(q20, "vol=10") != NULL, "settled to vol=10 after the toggle storm");
check(strstr(q20, "underrun=0") != NULL && strstr(q20, "dropped=0") != NULL,
"no underrun/dropped frames during 60 volume toggles");
wait_wav_open(TWAV, 3000);
usleep(250000);
do_ctrl(&c20, MSG_AUDIO_STOP, a20.stream_id);
close(c20.fd);
stop_audiod();
struct wavstat st = wav_s16_stats(TWAV);
check(st.ok && st.nsamp > 8000, "T20 wav captured");
if (st.ok) {
printf(" (nsamp=%ld peak=%d max_delta=%d head_rms=%.2f tail_rms=%.2f; hard jump ~16380)\n",
st.nsamp, st.peak, st.maxdelta, st.head_rms, st.tail_rms);
check(st.maxdelta < 2000, "rapid toggles never hard-jump the gain (no click)");
check(st.peak > 4000, "storm still contains loud passages (toggles took effect)");
/* 停手后在 10%:尾部应比头部(满音量段)低 ≥40dB(10% = -77.5dB 理论) */
check(st.tail_rms > 0.0 && st.head_rms > st.tail_rms * 100.0,
"tail settles >=40dB below head (gain really applied at vol=10)");
}
}
}
stop_audiod();
unlink(conf20);
}
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail); printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0; return g_fail ? 1 : 0;
} }
+44
View File
@@ -177,6 +177,50 @@ int main(void)
vol_gain_ramp_apply(&c, (uint8_t *)sm, sizeof sm); /* 第二块:走完(跨块精确到点) */ vol_gain_ramp_apply(&c, (uint8_t *)sm, sizeof sm); /* 第二块:走完(跨块精确到点) */
check(c.cur_q15 == tgt && c.left == 0, "short ramp ends exactly at target (no overshoot)"); check(c.cur_q15 == tgt && c.left == 0, "short ramp ends exactly at target (no overshoot)");
check(c.bump_left == 0, "no residual bump after ramp completes"); check(c.bump_left == 0, "no residual bump after ramp completes");
/* ---- G4b:连续拖滑块(rapid toggle)——块长 100 < 斜坡 512,目标中途反复反向 ----
* 真实场景是用户按住音量键/拖滑块,每块都改目标、大多数改动落在斜坡中段。
* 回归点(评审 P2-1):①每次从**当前** cur 重起斜坡,不许跳到位 ②增益全程在
* [q15(10)=4, 32768] 内(不出现 0 或放大)③逐样本增益只朝目标单调走、相邻样本
* 跳变 ≤64 LSB(硬跳变会是 ~16380)④停手后精确收敛到目标且无残留余额。 */
{
const int blk_n = 100; /* < VOL_GAIN_RAMP_SAMPLES:保证落在斜坡中段 */
const int toggles = 60;
int16_t blk[100];
for (int i = 0; i < blk_n; i++) blk[i] = 16384; /* 恒定幅度:输出差 = 增益差/2 */
int32_t lo = vol_gain_q15(10), hi = vol_gain_q15(100);
int midramp = 0, out_of_band = 0, maxdelta = 0, nonmono = 0;
vol_gain_ramp_snap(&c, 100);
for (int k = 0; k < toggles; k++) {
if (c.left > 0) midramp++; /* 上一次斜坡还没走完就被改目标 */
vol_gain_ramp_set(&c, (k & 1) ? 100 : 10, VOL_GAIN_RAMP_SAMPLES);
int32_t g_before = c.cur_q15;
for (int i = 0; i < blk_n; i++) blk[i] = 16384; /* 每块重铺原始信号 */
vol_gain_ramp_apply(&c, (uint8_t *)blk, sizeof blk);
if (c.cur_q15 < lo || c.cur_q15 > hi) out_of_band++;
/* 块内逐样本:只朝目标走(单调),且相邻样本差 ≤64 */
int up = c.tgt_q15 > g_before;
for (int i = 1; i < blk_n; i++) {
int d = blk[i] - blk[i - 1];
if (d < 0) d = -d;
if (d > maxdelta) maxdelta = d;
if (up ? (blk[i] < blk[i - 1]) : (blk[i] > blk[i - 1])) nonmono++;
if (blk[i] > 16384 || blk[i] < 0) out_of_band++; /* 不许放大/越界 */
}
}
check(midramp >= toggles - 2, "toggle storm actually lands mid-ramp (>=58/60)");
check(out_of_band == 0, "gain stays within [q15(10)=4, 32768] and never amplifies");
check(nonmono == 0, "each block moves monotonically toward its target (no zigzag)");
check(maxdelta <= 64, "rapid toggles: per-sample delta <= 64 LSB (hard jump ~16380)");
/* 停手:回到 10% 并推几块大缓冲,确认精确收敛、状态无残留 */
vol_gain_ramp_set(&c, 10, VOL_GAIN_RAMP_SAMPLES);
for (int i = 0; i < blk_n; i++) blk[i] = 16384;
for (int k = 0; k < 8; k++) vol_gain_ramp_apply(&c, (uint8_t *)blk, sizeof blk);
check(c.cur_q15 == vol_gain_q15(10) && c.cur_q15 == 4 && c.left == 0 && c.bump_left == 0,
"after toggle storm: cur=4 (q15 of 10%), left=0, bump_left=0");
printf(" (toggle storm: %d/60 mid-ramp reversals, max per-sample delta %d LSB)\n",
midramp, maxdelta);
}
} }
/* ==== G5 稳态快路:vol=100 缓冲原样、vol=0 全 0 ==== */ /* ==== G5 稳态快路:vol=100 缓冲原样、vol=0 全 0 ==== */
+14 -1
View File
@@ -13,6 +13,17 @@
* *
* 全整数:增益表开机算一次(Q31 定点迭代乘法,不引 libm/软浮点),逐样本一次 32 位乘 + * 全整数:增益表开机算一次(Q31 定点迭代乘法,不引 libm/软浮点),逐样本一次 32 位乘 +
* 移位 + 饱和;100% 走「单位增益直接返回」快路(逐字节不变、零开销),0% 走 memset。 * 移位 + 饱和;100% 走「单位增益直接返回」快路(逐字节不变、零开销),0% 走 memset。
*
* ---- App / SDK 侧契约(M55 定案;MEOW_APP_SDK.md 里「BT 输出目标时音量由 AVRCP 侧控」已过期)----
* - `meow_audio_set_volume(pct)` 在**两种输出目标下都生效**:DAC 目标写 CS43198 音量寄存器
* (numid=1/2 + DSD_VOLUME 双组),BT 目标写本模块的软件增益(写进 A2DP 的 PCM 样本)。
* 同一 pct 在两条路上是**同一条听感曲线**(vol_gain_dac_att_reg),不是「线性 vs 非线性」两套。
* - BT 下 AVRCP 被钉在 127(耳机功放满量程)——那是为了让耳机的模拟级不二次衰减,
* **不是**「音量交给耳机控」:用户的 vol 值由 audiod 的数字增益执行,`vol=0` 是真正的全 0 静音。
* - `pct` 只在 `[0,100]` 有意义:`<=0` 一律真静音,`>100` 一律按 100(满量程)处理。
* (`settings.conf` 里被手改成如 `audio.volume = 150` 时表现为满量程,不会超过 100% 的合法响度。)
* - 16bit 写路径的固有下限:约 <=-78dB 的档位(pct<=10)只剩 1 LSB 量级,听感上是静音;
* 实用区间约 60..100%(与 DAC 相同,因为曲线同源)。
*/ */
#ifndef MEOW_VOL_GAIN_H #ifndef MEOW_VOL_GAIN_H
#define MEOW_VOL_GAIN_H #define MEOW_VOL_GAIN_H
@@ -26,7 +37,9 @@
/* 开机调用一次(幂等):生成 256 项增益表。之后查表全只读、无锁。 */ /* 开机调用一次(幂等):生成 256 项增益表。之后查表全只读、无锁。 */
void vol_gain_init(void); void vol_gain_init(void);
/* pct(0..100) → DAC 衰减寄存器 0..255。**dac_vol_set() 也调它**,保证两条路同一条锥度。 */ /* pct(0..100) → DAC 衰减寄存器 0..255。**dac_vol_set() 也调它**,保证两条路同一条锥度。
* pct<=0 → 255(真静音);pct>=100 → 0(0dB 满量程,**包括 pct>100**:越界按满量程截断,
* 不会给出 >100% 的响度)。 */
int vol_gain_dac_att_reg(int pct); int vol_gain_dac_att_reg(int pct);
/* pct → 线性增益 Q15(32768=单位增益,0=真静音)。pct 越界自动 clamp。 */ /* pct → 线性增益 Q15(32768=单位增益,0=真静音)。pct 越界自动 clamp。 */