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

+7 -4
View File
@@ -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 if ! printf 'int main(void){return 0;}\n' | "$HOST_CC" -x c - -o /dev/null "$AS_LINK" 2>/dev/null; then
AS_LINK="-lasound" AS_LINK="-lasound"
fi 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 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" $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_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 ==" echo "== host builds =="
ls -la 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 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) ==" echo "== host regression (test_bt_dev: BT DEV auto-detect) =="
./test_bt_dev || { echo "!! test_bt_dev FAILED"; exit 1; } ./test_bt_dev || { echo "!! test_bt_dev FAILED"; exit 1; }
echo "== host regression (test_audiod_host) ==" 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 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"
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2" 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 -lpthread -L"$LIBBT" -lasound
echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) ==" echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) =="
ls -la meow_audiod ls -la meow_audiod
+51 -14
View File
@@ -60,6 +60,7 @@
#include <sys/wait.h> #include <sys/wait.h>
#include "meow_ipc.h" #include "meow_ipc.h"
#include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */ #include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */
#include "vol_gain.h" /* M55:BT 写路径软件增益(与 DAC 同一锥度,全整数) */
#ifdef __linux__ #ifdef __linux__
#include <sound/asound.h> #include <sound/asound.h>
@@ -118,7 +119,7 @@ static char g_out_wav[256] = DEFAULT_OUT_WAV;
static const char *g_amixer = "/usr/bin/amixer"; /* 环境可覆盖 */ 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_manual = -1; /* -1=auto;2=3.5;3=4.4 */
static int g_route_cur = -1; /* 最近已知路由;-1=未知 */ 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 测试) */ 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_fail_req = 0; /* 写路径判死:请求播放线程下一拍重配(回落 DAC) */
static int g_bt_dev = 0; /* 当前「设备」是 BT PCM(dev_close 分流;g_mu 保护) */ 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 样本) ---- /* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ----
* 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16), * 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16),
* ~100ms 窗口(rate/10 帧)到点才结算一次 RMS/peak 并发布快照——每帧仅几次整数加乘, * ~100ms 窗口(rate/10 帧)到点才结算一次 RMS/peak 并发布快照——每帧仅几次整数加乘,
@@ -397,8 +404,10 @@ static int target_grant_fmt(int native_fmt)
return AUDIO_FMT_S16; return AUDIO_FMT_S16;
} }
/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。 /* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙路径(AVRCP 侧 + 写路径软件增益)。
* BT 回落 DAC(bt=fail)时实际由 DAC 控,故报 dac——面板据此显示「蓝牙(失败)」。 */ * BT 回落 DAC(bt=fail)时实际由 DAC 控,故报 dac——面板据此显示「蓝牙(失败)」。
* ⚠️ M55 起 BT 的实际音量是 dev_write 里的软件增益(AVRCP 只负责把耳机功放钉在满量程),
* 但 `volctl=avrcp` 作为面板/launcher 已在用的既有取值**保持不变**(改值属跨组件契约变更)。 */
static const char *volctl_name(void) static const char *volctl_name(void)
{ {
if (g_target != AUDIO_OUT_BT) return "dac"; 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) static void dac_vol_set(int pct)
{ {
int t = 100 - pct, dac; /* 锥度**只有一处实现**(vol_gain_dac_att_reg,M55 起 BT 软件增益也用同一张表):
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */ * CS43198 numid=1/2 = 衰减量(0 最响、255=-127.5dB、0.5dB/步)+ 平方锥度(听感线性)。 */
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */ int dac = vol_gain_dac_att_reg(pct);
if (dac > 255) dac = 255;
if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
/* 音量收编 v2:**泛化双组写**——同刻写 PCM(numid=1/2) 与 DSD_VOLUME,保证 DSD↔PCM 切换 /* 音量收编 v2:**泛化双组写**——同刻写 PCM(numid=1/2) 与 DSD_VOLUME,保证 DSD↔PCM 切换
* 音量连续 + 防爆音闭环(决策①,勿只写 PCM)。锥度沿用平方(决策②)。 */ * 音量连续 + 防爆音闭环(决策①,勿只写 PCM)。锥度沿用平方(决策②)。 */
amixer_set(1, dac, 1); amixer_set(1, dac, 1);
@@ -919,7 +926,8 @@ static int settings_get_volume(void)
* 把播放器解码输出透传过去。BT 是**独立目标**: * 把播放器解码输出透传过去。BT 是**独立目标**:
* - 不开 hw:0,0(不 period=1024×4、不 FPGA numid=9 武装、不 DAC 音量反转); * - 不开 hw:0,0(不 period=1024×4、不 FPGA numid=9 武装、不 DAC 音量反转);
* - 保留音频流透传 + 格式归一(grant_fmt=S16,plug 层转 codec 协商率); * - 保留音频流透传 + 格式归一(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 * 链接说明(2026-09-03 真机定案):`pcm.bluealsa` 的 bluealsa 是**独立 pcm 插件**,libasound
* 在运行时以 dlopen 加载 /usr/lib/alsa-lib/libasound_module_pcm_bluealsa.so(glibc,依赖 * 在运行时以 dlopen 加载 /usr/lib/alsa-lib/libasound_module_pcm_bluealsa.so(glibc,依赖
* libdbus-1.so.3)——故 audiod 必须以**动态 glibc** 形态链接设备现有 libasound.so.2 * 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、 /* 音量应用(含 BT 隔离):DAC 及「BT 回落 DAC」→ 感知锥度反转 + 持久化(回落时声音实际从
* 不持久化 audio.volume(那是 DAC 音量键);DAC 及「BT 回落 DAC」→ 原感知锥度反转 + 持久化 * DAC 出,音量必须由 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 * target 为调用方在持锁下快照的 g_target;本函数不持 g_mu 执行(g_vol_pct 为 plain int
* 松散写,同 route_thread 的 g_route_cur 语义,MIPS32 int 写天然原子)。 */ * 松散写,同 route_thread 的 g_route_cur 语义,MIPS32 int 写天然原子)。 */
static void vol_apply(int pct, int target) 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 < 0) pct = 0;
if (pct > 100) pct = 100; if (pct > 100) pct = 100;
if (target == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL) { 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; return;
} }
g_vol_pct = pct; g_vol_pct = pct;
@@ -1421,6 +1437,10 @@ static int fd_high(int fd)
static int dev_open(void) 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)。 /* 输出目标=BT 且走真机 ALSA:打开 libasound 的 bluealsa PCM(内联 DEV/CODEC,不开 hw:0,0)。
* 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑。 * 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑。
* **失败必回落 DAC**(M50 硬验收:没有 A2DP 端点时整机不许静音;同时区间由 bt_note_fail * **失败必回落 DAC**(M50 硬验收:没有 A2DP 端点时整机不许静音;同时区间由 bt_note_fail
@@ -1480,6 +1500,20 @@ static void dev_write(uint8_t *buf, size_t n)
} }
g_dev_bytes += 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 (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_NULL) { g_played_frames += n / (size_t)frame_size(); return; }
if (g_sink == SINK_ALSA) { if (g_sink == SINK_ALSA) {
@@ -2114,7 +2148,8 @@ static void *io_thread(void *arg)
/* ---- 音量执行线程(事项三):异步落 amixer + 持久化,不在 IO 线程持锁 popen ---- /* ---- 音量执行线程(事项三):异步落 amixer + 持久化,不在 IO 线程持锁 popen ----
* 等待 g_vol_cond 取末值(连续请求合并=去抖),放锁后执行 dac_vol_set(3×popen,有界) * 等待 g_vol_cond 取末值(连续请求合并=去抖),放锁后执行 dac_vol_set(3×popen,有界)
* + settings_set_volume(文件 IO),完成后只置 ACK 标记,由 IO 线程统一发送(防 fd 复用竞态)。 * + 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) static void *vol_thread(void *arg)
{ {
(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(); if (g_sink == SINK_ALSA && g_target == AUDIO_OUT_DAC) hw_probe();
g_vol_pct = settings_get_volume(); 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", logmsg("volume init = %d%% target=%s codec=%s volctl=%s",
g_vol_pct, target_name(), g_bt_codec, volctl_name()); g_vol_pct, target_name(), g_bt_codec, volctl_name());
+209 -8
View File
@@ -346,8 +346,49 @@ static int wait_wav_open(const char *path, int tmo_ms)
return -1; 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) int main(void)
{ {
setbuf(stdout, NULL); /* 无缓冲:崩溃/卡死时也能看到跑到哪一档(回归排障) */
unlink(TSOCK); unlink(TWAV); unlink(TSOCK); unlink(TWAV);
g_audiod_pid = start_audiod(); g_audiod_pid = start_audiod();
if (g_audiod_pid < 0) { printf("cannot fork audiod\n"); return 1; } 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(eok, "fresh OPEN after overflow (table recycled, no exhaustion/wedge)");
check(stop_audiod_tmo(5000) != 0, "audiod not wedged by overflow (SIGTERM clean exit)"); 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 传输留给真机),验证目标选择/音量隔离/格式逻辑: * 宿主以 FILE sink 跑(真 bluealsa 传输留给真机),验证目标选择/音量隔离/格式逻辑:
* - settings.conf `audio.output_bt=1` + `bt.codec=SBC` → target=bt / codec=SBC / volctl=avrcp / grant fmt=S16; * - 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、 * - BT 下 MSG_AUDIO_VOLUME 设置(M55 语义):g_vol_pct 与持久化**必须更新**(旧版提前 return
* volctl 仍 avrcp(隔离成立); * 什么都不做 → 重启音量跳回去),但**绝不写 DAC numid=1/2/11**(amixer 轨迹必须为空),
* volctl 仍 avrcp —— 音量改由写路径的软件增益生效(曲线/单调性/静音见 T18 + test_vol_gain);
* - MSG_AUDIO_OUTPUT 切回 DAC:target=dac / volctl=dac;此后音量设置写 DAC + 持久化 audio.volume。 * - MSG_AUDIO_OUTPUT 切回 DAC:target=dac / volctl=dac;此后音量设置写 DAC + 持久化 audio.volume。
* 断言:BT 路径绝不动 DAC 音量(plan §三/§五「两套隔离」);BT 格式归一=S16;codec 读到生效。 ---- */ * 断言: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); unlink(TSOCK); unlink(TWAV);
const char *tconf = "/tmp/meow_test_settings.conf"; const char *tconf = "/tmp/meow_test_settings.conf";
const char *trace11 = "/tmp/meow_amixer_trace11.txt";
unlink(trace11);
FILE *tf = fopen(tconf, "w"); 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); } 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); usleep(400000);
struct cli b11; struct cli b11;
check(cli_connect(&b11) == 0, "connect B11"); check(cli_connect(&b11) == 0, "connect B11");
@@ -818,7 +862,7 @@ int main(void)
/* BT/file sink 写一段流(应正常落盘到 WAV) */ /* 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)); 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); 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; int32_t pct = 40;
if (sf(b11.fd, MSG_AUDIO_VOLUME, 8, &pct, sizeof pct) == 0) { 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"), check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "volctl=avrcp"),
"vol set while bt -> volctl still avrcp (isolation)"); "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]; 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); } 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 */ /* 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); } 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"); 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); do_ctrl(&b11, MSG_AUDIO_STOP, ack11.stream_id);
close(b11.fd); close(b11.fd);
stop_audiod(); stop_audiod();
unlink(trace11);
/* ---- T12:24/32bit 高清流 byte-exact(A3 高清实测;FILE sink 按请求保真,S32 升位不降位)。 /* ---- T12:24/32bit 高清流 byte-exact(A3 高清实测;FILE sink 按请求保真,S32 升位不降位)。
* ① S32@192000:open 应 grant S32(FILE/NULL sink 按 native 高保真透传);写已知 32-bit 交织帧, * ① S32@192000:open 应 grant S32(FILE/NULL sink 按 native 高保真透传);写已知 32-bit 交织帧,
@@ -1159,6 +1222,144 @@ int main(void)
stop_audiod(); stop_audiod();
unlink(fake17); unlink(trace17); 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); printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0; return g_fail ? 1 : 0;
} }
+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;
}
+116
View File
@@ -0,0 +1,116 @@
/* vol_gain.c — 纯整数增益表 + 缩放/斜坡(宿主与目标机共用)。见 vol_gain.h 的定性说明。 */
#include "vol_gain.h"
#include <string.h>
/* 每衰减步 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);
}
}
+61
View File
@@ -0,0 +1,61 @@
/* vol_gain.h — 蓝牙(A2DP)写路径的软件音量增益(M55)。
*
* 为什么需要:BT 输出时 AVRCP 被钉在 127(耳机功放满量程),`vol_apply()` 旧版在 BT 分支
* 直接 return——音量在整条 BT 路径上被跳过,`meow_audio vol <n>` 完全无效(真机现象:
* 音量 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 <stdint.h>
#include <stddef.h>
#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