From 33bef36d1639e908aac74446b3c0e8ed61feb6a6 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 18:14:19 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(audiod):=20M50=20=E8=93=9D=E7=89=99?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E7=AB=AF=E5=88=B0=E7=AB=AF=20=E2=80=94=20?= =?UTF-8?q?=E5=86=85=E8=81=94=20DEV/CODEC=20=E6=89=93=E5=BC=80=20+=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=BF=85=E5=9B=9E=E8=90=BD=20DAC=20+=20MAC?= =?UTF-8?q?=20=E8=A7=A3=E6=9E=90=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① ALSA 正确用法(核心):不再写 /usr/data/alsa.conf(HiBy 播放器数据文件、语法非法、 libasound 从不加载,改它对解析零影响)。改为 snd_pcm_open("bluealsa:DEV=, PROFILE=a2dp,CODEC="),参数名严格取 /etc/alsa/conf.d/20-bluealsa.conf 的 pcm.bluealsa @args[DEV PROFILE CODEC VOL SOFTVOL DELAY SRV](实测未知参数会被 alsa-lib 拒绝);删掉 bt_apply_device_conf/bt_apply_codec_conf 两条改写分支。 ② 硬 bug:bt_dev_detect_mac() 旧逻辑取端点 token「最后一个 '/' 之后」——真机 bluealsa-cli list-pcms 输出是 D-Bus 路径 /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/ a2dpsrc/source,旧代码会把 "source" 当 MAC(旧单测喂的是人造输入所以一直是绿的)。 改按 dev_XX_XX_XX_XX_XX_XX 段解析,兼容老式 a2dpsrc/。 ③ codec:settings bt.codec 经 bt_dev_codec_token() 归一成 bluealsa canonical 名 (v4.1.1 a2dp-codecs.c 别名表,strcasecmp);auto/未知 → unchanged(不干预协商)。 ④ 失败必回落(硬验收):新增 BT_ST_IDLE/OK/FAIL 状态机 —— 无端点/未连接/set_params 失败/写路径致命错(ENODEV 等)都回落 DAC 继续出声,绝不整机静音;回落前同步武装 numid=9(hbc3000 未烧+enable 开 PCM 会楔死整机);STATE 增 bt=/btdev=/btcodec= 三键供面板显示「输出=蓝牙(失败)」,volctl 回落时报 dac。 ⑤ 防重试风暴:只在开流/重配边界重试,指数退避 2s→60s(bt_dev_should_try),写循环内 绝不重试;用户显式切目标才清退避。 ⑥ 音量:BT 真走 A2DP 时仍不下发 DAC 反转(回归确认),回落 DAC 时按 DAC 音量控; AVRCP 一键置最大从 100 改 127(bluealsa-cli volume 满量程 [0,127],旧值只到 79%)。 ⑦ build_audiod.sh:ROOT/REPO 自动推导,ZIG/CC/LIBBT/ASOUND_SRC 可环境变量覆盖(旧 hiby_os_crack 路径在多数 checkout 里不存在),宿主链接 -l:libasound.so.2 失败退 -lasound;保留 M43 的 meow_audio time32 cli 目标。 宿主:test_bt_dev 51/51(真机 D-Bus 路径格式/别名表/退避),test_audiod_host 109/0, 0 warning。真机:新二进制 glibc-dynamic 上机正常,DAC 出声回归通过,无端点回落 DAC 出声(played/rms 实证),/usr/data/alsa.conf 全流程零写入;耳机出声一项待耳机上电。 不提交二进制、不改 fw_content.conf 钉值(按塔交付形态政策,由 M51 统一重编)。 --- patch_v2/meow_audiod/bt_dev.c | 265 ++++++++++------- patch_v2/meow_audiod/bt_dev.h | 58 +++- patch_v2/meow_audiod/build_audiod.sh | 54 +++- patch_v2/meow_audiod/meow_audiod.c | 365 ++++++++++++++---------- patch_v2/meow_audiod/test_audiod_host.c | 5 + patch_v2/meow_audiod/test_bt_dev.c | 197 ++++++++----- 6 files changed, 605 insertions(+), 339 deletions(-) diff --git a/patch_v2/meow_audiod/bt_dev.c b/patch_v2/meow_audiod/bt_dev.c index 2d1f059..2634fa9 100644 --- a/patch_v2/meow_audiod/bt_dev.c +++ b/patch_v2/meow_audiod/bt_dev.c @@ -1,18 +1,83 @@ -/* bt_dev.c — 蓝牙 A2DP 设备自识别(纯逻辑实现,见 bt_dev.h 契约)。 +/* bt_dev.c — 蓝牙 A2DP 端点选择 + PCM 名构造 + 回落退避(纯逻辑实现,见 bt_dev.h 契约)。 * * 纯 libc(string / stdlib),无任何 meow_* 调用、无 popen、无文件 I/O —— meow_audiod 负责 - * 读取 /usr/data/alsa.conf、执行 `bluealsa-cli list-pcms` 及写回。宿主单测零链路重。 + * 执行 `bluealsa-cli list-pcms`、调用 libasound 及维护运行时状态。 */ #include "bt_dev.h" #include #include #include +#include + +/* 是否 a2dpsrc / a2dp_source 端点行(bluealsa 4.1.1 list-pcms 的端点头) */ +static int is_src_line(const char *p) +{ + return strstr(p, "a2dpsrc") != NULL || strstr(p, "a2dp_source") != NULL; +} + +/* 从 D-Bus 对象路径取 MAC。bluealsa 4.1.1 的 PCM 路径是 + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * (dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X,见 src/ba-transport.c / src/shared/dbus-client.c)。 + * 返回 0=解析出 17 字符 MAC(大写、冒号分隔)。 */ +static int parse_dev_seg(const char *line, char *mac, size_t macsz) +{ + if (!line || !mac || macsz < 18) return -1; + const char *d = strstr(line, "dev_"); + if (!d) return -1; + const char *p = d + 4; + char out[18]; + size_t o = 0; + for (int i = 0; i < 6; i++) { + unsigned v = 0; + int digits = 0; + for (; digits < 2; digits++) { + char c = *p; + int h = -1; + if (c >= '0' && c <= '9') h = c - '0'; + else if (c >= 'A' && c <= 'F') h = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') h = c - 'a' + 10; + if (h < 0) break; + v = v * 16u + (unsigned)h; + p++; + } + if (digits == 0) return -1; + o += (size_t)snprintf(out + o, sizeof out - o, (i == 0) ? "%02X" : ":%02X", v); + if (i < 5) { + if (*p != '_') return -1; + p++; + } + } + if (o != 17) return -1; + memcpy(mac, out, 18); + return 0; +} + +/* 兜底:端点 token 的最后一段直接是 MAC(老式/简化输出 `a2dpsrc/AA:BB:...`)。 */ +static int parse_tail_mac(const char *hit, char *mac, size_t macsz) +{ + const char *te = hit; + while (*te && *te != ' ' && *te != '\t' && *te != '\n') te++; + const char *slash = NULL; + for (const char *q = hit; q < te; q++) if (*q == '/') slash = q; + if (!slash || (slash + 1) >= te) return -1; + const char *start = slash + 1; + size_t len = (size_t)(te - start); + if (len != 17 || len >= macsz) return -1; + int colons = 0; + for (size_t i = 0; i < len; i++) if (start[i] == ':') colons++; + if (colons != 5) return -1; + memcpy(mac, start, len); + mac[len] = 0; + return 0; +} /* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。 - * 端点形如 `a2dpsrc/AA:BB:CC:DD:EE:FF`(MAC 在端点 token 内最后一个 '/' 之后到空白前)。 + * 真实输出是 D-Bus 对象路径(每行一个): + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * MAC 取自 `dev_XX_XX_XX_XX_XX_XX` 段(下划线 → 冒号),另兼容简化形式 `a2dpsrc/AA:BB:...`。 * 逐个**有界行**匹配(strstr 必须在行内,避免跨换行误把后一行当本行端点——如行 1 - * "profile/aptx" 与行 3 "a2dp_source/..." 的经典误判)。 */ + * "hfphf/sink" 与行 3 "a2dpsrc/source" 的经典误判)。 */ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz) { if (!listpcms || !mac || macsz < 2) return -1; @@ -27,112 +92,112 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz) memcpy(line, ln, linelen); line[linelen] = 0; const char *p = line; while (*p == ' ' || *p == '\t') p++; - const char *hit = strstr(p, "a2dpsrc"); - if (!hit) hit = strstr(p, "a2dp_source"); - if (hit) { - const char *te = hit; - while (*te && *te != ' ' && *te != '\t' && *te != '\n') te++; - const char *slash = NULL; - for (const char *q = hit; q < te; q++) if (*q == '/') slash = q; - if (slash && (slash + 1) < te) { - const char *start = slash + 1; - size_t len = (size_t)(te - start); - if (len > 0 && len < macsz) { - memcpy(mac, start, len); - mac[len] = 0; - return 0; - } - } + if (is_src_line(p)) { + if (parse_dev_seg(p, mac, macsz) == 0) return 0; + const char *hit = strstr(p, "a2dpsrc"); + if (!hit) hit = strstr(p, "a2dp_source"); + if (hit && parse_tail_mac(hit, mac, macsz) == 0) return 0; } if (!nl) break; ln = nl + 1; } + mac[0] = 0; return -1; } -/* 在单行内把 anchor("DEV=" / "device")后的 MAC 值替换为 mac,保留引号风格。 - * 行未含 anchor → 返回 1(out=原样);含 anchor → 写回新行并返回 0。 */ -static int line_replace_mac(char *out, size_t outsz, const char *line, - const char *anchor, const char *mac) +int bt_dev_count_src(const char *listpcms) { - const char *at = strstr(line, anchor); - if (!at) { snprintf(out, outsz, "%s", line); return 1; } - const char *vp = at + strlen(anchor); - while (*vp == ' ' || *vp == '\t') vp++; - if (*vp == '"') vp++; /* 引号风格:值可带双引号 */ - const char *ve = vp; - while (*ve && *ve != ' ' && *ve != '\t' && *ve != '\n' && - *ve != '"' && *ve != '{' && *ve != '}') ve++; - size_t prefix = (size_t)(vp - line); /* 前段(含开引号,若带) */ - size_t tail = (size_t)(ve - line); /* 值终点(不含闭引号,若带) */ - /* 前段 + mac + 尾段(尾段保留闭引号与其后内容) */ - snprintf(out, outsz, "%.*s%s%s", (int)prefix, line, mac, line + tail); - return 0; -} - -/* 幂等改写 alsa.conf:只把 `pcm.bluealsa` 块内 `DEV=` 与 `device` 后的 MAC 换成 mac。 - * 逐行处理;无 pcm.bluealsa / 无 DEV= / device token → -1;两端已一致 → 1;改了 → 0。 */ -int bt_dev_rewrite(const char *in, size_t in_len, const char *mac, - char **out, size_t *out_len) -{ - if (!in || !mac || !mac[0] || !out || !out_len || in_len == 0) return -1; - if (!strstr(in, "pcm.bluealsa") || - (!strstr(in, "DEV=") && !strstr(in, "device"))) return -1; - - size_t cap = in_len + 128; - char *res = malloc(cap ? cap : 1); - if (!res) return -1; - size_t o = 0, changed = 0; - const char *p = in, *end = in + in_len; - - while (p < end) { - const char *nl = memchr(p, '\n', (size_t)(end - p)); - const char *le = nl ? nl : end; - size_t linelen = (size_t)(le - p); - char line[512], newline[512]; - if (linelen >= sizeof line) linelen = sizeof line - 1; - memcpy(line, p, linelen); line[linelen] = 0; - - const char *k = line; - while (*k == ' ' || *k == '\t') k++; - int anchored = 0; - if (strstr(k, "DEV=")) { - line_replace_mac(newline, sizeof newline, line, "DEV=", mac); - anchored = 1; - } else if (strncmp(k, "device", 6) == 0 && - (k[6] == ' ' || k[6] == '\t' || k[6] == 0)) { - line_replace_mac(newline, sizeof newline, line, "device", mac); - anchored = 1; - } - if (anchored) { - if (strcmp(line, newline) != 0) changed = 1; - size_t n = strlen(newline); - if (o + n + 2 > cap) { - cap = o + n + 256; - char *t = realloc(res, cap); - if (!t) { free(res); return -1; } - res = t; - } - memcpy(res + o, newline, n); o += n; - if (nl) res[o++] = '\n'; - } else { - size_t n = linelen; - if (o + n + 2 > cap) { - cap = o + n + 256; - char *t = realloc(res, cap); - if (!t) { free(res); return -1; } - res = t; - } - memcpy(res + o, p, n); o += n; - if (nl) res[o++] = '\n'; - } + if (!listpcms) return 0; + int n = 0; + const char *ln = listpcms; + while (ln && *ln) { + const char *nl = strchr(ln, '\n'); + const char *p = ln; + while (*p == ' ' || *p == '\t') p++; + if (is_src_line(p)) n++; if (!nl) break; - p = nl + 1; + ln = nl + 1; } + return n; +} - if (!changed) { free(res); return 1; } - res[o] = 0; - *out = res; - *out_len = o; +/* bluealsa 4.1.1 src/shared/a2dp-codecs.c 的 canonical 名与别名(strcasecmp 匹配)。 + * 只列本机可达的音频 codec;auto/unchanged/未知 → unchanged(不干预协商)。 */ +static const struct { + const char *canonical; + const char *aliases[4]; +} codecs[] = { + { "SBC", { "SBC", NULL } }, + { "MP3", { "MP3", "MPEG12", "MPEG", NULL } }, + { "AAC", { "AAC", "MPEG24", NULL } }, + { "ATRAC", { "ATRAC", NULL } }, + { "aptX", { "aptX", "apt-X", NULL } }, + { "aptX-AD", { "aptX-AD", "apt-X-AD", NULL } }, + { "aptX-HD", { "aptX-HD", "apt-X-HD", NULL } }, + { "aptX-LL", { "aptX-LL", "apt-X-LL", NULL } }, + { "aptX-TWS", { "aptX-TWS", "apt-X-TWS", NULL } }, + { "FastStream", { "FastStream", "FS", NULL } }, + { "LC3plus", { "LC3plus", NULL } }, + { "LDAC", { "LDAC", NULL } }, + { "LHDC", { "LHDC", NULL } }, +}; + +const char *bt_dev_codec_token(const char *setting, char *out, size_t outsz) +{ + const char *auto_tok = "unchanged"; + if (!out || outsz < 2) return auto_tok; + if (!setting || !setting[0]) { snprintf(out, outsz, "%s", auto_tok); return out; } + if (!strcasecmp(setting, "auto") || !strcasecmp(setting, "unchanged")) { + snprintf(out, outsz, "%s", auto_tok); + return out; + } + for (size_t i = 0; i < sizeof codecs / sizeof codecs[0]; i++) + for (size_t n = 0; n < 4 && codecs[i].aliases[n]; n++) + if (!strcasecmp(codecs[i].aliases[n], setting)) { + snprintf(out, outsz, "%s", codecs[i].canonical); + return out; + } + snprintf(out, outsz, "%s", auto_tok); /* 未知值:不干预协商(不把垃圾塞进 PCM 名) */ + return out; +} + +/* MAC 合法性:6 组两位十六进制,'/' 或 ':' 分隔都不接受(只认冒号,bluealsa 约定)。 */ +static int mac_valid(const char *mac) +{ + if (!mac || strlen(mac) != 17) return 0; + for (int i = 0; i < 17; i++) { + if ((i % 3) == 2) { if (mac[i] != ':') return 0; } + else { + char c = mac[i]; + int hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + if (!hex) return 0; + } + } + return 1; +} + +int bt_dev_pcm_name(const char *mac, const char *codec_token, char *out, size_t outsz) +{ + if (!out || outsz < 32 || !mac_valid(mac)) return -1; + const char *tok = (codec_token && codec_token[0]) ? codec_token : "unchanged"; + int n = snprintf(out, outsz, "bluealsa:DEV=%s,PROFILE=a2dp,CODEC=%s", mac, tok); + if (n < 0 || (size_t)n >= outsz) { out[0] = 0; return -1; } return 0; } + +unsigned bt_dev_backoff_ms(unsigned fails) +{ + if (fails == 0) return 0; + unsigned shift = fails - 1; + if (shift > 5) shift = 5; + unsigned long d = 2000UL << shift; /* 2s,4s,8s,16s,32s,64s */ + if (d > 60000UL) d = 60000UL; + return (unsigned)d; +} + +int bt_dev_should_try(unsigned fails, long long since_ms) +{ + if (fails == 0) return 1; + if (since_ms < 0) return 1; + return since_ms >= (long long)bt_dev_backoff_ms(fails) ? 1 : 0; +} diff --git a/patch_v2/meow_audiod/bt_dev.h b/patch_v2/meow_audiod/bt_dev.h index 8c6ed76..96a3219 100644 --- a/patch_v2/meow_audiod/bt_dev.h +++ b/patch_v2/meow_audiod/bt_dev.h @@ -1,12 +1,19 @@ -/* bt_dev.h — 蓝牙 A2DP 设备自识别(DEV 自动更新纯逻辑,宿主可单测)。 +/* bt_dev.h — 蓝牙 A2DP 端点选择 + PCM 名构造 + 回落退避(纯逻辑,宿主可单测)。 * - * 背景:/usr/data/alsa.conf 的 `pcm.bluealsa`(plug→bluealsa)需把 DEV/device 钉在当前 - * 连接的 A2DP sink 设备 MAC 上才能正确出声。本模块提供两段纯逻辑(无 meow_* / popen / 文件 - * I/O —— meow_audiod 负责读文件与执行系统命令): - * - bt_dev_detect_mac:从 `bluealsa-cli list-pcms` 输出提取第一个 a2dpsrc 端点的设备 MAC; - * - bt_dev_rewrite:幂等改写 alsa.conf 文本,把 `pcm.bluealsa` 块内 `DEV=` 与 - * `device ` 两处换成目标 MAC(保留引号风格 / codec / 其余 token)。 - * 纯 libc(string / stdlib),独立编译,宿主零链接重。 + * 背景(M50 真机定案):`/usr/data/alsa.conf` **不是** alsa 配置(是 HiBy 播放器的数据文件, + * libasound 从不加载),改它对实际解析零影响。真正生效的是 alsa-lib 自带的 + * `/etc/alsa/conf.d/20-bluealsa.conf`: + * + * pcm.bluealsa { + * @args [ DEV PROFILE CODEC VOL SOFTVOL DELAY SRV ] ← 只有这 7 个名字 + * ... type plug / slave.pcm { type bluealsa ... } + * } + * + * 其 `@args` 均可从 PCM 名内联传入(`snd_pcm_open("bluealsa:DEV=,PROFILE=a2dp, + * CODEC=")`),未声明的参数会被 alsa-lib 拒绝(parse_args: Unknown parameter)—— + * 所以本模块只生成这 7 个里的 3 个,且**不改任何文件**。 + * + * 纯 libc(string/stdlib),无 meow_* / popen / 文件 I/O —— meow_audiod 负责探测与打开。 */ #ifndef BT_DEV_H #define BT_DEV_H @@ -14,14 +21,35 @@ #include /* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。 - * 返回 0=找到并写入 mac(NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。macsz 建议 ≥18。 */ + * 真实行为 D-Bus 对象路径(bluealsa 4.1.1): + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * 返回 0=找到并写入 mac(17 字符、大写冒号分隔,NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。 + * macsz ≥ 18。 */ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz); -/* 幂等改写 alsa.conf 文本(in_len 不含 NUL):把 `pcm.bluealsa` 块内 `DEV=` 与 - * `device ` 两处 MAC 换成 mac(保留引号风格、codec/其它 token 原样)。 - * 返回 0=已改动(*out 与 *out_len 给出新文本,调用方负责 free);1=无需改动(两端已一致); - * -1=无 pcm.bluealsa / 无 DEV= 或 device token / 参数非法(不动)。 */ -int bt_dev_rewrite(const char *in, size_t in_len, const char *mac, - char **out, size_t *out_len); +/* 统计 list-pcms 输出里的 a2dpsrc/a2dp_source 端点数(0=无可用 A2DP 源端点)。 + * 用于「先探测端点、再决定是否打开」——无端点时省掉一次注定失败的 open。 */ +int bt_dev_count_src(const char *listpcms); + +/* settings.conf `bt.codec` → bluealsa CODEC 参数 token。 + * bluealsa 4.1.1 的 a2dp_codecs_codec_id_from_string() 用 strcasecmp 匹配 canonical 名与别名, + * 故大小写不敏感;本函数把它归一成 canonical 名(日志/STATE 用),未知值/auto/unchanged → + * "unchanged"(不改协商结果,等价 20-bluealsa.conf 的 defaults.bluealsa.codec)。 + * 返回 out(调用方给的 buf)。outsz 建议 ≥16。 */ +const char *bt_dev_codec_token(const char *setting, char *out, size_t outsz); + +/* 组 snd_pcm_open 的 PCM 名: + * bluealsa:DEV=,PROFILE=a2dp,CODEC= + * mac 非 6 组十六进制 → -1(不拼非法名字);token 为空 → 用 "unchanged"。 + * 返回 0=写好(out NUL 结尾)。outsz 建议 ≥96。 */ +int bt_dev_pcm_name(const char *mac, const char *codec_token, char *out, size_t outsz); + +/* 回落退避:连续失败 fails 次后的重试间隔 ms(2s→4s→8s→16s→32s→60s 封顶;fails=0 → 0)。 + * 防重试风暴(M10 教训):失败越多等越久,绝不每次 OPEN 都硬打一次 BT。 */ +unsigned bt_dev_backoff_ms(unsigned fails); + +/* 纯决策:此刻是否允许再尝试一次 BT。fails=0 → 允许;否则要求距上次失败 ≥ 退避间隔。 + * since_ms<0(时钟异常)按「允许」处理,避免永久卡死。 */ +int bt_dev_should_try(unsigned fails, long long since_ms); #endif /* BT_DEV_H */ diff --git a/patch_v2/meow_audiod/build_audiod.sh b/patch_v2/meow_audiod/build_audiod.sh index fc8ab8e..3f96e46 100755 --- a/patch_v2/meow_audiod/build_audiod.sh +++ b/patch_v2/meow_audiod/build_audiod.sh @@ -17,18 +17,40 @@ # ./build_audiod.sh # 交叉编译 → meow_audiod(mipsel glibc-dynamic,BT 直链 libasound) # ./build_audiod.sh cli # 交叉编译 meow_audio CLI → meow_audio(mipsel musl-static) # ./build_audiod.sh host # 宿主编译 *_host 并跑 test_audiod_host 回归(FILE sink) +# +# M50 蓝牙输出端到端:audiod 不再读写 /usr/data/alsa.conf(那是 HiBy 播放器的数据文件、语法非法、 +# libasound 从不加载),改为 snd_pcm_open("bluealsa:DEV=,PROFILE=a2dp,CODEC=") +# 内联参数(对应 /etc/alsa/conf.d/20-bluealsa.conf 的 pcm.bluealsa @args),失败必回落 DAC。 +# +# 环境变量覆盖:ROOT / REPO 自动推导,ZIG / CC / LIBBT / ASOUND_SRC / CC_HOST 可显式指定。 set -e cd "$(dirname "$0")" -ROOT="$HOME/Project/R3ProII" -CC="$ROOT/.tools/zig-mipsel-cc" -ZIG="$ROOT/.tools/zig-env/bin/zig" -INC="-I$ROOT/patch_v2/meow_launcherd" +# ---- 路径解析(M50:全部可被环境变量覆盖,不再硬绑某台机器的目录布局)---- +# ROOT 只用于可选的历史路径(旧 .tools/zig-env、hiby_os_crack 解包树);缺省布局下不依赖它。 +ROOT="${ROOT:-$HOME/Project/R3ProII}" +REPO="$(cd "$(dirname "$0")/../.." && pwd)" # 仓库根(worktree 亦可) +# mipsel 静态 CC:优先显式环境变量 → 历史 zig 包装脚本 → PATH 里的 zig。 +CC="${CC:-}" +if [ -z "$CC" ]; then + if [ -x "$ROOT/.tools/zig-mipsel-cc" ]; then CC="$ROOT/.tools/zig-mipsel-cc"; else CC="zig cc --target=mipsel-linux-musleabi"; fi +fi +# 交叉编译用的 zig:显式 ZIG → 历史 zig-env → PATH。 +ZIG="${ZIG:-}" +if [ -z "$ZIG" ]; then + if [ -x "$ROOT/.tools/zig-env/bin/zig" ]; then ZIG="$ROOT/.tools/zig-env/bin/zig"; else ZIG="$(command -v zig || true)"; fi +fi +[ -n "$ZIG" ] || { echo "!! zig not found (set ZIG=/path/to/zig)"; exit 1; } +INC="-I$REPO/patch_v2/meow_launcherd" # 共享协议头(相对仓库根,不再绑 ROOT) if [ "$1" = "host" ]; then HOST_CC="${CC_HOST:-cc}" - # BT 目标直链宿主 libasound.so.2(无 asoundlib.h,原型内联于 meow_audiod.c;-l: 精确到版本, - # 不依赖 unversioned dev 链接)。删 -ldl:BT 不再用 dlopen。 - "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread -l:libasound.so.2 + # BT 目标直链宿主 libasound(无 asoundlib.h,原型内联于 meow_audiod.c)。删 -ldl:BT 不再用 dlopen。 + # 优先 -l:libasound.so.2(精确到版本,不依赖 unversioned dev 链接);宿主只装了无版本号 + # libasound.so(如 Termux)时退回 -lasound——两者符号集对我们用的 9 个原型等价。 + if ! "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host \ + -lpthread -l:libasound.so.2 2>/dev/null; then + "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread -lasound + fi "$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm "$HOST_CC" $INC -Os -Wall -Wextra test_audiod_host.c -o test_audiod_host -lm "$HOST_CC" -Os -Wall -Wextra test_bt_dev.c bt_dev.c -o test_bt_dev -lm @@ -59,15 +81,25 @@ fi # 当前钉在 fw_content.conf 的出货件则是旧工具链静态 musl,其 4403→ENOSYS→4263+宽度转换本身健康。 # 两种形态都不需要 meow_time32.c(其 __clock_* 接管针对 zig musl 内部符号)。musl-static 的 # meow_audio CLI 已链 time32。 -# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件镜像自拷贝(idempotent)。 -LIBBT="libbt" -ASOUND_SRC="$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0" +# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件树自拷贝(idempotent)。 +# M50:LIBBT / ASOUND_SRC 可被环境变量覆盖;ASOUND_SRC 候选按「显式 → 历史解包树 → 仓内固件基线」 +# 顺序取第一个存在的(旧值 $ROOT/hiby_os_crack/... 在多数 checkout 里不存在,set -e 下会直接卡住构建)。 +LIBBT="${LIBBT:-libbt}" +if [ -z "$ASOUND_SRC" ]; then + for c in "$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0" \ + "$REPO/devdocs/fw-baseline/squashfs-root/usr/lib/libasound.so.2.0.0" \ + "$REPO/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0"; do + [ -f "$c" ] && { ASOUND_SRC="$c"; break; } + done +fi +[ -n "$ASOUND_SRC" ] && [ -f "$ASOUND_SRC" ] || { + echo "!! libasound.so.2.0.0 not found (set ASOUND_SRC=/path/to/libasound.so.2.0.0)"; exit 1; } mkdir -p "$LIBBT" if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so" ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2" "$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod \ - -lpthread -Llibbt -lasound + -lpthread -L"$LIBBT" -lasound echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) ==" ls -la meow_audiod md5sum meow_audiod diff --git a/patch_v2/meow_audiod/meow_audiod.c b/patch_v2/meow_audiod/meow_audiod.c index 5ae2e8d..cd3e9b3 100644 --- a/patch_v2/meow_audiod/meow_audiod.c +++ b/patch_v2/meow_audiod/meow_audiod.c @@ -59,7 +59,7 @@ #include #include #include "meow_ipc.h" -#include "bt_dev.h" /* P2:DEV 自识别(a2dpsrc 端点 → MAC → alsa.conf 幂等改写) */ +#include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */ #ifdef __linux__ #include @@ -76,9 +76,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" /* 部分配置只定义别名节点,防打开失败再试一次 */ +/* ---- 蓝牙 A2DP source(P2 输出目标 OUT_BT)---- + * M50 真机定案:**不写任何配置文件**。设备真正生效的是 alsa-lib 自带的 + * /etc/alsa/conf.d/20-bluealsa.conf 里的 `pcm.bluealsa`(type plug → slave type bluealsa, + * @args = DEV PROFILE CODEC VOL SOFTVOL DELAY SRV)。DEV/CODEC 直接从 PCM 名内联传入, + * 一次 snd_pcm_open 即完成「钉设备 + 钉 codec」,无需 DEV 自识别改写文件。 + * ⚠️ /usr/data/alsa.conf 是 HiBy 播放器的数据文件、语法非法、libasound 从不加载——不读也不写。 */ +#define BT_PCM_BARE "bluealsa" /* 兜底:仅当内联名失败时试一次配置缺省(DEV 通配) */ #define BT_CODEC_AUTO "auto" #define BT_VOL_FULL_PCM "/usr/bin/bluealsa-cli" /* 一键设 AVRCP 100%(best-effort,不存在则跳过) */ @@ -203,11 +207,25 @@ static int g_dev_dsd = 0; /* 当前打开设备为 DoP DSD(决 static int g_reconfig_pending = 0; /* ---- 蓝牙 A2DP source 运行时状态(g_mu 保护;仅 g_target==OUT_BT && g_sink==SINK_ALSA 时用) ---- - * 直接调用 libasound 插件 PCM(pcm.bluealsa)。audiod 以动态 glibc 链接设备 libasound.so.2 - * (见 bt_* 段注释),此处仅存 snd_pcm_t 不透明句柄。 */ + * 直接调用 libasound 插件 PCM(见 bt_* 段注释),此处仅存 snd_pcm_t 不透明句柄。 + * + * 失败状态机(M50 硬验收点:失败必回落 DAC,绝不整机静音): + * BT_ST_IDLE —— 尚未尝试(启动初值 / 刚切到 BT 未开流) + * BT_ST_OK —— snd_pcm_open 成功,音频走 A2DP + * BT_ST_FAIL —— 最近一次尝试失败(无端点/未连接/set_params 失败/写路径致命错), + * 音频已回落 DAC;重试按 bt_dev_backoff_ms() 指数退避(2s→60s), + * 且只在「开流/重配」边界触发,绝不在写循环里重试(防 M10 重试风暴)。 */ +enum { BT_ST_IDLE = 0, BT_ST_OK = 1, BT_ST_FAIL = 2 }; static void *g_bt_hdl = NULL; /* snd_pcm_t *(libasound 不透明句柄) */ static int g_bt_open = 0; /* BT PCM 已打开 */ static int g_bt_vol_full_done = 0; /* 已对 AVRCP 置 100%(幂等,一次即可) */ +static int g_bt_state = BT_ST_IDLE; /* BT_ST_*(见上) */ +static unsigned g_bt_fail_n = 0; /* 连续失败次数(成功后归零) */ +static long long g_bt_last_fail_ms = 0; /* 上次失败时刻(CLOCK_MONOTONIC ms) */ +static char g_bt_mac[20] = ""; /* 已打开/最近探测到的 A2DP 端点 MAC */ +static char g_bt_tok[16] = "unchanged"; /* 实际下发的 CODEC token(canonical) */ +static int g_bt_fail_req = 0; /* 写路径判死:请求播放线程下一拍重配(回落 DAC) */ +static int g_bt_dev = 0; /* 当前「设备」是 BT PCM(dev_close 分流;g_mu 保护) */ /* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ---- * 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16), @@ -378,10 +396,12 @@ static int target_grant_fmt(int native_fmt) return AUDIO_FMT_S16; } -/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。 */ +/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。 + * BT 回落 DAC(bt=fail)时实际由 DAC 控,故报 dac——面板据此显示「蓝牙(失败)」。 */ static const char *volctl_name(void) { - return (g_target == AUDIO_OUT_BT) ? "avrcp" : "dac"; + if (g_target != AUDIO_OUT_BT) return "dac"; + return (g_bt_state == BT_ST_FAIL) ? "dac" : "avrcp"; } /* 整数开平方(uint64→uint32,逐位法;替代软浮点 sqrt) */ @@ -860,7 +880,8 @@ static int settings_get_output_target(void) } /* 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)。 */ + * 生效路径(M50):bt_dev_codec_token() 归一成 bluealsa canonical 名后,作为 CODEC 参数内联进 + * PCM 名(bluealsa:DEV=…,PROFILE=a2dp,CODEC=…);auto/未知 → unchanged(不干预协商)。 */ static const char *settings_get_bt_codec(void) { char buf[24]; @@ -926,137 +947,108 @@ const char *snd_strerror(int errnum); static void bt_close(void); /* 前向:bt_write 判死路径要用 */ static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */ -/* 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) +/* 单调时钟 ms(退避计时;CLOCK_MONOTONIC 不受系统时间跳变影响) */ +static long long now_ms(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); + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long)ts.tv_sec * 1000 + (long long)(ts.tv_nsec / 1000000); } -/* P2 BT DEV 自识别:把 /usr/data/alsa.conf 的 `pcm.bluealsa` DEV/device 自动改为**当前连接的 - * A2DP sink 设备** MAC(bluealsa-cli list-pcms 的 a2dpsrc 端点)。幂等(设备未变不重写)、 - * 无设备/失败跳过;只改 DEV/device 两处、保留 codec/其余 token。BT 打开前调用,实现 - * 「连哪个设备就播哪个」。跟 bt_apply_codec_conf 同构(外科式 token 替换,绝不建结构)。 */ -static void bt_apply_device_conf(void) +/* 记录一次 BT 失败:状态置 FAIL(→ 回落 DAC)+ 失败计数递增(退避依据)+ 日志。 + * 只在「开流/重配」边界或写路径判死时调用;写循环内绝不重试(防 M10 重试风暴)。 */ +static void bt_note_fail(const char *why) { - char mac[32] = ""; + g_bt_state = BT_ST_FAIL; + g_bt_fail_n++; + g_bt_last_fail_ms = now_ms(); + logmsg("bt: FAIL (%s) -> fallback DAC; fail#%u, next retry in %ums", + why ? why : "?", g_bt_fail_n, bt_dev_backoff_ms(g_bt_fail_n)); +} + +/* 成功:状态 OK、失败计数归零。 */ +static void bt_note_ok(void) +{ + g_bt_state = BT_ST_OK; + g_bt_fail_n = 0; +} + +/* 探测当前可用的 A2DP source 端点(bluealsa-cli list-pcms,1.5s 有界 popen)。 + * 返回端点数量;mac 非空时写入第一个 a2dpsrc 端点的设备 MAC。 + * 注意:bluealsa-cli v4.1.1 **没有** `a2dp list` 子命令,只有 list-pcms/list-services。 */ +static int bt_probe(char *mac, size_t macsz) +{ + if (mac && macsz) mac[0] = 0; char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); - if (!o) { logmsg("bt: dev conf skipped (no bluealsa-cli)"); return; } - if (bt_dev_detect_mac(o, mac, sizeof mac) != 0) { - logmsg("bt: dev conf skipped (no a2dpsrc pcm)"); - return; - } - logmsg("bt: current a2dp dev %s", mac); - const char *path = "/usr/data/alsa.conf"; - FILE *f = fopen(path, "r"); - if (!f) return; - fseek(f, 0, SEEK_END); long sz = ftell(f); - if (sz <= 0 || sz > 64 * 1024) { fclose(f); return; } - fseek(f, 0, SEEK_SET); - char *data = malloc((size_t)sz + 1); - if (!data) { fclose(f); return; } - size_t rl = fread(data, 1, (size_t)sz, f); data[rl] = 0; - fclose(f); - char *outdata = NULL; size_t outlen = 0; - int rc = bt_dev_rewrite(data, rl, mac, &outdata, &outlen); - free(data); - if (rc == 0) { - int fd = open(path, O_WRONLY | O_TRUNC, 0644); - if (fd >= 0) { - if (write(fd, outdata, outlen) < 0) { } - close(fd); - logmsg("bt: dev conf auto-updated %s -> %s", path, mac); - } else { - logmsg("bt: dev conf %s not writable, skip", path); - } - } else if (rc == 1) { - logmsg("bt: dev conf ok (already %s)", mac); - } else { - logmsg("bt: dev conf skip (no pcm.bluealsa/no DEV token)"); - } - if (outdata) free(outdata); + if (!o) { logmsg("bt: probe skipped (no bluealsa-cli)"); return 0; } + int n = bt_dev_count_src(o); + if (n > 0 && mac && macsz && bt_dev_detect_mac(o, mac, macsz) != 0) mac[0] = 0; + return n; } -/* BT 打开: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) +/* 按 PCM 名打开(libasound → NONBLOCK open + set_params S16/RW/ch/rate/soft_resample/100ms)。 + * 返回 0=ok(g_bt_hdl/g_bt_open/g_dev_* 已置位),-1=失败(句柄已 close,未记账)。 */ +static int bt_open_pcm(const char *name) { void *pcm = NULL; - const char *names[2] = { BT_PCM_DEFAULT, BT_PCM_FALLBACK }; - int r = -1; - for (int i = 0; i < 2; i++) { - r = snd_pcm_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], snd_strerror(r)); - pcm = NULL; + int r = snd_pcm_open(&pcm, name, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (r != 0) { + logmsg("bt: snd_pcm_open %s: %s", name, snd_strerror(r)); + return -1; } - if (r != 0) return -1; - g_bt_hdl = pcm; snd_pcm_nonblock(pcm, 1); unsigned rate = g_st.rate ? g_st.rate : 44100; unsigned ch = g_st.ch ? g_st.ch : 2; r = snd_pcm_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, snd_strerror(r)); - snd_pcm_close(pcm); g_bt_hdl = NULL; return -1; + logmsg("bt: set_params %u/%u/S16 (%s): %s", rate, ch, name, snd_strerror(r)); + snd_pcm_close(pcm); + return -1; } + g_bt_hdl = pcm; 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%(一次性;失败仅日志) */ + g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = AUDIO_FMT_S16; + logmsg("bt: PCM ready name=%s rate=%u ch=%u fmt=S16 codec=%s", name, rate, ch, g_bt_tok); return 0; } +/* 尝试把输出打开到 BT:退避门 → 端点探测(list-pcms)→ 内联 DEV/CODEC 打开 → + * 兜底裸 "bluealsa"(配置缺省 DEV 通配)→ 记账失败。 + * 返回 0=BT 就绪(音频走 A2DP);-1=本次不可用(调用方负责回落 DAC)。 + * ⚠️ 在 playback 线程持 g_mu 时执行(popen ≤1.5s + snd_pcm_open),所以只允许在开流/重配 + * 边界调用;连续失败由退避门(2s→60s)挡住,不会形成重试风暴。 */ +static int bt_try_open(void) +{ + long long since = now_ms() - g_bt_last_fail_ms; + if (!bt_dev_should_try(g_bt_fail_n, since)) { + static unsigned s_skip = 0; + if ((s_skip++ % 16) == 0) + logmsg("bt: retry gated (fail#%u, need %ums, elapsed %lldms)", + g_bt_fail_n, bt_dev_backoff_ms(g_bt_fail_n), since); + return -1; + } + char mac[20] = ""; + int eps = bt_probe(mac, sizeof mac); + if (eps <= 0 || !mac[0]) { bt_note_fail("no a2dp endpoint (device not connected?)"); return -1; } + snprintf(g_bt_mac, sizeof g_bt_mac, "%s", mac); + bt_dev_codec_token(g_bt_codec, g_bt_tok, sizeof g_bt_tok); + char name[96]; + if (bt_dev_pcm_name(g_bt_mac, g_bt_tok, name, sizeof name) != 0) { bt_note_fail("bad endpoint MAC"); return -1; } + logmsg("bt: endpoint %s (x%d) codec setting=%s -> token=%s", g_bt_mac, eps, g_bt_codec, g_bt_tok); + const char *opened = NULL; + if (bt_open_pcm(name) == 0) opened = name; + else if (bt_open_pcm(BT_PCM_BARE) == 0) opened = BT_PCM_BARE; + if (opened) { + bt_note_ok(); + g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 100% */ + bt_vol_full(); + return 0; + } + bt_note_fail("snd_pcm_open/set_params failed"); + return -1; +} + /* BT 写:非阻塞 writei + wait 超时(镜像 DAC O_NONBLOCK+poll 护栏),xrun recover。 * 设备停摆/无连接 → 丢帧(g_played_frames 照记),绝不把播放线程永久压进 libasound。 */ static void bt_write(uint8_t *buf, size_t n) @@ -1084,13 +1076,18 @@ static void bt_write(uint8_t *buf, size_t n) 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); + logmsg("bt WEDGED: %d consecutive waits, close + fallback DAC", stalls); bt_close(); g_bt_open = 0; g_dev_open = 0; + g_bt_fail_req = 1; /* 播放线程下一拍重配 → 回落 DAC(不静音) */ g_played_frames += n / fs; return; } continue; } - if (pr < 0 && errno != EINTR) { logmsg("bt wait err: %s", strerror(errno)); break; } + if (pr < 0 && errno != EINTR) { + logmsg("bt wait err: %s -> fallback DAC", strerror(errno)); + bt_close(); g_bt_open = 0; g_dev_open = 0; g_bt_fail_req = 1; + break; + } continue; } if (err == EPIPE || err == ESTRPIPE) { /* xrun:recover 后继续 */ @@ -1099,7 +1096,10 @@ static void bt_write(uint8_t *buf, size_t n) snd_pcm_recover(g_bt_hdl, err, 1); continue; } - logmsg("bt writei err: %s", snd_strerror(err)); + /* 端点消失/设备断开(ENODEV/ENXIO/EIO/ESHUTDOWN…):关 PCM 并请求回落 DAC。 + * **不在这里重试**(写循环内重试 = M10 重试风暴);退避重试在下次开流/重配边界。 */ + logmsg("bt writei err: %s -> fallback DAC", snd_strerror(err)); + bt_close(); g_bt_open = 0; g_dev_open = 0; g_bt_fail_req = 1; break; } if (w == 0) break; @@ -1119,14 +1119,15 @@ static void bt_close(void) } /* 音量隔离(PLAN §五):路由=BT 时**不下发 DAC 音量反转**(音量由 AVRCP/耳机侧控)。 - * 仅记录 DAC 音量(g_vol_pct 语义=本地 DAC 音量);BT 侧 best-effort 促成 AVRCP 100% - * (避免"窃听音量误解"——耳机侧音量钉到最响,设备侧滑块即绝对控制)。 */ + * BT 侧 best-effort 促成 AVRCP 最大音量(127 = bluealsa-cli volume 的满量程 [0,127]; + * 旧代码发 100 只到 79%,耳机偏轻的隐性原因之一),设备侧滑块即绝对控制。 + * PCM-PATH 取 list-pcms 的整行(D-Bus 对象路径)——bluealsa-cli 要的就是它,不是 MAC。 */ 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; } + if (!o) { logmsg("bt: avrcp set-max skipped (no bluealsa-cli)"); return; } char pcm[128] = ""; const char *ln = o; while (ln && *ln) { @@ -1143,22 +1144,45 @@ static void bt_vol_full(void) if (!nl) break; ln = nl + 1; } - if (!pcm[0]) { logmsg("bt: avrcp set-100 skipped (no a2dpsrc pcm found)"); return; } + if (!pcm[0]) { logmsg("bt: avrcp set-max 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); + snprintf(cmd, sizeof cmd, "%s volume %s 127 >/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)"); + logmsg("bt: avrcp volume set max(127) (%s)%s", pcm, r ? " ok" : " (exec failed; AVRCP still controls)"); } -/* 音量应用(含 BT 隔离):BT → 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、不持久化 - * audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。 +/* 实际输出是否真的在蓝牙上(BT_ST_OK)。STATE/路由/音量都以「实际」而非「请求」为准判断, + * 但 target= 仍报用户请求值(面板据此显示「输出=蓝牙(失败)」)。 */ +static int bt_effective_bt(void) +{ + return g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA && g_bt_state == BT_ST_OK; +} + +/* 请求了 BT 但当前回落 DAC(STATE 的 bt=fail;此时 DAC 音量/路由语义生效)。 */ +static int bt_fallback_active(void) +{ + return g_target == AUDIO_OUT_BT && g_bt_state == BT_ST_FAIL; +} + +static const char *bt_state_name(void) +{ + switch (g_bt_state) { + case BT_ST_OK: return "ok"; + case BT_ST_FAIL: return "fail"; + default: return "idle"; + } +} + +/* 音量应用(含 BT 隔离):BT(真实走 A2DP)→ 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、 + * 不持久化 audio.volume(那是 DAC 音量键);DAC 及「BT 回落 DAC」→ 原感知锥度反转 + 持久化 + * (回落时声音实际从 DAC 出,音量必须由 DAC 控,否则面板拖动无感)。 * target 为调用方在持锁下快照的 g_target;本函数不持 g_mu 执行(g_vol_pct 为 plain int * 松散写,同 route_thread 的 g_route_cur 语义,MIPS32 int 写天然原子)。 */ static void vol_apply(int pct, int target) { if (pct < 0) pct = 0; if (pct > 100) pct = 100; - if (target == AUDIO_OUT_BT) { + if (target == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL) { logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct); return; } @@ -1176,6 +1200,10 @@ static void target_set(int target) 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; + /* 用户显式切目标:清 BT 退避(切 BT 时立刻尝试一次;仍失败就按退避回落,不死缠蓝牙) */ + g_bt_fail_n = 0; + g_bt_last_fail_ms = 0; + g_bt_state = BT_ST_IDLE; if (g_st.active) g_reconfig_pending = 1; pthread_cond_signal(&g_cond); } @@ -1302,18 +1330,37 @@ static void wav_header(int fd, int rate, int ch, int bits, uint32_t data_len) /* ---- 播放线程设备管理(dev_open/close/write 皆在播放线程调用) ---- */ static void dev_close(void); /* 前向:dev_write 的 wedge 判死路径要用 */ +/* DAC 回落前的端口武装(复制 route_thread 的 boot-arm 语义,但同步执行)。 + * 为什么必须同步:hbc3000 FPGA 未烧录 + enable 时开 PCM 会在第二次 hbc3000_start 死锁整机 + * (M2.91 事故)。BT→DAC 回落是**流内**切换,不能等路由线程 1s 节拍,否则 PCM 可能先开。 */ +static void dac_fallback_arm(void) +{ + int hb = read_switch("/sys/class/switch/headset/state"); + int bal = read_switch("/sys/class/switch/balance/state"); + int port = (bal == 1) ? 3 : (hb == 1) ? 2 : 4; + int cur = amixer_get(9, 1); + if (cur == port) { + g_route_cur = port; + logmsg("bt: fallback arm skipped (numid=9 already %d)", port); + return; + } + logmsg("bt: fallback arm numid=9 -> %d (was %d)", port, cur); + apply_route(port); +} + static int dev_open(void) { - /* 输出目标=BT 且走真机 ALSA:打开 libasound 的 pcm.bluealsa(不开 hw:0,0)。 - * 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑, - * 真实 bluealsa 传输留给真机(见 bt_* 注释)。失败 → 降级丢帧(自感知兜底 §五)。 */ + /* 输出目标=BT 且走真机 ALSA:打开 libasound 的 bluealsa PCM(内联 DEV/CODEC,不开 hw:0,0)。 + * 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑。 + * **失败必回落 DAC**(M50 硬验收:没有 A2DP 端点时整机不许静音;同时区间由 bt_note_fail + * 记账退避,STATE 报 bt=fail 供面板显示「输出=蓝牙(失败)」)。 */ if (g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA) { - bt_apply_device_conf(); /* P2:DEV 幂等自更新到当前连接 A2DP 设备(连哪台播哪台) */ - 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; + if (bt_try_open() == 0) { g_bt_dev = 1; return 0; } /* g_bt_open=1(真实句柄在 g_bt_hdl) */ + logmsg("bt: open unavailable -> DAC fallback (audio continues)"); + dac_fallback_arm(); + /* 落到下面的 DAC 打开路径(grant fmt 已是 S16,DAC 支持) */ } + g_bt_dev = 0; 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) { @@ -1362,7 +1409,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 (bt_effective_bt()) { bt_write(buf, n); return; } if (g_sink == SINK_NULL) { g_played_frames += n / (size_t)frame_size(); return; } if (g_sink == SINK_ALSA) { #ifdef AUDIOD_HAVE_ALSA @@ -1428,7 +1475,9 @@ 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; } + /* 分流用 g_bt_dev(而非 g_bt_open):写路径判死时 bt_write 已 bt_close()(g_bt_open=0), + * 若此时按 g_bt_open 走 DAC 分支会把 g_dev_fd(=0) 当成真 fd 关掉(误关 stdin)。 */ + if (g_bt_dev) { bt_close(); g_bt_dev = 0; 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) { @@ -1484,7 +1533,8 @@ static void reconfigure_locked(void) g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = fmt; if (g_dev_fd == -2) g_dev_fd = -1; /* NULL sink:无真实 fd */ g_dev_dsd = (do_dsd_seq && new_dsd); - if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原(含 DSD_VOLUME) */ + if ((g_target == AUDIO_OUT_DAC || bt_fallback_active()) && g_sink == SINK_ALSA) + dac_vol_set(saved_vol); /* 复原(含 DSD_VOLUME);BT 回落 DAC 时同样要落 DAC 音量 */ logmsg("device open ok: %u/%u/%s sink=%d target=%s%s", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt), g_sink, tgt, g_dev_dsd ? " dsd" : ""); } else { @@ -1541,6 +1591,19 @@ static void *playback_thread(void *arg) pthread_mutex_unlock(&g_mu); if (n == 0) continue; dev_write(buf, n); + /* BT 写路径判死(端点消失/耳机断电/wedge)→ 下一拍重配回落 DAC。 + * 记账/状态在持锁下做;退避门保证这次重配不会立刻再打一次 BT(不形成重试风暴)。 */ + if (g_bt_fail_req) { + pthread_mutex_lock(&g_mu); + if (g_bt_fail_req) { + g_bt_fail_req = 0; + g_bt_dev = 0; + bt_note_fail("write path fatal (endpoint gone?)"); + g_reconfig_pending = 1; + pthread_cond_signal(&g_cond); + } + pthread_mutex_unlock(&g_mu); + } } pthread_mutex_lock(&g_mu); if (g_dev_open) dev_close(); @@ -1706,9 +1769,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 target=%s codec=%s", + logmsg("OPEN grant: sid=%u app=%s rate=%u ch=%u fmt=%s target=%s bt=%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); + fmt_name((int)g_st.fmt), target_name(), bt_state_name(), g_bt_codec); break; } case MSG_AUDIO_WRITE: { @@ -1749,15 +1812,19 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin } case MSG_AUDIO_QUERY: { char txt[1024]; + char bttok[16]; /* 路由口用缓存值 g_route_cur(路由线程维护),不在 IO 热路径 popen amixer—— * 驱动 wedge 时 amixer 虽有界但仍会拖慢 IO 循环 1.5s/次。 */ int rb = g_route_cur; int vol = g_vol_pct; + /* btcodec= 报「当前设置归一后的 token」(不是上次打开用的),便于面板/CLI 直接核对 + * settings bt.codec 到底会被下发放什么;纯函数,无 I/O。 */ + bt_dev_codec_token(g_bt_codec, bttok, sizeof bttok); snprintf(txt, sizeof txt, "active=%d app=%s sid=%u rate=%u ch=%u fmt=%s dsd=%d " "written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d " "route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u " - "target=%s codec=%s\n", + "target=%s bt=%s btdev=%s btcodec=%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, @@ -1772,7 +1839,8 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin (g_route_manual != -1 ? g_route_manual : rb), vol, volctl_name(), g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq, - target_name(), g_bt_codec); + target_name(), bt_state_name(), g_bt_mac[0] ? g_bt_mac : "-", + bttok, g_bt_codec); send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1); break; } @@ -2009,8 +2077,11 @@ static void *route_thread(void *arg) pthread_mutex_lock(&g_mu); int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */ int tgt = g_target; /* 输出目标快照(决定是否武装 FPGA) */ + /* M50:BT 失败回落 DAC 时 **必须武装**(否则 FPGA 未烧 + enable 下开 PCM 会楔死整机)。 + * 只有「请求 BT 且 BT 真的在用」才跳过 numid=9。 */ + int bt_hold = (tgt == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL); pthread_mutex_unlock(&g_mu); - if (tgt == AUDIO_OUT_BT) { + if (bt_hold) { 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 后补一次武装 */ diff --git a/patch_v2/meow_audiod/test_audiod_host.c b/patch_v2/meow_audiod/test_audiod_host.c index 1644eaf..2bcaccd 100644 --- a/patch_v2/meow_audiod/test_audiod_host.c +++ b/patch_v2/meow_audiod/test_audiod_host.c @@ -810,6 +810,11 @@ int main(void) 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)"); + /* M50:BT 运行时状态三键(面板据此显示「输出=蓝牙(失败)」)。FILE sink 从不尝试 BT 打开 + * → bt=idle 且 btdev=-(无端点);token 由 settings bt.codec=SBC 归一(canonical 同名)。 */ + check(strstr(q11, "bt=idle") != NULL, "bt=idle before any open attempt (FILE sink)"); + check(strstr(q11, "btdev=-") != NULL, "btdev=- when no endpoint opened"); + check(strstr(q11, "btcodec=SBC") != NULL, "btcodec=SBC (settings token normalized)"); /* 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); diff --git a/patch_v2/meow_audiod/test_bt_dev.c b/patch_v2/meow_audiod/test_bt_dev.c index 6fa1957..cbbd317 100644 --- a/patch_v2/meow_audiod/test_bt_dev.c +++ b/patch_v2/meow_audiod/test_bt_dev.c @@ -1,10 +1,17 @@ -/* test_bt_dev.c — 蓝牙 DEV 自识别宿主验证(纯逻辑,bt_dev.c)。 +/* test_bt_dev.c — 蓝牙端点选择 / PCM 名构造 / 回落退避 宿主验证(纯逻辑,bt_dev.c)。 * - * 覆盖矩阵(对齐任务B 宿主测试要求): - * D1 bt_dev_detect_mac:a2dpsrc/ 端点 → 提取 MAC(含 a2dp_source、多空行、多设备取第一个、 - * 无 a2dpsrc / 空输出 / NULL → -1)。 - * D2 bt_dev_rewrite:真实 alsa.conf 文本 → DEV=、device 两处 MAC 换成目标,保留 codec/其余 token; - * 同值 → 1(无需改动);无 pcm.bluealsa / 无 DEV= → -1;带引号风格保留。 + * 输入全部取自**真机实测格式**(bluealsa v4.1.1,设备 /usr/bin/bluealsa 的 + * `dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X` + src/ba-transport.c 的 `/a2dpsrc/source`): + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * + * 覆盖矩阵: + * D1 bt_dev_detect_mac:真实 D-Bus 路径 → MAC;多端点/多设备取第一个 a2dpsrc; + * hfphf/a2dpsnk 不算;简化 `a2dpsrc/` 兼容;空/无源端点/NULL/坏 dev_ 段 → -1。 + * D2 bt_dev_count_src:0/1/2 个源端点。 + * D3 bt_dev_codec_token:settings bt.codec → bluealsa canonical(4.1.1 别名表 strcasecmp); + * auto/空/unchanged/垃圾 → unchanged。 + * D4 bt_dev_pcm_name:内联 DEV/PROFILE/CODEC;坏 MAC/小缓冲 → -1。 + * D5 bt_dev_backoff_ms / should_try:2s→60s 封顶 + 门控(防重试风暴)。 * * 编译(在 patch_v2/meow_audiod 下): * cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c @@ -26,91 +33,149 @@ int main(void) { setbuf(stdout, NULL); - /* ---- D1 设备列表 → 选中 a2dpsrc 连接设备 ---- */ + /* ==== D1 设备列表 → 选中 a2dpsrc 连接设备(真实 D-Bus 路径格式) ==== */ { char mac[32] = ""; int rc = bt_dev_detect_mac( - "a2dpsrc/E0:42:6D:79:C5:98\na2dpsnk/E0:42:6D:79:C5:98\n", mac, sizeof mac); - check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1a a2dpsrc/ -> extract MAC"); + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac); + check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, + "D1a real D-Bus path -> MAC (dev_ underscores -> colons)"); } { char mac[32] = ""; int rc = bt_dev_detect_mac( - "profile/aptx\n\na2dp_source/33:DE:2E:21:AD:30\n", mac, sizeof mac); - check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1b a2dp_source/ -> extract MAC"); + "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n" + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsnk/sink\n" + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac); + check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, + "D1b hfphf/a2dpsnk skipped -> first a2dpsrc"); } { char mac[32] = ""; int rc = bt_dev_detect_mac( - "a2dpsrc/AA:BB:CC:DD:EE:FF 0\na2dpsrc/11:22:33:44:55:66 1\n", mac, sizeof mac); + "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/source\n" + "/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/source\n", mac, sizeof mac); check(rc == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, "D1c multi-device -> first a2dpsrc"); } { char mac[32] = ""; - int rc = bt_dev_detect_mac("\n", mac, sizeof mac); - check(rc == -1, "D1d empty output -> -1"); + int rc = bt_dev_detect_mac("a2dpsrc/E0:42:6D:79:C5:98\n", mac, sizeof mac); + check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1d legacy a2dpsrc/ still parsed"); } { char mac[32] = ""; - int rc = bt_dev_detect_mac("a2dpsnk/11:22:33:44:55:66\n", mac, sizeof mac); - check(rc == -1, "D1e only sink (no a2dpsrc) -> -1"); + int rc = bt_dev_detect_mac("", mac, sizeof mac); + check(rc == -1, "D1e empty output -> -1"); + } + { + char mac[32] = ""; + int rc = bt_dev_detect_mac( + "/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsnk/sink\n", mac, sizeof mac); + check(rc == -1, "D1f only sink (no a2dpsrc) -> -1"); } { char mac[32] = ""; int rc = bt_dev_detect_mac(NULL, mac, sizeof mac); - check(rc == -1, "D1f NULL input -> -1"); + check(rc == -1, "D1g NULL input -> -1"); + } + { + char mac[32] = ""; + int rc = bt_dev_detect_mac( + "/org/bluealsa/hci0/dev_ZZ_42_6D_79_C5_98/a2dpsrc/source\n", mac, sizeof mac); + check(rc == -1 && mac[0] == 0, "D1h malformed dev_ seg -> -1 (no garbage MAC)"); + } + { + char mac[32] = ""; + int rc = bt_dev_detect_mac( + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source 1\n", mac, sizeof mac); + check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1i trailing token tolerated"); } - /* ---- D2 DEV 串入 / 保留其它 token ---- */ + /* ==== D2 源端点计数(有无 A2DP 端点的快速判定) ==== */ + check(bt_dev_count_src("") == 0, "D2a empty -> 0"); + check(bt_dev_count_src( + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsnk/sink\n") == 0, + "D2b only sink -> 0"); + check(bt_dev_count_src( + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n") == 1, + "D2c one a2dpsrc -> 1"); + check(bt_dev_count_src( + "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/source\n" + "/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/source\n") == 2, + "D2d two a2dpsrc -> 2"); + + /* ==== D3 settings bt.codec → bluealsa canonical token ==== */ { - const char *conf = - "pcm.bluealsa:DEV=E0:42:6D:79:C5:98 {\n" - " type plug\n" - " slave {\n" - " pcm {\n" - " type bluealsa\n" - " device E0:42:6D:79:C5:98\n" - " profile \"a2dp\"\n" - " codec \"LDAC\"\n" - " ldac_eqmid \"LDAC_ABR\"\n" - " }\n" - " }\n" - "}\n"; - char *out = NULL; size_t olen = 0; - int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen); - check(rc == 0 && out != NULL, "D2a rewrite returns changed"); - check(out && strstr(out, ":DEV=33:DE:2E:21:AD:30 {"), "D2b DEV= updated"); - check(out && strstr(out, "device 33:DE:2E:21:AD:30\n"), "D2c device updated"); - check(out && strstr(out, "codec \"LDAC\"") && strstr(out, "ldac_eqmid \"LDAC_ABR\""), - "D2d codec/other tokens preserved"); - /* 旧 MAC 不再出现 */ - check(out && !strstr(out, "E0:42:6D:79:C5:98"), "D2e old MAC gone"); - free(out); - } - { - const char *conf = - "pcm.bluealsa:DEV=33:DE:2E:21:AD:30 {\n" - " type plug\n slave {\n pcm {\n" - " type bluealsa\n device 33:DE:2E:21:AD:30\n" - " profile \"a2dp\"\n codec \"LDAC\"\n }\n }\n}\n"; - char *out = NULL; size_t olen = 0; - int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen); - check(rc == 1 && out == NULL, "D2f same MAC -> unchanged (idempotent)"); - } - { - const char *conf = "pcm.bluealsa {\n type bluealsa\n device \"E0:42:6D:79:C5:98\"\n}\n"; - char *out = NULL; size_t olen = 0; - int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen); - check(rc == 0 && out && strstr(out, "device \"33:DE:2E:21:AD:30\""), - "D2g quoted device preserved (no DEV= token)"); - free(out); - } - { - char *out = NULL; size_t olen = 0; - int rc = bt_dev_rewrite("pcm.hw {\n type hw\n}\n", 21, "33:DE:2E:21:AD:30", &out, &olen); - check(rc == -1 && out == NULL, "D2h no pcm.bluealsa -> -1"); + struct { const char *in; const char *want; const char *name; } t[] = { + { "LDAC", "LDAC", "D3a LDAC passthrough" }, + { "ldac", "LDAC", "D3b lowercase ldac -> canonical" }, + { "SBC", "SBC", "D3c SBC" }, + { "AAC", "AAC", "D3d AAC" }, + { "MPEG24", "AAC", "D3e MPEG24 alias -> AAC" }, + { "aptX", "aptX", "D3f aptX" }, + { "apt-X", "aptX", "D3g apt-X alias -> aptX" }, + { "aptx-hd", "aptX-HD", "D3h aptx-hd -> aptX-HD" }, + { "MPEG", "MP3", "D3i MPEG alias -> MP3" }, + { "MP3", "MP3", "D3j MP3" }, + { "FastStream", "FastStream", "D3k FastStream" }, + { "FS", "FastStream", "D3l FS alias -> FastStream" }, + { "auto", "unchanged", "D3m auto -> unchanged (do not touch negotiation)" }, + { "", "unchanged", "D3n empty -> unchanged" }, + { "unchanged", "unchanged", "D3o unchanged -> unchanged" }, + { "BOGUS", "unchanged", "D3p unknown -> unchanged (never inject junk)" }, + }; + for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) { + char tok[16] = ""; + const char *r = bt_dev_codec_token(t[i].in, tok, sizeof tok); + check(r == tok && strcmp(tok, t[i].want) == 0, t[i].name); + } + char tok[16] = ""; + check(bt_dev_codec_token(NULL, tok, sizeof tok) == tok && !strcmp(tok, "unchanged"), + "D3q NULL setting -> unchanged"); } - printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail); - return g_fail; + /* ==== D4 PCM 名构造(20-bluealsa.conf @args 只认 DEV/PROFILE/CODEC 这套名字) ==== */ + { + char n[96] = ""; + int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, sizeof n); + check(rc == 0 && strcmp(n, "bluealsa:DEV=33:DE:2E:21:AD:30,PROFILE=a2dp,CODEC=LDAC") == 0, + "D4a name = bluealsa:DEV=,PROFILE=a2dp,CODEC="); + } + { + char n[96] = ""; + int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "", n, sizeof n); + check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4b empty token -> CODEC=unchanged"); + } + { + char n[96] = ""; + int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", NULL, n, sizeof n); + check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4c NULL token -> CODEC=unchanged"); + } + { + char n[96] = ""; + check(bt_dev_pcm_name("33:DE:2E:21:AD", "LDAC", n, sizeof n) == -1, "D4d short MAC -> -1"); + check(bt_dev_pcm_name("GG:DE:2E:21:AD:30", "LDAC", n, sizeof n) == -1, "D4e non-hex MAC -> -1"); + check(bt_dev_pcm_name(NULL, "LDAC", n, sizeof n) == -1, "D4f NULL MAC -> -1"); + check(bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, 16) == -1, "D4g tiny buffer -> -1"); + } + + /* ==== D5 回落退避(防重试风暴:M10 教训) ==== */ + check(bt_dev_backoff_ms(0) == 0, "D5a fails=0 -> 0ms (immediate)"); + check(bt_dev_backoff_ms(1) == 2000, "D5b fails=1 -> 2s"); + check(bt_dev_backoff_ms(2) == 4000, "D5c fails=2 -> 4s"); + check(bt_dev_backoff_ms(3) == 8000, "D5d fails=3 -> 8s"); + check(bt_dev_backoff_ms(4) == 16000, "D5e fails=4 -> 16s"); + check(bt_dev_backoff_ms(5) == 32000, "D5f fails=5 -> 32s"); + check(bt_dev_backoff_ms(6) == 60000, "D5g fails=6 -> 60s (capped)"); + check(bt_dev_backoff_ms(99) == 60000, "D5h fails=99 -> 60s (capped)"); + check(bt_dev_should_try(0, 0) == 1, "D5i never failed -> try now"); + check(bt_dev_should_try(1, 1999) == 0, "D5j fail#1 @1999ms -> gated"); + check(bt_dev_should_try(1, 2000) == 1, "D5k fail#1 @2000ms -> allowed"); + check(bt_dev_should_try(3, 7999) == 0, "D5l fail#3 @7999ms -> gated"); + check(bt_dev_should_try(3, 8000) == 1, "D5m fail#3 @8000ms -> allowed"); + check(bt_dev_should_try(2, -1) == 1, "D5n clock anomaly (negative) -> allowed (no deadlock)"); + + printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS (%d checks)\n", + g_fail ? g_fail : g_pass); + return g_fail ? 1 : 0; } From 6e7c0d00c021f6fbc0a1d0c1b2c4aaf3ece86767 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 18:42:51 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(audiod):=20M50=20r2=20=E8=93=9D?= =?UTF-8?q?=E7=89=99=E6=89=93=E5=BC=80=E6=97=B6=E5=BA=8F=20=E2=80=94=20MAC?= =?UTF-8?q?=20=E6=94=B9=E4=BB=8E=20hcitool=20con=20=E5=8F=96=EF=BC=88list-?= =?UTF-8?q?pcms=20=E6=98=93=E5=A4=B1=EF=BC=89+=20=E7=9F=AD=E9=87=8D?= =?UTF-8?q?=E8=AF=95=20+=20=E8=A1=8C=E5=86=85=E5=8C=B9=E9=85=8D=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机(2026-11-11 18:2x/18:3x,MOONDROP Pudding 33:DE:2E:21:AD:30)暴露两个时序/解析问题: ① **list-pcms 易失,不能当 MAC 主来源**:bluealsa 的 a2dpsrc PCM 对象只在 A2DP profile 配置好后的秒级窗口内存在(无人开 PCM 时 1~3s 就消失)。r1 用它取 MAC → 探针一错过窗口 就 btdev=- / bt=fail 回落。改为 **主来源 `hcitool con` 的 ACL 行**(内核 ACL 是连接权威, 开销一个 popen),拿到 MAC 直接 snd_pcm_open("bluealsa:DEV=,...") 让 bluealsa 在 open 时自己创建 PCM;list-pcms 降级为兜底来源。 ② **ACL 刚建立时 profile 可能还没配置好** → open 会瞬时 ENODEV。改 3 次 × 300ms 短重试 (上限 600ms,不占播放线程太久);失败后才记为 fail#n 进退避(仍只在开流/重配边界)。 ③ **端点路径要 D-Bus 全串**(`bluealsa-cli volume|info` 要它,不是 MAC):新增 bt_dev_endpoint_path();PCM 撑住后再 list-pcms 取路径(此时稳定)→ bt_refresh_endpoint()。 ④ **命令换行 bug**:旧 vol_full 的 token 扫描不认 '\n',真机把端点路径连换行一起塞进 `bluealsa-cli volume \n 127`,第二行成了独立命令 → 音量设置实际没下发。改为 bt_dev_endpoint_path() 严格按空白/\r/\n 截断。 ⑤ **行内匹配 bug**:is_src_line() 对整串 strstr 会借到下一行的 a2dpsrc —— hfphf/source 行被误判成 A2DP 源端点(count_src 少算/endpoint_path 取错行)。改 line_is_src() 只在 本行内判定。 ⑥ STATE 增 `btacodec=`:读回 `bluealsa-cli info` 的 Selected codec。真机 MOONDROP Pudding 只有 SBC/AAC,settings 请求 LDAC 时 bluealsa 接受参数但实际选 AAC —— 面板必须看到 实际值(btcodec= 仍是请求值,btacodec= 是实际值)。 ⑦ AVRCP 满量程 127(bluealsa-cli volume 取值范围 [0,127],旧值 100 只到 79%)。 宿主:test_bt_dev 66/66(新增 hcitool con 真机原文 / 端点路径 / info 原文解析用例), test_audiod_host 109/0,交叉编译 0 warning(67944B,md5 0ec3b9a6…)。 真机:r1 二进制 18:26 那次已实测 BT 出声(bt=ok / played=55296 / bluealsa Running:true / Delay 17.2ms / Volume 127),18:33 起耳机自动关机,r2 的 hcitool-con 主路径未能在真机复验 (耳机不可达),详见报告。 --- patch_v2/meow_audiod/bt_dev.c | 126 ++++++++++++++++++++++++++-- patch_v2/meow_audiod/bt_dev.h | 18 +++- patch_v2/meow_audiod/meow_audiod.c | 130 +++++++++++++++++++---------- patch_v2/meow_audiod/test_bt_dev.c | 104 ++++++++++++++++++++++- 4 files changed, 321 insertions(+), 57 deletions(-) diff --git a/patch_v2/meow_audiod/bt_dev.c b/patch_v2/meow_audiod/bt_dev.c index 2634fa9..24eba6e 100644 --- a/patch_v2/meow_audiod/bt_dev.c +++ b/patch_v2/meow_audiod/bt_dev.c @@ -10,14 +10,72 @@ #include #include -/* 是否 a2dpsrc / a2dp_source 端点行(bluealsa 4.1.1 list-pcms 的端点头) */ -static int is_src_line(const char *p) +/* 是否 a2dpsrc / a2dp_source 端点行(bluealsa 4.1.1 list-pcms 的端点头)。 + * ⚠️ 必须**行内**判定:直接对整串 strstr 会让「本行」借到「下一行」里的 a2dpsrc + * (真机 hfphf/source 行会被误判成 A2DP 源端点——M50 实测踩到)。 */ +static int line_is_src(const char *ln) { + const char *nl = strchr(ln, '\n'); + size_t len = nl ? (size_t)(nl - ln) : strlen(ln); + char line[256]; + if (len >= sizeof line) len = sizeof line - 1; + memcpy(line, ln, len); + line[len] = 0; + const char *p = line; + while (*p == ' ' || *p == '\t') p++; return strstr(p, "a2dpsrc") != NULL || strstr(p, "a2dp_source") != NULL; } +/* 从 `hcitool con` 输出取第一条 ACL 连接的对端 BD_ADDR。 + * 内核 ACL 是「设备是否真的连着」的权威(M45/M49 结论);bluealsa 的 list-pcms 只在 + * profile 配置好后的**秒级窗口**里存在,不能当 MAC 的唯一来源(M50 真机实测)。 + * 行形如:`\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT` + * 返回 0=找到(17 字符大写冒号分隔),-1=无 ACL 连接/参数非法。 */ +int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz) +{ + if (!hcitool_con || !mac || macsz < 18) return -1; + mac[0] = 0; + const char *ln = hcitool_con; + while (ln && *ln) { + const char *nl = strchr(ln, '\n'); + const char *le = nl ? nl : (ln + strlen(ln)); + size_t linelen = (size_t)(le - ln); + char line[256]; + if (linelen >= sizeof line) linelen = sizeof line - 1; + memcpy(line, ln, linelen); line[linelen] = 0; + if (strstr(line, "ACL")) { + /* 行内找合法的 MAC token(6 组 2 位十六进制,冒号分隔) */ + for (const char *p = line; *p; p++) { + int ok = 1; + for (int i = 0; i < 17 && ok; i++) { + if ((i % 3) == 2) { if (p[i] != ':') ok = 0; } + else { + char c = p[i]; + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) ok = 0; + } + } + /* 必须是完整 token:前一个字符不是十六进制/冒号,后一个不是 */ + if (ok && (p == line || !(p[-1] == ':' || (p[-1] >= '0' && p[-1] <= '9') || + (p[-1] >= 'A' && p[-1] <= 'F') || (p[-1] >= 'a' && p[-1] <= 'f'))) + && !(p[17] == ':' || (p[17] >= '0' && p[17] <= '9') || + (p[17] >= 'A' && p[17] <= 'F') || (p[17] >= 'a' && p[17] <= 'f'))) { + for (int i = 0; i < 17; i++) { + char c = p[i]; + mac[i] = (c >= 'a' && c <= 'f') ? (char)(c - 'a' + 'A') : c; + } + mac[17] = 0; + return 0; + } + } + } + if (!nl) break; + ln = nl + 1; + } + return -1; +} + /* 从 D-Bus 对象路径取 MAC。bluealsa 4.1.1 的 PCM 路径是 - * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink * (dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X,见 src/ba-transport.c / src/shared/dbus-client.c)。 * 返回 0=解析出 17 字符 MAC(大写、冒号分隔)。 */ static int parse_dev_seg(const char *line, char *mac, size_t macsz) @@ -74,10 +132,10 @@ static int parse_tail_mac(const char *hit, char *mac, size_t macsz) /* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。 * 真实输出是 D-Bus 对象路径(每行一个): - * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink * MAC 取自 `dev_XX_XX_XX_XX_XX_XX` 段(下划线 → 冒号),另兼容简化形式 `a2dpsrc/AA:BB:...`。 * 逐个**有界行**匹配(strstr 必须在行内,避免跨换行误把后一行当本行端点——如行 1 - * "hfphf/sink" 与行 3 "a2dpsrc/source" 的经典误判)。 */ + * "hfphf/source" 与行 2 "a2dpsrc/sink" 的经典误判)。 */ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz) { if (!listpcms || !mac || macsz < 2) return -1; @@ -92,7 +150,7 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz) memcpy(line, ln, linelen); line[linelen] = 0; const char *p = line; while (*p == ' ' || *p == '\t') p++; - if (is_src_line(p)) { + if (line_is_src(line)) { if (parse_dev_seg(p, mac, macsz) == 0) return 0; const char *hit = strstr(p, "a2dpsrc"); if (!hit) hit = strstr(p, "a2dp_source"); @@ -112,15 +170,65 @@ int bt_dev_count_src(const char *listpcms) const char *ln = listpcms; while (ln && *ln) { const char *nl = strchr(ln, '\n'); - const char *p = ln; - while (*p == ' ' || *p == '\t') p++; - if (is_src_line(p)) n++; + if (line_is_src(ln)) n++; if (!nl) break; ln = nl + 1; } return n; } +/* 取第一个 a2dpsrc/a2dp_source 端点的**完整 D-Bus 路径**(`bluealsa-cli volume|info` 要的就是它, + * 不是 MAC)。行内按空白截断(含 \r,防 CRLF 把换行带进命令);越界/无端点 → -1。 */ +int bt_dev_endpoint_path(const char *listpcms, char *out, size_t outsz) +{ + if (!listpcms || !out || outsz < 2) return -1; + out[0] = 0; + const char *ln = listpcms; + while (ln && *ln) { + const char *nl = strchr(ln, '\n'); + const char *p = ln; + while (*p == ' ' || *p == '\t') p++; + if (line_is_src(ln)) { + size_t n = 0; + while (p[n] && p[n] != ' ' && p[n] != '\t' && p[n] != '\n' && p[n] != '\r' && n + 1 < outsz) { + out[n] = p[n]; + n++; + } + out[n] = 0; + return n ? 0 : -1; + } + if (!nl) break; + ln = nl + 1; + } + return -1; +} + +/* 从 `bluealsa-cli info ` 输出取实际协商到的 codec("Selected codec: AAC" → "AAC")。 + * 只认行首 "Selected codec:"("Available codecs: SBC AAC" 那行不能误匹配)。 */ +int bt_dev_info_codec(const char *info_out, char *out, size_t outsz) +{ + if (!info_out || !out || outsz < 2) return -1; + out[0] = 0; + const char *ln = info_out; + while (ln && *ln) { + const char *nl = strchr(ln, '\n'); + const char *p = ln; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "Selected codec:", 15) == 0) { + p += 15; + while (*p == ' ' || *p == '\t') p++; + size_t n = 0; + while (p[n] && p[n] != '\n' && p[n] != '\r' && n + 1 < outsz) { out[n] = p[n]; n++; } + while (n > 0 && (out[n-1] == ' ' || out[n-1] == '\t')) n--; + out[n] = 0; + return n ? 0 : -1; + } + if (!nl) break; + ln = nl + 1; + } + return -1; +} + /* bluealsa 4.1.1 src/shared/a2dp-codecs.c 的 canonical 名与别名(strcasecmp 匹配)。 * 只列本机可达的音频 codec;auto/unchanged/未知 → unchanged(不干预协商)。 */ static const struct { diff --git a/patch_v2/meow_audiod/bt_dev.h b/patch_v2/meow_audiod/bt_dev.h index 96a3219..15281cd 100644 --- a/patch_v2/meow_audiod/bt_dev.h +++ b/patch_v2/meow_audiod/bt_dev.h @@ -20,9 +20,14 @@ #include -/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。 +/* 从 `hcitool con` 输出取第一条 ACL 连接的对端 BD_ADDR(**MAC 主来源**:内核 ACL 是连接权威, + * list-pcms 只在 profile 配置后的秒级窗口内存在)。返回 0=找到,-1=无 ACL/参数非法。 */ +int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz); + +/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC + * (**兜底来源**:list-pcms 可见时用)。 * 真实行为 D-Bus 对象路径(bluealsa 4.1.1): - * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink * 返回 0=找到并写入 mac(17 字符、大写冒号分隔,NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。 * macsz ≥ 18。 */ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz); @@ -31,6 +36,15 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz); * 用于「先探测端点、再决定是否打开」——无端点时省掉一次注定失败的 open。 */ int bt_dev_count_src(const char *listpcms); +/* 取 list-pcms 里第一个 a2dpsrc/a2dp_source 端点的完整 D-Bus 路径 + * (/org/bluealsa/hci0/dev_XX_../a2dpsrc/sink)——`bluealsa-cli volume|info` 要的是它,不是 MAC。 + * 返回 0=写好(out NUL 结尾),-1=无端点/参数非法。outsz 建议 ≥96。 */ +int bt_dev_endpoint_path(const char *listpcms, char *out, size_t outsz); + +/* 从 `bluealsa-cli info` 输出取实际协商 codec:行首 "Selected codec: AAC" → "AAC"。 + * 返回 0=找到,-1=无该行/参数非法。 */ +int bt_dev_info_codec(const char *info_out, char *out, size_t outsz); + /* settings.conf `bt.codec` → bluealsa CODEC 参数 token。 * bluealsa 4.1.1 的 a2dp_codecs_codec_id_from_string() 用 strcasecmp 匹配 canonical 名与别名, * 故大小写不敏感;本函数把它归一成 canonical 名(日志/STATE 用),未知值/auto/unchanged → diff --git a/patch_v2/meow_audiod/meow_audiod.c b/patch_v2/meow_audiod/meow_audiod.c index cd3e9b3..6d8d388 100644 --- a/patch_v2/meow_audiod/meow_audiod.c +++ b/patch_v2/meow_audiod/meow_audiod.c @@ -223,7 +223,9 @@ static int g_bt_state = BT_ST_IDLE; /* BT_ST_*(见上) */ static unsigned g_bt_fail_n = 0; /* 连续失败次数(成功后归零) */ static long long g_bt_last_fail_ms = 0; /* 上次失败时刻(CLOCK_MONOTONIC ms) */ static char g_bt_mac[20] = ""; /* 已打开/最近探测到的 A2DP 端点 MAC */ -static char g_bt_tok[16] = "unchanged"; /* 实际下发的 CODEC token(canonical) */ +static char g_bt_tok[16] = "unchanged"; /* 下发的 CODEC token(canonical) */ +static char g_bt_used[16] = ""; /* bluealsa info 回报的实际协商 codec(AAC/SBC…) */ +static char g_bt_pcm[128] = ""; /* 端点 D-Bus 路径(volume/info 用) */ static int g_bt_fail_req = 0; /* 写路径判死:请求播放线程下一拍重配(回落 DAC) */ static int g_bt_dev = 0; /* 当前「设备」是 BT PCM(dev_close 分流;g_mu 保护) */ @@ -944,8 +946,9 @@ int snd_pcm_drain(void *pcm); int snd_pcm_close(void *pcm); const char *snd_strerror(int errnum); -static void bt_close(void); /* 前向:bt_write 判死路径要用 */ -static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */ +static void bt_close(void); /* 前向:bt_write 判死路径要用 */ +static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */ +static void bt_read_used_codec(void); /* 前向:bt_open 成功时读回实际协商 codec */ /* 单调时钟 ms(退避计时;CLOCK_MONOTONIC 不受系统时间跳变影响) */ static long long now_ms(void) @@ -973,17 +976,39 @@ static void bt_note_ok(void) g_bt_fail_n = 0; } -/* 探测当前可用的 A2DP source 端点(bluealsa-cli list-pcms,1.5s 有界 popen)。 - * 返回端点数量;mac 非空时写入第一个 a2dpsrc 端点的设备 MAC。 - * 注意:bluealsa-cli v4.1.1 **没有** `a2dp list` 子命令,只有 list-pcms/list-services。 */ -static int bt_probe(char *mac, size_t macsz) +/* 找「现在要往哪台设备播」的 MAC。**主来源 = 内核 ACL(hcitool con)**: + * 这是「耳机真的连着」的权威,且开销极小;bluealsa 的 list-pcms 只在 profile 配置后的 + * 秒级窗口里才列出端点(M50 真机:无人开 PCM 时 1~3s 就消失),**不能当唯一来源** + * (否则探针一错过窗口就 bt=fail 回落)。list-pcms 仅作兜底,并顺手记下端点 D-Bus 路径 + * (volume/info 要用;PCM 被撑住后它才稳定,见 bt_refresh_endpoint)。 + * 返回 1=找到 MAC(写入 mac),0=当前没有可播的蓝牙设备。 */ +static int bt_find_mac(char *mac, size_t macsz) { if (mac && macsz) mac[0] = 0; + char *o = popen_read_tmo(1200, "hcitool con 2>/dev/null"); + if (o && bt_dev_acl_mac(o, mac, macsz) == 0) { + logmsg("bt: ACL device %s (source=hcitool con)", mac); + return 1; + } + o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); + if (!o) { logmsg("bt: no ACL and no bluealsa-cli"); return 0; } + char path[128]; + if (bt_dev_endpoint_path(o, path, sizeof path) == 0) snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path); + if (bt_dev_detect_mac(o, mac, macsz) == 0) { + logmsg("bt: a2dp dev %s (source=list-pcms fallback)", mac); + return 1; + } + return 0; +} + +/* PCM 已打开后刷新端点路径(此时 bluealsa 的端点对象是稳定的),供 info/volume 用。 */ +static void bt_refresh_endpoint(void) +{ + if (g_bt_pcm[0]) return; char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); - if (!o) { logmsg("bt: probe skipped (no bluealsa-cli)"); return 0; } - int n = bt_dev_count_src(o); - if (n > 0 && mac && macsz && bt_dev_detect_mac(o, mac, macsz) != 0) mac[0] = 0; - return n; + if (!o) return; + char path[128]; + if (bt_dev_endpoint_path(o, path, sizeof path) == 0) snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path); } /* 按 PCM 名打开(libasound → NONBLOCK open + set_params S16/RW/ch/rate/soft_resample/100ms)。 @@ -1029,24 +1054,31 @@ static int bt_try_open(void) return -1; } char mac[20] = ""; - int eps = bt_probe(mac, sizeof mac); - if (eps <= 0 || !mac[0]) { bt_note_fail("no a2dp endpoint (device not connected?)"); return -1; } + if (!bt_find_mac(mac, sizeof mac)) { bt_note_fail("no connected BT device (hcitool con empty)"); return -1; } snprintf(g_bt_mac, sizeof g_bt_mac, "%s", mac); bt_dev_codec_token(g_bt_codec, g_bt_tok, sizeof g_bt_tok); char name[96]; - if (bt_dev_pcm_name(g_bt_mac, g_bt_tok, name, sizeof name) != 0) { bt_note_fail("bad endpoint MAC"); return -1; } - logmsg("bt: endpoint %s (x%d) codec setting=%s -> token=%s", g_bt_mac, eps, g_bt_codec, g_bt_tok); + if (bt_dev_pcm_name(g_bt_mac, g_bt_tok, name, sizeof name) != 0) { bt_note_fail("bad device MAC"); return -1; } + logmsg("bt: try open %s (codec setting=%s -> token=%s)", name, g_bt_codec, g_bt_tok); + /* 3 次 × 300ms 短重试:ACL 刚建立时 BlueZ 可能还没把 A2DP profile 配置完, + * 这一下会返回 ENODEV/EBUSY;等 300ms 再来通常就通了。3 次仍失败才记账进退避 + * (短重试上限 600ms,不会在播放线程里形成长时间阻塞)。 */ const char *opened = NULL; - if (bt_open_pcm(name) == 0) opened = name; - else if (bt_open_pcm(BT_PCM_BARE) == 0) opened = BT_PCM_BARE; - if (opened) { - bt_note_ok(); - g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 100% */ - bt_vol_full(); - return 0; + for (int i = 0; i < 3 && !opened; i++) { + if (i) { + struct timespec ts = { 0, 300 * 1000000L }; + nanosleep(&ts, NULL); + } + if (bt_open_pcm(name) == 0) opened = name; + else if (bt_open_pcm(BT_PCM_BARE) == 0) opened = BT_PCM_BARE; } - bt_note_fail("snd_pcm_open/set_params failed"); - return -1; + if (!opened) { bt_note_fail("snd_pcm_open/set_params failed (3 tries)"); return -1; } + bt_note_ok(); + bt_refresh_endpoint(); /* PCM 已撑住:端点对象稳定,取路径给 info/volume */ + bt_read_used_codec(); + g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 满量程 */ + bt_vol_full(); + return 0; } /* BT 写:非阻塞 writei + wait 超时(镜像 DAC O_NONBLOCK+poll 护栏),xrun recover。 @@ -1126,25 +1158,15 @@ 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-max 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; + char pcm[128]; + snprintf(pcm, sizeof pcm, "%s", g_bt_pcm); /* bt_probe 已取到端点路径(省一次 popen) */ + if (!pcm[0]) { + char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); + if (!o) { logmsg("bt: avrcp set-max skipped (no bluealsa-cli)"); return; } + if (bt_dev_endpoint_path(o, pcm, sizeof pcm) != 0) { + logmsg("bt: avrcp set-max skipped (no a2dpsrc pcm found)"); return; } - if (!nl) break; - ln = nl + 1; } - if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no a2dpsrc pcm found)"); return; } char cmd[256]; snprintf(cmd, sizeof cmd, "%s volume %s 127 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm); char *r = popen_read_tmo(1500, cmd); @@ -1173,6 +1195,28 @@ static const char *bt_state_name(void) } } +/* 读回 bluealsa 实际协商到的 codec(`bluealsa-cli info <端点路径>` → "Selected codec:")。 + * 为什么需要:耳机不一定支持 settings 里点名的 codec——真机 MOONDROP Pudding 只有 SBC/AAC, + * 我们请求 LDAC 时 bluealsa 接受参数但实际选中 AAC。STATE 报实际值(btacodec=)才不撒谎。 + * 失败仅日志(best-effort,不影响播放)。 */ +static void bt_read_used_codec(void) +{ + g_bt_used[0] = 0; + if (!g_bt_pcm[0]) return; + char cmd[256]; + snprintf(cmd, sizeof cmd, "%s info %s 2>/dev/null", BT_VOL_FULL_PCM, g_bt_pcm); + char *o = popen_read_tmo(1500, cmd); + if (!o) return; + char codec[16]; + if (bt_dev_info_codec(o, codec, sizeof codec) == 0) { + snprintf(g_bt_used, sizeof g_bt_used, "%s", codec); + if (strcasecmp(codec, g_bt_tok) != 0) + logmsg("bt: codec requested=%s but bluealsa selected=%s (device codec set)", g_bt_tok, codec); + else + logmsg("bt: codec %s confirmed by bluealsa", codec); + } +} + /* 音量应用(含 BT 隔离):BT(真实走 A2DP)→ 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、 * 不持久化 audio.volume(那是 DAC 音量键);DAC 及「BT 回落 DAC」→ 原感知锥度反转 + 持久化 * (回落时声音实际从 DAC 出,音量必须由 DAC 控,否则面板拖动无感)。 @@ -1824,7 +1868,7 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin "active=%d app=%s sid=%u rate=%u ch=%u fmt=%s dsd=%d " "written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d " "route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u " - "target=%s bt=%s btdev=%s btcodec=%s codec=%s\n", + "target=%s bt=%s btdev=%s btcodec=%s btacodec=%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, @@ -1840,7 +1884,7 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin vol, volctl_name(), g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq, target_name(), bt_state_name(), g_bt_mac[0] ? g_bt_mac : "-", - bttok, g_bt_codec); + bttok, g_bt_used[0] ? g_bt_used : "-", g_bt_codec); send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1); break; } diff --git a/patch_v2/meow_audiod/test_bt_dev.c b/patch_v2/meow_audiod/test_bt_dev.c index cbbd317..0feeec5 100644 --- a/patch_v2/meow_audiod/test_bt_dev.c +++ b/patch_v2/meow_audiod/test_bt_dev.c @@ -1,8 +1,9 @@ /* test_bt_dev.c — 蓝牙端点选择 / PCM 名构造 / 回落退避 宿主验证(纯逻辑,bt_dev.c)。 * - * 输入全部取自**真机实测格式**(bluealsa v4.1.1,设备 /usr/bin/bluealsa 的 - * `dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X` + src/ba-transport.c 的 `/a2dpsrc/source`): - * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source + * 输入全部取自**真机实测原文**(bluealsa v4.1.1;设备 /usr/bin/bluealsa 的 + * `dev_%.2X_..` + src/ba-transport.c 的 `//`): + * /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink ← 实机(本机 a2dp-source) + * (同一格式下 /a2dpsrc/source 只是 PCM 模式不同,解析只认 dev_XX_.. 段) * * 覆盖矩阵: * D1 bt_dev_detect_mac:真实 D-Bus 路径 → MAC;多端点/多设备取第一个 a2dpsrc; @@ -12,6 +13,9 @@ * auto/空/unchanged/垃圾 → unchanged。 * D4 bt_dev_pcm_name:内联 DEV/PROFILE/CODEC;坏 MAC/小缓冲 → -1。 * D5 bt_dev_backoff_ms / should_try:2s→60s 封顶 + 门控(防重试风暴)。 + * D6 bt_dev_endpoint_path:取整条 D-Bus 路径给 bluealsa-cli volume|info(真机行含 /sink)。 + * D7 bt_dev_info_codec:真机 `bluealsa-cli info` 原文 → Selected codec(不能误吃 Available 行)。 + * D8 bt_dev_acl_mac:真机 `hcitool con` 原文 → ACL 对端 MAC(MAC 主来源;list-pcms 只在秒级窗口存在)。 * * 编译(在 patch_v2/meow_audiod 下): * cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c @@ -99,6 +103,11 @@ int main(void) check(bt_dev_count_src( "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n") == 1, "D2c one a2dpsrc -> 1"); + /* 行内判定回归:hfphf 行在 a2dpsrc 行之前,不能被算成源端点(整串 strstr 会误判) */ + check(bt_dev_count_src( + "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n" + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n") == 1, + "D2e hfphf line before a2dpsrc -> count 1 (per-line match, not whole-buffer)"); check(bt_dev_count_src( "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/source\n" "/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/source\n") == 2, @@ -159,6 +168,95 @@ int main(void) check(bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, 16) == -1, "D4g tiny buffer -> -1"); } + /* ==== D6 端点完整路径(volume/info 要 D-Bus 路径,不是 MAC) ==== */ + { + char path[128] = ""; + int rc = bt_dev_endpoint_path( + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n", path, sizeof path); + check(rc == 0 && strcmp(path, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0, + "D6a real /a2dpsrc/sink line -> full path (trailing newline trimmed)"); + } + { + char path[128] = ""; + int rc = bt_dev_endpoint_path( + "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n" + "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\r\n", path, sizeof path); + check(rc == 0 && strcmp(path, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0, + "D6b skips hfphf, strips CRLF (no newline leaks into popen cmd)"); + } + { + char path[128] = ""; + check(bt_dev_endpoint_path("/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsnk/sink\n", + path, sizeof path) == -1, "D6c no a2dpsrc -> -1"); + check(bt_dev_endpoint_path("", path, sizeof path) == -1, "D6d empty -> -1"); + check(bt_dev_endpoint_path(NULL, path, sizeof path) == -1, "D6e NULL -> -1"); + } + + /* ==== D7 实际协商 codec(真机 info 原文:MOONDROP Pudding 只有 SBC/AAC,请求 LDAC 实际选 AAC) ==== */ + { + const char *info = + "Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n" + "Sequence: 0\n" + "Transport: A2DP-source\n" + "Mode: sink\n" + "Running: true\n" + "Format: S16_LE\n" + "Channels: 2\n" + "Sampling: 48000 Hz\n" + "Available codecs: SBC AAC\n" + "Selected codec: AAC\n" + "Delay: 17.2 ms\n" + "SoftVolume: false\n" + "Volume: L: 127 R: 127\n" + "Muted: L: false R: false\n"; + char codec[16] = ""; + check(bt_dev_info_codec(info, codec, sizeof codec) == 0 && strcmp(codec, "AAC") == 0, + "D7a real info -> Selected codec=AAC (not the Available line)"); + } + { + const char *info = "Available codecs: SBC AAC\nSelected codec: SBC\r\n"; + char codec[16] = ""; + check(bt_dev_info_codec(info, codec, sizeof codec) == 0 && strcmp(codec, "SBC") == 0, + "D7b CRLF trimmed"); + } + { + char codec[16] = ""; + check(bt_dev_info_codec("Running: false\n", codec, sizeof codec) == -1, + "D7c no Selected line -> -1"); + check(bt_dev_info_codec(NULL, codec, sizeof codec) == -1, "D7d NULL -> -1"); + } + + /* ==== D8 hcitool con → ACL MAC(真机原文;MAC 主来源) ==== */ + { + const char *con = + "Connections:\n" + "\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n"; + char mac[32] = ""; + check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, + "D8a real hcitool con (state 1 AUTH ENCRYPT) -> ACL MAC"); + } + { + const char *con = + "Connections:\n" + "\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n"; + char mac[32] = ""; + check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, + "D8b half-open ACL (state 5) still gives MAC (kernel is authority)"); + } + { + const char *con = "Connections:\n"; + char mac[32] = ""; + check(bt_dev_acl_mac(con, mac, sizeof mac) == -1 && mac[0] == 0, "D8c no ACL -> -1"); + check(bt_dev_acl_mac(NULL, mac, sizeof mac) == -1, "D8d NULL -> -1"); + } + { + /* 小写输入归一为大写(bluealsa DEV 参数按大写比较) */ + const char *con = "\t< ACL aa:bb:cc:dd:ee:ff handle 3 state 1 lm SLAVE \n"; + char mac[32] = ""; + check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, + "D8e lowercase -> uppercase canonical"); + } + /* ==== D5 回落退避(防重试风暴:M10 教训) ==== */ check(bt_dev_backoff_ms(0) == 0, "D5a fails=0 -> 0ms (immediate)"); check(bt_dev_backoff_ms(1) == 2000, "D5b fails=1 -> 2s"); From 6e67c74fe6feb5efb402eb44956cff8ccd9a1bcd Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 19:20:30 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(audiod):=20M50=20r3=20=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=20P2=20=E4=B8=89=E6=9D=A1=20=E2=80=94=20fd=20?= =?UTF-8?q?=E5=8D=AB=E7=94=9F=20/=20=E7=AB=AF=E7=82=B9=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=8C=89=20MAC=20=E7=B2=BE=E7=A1=AE=E5=8C=B9=E9=85=8D=20/=20?= =?UTF-8?q?=E5=A4=9A=20ACL=20=E9=80=90=E4=B8=AA=E8=AF=95=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审 r1(p2-3items/merge,reviewed 26c6077)三条: ① P2-1 dev_close() 在「BT 写路径判死 → 回落」这条路上会 close(0) 误关 stdin: 播放线程处理 g_bt_fail_req 时**提前把 g_bt_dev 清 0**,下拍 dev_close() 就落到 close(g_dev_fd) 分支,而 BT 打开成功时 dev_open() 返回 0(g_dev_fd==0)。 - 去掉那行提前清零(让 dev_close 走 BT 分支自己清),并同时置 g_dev_fd = -1 做第二道保险; - 把关闭决策抽成纯函数 bt_dev_close_kind(bt_dev, fd)(0=NONE/1=BT/2=FD):**fd 0/1/2 一律不当设备 fd 关**,宿主单测 D10 覆盖(含 bt_dev=0+fd=0 这条回归用例); - 补不变式「设备 fd 恒 >2」:dev_open 的 DAC/WAV open 过 fd_high()(0..2 时 F_DUPFD 升位, dup 失败则关掉并报打开失败——绝不留 stdio 区间里没人认领的设备 fd); - dev_close 注释改成诚实版:不再声称「已消除」,写明「BT 分支内自行清理;调用方不得 提前清 g_bt_dev」。 ② P2-2 g_bt_pcm 只写一次 → 换耳机 A→B 后 volume/info 打在 A 的端点上: - bt_refresh_endpoint() 每次先清空再探测,并用新 bt_dev_pick_endpoint() **按 g_bt_mac 精确挑端点**(找不到就留空,宁可跳过 volume/info 也不串台); - bt_vol_full() 删除「拿 list-pcms 第一条端点凑数」的兜底(同样会串台); - 单测 D9:两条端点里按 MAC 分别取 A/B、未知 MAC 返回 -1。 ③ P2-3 bt_dev_acl_mac() 只取第一条 ACL,多设备(手机/手柄并存,M49 正在测 BT 控制面) 会挑错设备 → 耳机连着却回落 DAC: - 改名/重写为 bt_dev_acl_macs():收集**全部** ACL MAC(去重、上限由调用方给); - bt_find_mac() → bt_collect_candidates():①list-pcms 里已有端点的设备优先(真 A2DP) ②其后全部 ACL 设备;候选按可信度排序、去重; - bt_try_open() 改为对候选**轮流试开**,3 次尝试、300ms 间隔(1 候选=A,A,A 与 r2 语义 相同;2 候选=A,B,A;3 候选=A,B,C),仍受退避门控;**删除裸 "bluealsa" 兜底** (那会落到「最近连接」的错设备,review §P2-3 明确指出); - 单测 D8c/D8d:两条 ACL 全取到且保序、三条含重复时去重并受上限裁剪。 顺带清死代码(评审「其余观察 2」):删除 bt_dev_count_src()、bt_dev_endpoint_path() (生产路径已无调用者,前者早已只剩单测)。 宿主:test_bt_dev 71/71(新增 D8c/D8d/D9/D10 共 10 条),test_audiod_host 109/0, 交叉编译 0 warning(68604B,md5 9f58b2197ed91d24951918d41a78cf29,NEEDED 三条、 interpreter /lib/ld.so.1、GLIBC≤2.17),test_time32_abi.sh --builds 里 `build meow_audio src=yes macro=yes` 保持。真机项(耳机)未动:本轮不连蓝牙、不扫设备。 --- patch_v2/meow_audiod/bt_dev.c | 97 +++++++++++--------- patch_v2/meow_audiod/bt_dev.h | 22 +++-- patch_v2/meow_audiod/meow_audiod.c | 142 ++++++++++++++++++----------- patch_v2/meow_audiod/test_bt_dev.c | 129 ++++++++++++++------------ 4 files changed, 222 insertions(+), 168 deletions(-) diff --git a/patch_v2/meow_audiod/bt_dev.c b/patch_v2/meow_audiod/bt_dev.c index 24eba6e..b63b943 100644 --- a/patch_v2/meow_audiod/bt_dev.c +++ b/patch_v2/meow_audiod/bt_dev.c @@ -26,17 +26,21 @@ static int line_is_src(const char *ln) return strstr(p, "a2dpsrc") != NULL || strstr(p, "a2dp_source") != NULL; } -/* 从 `hcitool con` 输出取第一条 ACL 连接的对端 BD_ADDR。 +/* 从 `hcitool con` 输出收集**所有** ACL 连接的对端 BD_ADDR(按出现顺序去重)。 * 内核 ACL 是「设备是否真的连着」的权威(M45/M49 结论);bluealsa 的 list-pcms 只在 * profile 配置好后的**秒级窗口**里存在,不能当 MAC 的唯一来源(M50 真机实测)。 + * ⚠️ 多条 ACL 是常态(手机/手柄/第二个耳机同时连着)——**不能只取第一条**: + * 取错设备会 snd_pcm_open 失败 → 有耳机却回落 DAC(M50 r1 评审 P2-3)。 * 行形如:`\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT` - * 返回 0=找到(17 字符大写冒号分隔),-1=无 ACL 连接/参数非法。 */ -int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz) + * 返回找到的条数(0=无 ACL 连接)。macs 是 [max][18] 的二维数组,元素写成 NUL 结尾 + * 17 字符大写冒号 MAC。 */ +int bt_dev_acl_macs(const char *hcitool_con, char (*macs)[18], int max) { - if (!hcitool_con || !mac || macsz < 18) return -1; - mac[0] = 0; + if (!hcitool_con || !macs || max <= 0) return 0; + for (int i = 0; i < max; i++) macs[i][0] = 0; + int n = 0; const char *ln = hcitool_con; - while (ln && *ln) { + while (ln && *ln && n < max) { const char *nl = strchr(ln, '\n'); const char *le = nl ? nl : (ln + strlen(ln)); size_t linelen = (size_t)(le - ln); @@ -44,8 +48,7 @@ int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz) if (linelen >= sizeof line) linelen = sizeof line - 1; memcpy(line, ln, linelen); line[linelen] = 0; if (strstr(line, "ACL")) { - /* 行内找合法的 MAC token(6 组 2 位十六进制,冒号分隔) */ - for (const char *p = line; *p; p++) { + for (const char *p = line; *p && n < max; p++) { int ok = 1; for (int i = 0; i < 17 && ok; i++) { if ((i % 3) == 2) { if (p[i] != ':') ok = 0; } @@ -54,24 +57,27 @@ int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz) if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) ok = 0; } } - /* 必须是完整 token:前一个字符不是十六进制/冒号,后一个不是 */ - if (ok && (p == line || !(p[-1] == ':' || (p[-1] >= '0' && p[-1] <= '9') || - (p[-1] >= 'A' && p[-1] <= 'F') || (p[-1] >= 'a' && p[-1] <= 'f'))) - && !(p[17] == ':' || (p[17] >= '0' && p[17] <= '9') || - (p[17] >= 'A' && p[17] <= 'F') || (p[17] >= 'a' && p[17] <= 'f'))) { - for (int i = 0; i < 17; i++) { - char c = p[i]; - mac[i] = (c >= 'a' && c <= 'f') ? (char)(c - 'a' + 'A') : c; - } - mac[17] = 0; - return 0; + if (!ok || + !(p == line || !(p[-1] == ':' || (p[-1] >= '0' && p[-1] <= '9') || + (p[-1] >= 'A' && p[-1] <= 'F') || (p[-1] >= 'a' && p[-1] <= 'f'))) || + (p[17] == ':' || (p[17] >= '0' && p[17] <= '9') || + (p[17] >= 'A' && p[17] <= 'F') || (p[17] >= 'a' && p[17] <= 'f'))) + continue; + char mac[18]; + for (int i = 0; i < 17; i++) { + char c = p[i]; + mac[i] = (c >= 'a' && c <= 'f') ? (char)(c - 'a' + 'A') : c; } + mac[17] = 0; + int dup = 0; /* 同一设备可能在多行/多 profile 出现 */ + for (int i = 0; i < n; i++) if (strcmp(macs[i], mac) == 0) { dup = 1; break; } + if (!dup) memcpy(macs[n++], mac, 18); } } if (!nl) break; ln = nl + 1; } - return -1; + return n; } /* 从 D-Bus 对象路径取 MAC。bluealsa 4.1.1 的 PCM 路径是 @@ -163,37 +169,27 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz) return -1; } -int bt_dev_count_src(const char *listpcms) +/* 在 list-pcms 里按「设备 MAC」精确挑端点路径(A→B 换耳机后不能拿 A 的路径去发 volume/info)。 + * 返回 0=找到(写到 out),-1=列表里没有该 MAC 的端点。 */ +int bt_dev_pick_endpoint(const char *listpcms, const char *mac, char *out, size_t outsz) { - if (!listpcms) return 0; - int n = 0; - const char *ln = listpcms; - while (ln && *ln) { - const char *nl = strchr(ln, '\n'); - if (line_is_src(ln)) n++; - if (!nl) break; - ln = nl + 1; - } - return n; -} - -/* 取第一个 a2dpsrc/a2dp_source 端点的**完整 D-Bus 路径**(`bluealsa-cli volume|info` 要的就是它, - * 不是 MAC)。行内按空白截断(含 \r,防 CRLF 把换行带进命令);越界/无端点 → -1。 */ -int bt_dev_endpoint_path(const char *listpcms, char *out, size_t outsz) -{ - if (!listpcms || !out || outsz < 2) return -1; + if (!listpcms || !mac || !mac[0] || !out || outsz < 2) return -1; out[0] = 0; const char *ln = listpcms; while (ln && *ln) { const char *nl = strchr(ln, '\n'); - const char *p = ln; - while (*p == ' ' || *p == '\t') p++; - if (line_is_src(ln)) { + const char *le = nl ? nl : (ln + strlen(ln)); + size_t linelen = (size_t)(le - ln); + char line[256], lmac[18]; + if (linelen >= sizeof line) linelen = sizeof line - 1; + memcpy(line, ln, linelen); line[linelen] = 0; + if (line_is_src(line) && parse_dev_seg(line, lmac, sizeof lmac) == 0 && + strcasecmp(lmac, mac) == 0) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; size_t n = 0; - while (p[n] && p[n] != ' ' && p[n] != '\t' && p[n] != '\n' && p[n] != '\r' && n + 1 < outsz) { - out[n] = p[n]; - n++; - } + while (p[n] && p[n] != ' ' && p[n] != '\t' && p[n] != '\n' && p[n] != '\r' && + n + 1 < outsz) { out[n] = p[n]; n++; } out[n] = 0; return n ? 0 : -1; } @@ -203,6 +199,17 @@ int bt_dev_endpoint_path(const char *listpcms, char *out, size_t outsz) return -1; } +/* 关闭决策(纯函数,宿主可单测):当前「设备」该关谁。 + * bt_dev=1 → 关 BT PCM(句柄在 audiod 的 g_bt_hdl),**不碰 fd**; + * 否则 fd>2 才当设备 fd 关;**0/1/2 一律不动**——BT 打开成功时 dev_open() 返回 0, + * 任何「提前清 bt_dev」的路径都会落到 close(g_dev_fd) 上误关 stdin(M50 r1 评审 P2-1)。 + * 返回 0=NONE / 1=BT / 2=FD。 */ +int bt_dev_close_kind(int bt_dev, int fd) +{ + if (bt_dev) return 1; + return fd > 2 ? 2 : 0; +} + /* 从 `bluealsa-cli info ` 输出取实际协商到的 codec("Selected codec: AAC" → "AAC")。 * 只认行首 "Selected codec:"("Available codecs: SBC AAC" 那行不能误匹配)。 */ int bt_dev_info_codec(const char *info_out, char *out, size_t outsz) diff --git a/patch_v2/meow_audiod/bt_dev.h b/patch_v2/meow_audiod/bt_dev.h index 15281cd..75f2f41 100644 --- a/patch_v2/meow_audiod/bt_dev.h +++ b/patch_v2/meow_audiod/bt_dev.h @@ -20,9 +20,11 @@ #include -/* 从 `hcitool con` 输出取第一条 ACL 连接的对端 BD_ADDR(**MAC 主来源**:内核 ACL 是连接权威, - * list-pcms 只在 profile 配置后的秒级窗口内存在)。返回 0=找到,-1=无 ACL/参数非法。 */ -int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz); +/* 从 `hcitool con` 输出收集**所有** ACL 连接的对端 BD_ADDR(按出现顺序去重;MAC 主来源: + * 内核 ACL 是连接权威,list-pcms 只在 profile 配置后的秒级窗口内存在)。 + * 多设备是常态,调用方要**逐个试开**而不是只取第一条(M50 r1 评审 P2-3)。 + * macs 为 [max][18];返回条数(0=无 ACL)。 */ +int bt_dev_acl_macs(const char *hcitool_con, char (*macs)[18], int max); /* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC * (**兜底来源**:list-pcms 可见时用)。 @@ -32,14 +34,14 @@ int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz); * macsz ≥ 18。 */ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz); -/* 统计 list-pcms 输出里的 a2dpsrc/a2dp_source 端点数(0=无可用 A2DP 源端点)。 - * 用于「先探测端点、再决定是否打开」——无端点时省掉一次注定失败的 open。 */ -int bt_dev_count_src(const char *listpcms); -/* 取 list-pcms 里第一个 a2dpsrc/a2dp_source 端点的完整 D-Bus 路径 - * (/org/bluealsa/hci0/dev_XX_../a2dpsrc/sink)——`bluealsa-cli volume|info` 要的是它,不是 MAC。 - * 返回 0=写好(out NUL 结尾),-1=无端点/参数非法。outsz 建议 ≥96。 */ -int bt_dev_endpoint_path(const char *listpcms, char *out, size_t outsz); +/* 在 list-pcms 里按设备 MAC 精确挑端点路径(换耳机 A→B 后不能拿 A 的路径发 volume/info)。 + * 返回 0=找到,-1=该 MAC 无端点。 */ +int bt_dev_pick_endpoint(const char *listpcms, const char *mac, char *out, size_t outsz); + +/* 关闭决策(纯函数):bt_dev=1 → 关 BT PCM;否则 fd>2 才关;0/1/2 一律不动(护 stdin)。 + * 返回 0=NONE / 1=BT / 2=FD。 */ +int bt_dev_close_kind(int bt_dev, int fd); /* 从 `bluealsa-cli info` 输出取实际协商 codec:行首 "Selected codec: AAC" → "AAC"。 * 返回 0=找到,-1=无该行/参数非法。 */ diff --git a/patch_v2/meow_audiod/meow_audiod.c b/patch_v2/meow_audiod/meow_audiod.c index 6d8d388..4580075 100644 --- a/patch_v2/meow_audiod/meow_audiod.c +++ b/patch_v2/meow_audiod/meow_audiod.c @@ -82,7 +82,6 @@ * @args = DEV PROFILE CODEC VOL SOFTVOL DELAY SRV)。DEV/CODEC 直接从 PCM 名内联传入, * 一次 snd_pcm_open 即完成「钉设备 + 钉 codec」,无需 DEV 自识别改写文件。 * ⚠️ /usr/data/alsa.conf 是 HiBy 播放器的数据文件、语法非法、libasound 从不加载——不读也不写。 */ -#define BT_PCM_BARE "bluealsa" /* 兜底:仅当内联名失败时试一次配置缺省(DEV 通配) */ #define BT_CODEC_AUTO "auto" #define BT_VOL_FULL_PCM "/usr/bin/bluealsa-cli" /* 一键设 AVRCP 100%(best-effort,不存在则跳过) */ @@ -976,39 +975,52 @@ static void bt_note_ok(void) g_bt_fail_n = 0; } -/* 找「现在要往哪台设备播」的 MAC。**主来源 = 内核 ACL(hcitool con)**: - * 这是「耳机真的连着」的权威,且开销极小;bluealsa 的 list-pcms 只在 profile 配置后的 - * 秒级窗口里才列出端点(M50 真机:无人开 PCM 时 1~3s 就消失),**不能当唯一来源** - * (否则探针一错过窗口就 bt=fail 回落)。list-pcms 仅作兜底,并顺手记下端点 D-Bus 路径 - * (volume/info 要用;PCM 被撑住后它才稳定,见 bt_refresh_endpoint)。 - * 返回 1=找到 MAC(写入 mac),0=当前没有可播的蓝牙设备。 */ -static int bt_find_mac(char *mac, size_t macsz) +/* 收集「可以试着往它播」的设备 MAC 候选(按可信度排序,去重): + * ① list-pcms 里**已经存在端点**的设备 —— 那就是真的 A2DP 连接(最可信,窗口内命中时用它); + * ② `hcitool con` 的**全部** ACL 设备 —— 内核权威,覆盖「profile 还没配置好、端点还没出现」。 + * ⚠️ 多条 ACL 是常态(手机/手柄/第二副耳机);只取第一条会挑错设备 → 耳机连着却回落 DAC + * (M50 r1 评审 P2-3)。候选交给 bt_try_open 逐个试开(上限 3 次尝试)。 + * 返回候选数(0=当前没有可播的蓝牙设备)。 */ +static int bt_collect_candidates(char (*cand)[18], int max) { - if (mac && macsz) mac[0] = 0; - char *o = popen_read_tmo(1200, "hcitool con 2>/dev/null"); - if (o && bt_dev_acl_mac(o, mac, macsz) == 0) { - logmsg("bt: ACL device %s (source=hcitool con)", mac); - return 1; + if (!cand || max <= 0) return 0; + int n = 0; + char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); + if (o && o[0]) { + char m[18]; + if (bt_dev_detect_mac(o, m, sizeof m) == 0) { + memcpy(cand[n++], m, 18); + logmsg("bt: candidate#%d %s (source=list-pcms endpoint)", n, m); + } } - o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); - if (!o) { logmsg("bt: no ACL and no bluealsa-cli"); return 0; } - char path[128]; - if (bt_dev_endpoint_path(o, path, sizeof path) == 0) snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path); - if (bt_dev_detect_mac(o, mac, macsz) == 0) { - logmsg("bt: a2dp dev %s (source=list-pcms fallback)", mac); - return 1; + o = popen_read_tmo(1200, "hcitool con 2>/dev/null"); + if (o) { + char macs[4][18]; + int k = bt_dev_acl_macs(o, macs, 4); + for (int i = 0; i < k && n < max; i++) { + int dup = 0; + for (int j = 0; j < n; j++) if (strcmp(cand[j], macs[i]) == 0) { dup = 1; break; } + if (dup) continue; + memcpy(cand[n++], macs[i], 18); + logmsg("bt: candidate#%d %s (source=hcitool con ACL)", n, macs[i]); + } } - return 0; + return n; } -/* PCM 已打开后刷新端点路径(此时 bluealsa 的端点对象是稳定的),供 info/volume 用。 */ +/* 用当前 g_bt_mac 精确刷新端点路径(PCM 已打开后 bluealsa 端点对象才稳定)。 + * **必须按 MAC 匹配**:换耳机 A→B 后若沿用 A 的路径,volume/info 会打到旧设备上 + * (M50 r1 评审 P2-2)。找不到本设备的端点就留空(宁可跳过 volume/info 也不串台)。 */ static void bt_refresh_endpoint(void) { - if (g_bt_pcm[0]) return; + g_bt_pcm[0] = 0; char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); if (!o) return; char path[128]; - if (bt_dev_endpoint_path(o, path, sizeof path) == 0) snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path); + if (bt_dev_pick_endpoint(o, g_bt_mac, path, sizeof path) == 0) + snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path); + else + logmsg("bt: endpoint for %s not listed yet (volume/info skipped)", g_bt_mac); } /* 按 PCM 名打开(libasound → NONBLOCK open + set_params S16/RW/ch/rate/soft_resample/100ms)。 @@ -1053,28 +1065,29 @@ static int bt_try_open(void) g_bt_fail_n, bt_dev_backoff_ms(g_bt_fail_n), since); return -1; } - char mac[20] = ""; - if (!bt_find_mac(mac, sizeof mac)) { bt_note_fail("no connected BT device (hcitool con empty)"); return -1; } - snprintf(g_bt_mac, sizeof g_bt_mac, "%s", mac); + char cand[3][18]; + int ncand = bt_collect_candidates(cand, 3); + if (ncand <= 0) { bt_note_fail("no BT device (no a2dp endpoint, no ACL)"); return -1; } bt_dev_codec_token(g_bt_codec, g_bt_tok, sizeof g_bt_tok); - char name[96]; - if (bt_dev_pcm_name(g_bt_mac, g_bt_tok, name, sizeof name) != 0) { bt_note_fail("bad device MAC"); return -1; } - logmsg("bt: try open %s (codec setting=%s -> token=%s)", name, g_bt_codec, g_bt_tok); - /* 3 次 × 300ms 短重试:ACL 刚建立时 BlueZ 可能还没把 A2DP profile 配置完, - * 这一下会返回 ENODEV/EBUSY;等 300ms 再来通常就通了。3 次仍失败才记账进退避 - * (短重试上限 600ms,不会在播放线程里形成长时间阻塞)。 */ + /* 3 次尝试、每次间隔 300ms(ACL 刚建立时 BlueZ 可能还没配置完 A2DP profile,等一拍就通)。 + * 多候选时轮流试:1 个候选→A,A,A(=r2 语义);2 个→A,B,A;3 个→A,B,C。 + * 候选都来自「真的连着」的集合,每次尝试都有意义;总耗时上限 ≈600ms + open 时延。 + * **不再用裸 "bluealsa" 兜底**:那会落到「最近连接」的设备上,多设备时等于挑错人。 */ const char *opened = NULL; for (int i = 0; i < 3 && !opened; i++) { - if (i) { - struct timespec ts = { 0, 300 * 1000000L }; - nanosleep(&ts, NULL); + const char *mac = cand[i % ncand]; + if (i) { struct timespec ts = { 0, 300 * 1000000L }; nanosleep(&ts, NULL); } + char name[96]; + if (bt_dev_pcm_name(mac, g_bt_tok, name, sizeof name) != 0) continue; + logmsg("bt: try#%d open %s (codec setting=%s -> token=%s)", i + 1, name, g_bt_codec, g_bt_tok); + if (bt_open_pcm(name) == 0) { + opened = name; + snprintf(g_bt_mac, sizeof g_bt_mac, "%s", mac); } - if (bt_open_pcm(name) == 0) opened = name; - else if (bt_open_pcm(BT_PCM_BARE) == 0) opened = BT_PCM_BARE; } if (!opened) { bt_note_fail("snd_pcm_open/set_params failed (3 tries)"); return -1; } bt_note_ok(); - bt_refresh_endpoint(); /* PCM 已撑住:端点对象稳定,取路径给 info/volume */ + bt_refresh_endpoint(); /* 端点对象此时稳定;按 MAC 精确取路径(换设备不串台) */ bt_read_used_codec(); g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 满量程 */ bt_vol_full(); @@ -1158,15 +1171,11 @@ static void bt_vol_full(void) { if (g_bt_vol_full_done) return; g_bt_vol_full_done = 1; + /* 只用 bt_refresh_endpoint() 按 MAC 精确取到的路径;为空就**跳过**—— + * 宁可这一次不顶音量,也不拿别的设备的端点发 volume(换耳机 A→B 串台,评审 P2-2)。 */ char pcm[128]; - snprintf(pcm, sizeof pcm, "%s", g_bt_pcm); /* bt_probe 已取到端点路径(省一次 popen) */ - if (!pcm[0]) { - char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); - if (!o) { logmsg("bt: avrcp set-max skipped (no bluealsa-cli)"); return; } - if (bt_dev_endpoint_path(o, pcm, sizeof pcm) != 0) { - logmsg("bt: avrcp set-max skipped (no a2dpsrc pcm found)"); return; - } - } + snprintf(pcm, sizeof pcm, "%s", g_bt_pcm); + if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no endpoint path for %s)", g_bt_mac); return; } char cmd[256]; snprintf(cmd, sizeof cmd, "%s volume %s 127 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm); char *r = popen_read_tmo(1500, cmd); @@ -1392,6 +1401,24 @@ static void dac_fallback_arm(void) apply_route(port); } +/* 把设备 fd 升到 ≥3:BT 打开成功时 dev_open() 返回 0,而 0/1/2 是 stdio——把设备 fd + * 钉在 0..2 上等于给「谁误清了 g_bt_dev」埋地雷(dev_close 的 close_kind 守卫会跳过 0..2)。 + * open() 只在 stdio 被关掉时才可能返回 <3;这里 dup 一次让「设备 fd 恒 >2」成为不变式。 + * dup 也失败(极端 EMFILE)时把原 fd 关掉并报失败——绝不留一个「在 stdio 区间又没人认领」 + * 的设备 fd(那会变成打开的设备泄漏,比关一次 stdin 更糟)。 */ +static int fd_high(int fd) +{ + if (fd < 0 || fd > 2) return fd; + int nfd = fcntl(fd, F_DUPFD, 3); + if (nfd < 0) { + logmsg("fd %d in stdio range and dup failed: %s -> treat as open failure", fd, strerror(errno)); + close(fd); + return -1; + } + close(fd); + return nfd; +} + static int dev_open(void) { /* 输出目标=BT 且走真机 ALSA:打开 libasound 的 bluealsa PCM(内联 DEV/CODEC,不开 hw:0,0)。 @@ -1411,7 +1438,7 @@ static int dev_open(void) #ifdef AUDIOD_HAVE_ALSA /* O_NONBLOCK:写路径走 poll(POLLOUT)+限时(dev_write),write 本身永不睡眠—— * 驱动 wedge 时绝不能把播放线程永久压进内核(真机长流卡死根因防线)。 */ - int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK); + int fd = fd_high(open(DEV_PCM, O_RDWR | O_NONBLOCK)); if (fd < 0) { logmsg("dev open %s: %s", DEV_PCM, strerror(errno)); return -1; } if (alsa_hw_params(fd, rate, ch, alsa_fmt_to_snd((int)fmt)) != 0) { logmsg("dev hw_params %u/%u/%s failed", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt)); @@ -1425,7 +1452,7 @@ static int dev_open(void) #endif } if (g_sink == SINK_FILE) { - int fd = open(g_out_wav, O_WRONLY | O_CREAT | O_TRUNC, 0644); + int fd = fd_high(open(g_out_wav, O_WRONLY | O_CREAT | O_TRUNC, 0644)); if (fd < 0) { logmsg("dev open wav %s: %s", g_out_wav, strerror(errno)); return -1; } wav_header(fd, (int)rate, (int)ch, bits, 0); return fd; @@ -1519,9 +1546,15 @@ static void dev_write(uint8_t *buf, size_t n) static void dev_close(void) { - /* 分流用 g_bt_dev(而非 g_bt_open):写路径判死时 bt_write 已 bt_close()(g_bt_open=0), - * 若此时按 g_bt_open 走 DAC 分支会把 g_dev_fd(=0) 当成真 fd 关掉(误关 stdin)。 */ - if (g_bt_dev) { bt_close(); g_bt_dev = 0; g_dev_fd = -1; g_dev_open = 0; return; } + /* 关设备的决策交给 bt_dev_close_kind()(纯函数,宿主可单测): + * - **BT 分支内自行清理**:认的是 g_bt_dev(不是 g_bt_open——写路径判死时 bt_write 已 + * bt_close(),g_bt_open 已是 0),进来后把 g_bt_dev/g_dev_fd 一起复位; + * - ⚠️ **调用方不得提前清 g_bt_dev**:BT 打开成功时 dev_open() 返回 0,谁提前清 0, + * 这里就会落到下面 close(g_dev_fd) 分支上误关 stdin(M50 r1 评审 P2-1 就是这么复现的); + * - 非 BT 分支只关 fd>2 的真 fd(0/1/2 一律不动,第二道保险)。 */ + int kind = bt_dev_close_kind(g_bt_dev, g_dev_fd); + if (kind == 1) { bt_close(); g_bt_dev = 0; g_dev_fd = -1; g_dev_open = 0; return; } + if (kind == 0) { 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) { @@ -1641,7 +1674,10 @@ static void *playback_thread(void *arg) pthread_mutex_lock(&g_mu); if (g_bt_fail_req) { g_bt_fail_req = 0; - g_bt_dev = 0; + /* **不要**在这里清 g_bt_dev:dev_close() 靠它认出「当前设备是 BT PCM」并走 + * BT 分支清理;提前清 0 会让它落到 close(g_dev_fd),而 BT 打开时 g_dev_fd==0 + * →误关 stdin(M50 r1 评审 P2-1)。g_dev_fd 同时置 -1 做第二道保险。 */ + g_dev_fd = -1; bt_note_fail("write path fatal (endpoint gone?)"); g_reconfig_pending = 1; pthread_cond_signal(&g_cond); diff --git a/patch_v2/meow_audiod/test_bt_dev.c b/patch_v2/meow_audiod/test_bt_dev.c index 0feeec5..242e30a 100644 --- a/patch_v2/meow_audiod/test_bt_dev.c +++ b/patch_v2/meow_audiod/test_bt_dev.c @@ -8,14 +8,16 @@ * 覆盖矩阵: * D1 bt_dev_detect_mac:真实 D-Bus 路径 → MAC;多端点/多设备取第一个 a2dpsrc; * hfphf/a2dpsnk 不算;简化 `a2dpsrc/` 兼容;空/无源端点/NULL/坏 dev_ 段 → -1。 - * D2 bt_dev_count_src:0/1/2 个源端点。 + * D3 bt_dev_codec_token:settings bt.codec → bluealsa canonical(4.1.1 别名表 strcasecmp); * auto/空/unchanged/垃圾 → unchanged。 * D4 bt_dev_pcm_name:内联 DEV/PROFILE/CODEC;坏 MAC/小缓冲 → -1。 * D5 bt_dev_backoff_ms / should_try:2s→60s 封顶 + 门控(防重试风暴)。 - * D6 bt_dev_endpoint_path:取整条 D-Bus 路径给 bluealsa-cli volume|info(真机行含 /sink)。 * D7 bt_dev_info_codec:真机 `bluealsa-cli info` 原文 → Selected codec(不能误吃 Available 行)。 - * D8 bt_dev_acl_mac:真机 `hcitool con` 原文 → ACL 对端 MAC(MAC 主来源;list-pcms 只在秒级窗口存在)。 + * D8 bt_dev_acl_macs:真机 `hcitool con` 原文 → **全部** ACL 对端 MAC(多设备一等公民: + * 只取第一条会在手机/手柄同时连着时挑错设备,导致有耳机却回落 DAC)。 + * D9 bt_dev_pick_endpoint:按 MAC 精确挑端点(换耳机 A→B 后不能拿 A 的路径发 volume/info)。 + * D10 bt_dev_close_kind:关闭决策(BT 分支 / 真 fd / **0-2 一律不动**=护 stdin,P2-1 回归)。 * * 编译(在 patch_v2/meow_audiod 下): * cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c @@ -95,24 +97,6 @@ int main(void) check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1i trailing token tolerated"); } - /* ==== D2 源端点计数(有无 A2DP 端点的快速判定) ==== */ - check(bt_dev_count_src("") == 0, "D2a empty -> 0"); - check(bt_dev_count_src( - "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsnk/sink\n") == 0, - "D2b only sink -> 0"); - check(bt_dev_count_src( - "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n") == 1, - "D2c one a2dpsrc -> 1"); - /* 行内判定回归:hfphf 行在 a2dpsrc 行之前,不能被算成源端点(整串 strstr 会误判) */ - check(bt_dev_count_src( - "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n" - "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n") == 1, - "D2e hfphf line before a2dpsrc -> count 1 (per-line match, not whole-buffer)"); - check(bt_dev_count_src( - "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/source\n" - "/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/source\n") == 2, - "D2d two a2dpsrc -> 2"); - /* ==== D3 settings bt.codec → bluealsa canonical token ==== */ { struct { const char *in; const char *want; const char *name; } t[] = { @@ -168,30 +152,6 @@ int main(void) check(bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, 16) == -1, "D4g tiny buffer -> -1"); } - /* ==== D6 端点完整路径(volume/info 要 D-Bus 路径,不是 MAC) ==== */ - { - char path[128] = ""; - int rc = bt_dev_endpoint_path( - "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n", path, sizeof path); - check(rc == 0 && strcmp(path, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0, - "D6a real /a2dpsrc/sink line -> full path (trailing newline trimmed)"); - } - { - char path[128] = ""; - int rc = bt_dev_endpoint_path( - "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n" - "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\r\n", path, sizeof path); - check(rc == 0 && strcmp(path, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0, - "D6b skips hfphf, strips CRLF (no newline leaks into popen cmd)"); - } - { - char path[128] = ""; - check(bt_dev_endpoint_path("/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsnk/sink\n", - path, sizeof path) == -1, "D6c no a2dpsrc -> -1"); - check(bt_dev_endpoint_path("", path, sizeof path) == -1, "D6d empty -> -1"); - check(bt_dev_endpoint_path(NULL, path, sizeof path) == -1, "D6e NULL -> -1"); - } - /* ==== D7 实际协商 codec(真机 info 原文:MOONDROP Pudding 只有 SBC/AAC,请求 LDAC 实际选 AAC) ==== */ { const char *info = @@ -226,36 +186,85 @@ int main(void) check(bt_dev_info_codec(NULL, codec, sizeof codec) == -1, "D7d NULL -> -1"); } - /* ==== D8 hcitool con → ACL MAC(真机原文;MAC 主来源) ==== */ + /* ==== D8 hcitool con → 全部 ACL MAC(真机原文;MAC 主来源,多设备是常态) ==== */ { const char *con = "Connections:\n" "\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n"; - char mac[32] = ""; - check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, - "D8a real hcitool con (state 1 AUTH ENCRYPT) -> ACL MAC"); + char m[4][18]; + check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0, + "D8a real hcitool con (state 1 AUTH ENCRYPT) -> 1 ACL MAC"); } { const char *con = "Connections:\n" "\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n"; - char mac[32] = ""; - check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, + char m[4][18]; + check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0, "D8b half-open ACL (state 5) still gives MAC (kernel is authority)"); } { - const char *con = "Connections:\n"; - char mac[32] = ""; - check(bt_dev_acl_mac(con, mac, sizeof mac) == -1 && mac[0] == 0, "D8c no ACL -> -1"); - check(bt_dev_acl_mac(NULL, mac, sizeof mac) == -1, "D8d NULL -> -1"); + /* 多设备:第二副耳机 + 手机同时连着 —— 两条都要拿到,且顺序=输出顺序 */ + const char *con = + "Connections:\n" + "\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n" + "\t< ACL 5C:FB:7C:11:22:33 handle 5 state 1 lm SLAVE \n"; + char m[4][18]; + int n = bt_dev_acl_macs(con, m, 4); + check(n == 2 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0 && strcmp(m[1], "5C:FB:7C:11:22:33") == 0, + "D8c two ACLs -> both MACs in order (not just the first one)"); } { - /* 小写输入归一为大写(bluealsa DEV 参数按大写比较) */ - const char *con = "\t< ACL aa:bb:cc:dd:ee:ff handle 3 state 1 lm SLAVE \n"; - char mac[32] = ""; - check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, - "D8e lowercase -> uppercase canonical"); + /* 三设备 + 上限裁剪 + 去重(同一设备多 profile 行重复出现) */ + const char *con = + "Connections:\n" + "\t< ACL AA:BB:CC:DD:EE:01 handle 1 state 1 lm MASTER \n" + "\t< ACL AA:BB:CC:DD:EE:01 handle 2 state 1 lm SLAVE \n" + "\t< ACL AA:BB:CC:DD:EE:02 handle 3 state 1 lm MASTER \n" + "\t< ACL AA:BB:CC:DD:EE:03 handle 4 state 1 lm MASTER \n"; + char m[3][18]; + int n = bt_dev_acl_macs(con, m, 3); + check(n == 3 && strcmp(m[0], "AA:BB:CC:DD:EE:01") == 0 && + strcmp(m[1], "AA:BB:CC:DD:EE:02") == 0 && strcmp(m[2], "AA:BB:CC:DD:EE:03") == 0, + "D8d dedup + cap at max (3 distinct)"); } + { + char m[4][18]; + check(bt_dev_acl_macs("Connections:\n", m, 4) == 0, "D8e no ACL -> 0"); + check(bt_dev_acl_macs(NULL, m, 4) == 0, "D8f NULL -> 0"); + check(bt_dev_acl_macs("\t< ACL aa:bb:cc:dd:ee:ff handle 3 state 1 lm SLAVE \n", m, 4) == 1 && + strcmp(m[0], "AA:BB:CC:DD:EE:FF") == 0, "D8g lowercase -> uppercase canonical"); + } + + /* ==== D9 按 MAC 精确挑端点(换耳机 A→B 不串台) ==== */ + { + const char *pcms = + "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink\n" + "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink\n"; + char path[128] = ""; + check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:02", path, sizeof path) == 0 && + strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink") == 0, + "D9a picks B's path when MAC=B (not the first endpoint)"); + check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:01", path, sizeof path) == 0 && + strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink") == 0, + "D9b picks A's path when MAC=A"); + check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:03", path, sizeof path) == -1 && + path[0] == 0, "D9c unknown MAC -> -1 (never borrow another device's path)"); + check(bt_dev_pick_endpoint("/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/hfphf/source\n", + "AA:BB:CC:DD:EE:01", path, sizeof path) == -1, + "D9d non-a2dpsrc endpoint -> -1"); + check(bt_dev_pick_endpoint(NULL, "AA:BB:CC:DD:EE:01", path, sizeof path) == -1, "D9e NULL -> -1"); + } + + /* ==== D10 关闭决策(P2-1 回归:0/1/2 永不当设备 fd 关) ==== */ + check(bt_dev_close_kind(1, 0) == 1, "D10a bt_dev=1 + fd=0 -> close BT only (never fd 0)"); + check(bt_dev_close_kind(1, 5) == 1, "D10b bt_dev=1 + fd=5 -> close BT only"); + check(bt_dev_close_kind(0, 0) == 0, "D10c bt_dev=0 + fd=0 -> NONE (stdin guard, P2-1)"); + check(bt_dev_close_kind(0, 1) == 0, "D10d bt_dev=0 + fd=1 -> NONE (stdout guard)"); + check(bt_dev_close_kind(0, 2) == 0, "D10e bt_dev=0 + fd=2 -> NONE (stderr guard)"); + check(bt_dev_close_kind(0, 3) == 2, "D10f bt_dev=0 + fd=3 -> close FD"); + check(bt_dev_close_kind(0, -1) == 0, "D10g bt_dev=0 + fd=-1 -> NONE"); + check(bt_dev_close_kind(0, -2) == 0, "D10h bt_dev=0 + fd=-2 (NULL sink) -> NONE"); /* ==== D5 回落退避(防重试风暴:M10 教训) ==== */ check(bt_dev_backoff_ms(0) == 0, "D5a fails=0 -> 0ms (immediate)"); From 234b4d13b95bb6c9bc3f102c64e8b550aa9b4b5e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 19:38:32 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(audiod):=20build=5Faudiod.sh=20?= =?UTF-8?q?=E8=A7=A3=20rebase=20=E5=86=B2=E7=AA=81=E5=90=8E=E8=A1=A5?= =?UTF-8?q?=E4=B8=BB=20checkout=20=E5=9B=9E=E9=80=80=EF=BC=88worktree=20?= =?UTF-8?q?=E9=87=8C=E4=B9=9F=E8=83=BD=E6=89=BE=E5=88=B0=E5=9B=BA=E4=BB=B6?= =?UTF-8?q?=E6=A0=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rebase 到 main(ca042aa,M43 已合入)时 build_audiod.sh 冲突,解决原则:M43 的 cli 目标 time32 接入与 M50 的路径环境变量化**两者都保留**,守护目标仍不链 time32: cli : "$CC" $INC ... -static -DMEOW_TIME32_LIBC_OVERRIDES meow_audio.c ../sdk/meow_time32.c audiod : "$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 ... meow_audiod.c bt_dev.c -lpthread -L"$LIBBT" -lasound (M43 的守护例外注释按 main 的新措辞保留;LIBBT/ASOUND_SRC 环境变量覆盖与 host 链接兜底保留。) 解冲突时发现一个实际问题:`devdocs/fw-baseline/...` 与 `fw_apps/build/...` 这两条 ASOUND_SRC 候选**都没有入库**,在 git worktree 里运行时 REPO=worktree 根,两条都不存在 → 守护目标 直接 exit 1(本仓库正是按 worktree 并行开发的)。补一条回退:用 `git rev-parse --git-common-dir` 的父目录找到**主 checkout**(worktree 下即主仓根), 把主 checkout 里的 devdocs/ 与 fw_apps/ 也列为候选;普通 checkout 下 MAIN==REPO,行为不变。 自证(解完冲突、真实跑本脚本):meow_audiod 68604B/md5 9f58b219…(glibc-dynamic, NEEDED=libasound.so.2/libc.so.6/libpthread.so.0,interpreter /lib/ld.so.1); meow_audio 88888B/md5 3909c8d2…(static,含 S24/S32,4263=3/4403=0/16B-layout=OK); test_time32_abi.sh --builds ALL PASS(build meow_audio src=yes macro=yes)。 --- patch_v2/meow_audiod/build_audiod.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/patch_v2/meow_audiod/build_audiod.sh b/patch_v2/meow_audiod/build_audiod.sh index 3f96e46..04dc58c 100755 --- a/patch_v2/meow_audiod/build_audiod.sh +++ b/patch_v2/meow_audiod/build_audiod.sh @@ -84,11 +84,16 @@ fi # 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件树自拷贝(idempotent)。 # M50:LIBBT / ASOUND_SRC 可被环境变量覆盖;ASOUND_SRC 候选按「显式 → 历史解包树 → 仓内固件基线」 # 顺序取第一个存在的(旧值 $ROOT/hiby_os_crack/... 在多数 checkout 里不存在,set -e 下会直接卡住构建)。 +# MAIN = 主 checkout 根(在 git worktree 里运行时,devdocs/ 与 fw_apps/ 这类未入库的固件树 +# 只在主 checkout 里;git-common-dir 指向
/.git,其父目录即主 checkout)。普通 checkout 下 == REPO。 +MAIN="$(cd "$(git rev-parse --git-common-dir 2>/dev/null || echo .)/.." 2>/dev/null && pwd || true)" LIBBT="${LIBBT:-libbt}" if [ -z "$ASOUND_SRC" ]; then for c in "$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0" \ "$REPO/devdocs/fw-baseline/squashfs-root/usr/lib/libasound.so.2.0.0" \ - "$REPO/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0"; do + "$REPO/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0" \ + ${MAIN:+"$MAIN/devdocs/fw-baseline/squashfs-root/usr/lib/libasound.so.2.0.0"} \ + ${MAIN:+"$MAIN/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0"}; do [ -f "$c" ] && { ASOUND_SRC="$c"; break; } done fi From d36a3a02a677d6fe26db4469a78fa6c3a1e64f2e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 20:05:49 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(audiod):=20build=5Faudiod.sh=20?= =?UTF-8?q?=E8=AF=84=E5=AE=A1=20P2=20=E4=B8=A4=E6=9D=A1=20=E2=80=94=20CC?= =?UTF-8?q?=20=E5=8D=95=E4=B8=80=E5=91=BD=E4=BB=A4=E8=AF=8D=20+=20ZIG=20gu?= =?UTF-8?q?ard=20=E4=B8=8B=E6=B2=89=EF=BC=9B=E9=A1=BA=E6=89=8B=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=E5=8F=AF=E8=A7=81=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审 r2(p2-2items/merge,reviewed 234b4d1)两条: ① P2-1 `CC="zig cc --target=mipsel-linux-musleabi"` 是死代码:多词整串存进变量,`"$CC"` 按一个命令名展开 → `command not found`(评审实测 exit 127)。**M51 在 git worktree 里跑 必踩**(`$ROOT/.tools/zig-mipsel-cc` 基本不存在 → 走 PATH 回退 → meow_audio 编不出来, 而 meow_audio 正是 M43 带 time32 的那个 CLI)。 - `CC` 只存**单个命令词**,多词 target flags 另放 `CC_EXTRA`(未加引号展开,故意分词); - PATH 回退改为 `CC="$(command -v zig)"` + `CC_EXTRA="cc --target=mipsel-linux-musleabi"`; - cli 分支加 `[ -n "$CC" ] || { 明确提示 CC=/ZIG= 或把 zig 放进 PATH; exit 1; }`—— 缺编译器时是**清晰错误**,不再是 command not found。 ② P2-2 `[ -n "$ZIG" ] || exit 1` 放得太早:`$ZIG` 只有 audiod 目标用,却在 host/cli 分支之前, 没有 zig 的机器上 `build_audiod.sh host`(只需宿主 cc)会被无谓打断。guard 下沉到 audiod 段(MAIN 之后、LIBBT 之前),提示语也改成「audiod 目标需要它」。 顺带(评审附带观察): ③ host 目标不再用「先编一次 2>/dev/null、失败再编一次」兜底——那会把成功路径的 warning 吞掉。 改成先跑一个 trivial 链接探针决定 `AS_LINK`(`-l:libasound.so.2` 或 `-lasound`),真正的编译 全程保留 stderr。 ④ `libasound.so.2.0.0 not found` 现在**列出试过的全部路径** + 覆盖语法(M51 在陌生环境里的 唯一路标)。同时把 MAIN 的推导改成「git 不可用就留空」,避免产生 `/`、`$HOME` 这类伪候选。 自证(本 worktree 真跑): - `ROOT=/nonexistent ./build_audiod.sh cli` → 正常出件 88888B/md5 3909c8d2…(评审的复现命令) - PATH 里没有 zig → cli:`!! mipsel CC 未找到:设置 CC=…`(不再 command not found); audiod:`!! zig not found(audiod 目标需要它)…` - PATH 里没有 zig → **host 目标照常 4 个产物 + test_bt_dev 71/71 + test_audiod_host 109/0** - 正常环境两目标产物**一字未变**:meow_audiod 68604B/md5 9f58b219…、meow_audio 88888B/md5 3909c8d2… - `test_time32_abi.sh --builds` ALL PASS,`build meow_audio src=yes macro=yes` --- patch_v2/meow_audiod/build_audiod.sh | 44 +++++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/patch_v2/meow_audiod/build_audiod.sh b/patch_v2/meow_audiod/build_audiod.sh index 04dc58c..a7b92c2 100755 --- a/patch_v2/meow_audiod/build_audiod.sh +++ b/patch_v2/meow_audiod/build_audiod.sh @@ -29,28 +29,38 @@ cd "$(dirname "$0")" # ROOT 只用于可选的历史路径(旧 .tools/zig-env、hiby_os_crack 解包树);缺省布局下不依赖它。 ROOT="${ROOT:-$HOME/Project/R3ProII}" REPO="$(cd "$(dirname "$0")/../.." && pwd)" # 仓库根(worktree 亦可) -# mipsel 静态 CC:优先显式环境变量 → 历史 zig 包装脚本 → PATH 里的 zig。 +# mipsel 静态 CC:显式 CC → 历史 zig 包装脚本 → PATH 里的 zig(裸 zig 要另带 target flags)。 +# ⚠️ CC **只存单个命令词**:`"$CC"` 是按一个命令名展开的,把 "zig cc --target=..." 整串存进去 +# 会变成「找不到名叫 zig cc --target=... 的命令」(评审 P2-1,M51 在 worktree 里必踩)。 +# 多词 flags 走 CC_EXTRA(未加引号展开,故意按空白分词),与下面 ZIG 的写法保持一致。 CC="${CC:-}" +CC_EXTRA="" if [ -z "$CC" ]; then - if [ -x "$ROOT/.tools/zig-mipsel-cc" ]; then CC="$ROOT/.tools/zig-mipsel-cc"; else CC="zig cc --target=mipsel-linux-musleabi"; fi + if [ -x "$ROOT/.tools/zig-mipsel-cc" ]; then + CC="$ROOT/.tools/zig-mipsel-cc" # 历史包装脚本(内部已带 --target) + elif command -v zig >/dev/null 2>&1; then + CC="$(command -v zig)" + CC_EXTRA="cc --target=mipsel-linux-musleabi" + fi fi # 交叉编译用的 zig:显式 ZIG → 历史 zig-env → PATH。 ZIG="${ZIG:-}" if [ -z "$ZIG" ]; then if [ -x "$ROOT/.tools/zig-env/bin/zig" ]; then ZIG="$ROOT/.tools/zig-env/bin/zig"; else ZIG="$(command -v zig || true)"; fi fi -[ -n "$ZIG" ] || { echo "!! zig not found (set ZIG=/path/to/zig)"; exit 1; } INC="-I$REPO/patch_v2/meow_launcherd" # 共享协议头(相对仓库根,不再绑 ROOT) +# 注:$ZIG 只有 audiod 目标用;guard 放在 audiod 段之前(见下),host/cli 不因缺 zig 而中断。 if [ "$1" = "host" ]; then HOST_CC="${CC_HOST:-cc}" # BT 目标直链宿主 libasound(无 asoundlib.h,原型内联于 meow_audiod.c)。删 -ldl:BT 不再用 dlopen。 - # 优先 -l:libasound.so.2(精确到版本,不依赖 unversioned dev 链接);宿主只装了无版本号 - # libasound.so(如 Termux)时退回 -lasound——两者符号集对我们用的 9 个原型等价。 - if ! "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host \ - -lpthread -l:libasound.so.2 2>/dev/null; then - "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread -lasound + # 链接名先用「trivial 探针」决定:有 -l:libasound.so.2(精确版本)就用它,否则退 -lasound。 + # 用探针而**不是**「先编一次、失败再编一次 2>/dev/null」——后者会把成功路径的 warning 一起吞掉。 + AS_LINK="-l:libasound.so.2" + if ! printf 'int main(void){return 0;}\n' | "$HOST_CC" -x c - -o /dev/null "$AS_LINK" 2>/dev/null; then + AS_LINK="-lasound" fi + "$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread $AS_LINK "$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm "$HOST_CC" $INC -Os -Wall -Wextra test_audiod_host.c -o test_audiod_host -lm "$HOST_CC" -Os -Wall -Wextra test_bt_dev.c bt_dev.c -o test_bt_dev -lm @@ -65,9 +75,12 @@ if [ "$1" = "host" ]; then fi if [ "$1" = "cli" ]; then + [ -n "$CC" ] || { + echo "!! mipsel CC 未找到:设置 CC=/path/to/zig 或 CC=/path/to/zig-mipsel-cc(或把 zig 放进 PATH)" >&2 + exit 1; } # M43: meow_audio 是 musl-static(当前 zig 链),与其它系统组件同源受损 → 链 time32 修 # clock_gettime/usleep(monitor 的 100ms 窗口/轮询)。 - "$CC" $INC -Os -Wall -Wextra -static -DMEOW_TIME32_LIBC_OVERRIDES \ + "$CC" $CC_EXTRA $INC -Os -Wall -Wextra -static -DMEOW_TIME32_LIBC_OVERRIDES \ meow_audio.c ../sdk/meow_time32.c -o meow_audio -lm echo "== meow_audio (mipsel static CLI) ==" ls -la meow_audio @@ -86,19 +99,28 @@ fi # 顺序取第一个存在的(旧值 $ROOT/hiby_os_crack/... 在多数 checkout 里不存在,set -e 下会直接卡住构建)。 # MAIN = 主 checkout 根(在 git worktree 里运行时,devdocs/ 与 fw_apps/ 这类未入库的固件树 # 只在主 checkout 里;git-common-dir 指向
/.git,其父目录即主 checkout)。普通 checkout 下 == REPO。 -MAIN="$(cd "$(git rev-parse --git-common-dir 2>/dev/null || echo .)/.." 2>/dev/null && pwd || true)" +MAIN="" +_gcd="$(git rev-parse --git-common-dir 2>/dev/null)" && MAIN="$(cd "$_gcd/.." 2>/dev/null && pwd)" || MAIN="" +[ -n "$ZIG" ] || { + echo "!! zig not found(audiod 目标需要它):设置 ZIG=/path/to/zig 或把 zig 放进 PATH" >&2 + exit 1; } LIBBT="${LIBBT:-libbt}" if [ -z "$ASOUND_SRC" ]; then + tried="" for c in "$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0" \ "$REPO/devdocs/fw-baseline/squashfs-root/usr/lib/libasound.so.2.0.0" \ "$REPO/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0" \ ${MAIN:+"$MAIN/devdocs/fw-baseline/squashfs-root/usr/lib/libasound.so.2.0.0"} \ ${MAIN:+"$MAIN/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0"}; do [ -f "$c" ] && { ASOUND_SRC="$c"; break; } + tried="$tried + - $c" done fi [ -n "$ASOUND_SRC" ] && [ -f "$ASOUND_SRC" ] || { - echo "!! libasound.so.2.0.0 not found (set ASOUND_SRC=/path/to/libasound.so.2.0.0)"; exit 1; } + echo "!! libasound.so.2.0.0 not found. 试过这些路径:$tried" >&2 + echo " 覆盖方式:ASOUND_SRC=/path/to/libasound.so.2.0.0 $0" >&2 + exit 1; } mkdir -p "$LIBBT" if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so"