2198 lines
99 KiB
C
2198 lines
99 KiB
C
#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 已烧)。
|
||
* 多余重烧收敛(AUDIO_HANG_INVESTIGATION):端口已是目标值(audiod 重启场景)时
|
||
* **不再**先写 5(disable) 强制重烧——每次开机做 5→目标 双写等于把 FPGA 烧录暴露翻倍,
|
||
* 正是驱动级非确定楔死触发面;改为只需读回一致即跳过,仅当未武装/端口不对时才写目标。
|
||
* ⚠️ 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) {
|
||
/* 端口已正确:跳过 5→target 双重强烧,只确认读回一致(cur==target 已由
|
||
* 上一拍 amixer_get 证实)。原逻辑「cur==target 时先写 5(disable) 再回
|
||
* target」是给 audiod 重启/同值短路场景强制重烧用的,但每次开机无条件做
|
||
* 等于把 FPGA 烧录暴露翻倍——正是驱动级非确定楔死的触发面,故收敛掉。 */
|
||
g_route_cur = target; /* 保持 QUERY/monitor 最近端口与读回一致 */
|
||
logmsg("route: port already %d, skip re-arm (no 5->%d re-burn)", target, target);
|
||
} else {
|
||
/* 未武装(cur=0/读失败)或端口不对:写目标端口。这是 M2.91「未武装则武装」
|
||
* 防挂死核心(FPGA 未烧+enable 时二次 hbc3000_start 死锁整机),绝不可裸奔;
|
||
* 直接写 target 即触发烧录,无需再绕写 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;
|
||
}
|