From 71b2dafeccc2adce11861bc6fbea1af8e5572b27 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 3 Sep 2026 20:52:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(bt):=20=E8=93=9D=E7=89=99P2=20source?= =?UTF-8?q?=E5=AE=9E=E8=A3=85=20=E2=80=94=20audiod=20OUT=5FBT=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E7=9B=AE=E6=A0=87(libasound=E7=9B=B4=E8=BF=9Epcm.blue?= =?UTF-8?q?alsa,BT=E7=8B=AC=E7=AB=8B=E7=9B=AE=E6=A0=87=E4=B8=8D=E5=8A=A8DA?= =?UTF-8?q?C=E7=8B=AC=E5=8D=A0/FPGA/=E9=9F=B3=E9=87=8F=E5=8F=8D=E8=BD=AC)+?= =?UTF-8?q?=E9=9F=B3=E9=87=8FAVRCP=E9=9A=94=E7=A6=BB(=E4=B8=8D=E4=B8=8B?= =?UTF-8?q?=E5=8F=91DAC=E5=8F=8D=E8=BD=AC)+codec=E7=94=9F=E6=95=88+MSG=5FA?= =?UTF-8?q?UDIO=5FOUTPUT/CLI;=E5=AE=BF=E4=B8=BB67pass/0warning;sink?= =?UTF-8?q?=E6=A0=87=E8=AE=B0P2.5;=E9=81=97=E7=95=99mipsel=E9=9D=99?= =?UTF-8?q?=E6=80=81dlopen=E4=B8=8D=E6=94=AF=E6=8C=81libasound(=E7=A1=AC?= =?UTF-8?q?=E9=9B=86=E6=88=90=E9=A1=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patch_v2/meow_audiod/build_audiod.sh | 5 +- patch_v2/meow_audiod/meow_audio.c | 22 +- patch_v2/meow_audiod/meow_audiod.c | 496 ++++++++++++++++++++++-- patch_v2/meow_audiod/test_audiod_host.c | 88 +++++ patch_v2/meow_launcherd/meow_ipc.h | 18 +- 5 files changed, 587 insertions(+), 42 deletions(-) diff --git a/patch_v2/meow_audiod/build_audiod.sh b/patch_v2/meow_audiod/build_audiod.sh index 762ecfc..6eaf344 100755 --- a/patch_v2/meow_audiod/build_audiod.sh +++ b/patch_v2/meow_audiod/build_audiod.sh @@ -16,7 +16,10 @@ INC="-I$ROOT/patch_v2/meow_launcherd" if [ "$1" = "host" ]; then HOST_CC="${CC_HOST:-cc}" - "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c -o meow_audiod_host -lpthread + # -ldl:BT 输出目标用 dlopen 运行时解析 libasound(pcm.bluealsa 是 alsa-lib 插件,非内核节点; + # 主机/交叉工具链无 asoundlib.h/libasound,故不静态链接)。mipsel 静态 musl 不支持 dlopen,见 + # meow_audiod.c bt 段注释(真机 mipsel 需主会话提供 musl-libasound 或改动态,本任务以源码+宿主为准)。 + "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c -o meow_audiod_host -lpthread -ldl "$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm echo "== host builds ==" ls -la meow_audiod_host meow_audio_host diff --git a/patch_v2/meow_audiod/meow_audio.c b/patch_v2/meow_audiod/meow_audio.c index ef0932c..9461cb9 100644 --- a/patch_v2/meow_audiod/meow_audio.c +++ b/patch_v2/meow_audiod/meow_audio.c @@ -7,6 +7,7 @@ * meow_audio tone [freq] [ms] [rate] 正弦测试音(默认 440Hz 500ms 44100;排查无声不须开 App) * meow_audio route <0|2|3> 手动切路由(0=auto 2=3.5 3=4.4;调试/面板) * meow_audio vol [0-100] 设置音量(无参=查询;v1 透传写 DAC numid=1/2 + settings.conf) + * meow_audio output 输出目标(P2 蓝牙:bt=A2DP source,音量走 AVRCP 不下发 DAC 反转) * meow_audio monitor [秒] 终端实时电平表(rms/peak dBFS + 文本电平条,200ms 刷新) * * 传输:AF_UNIX SOCK_STREAM 到 audiod socket(env MEOW_AUDIO_SOCK / --sock 覆盖,宿主/qemu)。 @@ -273,6 +274,18 @@ static int cmd_vol(int pct) return 0; } +/* P2 蓝牙:切换输出目标(0=DAC 1=BT)。audiod 侧重配 PCM;BT 时音量走 AVRCP,不下发 DAC 反转。 */ +static int cmd_output(int target) +{ + m_audio_output_t o; o.target = (uint32_t)target; + uint8_t rbuf[256]; uint16_t rt; + int n = req_resp(MSG_AUDIO_OUTPUT, &o, sizeof o, &rt, rbuf, sizeof rbuf); + if (n < 0 || rt != MSG_ACK) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; } + m_ack_t a; memcpy(&a, rbuf, sizeof a); + printf("output %s\n", a.status == AUDIO_OUT_BT ? "bt" : "dac"); + return 0; +} + /* 正弦生成:填 nframes 帧(grant 交织格式)到 buf(上限 bufcap,帧对齐)。 * start 为起始帧号;返回实际写入字节数。 */ static size_t gen_sine(uint8_t *buf, size_t bufcap, int grant_fmt, int grant_ch, @@ -373,7 +386,8 @@ static int cmd_tone(int freq, int ms, int rate) static void usage(const char *argv0) { - fprintf(stderr, "usage: %s status | tone [freq] [ms] [rate] | route <0|2|3> | vol [0-100] | monitor [秒]\n" + fprintf(stderr, "usage: %s status | tone [freq] [ms] [rate] | route <0|2|3> | vol [0-100] | output | monitor [秒]\n" + " output: 切换输出目标(dac=本地 hw:0,0 / bt=蓝牙 A2DP source;bt 时音量=AVRCP 侧控)\n" " monitor: 终端实时电平表(200ms 刷新+文本电平条;缺省 10s,0=持续到 Ctrl-C)\n" " env: MEOW_AUDIO_SOCK (socket 缺省 %s)\n", argv0, AUD_SOCK_DEFAULT); } @@ -408,6 +422,12 @@ int main(int argc, char **argv) int pct = (argc >= 3) ? atoi(argv[2]) : -1; if (pct > 100) pct = 100; int rc = cmd_vol(pct); close(g_fd); return rc; + } else if (!strcmp(cmd, "output")) { + if (argc < 3) { fprintf(stderr, "output: \n"); return 2; } + int t = (strcmp(argv[2], "bt") == 0) ? AUDIO_OUT_BT : (strcmp(argv[2], "dac") == 0) ? AUDIO_OUT_DAC : -1; + if (t < 0) { fprintf(stderr, "output: dac|bt\n"); return 2; } + if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; } + int rc = cmd_output(t); close(g_fd); return rc; } else if (!strcmp(cmd, "tone")) { int freq = (argc >= 3) ? atoi(argv[2]) : 440; int ms = (argc >= 4) ? atoi(argv[3]) : 500; diff --git a/patch_v2/meow_audiod/meow_audiod.c b/patch_v2/meow_audiod/meow_audiod.c index c834211..90bb96f 100644 --- a/patch_v2/meow_audiod/meow_audiod.c +++ b/patch_v2/meow_audiod/meow_audiod.c @@ -57,6 +57,7 @@ #include #include #include +#include #include "meow_ipc.h" #ifdef __linux__ @@ -74,6 +75,13 @@ #define SETTINGS_CONF "/usr/data/meow/settings.conf" #define DEFAULT_OUT_WAV "/tmp/meow_audiod.wav" +/* ---- 蓝牙 A2DP source(P2 输出目标 OUT_BT) ---- */ +#define BT_PCM_DEFAULT "pcm.bluealsa" /* alsa.conf `pcm.bluealsa`(plug→bluealsa);aplay -D bluealsa 等价 */ +#define BT_PCM_FALLBACK "bluealsa" /* 部分配置只定义别名节点,防打开失败再试一次 */ +#define BT_SO "libasound.so.2" /* 运行时 dlopen(不静态链接:设备为 musl 静态勿 dlopen,见 main 注释) */ +#define BT_CODEC_AUTO "auto" +#define BT_VOL_FULL_PCM "/usr/bin/bluealsa-cli" /* 一键设 AVRCP 100%(best-effort,不存在则跳过) */ + #define CLI_MAX 8 /* 同连客户端上限(防 fd 泄漏) */ #define READ_BUF_MAX (MEOW_IPC_MAX + MEOW_IPC_HDR_LEN) #define FRAME_READ_MAX (MEOW_IPC_MAX + MEOW_IPC_HDR_LEN) @@ -96,12 +104,18 @@ static int g_sink = SINK_ALSA; static int g_hw_fmt = AUDIO_FMT_S16; /* 硬件最优格式(ALSA 探测;FILE/NULL=S16) */ static int g_hw_probed = 0; +/* 输出目标(BLUETOOTH_FULL_OVERHAUL_PLAN §五):OUT_DAC=本地 hw:0,0;OUT_BT=蓝牙 A2DP source。 + * **BT 是独立目标,不共用 DAC 逻辑**:period=1024×4/FPGA numid=9/音量反转均不得作用于 BT。 */ +static int g_target = AUDIO_OUT_DAC; +static char g_bt_codec[24] = BT_CODEC_AUTO; /* settings.conf `bt.codec`(auto/SBC/AAC/aptX/LDAC…) */ +static char g_settings_path[256] = SETTINGS_CONF; /* MEOW_AUDIO_SETTINGS 可覆盖(宿主测) */ + static char g_sock[108] = AUD_SOCK_DEFAULT; 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(缓存) */ +static int g_vol_pct = VOL_DEFAULT; /* 当前音量 0-100(缓存:DAC 音量;BT 时 volctl=avrcp) */ static int g_force = 0; /* MEOW_AUDIO_FORCE=1 跳过启动门控(宿主/qemu 测试) */ /* ---- 缓冲状态(+日志) ---- */ @@ -172,6 +186,15 @@ static int g_dev_open = 0; static uint32_t g_dev_rate = 0, g_dev_ch = 0, g_dev_fmt = 0; static int g_reconfig_pending = 0; +/* ---- 蓝牙 A2DP source 运行时状态(g_mu 保护;仅 g_target==OUT_BT && g_sink==SINK_ALSA 时用) ---- + * 直接写 libasound 插件 PCM(pcm.bluealsa). 不静态链接 libasound:主机/工具链均无 asoundlib, + * 用 dlopen/dlsym 运行时解析(设备 libasound.so.2 在 /usr/lib;见 dl 注释)。 */ +static void *g_bt_hdl = NULL; /* snd_pcm_t *(libasound 不透明句柄) */ +static int g_bt_open = 0; /* BT PCM 已打开 */ +static void *g_bt_so = NULL; /* dlopen 句柄 */ +static int g_bt_loaded = 0; /* dlopen 尝试过(失败不再重试风暴) */ +static int g_bt_vol_full_done = 0; /* 已对 AVRCP 置 100%(幂等,一次即可) */ + /* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ---- * 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16), * ~100ms 窗口(rate/10 帧)到点才结算一次 RMS/peak 并发布快照——每帧仅几次整数加乘, @@ -319,6 +342,26 @@ static const char *fmt_name(int fmt) } static int frame_size(void) { return g_st.ch * fmt_bps(g_st.fmt); } +/* 输出目标名(日志/QUERY 用) */ +static const char *target_name(void) +{ + return (g_target == AUDIO_OUT_BT) ? "bt" : "dac"; +} + +/* 授权给客户端的硬件最优格式。BT(A2DP source)=S16(codec 基座,pcm.bluealsa 前置 plug 层自动转 + * codec 协商率);DAC=hw 探测最优(FILE/NULL 宿主=S16)。 */ +static int target_grant_fmt(void) +{ + if (g_target == AUDIO_OUT_BT) return AUDIO_FMT_S16; + return (g_sink == SINK_ALSA) ? g_hw_fmt : AUDIO_FMT_S16; +} + +/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。 */ +static const char *volctl_name(void) +{ + return (g_target == AUDIO_OUT_BT) ? "avrcp" : "dac"; +} + /* 整数开平方(uint64→uint32,逐位法;替代软浮点 sqrt) */ static uint32_t isqrt64(uint64_t x) { @@ -643,7 +686,7 @@ static void route_evaluate(int manual, int *last_hb, int *last_bal, int *last_ta /* ---- settings.conf 写 audio.volume(同 launcher 同源同键) ---- */ static void settings_set_volume(int pct) { - FILE *f = fopen(SETTINGS_CONF, "r"); + FILE *f = fopen(g_settings_path, "r"); char *data = NULL; size_t len = 0; if (f) { @@ -687,7 +730,7 @@ static void settings_set_volume(int pct) if (n > 0) o += (size_t)n; } mkdir("/usr/data/meow", 0755); - int fd = open(SETTINGS_CONF, O_WRONLY | O_CREAT | O_TRUNC, 0644); + int fd = open(g_settings_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd >= 0) { /* 若原文件带 "# meow-settings 1" 头则保留——我们已被动保留了内容(含头)。 * 无头(新文件)补一行头,符合 meow_settings 的解析预期。 */ @@ -704,12 +747,65 @@ static void settings_set_volume(int pct) close(fd); } free(out); - logmsg("audio.volume persisted -> %d (%s)", pct, SETTINGS_CONF); + logmsg("audio.volume persisted -> %d (%s)", pct, g_settings_path); +} + +/* 通用设置读取:取 `key = value` 的 value(大小写不敏感前缀匹配;返回 0 找到,-1 未找到)。 + * line 缓冲内部复用;仅用于启动时一次性读取 output_bt/codec。 */ +static int settings_get(const char *key, char *out, size_t n) +{ + FILE *f = fopen(g_settings_path, "r"); + if (!f) return -1; + int rc = -1; + char line[256]; + size_t klen = strlen(key); + while (fgets(line, sizeof line, f)) { + char *k = line; + while (*k == ' ' || *k == '\t') k++; + if (strncasecmp(k, key, klen) == 0) { + char *eq = strchr(k, '='); + if (eq) { + const char *v = eq + 1; + while (*v == ' ' || *v == '\t') v++; + size_t o = 0; + while (v[o] && v[o] != '\n' && v[o] != '\r' && o < n - 1) { out[o] = v[o]; o++; } + out[o] = 0; + rc = 0; + break; + } + } + } + fclose(f); + return rc; +} + +/* P2 蓝牙:读 settings.conf `audio.output_bt`(0/1) → 输出目标(0=DAC 1=BT)。缺省 DAC。 */ +static int settings_get_output_target(void) +{ + char buf[16]; + if (settings_get("audio.output_bt", buf, sizeof buf) == 0) { + if (buf[0] == '1') return AUDIO_OUT_BT; + if (buf[0] == '0') return AUDIO_OUT_DAC; + } + return AUDIO_OUT_DAC; +} + +/* P2 蓝牙:读 settings.conf `bt.codec`(auto/SBC/AAC/aptX/LDAC…)→ g_bt_codec。缺省 auto。 + * codec 经 /usr/data/alsa.conf 的 `pcm.bluealsa` codec 参数生效(audiod BT 打开前 best-effort sync)。 */ +static const char *settings_get_bt_codec(void) +{ + char buf[24]; + if (settings_get("bt.codec", buf, sizeof buf) == 0 && buf[0]) { + snprintf(g_bt_codec, sizeof g_bt_codec, "%s", buf); + } else { + snprintf(g_bt_codec, sizeof g_bt_codec, "%s", BT_CODEC_AUTO); + } + return g_bt_codec; } static int settings_get_volume(void) { - FILE *f = fopen(SETTINGS_CONF, "r"); + FILE *f = fopen(g_settings_path, "r"); if (!f) return g_vol_pct; char line[128]; int v = g_vol_pct; @@ -725,6 +821,273 @@ static int settings_get_volume(void) return v; } +/* ============================================================================ + * 蓝牙 A2DP source(输出目标 OUT_BT,PLAN §三/§五) + * ---------------------------------------------------------------------------- + * audiod 用 libasound **运行时 dlopen** 直连 `pcm.bluealsa`(plug→bluealsa,播放向,A2DP), + * 把播放器解码输出透传过去。BT 是**独立目标**: + * - 不开 hw:0,0(不 period=1024×4、不 FPGA numid=9 武装、不 DAC 音量反转); + * - 保留音频流透传 + 格式归一(grant_fmt=S16,plug 层转 codec 协商率); + * - 音量走 AVRCP/耳机侧(音量隔离:绝不下发 DAC numid=1/2 反转,见 vol_apply)。 + * dlopen 说明:主机/交叉工具链均无 asoundlib.h/libasound,静态链接不可行;且设备 musl-静态 + * 二进制本身不支持 dlopen("Dynamic loading not supported")——因此 mipsel 真机若要生效, + * 需主会话提供 musl-mipsel libasound 并用 `-lasound` 直接链接(或改动态 musl-PIE)。本源码以 + * dlopen 路径编译到宿主(0 warning),真机 mipsel 链接此点列为本任务风险清单。禁止旁路:不跑 + * bluealsa-aplay/bluealsa-cli open(sink/D-Bus 或新建 daemon),audiod 内换目标 PCM 是唯一最简。 + * ========================================================================== */ +#define SND_PCM_STREAM_PLAYBACK 0 +#define SND_PCM_ACCESS_RW_INTERLEAVED 3 +#define SND_PCM_FORMAT_S16_LE 2 +#define SND_PCM_NONBLOCK 1 + +static int (*g_fn_open)(void **, const char *, int, int); +static int (*g_fn_set_params)(void *, int, int, unsigned, unsigned, int, unsigned); +static int (*g_fn_nonblock)(void *, int); +static long (*g_fn_writei)(void *, const void *, unsigned long); +static int (*g_fn_wait)(void *, int); +static int (*g_fn_recover)(void *, int, int); +static int (*g_fn_drain)(void *); +static int (*g_fn_close)(void *); +static const char *(*g_fn_strerror)(int); +/* dlsym→函数指针:用 memcpy 转(POSIX 可移植惯例;直接 `(void*)&fn` 或 union 会触发 + * -Wstrict-aliasing / 函数指针类型告警,memcpy 无此问题)。 */ +#define BT_LOAD_SYM(field, name) do { \ + void *p_ = dlsym(g_bt_so, name); \ + if (!p_) { logmsg("bt: dlsym %s failed", name); dlclose(g_bt_so); g_bt_so = NULL; return -1; } \ + memcpy(&g_##field, &p_, sizeof g_##field); \ +} while (0) +static void bt_close(void); /* 前向:bt_write 判死路径要用 */ +static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */ + +static int bt_load(void) +{ + if (g_bt_so) return 0; + if (g_bt_loaded) return -1; /* 已试过且失败:不再重试(防 dlopen 风暴) */ + g_bt_loaded = 1; + g_bt_so = dlopen(BT_SO, RTLD_NOW); + if (!g_bt_so) { logmsg("bt: dlopen %s failed: %s", BT_SO, dlerror()); return -1; } + BT_LOAD_SYM(fn_open, "snd_pcm_open"); + BT_LOAD_SYM(fn_set_params,"snd_pcm_set_params"); + BT_LOAD_SYM(fn_nonblock, "snd_pcm_nonblock"); + BT_LOAD_SYM(fn_writei, "snd_pcm_writei"); + BT_LOAD_SYM(fn_wait, "snd_pcm_wait"); + BT_LOAD_SYM(fn_recover, "snd_pcm_recover"); + BT_LOAD_SYM(fn_drain, "snd_pcm_drain"); + BT_LOAD_SYM(fn_close, "snd_pcm_close"); + BT_LOAD_SYM(fn_strerror, "snd_strerror"); + logmsg("bt: libasound loaded via dlopen"); + return 0; +} + +/* bt.codec 生效:bluealsa 的 codec 是 config-time 参数(/usr/data/alsa.conf `pcm.bluealsa` 块的 + * `codec "X"`)。audiod 在 BT 打开前做一次**幂等且外科式**的 token 替换:只把 bluealsa 块内的 + * `codec "..."` 改成 `codec ""`(其余字符不动)。仅真机且文件可写时执行; + * 宿主/只读/找不到 codec token → 跳过(绝不破坏 alsa.conf——audiod 只调整既有 token,不新建结构)。 */ +static void bt_apply_codec_conf(void) +{ + if (!g_bt_codec[0] || !strcmp(g_bt_codec, BT_CODEC_AUTO) || !strcmp(g_bt_codec, "unchanged")) return; + const char *path = "/usr/data/alsa.conf"; + /* 先探测是否已有 codec token:无则不动(避免误建/无权写) */ + { + FILE *pf = fopen(path, "r"); + if (!pf) return; + int hit = 0; char l[160]; + while (fgets(l, sizeof l, pf)) { + char *k = l; while (*k==' '||*k=='\t') k++; + if (strncmp(k, "codec", 5) == 0) { hit = 1; break; } + } + fclose(pf); + if (!hit) return; + } + FILE *f = fopen(path, "r"); + if (!f) return; + char *data = NULL; size_t len = 0; + fseek(f, 0, SEEK_END); long sz = ftell(f); + if (sz <= 0 || sz > 64 * 1024) { fclose(f); return; } + fseek(f, 0, SEEK_SET); + data = malloc((size_t)sz + 1); + if (!data) { fclose(f); return; } + len = fread(data, 1, (size_t)sz, f); data[len] = 0; + fclose(f); + char *out = malloc(len + 64); + if (!out) { free(data); return; } + out[0] = 0; size_t o = 0; int changed = 0; + char *save = NULL; + char *ln = strtok_r(data, "\n", &save); + while (ln) { + char *k = ln; while (*k==' '||*k=='\t') k++; + int is_codec = (strncmp(k, "codec", 5) == 0); + if (is_codec && !changed) { + int n = snprintf(out + o, len + 64 - o, "codec \"%s\"\n", g_bt_codec); + if (n > 0) o += (size_t)n; + changed = 1; + } else { + int n = snprintf(out + o, len + 64 - o, "%s\n", ln); + if (n > 0) o += (size_t)n; + } + ln = strtok_r(NULL, "\n", &save); + } + free(data); + if (changed) { + int fd = open(path, O_WRONLY | O_TRUNC, 0644); + if (fd >= 0) { if (write(fd, out, o) < 0) { } close(fd); logmsg("bt: codec conf %s -> %s", path, g_bt_codec); } + else logmsg("bt: codec conf %s not writable, skip", path); + } + free(out); +} + +/* BT 打开:dlopen libasound → snd_pcm_open(pcm.bluealsa/bluealsa, PLAYBACK, NONBLOCK) → + * set_params(S16_LE, RW, ch, rate, soft_resample, 100ms)。返回 0=ok。失败置 g_bt_open=0。 */ +static int bt_open(void) +{ + if (bt_load() != 0) return -1; + void *pcm = NULL; + const char *names[2] = { BT_PCM_DEFAULT, BT_PCM_FALLBACK }; + int r = -1; + for (int i = 0; i < 2; i++) { + r = g_fn_open(&pcm, names[i], SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (r == 0) { logmsg("bt: opened %s", names[i]); break; } + logmsg("bt: snd_pcm_open %s: %s", names[i], g_fn_strerror ? g_fn_strerror(r) : "?"); + pcm = NULL; + } + if (r != 0) return -1; + g_bt_hdl = pcm; + g_fn_nonblock(pcm, 1); + unsigned rate = g_st.rate ? g_st.rate : 44100; + unsigned ch = g_st.ch ? g_st.ch : 2; + r = g_fn_set_params(pcm, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, + ch, rate, 1, 100000); + if (r < 0) { + logmsg("bt: set_params %u/%u/S16: %s", rate, ch, g_fn_strerror ? g_fn_strerror(r) : "?"); + g_fn_close(pcm); g_bt_hdl = NULL; return -1; + } + g_bt_open = 1; + logmsg("bt: PCM ready rate=%u ch=%u fmt=S16 codec=%s", rate, ch, g_bt_codec); + bt_vol_full(); /* best-effort AVRCP 100%(一次性;失败仅日志) */ + return 0; +} + +/* BT 写:非阻塞 writei + wait 超时(镜像 DAC O_NONBLOCK+poll 护栏),xrun recover。 + * 设备停摆/无连接 → 丢帧(g_played_frames 照记),绝不把播放线程永久压进 libasound。 */ +static void bt_write(uint8_t *buf, size_t n) +{ + if (!g_bt_open || !g_bt_hdl) { + static unsigned s_drop = 0; + if ((s_drop++ % 256) == 0) + logmsg("bt_write dropped %zu bytes (bt not open, x%u)", n, s_drop); + g_played_frames += n / (size_t)frame_size(); + return; + } + size_t fs = (size_t)frame_size(); + if (!fs) return; + unsigned long left = (unsigned long)(n / fs); + if (!left) return; + const uint8_t *off = buf; + int stalls = 0; + while (left > 0 && !g_stop) { + long w = g_fn_writei(g_bt_hdl, off, left); + if (w > 0) { off += (size_t)w * fs; left -= (unsigned long)w; stalls = 0; continue; } + if (w < 0) { + int err = (int)(-w); + if (err == EAGAIN || err == EWOULDBLOCK) { + int pr = g_fn_wait(g_bt_hdl, ALSA_POLL_MS); + if (pr == 0) { /* 无空间超时 → 停摆计数(镜像 DAC stall) */ + g_stall++; stalls++; + if (stalls >= ALSA_STALL_MAX) { + logmsg("bt WEDGED: %d consecutive waits, close + drop until next OPEN", stalls); + bt_close(); g_bt_open = 0; g_dev_open = 0; + g_played_frames += n / fs; return; + } + continue; + } + if (pr < 0 && errno != EINTR) { logmsg("bt wait err: %s", strerror(errno)); break; } + continue; + } + if (err == EPIPE || err == ESTRPIPE) { /* xrun:recover 后继续 */ + g_underrun++; + logmsg("bt xrun #%u -> recover", (unsigned)g_underrun); + g_fn_recover(g_bt_hdl, err, 1); + continue; + } + logmsg("bt writei err: %s", g_fn_strerror ? g_fn_strerror(err) : "?"); + break; + } + if (w == 0) break; + } + unsigned long done = (unsigned long)(n / fs) - left; + g_played_frames += done; +} + +static void bt_close(void) +{ + if (g_bt_hdl) { + g_fn_drain(g_bt_hdl); + g_fn_close(g_bt_hdl); + g_bt_hdl = NULL; + } + g_bt_open = 0; +} + +/* 音量隔离(PLAN §五):路由=BT 时**不下发 DAC 音量反转**(音量由 AVRCP/耳机侧控)。 + * 仅记录 DAC 音量(g_vol_pct 语义=本地 DAC 音量);BT 侧 best-effort 促成 AVRCP 100% + * (避免"窃听音量误解"——耳机侧音量钉到最响,设备侧滑块即绝对控制)。 */ +static void bt_vol_full(void) +{ + if (g_bt_vol_full_done) return; + g_bt_vol_full_done = 1; + char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); + if (!o) { logmsg("bt: avrcp set-100 skipped (no bluealsa-cli)"); return; } + char pcm[128] = ""; + const char *ln = o; + while (ln && *ln) { + const char *nl = strchr(ln, '\n'); + const char *p = ln; + while (*p == ' ' || *p == '\t') p++; + if (strstr(p, "a2dpsrc") || strstr(p, "a2dp_source")) { + const char *tok = p; + while (*tok && *tok != ' ' && *tok != '\t') tok++; + size_t tlen = (size_t)(tok - p); + if (tlen && tlen < sizeof pcm) { memcpy(pcm, p, tlen); pcm[tlen] = 0; } + break; + } + if (!nl) break; + ln = nl + 1; + } + if (!pcm[0]) { logmsg("bt: avrcp set-100 skipped (no a2dpsrc pcm found)"); return; } + char cmd[256]; + snprintf(cmd, sizeof cmd, "%s volume %s 100 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm); + char *r = popen_read_tmo(1500, cmd); + logmsg("bt: avrcp volume set 100%% (%s)%s", pcm, r ? " ok" : " (exec failed; AVRCP still controls)"); +} + +/* 音量应用(含 BT 隔离):BT → 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、不持久化 + * audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。 */ +static void vol_apply(int pct) +{ + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + if (g_target == AUDIO_OUT_BT) { + logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct); + return; + } + g_vol_pct = pct; + dac_vol_set(pct); + settings_set_volume(pct); +} + +/* 输出目标切换(g_mu 已持)。target 改变 → 有流则标重配(重启 PCM 到新目标)。 */ +static void target_set(int target) +{ + if (target != AUDIO_OUT_DAC && target != AUDIO_OUT_BT) { + logmsg("output: invalid target %d", target); return; + } + if (g_target == target) { logmsg("output: target already %d", target); return; } + logmsg("output: target %d -> %d (%s)", g_target, target, target == AUDIO_OUT_BT ? "BT" : "DAC"); + g_target = target; + if (g_st.active) g_reconfig_pending = 1; + pthread_cond_signal(&g_cond); +} + /* ---- ALSA UAPI ---- */ #ifdef AUDIOD_HAVE_ALSA static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); } @@ -837,6 +1200,15 @@ static void dev_close(void); /* 前向:dev_write 的 wedge 判死路径要 static int dev_open(void) { + /* 输出目标=BT 且走真机 ALSA:打开 libasound 的 pcm.bluealsa(不开 hw:0,0)。 + * 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑, + * 真实 bluealsa 传输留给真机(见 bt_* 注释)。失败 → 降级丢帧(自感知兜底 §五)。 */ + if (g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA) { + bt_apply_codec_conf(); + if (bt_open() == 0) return 0; /* g_bt_open=1;返回 0=逻辑已开(真实句柄在 g_bt_hdl) */ + logmsg("bt: open failed (degrade: drop PCM until next OPEN)"); + return -1; + } uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt; int bits = fmt_bps((int)fmt) * 8; if (g_sink == SINK_ALSA) { @@ -885,6 +1257,7 @@ static void dev_write(uint8_t *buf, size_t n) } g_dev_bytes += n; } + if (g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA) { bt_write(buf, n); return; } if (g_sink == SINK_NULL) { g_played_frames += n / (size_t)frame_size(); return; } if (g_sink == SINK_ALSA) { #ifdef AUDIOD_HAVE_ALSA @@ -950,6 +1323,7 @@ static void dev_write(uint8_t *buf, size_t n) static void dev_close(void) { + if (g_bt_open) { bt_close(); g_dev_fd = -1; g_dev_open = 0; return; } if (g_sink == SINK_FILE && g_dev_fd >= 0 && g_dev_fd != -2) { off_t end = lseek(g_dev_fd, 0, SEEK_END); if (end > 44) { @@ -971,17 +1345,20 @@ static void dev_close(void) static void reconfigure_locked(void) { uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt; - logmsg("reconfigure device: %u/%u/%s", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt)); + const char *tgt = (g_target == AUDIO_OUT_BT) ? "bt" : "dac"; + logmsg("reconfigure device: %u/%u/%s target=%s", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt), tgt); int saved_vol = g_vol_pct; - if (g_sink == SINK_ALSA) dac_vol_mute(); /* 防爆音:重配前静音 */ + /* 防爆音仅 DAC(重配前静音 + 复原);BT 独立目标,跳过 DAC 音量(音量由 AVRCP 侧控)。 */ + if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_mute(); dev_close(); g_dev_fd = dev_open(); if (g_dev_fd >= 0 || (g_sink == SINK_NULL && g_dev_fd == -2)) { g_dev_open = 1; g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = fmt; if (g_dev_fd == -2) g_dev_fd = -1; /* NULL sink:无真实 fd */ - if (g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原 */ - logmsg("device open ok: %u/%u/%s sink=%d", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt), g_sink); + if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原 */ + logmsg("device open ok: %u/%u/%s sink=%d target=%s", (unsigned)rate, (unsigned)ch, + fmt_name((int)fmt), g_sink, tgt); } else { logmsg("device open FAILED !!"); } @@ -1121,14 +1498,18 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin logmsg("open rejected: ch=%u fmt=%d rate=%u", (unsigned)o.ch, o.fmt, (unsigned)o.rate); return; } - hw_probe(); - if (g_sink == SINK_ALSA && !g_hw_probed) { - m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_NODEV; - send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a); - logmsg("open rejected: no usable PCM device"); - return; + /* 输出目标=BT:不依赖 DAC hw 探测(BT 走 pcm.bluealsa,与 hw:0,0/FGA numid=9 解耦)—— + * 探测只对 DAC 有意义;BT 打开失败在播放线程降级丢帧(plan §五自感知兜底)。 */ + if (g_target == AUDIO_OUT_DAC) { + hw_probe(); + if (g_sink == SINK_ALSA && !g_hw_probed) { + m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_NODEV; + send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a); + logmsg("open rejected: no usable PCM device (DAC)"); + return; + } } - int grant_fmt = (g_sink == SINK_ALSA) ? g_hw_fmt : AUDIO_FMT_S16; + int grant_fmt = target_grant_fmt(); /* DAC=hw 最优;BT=S16(plug 层转 codec 协商率) */ uint32_t sid = g_next_sid++; /* 抢占:已有当前流 → 旧客户端收 PREEMPTED */ if (g_st.active) { @@ -1178,7 +1559,9 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin a.grant_fmt = g_st.fmt; a.err = MEOW_OK; send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a); - logmsg("OPEN grant: sid=%u app=%s rate=%u ch=%u fmt=%s", (unsigned)sid, g_st.app_id, (unsigned)o.rate, (unsigned)g_st.ch, fmt_name((int)g_st.fmt)); + logmsg("OPEN grant: sid=%u app=%s rate=%u ch=%u fmt=%s target=%s codec=%s", + (unsigned)sid, g_st.app_id, (unsigned)o.rate, (unsigned)g_st.ch, + fmt_name((int)g_st.fmt), target_name(), g_bt_codec); break; } case MSG_AUDIO_WRITE: { @@ -1226,7 +1609,8 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin snprintf(txt, sizeof txt, "active=%d app=%s sid=%u rate=%u ch=%u fmt=%s " "written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d " - "route_mode=%s port=%d vol=%d rms=%d peak=%d lvlseq=%u\n", + "route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u " + "target=%s codec=%s\n", g_st.active ? 1 : 0, g_st.active ? g_st.app_id : "-", (unsigned)g_st.sid, (unsigned)g_st.rate, (unsigned)g_st.ch, @@ -1238,8 +1622,9 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin (unsigned)g_stall, (unsigned)g_txdrop, g_paused, g_route_manual == -1 ? "auto" : "manual", (g_route_manual != -1 ? g_route_manual : rb), - vol, - g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq); + vol, volctl_name(), + g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq, + target_name(), g_bt_codec); send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1); break; } @@ -1257,10 +1642,8 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin int32_t pct = -1; if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct); if (pct >= 0) { - if (pct > 100) pct = 100; - g_vol_pct = (int)pct; - dac_vol_set((int)pct); - settings_set_volume((int)pct); + /* 音量应用含 BT 隔离:target=BT 时绝不下发 DAC 反转(vol_apply 内判断) */ + vol_apply((int)pct); } else { g_vol_pct = settings_get_volume(); } @@ -1268,6 +1651,14 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a); break; } + case MSG_AUDIO_OUTPUT: { + uint32_t target = AUDIO_OUT_DAC; + if (len >= sizeof(uint32_t)) memcpy(&target, pl, sizeof target); + target_set((int)target); + m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_target; + send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a); + break; + } default: break; } @@ -1411,22 +1802,35 @@ static void *route_thread(void *arg) * 启动 PCM 会在第二次 hbc3000_start 死锁整机。写 Output Port Switch 是烧录触发器 * (驱动 put:set_spdif_mode→program_ram→enabled)。开机端口恒 0=未烧,故启动时 * 无条件写一次:4.4 在→3,3.5 在→2,都没→4(官方播放器拔插孔后的选择,保 FPGA 已烧)。 - * 端口已是目标值(audiod 重启场景,驱动同值短路不写)→ 先写 5(disable) 强制重烧。 */ - { - int hb = read_switch("/sys/class/switch/headset/state"); - int bal = read_switch("/sys/class/switch/balance/state"); - int target = (bal == 1) ? 3 : (hb == 1) ? 2 : 4; - int cur = amixer_get(9, 1); - if (cur == target) { logmsg("route: port already %d, force re-arm via 5", target); apply_route(5); } - apply_route(target); - last_hb = hb; last_bal = bal; last_target = (hb == 0 && bal == 0) ? -1 : target; - logmsg("route: boot arm done (port %d %s, FPGA programmed)", target, port_name(target)); - } + * 端口已是目标值(audiod 重启场景,驱动同值短路不写)→ 先写 5(disable) 强制重烧。 + * ⚠️ P2 蓝牙:numnid=9/FPGA 武装只作用于 **DAC 物理路径**(plan §五解耦);target=BT 时 + * 不开 hw:0,0,无 hbc3000 start 死锁风险,故 BT 期间跳过 numid=9/FPGA(切回 DAC 再武装)。 */ + int need_arm = 1; /* 启动(DAC 起点)武装一次;BT 期间置 1(回 DAC 再武装) */ while (!g_stop) { pthread_mutex_lock(&g_mu); int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */ + int tgt = g_target; /* 输出目标快照(决定是否武装 FPGA) */ pthread_mutex_unlock(&g_mu); - route_evaluate(manual, &last_hb, &last_bal, &last_target); + if (tgt == AUDIO_OUT_BT) { + static int s_logged = 0; + if (!s_logged) { s_logged = 1; logmsg("route: output=BT, numid=9/FPGA not armed (DAC dormant)"); } + need_arm = 1; /* 回 DAC 后补一次武装 */ + last_target = -1; + } else { + if (need_arm) { /* 启动 or BT→DAC:一次性武装(同原 boot-arm) */ + int hb = read_switch("/sys/class/switch/headset/state"); + int bal = read_switch("/sys/class/switch/balance/state"); + int target = (bal == 1) ? 3 : (hb == 1) ? 2 : 4; + int cur = amixer_get(9, 1); + if (cur == target) { logmsg("route: port already %d, force re-arm via 5", target); apply_route(5); } + apply_route(target); + last_hb = hb; last_bal = bal; last_target = (hb == 0 && bal == 0) ? -1 : target; + logmsg("route: boot/DAC-return arm done (port %d %s)", target, port_name(target)); + need_arm = 0; + } else { + route_evaluate(manual, &last_hb, &last_bal, &last_target); + } + } int naps = 5; /* 1000ms / 200ms 分段,即时响应 SIGTERM */ for (int k = 0; k < naps && !g_stop; k++) { struct timespec ts = { 0, 200 * 1000000L }; @@ -1544,8 +1948,11 @@ int main(int argc, char **argv) else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { printf("meow_audiod [--sock ] [--out ]\n" " env: MEOW_AUDIO_SOCK/MEOW_AUDIO_OUT/MEOW_AUDIO_SINK(alsa|file|null)/MEOW_AUDIO_AMIXER/MEOW_AUDIO_FORCE=1\n" + " MEOW_AUDIO_SETTINGS (settings.conf 路径, 缺省 %s; 宿主测覆盖)\n" + " MEOW_AUDIO_TARGET (0=dac 1=bt, 覆盖 settings audio.output_bt)\n" " MEOW_AUDIO_RING_WAIT_MS (ring 满写等待上限, 缺省 5000)\n" - " MEOW_AUDIO_TEST_WRITE_DELAY_MS / MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES (故障注入, 仅测试)\n"); + " MEOW_AUDIO_TEST_WRITE_DELAY_MS / MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES (故障注入, 仅测试)\n", + SETTINGS_CONF); return 0; } } @@ -1556,6 +1963,7 @@ int main(int argc, char **argv) if ((e = getenv("MEOW_AUDIO_RING_WAIT_MS")) && atoi(e) > 0) g_ring_wait_ms = atoi(e); if ((e = getenv("MEOW_AUDIO_TEST_WRITE_DELAY_MS")) && atoi(e) > 0) g_test_write_delay_ms = atoi(e); if ((e = getenv("MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES")) && atoll(e) > 0) g_test_wedge_after = atoll(e); + if ((e = getenv("MEOW_AUDIO_SETTINGS")) && e[0]) snprintf(g_settings_path, sizeof g_settings_path, "%s", e); const char *sink = getenv("MEOW_AUDIO_SINK"); if (sink && sink[0]) { if (!strcmp(sink, "file")) g_sink = SINK_FILE; @@ -1582,9 +1990,19 @@ int main(int argc, char **argv) g_sock, g_sink, g_out_wav, g_force, g_ring_wait_ms, g_test_write_delay_ms > 0 ? " TEST-DELAY" : "", g_test_wedge_after >= 0 ? " TEST-WEDGE" : ""); - if (g_sink == SINK_ALSA) hw_probe(); + /* P2 蓝牙:读 settings.conf(可经 env 覆盖)→ 输出目标 + codec。env MEOW_AUDIO_TARGET + * 优先级最高(宿主测/调试用),否则 `audio.output_bt`。 */ + settings_get_bt_codec(); + g_target = settings_get_output_target(); + if ((e = getenv("MEOW_AUDIO_TARGET")) && e[0]) { + int t = atoi(e); + if (t == AUDIO_OUT_BT) g_target = AUDIO_OUT_BT; + else if (t == AUDIO_OUT_DAC) g_target = AUDIO_OUT_DAC; + } + if (g_sink == SINK_ALSA && g_target == AUDIO_OUT_DAC) hw_probe(); g_vol_pct = settings_get_volume(); - logmsg("volume init = %d%%", g_vol_pct); + logmsg("volume init = %d%% target=%s codec=%s volctl=%s", + g_vol_pct, target_name(), g_bt_codec, volctl_name()); /* g_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */ { diff --git a/patch_v2/meow_audiod/test_audiod_host.c b/patch_v2/meow_audiod/test_audiod_host.c index beeb2ee..aa101a9 100644 --- a/patch_v2/meow_audiod/test_audiod_host.c +++ b/patch_v2/meow_audiod/test_audiod_host.c @@ -153,6 +153,22 @@ static int start_audiod(void) return start_audiod_ex("file", NULL, NULL); } +/* 带自定义 settings.conf 的启动(P2 蓝牙:测 audio.output_bt / bt.codec 读取生效) */ +static int start_audiod_settings(const char *sink, const char *settings_path) +{ + pid_t pid = fork(); + if (pid == 0) { + setenv("MEOW_AUDIO_SINK", sink, 1); + setenv("MEOW_AUDIO_OUT", TWAV, 1); + setenv("MEOW_AUDIO_SOCK", TSOCK, 1); + setenv("MEOW_AUDIO_FORCE", "1", 1); + if (settings_path) setenv("MEOW_AUDIO_SETTINGS", settings_path, 1); + execl("./meow_audiod_host", "meow_audiod_host", (char *)0); + _exit(127); + } + return pid; +} + /* 直接客户端 open */ static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t *ack) { @@ -690,6 +706,78 @@ 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。 + * 宿主以 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(隔离成立); + * - 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"); + unlink(TSOCK); unlink(TWAV); + const char *tconf = "/tmp/meow_test_settings.conf"; + 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); + usleep(400000); + struct cli b11; + check(cli_connect(&b11) == 0, "connect B11"); + m_audio_open_ack_t ack11; + check(do_open(&b11, 44100, 2, AUDIO_FMT_S16, &ack11) == 0 && ack11.err == MEOW_OK, "open B11 (bt)"); + char q11[1024]; + check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "target=bt"), + "settings output_bt=1 -> target=bt"); + check(strstr(q11, "codec=SBC") != NULL, "settings bt.codec=SBC -> codec=SBC"); + check(strstr(q11, "volctl=avrcp") != NULL, "bt -> volctl=avrcp (AVRCP vol control)"); + check(strstr(q11, "fmt=S16") != NULL, "bt grant fmt=S16 (format normalize)"); + /* 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 */ + { + int32_t pct = 40; + if (sf(b11.fd, MSG_AUDIO_VOLUME, 8, &pct, sizeof pct) == 0) { + uint8_t ab[64]; uint16_t at; rf(b11.fd, &at, ab, sizeof ab); /* 消费 ACK */ + } + usleep(100000); + } + 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)"); + { + 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"); + } + /* MSG_AUDIO_OUTPUT 切回 DAC */ + { + m_audio_output_t oo; oo.target = AUDIO_OUT_DAC; + if (sf(b11.fd, MSG_AUDIO_OUTPUT, 9, &oo, sizeof oo) == 0) { + uint8_t ab[64]; uint16_t at; rf(b11.fd, &at, ab, sizeof ab); + } + usleep(150000); + } + check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "target=dac"), + "MSG_AUDIO_OUTPUT -> target=dac"); + check(strstr(q11, "volctl=dac") != NULL, "dac -> volctl=dac"); + /* 现在音量设置应写 DAC + 持久化 */ + { + int32_t pct = 40; + if (sf(b11.fd, MSG_AUDIO_VOLUME, 10, &pct, sizeof pct) == 0) { + uint8_t ab[64]; uint16_t at; rf(b11.fd, &at, ab, sizeof ab); + } + usleep(150000); + } + check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "vol=40"), "dac vol set -> g_vol_pct=40"); + { + 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, "dac vol set persists audio.volume=40"); + } + do_ctrl(&b11, MSG_AUDIO_STOP, ack11.stream_id); + close(b11.fd); + stop_audiod(); + printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail); return g_fail ? 1 : 0; } diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index a35ff69..2a50eab 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -100,6 +100,10 @@ enum { MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */ MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */ MSG_AUDIO_VOLUME = 0x00CA, /* ↑ 音量(i32 pct:-1=查询 0-100=设置;v1 透传写 DAC numid=1/2) */ + /* P2 蓝牙(BLUETOOTH_FULL_OVERHAUL_PLAN §五):输出目标切换。audiod 读 settings.conf + * `audio.output_bt`(0/1) 或收本消息改写目标(0=OUT_DAC 1=OUT_BT),重配 PCM。 + * 与物理路由 numid=9 解耦:蓝牙非物理端口,不写 numid=9、不触发 FPGA 烧录。 */ + MSG_AUDIO_OUTPUT = 0x00CB, /* ↑ 输出目标(u32 target:0=DAC 1=BT;ui/CLI 下发) */ /* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ---- * 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。 * 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power: @@ -529,11 +533,23 @@ typedef struct m_audio_route { } m_audio_route_t; /* 4 B */ /* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2 - * + 持久化 settings.conf `audio.volume`,与 watchdog/launcher 同源同键)。 */ + * + 持久化 settings.conf `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时 + * **不下发 DAC 音量反转**(音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */ typedef struct m_audio_volume { int32_t pct; /* -1=查询;0-100=设置 */ } m_audio_volume_t; /* 4 B */ +/* 输出目标枚举(msg AUDIO_OUTPUT / settings.conf `audio.output_bt` 共用;亦用于 CLI output) */ +enum { + AUDIO_OUT_DAC = 0, /* 本地 DAC(hw:0,0,独占/period=1024×4/FPGA numid=9/音量反转) */ + AUDIO_OUT_BT = 1 /* 蓝牙 A2DP source(pcm.bluealsa 播放向,透传+格式归一,AVRCP 控音量) */ +}; + +/* MSG_AUDIO_OUTPUT ↑:切换输出目标(ui/CLI 下发)。0=DAC 1=BT。audiod 侧重配 PCM。 */ +typedef struct m_audio_output { + uint32_t target; /* AUDIO_OUT_DAC / AUDIO_OUT_BT */ +} m_audio_output_t; /* 4 B */ + /* ================= meow_netmgrd 网络管理服务(devdocs/plan/NETMGR_DESIGN.md §3/§10)===== * 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。 * 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr: