diff --git a/patch_v2/meow_audiod/build_audiod.sh b/patch_v2/meow_audiod/build_audiod.sh index a7b92c2..8f51bd2 100755 --- a/patch_v2/meow_audiod/build_audiod.sh +++ b/patch_v2/meow_audiod/build_audiod.sh @@ -60,13 +60,16 @@ if [ "$1" = "host" ]; then if ! printf 'int main(void){return 0;}\n' | "$HOST_CC" -x c - -o /dev/null "$AS_LINK" 2>/dev/null; then AS_LINK="-lasound" fi - "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread $AS_LINK + "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c vol_gain.c -o meow_audiod_host -lpthread $AS_LINK "$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm "$HOST_CC" $INC -Os -Wall -Wextra test_audiod_host.c -o test_audiod_host -lm "$HOST_CC" -Os -Wall -Wextra test_bt_dev.c bt_dev.c -o test_bt_dev -lm + "$HOST_CC" -Os -Wall -Wextra test_vol_gain.c vol_gain.c -o test_vol_gain -lm echo "== host builds ==" - ls -la meow_audiod_host meow_audio_host test_audiod_host test_bt_dev - file meow_audiod_host meow_audio_host test_audiod_host test_bt_dev + ls -la meow_audiod_host meow_audio_host test_audiod_host test_bt_dev test_vol_gain + file meow_audiod_host meow_audio_host test_audiod_host test_bt_dev test_vol_gain + echo "== host regression (test_vol_gain: BT 增益表/缩放/斜坡) ==" + ./test_vol_gain || { echo "!! test_vol_gain FAILED"; exit 1; } echo "== host regression (test_bt_dev: BT DEV auto-detect) ==" ./test_bt_dev || { echo "!! test_bt_dev FAILED"; exit 1; } echo "== host regression (test_audiod_host) ==" @@ -125,7 +128,7 @@ mkdir -p "$LIBBT" if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so" ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2" -"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod \ +"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c vol_gain.c -o meow_audiod \ -lpthread -L"$LIBBT" -lasound echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) ==" ls -la meow_audiod diff --git a/patch_v2/meow_audiod/meow_audiod.c b/patch_v2/meow_audiod/meow_audiod.c index 32a7a07..c853ad5 100644 --- a/patch_v2/meow_audiod/meow_audiod.c +++ b/patch_v2/meow_audiod/meow_audiod.c @@ -60,6 +60,7 @@ #include #include "meow_ipc.h" #include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */ +#include "vol_gain.h" /* M55:BT 写路径软件增益(与 DAC 同一锥度,全整数) */ #ifdef __linux__ #include @@ -118,7 +119,7 @@ static char g_out_wav[256] = DEFAULT_OUT_WAV; static const char *g_amixer = "/usr/bin/amixer"; /* 环境可覆盖 */ static int g_route_manual = -1; /* -1=auto;2=3.5;3=4.4 */ static int g_route_cur = -1; /* 最近已知路由;-1=未知 */ -static int g_vol_pct = VOL_DEFAULT; /* 当前音量 0-100(缓存:DAC 音量;BT 时 volctl=avrcp) */ +static int g_vol_pct = VOL_DEFAULT; /* 当前音量 0-100(BT 时 = 写路径软件增益的输入,见 vol_apply) */ static int g_force = 0; /* MEOW_AUDIO_FORCE=1 跳过启动门控(宿主/qemu 测试) */ /* ---- 缓冲状态(+日志) ---- */ @@ -228,6 +229,12 @@ static char g_bt_pcm[128] = ""; /* 端点 D-Bus 路径(volume/in static int g_bt_fail_req = 0; /* 写路径判死:请求播放线程下一拍重配(回落 DAC) */ static int g_bt_dev = 0; /* 当前「设备」是 BT PCM(dev_close 分流;g_mu 保护) */ +/* ---- BT 软件增益(M55):AVRCP 钉在 127(耳机功放满量程),音量在写路径逐样本做 ---- + * g_bt_gain 是**播放线程私有**状态(dev_write/dev_open 都只在该线程跑),不持锁、不用原子; + * 控制线程只经 g_vol_pct(单个对齐 int)传目标值,音频线程每块读一次 → 最坏晚一块生效。 + * 见 vol_gain.h:曲线与 dac_vol_set 同源(0.5dB/步、平方锥度),100%=逐字节不变,0%=全 0。 */ +static vol_gain_ramp_t g_bt_gain; + /* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ---- * 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16), * ~100ms 窗口(rate/10 帧)到点才结算一次 RMS/peak 并发布快照——每帧仅几次整数加乘, @@ -397,8 +404,10 @@ static int target_grant_fmt(int native_fmt) return AUDIO_FMT_S16; } -/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。 - * BT 回落 DAC(bt=fail)时实际由 DAC 控,故报 dac——面板据此显示「蓝牙(失败)」。 */ +/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙路径(AVRCP 侧 + 写路径软件增益)。 + * BT 回落 DAC(bt=fail)时实际由 DAC 控,故报 dac——面板据此显示「蓝牙(失败)」。 + * ⚠️ M55 起 BT 的实际音量是 dev_write 里的软件增益(AVRCP 只负责把耳机功放钉在满量程), + * 但 `volctl=avrcp` 作为面板/launcher 已在用的既有取值**保持不变**(改值属跨组件契约变更)。 */ static const char *volctl_name(void) { if (g_target != AUDIO_OUT_BT) return "dac"; @@ -660,11 +669,9 @@ static int g_dsd_active = 0; /* 当前授权流为 DoP DSD(g_mu 保护;决 static void dac_vol_set(int pct) { - int t = 100 - pct, dac; - if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */ - else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */ - if (dac > 255) dac = 255; - if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */ + /* 锥度**只有一处实现**(vol_gain_dac_att_reg,M55 起 BT 软件增益也用同一张表): + * CS43198 numid=1/2 = 衰减量(0 最响、255=-127.5dB、0.5dB/步)+ 平方锥度(听感线性)。 */ + int dac = vol_gain_dac_att_reg(pct); /* 音量收编 v2:**泛化双组写**——同刻写 PCM(numid=1/2) 与 DSD_VOLUME,保证 DSD↔PCM 切换 * 音量连续 + 防爆音闭环(决策①,勿只写 PCM)。锥度沿用平方(决策②)。 */ amixer_set(1, dac, 1); @@ -919,7 +926,8 @@ static int settings_get_volume(void) * 把播放器解码输出透传过去。BT 是**独立目标**: * - 不开 hw:0,0(不 period=1024×4、不 FPGA numid=9 武装、不 DAC 音量反转); * - 保留音频流透传 + 格式归一(grant_fmt=S16,plug 层转 codec 协商率); - * - 音量走 AVRCP/耳机侧(音量隔离:绝不下发 DAC numid=1/2 反转,见 vol_apply)。 + * - 音量:AVRCP 钉在 127(耳机功放满量程),实际音量在**写路径做软件增益**(M55)—— + * 仍绝不下发 DAC numid=1/2 反转(见 vol_apply / dev_write)。 * 链接说明(2026-09-03 真机定案):`pcm.bluealsa` 的 bluealsa 是**独立 pcm 插件**,libasound * 在运行时以 dlopen 加载 /usr/lib/alsa-lib/libasound_module_pcm_bluealsa.so(glibc,依赖 * libdbus-1.so.3)——故 audiod 必须以**动态 glibc** 形态链接设备现有 libasound.so.2 @@ -1226,9 +1234,11 @@ static void bt_read_used_codec(void) } } -/* 音量应用(含 BT 隔离):BT(真实走 A2DP)→ 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、 - * 不持久化 audio.volume(那是 DAC 音量键);DAC 及「BT 回落 DAC」→ 原感知锥度反转 + 持久化 - * (回落时声音实际从 DAC 出,音量必须由 DAC 控,否则面板拖动无感)。 +/* 音量应用(含 BT 隔离):DAC 及「BT 回落 DAC」→ 感知锥度反转 + 持久化(回落时声音实际从 + * DAC 出,音量必须由 DAC 控,否则面板拖动无感)。 + * BT(真实走 A2DP)→ **不写 DAC numid=1/2**(M50 T11 隔离断言),音量改由写路径的软件增益 + * 生效(M55,见 vol_gain.h)——但 g_vol_pct 与持久化**必须更新**:旧版在这里 logmsg+return, + * 连 g_vol_pct 都不动,于是 BT 音量永远停在满量程、重启又跳回去(真机「音量 10 还是很响」)。 * target 为调用方在持锁下快照的 g_target;本函数不持 g_mu 执行(g_vol_pct 为 plain int * 松散写,同 route_thread 的 g_route_cur 语义,MIPS32 int 写天然原子)。 */ static void vol_apply(int pct, int target) @@ -1236,7 +1246,13 @@ static void vol_apply(int pct, int target) if (pct < 0) pct = 0; if (pct > 100) pct = 100; if (target == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL) { - logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct); + /* 只写 g_vol_pct(音频线程每块读一次 → 起 512 样本斜坡,不「叮」)+ 持久化。 + * AVRCP 仍保持 bt_vol_full() 设的 127(耳机功放满量程,增益全在数字域)。 */ + g_vol_pct = pct; + settings_set_volume(pct); + int reg = vol_gain_dac_att_reg(pct); /* 与 DAC 同一个衰减步数(0.5dB/步) */ + logmsg("vol -> %d%% (BT software gain q15=%d/32768, -%d.%ddB, AVRCP stays 127, DAC numid=1/2 NOT written)", + pct, vol_gain_q15(pct), (reg * 5) / 10, (reg * 5) % 10); return; } g_vol_pct = pct; @@ -1421,6 +1437,10 @@ static int fd_high(int fd) static int dev_open(void) { + /* M55:开流边界把 BT 软件增益**直接对齐**到当前音量(新流不做淡入——否则每首歌开头 + * 会有 11ms 的渐强)。判据与 vol_apply 的 BT 分支一致:请求 BT 且未回落 DAC。 */ + if (g_target == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL) + vol_gain_ramp_snap(&g_bt_gain, g_vol_pct); /* 输出目标=BT 且走真机 ALSA:打开 libasound 的 bluealsa PCM(内联 DEV/CODEC,不开 hw:0,0)。 * 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑。 * **失败必回落 DAC**(M50 硬验收:没有 A2DP 端点时整机不许静音;同时区间由 bt_note_fail @@ -1480,6 +1500,20 @@ static void dev_write(uint8_t *buf, size_t n) } g_dev_bytes += n; } + /* M55 蓝牙软件增益:**BT 路径唯一真正控制音量的地方**(AVRCP 钉 127 = 耳机功放满量程)。 + * 判据与 vol_apply 的 BT 分支一致(target=BT 且未回落 DAC)——回落时声音由 DAC 出, + * 音量必须交给 DAC numid=1/2,数字域不得再衰减一次(否则会是「两套音量相乘」)。 + * 缓冲此时恒为 S16:target_grant_fmt() 对 BT 目标一律授 S16(ALSA/FILE/NULL 三种 sink 同理)。 + * 每块只读一次 g_vol_pct(控制线程单点松散写);斜坡状态只在本线程。 */ + if (g_target == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL) { + if (g_dev_fmt == AUDIO_FMT_S16) { + vol_gain_ramp_set(&g_bt_gain, g_vol_pct, VOL_GAIN_RAMP_SAMPLES); + vol_gain_ramp_apply(&g_bt_gain, buf, n); + } else { + static int s_lg = 0; + if (!s_lg) { s_lg = 1; logmsg("bt gain: skip (device fmt=%s, expected S16)", fmt_name((int)g_dev_fmt)); } + } + } if (bt_effective_bt()) { bt_write(buf, n); return; } if (g_sink == SINK_NULL) { g_played_frames += n / (size_t)frame_size(); return; } if (g_sink == SINK_ALSA) { @@ -2114,7 +2148,8 @@ static void *io_thread(void *arg) /* ---- 音量执行线程(事项三):异步落 amixer + 持久化,不在 IO 线程持锁 popen ---- * 等待 g_vol_cond 取末值(连续请求合并=去抖),放锁后执行 dac_vol_set(3×popen,有界) * + settings_set_volume(文件 IO),完成后只置 ACK 标记,由 IO 线程统一发送(防 fd 复用竞态)。 - * BT 目标只记日志不动 DAC/不持久化(AVRCP 隔离,g_vol_pct 保持不变)。 */ + * BT 目标:不写 DAC、不 popen,只更新 g_vol_pct(音频线程据此走软件增益)+ 持久化 + * (M55 起持久化必须做——否则重启后音量跳回去,见 vol_apply)。 */ static void *vol_thread(void *arg) { (void)arg; @@ -2373,6 +2408,8 @@ int main(int argc, char **argv) } if (g_sink == SINK_ALSA && g_target == AUDIO_OUT_DAC) hw_probe(); g_vol_pct = settings_get_volume(); + vol_gain_init(); /* M55:BT 增益表开机算一次(全整数,无浮点) */ + vol_gain_ramp_snap(&g_bt_gain, g_vol_pct); /* 首个流直接以当前音量起(不淡入) */ logmsg("volume init = %d%% target=%s codec=%s volctl=%s", g_vol_pct, target_name(), g_bt_codec, volctl_name()); diff --git a/patch_v2/meow_audiod/test_audiod_host.c b/patch_v2/meow_audiod/test_audiod_host.c index 2bcaccd..17715ac 100644 --- a/patch_v2/meow_audiod/test_audiod_host.c +++ b/patch_v2/meow_audiod/test_audiod_host.c @@ -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; } \ No newline at end of file diff --git a/patch_v2/meow_audiod/test_vol_gain.c b/patch_v2/meow_audiod/test_vol_gain.c new file mode 100644 index 0000000..51898ce --- /dev/null +++ b/patch_v2/meow_audiod/test_vol_gain.c @@ -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 +#include +#include +#include +#include + +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; +} diff --git a/patch_v2/meow_audiod/vol_gain.c b/patch_v2/meow_audiod/vol_gain.c new file mode 100644 index 0000000..6ba9e19 --- /dev/null +++ b/patch_v2/meow_audiod/vol_gain.c @@ -0,0 +1,116 @@ +/* vol_gain.c — 纯整数增益表 + 缩放/斜坡(宿主与目标机共用)。见 vol_gain.h 的定性说明。 */ +#include "vol_gain.h" + +#include + +/* 每衰减步 0.5dB 的振幅系数:10^(-0.5/20) = 10^(-0.025) = 0.9440608762859234。 + * Q31 = round(0.9440608762859234 × 2^31) = 2027355295(离线算出并钉死;运行期不碰浮点)。 */ +#define VOL_GAIN_STEP_Q31 2027355295u + +static int32_t s_gain_q15[256]; /* 下标=衰减寄存器值;只读(init 之后) */ +static int s_gain_ready = 0; + +/* 衰减步 → 增益的整数迭代:acc 以 Q31(15 位整数 + 16 位小数)表示 Q15 增益, + * 每步 acc = acc × STEP >> 31。255 步累计相对误差 ~2^-23,实测与 pow() 相差 ≤1 LSB。 */ +void vol_gain_init(void) +{ + if (s_gain_ready) return; + uint64_t acc = (uint64_t)VOL_GAIN_UNITY << 16; + for (int reg = 0; reg < 256; reg++) { + int32_t g = (int32_t)((acc + 32768u) >> 16); + if (g > VOL_GAIN_UNITY) g = VOL_GAIN_UNITY; + s_gain_q15[reg] = g; + acc = (acc * VOL_GAIN_STEP_Q31 + (1u << 30)) >> 31; + } + s_gain_ready = 1; +} + +/* dac_vol_set() 的锥度原样搬来(平方锥度;255=-127.5dB)。**单一出处**:dac_vol_set 与 + * BT 增益表都取这个值,谁都不许自己再算一遍,否则 DAC/BT 曲线会各走各的。 */ +int vol_gain_dac_att_reg(int pct) +{ + if (pct <= 0) return 255; /* 真静音 */ + if (pct > 100) pct = 100; + int reg = (100 - pct) * (100 - pct) * 192 / 10000; + if (reg > 255) reg = 255; + if (reg < 0) reg = 0; + return reg; +} + +int vol_gain_q15(int pct) +{ + if (!s_gain_ready) vol_gain_init(); /* 未初始化就查=防御(正常由 main 先 init) */ + if (pct <= 0) return 0; /* vol=0 必须真静音,不给「很小声」留余地 */ + return s_gain_q15[vol_gain_dac_att_reg(pct)]; +} + +int16_t vol_gain_scale_s16(int16_t in, int g_q15) +{ + int32_t v = (int32_t)in * g_q15; /* |in|≤32768 × g≤32768 → |v|≤2^30,int32 不溢出 */ + v = (v + (1 << 14)) >> 15; /* 四舍五入回 S16 量程(单位增益时恒等) */ + if (v > 32767) return 32767; + if (v < -32768) return -32768; + return (int16_t)v; +} + +void vol_gain_ramp_snap(vol_gain_ramp_t *r, int pct) +{ + r->cur_q15 = r->tgt_q15 = vol_gain_q15(pct); + r->step_q15 = 0; + r->bump = 0; + r->bump_left = 0; + r->left = 0; +} + +void vol_gain_ramp_set(vol_gain_ramp_t *r, int pct, int ramp_samples) +{ + int32_t tgt = vol_gain_q15(pct); + if (tgt == r->tgt_q15) return; /* 目标没变:不重置进行中的斜坡 */ + r->tgt_q15 = tgt; + int32_t d = tgt - r->cur_q15; + if (ramp_samples <= 0 || d == 0) { + r->cur_q15 = tgt; + r->step_q15 = 0; + r->bump = 0; + r->bump_left = 0; + r->left = 0; + return; + } + /* 线性斜坡 = 固定步进 + 余额分摊(不除法的 Bresenham): + * n 个样本走完 d,step=d/n 截断,余数 rem 按 ±1 分摊到前 |rem| 个样本。 + * 好处:**最后一步不会突然补差**(旧版 step 截断后末尾要一次跳 500+ LSB = 爆音), + * 全程每样本增益变化 ≤ |step|+1,且终点精确等于目标(无过冲)。 */ + int32_t ad = (d > 0) ? d : -d; + int32_t n = ramp_samples; + if (n > ad) n = ad; /* 增益差比斜坡短:缩短斜坡(步进仍至少 1 LSB) */ + int32_t step = d / n; + int32_t rem = d - step * n; /* |rem| < n,与 d 同号 */ + r->left = n; + r->step_q15 = step; + r->bump = (rem > 0) ? 1 : -1; + r->bump_left = (rem > 0) ? rem : -rem; +} + +void vol_gain_ramp_apply(vol_gain_ramp_t *r, uint8_t *buf, size_t nbytes) +{ + size_t ns = nbytes / 2; + if (!buf || !ns) return; + if (r->left <= 0) { + /* 稳态快路:单位增益→原样返回(vol=100 逐字节不变);静音→memset 全 0(vol=0 真静音) */ + if (r->cur_q15 == VOL_GAIN_UNITY) return; + if (r->cur_q15 == 0) { memset(buf, 0, ns * 2); return; } + } + for (size_t i = 0; i < ns; i++) { + int32_t g = r->cur_q15; + if (r->left > 0) { + r->cur_q15 += r->step_q15; + if (r->bump_left > 0) { r->cur_q15 += r->bump; r->bump_left--; } + if (--r->left == 0) r->cur_q15 = r->tgt_q15; /* 兜齐(理论上已精确相等) */ + g = r->cur_q15; + } + int16_t s; + memcpy(&s, buf + i * 2, 2); /* 字节存取:不假设缓冲对齐、不违反严格别名 */ + s = vol_gain_scale_s16(s, g); + memcpy(buf + i * 2, &s, 2); + } +} diff --git a/patch_v2/meow_audiod/vol_gain.h b/patch_v2/meow_audiod/vol_gain.h new file mode 100644 index 0000000..5235ee5 --- /dev/null +++ b/patch_v2/meow_audiod/vol_gain.h @@ -0,0 +1,61 @@ +/* vol_gain.h — 蓝牙(A2DP)写路径的软件音量增益(M55)。 + * + * 为什么需要:BT 输出时 AVRCP 被钉在 127(耳机功放满量程),`vol_apply()` 旧版在 BT 分支 + * 直接 return——音量在整条 BT 路径上被跳过,`meow_audio vol ` 完全无效(真机现象: + * 音量 10 仍是满响度;`bluealsa-cli volume … → L: 127 R: 127`)。 + * 而每次改音量 fork 一次 `bluealsa-cli volume` 约 25–30ms(单核 1GHz),不能放在滑块每一步; + * 大量耳机还忽略 AVRCP absolute volume。→ 定案:AVRCP 保持 127,增益在写路径上做。 + * + * 曲线与 DAC **同源**:CS43198 numid=1/2 是反转衰减(0=0dB,255=-127.5dB,0.5dB/步), + * dac_vol_set() 的锥度是「(1-pct)²×96dB」。这里不重写锥度,而是直接取 + * vol_gain_dac_att_reg(pct)(与 dac_vol_set 同一个函数)→ 增益 = 10^(-0.5dB×reg/20), + * 于是「同一百分比」在 DAC 与 BT 上得到同一条听感曲线(非线性、不重排)。 + * + * 全整数:增益表开机算一次(Q31 定点迭代乘法,不引 libm/软浮点),逐样本一次 32 位乘 + + * 移位 + 饱和;100% 走「单位增益直接返回」快路(逐字节不变、零开销),0% 走 memset。 + */ +#ifndef MEOW_VOL_GAIN_H +#define MEOW_VOL_GAIN_H + +#include +#include + +#define VOL_GAIN_UNITY 32768 /* Q15 单位增益:逐样本 (in*32768+16384)>>15 ≡ in */ +#define VOL_GAIN_RAMP_SAMPLES 512 /* 增益变更斜坡长度(44.1k ≈ 11.6ms;防「叮」一声爆音) */ + +/* 开机调用一次(幂等):生成 256 项增益表。之后查表全只读、无锁。 */ +void vol_gain_init(void); + +/* pct(0..100) → DAC 衰减寄存器 0..255。**dac_vol_set() 也调它**,保证两条路同一条锥度。 */ +int vol_gain_dac_att_reg(int pct); + +/* pct → 线性增益 Q15(32768=单位增益,0=真静音)。pct 越界自动 clamp。 */ +int vol_gain_q15(int pct); + +/* 纯函数:单个 S16 样本按 Q15 增益缩放(32 位中间量 + 四舍五入 + 饱和)。 + * 增益恒 ≤ 32768 → |in*g| ≤ 2^30 不溢出;单位增益时逐样本恒等。 */ +int16_t vol_gain_scale_s16(int16_t in, int g_q15); + +/* ---- 斜坡状态(**音频线程私有**,调用方持有;不做任何加锁) ---- + * 目标值由控制线程经单个对齐 int 松散写进来(MIPS32 int 写天然原子),音频线程每 + * dev_write 读一次即可,最坏晚一帧生效。 */ +typedef struct { + int32_t cur_q15; /* 当前位置 */ + int32_t tgt_q15; /* 目标增益 */ + int32_t step_q15; /* 每样本基础步进 */ + int32_t bump; /* 余额分摊方向(±1,0=无余额) */ + int32_t bump_left; /* 还要分摊多少样本 */ + int32_t left; /* 剩余斜坡样本数(0=已到位) */ +} vol_gain_ramp_t; + +/* 立即跳到 pct 对应增益(开流/换设备/初始化用:新流不该淡入)。 */ +void vol_gain_ramp_snap(vol_gain_ramp_t *r, int pct); + +/* 目标变为 pct 对应增益;与当前目标相同时不动(不打断进行中的斜坡)。 + * ramp_samples<=0 → 立即生效。 */ +void vol_gain_ramp_set(vol_gain_ramp_t *r, int pct, int ramp_samples); + +/* 就地缩放一段 S16_LE 交织 PCM(nbytes 字节;奇数尾字节原样保留)。 */ +void vol_gain_ramp_apply(vol_gain_ramp_t *r, uint8_t *buf, size_t nbytes); + +#endif