Files
MeowOS-For-R3P2/patch_v2/meow_audiod/meow_audiod.c
T
AskaEth 121d30ec95 fix(audiod): 孤儿会话回收 — 客户端崩溃/被杀(sig=9)/正常退出后会话完整回收,STATE 回干净无流基线
根因: cli_free(EOF)与 STOP 路径只置 active=0 + 唤醒排空,未清 sid/written/played/underrun/
rms/peak/lvlseq——ncm 被内核 OOM 杀后面板 /api/overview audio 块残留(active=0 但 sid/统计挂着)。
修法: 两段式 session_end_locked(即时置死+清 sid/app_id,保留 rate/ch/fmt 供排空算 frame_size)+
session_clean_locked(播放线程排空完成后彻底归零 rate/ch/fmt/written/played/underrun/drop/stall/
rms/peak/lvlseq)。顺带修 dac_vol_set 一处 -Wmisleading-indentation。
宿主 51/51 PASS(47 既有 + T9 SIGKILL 孤儿回收断言 STATE 全清,模拟子进程崩溃). meow_audiod b122c9b8 交叉0warning
2026-09-02 09:32:48 +08:00

1631 lines
69 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#define _GNU_SOURCE
/* meow_audiod.c — MeowOS 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md)。
*
* 定位:把**所有与声卡硬件的交互**收敛到本守护(svcd 托管,板载 /usr/bin/meow_audiod)。
* App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM;audiod 独占持有 hw:0,0。
*
* 核心设计(用户定案,勿推翻):
* - 独占 + 抢占(后开流赢,旧流收 MSG_AUDIO_PREEMPTED 并被置 draining,PCM 丢弃);
* 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。
* - 单进程双线程(+路由线程):IO 线程(accept/读帧/解析/ring 写)、播放线程
* (独占 ALSA 直开,从 ring 取数写 DAC,xrun PREPARE 恢复)、路由线程
* (轮询 headset/balance 插孔 switch 状态 → amixer cset numid=9)。
* - 格式归一 = 协商:grant_rate/ch/fmt 回客户端应产出的格式(SDK 负责 mono→stereo
* 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。
* - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。
* - meow_route 已并入(§5):本进程在=路由在;独立 meow_route 服务退役。
*
* 启动门控(红线):main() 先读 /tmp/meow_boot_mode;非 "meow" → 日志一行 → exit(0)
* (HibyOS 模式下 audiod 物理不存在,绝不碰声卡)。测试/宿主用 env MEOW_AUDIO_FORCE=1
* 跳过门控。
*
* 防卡死护栏(2026-09-01,真机长流卡死教训——驱动 wedge 时绝不允许级联冻死全守护):
* - ring 写**有界等待**(MEOW_AUDIO_RING_WAIT_MS,缺省 5000ms):超时丢弃该 PCM 块并计数
* (dropped=),IO 线程永不永久睡眠 → accept/读循环永生;
* - 所有 cond 等待均为 timedwait(1s 周期自愈,防丢失唤醒);
* - popen(amixer) 收割有界:WNOHANG + SIGKILL + 仍不死则放弃收割(宁可泄漏不卡线程);
* - ALSA 写改 O_NONBLOCK + poll(POLLOUT,1s):连续 3 次停摆判定设备 wedge → 关卡进入
* 丢弃模式(stall=),下一路 OPEN 重开重试;
* - server→client 下行帧单发 write,写不全即整帧丢弃(txdrop=),绝不因客户端不读堵 IO 循环;
* - 主线程 watchdog:IO/播放线程心跳 10s 无活动即告警日志;
* - io 线程 poll 出错只记日志不退出(listener 不丢)。
*
* 音频路径双形态:
* - 真机/ALSA:env MEOW_AUDIO_SINK 缺省自动检测 /dev/snd/pcmC0D0p ∈ 存在 → open ALSA UAPI
* (非 mmap:HW_PARAMS + PREPARE + write)。musl 的 sound/asound.h masks/intervals 是
* 重索引数组(见 audio_out.c 注释),访问须减 FIRST_MASK/FIRST_INTERVAL。
* - 无 ALSA(宿主/qemu):自动回退 FILE sink(写 WAV)或 NULL(env MEOW_AUDIO_SINK=null)。
*
* 编译(mipsel 静态):build_audiod.sh。宿主编译:build_audiod.sh host。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <poll.h>
#include <pthread.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include "meow_ipc.h"
#ifdef __linux__
#include <sound/asound.h>
#include <sys/ioctl.h>
#define AUDIOD_HAVE_ALSA 1
#endif
/* ---- 缺省路径 ---- */
#define AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock"
#define DEV_PCM "/dev/snd/pcmC0D0p"
#define LOG_DIR_CARD "/usr/data/mnt/sd_0/MeowOS/logs"
#define BOOT_MODE_PATH "/tmp/meow_boot_mode"
#define BOOTTS_CACHE "/tmp/meow_bootts"
#define SETTINGS_CONF "/usr/data/meow/settings.conf"
#define DEFAULT_OUT_WAV "/tmp/meow_audiod.wav"
#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)
#define READ_CHUNK 8192 /* 播放线程单次从 ring 取出的字节上限(帧对齐裁剪) */
#define VOL_DEFAULT 60 /* settings.conf audio.volume 缺省 */
#define RING_MIN 4096
#define RING_MAX (1u << 20)
#define RING_WAIT_DEFAULT 5000 /* ring 满时 IO 线程等待上限 ms(超时丢弃,防背压变死锁) */
#define COND_WAIT_MS 1000 /* 播放线程 cond 周期醒(自愈丢失唤醒 + 响应 g_stop) */
#define ALSA_POLL_MS 1000 /* ALSA 可写 poll 超时;连续 ALSA_STALL_MAX 次判定 wedge */
#define ALSA_STALL_MAX 3
#define WD_SILENT_SEC 10 /* watchdog:线程心跳静默上限(> RING_WAIT 余量) */
/* 客户端流状态 */
enum { CLS_OPEN = 0, CLS_PAUSED = 1, CLS_DRAINING = 2 }; /* DRAINING=被抢占,PCM 丢弃 */
/* Sink 枚举 */
enum { SINK_ALSA = 0, SINK_FILE = 1, SINK_NULL = 2 };
static int g_sink = SINK_ALSA;
static int g_hw_fmt = AUDIO_FMT_S16; /* 硬件最优格式(ALSA 探测;FILE/NULL=S16) */
static int g_hw_probed = 0;
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_force = 0; /* MEOW_AUDIO_FORCE=1 跳过启动门控(宿主/qemu 测试) */
/* ---- 缓冲状态(+日志) ---- */
static char g_logfile[300] = "";
static int g_logfd = -1;
static volatile sig_atomic_t g_stop = 0;
/* ---- 共享态(g_mu 保护 + g_cond 唤醒;g_cond 用 CLOCK_MONOTONIC,main 中初始化) ---- */
/* 前向声明(供 cli_free/dispatch/播放线程使用;定义见下) */
static void session_end_locked(void);
static void session_clean_locked(void);
static void _audio_mark_reconfig_needed(void);
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cond;
struct client {
int used;
int fd;
char app_id[MEOW_APP_ID_MAX];
uint32_t sid;
int state; /* CLS_* */
uint8_t rbuf[READ_BUF_MAX];
size_t rlen;
};
static struct client g_cli[CLI_MAX];
/* 当前授权流(喂养 DAC 的唯一流) */
struct stream_info {
int active;
uint32_t sid;
char app_id[MEOW_APP_ID_MAX];
uint32_t rate;
uint16_t ch; /* grant ch(DAC 双声道 → 2) */
uint16_t fmt; /* grant fmt */
};
static struct stream_info g_st;
static int g_paused = 0; /* 当前流被暂停 */
static uint32_t g_next_sid = 1;
static uint64_t g_written_frames = 0; /* 当前流:客户端写入 ring 的帧 */
static uint64_t g_played_frames = 0; /* 当前流:播放线程写到设备的帧 */
static uint32_t g_underrun = 0;
static uint64_t g_drop_frames = 0; /* ring 满超时丢弃的帧(背压护栏,QUERY dropped=) */
static uint32_t g_stall = 0; /* ALSA 可写 poll 超时次数(设备停摆;QUERY stall=) */
static uint32_t g_txdrop = 0; /* server→client 下行帧丢弃(客户端不读;QUERY txdrop=) */
/* ring 写等待上限(env MEOW_AUDIO_RING_WAIT_MS 覆盖;测试用小值) */
static int g_ring_wait_ms = RING_WAIT_DEFAULT;
/* 线程心跳(watchdog 用;宽松计数,不加锁) */
static volatile unsigned g_hb_io = 0;
static volatile unsigned g_hb_play = 0;
/* 测试钩子(仅 env 显式开启;MEOW_AUDIO_TEST_WRITE_DELAY_MS / _WEDGE_AFTER_BYTES) */
static int g_test_write_delay_ms = 0;
static long long g_test_wedge_after = -1; /* dev_write 累计字节≥此值后模拟设备 wedge(睡到 g_stop) */
static uint64_t g_dev_bytes = 0;
/* ring(SPSC:IO 线程写,播放线程读;g_mu + g_cond 保护) */
static uint8_t *g_ring = NULL;
static size_t g_ring_cap = 0;
static size_t g_rpos = 0, g_wpos = 0;
/* 播放线程设备状态 */
static int g_dev_fd = -1;
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;
/* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ----
* 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16),
* ~100ms 窗口(rate/10 帧)到点才结算一次 RMS/peak 并发布快照——每帧仅几次整数加乘,
* 不拖慢播放线程;无流时无数据读出即零开销。不用浮点(MIPS 软浮点昂贵)。 */
#define METER_FLOOR_DB10 (-990) /* 静音地板 -99.0 dBFS */
static uint64_t g_m_sumsq = 0; /* 窗口内样本平方和(16bit 域) */
static uint32_t g_m_peak = 0; /* 窗口峰值(0..32768) */
static uint32_t g_m_frames = 0; /* 窗口已计帧 */
static uint32_t g_m_win = 0; /* 窗口目标帧数(rate/10;0=未配置/无流) */
/* 发布快照(g_mu 保护;deci-dB,METER_FLOOR_DB10=静音) */
static int g_lvl_rms_db10 = METER_FLOOR_DB10;
static int g_lvl_peak_db10 = METER_FLOOR_DB10;
static uint32_t g_lvl_seq = 0; /* 快照代际(每次结算/开流复位递增) */
/* ---- 全面监控日志(主线程 1s 节拍写;流活跃才写,流停止即停写,无流零开销) ---- */
static int g_monfd = -1;
static uint64_t g_mon_prev_played = 0;
static int64_t g_mon_prev_ms = 0;
/* ---- 日志 ---- */
static void logmsg(const char *fmt, ...)
{
if (g_logfd < 0) return;
char b[512];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(b, sizeof b, fmt, ap);
va_end(ap);
if (n < 0) return;
if (n > (int)sizeof b - 1) n = (int)sizeof b - 1;
if (write(g_logfd, "[audiod] ", 9) < 0) { }
if (write(g_logfd, b, (size_t)n) < 0) { }
if (write(g_logfd, "\n", 1) < 0) { }
/* 同时到 stderr(supervisor/svcd 捕获 + 真机 SSH 直达) */
fprintf(stderr, "[audiod] %s\n", b);
}
/* ---- bootts 复用(与 launcher 共用 /tmp/meow_bootts 缓存,一次开机一份日志) ---- */
static void get_bootts(char *out, size_t n)
{
int fd = open(BOOTTS_CACHE, O_RDONLY);
if (fd >= 0) {
int r = read(fd, out, (int)n - 1);
close(fd);
if (r > 0) { out[r] = 0; return; }
}
time_t now = time(NULL), up = 0;
fd = open("/proc/uptime", O_RDONLY);
if (fd >= 0) {
char buf[64];
int r = read(fd, buf, sizeof buf - 1);
close(fd);
if (r > 0) { buf[r] = 0; char *p = strchr(buf, ' '); if (p) *p = 0; up = (time_t)atof(buf); }
}
struct tm tm;
localtime_r(&now - up, &tm);
strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm);
fd = open(BOOTTS_CACHE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { if (write(fd, out, strlen(out)) < 0) { } close(fd); }
}
static void log_open(void)
{
char ts[64];
get_bootts(ts, sizeof ts);
if (access(LOG_DIR_CARD, W_OK) == 0)
snprintf(g_logfile, sizeof g_logfile, "%s/%s.log", LOG_DIR_CARD, ts);
else
snprintf(g_logfile, sizeof g_logfile, "/tmp/%s.log", ts);
g_logfd = open(g_logfile, O_WRONLY | O_CREAT | O_APPEND, 0644);
}
/* ---- 启动门控 ---- */
static int boot_mode_ok(void)
{
if (g_force) return 1;
FILE *f = fopen(BOOT_MODE_PATH, "r");
if (!f) return 0; /* 文件缺失=非 meow(未知安全默认) */
char buf[16] = "";
if (fscanf(f, "%15s", buf) != 1) { fclose(f); return 0; }
fclose(f);
return strcmp(buf, "meow") == 0;
}
/* ---- 帧发送(仅 IO 线程用:OPEN_ACK/STATE/PREEMPTED/ACK) ---- */
/* 单发即丢:客户端 fd 是 O_NONBLOCK;拼装整帧一次 write(),写不全(对端不读/缓冲满)
* 就整帧丢弃并计数——绝不为慢客户端堵 IO 循环,也绝不写出半个帧让对端协议错位。
* 丢 OPEN_ACK/ACK 的后果 = 客户端 3s 超时判流死重连(可接受);丢 PREEMPTED 由 App 侧
* 播放停摆自检兜底。 */
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
{
if (len > MEOW_IPC_MAX) return -1;
uint8_t frame[MEOW_IPC_HDR_LEN + MEOW_IPC_MAX];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(frame + 0, &magic, 4);
memcpy(frame + 4, &ver, 2);
memcpy(frame + 6, &type, 2);
memcpy(frame + 8, &req_id, 4);
memcpy(frame + 12, &len, 4);
if (len && payload) memcpy(frame + MEOW_IPC_HDR_LEN, payload, len);
size_t total = MEOW_IPC_HDR_LEN + len;
ssize_t w = write(fd, frame, total);
if (w != (ssize_t)total) {
g_txdrop++;
if ((g_txdrop & 63u) == 1u)
logmsg("tx drop: fd=%d type=0x%04x len=%u wrote=%zd (total x%u, client not reading?)",
fd, (unsigned)type, (unsigned)len, w, (unsigned)g_txdrop);
return -1;
}
return 0;
}
/* cond 有界等待(g_cond 为 CLOCK_MONOTONIC;调用方已持 g_mu)。 */
static void cond_wait_tmo(int ms)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
ts.tv_sec += ms / 1000;
ts.tv_nsec += (long)(ms % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
pthread_cond_timedwait(&g_cond, &g_mu, &ts);
}
/* ---- frame 大小/位深 ---- */
static int fmt_bps(int fmt)
{
switch (fmt) {
case AUDIO_FMT_S16: return 2;
case AUDIO_FMT_S24:
case AUDIO_FMT_S32:
case AUDIO_FMT_FLOAT: return 4;
default: return 2;
}
}
static const char *fmt_name(int fmt)
{
switch (fmt) {
case AUDIO_FMT_S16: return "S16";
case AUDIO_FMT_S24: return "S24";
case AUDIO_FMT_S32: return "S32";
case AUDIO_FMT_FLOAT: return "FLOAT";
default: return "?";
}
}
static int frame_size(void) { return g_st.ch * fmt_bps(g_st.fmt); }
/* 整数开平方(uint64→uint32,逐位法;替代软浮点 sqrt) */
static uint32_t isqrt64(uint64_t x)
{
uint64_t r = 0, bit = 1ULL << 62;
while (bit > x) bit >>= 2;
while (bit) {
if (x >= r + bit) { x -= r + bit; r = (r >> 1) + bit; }
else r >>= 1;
bit >>= 2;
}
return (uint32_t)r;
}
/* 线性幅度(16bit 域 0..32768)→ deci-dBFS(×10 整数,相对满幅 32768)。
* log2 = 整数位 + octave 内线性插值;最大误差 ~0.5dB,电平表/日志足够。 */
static int db10_of_lin(uint32_t v)
{
if (v == 0) return METER_FLOOR_DB10;
if (v > 32768) v = 32768;
int b = 0;
uint32_t t = v;
while (t >>= 1) b++; /* b = floor(log2(v)) ∈ 0..15 */
uint32_t frac = v - (1u << b);
/* 20*log10(2)*10 = 60.206 */
int64_t db10 = (int64_t)60206 * (b - 15) / 1000;
db10 += (int64_t)60206 * (int64_t)frac / ((int64_t)1000 << b);
if (db10 < METER_FLOOR_DB10) db10 = METER_FLOOR_DB10;
return (int)db10;
}
/* 窗口结算 + 发布快照(调用方已持 g_mu) */
static void meter_window_end(void)
{
uint64_t samples = (uint64_t)g_m_frames * (g_st.ch ? g_st.ch : 2);
uint32_t rms = samples ? isqrt64(g_m_sumsq / samples) : 0;
g_lvl_rms_db10 = db10_of_lin(rms);
g_lvl_peak_db10 = db10_of_lin(g_m_peak);
g_lvl_seq++;
g_m_sumsq = 0; g_m_peak = 0; g_m_frames = 0;
}
/* 喂一块 PCM 进电平表(调用方已持 g_mu,播放线程 ring_read 后紧跟调用)。
* 逐样本整数累加;**逐帧判窗口边界**(帧循环里一次加+比较,开销可忽略)——
* 若按块末尾才结算,块越过窗口边界的部分会混进旧窗口(宿主流失真正弦+静音
* 混合窗口实测 rms 偏差 7dB+)。fmt 为 grant 格式;S24/S32 归一 >>16。 */
static void meter_feed(const uint8_t *buf, size_t n)
{
if (!g_m_win) return;
int bps = fmt_bps(g_st.fmt);
int ch = g_st.ch ? (int)g_st.ch : 2;
size_t fs = (size_t)ch * (size_t)bps;
if (fs == 0) return;
size_t frames = n / fs;
for (size_t i = 0; i < frames; i++) {
const uint8_t *p = buf + i * fs;
for (int c = 0; c < ch; c++, p += bps) {
int32_t v;
if (bps == 2) { int16_t s; memcpy(&s, p, 2); v = s; }
else { int32_t s; memcpy(&s, p, 4); v = s >> 16; }
uint32_t a = (v < 0) ? (uint32_t)(-(int64_t)v) : (uint32_t)v;
g_m_sumsq += (uint64_t)a * a;
if (a > g_m_peak) g_m_peak = a;
}
if (++g_m_frames >= g_m_win) meter_window_end();
}
}
static size_t ring_bytes(void) /* 已缓冲字节 */
{
return (g_wpos >= g_rpos) ? (g_wpos - g_rpos) : (g_ring_cap - g_rpos + g_wpos);
}
/* 播放线程读整块(调用方已持 g_mu;返回实际移动字节,帧对齐裁剪) */
static size_t ring_read_locked(uint8_t *dst, size_t want)
{
size_t avail = ring_bytes();
size_t fs = (size_t)frame_size();
if (fs == 0) fs = 1;
if (avail > want) avail = want;
size_t n = (avail / fs) * fs; /* 帧对齐 */
if (n == 0) return 0;
size_t first = g_ring_cap - g_rpos;
size_t take = n < first ? n : first;
memcpy(dst, g_ring + g_rpos, take);
if (take < n) memcpy(dst + take, g_ring, n - take);
g_rpos = (g_rpos + n) % g_ring_cap;
pthread_cond_signal(&g_cond); /* 腾出空间唤醒阻塞的 ring 写入方(IO 线程) */
return n;
}
/* IO 线程写整块(调用方已持 g_mu)。**有界等待**:ring 满最多等 g_ring_wait_ms,
* 超时丢弃该块(返回 0)——背压绝不能退化成 IO 线程永久睡眠(真机长流卡死根因:
* 播放线程堵死 → ring 满 → IO cond_wait 永不醒 → accept 停摆 → 全链冻结)。
* sid 为写入方开流时的代际:等待期间被抢占(g_st.sid 变化)立即丢弃(返回 0),
* 防止旧流 PCM 注入新流 ring(ring_rebuild 后错位)。
* 返回实际写入字节数;0=被丢弃(超时/流死/被抢占)。 */
static size_t ring_write_locked(const uint8_t *src, size_t want, uint32_t sid)
{
size_t fs = (size_t)frame_size();
if (fs == 0) fs = 1;
size_t n = (want / fs) * fs;
if (n == 0) return 0;
if (!g_ring || g_ring_cap < n) return 0; /* ring 未建/异常小:直接丢 */
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += g_ring_wait_ms / 1000;
deadline.tv_nsec += (long)(g_ring_wait_ms % 1000) * 1000000L;
if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; }
for (;;) {
if (g_stop) return 0;
/* 流失效/代际变化:非当前授权、被抢占、被停止 → 丢弃 */
if (!g_st.active || g_st.sid != sid)
return 0;
size_t freeb = g_ring_cap - ring_bytes();
if (freeb >= n) break;
int wr = pthread_cond_timedwait(&g_cond, &g_mu, &deadline);
if (wr == ETIMEDOUT) {
g_drop_frames += n / fs;
static unsigned s_droplog = 0;
if ((s_droplog++ & 63u) == 0)
logmsg("ring full >%dms: dropped %zu bytes (total dropped=%llu frames, playback stuck?)",
g_ring_wait_ms, n, (unsigned long long)g_drop_frames);
return 0;
}
}
size_t wpos = g_wpos;
size_t first = g_ring_cap - wpos;
size_t take = n < first ? n : first;
memcpy(g_ring + wpos, src, take);
if (take < n) memcpy(g_ring, src + take, n - take);
g_wpos = (g_wpos + n) % g_ring_cap;
pthread_cond_signal(&g_cond);
return n;
}
/* 重建 ring(调用方已持 g_mu;按当前授权流参数算 ~200ms 容量;重建=清空旧流缓冲) */
static void ring_rebuild(void)
{
size_t fs = (size_t)frame_size();
if (fs == 0) fs = 1;
size_t want = (size_t)((uint64_t)g_st.rate * fs / 5); /* 0.2s */
if (want < RING_MIN) want = RING_MIN;
if (want > RING_MAX) want = RING_MAX;
size_t cap = (want / fs) * fs;
if (cap < fs) cap = fs;
free(g_ring);
g_ring = malloc(cap);
g_ring_cap = g_ring ? cap : 0;
if (!g_ring) g_ring_cap = 0;
g_rpos = g_wpos = 0;
logmsg("ring rebuilt: cap=%zu bytes (~%u ms @ %uHz %s x%u)",
g_ring_cap, (unsigned)(g_ring_cap ? g_ring_cap * 1000 / ((uint64_t)g_st.rate * fs) : 0),
(unsigned)g_st.rate, fmt_name(g_st.fmt), (unsigned)g_st.ch);
}
/* ---- amixer 封装(popen 限时;无 amixer → 优雅失败) ---- */
/* 读子进程 stdout(非阻塞+poll 限时)。收割**必须有界**:驱动 wedge 时 amixer 可能
* 挂在内核 ioctl 里,无条件 waitpid 会把调用线程(路由线程持 g_mu / IO 线程)一起拖死
* ——真机长流卡死的级联路径之一。策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms;
* 仍不死(D 态)→ 放弃收割(泄漏一个子进程,换线程不卡死;已记日志)。 */
static char *popen_read_tmo(int tmo_ms, const char *cmd)
{
int p[2];
if (pipe(p) < 0) return NULL;
pid_t pid = fork();
if (pid < 0) { close(p[0]); close(p[1]); return NULL; }
if (pid == 0) { dup2(p[1], 1); close(p[0]); close(p[1]); execl("/bin/sh", "sh", "-c", cmd, (char *)0); _exit(127); }
close(p[1]);
fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK);
static char buf[4096];
size_t off = 0;
int remain = tmo_ms;
while (remain > 0 && off < sizeof buf - 1) {
struct pollfd pf = { p[0], POLLIN, 0 };
int r = poll(&pf, 1, remain);
if (r < 0) { if (errno == EINTR) continue; break; }
if (r == 0) break;
ssize_t rd = read(p[0], buf + off, sizeof buf - 1 - off);
if (rd == 0) break;
if (rd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; break; }
off += (size_t)rd;
}
close(p[0]);
int reaped = 0;
for (int i = 0; i < 30; i++) { /* 300ms 优雅退出窗口 */
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; }
struct timespec ts = { 0, 10 * 1000000L };
nanosleep(&ts, NULL);
}
if (!reaped) {
logmsg("popen: child %ld did not exit in 300ms, SIGKILL (%s)", (long)pid, cmd);
kill(pid, SIGKILL);
for (int i = 0; i < 30; i++) {
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; }
struct timespec ts = { 0, 10 * 1000000L };
nanosleep(&ts, NULL);
}
if (!reaped)
logmsg("popen: child %ld unkillable (D-state, driver wedged?) — abandon reap", (long)pid);
}
buf[off] = 0;
return buf;
}
static int amixer_set(int numid, int val, int card)
{
char cmd[160];
snprintf(cmd, sizeof cmd, "%s%s cset numid=%d %d >/dev/null 2>&1",
g_amixer, (card ? " -c 0" : ""), numid, val);
char *o = popen_read_tmo(1500, cmd);
if (!o) return -1;
/* popen_read_tmo 用的是静态缓冲,这里不 free(与 meow_route.c 同权衡) */
return 0;
}
static int amixer_get(int numid, int card)
{
char cmd[160];
snprintf(cmd, sizeof cmd, "%s%s cget numid=%d 2>/dev/null", g_amixer, (card ? " -c 0" : ""), numid);
char *o = popen_read_tmo(1500, cmd);
if (!o) return -1;
const char *ln = o;
while (ln && *ln) {
const char *nl = strchr(ln, '\n');
const char *p = ln;
while (*p == ' ' || *p == '\t') p++;
if (p[0] == ':' && strncmp(p, ": values=", 9) == 0) { int v = atoi(p + 9); return v; }
if (!nl) break;
ln = nl + 1;
}
return -1;
}
static void dac_vol_set(int pct)
{
int t = 100 - pct, dac;
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
if (dac > 255) dac = 255;
if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
amixer_set(1, dac, 1);
amixer_set(2, dac, 1);
logmsg("DAC vol -> %d%% (L/R=%d)", pct, dac);
}
static void dac_vol_mute(void)
{
/* CS43198 寄存器=衰减量(0=最响 255=静音)。⚠️ 写 0 是「最大响度」不是静音——
* 旧版写 0 在闪退/重配瞬间造成满幅炸音(真机事故 2026-09-01)。真静音写 255。 */
amixer_set(1, 255, 1);
amixer_set(2, 255, 1);
}
static const char *port_name(int port)
{
switch (port) {
case 2: return "3.5 headset";
case 3: return "4.4 balance";
case 4: return "i2s/spdif digital";
case 5: return "none(disable)";
default: return "idle";
}
}
/* 应用路由(numid=9)。port 0=idle;写后 cget 校验,失败记日志不重试风暴。
* g_route_cur 为 plain int 松散写(路由线程不持 g_mu 调本函数——amixer popen 虽已有界,
* 仍不该在持锁状态执行外部命令;int 写对 MIPS32 天然原子,竞争最坏是 QUERY 读到上一拍值)。 */
static void apply_route(int port)
{
if (amixer_set(9, port, 1) < 0) {
logmsg("route: amixer cset numid=9 %d exec failed", port);
return;
}
int rb = amixer_get(9, 1);
if (rb == port) logmsg("route: numid=9 -> %d (%s) ok", port, port_name(port));
else logmsg("route: verify failed (wanted %d got %d)", port, rb);
g_route_cur = port;
}
/* 读 switch 文件(0/1 插孔态,-1 读失败) */
static int read_switch(const char *path)
{
char buf[32];
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) return -1;
buf[n] = 0;
return atoi(buf);
}
/* 路由评估(移植自 meow_route.c):4.4 优先;都不插不动;写失败不重试风暴。
* manual:调用方快照的 g_route_manual(本函数不持 g_mu、不读全局)。 */
static void route_evaluate(int manual, int *last_hb, int *last_bal, int *last_target)
{
if (manual != -1) return; /* 手动模式:不自动切 */
int hb = read_switch("/sys/class/switch/headset/state");
int bal = read_switch("/sys/class/switch/balance/state");
if (hb < 0 || bal < 0) return;
int prev_hb = *last_hb, prev_bal = *last_bal;
*last_hb = hb; *last_bal = bal;
int target;
if (bal == 1) target = 3; /* 4.4 优先 */
else if (hb == 1) target = 2;
else target = 0; /* 都不插:保持上次(0=不动?) 用 -1 表示不动 */
if (bal == 0 && hb == 0) {
if (prev_hb != hb || prev_bal != bal)
logmsg("route: no jack (hs=%d bal=%d), keep last port", hb, bal);
*last_target = -1;
return;
}
int changed = (prev_hb != hb || prev_bal != bal);
if (!changed && *last_target == target) return;
int cur = amixer_get(9, 1);
if (cur == target) { *last_target = target; if (changed) logmsg("route: %s already", port_name(target)); return; }
logmsg("route: jack changed -> %s (wanted %d, was %d)", port_name(target), target, cur);
apply_route(target);
*last_target = target;
}
/* ---- settings.conf 写 audio.volume(同 launcher 同源同键) ---- */
static void settings_set_volume(int pct)
{
FILE *f = fopen(SETTINGS_CONF, "r");
char *data = NULL;
size_t len = 0;
if (f) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
if (sz > 0 && sz < 64 * 1024) {
fseek(f, 0, SEEK_SET);
data = malloc((size_t)sz + 1);
if (data) { len = fread(data, 1, (size_t)sz, f); data[len] = 0; }
}
fclose(f);
}
char val[16];
snprintf(val, sizeof val, "%d", pct);
char *out = malloc(len + 64);
if (!out) { free(data); return; }
out[0] = 0;
size_t o = 0;
int replaced = 0;
if (data) {
char *save = NULL;
char *ln = strtok_r(data, "\n", &save);
while (ln) {
char *k = ln;
while (*k == ' ' || *k == '\t') k++;
int m = strncmp(k, "audio.volume", 12);
if (m != 0) {
int n = snprintf(out + o, len + 64 - o, "%s\n", ln);
if (n > 0) o += (size_t)n;
} else {
int t = snprintf(out + o, len + 64 - o, "audio.volume = %s\n", val);
if (t > 0) o += (size_t)t;
replaced = 1;
}
ln = strtok_r(NULL, "\n", &save);
}
free(data);
}
if (!replaced) {
int n = snprintf(out + o, len + 64 - o, "audio.volume = %s\n", val);
if (n > 0) o += (size_t)n;
}
mkdir("/usr/data/meow", 0755);
int fd = open(SETTINGS_CONF, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
/* 若原文件带 "# meow-settings 1" 头则保留——我们已被动保留了内容(含头)。
* 无头(新文件)补一行头,符合 meow_settings 的解析预期。 */
if (!replaced && o > 0) {
/* 检查首行是否已含头 */
int has_hdr = (strncmp(out, "# meow-settings", 15) == 0);
if (!has_hdr) {
char tmp[64];
int hn = snprintf(tmp, sizeof tmp, "# meow-settings 1\n");
if (write(fd, tmp, (size_t)hn) < 0) { }
}
}
if (write(fd, out, o) < 0) { }
close(fd);
}
free(out);
logmsg("audio.volume persisted -> %d (%s)", pct, SETTINGS_CONF);
}
static int settings_get_volume(void)
{
FILE *f = fopen(SETTINGS_CONF, "r");
if (!f) return g_vol_pct;
char line[128];
int v = g_vol_pct;
while (fgets(line, sizeof line, f)) {
char *k = line;
while (*k == ' ' || *k == '\t') k++;
if (strncmp(k, "audio.volume", 12) == 0) {
char *eq = strchr(k, '=');
if (eq) { v = atoi(eq + 1); break; }
}
}
fclose(f);
return v;
}
/* ---- ALSA UAPI ---- */
#ifdef AUDIOD_HAVE_ALSA
static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); }
static void pivset(struct snd_interval *iv, unsigned val)
{
iv->min = iv->max = val;
iv->integer = 1;
iv->openmin = iv->openmax = 0;
iv->empty = 0;
}
/* 初始化为「全放行」(libasound snd_pcm_hw_params_any 语义):所有 mask 全 1、所有 interval
* [0, UINT_MAX]。**必须**:memset 0 的 interval(min=max=0)等价于「只能取 0」,内核 refine
* 与设备真实约束求交必为空 → HW_PARAMS 恒 EINVAL(真机踩过,agent-244 probe_alsa 全灭定位)。 */
static void alsa_params_any(struct snd_pcm_hw_params *p)
{
memset(p, 0, sizeof *p);
unsigned nm = SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1;
for (unsigned i = 0; i < nm; i++)
for (unsigned b = 0; b < sizeof p->masks[i].bits / sizeof p->masks[i].bits[0]; b++)
p->masks[i].bits[b] = ~0u;
unsigned ni = SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1;
for (unsigned i = 0; i < ni; i++) {
p->intervals[i].min = 0;
p->intervals[i].max = ~0u;
}
}
static int alsa_hw_params(int fd, unsigned rate, unsigned ch, unsigned fmt)
{
struct snd_pcm_hw_params p;
alsa_params_any(&p);
p.flags = 0;
/* mask 必须「先清零再单点」:params_any 已把 mask 置全 1,pmask 只 OR 不清位——
* 若 access 留全 1,内核 choose 取最低 bit=MMAP_INTERLEAVED,runtime->access≠RW_INTERLEAVED
* → 之后 write() 恒 EINVAL(真机踩过第二坑;libasound 靠 plug 层自动转 mmap 掩盖了它)。 */
struct snd_mask *am = &p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(am, 0, sizeof *am); pmask(am, SNDRV_PCM_ACCESS_RW_INTERLEAVED);
struct snd_mask *fm = &p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(fm, 0, sizeof *fm); pmask(fm, fmt);
struct snd_mask *sm = &p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(sm, 0, sizeof *sm); pmask(sm, SNDRV_PCM_SUBFORMAT_STD);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], ch);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], rate);
/* M2.94 长流停摆根治(真机实证 60s 零 stall):显式收窄周期几何到 tiny_alsa 惯例
* period=1024 frames × 4。否则驱动默认 buffer 巨大(上限 1048576 帧 ≈23s),
* hbc3000 桥靠周期中断驱动,中断稀缺 → 桥时钟域 wedge → 播 12s 左右停摆。
* (tonepcm2 几何矩阵实测:1024×4 连续 60s 实时消费零 stall。) */
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_PERIOD_SIZE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], 1024);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_PERIODS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], 4);
p.rmask = ~0u; p.info = 0; p.msbits = 0; p.rate_num = 0; p.rate_den = 0;
return ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p);
}
#endif
static int alsa_fmt_to_snd(int fmt)
{
switch (fmt) {
case AUDIO_FMT_S16: return SNDRV_PCM_FORMAT_S16_LE;
case AUDIO_FMT_S24: return SNDRV_PCM_FORMAT_S24_LE;
case AUDIO_FMT_S32: return SNDRV_PCM_FORMAT_S32_LE;
case AUDIO_FMT_FLOAT: return SNDRV_PCM_FORMAT_FLOAT_LE;
default: return SNDRV_PCM_FORMAT_S16_LE;
}
}
/* 探测硬件最优格式(设备空闲时调用;缓存 g_hw_fmt)。返回 0=确定,-1=无法探测。 */
static int hw_probe(void)
{
if (g_sink != SINK_ALSA) { g_hw_fmt = AUDIO_FMT_S16; g_hw_probed = 1; return 0; }
if (g_hw_probed) return 0;
#ifdef AUDIOD_HAVE_ALSA
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd >= 0) {
if (alsa_hw_params(fd, 44100, 2, SNDRV_PCM_FORMAT_S16_LE) == 0) { g_hw_fmt = AUDIO_FMT_S16; close(fd); g_hw_probed = 1; return 0; }
close(fd);
}
fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd >= 0) {
if (alsa_hw_params(fd, 44100, 2, SNDRV_PCM_FORMAT_S32_LE) == 0) { g_hw_fmt = AUDIO_FMT_S32; close(fd); g_hw_probed = 1; return 0; }
close(fd);
}
#endif
g_hw_fmt = AUDIO_FMT_S16;
g_hw_probed = 1;
return -1;
}
/* ---- FILE/NULL sink ---- */
static void wav_header(int fd, int rate, int ch, int bits, uint32_t data_len)
{
unsigned char h[44];
memset(h, 0, sizeof h);
memcpy(h + 0, "RIFF", 4);
uint32_t le = 36 + data_len; memcpy(h + 4, &le, 4);
memcpy(h + 8, "WAVE", 4);
memcpy(h + 12, "fmt ", 4);
le = 16; memcpy(h + 16, &le, 4);
uint16_t s16 = 1; memcpy(h + 20, &s16, 2);
s16 = (uint16_t)ch; memcpy(h + 22, &s16, 2);
le = (uint32_t)rate; memcpy(h + 24, &le, 4);
le = (uint32_t)(rate * ch * bits / 8); memcpy(h + 28, &le, 4);
s16 = (uint16_t)(ch * bits / 8); memcpy(h + 32, &s16, 2);
s16 = (uint16_t)bits; memcpy(h + 34, &s16, 2);
memcpy(h + 36, "data", 4);
le = data_len; memcpy(h + 40, &le, 4);
if (write(fd, h, 44) != 44) { }
}
/* ---- 播放线程设备管理(dev_open/close/write 皆在播放线程调用) ---- */
static void dev_close(void); /* 前向:dev_write 的 wedge 判死路径要用 */
static int dev_open(void)
{
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) {
#ifdef AUDIOD_HAVE_ALSA
/* O_NONBLOCK:写路径走 poll(POLLOUT)+限时(dev_write),write 本身永不睡眠——
* 驱动 wedge 时绝不能把播放线程永久压进内核(真机长流卡死根因防线)。 */
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd < 0) { logmsg("dev open %s: %s", DEV_PCM, strerror(errno)); return -1; }
if (alsa_hw_params(fd, rate, ch, alsa_fmt_to_snd((int)fmt)) != 0) {
logmsg("dev hw_params %u/%u/%s failed", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt));
close(fd);
return -1;
}
ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0);
return fd;
#else
return -1;
#endif
}
if (g_sink == SINK_FILE) {
int fd = open(g_out_wav, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { logmsg("dev open wav %s: %s", g_out_wav, strerror(errno)); return -1; }
wav_header(fd, (int)rate, (int)ch, bits, 0);
return fd;
}
return -2; /* NULL sink */
}
static void dev_write(uint8_t *buf, size_t n)
{
/* 测试钩子(仅 env 开启):写延迟(模拟慢设备/实时 pacing)与 wedge(模拟驱动卡死——
* 睡到 g_stop,复现真机「播放线程永不返回」故障注入,验证 IO 线程/ring 不死锁)。 */
if (g_test_write_delay_ms > 0) {
struct timespec ts = { g_test_write_delay_ms / 1000, (long)(g_test_write_delay_ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
if (g_test_wedge_after >= 0) {
if (g_dev_bytes >= (uint64_t)g_test_wedge_after) {
static int s_wedged = 0;
if (!s_wedged) { s_wedged = 1; logmsg("TEST: device wedged (simulated after %llu bytes)", (unsigned long long)g_dev_bytes); }
while (!g_stop) {
struct timespec ts = { 0, 100 * 1000000L };
nanosleep(&ts, NULL);
}
return;
}
g_dev_bytes += n;
}
if (g_sink == SINK_NULL) { g_played_frames += n / (size_t)frame_size(); return; }
if (g_sink == SINK_ALSA) {
#ifdef AUDIOD_HAVE_ALSA
if (g_dev_fd < 0) {
/* 设备开失败(hw_params 拒/无卡/wedge 后被判死):丢弃 PCM 防 ring 堵死 + 背压卡死客户端;
* 日志限频(每 256 次一记),避免 write(-1) 错误风暴(真机踩过)。 */
static unsigned s_drop = 0;
if ((s_drop++ % 256) == 0)
logmsg("dev_write dropped %zu bytes (device not open, x%u)", n, s_drop);
g_played_frames += n / (size_t)frame_size();
return;
}
/* O_NONBLOCK + poll(POLLOUT) 模式:写前等可写(上限 ALSA_POLL_MS)。
* 连续 ALSA_STALL_MAX 次超时 = 设备停摆(驱动 wedge 特征)→ PREPARE 自救仍无效
* 则关卡进入丢弃模式(g_dev_open=0),下一路 OPEN 重开重试——播放线程绝不永久
* 堵在内核 write 里(真机长流卡死的级联源头)。 */
int stalls = 0;
size_t off = 0;
while (off < n) {
if (g_stop) break;
struct pollfd pf;
pf.fd = g_dev_fd; pf.events = POLLOUT; pf.revents = 0;
int pr = poll(&pf, 1, ALSA_POLL_MS);
if (pr == 0) {
g_stall++;
stalls++;
logmsg("alsa stall #%u (poll %dms no space, xrun-ish) -> PREPARE",
(unsigned)g_stall, ALSA_POLL_MS);
ioctl(g_dev_fd, SNDRV_PCM_IOCTL_PREPARE, 0);
if (stalls >= ALSA_STALL_MAX) {
logmsg("alsa WEDGED: %d consecutive stalls, close device + drop PCM until next OPEN", stalls);
dev_close();
return;
}
continue;
}
if (pr < 0) { if (errno == EINTR) continue; logmsg("alsa poll err: %s", strerror(errno)); break; }
ssize_t w = write(g_dev_fd, buf + off, n - off);
if (w > 0) { off += (size_t)w; stalls = 0; continue; }
if (w < 0) {
if (errno == EINTR || errno == EAGAIN) continue;
if (errno == EPIPE) { /* xrun:重备 + 恢复(设计 §2) */
g_underrun++;
logmsg("xrun #%u -> PREPARE", (unsigned)g_underrun);
ioctl(g_dev_fd, SNDRV_PCM_IOCTL_PREPARE, 0);
continue;
}
logmsg("alsa write err: %s", strerror(errno));
break;
}
break;
}
size_t done = (off / (size_t)frame_size()) * (size_t)frame_size();
g_played_frames += done / (size_t)frame_size();
#endif
return;
}
if (g_sink == SINK_FILE) {
if (write(g_dev_fd, buf, n) > 0)
g_played_frames += n / (size_t)frame_size();
}
}
static void dev_close(void)
{
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) {
uint32_t data_len = (uint32_t)(end - 44);
lseek(g_dev_fd, 4, SEEK_SET); /* RIFF 总长(36 + data) */
uint32_t le = 36 + data_len; if (write(g_dev_fd, &le, 4) != 4) { }
lseek(g_dev_fd, 40, SEEK_SET); /* data 块长(rate/ch/bits 头已由 wav_header 写入) */
if (write(g_dev_fd, &data_len, 4) != 4) { }
}
close(g_dev_fd);
} else if (g_dev_fd >= 0 && g_dev_fd != -2) {
close(g_dev_fd);
}
g_dev_fd = -1;
g_dev_open = 0;
}
/* 播放线程:处理一次重配(静音 → 关旧 → 开新 → 复原)。调用方已持 g_mu。 */
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));
int saved_vol = g_vol_pct;
if (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);
} else {
logmsg("device open FAILED !!");
}
g_reconfig_pending = 0;
}
/* 播放线程主体。
* 语义:只有当前授权流(g_st.active)喂 DAC;暂停时不读 ring(保留已缓冲);
* 停止/EOF 时不丢尾——先排空 ring 再关设备(否则 tone 末尾/缓冲尾被截断)。
* 所有等待为 COND_WAIT_MS 周期 timedwait:丢失唤醒最多迟 1s 自愈,SIGTERM 也能及时退。 */
static void *playback_thread(void *arg)
{
(void)arg;
static uint8_t buf[READ_CHUNK];
for (;;) {
g_hb_play++;
int have = 0;
pthread_mutex_lock(&g_mu);
for (;;) {
g_hb_play++;
if (g_stop) { have = -1; break; }
int fs = frame_size();
if (fs <= 0) { cond_wait_tmo(COND_WAIT_MS); continue; } /* 无流 param → 等 */
if (g_reconfig_pending) { have = 1; break; } /* 重配 */
int draining = !g_st.active;
int paused = g_st.active && g_paused;
size_t av = ring_bytes();
if ((draining && av < (size_t)fs) || paused) {
/* 排空完毕(或暂停):若设备仍开且已排空 → 关;否则等待 */
if (draining && g_dev_open) dev_close();
if (draining) session_clean_locked(); /* 流已死且排空完成:会话统计彻底归零 */
cond_wait_tmo(COND_WAIT_MS);
continue;
}
if (draining || av >= (size_t)fs) { have = 2; break; } /* 有数据可读(或排空态) */
cond_wait_tmo(COND_WAIT_MS);
}
if (have == -1) { pthread_mutex_unlock(&g_mu); break; }
if (have == 1) { reconfigure_locked(); pthread_mutex_unlock(&g_mu); continue; }
if (have == 0) { if (g_dev_open) dev_close(); pthread_mutex_unlock(&g_mu); continue; }
size_t want = READ_CHUNK;
size_t fs = (size_t)frame_size();
if (fs > 0) want = (want / fs) * fs;
size_t n = ring_read_locked(buf, want);
if (n) meter_feed(buf, n); /* 电平表:整数累加,窗口到点才结算 */
pthread_mutex_unlock(&g_mu);
if (n == 0) continue;
dev_write(buf, n);
}
pthread_mutex_lock(&g_mu);
if (g_dev_open) dev_close();
pthread_mutex_unlock(&g_mu);
return NULL;
}
/* ---- 客户端管理 ---- */
static struct client *cli_by_fd(int fd)
{
for (int i = 0; i < CLI_MAX; i++)
if (g_cli[i].used && g_cli[i].fd == fd) return &g_cli[i];
return NULL;
}
static struct client *cli_alloc(int fd)
{
struct client *freec = NULL;
for (int i = 0; i < CLI_MAX; i++) {
if (g_cli[i].used) continue;
freec = &g_cli[i];
break;
}
if (!freec) return NULL;
memset(freec, 0, sizeof *freec);
freec->used = 1;
freec->fd = fd;
freec->state = CLS_OPEN;
freec->sid = 0;
return freec;
}
static void cli_free(struct client *c)
{
if (!c || !c->used) return;
if (c->fd >= 0) close(c->fd);
c->used = 0;
c->fd = -1;
if (g_st.active && g_st.sid == c->sid) {
logmsg("client EOF: stream %u recycled (app=%s)", (unsigned)c->sid, g_st.app_id);
session_end_locked();
}
}
/* ---- 流结束/会话回收(客户端死亡或显式 STOP 后,STATE 须回到干净无流基线) ----
* 两段式:
* - session_end_locked():流结束标记(停止/EOF 即时调用)。置 inactive + 清会话身份
* (sid/app_id)+ 唤醒播放线程排空 ring。**不清空 ring、不清 rate/ch/fmt**:未播完的缓冲
* 要写完(排空)才关设备,否则 tone/播放末尾被截断;排空期间 frame_size() 仍依赖 rate/ch/fmt。
* ring 的重置只发生在 OPEN 授予新流时(ring_rebuild)。
* - session_clean_locked():会话统计彻底清零(流已结束且 ring 排空完毕的迟后调用)——
* STATE 归零:rate/ch/fmt=0、written=played=0、underrun/drop/stall=0、rms/peak=-inf、lvlseq=0。
* 此刻 rate/ch/fmt 已不再被排空引用,可安全清零。txdrop 为全局客户端级(不随流清),不动。 */
static void session_end_locked(void)
{
g_st.active = 0;
g_st.sid = 0;
g_st.app_id[0] = 0;
g_paused = 0;
g_reconfig_pending = 0;
pthread_cond_signal(&g_cond);
}
static void session_clean_locked(void)
{
g_st.rate = 0;
g_st.ch = 0;
g_st.fmt = 0;
g_written_frames = 0;
g_played_frames = 0;
g_underrun = 0;
g_drop_frames = 0;
g_stall = 0;
g_m_sumsq = 0;
g_m_peak = 0;
g_m_frames = 0;
g_m_win = 0;
g_lvl_rms_db10 = g_lvl_peak_db10 = METER_FLOOR_DB10;
g_lvl_seq = 0;
}
/* 处理一个客户端帧(调用方已持 g_mu) */
static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uint8_t *pl, uint32_t len)
{
switch (type) {
case MSG_AUDIO_OPEN: {
if (len < sizeof(m_audio_open_t)) { m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_INVAL; send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a); return; }
m_audio_open_t o;
memcpy(&o, pl, sizeof o);
if (o.ch > 2 || o.fmt < AUDIO_FMT_S16 || o.fmt > AUDIO_FMT_FLOAT || o.fmt == AUDIO_FMT_FLOAT || o.rate == 0) {
m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_INVAL;
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
logmsg("open rejected: ch=%u fmt=%d rate=%u", (unsigned)o.ch, o.fmt, (unsigned)o.rate);
return;
}
hw_probe();
if (g_sink == SINK_ALSA && !g_hw_probed) {
m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_NODEV;
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
logmsg("open rejected: no usable PCM device");
return;
}
int grant_fmt = (g_sink == SINK_ALSA) ? g_hw_fmt : AUDIO_FMT_S16;
uint32_t sid = g_next_sid++;
/* 抢占:已有当前流 → 旧客户端收 PREEMPTED */
if (g_st.active) {
struct client *old = NULL;
for (int i = 0; i < CLI_MAX; i++)
if (g_cli[i].used && g_cli[i].sid == g_st.sid) { old = &g_cli[i]; break; }
if (old && old != c) {
m_audio_preempted_t p;
memset(&p, 0, sizeof p);
p.stream_id = g_st.sid;
snprintf(p.by_app, sizeof p.by_app, "%s", o.app_id);
send_frame(old->fd, MSG_AUDIO_PREEMPTED, 0, &p, sizeof p);
old->state = CLS_DRAINING;
logmsg("PREEMPTED stream %u by %s", (unsigned)g_st.sid, o.app_id);
}
}
/* 授权新流 */
c->sid = sid;
c->state = CLS_OPEN;
snprintf(c->app_id, sizeof c->app_id, "%s", o.app_id);
g_st.active = 1;
g_st.sid = sid;
snprintf(g_st.app_id, sizeof g_st.app_id, "%s", o.app_id);
g_st.rate = o.rate;
g_st.ch = 2; /* DAC 双声道:mCh→stereo 由 SDK 复制 */
g_st.fmt = (uint16_t)grant_fmt;
g_paused = 0;
g_written_frames = g_played_frames = 0;
g_underrun = 0;
g_drop_frames = 0; /* 每流计数(txdrop 为全局客户端级,不随流清) */
g_stall = 0;
g_dev_bytes = 0; /* 测试钩子计数随新流归零 */
/* 电平表复位 + 窗口目标(~100ms) */
g_m_win = g_st.rate / 10;
if (!g_m_win) g_m_win = 1;
g_m_sumsq = 0; g_m_peak = 0; g_m_frames = 0;
g_lvl_rms_db10 = g_lvl_peak_db10 = METER_FLOOR_DB10;
g_lvl_seq++;
/* 重建 ring + 标记重配 */
ring_rebuild();
_audio_mark_reconfig_needed();
m_audio_open_ack_t a;
memset(&a, 0, sizeof a);
a.stream_id = sid;
a.grant_rate = o.rate;
a.grant_ch = g_st.ch;
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));
break;
}
case MSG_AUDIO_WRITE: {
/* 仅当前授权流、非 draining、非 paused 才入 ring;否则丢弃 */
if (g_st.active && c->sid == g_st.sid && c->state != CLS_DRAINING && !g_paused && c->state != CLS_PAUSED) {
size_t wn = ring_write_locked(pl, len, g_st.sid);
int fs = frame_size();
if (wn && fs > 0)
g_written_frames += wn / (uint32_t)fs;
}
break;
}
case MSG_AUDIO_PAUSE:
case MSG_AUDIO_RESUME:
case MSG_AUDIO_STOP: {
uint32_t sid = 0;
if (len >= sizeof(uint32_t)) memcpy(&sid, pl, sizeof sid);
if (sid == g_st.sid && g_st.active) {
if (type == MSG_AUDIO_PAUSE) { g_paused = 1; c->state = CLS_PAUSED; logmsg("pause sid=%u", (unsigned)sid); }
else if (type == MSG_AUDIO_RESUME){ g_paused = 0; c->state = CLS_OPEN; logmsg("resume sid=%u", (unsigned)sid); }
/* PAUSE/RESUME 必须唤醒播放线程(它可能正 timedwait 在 paused 分支;
* 旧代码 RESUME 不 signal,恢复后要等下一块 WRITE 才醒) */
if (type != MSG_AUDIO_STOP)
pthread_cond_signal(&g_cond);
else /* STOP */ {
logmsg("STOP sid=%u (app=%s)", (unsigned)sid, g_st.app_id);
c->sid = 0;
c->state = CLS_OPEN;
session_end_locked();
}
} else if (type == MSG_AUDIO_STOP) {
/* 非当前/已 draining 流:仅清客户端绑定 */
if (c->sid == sid) { c->sid = 0; }
}
m_ack_t a; a.code = MEOW_OK; a.status = 0;
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
break;
}
case MSG_AUDIO_QUERY: {
char txt[1024];
/* 路由口用缓存值 g_route_cur(路由线程维护),不在 IO 热路径 popen amixer——
* 驱动 wedge 时 amixer 虽有界但仍会拖慢 IO 循环 1.5s/次。 */
int rb = g_route_cur;
int vol = g_vol_pct;
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",
g_st.active ? 1 : 0,
g_st.active ? g_st.app_id : "-",
(unsigned)g_st.sid, (unsigned)g_st.rate, (unsigned)g_st.ch,
g_st.active ? fmt_name((int)g_st.fmt) : "-",
(unsigned long long)g_written_frames,
(unsigned long long)g_played_frames,
(unsigned)g_underrun,
(unsigned long long)g_drop_frames,
(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);
send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1);
break;
}
case MSG_AUDIO_ROUTE: {
uint32_t port = 0;
if (len >= sizeof(uint32_t)) memcpy(&port, pl, sizeof port);
if (port == 0) { g_route_manual = -1; logmsg("route manual -> auto"); }
else if (port == 2 || port == 3) { g_route_manual = (int)port; apply_route((int)port); }
else { logmsg("route: invalid port %u", (unsigned)port); }
m_ack_t a; a.code = MEOW_OK; a.status = 0;
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
break;
}
case MSG_AUDIO_VOLUME: {
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);
} else {
g_vol_pct = settings_get_volume();
}
m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_vol_pct;
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
break;
}
default:
break;
}
}
/* 需要时标着重配:参数与当前设备不同才重开 */
static void _audio_mark_reconfig_needed(void)
{
if (!g_dev_open || g_dev_rate != g_st.rate || g_dev_ch != g_st.ch || g_dev_fmt != g_st.fmt)
g_reconfig_pending = 1;
pthread_cond_signal(&g_cond);
}
/* 读客户端帧(调用方已持 g_mu)。读失败(EOF/错)→ 回收。 */
static void cx_read_frames(struct client *c)
{
ssize_t r = read(c->fd, c->rbuf + c->rlen, sizeof c->rbuf - c->rlen);
if (r <= 0) {
if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return;
cli_free(c);
return;
}
c->rlen += (size_t)r;
/* 处理所有完整帧 */
while (c->rlen >= MEOW_IPC_HDR_LEN) {
uint32_t magic, len;
uint16_t ver, type;
memcpy(&magic, c->rbuf, 4);
memcpy(&ver, c->rbuf + 4, 2);
memcpy(&type, c->rbuf + 6, 2);
memcpy(&len, c->rbuf + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) {
logmsg("bad frame from client (magic=%08x ver=%u) -> drop conn", (unsigned)magic, (unsigned)ver);
cli_free(c);
return;
}
if (len > MEOW_IPC_MAX) {
logmsg("frame too long (%u) -> drop conn", (unsigned)len);
cli_free(c);
return;
}
size_t total = MEOW_IPC_HDR_LEN + len;
if (c->rlen < total) break;
uint32_t req_id;
memcpy(&req_id, c->rbuf + 8, 4);
dispatch(c, type, req_id, c->rbuf + MEOW_IPC_HDR_LEN, len);
memmove(c->rbuf, c->rbuf + total, c->rlen - total);
c->rlen -= total;
}
}
/* ---- IO 线程主体 ---- */
static void *io_thread(void *arg)
{
(void)arg;
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd < 0) { logmsg("io: socket: %s", strerror(errno)); return NULL; }
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", g_sock);
unlink(g_sock);
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) {
logmsg("io: bind %s: %s", g_sock, strerror(errno));
close(lfd);
return NULL;
}
chmod(g_sock, 0666);
if (listen(lfd, 8) != 0) { logmsg("io: listen: %s", strerror(errno)); close(lfd); return NULL; }
logmsg("io thread listening on %s", g_sock);
for (;;) {
g_hb_io++;
if (g_stop) break;
struct pollfd pfds[CLI_MAX + 1];
int n = 1;
pfds[0].fd = lfd; pfds[0].events = POLLIN; pfds[0].revents = 0;
for (int i = 0; i < CLI_MAX; i++) {
if (!g_cli[i].used) continue;
pfds[n].fd = g_cli[i].fd; pfds[n].events = POLLIN; pfds[n].revents = 0;
n++;
}
int pr = poll(pfds, (nfds_t)n, 200);
if (pr < 0) {
if (errno == EINTR) continue;
/* poll 出错绝不退出线程(listener 一丢就是全守护失联,CLI ECONNREFUSED);
* 记日志(限频)+ 短睡后继续。 */
static unsigned s_pollerr = 0;
if ((s_pollerr++ & 15u) == 0) logmsg("io: poll err: %s (x%u, keep going)", strerror(errno), s_pollerr);
struct timespec ts = { 0, 100 * 1000000L };
nanosleep(&ts, NULL);
continue;
}
/* pfds 是本轮新数组(revents 本轮清零),直接以 revents 为准(238 教训:每圈重建,
* 杜绝陈旧 revents。勿在此清零)。 */
struct pollfd snaps[CLI_MAX + 1];
memcpy(snaps, pfds, sizeof snaps);
pthread_mutex_lock(&g_mu);
(void)pr;
/* 处理 listen */
if ((snaps[0].revents & POLLIN)) {
int cfd = accept(lfd, NULL, NULL);
if (cfd >= 0) {
fcntl(cfd, F_SETFL, fcntl(cfd, F_GETFL, 0) | O_NONBLOCK);
int one = 1;
setsockopt(cfd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one);
struct client *c = cli_alloc(cfd);
if (!c) { close(cfd); logmsg("io: client limit reached, drop"); }
else logmsg("io: client connected (fd=%d)", cfd);
}
}
/* 处理客户读数(用本轮 revents 快照,避免连接被 cli_free 后误用) */
for (int i = 1; i < n; i++) {
if (snaps[i].revents == 0) continue;
struct client *c = cli_by_fd(snaps[i].fd);
if (!c) continue;
if (snaps[i].revents & (POLLHUP | POLLERR | POLLNVAL)) { cli_free(c); continue; }
if (snaps[i].revents & POLLIN) cx_read_frames(c);
}
pthread_mutex_unlock(&g_mu);
/* 路由线程与播放线程独立;此处不再做 */
}
pthread_mutex_lock(&g_mu);
for (int i = 0; i < CLI_MAX; i++) if (g_cli[i].used) cli_free(&g_cli[i]);
pthread_mutex_unlock(&g_mu);
close(lfd);
return NULL;
}
/* ---- 路由线程主体 ---- */
static void *route_thread(void *arg)
{
(void)arg;
int last_hb = -1, last_bal = -1, last_target = -1;
logmsg("route thread init (numid=9, %s)", g_amixer);
/* M2.91 挂死根治(AUDIO_HANG_INVESTIGATION.md):hbc3000 FPGA 未烧录+enable 时
* 启动 PCM 会在第二次 hbc3000_start 死锁整机。写 Output Port Switch 是烧录触发器
* (驱动 put:set_spdif_mode→program_ram→enabled)。开机端口恒 0=未烧,故启动时
* 无条件写一次:4.4 在→3,3.5 在→2,都没→4(官方播放器拔插孔后的选择,保 FPGA 已烧)。
* 端口已是目标值(audiod 重启场景,驱动同值短路不写)→ 先写 5(disable) 强制重烧。 */
{
int hb = read_switch("/sys/class/switch/headset/state");
int bal = read_switch("/sys/class/switch/balance/state");
int target = (bal == 1) ? 3 : (hb == 1) ? 2 : 4;
int cur = amixer_get(9, 1);
if (cur == target) { logmsg("route: port already %d, force re-arm via 5", target); apply_route(5); }
apply_route(target);
last_hb = hb; last_bal = bal; last_target = (hb == 0 && bal == 0) ? -1 : target;
logmsg("route: boot arm done (port %d %s, FPGA programmed)", target, port_name(target));
}
while (!g_stop) {
pthread_mutex_lock(&g_mu);
int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */
pthread_mutex_unlock(&g_mu);
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 };
nanosleep(&ts, NULL);
}
}
return NULL;
}
/* ---- 全面监控日志(§8 扩展:流活跃时每 1s 一行;主线程节拍驱动,不碰播放线程) ---- */
/* deci-dB → "-13.1" 文本(静音地板显 "-inf") */
static void fmt_db10(char *out, size_t n, int v)
{
if (v <= METER_FLOOR_DB10) { snprintf(out, n, "-inf"); return; }
int neg = v < 0;
int a = neg ? -v : v;
snprintf(out, n, "%s%d.%d", neg ? "-" : "", a / 10, a % 10);
}
static void mon_close(void)
{
if (g_monfd >= 0) { close(g_monfd); g_monfd = -1; }
}
/* 主线程 1s 节拍调用。流活跃 → 懒打开 TF 卡 MeowOS/logs/audio_monitor_<bootts>.log
* (无卡降级 /tmp)并写一行(时间戳+app+参数+rms/peak dBFS+帧计数+underrun/stall/
* dropped/txdrop+路由口+FPGA 状态+音量%+hw 消费速率帧/s);流停止即停写关文件。
* 快照先取后写:文件 IO 不持 g_mu。 */
static void mon_tick(void)
{
pthread_mutex_lock(&g_mu);
int active = g_st.active;
char app[MEOW_APP_ID_MAX]; snprintf(app, sizeof app, "%s", g_st.app_id);
uint32_t sid = g_st.sid, rate = g_st.rate;
unsigned ch = g_st.ch, fmt = g_st.fmt;
uint64_t wr = g_written_frames, pl = g_played_frames, dr = g_drop_frames;
unsigned ur = g_underrun, st = g_stall, tx = g_txdrop;
int paused = g_paused, vol = g_vol_pct, rmanual = g_route_manual, rcur = g_route_cur;
int rms = g_lvl_rms_db10, pk = g_lvl_peak_db10;
pthread_mutex_unlock(&g_mu);
if (!active) { /* 流停止即停写 */
if (g_monfd >= 0) {
mon_close();
logmsg("monitor log closed (stream ended)");
}
g_mon_prev_ms = 0;
return;
}
struct timespec tp;
clock_gettime(CLOCK_MONOTONIC, &tp);
int64_t now_ms = (int64_t)tp.tv_sec * 1000 + tp.tv_nsec / 1000000;
if (g_monfd < 0) { /* 懒打开(流开始才建文件) */
char ts[64];
get_bootts(ts, sizeof ts);
char path[368];
if (access(LOG_DIR_CARD, W_OK) == 0)
snprintf(path, sizeof path, "%s/audio_monitor_%s.log", LOG_DIR_CARD, ts);
else
snprintf(path, sizeof path, "/tmp/audio_monitor_%s.log", ts);
g_monfd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (g_monfd < 0) return; /* 打不开就静默降级(下拍再试) */
logmsg("monitor log -> %s", path);
const char *hdr = "# meow_audiod monitor: time | app sid rate/ch/fmt | rms/peak dBFS | written played underrun stall dropped txdrop | route fpga vol hwrate\n";
if (write(g_monfd, hdr, strlen(hdr)) < 0) { }
g_mon_prev_played = pl;
g_mon_prev_ms = now_ms;
}
int64_t dt = now_ms - g_mon_prev_ms;
uint64_t hwrate = (dt > 0 && pl >= g_mon_prev_played)
? (pl - g_mon_prev_played) * 1000 / (uint64_t)dt : 0;
g_mon_prev_played = pl;
g_mon_prev_ms = now_ms;
struct tm tm;
time_t now = time(NULL);
localtime_r(&now, &tm);
char tbuf[32];
strftime(tbuf, sizeof tbuf, "%Y-%m-%d %H:%M:%S", &tm);
char rmsb[16], pkb[16];
fmt_db10(rmsb, sizeof rmsb, rms);
fmt_db10(pkb, sizeof pkb, pk);
int port = (rmanual != -1) ? rmanual : rcur;
char line[512];
int n = snprintf(line, sizeof line,
"%s app=%s sid=%u %uHz/%uch/%s rms=%sdBFS peak=%sdBFS "
"written=%llu played=%llu underrun=%u stall=%u dropped=%llu txdrop=%u "
"route=%d(%s) fpga=%s vol=%d%% hwrate=%lluf/s%s\n",
tbuf, app, (unsigned)sid, (unsigned)rate, ch, fmt_name((int)fmt),
rmsb, pkb,
(unsigned long long)wr, (unsigned long long)pl,
ur, st, (unsigned long long)dr, tx,
port, port_name(port),
rcur >= 0 ? "armed" : "unknown",
vol, (unsigned long long)hwrate,
paused ? " paused" : "");
if (n > 0 && write(g_monfd, line, (size_t)n) < 0) { }
}
static void on_sig(int sig)
{
(void)sig;
/* 只置标志(async-signal-safe):所有等待均为有界 timedwait(播放 1s / IO poll 200ms /
* 路由 200ms 分段 / ring 写 ≤g_ring_wait_ms),线程自行看到 g_stop 退出——
* 不在信号处理器里调 pthread_cond_signal(非 async-signal-safe)。 */
g_stop = 1;
}
int main(int argc, char **argv)
{
const char *e;
/* 参数/env 覆盖 */
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--sock") && i + 1 < argc) snprintf(g_sock, sizeof g_sock, "%s", argv[++i]);
else if (!strcmp(argv[i], "--out") && i + 1 < argc) snprintf(g_out_wav, sizeof g_out_wav, "%s", argv[++i]);
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_RING_WAIT_MS (ring 满写等待上限, 缺省 5000)\n"
" MEOW_AUDIO_TEST_WRITE_DELAY_MS / MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES (故障注入, 仅测试)\n");
return 0;
}
}
if ((e = getenv("MEOW_AUDIO_SOCK")) && e[0]) snprintf(g_sock, sizeof g_sock, "%s", e);
if ((e = getenv("MEOW_AUDIO_OUT")) && e[0]) snprintf(g_out_wav, sizeof g_out_wav, "%s", e);
if ((e = getenv("MEOW_AUDIO_AMIXER")) && e[0]) g_amixer = e;
if ((e = getenv("MEOW_AUDIO_FORCE")) && e[0] == '1') g_force = 1;
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);
const char *sink = getenv("MEOW_AUDIO_SINK");
if (sink && sink[0]) {
if (!strcmp(sink, "file")) g_sink = SINK_FILE;
else if (!strcmp(sink, "null")) g_sink = SINK_NULL;
else if (!strcmp(sink, "alsa")) g_sink = SINK_ALSA;
} else {
/* 自动:/dev/snd/pcmC0D0p 存在 → ALSA;否则 FILE */
if (access(DEV_PCM, F_OK) == 0) g_sink = SINK_ALSA;
else g_sink = SINK_FILE;
}
signal(SIGPIPE, SIG_IGN);
signal(SIGTERM, on_sig);
signal(SIGINT, on_sig);
/* --- 启动门控(红线) --- */
if (!boot_mode_ok()) {
fprintf(stderr, "[audiod] not MeowOS mode (/tmp/meow_boot_mode != meow), exit\n");
return 0;
}
log_open();
logmsg("meow_audiod start: sock=%s sink=%d wav=%s force=%d ring_wait=%dms%s%s",
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();
g_vol_pct = settings_get_volume();
logmsg("volume init = %d%%", g_vol_pct);
/* g_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
{
pthread_condattr_t ca;
pthread_condattr_init(&ca);
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
pthread_cond_init(&g_cond, &ca);
pthread_condattr_destroy(&ca);
}
pthread_t ti, tp, tr;
pthread_create(&ti, NULL, io_thread, NULL);
pthread_create(&tp, NULL, playback_thread, NULL);
pthread_create(&tr, NULL, route_thread, NULL);
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
unsigned last_io = 0, last_play = 0;
int io_silent = 0, play_silent = 0;
while (!g_stop) {
struct timespec ts = { 1, 0 };
nanosleep(&ts, NULL);
if (g_stop) break;
unsigned io = g_hb_io, pl = g_hb_play;
io_silent = (io == last_io) ? io_silent + 1 : 0; last_io = io;
play_silent = (pl == last_play) ? play_silent + 1 : 0; last_play = pl;
if (io_silent == WD_SILENT_SEC || (io_silent > WD_SILENT_SEC && io_silent % 30 == 0))
logmsg("WATCHDOG: io thread silent %ds (accept/read loop stuck?)", io_silent);
if (play_silent == WD_SILENT_SEC || (play_silent > WD_SILENT_SEC && play_silent % 30 == 0)) {
pthread_mutex_lock(&g_mu);
int act = g_st.active;
uint64_t wr = g_written_frames, plf = g_played_frames;
pthread_mutex_unlock(&g_mu);
logmsg("WATCHDOG: playback thread silent %ds (active=%d written=%llu played=%llu, device wedged?)",
play_silent, act, (unsigned long long)wr, (unsigned long long)plf);
}
mon_tick(); /* 流活跃才写监控日志(无流零开销) */
}
logmsg("meow_audiod shutdown");
mon_close();
pthread_join(ti, NULL);
pthread_join(tp, NULL);
pthread_join(tr, NULL);
unlink(g_sock);
if (g_logfd >= 0) close(g_logfd);
return 0;
}