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:
1 parent
4040b41649
commit
e37c622597
6 files changed
+678
-26
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user