feat(bt): 蓝牙P2 source实装 — audiod OUT_BT输出目标(libasound直连pcm.bluealsa,BT独立目标不动DAC独占/FPGA/音量反转)+音量AVRCP隔离(不下发DAC反转)+codec生效+MSG_AUDIO_OUTPUT/CLI;宿主67pass/0warning;sink标记P2.5;遗留mipsel静态dlopen不支持libasound(硬集成项)

This commit is contained in:
AskaEth committed 2026-09-03 20:52:06 +08:00
1 parent 1f6d390e1c
commit 71b2dafecc
5 files changed
+587 -42

No files matched your search

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