Compare commits

..
4 Commits
13 changed files with 1161 additions and 67 deletions

No files matched your search

+38 -10
View File
@@ -1,26 +1,41 @@
#!/bin/bash
# build_audiod.sh — meow_audiod 交叉编译(mipsel 静态)+ 宿主多目标可选。
# build_audiod.sh — meow_audiod 交叉编译(mipsel glibc-dynamic)+ 宿主多目标可选。
#
# 目标:R3 Pro II / MeowOS(mipsel-linux-muslsf,静态)。audiod 复用 meow_ipc.h(共享协议头,
# 目标:R3 Pro II / MeowOS(MIPS32 o32,真实 ABI)。audiod 复用 meow_ipc.h(共享协议头,
# -I../meow_launcherd)。audiod 为 svcd 托管的独立守护;meow_audio 为用户侧 CLI。
#
# 【P2 蓝牙链接定案 2026-09-03】设备为 glibc-2.22(ld-2.22.so / libc-2.22.so,非 musl),且
# libasound 会在运行时 dlopen 单独的 bluealsa pcm 插件(/usr/lib/alsa-lib/libasound_module_pcm_bluealsa.so)
# ——故 audiod 必须以**动态 glibc** 形态链接设备现有 libasound.so.2:
# - 用 zig --target=mipsel-linux-gnueabihf.2.22(硬浮点 o32,glibc-2.22 版本引用,设备 libs 齐);
# libasound 原型在 meow_audiod.c 内联声明(不依赖 asoundlib.h)。
# - 删 -static:musl-static 无 dlopen(实测 "Dynamic loading not supported",无法加载 bluealsa 插件);
# musl-dynamic 需设备不存在的 musl ld.so。libasound 链接库来自 libbt/(设备 libasound.so.2 副本)。
# - meow_audio CLI 无 libasound 依赖,保持 musl-static(zig-mipsel-cc)。
#
# 用法:
# ./build_audiod.sh # 交叉编译 → meow_audiod(mipsel 静态)
# ./build_audiod.sh cli # 交叉编译 meow_audio CLI → meow_audio(mipsel 静态)
# ./build_audiod.sh host # 宿主编译 meow_audiod_host + meow_audio_host(集成测试)
# ./build_audiod.sh # 交叉编译 → meow_audiod(mipsel glibc-dynamic,BT 直链 libasound)
# ./build_audiod.sh cli # 交叉编译 meow_audio CLI → meow_audio(mipsel musl-static)
# ./build_audiod.sh host # 宿主编译 *_host 并跑 test_audiod_host 回归(FILE sink)
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
ZIG="$ROOT/.tools/zig-env/bin/zig"
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
# BT 目标直链宿主 libasound.so.2(无 asoundlib.h,原型内联于 meow_audiod.c;-l: 精确到版本,
# 不依赖 unversioned dev 链接)。删 -ldl:BT 不再用 dlopen。
"$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c -o meow_audiod_host -lpthread -l:libasound.so.2
"$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm
"$HOST_CC" $INC -Os -Wall -Wextra test_audiod_host.c -o test_audiod_host -lm
echo "== host builds =="
ls -la meow_audiod_host meow_audio_host
file meow_audiod_host meow_audio_host
ls -la meow_audiod_host meow_audio_host test_audiod_host
file meow_audiod_host meow_audio_host test_audiod_host
echo "== host regression (test_audiod_host) =="
./test_audiod_host || { echo "!! host regression FAILED"; exit 1; }
exit 0
fi
@@ -33,8 +48,21 @@ if [ "$1" = "cli" ]; then
exit 0
fi
"$CC" $INC -Os -Wall -Wextra -static meow_audiod.c -o meow_audiod -lpthread
echo "== meow_audiod (mipsel static) =="
# meow_audiod:mipsel glibc-dynamic,BT 直链 libasound(-Llibbt -lasound)。-lpthread 供播放线程。
# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件镜像自拷贝(idempotent)。
LIBBT="libbt"
ASOUND_SRC="$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0"
mkdir -p "$LIBBT"
if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so"
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2"
"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c -o meow_audiod \
-lpthread -Llibbt -lasound
echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) =="
ls -la meow_audiod
md5sum meow_audiod
file meow_audiod
echo "-- dynamic NEEDED --"
readelf -d meow_audiod 2>/dev/null | grep -iE "needed" || true
echo "-- glibc version refs (max must be <= 2.22) --"
readelf --version-info meow_audiod 2>/dev/null | grep -oE "GLIBC_2\.[0-9]+" | sort -uV | tail -3 || true
+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;
+421 -30
View File
@@ -74,6 +74,12 @@
#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_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 +102,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 +184,13 @@ 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)。audiod 以动态 glibc 链接设备 libasound.so.2
* (见 bt_* 段注释),此处仅存 snd_pcm_t 不透明句柄。 */
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%(幂等,一次即可) */
/* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ----
* 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16),
* ~100ms 窗口(rate/10 帧)到点才结算一次 RMS/peak 并发布快照——每帧仅几次整数加乘,
@@ -319,6 +338,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 +682,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 +726,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 +743,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 +817,250 @@ static int settings_get_volume(void)
return v;
}
/* ============================================================================
* 蓝牙 A2DP source(输出目标 OUT_BT,PLAN §三/§五)
* ----------------------------------------------------------------------------
* audiod **直接调用 libasound** 连 `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)。
* 链接说明(2026-09-03 真机定案):`pcm.bluealsa` 的 bluealsa 是**独立 pcm 插件**,libasound
* 在运行时以 dlopen 加载 /usr/lib/alsa-lib/libasound_module_pcm_bluealsa.so(glibc,依赖
* libdbus-1.so.3)——故 audiod 必须以**动态 glibc** 形态链接设备现有 libasound.so.2
* (build_audiod.sh:zig --target=mipsel-linux-gnueabihf.2.22,删 -static)。musl-static 不可行
* (实测 "Dynamic loading not supported",无 dlopen 无法加载插件);musl-dynamic 亦不可行(设备
* 无 musl ld.so)。libasound 原型在此内联声明(避免依赖 asoundlib.h;工具链不提供)。
* 禁止旁路:不跑 bluealsa-aplay/bluealsa-cli open(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
/* libasound 相关函数原型(内联声明,避免依赖 asoundlib.h;snd_pcm_t 不透明 → void*) */
int snd_pcm_open(void **pcm, const char *name, int stream, int mode);
int snd_pcm_set_params(void *pcm, int format, int access, unsigned channels,
unsigned rate, int soft_resample, unsigned latency_us);
int snd_pcm_nonblock(void *pcm, int nonblock);
long snd_pcm_writei(void *pcm, const void *buffer, unsigned long size);
int snd_pcm_wait(void *pcm, int timeout);
int snd_pcm_recover(void *pcm, int err, int silent);
int snd_pcm_drain(void *pcm);
int snd_pcm_close(void *pcm);
const char *snd_strerror(int errnum);
static void bt_close(void); /* 前向:bt_write 判死路径要用 */
static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */
/* 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 打开: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)
{
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;
}
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;
}
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 = snd_pcm_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 = snd_pcm_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);
snd_pcm_recover(g_bt_hdl, err, 1);
continue;
}
logmsg("bt writei err: %s", snd_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) {
snd_pcm_drain(g_bt_hdl);
snd_pcm_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 +1173,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 +1230,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 +1296,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 +1318,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 +1471,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;
}
/* 输出目标=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");
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 +1532,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 +1582,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 +1595,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 +1615,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 +1624,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,8 +1775,22 @@ 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) 强制重烧。 */
{
* 端口已是目标值(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);
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;
@@ -1420,13 +1798,12 @@ static void *route_thread(void *arg)
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) {
pthread_mutex_lock(&g_mu);
int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */
pthread_mutex_unlock(&g_mu);
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 +1921,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 +1936,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 +1963,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;
}
+1 -1
View File
@@ -29,7 +29,7 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
font_theme.c keys.c util.c meow_dep.c \
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
"../sdk/meow_lyric.c" "../sdk/meow_lyric_view.c" \
"../sdk/meow_lyric.c" "../sdk/meow_lyric_view.c" "../sdk/meow_img_color.c" \
"../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
echo "== meow_launcherd =="
ls -la meow_launcherd
+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:
+79 -10
View File
@@ -12,6 +12,7 @@
#include "meow_launcher_internal.h"
#include "np_gesture.h" /* 批4:双页方向感知手势决策(header-only,宿主可单测) */
#include "meow_lyric_view.h" /* 批2控件:歌词页渲染(批4接入,数据批3 meow_media_lyric_*) */
#include "meow_img_color.h" /* 批5:封面双色取色原语(独立源码,不并入 meow_ui.a) */
#define NP_COVER 300 /* 封面边长(方,居中) */
@@ -66,6 +67,56 @@ static void np_cover_load(const meow_media_session_t *s)
g_np_cover = meow_img_load_file(path);
}
/* ==================================================================
* 批5:封面双色取色(主色+次色)→ 播放页/歌词页动态主题。
* - 何时取:进入 Now Playing 页 / 封面路径变化时(np_cover_theme_update 幂等,按路径缓存)。
* - 取色成本:meow_img_duocolor 内部自适副采样(等效缩略图,≤~3.6k 样本),不额外解码/分配。
* - 缓存:按 artwork 路径;路径未变不重取(g_np_cc_valid + g_np_cc_path)。
* - 回退:无封面/取色失败(g_np_cc_valid=0)→ 歌词页 set_colors(NULL) 用 meow_theme 默认;
* 播放页背景/文字走 s_ui_theme() 主题面。
* ================================================================== */
static meow_img_color_theme g_np_cc;
static int g_np_cc_valid = 0;
static char g_np_cc_path[200] = "";
/* 校验/刷新取色缓存(封面路径变化才重取;幂等)。 */
static void np_cover_theme_update(void)
{
const meow_media_session_t *s = meow_media_current();
const char *art = s ? s->artwork : "";
if (g_np_cc_valid && strcmp(g_np_cc_path, art) == 0) return; /* 缓存命中 */
snprintf(g_np_cc_path, sizeof g_np_cc_path, "%s", art);
g_np_cc_valid = 0;
if (!g_np_cover || !art[0]) return; /* 无封面 → 回退主题 */
uint32_t mainc = 0, secondc = 0;
if (meow_img_duocolor(g_np_cover, &mainc, &secondc) != 0) return; /* 取色失败 → 回退主题 */
meow_img_color_derive(mainc, secondc, meow_theme_get(), &g_np_cc);
g_np_cc_valid = 1;
}
/* 把封面取色注入歌词页(无取色 → NULL 回退 meow_theme 默认)。 */
static void np_apply_lyric_colors(void)
{
if (!g_np_lyric) return;
meow_lyric_view_set_colors(g_np_lyric, g_np_cc_valid ? &g_np_cc.lyr : NULL);
}
/* 竖向双色渐变填充(无 GPU,逐行整数插值;封面主题背景用)。 */
static void np_fill_gradient(int x, int y, int w, int h, uint32_t top, uint32_t bottom)
{
if (w <= 0 || h <= 0) return;
int tr = (top >> 16) & 255, tg = (top >> 8) & 255, tb = top & 255;
int br = (bottom >> 16) & 255, bg = (bottom >> 8) & 255, bb = bottom & 255;
int span = h - 1;
for (int yy = 0; yy < h; yy++) {
int t = span > 0 ? yy * 255 / span : 0;
int r = tr + ((br - tr) * t + 128) / 255;
int g = tg + ((bg - tg) * t + 128) / 255;
int b = tb + ((bb - tb) * t + 128) / 255;
fill_rect(x, y + yy, w, 1, ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b);
}
}
#if 0 /* ================================================================
* M7.0b 退役:Now Playing 手绘控制钮/进度条改走 SDK 控件(meow_iconbtn +
* meow_progress),命中/状态/进度全由 SDK 接管;保留作回退位(删除 #if 0 行恢复旧手绘)。
@@ -166,13 +217,14 @@ static void np_widgets_create(void)
g_np_init = 1;
}
/* 主题刷新:播放钮底色/图标随当前主题(明暗/风格切换后跟手)。 */
/* 主题刷新:播放钮底色/图标随当前主题(明暗/风格切换后跟手);批5 封面取色时随 accent。 */
static void np_refresh_theme(void)
{
if (!g_np_init) return;
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
op.bg = g_np_cc_valid ? g_np_cc.accent : th->primary;
op.fg = g_np_cc_valid ? g_np_cc.accent_fg : th->onPrimary;
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
}
@@ -201,8 +253,9 @@ static void np_draw_progress(const meow_media_session_t *s, uint32_t pos, const
unsigned dm = (s->dur_ms ? s->dur_ms : 0) / 60000u, ds = (s->dur_ms ? s->dur_ms : 0) % 60000u / 1000u;
snprintf(l, sizeof l, "%u:%02u", lm, ls);
snprintf(rbuf, sizeof rbuf, "%u:%02u", dm, ds);
ui_draw(NP_PROG_X, NP_TIME_Y, l, th->on_surface_variant, 16);
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, th->on_surface_variant, 16);
uint32_t tcol = g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant;
ui_draw(NP_PROG_X, NP_TIME_Y, l, tcol, 16);
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, tcol, 16);
}
/* ==================================================================
@@ -238,14 +291,20 @@ static void np_lyric_refresh(const meow_media_session_t *s)
g_np_lyric_last_valid = valid;
}
/* 歌词页整页绘制:铺 theme surface 底 → lyric_view(自绘 scrim + 歌词/空态)。 */
/* 歌词页整页绘制:铺封面取色渐变(或 theme surface)底 → lyric_view(自绘 scrim + 歌词/空态)。 */
static void np_draw_lyric_page(int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
if (!g_np_init) np_widgets_create();
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
/* 批5:封面取色背景(主→次渐变)或主题面;并注入歌词页 set_colors。 */
np_cover_theme_update();
if (g_np_cc_valid)
np_fill_gradient(0, SB_BAR_H, W, H - SB_BAR_H, g_np_cc.bg_top, g_np_cc.bg_bottom);
else
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
np_apply_lyric_colors();
np_lyric_refresh(meow_media_current());
if (g_np_lyric)
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
@@ -264,10 +323,10 @@ static void np_draw_page(int redraw_sb)
return;
}
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) {
/* 空态占位(M7.0b 文案走 meow_str,未知 id 回退中文) */
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
const char *t1 = meow_str_zh("np.not_playing", "未在播放");
ui_draw((W - ui_width(t1, 26)) / 2, 300, t1, th->on_surface_variant, 26);
const char *t2 = meow_str_zh("np.open_app_hint", "打开播放 App 以显示正在播放");
@@ -278,6 +337,14 @@ static void np_draw_page(int redraw_sb)
return;
}
np_cover_load(s);
/* 批5:封面取色(主/次色)+ 背景渐变或主题面;并注入歌词页 set_colors。
* np_cover_theme_update 幂等(封面路径不变不重取);无封面/取色失败回退 meow_theme。 */
np_cover_theme_update();
if (g_np_cc_valid)
np_fill_gradient(0, SB_BAR_H, W, H - SB_BAR_H, g_np_cc.bg_top, g_np_cc.bg_bottom);
else
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
np_apply_lyric_colors();
/* 封面:解码缓存 + 圆角裁方;无封面回退主题色占位 + 首字 */
if (g_np_cover) {
meow_gfx_fill_round(NP_COVER_X - 6, NP_COVER_Y - 6, NP_COVER + 12, NP_COVER + 12, 18, th->surface_container);
@@ -290,11 +357,11 @@ static void np_draw_page(int redraw_sb)
ui_draw(NP_COVER_X + (NP_COVER - cw) / 2, NP_COVER_Y + (NP_COVER - meow_font_line_height(22)) / 2,
t, meow_theme_get()->onPrimary, 22);
}
/* 元数据 */
np_center_text(NP_TITLE_Y, s->title, 26, th->on_surface, W - 40);
np_center_text(NP_ARTIST_Y, s->artist, 18, th->on_surface_variant, W - 60);
/* 元数据(批5:封面取色时文字随 fg/fg_dim,否则主题面) */
np_center_text(NP_TITLE_Y, s->title, 26, g_np_cc_valid ? g_np_cc.fg : th->on_surface, W - 40);
np_center_text(NP_ARTIST_Y, s->artist, 18, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 60);
if (s->album[0])
np_center_text(NP_ALBUM_Y, s->album, 16, th->on_surface_variant, W - 80);
np_center_text(NP_ALBUM_Y, s->album, 16, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 80);
/* 进度(SDK meow_progress) */
uint32_t pos = meow_media_display_pos(s);
np_draw_progress(s, pos, th);
@@ -564,4 +631,6 @@ void run_now_playing(struct app *apps, int napps)
kp_close(&kp_power);
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
g_np_cover_path[0] = 0;
g_np_cc_valid = 0; /* 批5:退出清取色缓存,下次进入重新取 */
g_np_cc_path[0] = 0;
}
+33 -1
View File
@@ -755,6 +755,38 @@
}
return homeCard('WiFi / DNS', '<div class="home-card-grid">' + rows + '</div>');
}
/* 蓝牙状态卡(M6.30):数据来自 overview 的 net.bt(netmgrd status pass-through)。
* 展示:蓝牙开关/已连接/已配对/本机地址/A2DP 编解码·采样率;输出目标(DAC|蓝牙)与音量控制(avrcp/dac)
* 依赖面板后端透传 audiod 的 target/volctl 键——当前 audio_fill 未透传,缺失时优雅降级为「—」(不谎报)。 */
function btCard() {
const nd = homeData && homeData.net;
const ad = homeData && homeData.audio;
let bt = nd && nd.bt;
if (!bt || !isAvail(nd)) return homeCard('蓝牙', homeUnavailable());
if (bt.available === false) return homeCard('蓝牙', '<div class="home-sub">无蓝牙</div>');
const on = !!bt.powered;
const conn = (bt.connected != null) ? bt.connected : 0;
let rows = '';
rows += homeKv('蓝牙', on ? '<span class="home-chip ok">已开启</span>' : '<span class="home-chip warn">已关闭</span>');
rows += homeKv('连接', conn > 0 ? ('<span class="home-chip ok">已连接 ' + esc(conn) + '</span>') : '<span class="home-chip mut">未连接</span>');
rows += homeKv('已配对', bt.paired != null ? esc(bt.paired) : '—');
if (bt.address) rows += homeKv('本机地址', esc(bt.address));
if (conn > 0) {
rows += homeKv('A2DP 编解码', bt.a2dp_codec ? esc(bt.a2dp_codec) : '—');
rows += homeKv('采样率', bt.a2dp_rate ? esc(bt.a2dp_rate) : '—');
} else {
rows += homeKv('A2DP 编解码', '<span class="home-chip mut">—</span>');
}
const target = ad && ad.target != null ? ad.target : null;
const volctl = ad && ad.volctl != null ? ad.volctl : null;
const targetName = target === 'bt' ? '蓝牙' : (target === 'dac' ? 'DAC' : null);
rows += homeKv('输出目标', targetName ? ('<span class="home-chip ' + (target === 'bt' ? 'ok' : 'mut') + '">' + esc(targetName) + '</span>') : '<span class="home-chip mut">—</span>');
rows += homeKv('音量控制', volctl ? esc(volctl) : '—');
if (ad && ad.vol != null && ad.vol >= 0) {
rows += homeKv('音量', esc(ad.vol) + ' %' + (volctl === 'avrcp' ? ' <span class="muted">(AVRCP)</span>' : ''));
}
return homeCard('蓝牙', '<div class="home-card-grid">' + rows + '</div>');
}
function healthCard() {
const ams = homeData && homeData.ams;
const svcd = homeData && homeData.svcd;
@@ -840,7 +872,7 @@
if (!homeData) { body.innerHTML = '<p class="placeholder">加载总览…</p>'; return; }
let h = powerSummaryCard() + eventsBar(); // 摘要条(功耗/事件)置顶
h += '<div class="home-grid">' +
deviceCard() + batteryCard() + audioCard() + netCard() + healthCard() + storageCard() + perfCard() +
deviceCard() + batteryCard() + audioCard() + netCard() + btCard() + healthCard() + storageCard() + perfCard() +
'</div>';
body.innerHTML = h;
const ad = homeData.audio;
+154
View File
@@ -0,0 +1,154 @@
/*
* meow_img_color.c — MeowOS SDK 封面双色渐变取色实现(见 meow_img_color.h 契约)。
*
* 独立源码,不并入 meow_ui.a(铁律:避免并发重编/四 App 重链)。纯函数,无任何 meow_* 调用,
* 宿主单测零链接重(仅需本 .c + meow_img_color.h 及其 include 链)。
*
* 布局耦合说明:meow_img 为 opaque(对外仅前向声明,结构体私有于 meow_ui_img.c)。为在不动
* meow_ui.a 二进制的前提下直读封面像素(批5 取色),本 TU 按 meow_ui_img.c 自 A1 起稳定的
* `struct meow_img { int w, h; unsigned char *rgba; };` 未布局局部补全。两 TU 各持同一定义,
* 链接后字段偏移一致(本项目内 ABI 稳定)。主会话下次重建 SDK 时可上收为 meow_ui_img.c 的
* 正式访问器,届时本补全即删除。
*/
#include "meow_img_color.h"
#include <string.h>
struct meow_img { int w, h; unsigned char *rgba; }; /* 见上:局部补全(与 meow_ui_img.c 一致) */
#define MIC_BUCKETS 4 /* 每通道色阶数 ⇒ 4×4×4=64 桶 */
#define MIC_NBUCKET (MIC_BUCKETS*MIC_BUCKETS*MIC_BUCKETS)
#define MIC_ALPHA_MIN 64 /* 跳过近透明(alpha<64) */
#define MIC_LUMA_MIN 24 /* 跳过近黑 */
#define MIC_LUMA_MAX 244 /* 跳过近白 */
#define MIC_MIN_VALID 8 /* 有效采样下限(低于=取色失败回退主题) */
#define MIC_TARGET 900 /* 目标采样数(副采样步进自适,×4 作上限) */
#define MIC_SEC_FRAC 7U /* 次桶显著门槛:≥主桶 1/7(≈14%) */
static int mic_luma(uint32_t c)
{
int r = (int)((c >> 16) & 255), g = (int)((c >> 8) & 255), b = (int)(c & 255);
return (r * 299 + g * 587 + b * 114) / 1000;
}
/* 两色线性混合(t=0..255,0=纯 a、255=纯 b;逐通道整数,无浮点)。 */
static uint32_t mic_mix(uint32_t a, uint32_t b, int t)
{
if (t < 0) t = 0;
if (t > 255) t = 255;
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
int r = ar + ((br - ar) * t + 128) / 255;
int g = ag + ((bg - ag) * t + 128) / 255;
int bl = ab + ((bb - ab) * t + 128) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
}
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
uint32_t *main, uint32_t *second)
{
if (!rgba || w <= 0 || h <= 0 || !main || !second) return -1;
/* 副采样步进:使采样数≈MIC_TARGET(×4 上限),大图/缩略图成本恒定。 */
int step = 1;
while ((long)(w / step) * (long)(h / step) > (long)MIC_TARGET * 4 && step < 64)
step++;
long cnt[MIC_NBUCKET];
long sr[MIC_NBUCKET], sg[MIC_NBUCKET], sb[MIC_NBUCKET], sw[MIC_NBUCKET];
memset(cnt, 0, sizeof cnt);
memset(sr, 0, sizeof sr);
memset(sg, 0, sizeof sg);
memset(sb, 0, sizeof sb);
memset(sw, 0, sizeof sw);
long total = 0;
for (int y = 0; y < h; y += step) {
for (int x = 0; x < w; x += step) {
const uint8_t *p = rgba + ((size_t)y * (size_t)w + (size_t)x) * 4;
int a = p[3];
if (a < MIC_ALPHA_MIN) continue;
int r = p[0], g = p[1], b = p[2];
if ((r * 299 + g * 587 + b * 114) / 1000 < MIC_LUMA_MIN) continue; /* 近黑 */
if ((r * 299 + g * 587 + b * 114) / 1000 > MIC_LUMA_MAX) continue; /* 近白 */
int mx = r, mn = r;
if (g > mx) mx = g;
if (b > mx) mx = b;
if (g < mn) mn = g;
if (b < mn) mn = b;
int sat = mx - mn; /* 饱和度 0..255 */
long wt = 1 + sat / 24; /* 1..~11:偏向鲜艳(跳过灰蒙蒙) */
int qr = r * MIC_BUCKETS / 256, qg = g * MIC_BUCKETS / 256, qb = b * MIC_BUCKETS / 256;
int bi = (qr * MIC_BUCKETS + qg) * MIC_BUCKETS + qb;
if (bi < 0 || bi >= MIC_NBUCKET) continue;
cnt[bi] += wt;
sr[bi] += (long)r * wt; sg[bi] += (long)g * wt; sb[bi] += (long)b * wt;
sw[bi] += wt;
total++;
}
}
if (total < MIC_MIN_VALID) return -1; /* 全透明/近黑白 → 失败回退主题 */
/* 主色:加权最大桶中心 */
int mi = -1;
long best = -1;
for (int i = 0; i < MIC_NBUCKET; i++)
if (cnt[i] > best) { best = cnt[i]; mi = i; }
if (mi < 0 || sw[mi] <= 0) return -1;
long mr0 = sr[mi] / sw[mi], mg0 = sg[mi] / sw[mi], mb0 = sb[mi] / sw[mi];
*main = ((uint32_t)mr0 << 16) | ((uint32_t)mg0 << 8) | (uint32_t)mb0;
/* 次色:与主色 RGB 色距最大的显著桶;无显著桶→主色缩暗 2/3(渐变互补)。 */
long thresh = best > 0 ? best / MIC_SEC_FRAC : 1;
int si = -1;
long sd = -1;
for (int i = 0; i < MIC_NBUCKET; i++) {
if (i == mi || cnt[i] < thresh || sw[i] <= 0) continue;
long dr = sr[i] / sw[i] - mr0, dg = sg[i] / sw[i] - mg0, db = sb[i] / sw[i] - mb0;
long d = dr * dr + dg * dg + db * db;
if (d > sd) { sd = d; si = i; }
}
if (si >= 0)
*second = ((uint32_t)(sr[si] / sw[si]) << 16) | ((uint32_t)(sg[si] / sw[si]) << 8) |
(uint32_t)(sb[si] / sw[si]);
else
*second = ((uint32_t)(mr0 * 2 / 3) << 16) | ((uint32_t)(mg0 * 2 / 3) << 8) |
(uint32_t)(mb0 * 2 / 3);
return 0;
}
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second)
{
if (!im) return -1;
/* 局部补全 struct 视图直读像素(布局与 meow_ui_img.c 一致,见顶部说明)。 */
return meow_img_duocolor_rgba(im->rgba, im->w, im->h, main, second);
}
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
meow_img_color_theme *out)
{
if (!out) return;
uint32_t mainv = main ? main : (th ? th->primary : 0x808080);
uint32_t secv = second ? second : (th ? th->secondary : mainv);
/* 背景平均亮度 → 文字可读色(黑/白) */
int luavg = (mic_luma(mainv) + mic_luma(secv)) / 2;
uint32_t fg = (luavg >= 140) ? 0x1C1C1C : 0xFFFFFF;
uint32_t fgd = mic_mix(fg, mic_mix(mainv, 0x0A0A0A, 150), 90); /* 次要文字向背景靠(降对比) */
/* 背景渐变:主/次各压暗 55%(防刺眼 + 保文字可读) */
uint32_t bg_top = mic_mix(mainv, 0x0A0A0A, 150);
uint32_t bg_bottom = mic_mix(secv, 0x0A0A0A, 150);
/* 强调=次色(与主色底对比);其上图标按 accent 亮度选黑/白 */
uint32_t accent = secv;
uint32_t accent_fg = (mic_luma(accent) >= 140) ? 0x1C1C1C : 0xFFFFFF;
out->bg_top = bg_top; out->bg_bottom = bg_bottom;
out->fg = fg; out->fg_dim = fgd;
out->accent = accent; out->accent_fg = accent_fg;
/* 歌词页覆盖:scrim=主色压暗、cur=fg(可读)、gray 留 0(lyric_view 以 cur 自动递减)。 */
meow_lyric_view_colors *lc = &out->lyr;
memset(lc, 0, sizeof *lc);
lc->scrim = mic_mix(mainv, 0x000000, 150);
lc->cur = fg;
lc->scrim_alpha = 176;
}
+67
View File
@@ -0,0 +1,67 @@
/*
* meow_img_color.h — MeowOS SDK 封面双色渐变取色(批5)。
*
* 定位:从歌曲封面(已解码 RGBA 像素/meow_img 句柄)提取「主色 + 次色」,供播放页/歌词页动态主题
* (背景渐变/文字 accent)。纯 C 分桶统计,无 GPU、无浮点、无堆分配。
*
* 组织:独立公共头(仿 meow_lyric_view.h 模式,不随 meow_ui.h 伞头发布),取色原语是独立源码
* meow_img_color.c——**不并入 meow_ui.a**(铁律:避免并发重编/四 App 重链)。App/launcher 需显式
* `#include "meow_img_color.h"` 并在构建时单链 meow_img_color.c(宿主单测同法)。
*
* 约束(0 warning / 最小侵入):
* - 不修改 meow_ui.a 任何成员(meow_img 保持 opaque 对外)。为直读封面像素,本 TU 按
* meow_ui_img.c 自 A1 起稳定的 `struct meow_img { int w,h; unsigned char *rgba; }` 布局
* 局部补全(见 .c 顶部说明)。主会话下次重建 SDK 时可上收为 meow_ui_img.c 的正式访问器。
* - 纯函数:meow_img_duocolor_rgba / meow_img_duocolor / meow_img_color_derive 不调用任何
* meow_* 符号,宿主单测可零链接重(仅需本头 + 本 .c)。
*
* 色彩语义:
* - 输入 RGBA(stb_image 输出,4 通道 straight alpha),w×h,最多遍历 ~3.6k 采样(内部自适
* 副采样步进,等效缩略图成本,避免全尺寸逐像素)。
* - 色域分桶 4×4×4=64(RGB 各 4 段),跳过近黑(luma<24)/近白(luma>244)/透明(alpha<64),
* 按「像素数 + 饱和度」加权;主色=加权最大桶中心;次色=与主色色距最大的显著桶(≥主桶 14%)
* 中心;无显著桶→次色=主色缩暗 2/3(渐变互补)。
* - 返回 0=成功;-1=参数非法 / 有效采样不足(调用方回退全局 meow_theme)。
*/
#ifndef MEOW_IMG_COLOR_H
#define MEOW_IMG_COLOR_H
#include "meow_ui.h" /* meow_img 句柄 + meow_theme */
#include "meow_lyric_view.h" /* meow_lyric_view_colors(派生歌词页覆盖) */
#ifdef __cplusplus
extern "C" {
#endif
/* 从 RGBA 像素缓冲取主色+次色(纯算法;样本副采样)。返回 0=成功;-1=失败。 */
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
uint32_t *main, uint32_t *second);
/* 从 meow_img 句柄取主色+次色(内部直读 rgba 缓冲,见 .c 布局说明)。返回 0=成功;-1=失败。 */
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second);
/* 由主/次色派生播放页 + 歌词页封面主题(纯函数,可宿主单测)。
* th 用于默认回退色(main/second 为 0 时);调用方应传 meow_theme_get()。
* 派生语义:
* - 背景渐变 = 主色(顶)→ 次色(底)各压暗 55%(防刺眼 + 保文字可读);
* - fg = 可读文字色(按主次平均亮度选黑/白),fg_dim = 向背景靠拢的次要文字色;
* - accent = 次色(与主色底对比),accent_fg = accent 上可读图标色;
* - lyr = 歌词页 set_colors 覆盖:scrim=主色压暗(alpha 176)、cur=fg,gray 留 0(由 lyric_view
* 以「cur 为基准自动递减」——批5 已修正对覆盖 cur 亦然)。 */
typedef struct meow_img_color_theme {
uint32_t bg_top; /* 播放页渐变顶 */
uint32_t bg_bottom; /* 播放页渐变底 */
uint32_t fg; /* 主要文字色(背景上可读) */
uint32_t fg_dim; /* 次要文字色 */
uint32_t accent; /* 强调色(控件/进度/封面占位) */
uint32_t accent_fg; /* accent 上可读前景(图标) */
meow_lyric_view_colors lyr; /* 歌词页 set_colors 覆盖 */
} meow_img_color_theme;
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
meow_img_color_theme *out);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_IMG_COLOR_H */
+14 -10
View File
@@ -84,20 +84,24 @@ static void lv_resolve_colors(const meow_lyric_view *lv, const meow_theme *th,
/* 内建默认(theme 驱动) */
uint32_t scrim = th->scrim;
if (scrim == 0) scrim = th->surfaceDim; /* scrim 缺省为黑:用 surfaceDim 兜底(非纯黑) */
uint32_t cur = th->onSurface; /* dark=0xFFFFFF 纯白(活动行高亮) */
out->scrim = scrim;
out->cur = cur;
out->cur = th->onSurface; /* dark=0xFFFFFF 纯白(活动行高亮) */
out->scrim_alpha = 176;
out->gray[0] = cur;
for (int i = 1; i < 6; i++) out->gray[i] = lv_scale_col(cur, lv_gray_fac[i]);
if (!lv->has_colors) return;
/* 用户覆盖(0 字段=沿用上面内建默认;scrim_alpha<0=沿用默认 176) */
if (!lv->has_colors) {
out->gray[0] = out->cur;
for (int i = 1; i < 6; i++) out->gray[i] = lv_scale_col(out->cur, lv_gray_fac[i]);
return;
}
/* 用户覆盖(0 字段=沿用内建默认;scrim_alpha<0=沿用默认 176)。
* gray 显式字段优先;未给字段以「解析后 cur」为基准自动递减(对齐头文档
* "gray[1..5]=对 cur 逐级缩灰"——覆盖 cur 后灰阶也应随 cur,而非随主题默认 onSurface)。 */
if (lv->colors.scrim != 0) out->scrim = lv->colors.scrim;
if (lv->colors.cur != 0) { out->cur = lv->colors.cur; out->gray[0] = lv->colors.cur; }
if (lv->colors.cur != 0) out->cur = lv->colors.cur;
if (lv->colors.scrim_alpha >= 0) out->scrim_alpha = lv->colors.scrim_alpha;
for (int i = 0; i < 6; i++)
if (lv->colors.gray[i] != 0) out->gray[i] = lv->colors.gray[i];
out->gray[0] = (lv->colors.gray[0] != 0) ? lv->colors.gray[0] : out->cur;
for (int i = 1; i < 6; i++)
out->gray[i] = (lv->colors.gray[i] != 0) ? lv->colors.gray[i]
: lv_scale_col(out->cur, lv_gray_fac[i]);
}
/* 距活动行 dist 的生效行色(dist 越界 clamp 0..5)。 */
+225
View File
@@ -0,0 +1,225 @@
/* test_meow_img_color.c — 批5 封面双色渐变取色宿主验证。
*
* 覆盖矩阵(对齐任务批5):
* C1 纯色 → 主色=该色(桶平均精确);无显著次桶 → 次色=主色缩暗 2/3(渐变互补)
* C2 两色图 → 主/次正确分离(各桶平均精确)
* C3 近黑/近白/透明跳过:近黑+鲜艳 → 主=鲜艳;全近黑/全近白/全透明 → -1(回退主题)
* C4 尺寸缩放/副采样:12×12 与 120×120 同内容 → 主色不变
* C5 meow_img_duocolor 桥接(局部 struct meow_img 直读 rgba)
* C6 meow_img_color_derive:可读 fg/accent_fg、背景渐变、scrim=主色压暗
* C7 set_colors 注入/回退(lyric_view 层):注入 derive.lyr → cur=fg、gray 随 cur;
* NULL 回退 meow_theme 默认
*
* 编译(在 patch_v2/sdk 下;链法同 test_meow_lyric_view + 单链 meow_img_color.c):
* gcc -Wall -Wextra -o test_meow_img_color test_meow_img_color.c test_host_stub.c \
* meow_img_color.c meow_lyric.c meow_lyric_view.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_img_color
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp(如需真实像素断言) */
#include "meow_lyric_view.h"
#include "meow_img_color.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
/* 局部补全 meow_img(与 meow_ui_img.c / meow_img_color.c 布局一致):构造假封面做桥接测试。 */
struct meow_img { int w, h; unsigned char *rgba; };
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
/* 构造 RGBA 缓冲:rows 描述每行占 w 空格,n 个 (start,count,r,g,b,a) 段。 */
static unsigned char *make_rgba(int w, int h)
{
unsigned char *buf = (unsigned char *)calloc((size_t)w * h * 4, 1);
assert(buf);
return buf;
}
static void fill_px(unsigned char *buf, int w, int x, int y, int r, int g, int b, int a)
{
unsigned char *p = buf + ((size_t)y * w + (size_t)x) * 4;
p[0] = (unsigned char)r; p[1] = (unsigned char)g; p[2] = (unsigned char)b; p[3] = (unsigned char)a;
}
/* 填充一块实心色(x0,y0,w0,h0)。 */
static void fill_rect_px(unsigned char *buf, int W, int x0, int y0, int w0, int h0,
int r, int g, int b, int a)
{
for (int y = y0; y < y0 + h0; y++)
for (int x = x0; x < x0 + w0; x++)
fill_px(buf, W, x, y, r, g, b, a);
}
static void c1_solid(void)
{
printf("C1 纯色 → 主色精确,次色=主色缩暗 2/3\n");
int W = 12, H = 12;
unsigned char *buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, H, 0x30, 0x60, 0xA0, 0xFF);
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
CHECK(rc == 0, "纯色取色成功");
CHECK(m == 0x3060A0, "主色==0x3060A0(桶中心精确)");
CHECK(s == 0x20406A, "次色==0x20406A(主色缩暗 2/3 渐变互补)");
free(buf);
}
static void c2_two_colors(void)
{
printf("C2 两色图 → 主/次分离\n");
int W = 16, H = 16;
unsigned char *buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, 8, 0xE0, 0x30, 0x40, 0xFF); /* 上半:红 */
fill_rect_px(buf, W, 0, 8, W, 8, 0x20, 0x40, 0xE0, 0xFF); /* 下半:蓝 */
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
CHECK(rc == 0, "两色取色成功");
CHECK(m != s, "主色 != 次色");
/* 蓝的饱和权重更高(sat=192 vs 红 176)→ 主色偏蓝,次色偏红 */
CHECK(((m >> 16) & 0xFF) < 0x80 && (m & 0xFF) > 0xA0, "主色为蓝系(B 高)");
CHECK(((s >> 16) & 0xFF) > 0xA0 && (s & 0xFF) < 0x80, "次色为红系(R 高)");
free(buf);
}
static void c3_skip_near(void)
{
printf("C3 近黑/近白/透明跳过\n");
/* 近黑 + 一排鲜艳 → 主=鲜艳(近黑被跳过) */
int W = 12, H = 12;
unsigned char *buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, 11, 0x08, 0x08, 0x08, 0xFF); /* 近黑(luma<24) */
fill_rect_px(buf, W, 0, 11, W, 1, 0xE0, 0x30, 0x40, 0xFF); /* 鲜艳一行 */
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
CHECK(rc == 0, "近黑+鲜艳 → 成功");
CHECK(m == 0xE03040, "主色==鲜艳色(近黑被跳过)");
free(buf);
/* 全近黑 → -1(回退主题) */
buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, H, 0x10, 0x10, 0x10, 0xFF);
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全近黑 → -1");
free(buf);
/* 全近白 → -1 */
buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, H, 0xF8, 0xF8, 0xF8, 0xFF);
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全近白 → -1");
free(buf);
/* 全透明 → -1 */
buf = make_rgba(W, H);
/* 默认 calloc → rgba=0 → alpha=0 全部跳过 */
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全透明 → -1");
free(buf);
}
static void c4_size_invariant(void)
{
printf("C4 尺寸缩放/副采样不变性\n");
/* 12×12 vs 120×120:同内容上下半红蓝 → 主色(蓝)一致 */
uint32_t m1 = 0, s1 = 0, m2 = 0, s2 = 0;
unsigned char *b1 = make_rgba(12, 12);
fill_rect_px(b1, 12, 0, 0, 12, 6, 0xE0, 0x30, 0x40, 0xFF);
fill_rect_px(b1, 12, 0, 6, 12, 6, 0x20, 0x40, 0xE0, 0xFF);
CHECK(meow_img_duocolor_rgba(b1, 12, 12, &m1, &s1) == 0, "小图取色成功");
unsigned char *b2 = make_rgba(120, 120);
fill_rect_px(b2, 120, 0, 0, 120, 60, 0xE0, 0x30, 0x40, 0xFF);
fill_rect_px(b2, 120, 0, 60, 120, 60, 0x20, 0x40, 0xE0, 0xFF);
CHECK(meow_img_duocolor_rgba(b2, 120, 120, &m2, &s2) == 0, "大图取色成功");
CHECK(m1 == m2, "大/小图主色一致(副采样不变性)");
free(b1);
free(b2);
}
static void c5_img_bridge(void)
{
printf("C5 meow_img_duocolor 桥接(局部 struct meow_img)\n");
int W = 4, H = 4;
struct meow_img im;
im.w = W; im.h = H;
im.rgba = make_rgba(W, H);
fill_rect_px(im.rgba, W, 0, 0, W, 2, 0xE0, 0x30, 0x40, 0xFF);
fill_rect_px(im.rgba, W, 0, 2, W, 2, 0x20, 0x40, 0xE0, 0xFF);
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor((const meow_img *)&im, &m, &s);
CHECK(rc == 0, "meow_img 句柄取色成功");
CHECK(((m >> 16) & 0xFF) < 0x80 && (m & 0xFF) > 0xA0, "桥接主色为蓝系");
/* NULL → -1 */
CHECK(meow_img_duocolor(NULL, &m, &s) == -1, "NULL 句柄 → -1");
free(im.rgba);
}
static void c6_derive(void)
{
printf("C6 meow_img_color_derive 派生主题\n");
meow_theme th; memset(&th, 0, sizeof th);
th.primary = 0x204060; th.secondary = 0xE0B050; /* 深蓝主 + 琥珀次 */
meow_img_color_theme ct;
meow_img_color_derive(0x204060, 0xE0B050, &th, &ct);
/* 可读性:主深次亮 → 平均亮度偏中低 → 文字白 */
CHECK(ct.fg == 0xFFFFFF, "fg==白色(背景偏暗可读)");
CHECK(ct.accent == 0xE0B050, "accent==次色(琥珀)");
CHECK(ct.accent_fg == 0x1C1C1C, "accent_fg==深色(琥珀上可读图标)");
CHECK(ct.bg_top != ct.bg_bottom, "背景双色渐变(顶!=底)");
/* scrim=主色压暗:比原主色更暗(向黑靠) */
CHECK(((ct.lyr.scrim >> 16) & 0xFF) < 0x20, "lyr.scrim 主色压暗(R 递减)");
CHECK(ct.lyr.cur == 0xFFFFFF, "lyr.cur==fg(可读)");
CHECK(ct.lyr.scrim_alpha == 176, "lyr.scrim_alpha==176");
}
static void c7_set_colors_inject(void)
{
printf("C7 set_colors 注入/回退(lyric_view 层)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, NULL, NULL);
assert(lv);
uint32_t def_cur = meow_lyric_view_cur_color(lv); /* dark=0xFFFFFF 纯白 */
/* 注入封面派生主题 */
meow_img_color_theme ct;
meow_img_color_derive(0x204060, 0xE0B050, meow_theme_get(), &ct);
meow_lyric_view_set_colors(lv, &ct.lyr);
CHECK(meow_lyric_view_cur_color(lv) == ct.lyr.cur, "注入后 cur==派生 fg(可读)");
/* gray 随覆盖 cur 自动递减(非白 cur 亦然:红色 cur → 灰阶=0xBB0000 级) */
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
c.cur = 0xFF0000;
meow_lyric_view_set_colors(lv, &c);
CHECK(meow_lyric_view_cur_color(lv) == 0xFF0000, "覆盖 cur=红生效");
CHECK(meow_lyric_view_line_color(lv, 1) == 0xBB0000, "gray(1) 随覆盖 cur 递减(0xBB0000)");
/* 回退:NULL → 主题默认 */
meow_lyric_view_set_colors(lv, NULL);
CHECK(meow_lyric_view_cur_color(lv) == def_cur, "set_colors(NULL) 回退主题默认");
meow_lyric_view_free(lv);
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/imgcolor.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
meow_ui_activity_push(0x57);
c1_solid();
c2_two_colors();
c3_skip_near();
c4_size_invariant();
c5_img_bridge();
c6_derive();
c7_set_colors_inject();
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+3 -3
View File
@@ -28,7 +28,7 @@
# ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)----
patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|8de51144538d698c3ff82ee6651b8b1a|引导选单 M6.25(开机菜单/第二屏亮度按用户设置恢复:显示前读 settings.conf 写回,方案A)
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|505f91c7a728fe1a6b1c8b7d753a471b|M6.29 播放页任意右滑返回桌面(patch4手势回退修正)
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|bd09fc3018b7825cd3dc27739eba61d5|M6.30 播放页右滑返回+批3歌词数据源+批4双页歌词+批5封面双色取色+蓝牙P1子页
patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|34ddbf0275526eaf9be988134845c3af|应用看门狗 M6.23(亮屏亮度按用户设置 display.brightness 恢复,修 panel ON 固定60跳变)
patch_v2/meow_svcd/meow_svcd|/usr/bin/meow_svcd|605646c4cc93f2baf29071ea6bc870ea|服务管理器 M2.77(+崩溃上报 meow_crash agent-187)
mips-repo/bins/meow_mdns/bin/meow_mdns|/usr/bin/meow_mdns|d6b0151bda26768f19952dae1342610d|mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194)
@@ -38,8 +38,8 @@ patch_v2/meow_svc_power/meow_svc_power|/usr/bin/meow_svc_power|39ae3aa255695a97f
patch_v2/meow_verify/meow_verify|/usr/bin/meow_verify|6c2e8ed24aebe5caf41e7aed391312d5|II期验签 2B(多公钥+v2签名块 agent-221)
patch_v2/meow_crash/meow_crash|/usr/bin/meow_crash|f02c4f45099157dc388b3d10b443aa57|系统级崩溃捕获 (agent-187) + log升级: 清理前缀扩 perf_/hang_ 承接1GB纪律 (337)
patch_v2/meow_svc_perf/meow_svc_perf|/usr/bin/meow_svc_perf|566f0615698efe32c1f90a9c62f09148|性能专项服务 M2.75 + log升级(337:卡死快照+perf基线) + OOM治理C(348:Cached/Dirty监控降载, /tmp/meow_cachegov_c.state单点)
patch_v2/meow_audiod/meow_audiod|/usr/bin/meow_audiod|b122c9b81880b00f705bca30e42f3d58|独占音频守护 M6.5(M6.0 全量+315 孤儿会话两段式回收:客户端崩溃/OOM 杀后 STATE 回干净无流基线)
patch_v2/meow_audiod/meow_audio|/usr/bin/meow_audio|891ab26475ef9e9931bf9147d265dce4|audiod 用户侧 CLI M2.97(+monitor 实时电平表/文本电平条;status 补 rms/peak)
patch_v2/meow_audiod/meow_audiod|/usr/bin/meow_audiod|37de4637a8183a672beff59ce4f3807d|独占音频守护 M6.30(M6.5 315回收+蓝牙P2 OUT_BT输出目标+libasound直连pcm.bluealsa glibc-dynamic脱离dlopen)
patch_v2/meow_audiod/meow_audio|/usr/bin/meow_audio|7a9cd32fbd6400564a7f98230f95c63d|audiod 用户侧 CLI M6.30(+蓝牙P2 audio_output/OUT_BT codec 展示;monitor 实时电平表/文本电平条;status 补 rms/peak)
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_powermgr|1ef2f8afc07acc7aa09dee9f7149aba8|功耗管理专项服务期2 M6.1(期1+history/top/events+mp2731 current_now 实测充电功率+WiFi 未关联档/BT rfkill 细化+系数路径 /data/meow/powermgr/;IPC 0xD0-D7)
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_power|1ef2f8afc07acc7aa09dee9f7149aba8|meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩)
patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_netmgrd|9c345b2e43138bc0299c749ce0f6bb5d|网络管理服务期6 M6.28(蓝牙P1 BT_CTL权威收口:hcitool con判连接/paired-devices补名/a2dp详情/connect-disconnect-pair-unpair-scan)