fix(audiod): M50 蓝牙输出端到端 — 内联 DEV/CODEC 打开 + 失败必回落 DAC + MAC 解析修正
① ALSA 正确用法(核心):不再写 /usr/data/alsa.conf(HiBy 播放器数据文件、语法非法、
libasound 从不加载,改它对解析零影响)。改为 snd_pcm_open("bluealsa:DEV=<mac>,
PROFILE=a2dp,CODEC=<token>"),参数名严格取 /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/<MAC>。
③ 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 统一重编)。
This commit is contained in:
1 parent
ca042aa79c
commit
33bef36d16
6 files changed
+605
-339
No files matched your search
+218
-147
@@ -59,7 +59,7 @@
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include "meow_ipc.h"
|
||||
#include "bt_dev.h" /* P2:DEV 自识别(a2dpsrc 端点 → MAC → alsa.conf 幂等改写) */
|
||||
#include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sound/asound.h>
|
||||
@@ -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 "<g_bt_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 后补一次武装 */
|
||||
|
||||
Reference in new issue
Block a user