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:
1 parent
1f6d390e1c
commit
71b2dafecc
5 files changed
+587
-42
No files matched your search
@@ -16,7 +16,10 @@ INC="-I$ROOT/patch_v2/meow_launcherd"
|
|||||||
|
|
||||||
if [ "$1" = "host" ]; then
|
if [ "$1" = "host" ]; then
|
||||||
HOST_CC="${CC_HOST:-cc}"
|
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
|
"$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm
|
||||||
echo "== host builds =="
|
echo "== host builds =="
|
||||||
ls -la meow_audiod_host meow_audio_host
|
ls -la meow_audiod_host meow_audio_host
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* meow_audio tone [freq] [ms] [rate] 正弦测试音(默认 440Hz 500ms 44100;排查无声不须开 App)
|
* 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 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 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 刷新)
|
* meow_audio monitor [秒] 终端实时电平表(rms/peak dBFS + 文本电平条,200ms 刷新)
|
||||||
*
|
*
|
||||||
* 传输:AF_UNIX SOCK_STREAM 到 audiod socket(env MEOW_AUDIO_SOCK / --sock 覆盖,宿主/qemu)。
|
* 传输:AF_UNIX SOCK_STREAM 到 audiod socket(env MEOW_AUDIO_SOCK / --sock 覆盖,宿主/qemu)。
|
||||||
@@ -273,6 +274,18 @@ static int cmd_vol(int pct)
|
|||||||
return 0;
|
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,帧对齐)。
|
/* 正弦生成:填 nframes 帧(grant 交织格式)到 buf(上限 bufcap,帧对齐)。
|
||||||
* start 为起始帧号;返回实际写入字节数。 */
|
* start 为起始帧号;返回实际写入字节数。 */
|
||||||
static size_t gen_sine(uint8_t *buf, size_t bufcap, int grant_fmt, int grant_ch,
|
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)
|
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"
|
" monitor: 终端实时电平表(200ms 刷新+文本电平条;缺省 10s,0=持续到 Ctrl-C)\n"
|
||||||
" env: MEOW_AUDIO_SOCK (socket 缺省 %s)\n", argv0, AUD_SOCK_DEFAULT);
|
" 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;
|
int pct = (argc >= 3) ? atoi(argv[2]) : -1;
|
||||||
if (pct > 100) pct = 100;
|
if (pct > 100) pct = 100;
|
||||||
int rc = cmd_vol(pct); close(g_fd); return rc;
|
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")) {
|
} else if (!strcmp(cmd, "tone")) {
|
||||||
int freq = (argc >= 3) ? atoi(argv[2]) : 440;
|
int freq = (argc >= 3) ? atoi(argv[2]) : 440;
|
||||||
int ms = (argc >= 4) ? atoi(argv[3]) : 500;
|
int ms = (argc >= 4) ? atoi(argv[3]) : 500;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
#include "meow_ipc.h"
|
#include "meow_ipc.h"
|
||||||
|
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
@@ -74,6 +75,13 @@
|
|||||||
#define SETTINGS_CONF "/usr/data/meow/settings.conf"
|
#define SETTINGS_CONF "/usr/data/meow/settings.conf"
|
||||||
#define DEFAULT_OUT_WAV "/tmp/meow_audiod.wav"
|
#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 CLI_MAX 8 /* 同连客户端上限(防 fd 泄漏) */
|
||||||
#define READ_BUF_MAX (MEOW_IPC_MAX + MEOW_IPC_HDR_LEN)
|
#define READ_BUF_MAX (MEOW_IPC_MAX + MEOW_IPC_HDR_LEN)
|
||||||
#define FRAME_READ_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_fmt = AUDIO_FMT_S16; /* 硬件最优格式(ALSA 探测;FILE/NULL=S16) */
|
||||||
static int g_hw_probed = 0;
|
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_sock[108] = AUD_SOCK_DEFAULT;
|
||||||
static char g_out_wav[256] = DEFAULT_OUT_WAV;
|
static char g_out_wav[256] = DEFAULT_OUT_WAV;
|
||||||
static const char *g_amixer = "/usr/bin/amixer"; /* 环境可覆盖 */
|
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_manual = -1; /* -1=auto;2=3.5;3=4.4 */
|
||||||
static int g_route_cur = -1; /* 最近已知路由;-1=未知 */
|
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 测试) */
|
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 uint32_t g_dev_rate = 0, g_dev_ch = 0, g_dev_fmt = 0;
|
||||||
static int g_reconfig_pending = 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 样本) ----
|
/* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ----
|
||||||
* 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16),
|
* 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16),
|
||||||
* ~100ms 窗口(rate/10 帧)到点才结算一次 RMS/peak 并发布快照——每帧仅几次整数加乘,
|
* ~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); }
|
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) */
|
/* 整数开平方(uint64→uint32,逐位法;替代软浮点 sqrt) */
|
||||||
static uint32_t isqrt64(uint64_t x)
|
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 同源同键) ---- */
|
/* ---- settings.conf 写 audio.volume(同 launcher 同源同键) ---- */
|
||||||
static void settings_set_volume(int pct)
|
static void settings_set_volume(int pct)
|
||||||
{
|
{
|
||||||
FILE *f = fopen(SETTINGS_CONF, "r");
|
FILE *f = fopen(g_settings_path, "r");
|
||||||
char *data = NULL;
|
char *data = NULL;
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
if (f) {
|
if (f) {
|
||||||
@@ -687,7 +730,7 @@ static void settings_set_volume(int pct)
|
|||||||
if (n > 0) o += (size_t)n;
|
if (n > 0) o += (size_t)n;
|
||||||
}
|
}
|
||||||
mkdir("/usr/data/meow", 0755);
|
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) {
|
if (fd >= 0) {
|
||||||
/* 若原文件带 "# meow-settings 1" 头则保留——我们已被动保留了内容(含头)。
|
/* 若原文件带 "# meow-settings 1" 头则保留——我们已被动保留了内容(含头)。
|
||||||
* 无头(新文件)补一行头,符合 meow_settings 的解析预期。 */
|
* 无头(新文件)补一行头,符合 meow_settings 的解析预期。 */
|
||||||
@@ -704,12 +747,65 @@ static void settings_set_volume(int pct)
|
|||||||
close(fd);
|
close(fd);
|
||||||
}
|
}
|
||||||
free(out);
|
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)
|
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;
|
if (!f) return g_vol_pct;
|
||||||
char line[128];
|
char line[128];
|
||||||
int v = g_vol_pct;
|
int v = g_vol_pct;
|
||||||
@@ -725,6 +821,273 @@ static int settings_get_volume(void)
|
|||||||
return v;
|
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 ---- */
|
/* ---- ALSA UAPI ---- */
|
||||||
#ifdef AUDIOD_HAVE_ALSA
|
#ifdef AUDIOD_HAVE_ALSA
|
||||||
static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); }
|
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)
|
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;
|
uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt;
|
||||||
int bits = fmt_bps((int)fmt) * 8;
|
int bits = fmt_bps((int)fmt) * 8;
|
||||||
if (g_sink == SINK_ALSA) {
|
if (g_sink == SINK_ALSA) {
|
||||||
@@ -885,6 +1257,7 @@ static void dev_write(uint8_t *buf, size_t n)
|
|||||||
}
|
}
|
||||||
g_dev_bytes += 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_NULL) { g_played_frames += n / (size_t)frame_size(); return; }
|
||||||
if (g_sink == SINK_ALSA) {
|
if (g_sink == SINK_ALSA) {
|
||||||
#ifdef AUDIOD_HAVE_ALSA
|
#ifdef AUDIOD_HAVE_ALSA
|
||||||
@@ -950,6 +1323,7 @@ static void dev_write(uint8_t *buf, size_t n)
|
|||||||
|
|
||||||
static void dev_close(void)
|
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) {
|
if (g_sink == SINK_FILE && g_dev_fd >= 0 && g_dev_fd != -2) {
|
||||||
off_t end = lseek(g_dev_fd, 0, SEEK_END);
|
off_t end = lseek(g_dev_fd, 0, SEEK_END);
|
||||||
if (end > 44) {
|
if (end > 44) {
|
||||||
@@ -971,17 +1345,20 @@ static void dev_close(void)
|
|||||||
static void reconfigure_locked(void)
|
static void reconfigure_locked(void)
|
||||||
{
|
{
|
||||||
uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt;
|
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;
|
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();
|
dev_close();
|
||||||
g_dev_fd = dev_open();
|
g_dev_fd = dev_open();
|
||||||
if (g_dev_fd >= 0 || (g_sink == SINK_NULL && g_dev_fd == -2)) {
|
if (g_dev_fd >= 0 || (g_sink == SINK_NULL && g_dev_fd == -2)) {
|
||||||
g_dev_open = 1;
|
g_dev_open = 1;
|
||||||
g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = fmt;
|
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_dev_fd == -2) g_dev_fd = -1; /* NULL sink:无真实 fd */
|
||||||
if (g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原 */
|
if (g_target == AUDIO_OUT_DAC && 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);
|
logmsg("device open ok: %u/%u/%s sink=%d target=%s", (unsigned)rate, (unsigned)ch,
|
||||||
|
fmt_name((int)fmt), g_sink, tgt);
|
||||||
} else {
|
} else {
|
||||||
logmsg("device open FAILED !!");
|
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);
|
logmsg("open rejected: ch=%u fmt=%d rate=%u", (unsigned)o.ch, o.fmt, (unsigned)o.rate);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hw_probe();
|
/* 输出目标=BT:不依赖 DAC hw 探测(BT 走 pcm.bluealsa,与 hw:0,0/FGA numid=9 解耦)——
|
||||||
if (g_sink == SINK_ALSA && !g_hw_probed) {
|
* 探测只对 DAC 有意义;BT 打开失败在播放线程降级丢帧(plan §五自感知兜底)。 */
|
||||||
m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_NODEV;
|
if (g_target == AUDIO_OUT_DAC) {
|
||||||
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
|
hw_probe();
|
||||||
logmsg("open rejected: no usable PCM device");
|
if (g_sink == SINK_ALSA && !g_hw_probed) {
|
||||||
return;
|
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++;
|
uint32_t sid = g_next_sid++;
|
||||||
/* 抢占:已有当前流 → 旧客户端收 PREEMPTED */
|
/* 抢占:已有当前流 → 旧客户端收 PREEMPTED */
|
||||||
if (g_st.active) {
|
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.grant_fmt = g_st.fmt;
|
||||||
a.err = MEOW_OK;
|
a.err = MEOW_OK;
|
||||||
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case MSG_AUDIO_WRITE: {
|
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,
|
snprintf(txt, sizeof txt,
|
||||||
"active=%d app=%s sid=%u rate=%u ch=%u fmt=%s "
|
"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 "
|
"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 ? 1 : 0,
|
||||||
g_st.active ? g_st.app_id : "-",
|
g_st.active ? g_st.app_id : "-",
|
||||||
(unsigned)g_st.sid, (unsigned)g_st.rate, (unsigned)g_st.ch,
|
(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,
|
(unsigned)g_stall, (unsigned)g_txdrop, g_paused,
|
||||||
g_route_manual == -1 ? "auto" : "manual",
|
g_route_manual == -1 ? "auto" : "manual",
|
||||||
(g_route_manual != -1 ? g_route_manual : rb),
|
(g_route_manual != -1 ? g_route_manual : rb),
|
||||||
vol,
|
vol, volctl_name(),
|
||||||
g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq);
|
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);
|
send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1257,10 +1642,8 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
|||||||
int32_t pct = -1;
|
int32_t pct = -1;
|
||||||
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
|
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
|
||||||
if (pct >= 0) {
|
if (pct >= 0) {
|
||||||
if (pct > 100) pct = 100;
|
/* 音量应用含 BT 隔离:target=BT 时绝不下发 DAC 反转(vol_apply 内判断) */
|
||||||
g_vol_pct = (int)pct;
|
vol_apply((int)pct);
|
||||||
dac_vol_set((int)pct);
|
|
||||||
settings_set_volume((int)pct);
|
|
||||||
} else {
|
} else {
|
||||||
g_vol_pct = settings_get_volume();
|
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);
|
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1411,22 +1802,35 @@ static void *route_thread(void *arg)
|
|||||||
* 启动 PCM 会在第二次 hbc3000_start 死锁整机。写 Output Port Switch 是烧录触发器
|
* 启动 PCM 会在第二次 hbc3000_start 死锁整机。写 Output Port Switch 是烧录触发器
|
||||||
* (驱动 put:set_spdif_mode→program_ram→enabled)。开机端口恒 0=未烧,故启动时
|
* (驱动 put:set_spdif_mode→program_ram→enabled)。开机端口恒 0=未烧,故启动时
|
||||||
* 无条件写一次:4.4 在→3,3.5 在→2,都没→4(官方播放器拔插孔后的选择,保 FPGA 已烧)。
|
* 无条件写一次:4.4 在→3,3.5 在→2,都没→4(官方播放器拔插孔后的选择,保 FPGA 已烧)。
|
||||||
* 端口已是目标值(audiod 重启场景,驱动同值短路不写)→ 先写 5(disable) 强制重烧。 */
|
* 端口已是目标值(audiod 重启场景,驱动同值短路不写)→ 先写 5(disable) 强制重烧。
|
||||||
{
|
* ⚠️ P2 蓝牙:numnid=9/FPGA 武装只作用于 **DAC 物理路径**(plan §五解耦);target=BT 时
|
||||||
int hb = read_switch("/sys/class/switch/headset/state");
|
* 不开 hw:0,0,无 hbc3000 start 死锁风险,故 BT 期间跳过 numid=9/FPGA(切回 DAC 再武装)。 */
|
||||||
int bal = read_switch("/sys/class/switch/balance/state");
|
int need_arm = 1; /* 启动(DAC 起点)武装一次;BT 期间置 1(回 DAC 再武装) */
|
||||||
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));
|
|
||||||
}
|
|
||||||
while (!g_stop) {
|
while (!g_stop) {
|
||||||
pthread_mutex_lock(&g_mu);
|
pthread_mutex_lock(&g_mu);
|
||||||
int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */
|
int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */
|
||||||
|
int tgt = g_target; /* 输出目标快照(决定是否武装 FPGA) */
|
||||||
pthread_mutex_unlock(&g_mu);
|
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 */
|
int naps = 5; /* 1000ms / 200ms 分段,即时响应 SIGTERM */
|
||||||
for (int k = 0; k < naps && !g_stop; k++) {
|
for (int k = 0; k < naps && !g_stop; k++) {
|
||||||
struct timespec ts = { 0, 200 * 1000000L };
|
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")) {
|
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
||||||
printf("meow_audiod [--sock <path>] [--out <wav>]\n"
|
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"
|
" 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_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;
|
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_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_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_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");
|
const char *sink = getenv("MEOW_AUDIO_SINK");
|
||||||
if (sink && sink[0]) {
|
if (sink && sink[0]) {
|
||||||
if (!strcmp(sink, "file")) g_sink = SINK_FILE;
|
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_sock, g_sink, g_out_wav, g_force, g_ring_wait_ms,
|
||||||
g_test_write_delay_ms > 0 ? " TEST-DELAY" : "",
|
g_test_write_delay_ms > 0 ? " TEST-DELAY" : "",
|
||||||
g_test_wedge_after >= 0 ? " TEST-WEDGE" : "");
|
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();
|
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 的基准;防系统时间跳变影响护栏超时) */
|
/* g_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -153,6 +153,22 @@ static int start_audiod(void)
|
|||||||
return start_audiod_ex("file", NULL, NULL);
|
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 */
|
/* 直接客户端 open */
|
||||||
static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t *ack)
|
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(eok, "fresh OPEN after overflow (table recycled, no exhaustion/wedge)");
|
||||||
check(stop_audiod_tmo(5000) != 0, "audiod not wedged by overflow (SIGTERM clean exit)");
|
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);
|
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -100,6 +100,10 @@ enum {
|
|||||||
MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */
|
MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */
|
||||||
MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */
|
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) */
|
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 ----
|
/* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ----
|
||||||
* 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。
|
* 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。
|
||||||
* 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power:
|
* 独立 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 */
|
} m_audio_route_t; /* 4 B */
|
||||||
|
|
||||||
/* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2
|
/* 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 {
|
typedef struct m_audio_volume {
|
||||||
int32_t pct; /* -1=查询;0-100=设置 */
|
int32_t pct; /* -1=查询;0-100=设置 */
|
||||||
} m_audio_volume_t; /* 4 B */
|
} 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)=====
|
/* ================= meow_netmgrd 网络管理服务(devdocs/plan/NETMGR_DESIGN.md §3/§10)=====
|
||||||
* 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。
|
* 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。
|
||||||
* 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr:
|
* 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr:
|
||||||
|
|||||||
Reference in new issue
Block a user