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

+209 -8
View File
@@ -346,8 +346,49 @@ static int wait_wav_open(const char *path, int tmo_ms)
return -1;
}
/* S16 立体声 WAV 的 data 段统计(M55 音量阶梯的**客观量化**:不靠耳朵)。
* 返回 ok=1 时填 rms / peak / maxdelta(相邻样本最大跳变,用来证明改音量没有硬跳变爆音)。 */
struct wavstat { int ok; long nsamp; double rms; int peak; int maxdelta; };
static struct wavstat wav_s16_stats(const char *path)
{
struct wavstat s;
memset(&s, 0, sizeof s);
FILE *f = fopen(path, "rb");
if (!f) return s;
unsigned char h[44];
if (fread(h, 1, 44, f) != 44) { fclose(f); return s; }
if (memcmp(h, "RIFF", 4) || memcmp(h + 8, "WAVE", 4)) { fclose(f); return s; }
uint16_t ch, bits; uint32_t data_len;
memcpy(&ch, h + 22, 2); memcpy(&bits, h + 34, 2); memcpy(&data_len, h + 40, 4);
if (ch != 2 || bits != 16) { fclose(f); return s; }
if (data_len > 1u << 20) data_len = 1u << 20;
size_t n = data_len / 2;
int16_t *b = malloc(n ? n * 2 : 2);
if (!b) { fclose(f); return s; }
size_t rd = fread(b, 2, n, f);
fclose(f);
if (rd == 0) { free(b); return s; }
long long sumsq = 0; int peak = 0, maxdelta = 0, prev = 0;
for (size_t i = 0; i < rd; i++) {
int v = b[i];
int a = v < 0 ? -v : v;
if (a > peak) peak = a;
sumsq += (long long)v * v;
if (i) { int d = v - prev; if (d < 0) d = -d; if (d > maxdelta) maxdelta = d; }
prev = v;
}
s.ok = 1;
s.nsamp = (long)rd;
s.rms = sqrt((double)sumsq / (double)rd);
s.peak = peak;
s.maxdelta = maxdelta;
free(b);
return s;
}
int main(void)
{
setbuf(stdout, NULL); /* 无缓冲:崩溃/卡死时也能看到跑到哪一档(回归排障) */
unlink(TSOCK); unlink(TWAV);
g_audiod_pid = start_audiod();
if (g_audiod_pid < 0) { printf("cannot fork audiod\n"); return 1; }
@@ -786,19 +827,22 @@ int main(void)
check(eok, "fresh OPEN after overflow (table recycled, no exhaustion/wedge)");
check(stop_audiod_tmo(5000) != 0, "audiod not wedged by overflow (SIGTERM clean exit)");
/* ---- T11:P2 蓝牙 输出目标(OUT_BT)+ 音量 AVRCP 隔离 + 格式归一 + codec。
/* ---- T11:P2 蓝牙 输出目标(OUT_BT)+ 音量隔离 + 格式归一 + codec。
* 宿主以 FILE sink 跑(真 bluealsa 传输留给真机),验证目标选择/音量隔离/格式逻辑:
* - settings.conf `audio.output_bt=1` + `bt.codec=SBC` → target=bt / codec=SBC / volctl=avrcp / grant fmt=S16;
* - BT 下 MSG_AUDIO_VOLUME 设置:**不下发 DAC 音量反转**——g_vol_pct 不变、不持久化 audio.volume、
* volctl 仍 avrcp(隔离成立);
* - BT 下 MSG_AUDIO_VOLUME 设置(M55 语义):g_vol_pct 与持久化**必须更新**(旧版提前 return
* 什么都不做 → 重启音量跳回去),但**绝不写 DAC numid=1/2/11**(amixer 轨迹必须为空),
* volctl 仍 avrcp —— 音量改由写路径的软件增益生效(曲线/单调性/静音见 T18 + test_vol_gain);
* - MSG_AUDIO_OUTPUT 切回 DAC:target=dac / volctl=dac;此后音量设置写 DAC + 持久化 audio.volume。
* 断言:BT 路径绝不动 DAC 音量(plan §三/§五「两套隔离」);BT 格式归一=S16;codec 读到生效。 ---- */
printf("[T11 BT output target + AVRCP vol isolation]\n");
printf("[T11 BT output target + vol isolation]\n");
unlink(TSOCK); unlink(TWAV);
const char *tconf = "/tmp/meow_test_settings.conf";
const char *trace11 = "/tmp/meow_amixer_trace11.txt";
unlink(trace11);
FILE *tf = fopen(tconf, "w");
if (tf) { fprintf(tf, "# meow-settings 1\naudio.volume = 60\naudio.output_bt = 1\nbt.codec = SBC\n"); fclose(tf); }
g_audiod_pid = start_audiod_settings("file", tconf);
g_audiod_pid = start_audiod_trace("file", tconf, trace11);
usleep(400000);
struct cli b11;
check(cli_connect(&b11) == 0, "connect B11");
@@ -818,7 +862,7 @@ int main(void)
/* BT/file sink 写一段流(应正常落盘到 WAV) */
do_write(&b11, pcm, (uint32_t)gen_sine(pcm, sizeof pcm, (int)(ack11.grant_fmt ? ack11.grant_fmt : AUDIO_FMT_S16), 600, 440, 44100));
usleep(150000);
/* 音量设置 while BT:不下发 DAC——g_vol_pct 不变(60)、不持久化 audio.volume=40、volctl 仍 avrcp */
/* 音量设置 while BT(M55):g_vol_pct + 持久化都变,但**不碰 DAC**(amixer 轨迹空) */
{
int32_t pct = 40;
if (sf(b11.fd, MSG_AUDIO_VOLUME, 8, &pct, sizeof pct) == 0) {
@@ -828,11 +872,20 @@ int main(void)
}
check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "volctl=avrcp"),
"vol set while bt -> volctl still avrcp (isolation)");
check(strstr(q11, "vol=60") != NULL, "vol set while bt -> DAC vol unchanged (60)");
check(strstr(q11, "vol=40") != NULL, "bt vol set -> g_vol_pct=40 (was silently dropped before M55)");
{
FILE *chk = fopen(tconf, "r"); int has40 = 0; char l[128];
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
check(!has40, "bt vol set does NOT persist audio.volume=40");
check(has40, "bt vol set persists audio.volume=40 (survives reboot)");
}
{
/* 隔离断言(M50 T11 不变):BT 真在用时一个 DAC 音量寄存器都没写 */
char trb[2048]; int trl = 0;
int fd = open(trace11, O_RDONLY);
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
if (trl > 0) trb[trl] = 0; else trb[0] = 0;
check(!strstr(trb, "numid=1 ") && !strstr(trb, "numid=2 ") && !strstr(trb, "numid=11 "),
"bt vol set writes NO DAC registers (numid=1/2/11 absent from amixer trace)");
}
/* MSG_AUDIO_OUTPUT 切回 DAC */
{
@@ -859,9 +912,19 @@ int main(void)
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
check(has40, "dac vol set persists audio.volume=40");
}
{
/* 反向断言:切回 DAC 后同一个音量设置**必须**落 DAC(证明上面的空轨迹是隔离而非没写) */
char trb[2048]; int trl = 0;
int fd = open(trace11, O_RDONLY);
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
if (trl > 0) trb[trl] = 0; else trb[0] = 0;
check(strstr(trb, "numid=1 ") && strstr(trb, "numid=2 ") && strstr(trb, "numid=11 "),
"after switch to dac the same vol set DOES write numid=1/2/11");
}
do_ctrl(&b11, MSG_AUDIO_STOP, ack11.stream_id);
close(b11.fd);
stop_audiod();
unlink(trace11);
/* ---- T12:24/32bit 高清流 byte-exact(A3 高清实测;FILE sink 按请求保真,S32 升位不降位)。
* ① S32@192000:open 应 grant S32(FILE/NULL sink 按 native 高保真透传);写已知 32-bit 交织帧,
@@ -1159,6 +1222,144 @@ int main(void)
stop_audiod();
unlink(fake17); unlink(trace17);
/* ---- T18:BT 软件增益(M55)——**客观量化**,不靠耳朵。
* target=bt + FILE sink 就是 BT 写路径(grant fmt 恒 S16),捕获的 WAV 就是写进 PCM 的样本。
* 逐档起独立实例(settings audio.volume = pct),写同一段 440Hz 正弦(同相位同幅度),
* 量 data 段 RMS/峰值:①随 pct 单调下降 ②vol=0 全 0(真静音)③vol=100 与输入逐字节相同
* ④实测峰值 = 输入峰值 × 增益表(曲线与 dac_vol_set 同源,不是线性)。 */
printf("[T18 BT software gain: volume ladder via wav capture]\n");
{
const int pcts18[] = { 100, 92, 75, 50, 25, 10, 0 };
const int expreg18[] = { 0, 1, 12, 48, 108, 155, 255 };
enum { NL = 7, NFR = 4096 };
static uint8_t tone18[NFR * 4];
static double rms18[NL];
static int peak18[NL];
const double amp_in = 0.5 * 32767.0;
int peak_in = (int)(amp_in + 0.5);
for (int i = 0; i < NFR; i++) {
double ph = 2.0 * M_PI * 440.0 * (double)i / 44100.0;
int16_t v = (int16_t)(sin(ph) * amp_in);
memcpy(tone18 + i * 4, &v, 2);
memcpy(tone18 + i * 4 + 2, &v, 2);
}
printf(" input tone: 440Hz S16 stereo, peak=%d (RMS %.1f), %d frames\n",
peak_in, peak_in / sqrt(2.0), NFR);
printf(" %-5s %-5s %-9s %-6s %-9s %-6s %s\n", "pct", "reg", "curve-dB", "peak", "rms", "gain", "maxdelta");
for (int k = 0; k < NL; k++) {
int pct = pcts18[k];
unlink(TSOCK); unlink(TWAV);
char conf18[96];
snprintf(conf18, sizeof conf18, "/tmp/meow_test_settings18_%d.conf", pct);
FILE *f18 = fopen(conf18, "w");
if (f18) { fprintf(f18, "# meow-settings 1\naudio.volume = %d\naudio.output_bt = 1\n", pct); fclose(f18); }
g_audiod_pid = start_audiod_settings("file", conf18);
usleep(350000);
struct cli c18;
if (cli_connect(&c18) != 0) { check(0, "T18 connect"); break; }
m_audio_open_ack_t a18;
if (do_open(&c18, 44100, 2, AUDIO_FMT_S16, &a18) != 0 || a18.err != MEOW_OK) {
check(0, "T18 open"); close(c18.fd); stop_audiod(); break;
}
for (size_t off = 0; off < sizeof tone18; off += 4096) {
size_t left = sizeof tone18 - off;
do_write(&c18, tone18 + off, (uint32_t)(left < 4096 ? left : 4096));
}
wait_wav_open(TWAV, 3000);
usleep(250000);
do_ctrl(&c18, MSG_AUDIO_STOP, a18.stream_id);
close(c18.fd);
stop_audiod();
struct wavstat st = wav_s16_stats(TWAV);
if (!st.ok) { check(0, "T18 wav readable"); continue; }
peak18[k] = st.peak;
rms18[k] = st.rms;
double g = 32768.0 * pow(10.0, -0.5 * (double)expreg18[k] / 20.0) / 32768.0;
printf(" %-5d %-5d %-9.2f %-6d %-9.2f %-6.6f %d\n",
pct, expreg18[k], -0.5 * (double)expreg18[k], st.peak, st.rms, g, st.maxdelta);
/* 实测峰值必须整落到曲线上(±2 LSB 覆盖整数四舍五入) */
int expect_peak = (int)(peak_in * g + 0.5);
char nm[96];
snprintf(nm, sizeof nm, "vol=%d measured peak == input*curve (%d vs %d)", pct, st.peak, expect_peak);
check(abs(st.peak - expect_peak) <= 2, nm);
if (pct == 0) check(st.peak == 0 && st.rms == 0.0, "vol=0 -> WAV data all zeros (true mute)");
if (pct == 100)
check(wav_byte_exact(TWAV, 44100, 2, 16, tone18, sizeof tone18) == 0,
"vol=100 -> WAV byte-exact with input (unity, no clipping)");
unlink(conf18);
}
/* 单调:pct 越高越响(严格下降,因为每档差 ≥0.5dB 且在 16bit 可分辨) */
int mono = 1;
for (int k = 0; k + 1 < NL; k++)
if (!(peak18[k] > peak18[k + 1] && rms18[k] > rms18[k + 1])) mono = 0;
check(mono, "RMS and peak decrease monotonically with pct (100>92>75>50>25>10>0)");
check(peak18[0] > 0 && peak18[NL - 1] == 0, "ladder spans full scale -> silence");
}
/* ---- T19:改音量不爆音 + 不丢帧(M55)。播 100% 中途发 vol=10:增益必须走 512 样本斜坡,
* 相邻样本最大跳变不得接近「一步跳到位」的量级(那会听见「叮」)。 */
printf("[T19 live volume change: ramp, no click, no drops]\n");
{
unlink(TSOCK); unlink(TWAV);
const char *conf19 = "/tmp/meow_test_settings19.conf";
FILE *f19 = fopen(conf19, "w");
if (f19) { fprintf(f19, "# meow-settings 1\naudio.volume = 100\naudio.output_bt = 1\n"); fclose(f19); }
g_audiod_pid = start_audiod_settings("file", conf19);
usleep(350000);
struct cli c19;
if (cli_connect(&c19) != 0) { check(0, "T19 connect"); }
else {
m_audio_open_ack_t a19;
if (do_open(&c19, 44100, 2, AUDIO_FMT_S16, &a19) != 0 || a19.err != MEOW_OK) {
check(0, "T19 open");
} else {
enum { NFR19 = 4096 };
static uint8_t t19[NFR19 * 4];
static int16_t s19[NFR19 * 2];
for (int i = 0; i < NFR19 * 2; i++) { /* 相位连续:中途切音量不制造跳变 */
double ph = 2.0 * M_PI * 440.0 * (double)(i / 2) / 44100.0;
s19[i] = (int16_t)(sin(ph) * 0.5 * 32767.0);
}
memcpy(t19, s19, sizeof t19);
for (size_t off = 0; off < sizeof t19 / 2; off += 4096) { /* 前半:vol=100 */
size_t left = sizeof t19 / 2 - off;
do_write(&c19, t19 + off, (uint32_t)(left < 4096 ? left : 4096));
}
usleep(200000);
int32_t pct = 10; /* 播放中途降音量 */
if (sf(c19.fd, MSG_AUDIO_VOLUME, 19, &pct, sizeof pct) == 0) {
uint8_t ab[64]; uint16_t at; rf(c19.fd, &at, ab, sizeof ab);
}
usleep(100000);
for (size_t off = sizeof t19 / 2; off < sizeof t19; off += 4096) { /* 后半:vol=10 */
size_t left = sizeof t19 - off;
do_write(&c19, t19 + off, (uint32_t)(left < 4096 ? left : 4096));
}
char q19[1024];
do_query(&c19, q19, sizeof q19);
check(strstr(q19, "vol=10") != NULL, "query reflects new vol=10 while streaming");
check(strstr(q19, "underrun=0") != NULL && strstr(q19, "dropped=0") != NULL,
"no underrun/dropped frames during live volume change");
wait_wav_open(TWAV, 3000);
usleep(250000);
do_ctrl(&c19, MSG_AUDIO_STOP, a19.stream_id);
close(c19.fd);
stop_audiod();
struct wavstat st = wav_s16_stats(TWAV);
check(st.ok && st.nsamp > 4000, "T19 wav captured");
if (st.ok) {
/* 440Hz@44.1k 正弦本身相邻样本差 ≈ 1027;硬跳变会是 ~16380。
* 斜坡后应 ≤ ~1100。阈值 2000 只放过正弦自身的斜率。 */
printf(" (max adjacent-sample delta = %d; hard gain jump would be ~16380)\n", st.maxdelta);
check(st.maxdelta < 2000, "volume change ramps (no hard jump = no click)");
check(st.peak > 0, "audio still audible after change");
}
}
}
stop_audiod();
unlink(conf19);
}
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}