fix(audiod): M50 r2 蓝牙打开时序 — MAC 改从 hcitool con 取(list-pcms 易失)+ 短重试 + 行内匹配修正
真机(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=<mac>,...") 让 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 <path>\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 主路径未能在真机复验
(耳机不可达),详见报告。
This commit is contained in:
1 parent
33bef36d16
commit
6e7c0d00c0
4 files changed
+321
-57
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user