Files
MeowOS-For-R3P2/patch_v2/meow_audiod/meow_audiod.c
T

2185 lines
98 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"
#include "bt_dev.h" /* P2:DEV 自识别(a2dpsrc 端点 → MAC → alsa.conf 幂等改写) */
#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"
/* ---- 蓝牙 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)
#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;
/* 输出目标(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(缓存:DAC 音量;BT 时 volctl=avrcp) */
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 / _AMIXER_TRACE) */
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;
static char g_amixer_trace_path[256] = ""; /* MEOW_AUDIO_AMIXER_TRACE:记录每次 amixer cset */
/* 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_dev_dsd = 0; /* 当前打开设备为 DoP DSD(决定 DOP_EN 硬件时序;g_mu 保护) */
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 并发布快照——每帧仅几次整数加乘,
* 不拖慢播放线程;无流时无数据读出即零开销。不用浮点(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:
case AUDIO_FMT_DOP: return 4; /* DOP=32-bit DoP 容器(16bit DSD + 8bit 标记 + 8bit 0),与 S32 同宽 */
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";
case AUDIO_FMT_DOP: return "DoP";
default: return "?";
}
}
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";
}
/* 授权给客户端的硬件最优格式。native_fmt=客户端请求格式。BT(A2DP source)=S16(codec 基座,
* pcm.bluealsa 前置 plug 层自动转 codec 协商率);DAC=hw 探测最优(S32 升位/S16 直通);
* FILE/NULL 宿主 sink:S24/S32 按请求**byte-exact 透传**(高清实测保真),S16 缺省,DOP 恒透传。 */
static int target_grant_fmt(int native_fmt)
{
if (native_fmt == AUDIO_FMT_DOP) return AUDIO_FMT_DOP; /* DoP 恒透传(audiod 只做管道) */
if (g_target == AUDIO_OUT_BT) return AUDIO_FMT_S16;
if (g_sink == SINK_ALSA) return g_hw_fmt; /* DAC 探测最优(S32 偏好) */
if (native_fmt == AUDIO_FMT_S24 || native_fmt == AUDIO_FMT_S32)
return native_fmt; /* FILE/NULL:高保真记录 */
return 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)
{
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;
/* 测试钩子(仅 env MEOW_AUDIO_AMIXER_TRACE=<path>):把每次 amixer cset 追加到文件,
* 供宿主断言「音量双组写」PCM+DSD_VOLUME 是否成对下发(真机无 amixer 时不写/不报错)。 */
if (g_amixer_trace_path[0]) {
int tfd = open(g_amixer_trace_path, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (tfd >= 0) {
char tb[64];
int tn = snprintf(tb, sizeof tb, "cset numid=%d %d\n", numid, val);
if (tn > 0) { if (write(tfd, tb, (size_t)tn) < 0) { } }
close(tfd);
}
}
/* 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;
}
/* ---- DSD(DoP)相关常量 + 状态 ----
* ⚠️ 真机核实项(AUDIOD_A3_DSD_PLAN §五,未做):DSD_VOLUME_A/B 寄存器地址/control、DoP 标记字节
* (0x05/0xFA vs 0xA5/0x5A)。下述 numid 为设计定案占位:DOP_EN=numid 10(plan §三「cset numid=10 1」),
* DSD_VOLUME 单 control 占位 numid 11(真实为 A/B 两路或 write_reg_val,待 DS1039 核实)。
* 未核实前 DSD_VOLUME 写为 best-effort(amixer 出错仅日志,绝不掩盖;正确地址真机定后再上)。 */
#define DOP_EN_NUMID 10
#define DSD_VOL_NUMID 11
static int g_dsd_active = 0; /* 当前授权流为 DoP DSD(g_mu 保护;决定 DOP_EN=1 与 DSD 音量作用域) */
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静音)+平方锥度(听感线性) */
/* 音量收编 v2:**泛化双组写**——同刻写 PCM(numid=1/2) 与 DSD_VOLUME,保证 DSD↔PCM 切换
* 音量连续 + 防爆音闭环(决策①,勿只写 PCM)。锥度沿用平方(决策②)。 */
amixer_set(1, dac, 1);
amixer_set(2, dac, 1);
amixer_set(DSD_VOL_NUMID, dac, 1);
logmsg("DAC/DSD vol -> %d%% (PCM L/R=%d, DSD=%d)", pct, dac, 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);
amixer_set(DSD_VOL_NUMID, 255, 1); /* DSD 同步真静音(防爆音闭环:DSD 也 255) */
}
/* ---- DSD(DoP)管道:audiod 只做 DOP_EN 时序 + DSD 音量 + 透传;不引 dsd2pcm(许可未核实,避风险)。
* 流级判定 vs 设备级判定分拆:
* - g_dsd_active:当前授权流为 DoP DSD(dispatch 授 DOP 时置位,session_clean 复位)——供 QUERY/monitor。
* - g_dev_dsd:当前**打开的设备**为 DSD(reconfigure/dev_open 后置位)——决定 DOP_EN 硬件时序。
* 进入 DSD(plan §三时序)由播放线程 reconfigure 执行:mute(PCM+DSD) → DOP_EN=1 → hw_params(容器率)
* → dac_vol_set(DSD_VOLUME) → unmute。本函数只做「流级可入性」判定:BT 拒(A2DP 不能承载 DoP),
* 非 ALSA(FILE/NULL)=纯透传管道允许。失败回退=报错关流(案 A:DSD 打不开/不支持→报错并关闭,绝不静默退化)。 */
static int dsd_enter(void)
{
if (g_target == AUDIO_OUT_BT) {
logmsg("dsd: enter rejected (BT/A2DP cannot carry DoP) -> close stream");
return -1;
}
if (g_sink != SINK_ALSA) {
logmsg("dsd: enter ok (sink=%d passthrough, no DOP_EN)", g_sink);
g_dsd_active = 1;
return 0;
}
g_dsd_active = 1; /* 流级置位;DOP_EN 硬件时序由播放线程 reconfigure 执行 */
logmsg("dsd: stream enters DoP DSD (DOP_EN sequenced on device reconfigure)");
return 0;
}
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(g_settings_path, "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(g_settings_path, 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, 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(g_settings_path, "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;
}
/* ============================================================================
* 蓝牙 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);
}
/* P2 BT DEV 自识别:把 /usr/data/alsa.conf 的 `pcm.bluealsa` DEV/device 自动改为**当前连接的
* A2DP sink 设备** MAC(bluealsa-cli list-pcms 的 a2dpsrc 端点)。幂等(设备未变不重写)、
* 无设备/失败跳过;只改 DEV/device 两处、保留 codec/其余 token。BT 打开前调用,实现
* 「连哪个设备就播哪个」。跟 bt_apply_codec_conf 同构(外科式 token 替换,绝不建结构)。 */
static void bt_apply_device_conf(void)
{
char mac[32] = "";
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
if (!o) { logmsg("bt: dev conf skipped (no bluealsa-cli)"); return; }
if (bt_dev_detect_mac(o, mac, sizeof mac) != 0) {
logmsg("bt: dev conf skipped (no a2dpsrc pcm)");
return;
}
logmsg("bt: current a2dp dev %s", mac);
const char *path = "/usr/data/alsa.conf";
FILE *f = fopen(path, "r");
if (!f) return;
fseek(f, 0, SEEK_END); long sz = ftell(f);
if (sz <= 0 || sz > 64 * 1024) { fclose(f); return; }
fseek(f, 0, SEEK_SET);
char *data = malloc((size_t)sz + 1);
if (!data) { fclose(f); return; }
size_t rl = fread(data, 1, (size_t)sz, f); data[rl] = 0;
fclose(f);
char *outdata = NULL; size_t outlen = 0;
int rc = bt_dev_rewrite(data, rl, mac, &outdata, &outlen);
free(data);
if (rc == 0) {
int fd = open(path, O_WRONLY | O_TRUNC, 0644);
if (fd >= 0) {
if (write(fd, outdata, outlen) < 0) { }
close(fd);
logmsg("bt: dev conf auto-updated %s -> %s", path, mac);
} else {
logmsg("bt: dev conf %s not writable, skip", path);
}
} else if (rc == 1) {
logmsg("bt: dev conf ok (already %s)", mac);
} else {
logmsg("bt: dev conf skip (no pcm.bluealsa/no DEV token)");
}
if (outdata) free(outdata);
}
/* 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); }
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_DOP: return SNDRV_PCM_FORMAT_S32_LE; /* DoP 走 32-bit 容器(16bit DSD + 标记 + 0) */
case AUDIO_FMT_FLOAT: return SNDRV_PCM_FORMAT_FLOAT_LE;
default: return SNDRV_PCM_FORMAT_S16_LE;
}
}
/* 探测硬件最优格式(设备空闲时调用;缓存 g_hw_fmt)。返回 0=确定,-1=无法探测。
* A3(24/32bit 高清流):**偏好 S32**——CS43198 为 32bit DAC,S32 可顺带把 16bit 内容做无损升位、
* 24/32bit 直通不降位(plan §一「S16 直通/S32 升位」)。按高清率优先探测 S32(192k→176.4k→44.1k),
* 全失败再退 S16。旧实现只在 44.1k 试 S16 并**立即返回**(永远拿不到 S32,高清降位)。
* ⚠️ 探测只 open+hw_params(非 start),与 FPGA 武装无关;真机此路径仍需 AUDIO 实测复核。 */
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
static const unsigned rates[3] = { 192000u, 176400u, 44100u };
for (int r = 0; r < 3 && !g_hw_probed; r++) {
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd >= 0) {
if (alsa_hw_params(fd, rates[r], 2, SNDRV_PCM_FORMAT_S32_LE) == 0)
{ g_hw_fmt = AUDIO_FMT_S32; g_hw_probed = 1; }
close(fd);
}
}
for (int r = 0; r < 3 && !g_hw_probed; r++) {
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd >= 0) {
if (alsa_hw_params(fd, rates[r], 2, SNDRV_PCM_FORMAT_S16_LE) == 0)
{ g_hw_fmt = AUDIO_FMT_S16; g_hw_probed = 1; }
close(fd);
}
}
#endif
if (!g_hw_probed) { g_hw_fmt = AUDIO_FMT_S16; g_hw_probed = 1; }
logmsg("hw probe: optimal fmt=%s", fmt_name(g_hw_fmt));
return 0;
}
/* ---- 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)
{
/* 输出目标=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_device_conf(); /* P2:DEV 幂等自更新到当前连接 A2DP 设备(连哪台播哪台) */
bt_apply_codec_conf();
if (bt_open() == 0) return 0; /* g_bt_open=1;返回 0=逻辑已开(真实句柄在 g_bt_hdl) */
logmsg("bt: open failed (degrade: drop PCM until next OPEN)");
return -1;
}
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_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
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_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) {
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。
* DSD(audiod 只透传):DoP 时序「mute(PCM+DSD) → DOP_EN=1 → hw_params(容器率) → DSD_VOLUME → unmute」。
* DOP_EN 必须早于 dev_open 的 hw_params(plan §三),离开 DSD 时 DOP_EN=0 须早于关设备。 */
static void reconfigure_locked(void)
{
uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt;
const char *tgt = (g_target == AUDIO_OUT_BT) ? "bt" : "dac";
logmsg("reconfigure device: %u/%u/%s target=%s", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt), tgt);
int saved_vol = g_vol_pct;
int new_dsd = (fmt == AUDIO_FMT_DOP);
int do_dsd_seq = (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA); /* DOP_EN 仅 DAC 物理路径 */
if (do_dsd_seq) {
if (new_dsd) {
dac_vol_mute(); /* ① mute(PCM+DSD 双组) */
/* ② DOP_EN 写后校验:**DSD 打不开/不支持→报错关流(案 A,不静默退化)**——DOP_EN 未确认
* 为 1 则丢弃本流(active=0→播放线程不再喂 DAC),客户端经 STATE active=0 感知并重开/换目标。 */
if (amixer_set(DOP_EN_NUMID, 1, 1) < 0 || amixer_get(DOP_EN_NUMID, 1) != 1) {
logmsg("dsd: DOP_EN (numid=%d) not confirmed -> close stream (no silent degrade)", DOP_EN_NUMID);
dac_vol_mute();
amixer_set(DOP_EN_NUMID, 0, 1); /* 确保离开 DOP(兼容旧 DSD 设备残留) */
session_end_locked();
g_dev_dsd = 0;
g_reconfig_pending = 0;
return;
}
logmsg("dsd: DOP_EN -> 1 (DoP container)");
} else if (g_dev_dsd) {
logmsg("dsd: DOP_EN -> 0 (leave DoP)"); /* 离开 DSD:DOP_EN=0 后关设备 */
amixer_set(DOP_EN_NUMID, 0, 1);
}
}
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 */
g_dev_dsd = (do_dsd_seq && new_dsd);
if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原(含 DSD_VOLUME) */
logmsg("device open ok: %u/%u/%s sink=%d target=%s%s", (unsigned)rate, (unsigned)ch,
fmt_name((int)fmt), g_sink, tgt, g_dev_dsd ? " dsd" : "");
} else {
logmsg("device open FAILED !!");
g_dev_dsd = 0;
}
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 && g_dev_dsd && g_sink == SINK_ALSA && g_target == AUDIO_OUT_DAC) {
logmsg("dsd: DOP_EN -> 0 (stream drained)");
amixer_set(DOP_EN_NUMID, 0, 1);
g_dev_dsd = 0;
}
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_dsd_active = 0; /* 流级 DSD 位归零(DOP_EN 设备位由 drain/reconfigure 时序负责) */
g_dev_dsd = 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);
/* 合法 fmt:S16/S24/S32/DOP;FLOAT 拒(v1 未支持)。DOP 须带 AUDIO_FLAG_DSD(DoP 独立格式值)。 */
int fmt_inval = (o.fmt != AUDIO_FMT_S16 && o.fmt != AUDIO_FMT_S24 &&
o.fmt != AUDIO_FMT_S32 && o.fmt != AUDIO_FMT_DOP);
if (o.ch > 2 || o.ch < 1 || fmt_inval || (o.fmt == AUDIO_FMT_DOP && !(o.flags & AUDIO_FLAG_DSD)) || 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 flags=0x%x", (unsigned)o.ch, o.fmt, (unsigned)o.rate, (unsigned)o.flags);
return;
}
/* DSD 可入性判定(BT 拒 / FILE-NULL 透传允许);失败=报错关流(案 A,绝不静默退化) */
if (o.fmt == AUDIO_FMT_DOP && dsd_enter() != 0) {
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: DoP DSD not supported (target=%s sink=%d)", target_name(), g_sink);
return;
}
/* 输出目标=BT:不依赖 DAC hw 探测(BT 走 pcm.bluealsa,与 hw:0,0/FGA numid=9 解耦)——
* 探测只对 DAC 有意义;BT 打开失败在播放线程降级丢帧(plan §五自感知兜底)。 */
if (g_target == AUDIO_OUT_DAC) {
hw_probe();
if (g_sink == SINK_ALSA && !g_hw_probed) {
m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_NODEV;
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
logmsg("open rejected: no usable PCM device (DAC)");
return;
}
}
int grant_fmt = target_grant_fmt(o.fmt); /* DAC=hw 最优;BT=S16(plug 层转 codec 协商率);FILE/NULL=保真透传 */
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;DoP DSD 无 PCM 意义,窗口=0 即跳过——meter_feed early-return) */
g_m_win = (grant_fmt == AUDIO_FMT_DOP) ? 0 : (g_st.rate / 10);
if (!g_m_win && grant_fmt != AUDIO_FMT_DOP) 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 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: {
/* 仅当前授权流、非 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 dsd=%d "
"written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d "
"route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u "
"target=%s codec=%s\n",
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) : "-",
g_dsd_active ? 1 : 0,
(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, 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;
}
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) {
/* 音量应用含 BT 隔离:target=BT 时绝不下发 DAC 反转(vol_apply 内判断) */
vol_apply((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;
}
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;
}
}
/* 需要时标着重配:参数与当前设备不同才重开 */
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:一次 poll 收 1 个连接。**不可改用「while accept 排空积压」**——在连接
* 风暴下(对端持续 connect)accept 永不 EAGAIN → 持 g_mu 无限循环 → IO 线程卡死、
* 播放/路由/QUERY 全部被 g_mu 阻塞(宿主 T10 实测:drain 版风暴下 probe/OPEN 全 FAIL)。
* 单次 accept + 下一轮 poll 再收,既有界又保持 listener/IO 可服务(原版行为即正确,
* fd=N 反复 accept 是**对端短连接**所致,非本循环缺陷)。 */
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) 强制重烧。
* ⚠️ 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;
int cur = amixer_get(9, 1);
if (cur == target) { logmsg("route: port already %d, force re-arm via 5", target); apply_route(5); }
apply_route(target);
last_hb = hb; last_bal = bal; last_target = (hb == 0 && bal == 0) ? -1 : target;
logmsg("route: boot/DAC-return arm done (port %d %s)", target, port_name(target));
need_arm = 0;
} else {
route_evaluate(manual, &last_hb, &last_bal, &last_target);
}
}
int naps = 5; /* 1000ms / 200ms 分段,即时响应 SIGTERM */
for (int k = 0; k < naps && !g_stop; k++) {
struct timespec ts = { 0, 200 * 1000000L };
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 dsd=%d "
"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),
(fmt == AUDIO_FMT_DOP ? 1 : 0),
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_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 / MEOW_AUDIO_AMIXER_TRACE (故障注入/amixer 轨迹, 仅测试)\n",
SETTINGS_CONF);
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);
if ((e = getenv("MEOW_AUDIO_AMIXER_TRACE")) && e[0]) snprintf(g_amixer_trace_path, sizeof g_amixer_trace_path, "%s", 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;
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" : "");
/* 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%% target=%s codec=%s volctl=%s",
g_vol_pct, target_name(), g_bt_codec, volctl_name());
/* 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;
}