From 6e7c0d00c021f6fbc0a1d0c1b2c4aaf3ece86767 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 18:42:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(audiod):=20M50=20r2=20=E8=93=9D=E7=89=99?= =?UTF-8?q?=E6=89=93=E5=BC=80=E6=97=B6=E5=BA=8F=20=E2=80=94=20MAC=20?= =?UTF-8?q?=E6=94=B9=E4=BB=8E=20hcitool=20con=20=E5=8F=96=EF=BC=88list-pcm?= =?UTF-8?q?s=20=E6=98=93=E5=A4=B1=EF=BC=89+=20=E7=9F=AD=E9=87=8D=E8=AF=95?= =?UTF-8?q?=20+=20=E8=A1=8C=E5=86=85=E5=8C=B9=E9=85=8D=E4=BF=AE=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");