Files
MeowOS-For-R3P2/patch_v2/meow_audiod/test_vol_gain.c
T
AskaEth 2c0ec2ba15 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 完全一致,
证明二进制未变)。
2026-11-12 08:07:37 +08:00

279 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}