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(活改音量不爆音、不丢帧)。
This commit is contained in:
AskaEth committed 2026-11-12 08:07:37 +08:00
1 parent 4040b41649
commit e37c622597
6 files changed
+678 -26

No files matched your search

+234
View File
@@ -0,0 +1,234 @@
/* 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;
}