Files
MeowOS-For-R3P2/patch_v2/meow_audiod/test_vol_gain.c
T
AskaEth e37c622597 feat(audiod): BT 输出音量软件增益(曲线与 DAC 同源)+ vol_apply BT 分支真生效
真机现象:音量设 10 仍然满响度——vol_apply() 的 BT 分支 logmsg + return,
g_vol_pct 不更新、AVRCP 被 bt_vol_full() 钉在 127、写路径无任何软件缩放,
于是 BT 永远在耳机自己的满音量。

- 新增 vol_gain.{c,h}(纯整数,无 libm/软浮点):增益表由 dac_vol_set 的同一个
  衰减寄存器函数 vol_gain_dac_att_reg() 推出(0.5dB/步、平方锥度),
  Q31 迭代乘法开机算一次;vol=100 逐字节不变,vol=0 全 0(memset 快路)。
- dev_write(BT 路径)逐样本乘 + 32 位中间量 + 饱和;增益恒 <=1 故不可能削波。
- 改音量走 512 样本线性斜坡(Bresenham 余额分摊,末步不补差),防爆音。
- vol_apply() BT 分支改为更新 g_vol_pct + 持久化;DAC numid=1/2/11 仍不写
  (M50 T11 隔离断言不变)。
- dac_vol_set() 改用 vol_gain_dac_att_reg(),DAC/BT 两条路锥度不可能漂移。
- 测试:test_vol_gain(曲线/缩放/斜坡纯函数断言)+ T11 改为断言
  「BT 音量不写 DAC 寄存器」+ 新增 T18(wav 捕获音量阶梯 RMS/峰值)
  / T19(活改音量不爆音、不丢帧)。
2026-11-12 08:07:37 +08:00

235 lines
12 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");
}
/* ==== 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;
}