评审 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 完全一致, 证明二进制未变)。
279 lines
15 KiB
C
279 lines
15 KiB
C
/* 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <math.h>
|
||
#include <time.h>
|
||
|
||
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;
|
||
}
|