/* test_vol_gain.c — BT 软件增益(M55)宿主验证:曲线 / 缩放 / 斜坡 / 成本,全纯函数。 * * 为什么必须宿主断言(用户真机现象:音量 10 仍满响度): * - `vol_gain_dac_att_reg()` 与 dac_vol_set() 是**同一个函数**(0.5dB/步、平方锥度), * 所以 BT 与 DAC 在同一个百分比上得到同一条听感曲线——这条「同源」必须在测试里钉死, * 否则以后谁改一边就会静默漂移; * - `vol=0` 必须是**真静音**(输出全 0),`vol=100` 必须**逐字节不变**(单位增益), * 中间必须单调(百分比越小越轻); * - 极值输入(±32768/±32767)任何百分比下都不得溢出/削波(增益恒 ≤ 1,32 位中间量); * - 改音量必须走斜坡(512 样本),不能一步跳到位(跳变 = 爆音「叮」)。 * * 编译(在 patch_v2/meow_audiod 下): * cc -Os -Wall -Wextra -o test_vol_gain test_vol_gain.c vol_gain.c -lm * 运行:./test_vol_gain */ #include "vol_gain.h" #include #include #include #include #include static int g_pass = 0, g_fail = 0; static void check(int cond, const char *name) { if (cond) { printf(" PASS %s\n", name); g_pass++; } else { printf(" FAIL %s\n", name); g_fail++; } } /* 参考实现(**仅测试**用 double,目标机代码零浮点):DAC 寄存器值 → 线性增益。 */ static double ref_gain(int reg) { return pow(10.0, -0.5 * (double)reg / 20.0); } int main(void) { setbuf(stdout, NULL); vol_gain_init(); printf("=== vol_gain host test (M55 BT software gain) ===\n"); /* ==== G1 衰减寄存器:与 dac_vol_set 的锥度逐点相同(真机 DAC 实测语义) ==== */ printf("[G1 DAC attenuation register mapping (shared with dac_vol_set)]\n"); check(vol_gain_dac_att_reg(100) == 0, "pct=100 -> reg 0 (0dB, full scale)"); check(vol_gain_dac_att_reg(0) == 255, "pct=0 -> reg 255 (-127.5dB)"); check(vol_gain_dac_att_reg(50) == 48, "pct=50 -> reg 48 (-24.0dB)"); check(vol_gain_dac_att_reg(40) == 69, "pct=40 -> reg 69 (same as dac_vol_set T15)"); check(vol_gain_dac_att_reg(10) == 155, "pct=10 -> reg 155 (-77.5dB)"); check(vol_gain_dac_att_reg(-5) == 255 && vol_gain_dac_att_reg(140) == 0, "out-of-range pct clamps (<=0 mute, >=100 unity)"); { int mono = 1; for (int pct = 100; pct > 0; pct--) /* 百分比下降 → 衰减只增不减 */ if (vol_gain_dac_att_reg(pct - 1) < vol_gain_dac_att_reg(pct)) mono = 0; check(mono, "reg monotone non-decreasing as pct decreases"); } /* ==== G2 增益表:与 10^(-0.5dB*reg/20) 相差 ≤1 LSB ==== */ printf("[G2 gain table vs exact pow() reference]\n"); { int worst = 0, bad = 0; for (int pct = 1; pct <= 100; pct++) { int reg = vol_gain_dac_att_reg(pct); int got = vol_gain_q15(pct); int err = (int)lround(32768.0 * ref_gain(reg)) - got; if (err < 0) err = -err; if (err > 1) bad++; if (err > worst) worst = err; } check(bad == 0, "every pct within 1 LSB of exact 10^(-0.5dB*reg/20)"); printf(" (max |err| = %d LSB over 100 points, 0 worst-case at 1 LSB)\n", worst); } check(vol_gain_q15(100) == VOL_GAIN_UNITY, "pct=100 -> unity (32768)"); check(vol_gain_q15(0) == 0, "pct=0 -> 0 (true mute)"); { int mono = 1; for (int pct = 100; pct > 0; pct--) if (vol_gain_q15(pct - 1) > vol_gain_q15(pct)) mono = 0; check(mono, "gain monotone non-increasing as pct decreases"); } { /* 曲线不是线性:50% 处应远低于 -6dB(线性会正好 -6dB) */ double g50 = vol_gain_q15(50) / 32768.0; check(g50 < 0.1, "pct=50 is well below linear -6dB (perceptual taper kept)"); printf(" (pct=50 gain = %.4f = %.1f dB)\n", g50, 20.0 * log10(g50)); } /* ==== G3 单样本缩放:单位增益恒等 / 静音全 0 / 不溢出 ==== */ printf("[G3 per-sample scaling]\n"); { int bad = 0; for (int v = -32768; v <= 32767; v++) /* 全量程扫一遍:单位增益必须恒等 */ if (vol_gain_scale_s16((int16_t)v, VOL_GAIN_UNITY) != (int16_t)v) bad++; check(bad == 0, "unity gain is bit-exact over the whole int16 range"); } check(vol_gain_scale_s16(32767, 0) == 0 && vol_gain_scale_s16(-32768, 0) == 0, "gain 0 -> silence (both extremes)"); { /* 任何百分比 × 极值输入:不越界、不放大(|out| ≤ |in|) */ int bad = 0; const int16_t in[] = { 32767, -32768, 32766, -32767, 1, -1, 0 }; for (int pct = 0; pct <= 100; pct++) { int g = vol_gain_q15(pct); for (size_t i = 0; i < sizeof in / sizeof in[0]; i++) { int out = vol_gain_scale_s16(in[i], g); if (out > 32767 || out < -32768) bad++; if (abs(out) > abs((int)in[i])) bad++; } } check(bad == 0, "no clipping/overflow at extremes for every pct (gain <= unity)"); } /* ==== G4 斜坡:不一步跳到位(防爆音),且终点精确 ==== */ printf("[G4 ramp on volume change]\n"); { vol_gain_ramp_t r; vol_gain_ramp_snap(&r, 100); check(r.cur_q15 == VOL_GAIN_UNITY && r.left == 0, "snap -> unity, no pending ramp"); /* 100% → 10%:铺满一个 512 样本块 */ int16_t buf[VOL_GAIN_RAMP_SAMPLES * 2]; for (int i = 0; i < VOL_GAIN_RAMP_SAMPLES * 2; i++) buf[i] = 16384; vol_gain_ramp_set(&r, 10, VOL_GAIN_RAMP_SAMPLES); check(r.left == VOL_GAIN_RAMP_SAMPLES && r.step_q15 < 0, "ramp armed toward lower gain"); vol_gain_ramp_apply(&r, (uint8_t *)buf, sizeof buf); check(r.cur_q15 == vol_gain_q15(10) && r.left == 0, "ramp lands exactly on target q15"); check(buf[0] > buf[VOL_GAIN_RAMP_SAMPLES * 2 - 1], "first sample louder than last (no instant jump = no click)"); { int mono = 1; for (int i = 1; i < VOL_GAIN_RAMP_SAMPLES * 2; i++) if (buf[i] > buf[i - 1]) mono = 0; check(mono, "per-sample gain decreases monotonically through the ramp"); int maxstep = 0; for (int i = 1; i < VOL_GAIN_RAMP_SAMPLES * 2; i++) { int d = buf[i - 1] - buf[i]; if (d > maxstep) maxstep = d; } /* 一步跳到位会是 16384×(1-0.000122) ≈ 16382;斜坡每样本 ≤ |step|+1 = 65 → * 输出差 ≤ 65×16384/32768 ≈ 33 LSB(含余额分摊也在这个量级)。 */ check(maxstep <= 40, "no hard gain jump (max per-sample delta <= 40 LSB)"); printf(" (max per-sample delta during ramp = %d LSB; hard jump would be ~16382)\n", maxstep); } /* 跨块连续性:同一段数据分两块推,结果必须与单块一致(斜坡状态不丢) */ vol_gain_ramp_t a, b; vol_gain_ramp_snap(&a, 100); vol_gain_ramp_snap(&b, 100); int16_t big[1024], c1[512], c2[512]; for (int i = 0; i < 1024; i++) big[i] = 8192; memcpy(c1, big, sizeof c1); memcpy(c2, big + 512, sizeof c2); vol_gain_ramp_set(&a, 25, 700); vol_gain_ramp_apply(&a, (uint8_t *)big, sizeof big); vol_gain_ramp_set(&b, 25, 700); vol_gain_ramp_apply(&b, (uint8_t *)c1, sizeof c1); vol_gain_ramp_apply(&b, (uint8_t *)c2, sizeof c2); check(memcmp(big, c1, sizeof c1) == 0 && memcmp(big + 512, c2, sizeof c2) == 0, "ramp state survives buffer boundaries (chunked == single block)"); check(a.cur_q15 == vol_gain_q15(25) && b.cur_q15 == vol_gain_q15(25) && b.left == 0, "ramp longer than one block still reaches target exactly"); /* 目标重复设置不打断进行中的斜坡(每块都会 set 一次) */ vol_gain_ramp_t c; vol_gain_ramp_snap(&c, 100); vol_gain_ramp_set(&c, 10, 512); int32_t step0 = c.step_q15, left0 = c.left; vol_gain_ramp_set(&c, 10, 512); check(c.step_q15 == step0 && c.left == left0, "repeated set() with same pct does not restart ramp"); /* 增益差小于斜坡长度:斜坡自动缩短(保证每样本至少 1 LSB,终点精确不过冲) */ vol_gain_ramp_snap(&c, 100); int32_t tgt = vol_gain_q15(92); int32_t delta = VOL_GAIN_UNITY - tgt; /* 1833 LSB,远小于 4096 */ vol_gain_ramp_set(&c, 92, 4096); check(c.left == delta, "small delta -> ramp shortened to |delta| samples"); int16_t sm[1024]; for (int i = 0; i < 1024; i++) sm[i] = 1000; vol_gain_ramp_apply(&c, (uint8_t *)sm, sizeof sm); /* 第一块:斜坡中段 */ check(c.left > 0 && c.cur_q15 > tgt, "mid-ramp after one block (not yet at target)"); 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.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 ==== */ printf("[G5 steady-state fast paths]\n"); { vol_gain_ramp_t r; int16_t buf[256]; for (int i = 0; i < 256; i++) buf[i] = (int16_t)(i * 97 - 12000); int16_t orig[256]; memcpy(orig, buf, sizeof buf); vol_gain_ramp_snap(&r, 100); vol_gain_ramp_apply(&r, (uint8_t *)buf, sizeof buf); check(memcmp(buf, orig, sizeof buf) == 0, "pct=100 steady state: buffer untouched (bit-exact)"); vol_gain_ramp_snap(&r, 0); vol_gain_ramp_apply(&r, (uint8_t *)buf, sizeof buf); int allzero = 1; for (int i = 0; i < 256; i++) if (buf[i] != 0) allzero = 0; check(allzero, "pct=0 steady state: buffer all zeros (true mute)"); } /* ==== G6 成本参考(宿主):逐样本缩放吞吐(目标机另行实测) ==== */ printf("[G6 scaling throughput (host reference)]\n"); { enum { N = 1 << 20 }; int16_t *buf = malloc((size_t)N * 2); vol_gain_ramp_t r; if (buf) { for (int i = 0; i < N; i++) buf[i] = (int16_t)((i * 31) & 0x7FFF); vol_gain_ramp_snap(&r, 50); /* 稳态非快路:每个样本都乘 */ struct timespec t0, t1; clock_gettime(CLOCK_MONOTONIC, &t0); vol_gain_ramp_apply(&r, (uint8_t *)buf, (size_t)N * 2); clock_gettime(CLOCK_MONOTONIC, &t1); double ms = (double)(t1.tv_sec - t0.tv_sec) * 1e3 + (double)(t1.tv_nsec - t0.tv_nsec) / 1e6; /* 44.1k/2ch 每秒 88200 样本 */ double us_per_88200 = ms * 1000.0 * 88200.0 / (double)N; printf(" (%d samples in %.3f ms -> %.1f us of every 1000 ms at 44.1k/2ch)\n", N, ms, us_per_88200); check(ms < 1000.0, "host scaling throughput far above real-time (sanity)"); free(buf); } } /* ==== 附:给报告用的曲线表(百分比 / 衰减寄存器 / dB / Q15) ==== */ printf("[curve table pct -> reg / dB / q15 / relative]\n"); const int pcts[] = { 100, 92, 75, 60, 50, 40, 25, 10, 5, 1, 0 }; for (size_t i = 0; i < sizeof pcts / sizeof pcts[0]; i++) { int p = pcts[i], reg = vol_gain_dac_att_reg(p), g = vol_gain_q15(p); printf(" pct=%3d reg=%3d %7.2f dB q15=%6d x%.6f\n", p, reg, -0.5 * (double)reg, g, (double)g / 32768.0); } printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail); return g_fail ? 1 : 0; }