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
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user