/* audio.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" static int amixer_vol(int numid) { char cmd[96]; snprintf(cmd, sizeof cmd, "amixer -c 0 cget numid=%d 2>/dev/null", numid); FILE *p = popen(cmd, "r"); if (!p) return -1; char buf[128]; int v = -1; while (fgets(buf, sizeof buf, p)) { /* ": values=N" line — the other "values=1" on the type line is the * element's value COUNT, not the actual volume */ char *s = strstr(buf, ": values="); if (s) { v = atoi(s + 9); break; } } pclose(p); return v; } static void amixer_set(int numid, int v) { if (v < 0) return; char cmd[96]; snprintf(cmd, sizeof cmd, "amixer -c 0 cset numid=%d %d >/dev/null 2>&1", numid, v); system(cmd); } struct vol_snap vol_snapshot(void) { struct vol_snap s = { amixer_vol(1), amixer_vol(2) }; return s; } void vol_restore(struct vol_snap s) { logmsg("volume restore -> L=%d R=%d", s.l, s.r); amixer_set(1, s.l); amixer_set(2, s.r); } int settings_volume = MEOW_VOL_DEFAULT; /* 当前音量 0-100(= 写入 DAC 的映射值) */ int settings_volume_max = MEOW_VOL_MAX_DEFAULT; /* 最大音量上限 clamp */ int settings_volume_step = MEOW_VOL_STEP_DEFAULT; /* 跳步 */ /* M2.17/M2.16:把 meow_settings 当前值同步到本模块的缓存全局。读取方 * (DAC/状态栏/息屏/刷新率调度)统一从缓存取,避免散读 backend。 */ void settings_sync_cache_from_backend(void) { int v; if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v; if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v; if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v; if (meow_settings_get_enum("display.refresh_rate", &v) == 0) /* names "60,30,15" */ refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60; if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v; if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v; if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v; if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v; if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v; } /* meow_settings 变更回调:set_*(如音量键)后同步缓存全局,保证既有引用 * settings_volume 等变量的代码与 backend 单点一致(SYSTEM_SETTINGS §3)。 */ void settings_sync_cache_listener(const char *key) { if (!strcmp(key, "audio.volume")) { int v; if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v; } else if (!strcmp(key, "audio.volume_max")) { int v; if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v; } else if (!strcmp(key, "audio.volume_step")) { int v; if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v; } else if (!strcmp(key, "display.refresh_rate")) { int v; if (meow_settings_get_enum("display.refresh_rate", &v) == 0) refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60; } else if (!strcmp(key, "display.refresh_always_max")) { int v; if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v; } else if (!strcmp(key, "display.screen_off_timeout")) { int v; if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v; } else if (!strcmp(key, "display.screen_off_mode")) { int v; if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v; } else if (!strcmp(key, "power.longpress_menu_ms")) { int v; if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v; } else if (!strcmp(key, "power.forceoff_ms")) { int v; if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v; } } /* 启动加载(M2.11/M2.24):统一由 meow_settings 后端读 settings.conf; * 此处仅注册监听(一次性)+ 把 backend 当前值同步到缓存全局。重载(应用期 * watchdog 可能改音量)等价于 meow_settings_init() + 手动同步。 */ int settings_cache_registered = 0; /* M2.24:监听只注册一次 */ /* ---- MeowOS 真实 DAC 音量(CS43198,ANALYSIS §18.39)---- * HibyOS 播放器经 ioctl 直写 /dev/snd/controlC0 的 "Left/Right Playback * Volume"(numid 1/2,0-255)→ codec 驱动 cs43198_vol_*_put → I2C 直写 CS43198 * 寄存器(0x9000-0x9002/0x7000-0x7001,双片 L/R)。我们复刻同路径(amixer * cset = SNDRV_CTL_IOCTL_ELEM_WRITE),0-100 → 0-255 映射直写两声道。 * 读回不可用(驱动 get 为 dummy 恒 0;/dev/i2c-3 被驱动占用),故系统音量以 * settings_volume(写入 DAC 的映射值)为权威跟踪值。 */ void meow_dac_vol_write(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静音)+平方锥度(听感线性) */ char cmd[160]; snprintf(cmd, sizeof cmd, "amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1", dac, dac); system(cmd); logmsg("DAC vol -> L/R=%d (pct %d)", dac, pct); } static unsigned long long vol_dac_last_ms = 0; void meow_dac_vol_write_throttled(void) { unsigned long long now = now_ms(); if (now - vol_dac_last_ms < VOL_DRAG_DAC_MS) return; /* 节流窗口内跳过,等抬手统一写 */ vol_dac_last_ms = now; meow_dac_vol_write(settings_volume); } /* 抬手/点击提交连 save 时调用:无条件直写一次真实 DAC(绕过节流,保证最终音量落 DAC/落盘)。 */ void meow_dac_vol_flush(void) { vol_dac_last_ms = now_ms(); meow_dac_vol_write(settings_volume); } /* clamp 集中:0..MEOW_VOL_MAX,再 clamp 到上限 settings_volume_max。 */ int vol_clamp(int v) { if (v < 0) v = 0; if (v > MEOW_VOL_MAX) v = MEOW_VOL_MAX; if (v > settings_volume_max) v = settings_volume_max; return v; } int settings_volume_get(void) { return settings_volume; } int vol_ramp_dir = 0; /* +1 上 / -1 下 / 0 停 */ static int vol_ramp_target = -1; /* 目标值;-1=无 */ static unsigned long long vol_ramp_next_at = 0; static unsigned long long vol_ramp_press_at = 0; /* M2.18 Bug 4:本方向键按下时刻(长按判据) */ /* ---- 插孔消抖(2026-09-01,真机日志实证)---- * 4.4/3.5 耳机插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生 VOL_UP 幽灵按住 * (EVIOCGKEY 轮询到持续 down),vol_ramp 一路爬到上限:日志 225391「volume UP <- ramp」 * → 227213「ramp end -> 50」,紧接着 audiod 才报「route: jack changed -> 4.4 balance」。 * 修法:/sys/class/switch/{headset,balance}/state 任一变化后 JACK_DEBOUNCE_MS 内忽略音量键 * 边沿(vol_ramp_begin 不起、vol_ramp_tick 在爬也中止)。插拔耳机时没人按音量,误伤可忽略。 * 检测走 1s 节流读缓存——绝不在 10ms 主循环热路径上每圈读文件。 */ /* 宿主测试可 -D 覆盖(同 meow_settings.c MEOW_SETTINGS_FILE 惯例) */ #ifndef JACK_HS_STATE #define JACK_HS_STATE "/sys/class/switch/headset/state" #endif #ifndef JACK_BAL_STATE #define JACK_BAL_STATE "/sys/class/switch/balance/state" #endif static int jack_combo_cached = -1; /* hs*2+bal;-1=未初始化/节点缺失 */ static unsigned long long jack_poll_at = 0; /* 上次节流读时刻 */ static unsigned long long jack_changed_at = 0; /* 最近一次检测到插孔变化的时刻(0=无) */ /* 读两个插孔开关节点合成 hs*2+bal;两节点都不可读返回 -1(不更新缓存,不误判变化)。 */ static int jack_combo_read(void) { int hs = read_file_int(JACK_HS_STATE); int bal = read_file_int(JACK_BAL_STATE); if (hs < 0 && bal < 0) return -1; return (hs > 0 ? 2 : 0) | (bal > 0 ? 1 : 0); } /* 返回 1=插孔变化后消抖窗口内(音量键边沿应忽略/在爬 ramp 应中止)。 * 自节流:每 JACK_POLL_MS 才真正读一次 sysfs。 */ static int jack_debounce_active(void) { unsigned long long now = now_ms(); if (now - jack_poll_at >= JACK_POLL_MS) { jack_poll_at = now; int c = jack_combo_read(); if (c >= 0) { if (jack_combo_cached >= 0 && c != jack_combo_cached) { jack_changed_at = now; logmsg("jack state changed (%d -> %d), vol-key debounce %dms", jack_combo_cached, c, JACK_DEBOUNCE_MS); } jack_combo_cached = c; } } return jack_changed_at && (now - jack_changed_at < (unsigned long long)JACK_DEBOUNCE_MS); } static void vol_ramp_begin(int delta) { if (jack_debounce_active()) { /* 插孔瞬态期:忽略音量键边沿(幽灵按下) */ logmsg("volume adj %+d ignored (jack debounce)", delta); return; } int target = vol_clamp(settings_volume + delta); if (target == settings_volume) return; vol_ramp_dir = (target > settings_volume) ? 1 : -1; vol_ramp_target = target; vol_ramp_press_at = now_ms(); /* M2.18 Bug 4:记录该方向键按下时刻,供 CLICK_MS 长按判定 */ vol_ramp_next_at = 0; /* 立即迈第一步,给即时反馈 */ } /* 每主循环 tick 调;key_held=该方向按键是否仍按住(长按续爬)。返回 1=仍在渐变。 * M2.18 Bug 4 语义:单击( %d", settings_volume); return 0; } /* 兜底:ramp 总时长上限 VOL_RAMP_MAX_MS——防任何「持续按住」假象(ADC 噪声卡 key_held=1) * 一次打满/打到底。正常长按 0→上限(通常 ≤2.7s)不受影响;到顶想再调需松手重按。 */ if (now - vol_ramp_press_at >= (unsigned long long)VOL_RAMP_MAX_MS) { vol_ramp_dir = 0; vol_ramp_target = -1; meow_settings_save(); logmsg("volume ramp force-stop (%dms cap) -> %d", VOL_RAMP_MAX_MS, settings_volume); return 0; } /* 先看是否已到「当前目标值」(settings_volume 等于 target,而非候选 v)—— * 因为一旦在 target 再取 v=settings_volume+dir 便越过 target,导致 hold_wait 失效继续爬。 */ if (settings_volume == vol_ramp_target) { if (!key_held) { /* 松开 → 一段完成,结束(单击到 target 即停,只跳 step 格) */ vol_ramp_dir = 0; vol_ramp_target = -1; meow_settings_save(); return 0; } if (now - vol_ramp_press_at < CLICK_MS) { /* hold_wait:已到 target 但未到长按阈值 → 停在 target 不爬(保持待命不清 dir, * 等长按激活或松开);若直接续段,长按第一次到 target 就断了。 */ return 1; } /* 长按激活:续段(在 target 基础上再 +step) */ vol_ramp_target = vol_clamp(vol_ramp_target + vol_ramp_dir * settings_volume_step); if (vol_ramp_target == settings_volume) { /* 续段到上限无进展 → 结束 */ vol_ramp_dir = 0; vol_ramp_target = -1; meow_settings_save(); return 0; } } int v = vol_clamp(settings_volume + vol_ramp_dir); if (v == settings_volume) { /* 已达上下限 */ vol_ramp_dir = 0; vol_ramp_target = -1; meow_settings_save(); return 0; } meow_settings_set_int("audio.volume", v); /* 更新 backend 内存 + 同步缓存(不逐 1% 落盘) */ meow_dac_vol_write(settings_volume); /* 每 1% 写一次真实 DAC(settings_volume 已同步) */ return 1; } /* 步进调节(±settings_volume_step)→ 起线性渐变(1% 爬,见上)。 */ void settings_volume_adj(int delta) { vol_ramp_begin(delta); }