/* ============================================================ AUDIO PAGE — Audio Settings Design: Reference luxsin_x8_音频设置.png Sections: - 平衡与灵敏度: 左右平衡 slider, VU灵敏度 slider - 输出特性: 滤波特性, 耳机增益, 音量幅度 - 系统与接口: XLR极性, DAC增益, DAC阻抗 All list rows → navigate to /select page ============================================================ */ import { useDevice } from "@/contexts/DeviceContext"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useLocation } from "wouter"; import { useState, useEffect, useRef } from "react"; import BottomNav from "@/components/BottomNav"; import { navigateToSelect } from "./SelectPage"; import { getBootSoundMaxIndex, normalizeBootSound, parseFirmwareVersion, readBootSoundFromState, } from "@/lib/luxsinApi"; import localeZh from "@/locales/data-zh.json"; import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; function CyanSlider({ value, min, max, step = 1, onChange, onRelease }: { value: number; min: number; max: number; step?: number; onChange: (v: number) => void; /** Commit to device once when the user releases the thumb (pointer / keyboard). */ onRelease?: (v: number) => void; }) { const fillPct = ((value - min) / (max - min)) * 100; const emitRelease = (el: EventTarget | null) => { if (!(el instanceof HTMLInputElement)) return; onRelease?.(Number(el.value)); }; return (
onChange(Number(e.target.value))} onPointerUp={(e) => emitRelease(e.currentTarget)} onPointerCancel={(e) => emitRelease(e.currentTarget)} onKeyUp={(e) => { if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"].includes(e.key)) { emitRelease(e.currentTarget); } }} />
); } function SectionHeader({ title }: { title: string }) { return (
{title}
); } function ListRow({ label, value, onClick }: { label: string; value: string; onClick?: () => void }) { return ( ); } type LocaleDac = { dac?: { sectionBalanceSensitivity?: string; sectionOutput?: string; sectionSystemInterface?: string; balance?: string; sensitivity?: string; filters?: { label?: string; options?: Array<{ index: number; label: string }> }; dacGain?: { label?: string; options?: Array<{ index: number; label: string }> }; volumeStep?: string; bootVolume?: string | { label?: string; options?: Array<{ index: number; label: string }> }; xlr?: { label?: string; options?: Array<{ index: number; label: string }> }; mutePolar?: { label?: string; options?: Array<{ index: number; label: string }> }; IISMode?: { label?: string; options?: Array<{ index: number; label: string }> }; }; home?: { audioSet?: string }; }; const LOCALE_BY_LANG: Record = { 0: localeEn as LocaleDac, 1: localeZhHK as LocaleDac, 2: localeZh as LocaleDac, }; function optionLabels(items: Array<{ index: number; label: string }> | undefined, fallback: string[]) { if (!items || items.length === 0) return fallback; const sorted = [...items].sort((a, b) => a.index - b.index); return sorted.map((item) => item.label); } const STEP_OPTIONS = ["0.5dB", "1dB", "2dB", "3dB"]; const BOOT_VOLUME_FALLBACK = [ "Default", "-5dB", "-10dB", "-15dB", "-20dB", "-25dB", "-30dB", "-35dB", "-40dB", "-45dB", "-50dB", ]; function bootVolumeLabel(dacText: LocaleDac["dac"], index: number): string { const boot = dacText?.bootVolume; if (boot && typeof boot === "object" && boot.options?.length) { const hit = boot.options.find((o) => o.index === index); if (hit) return hit.label; const sorted = [...boot.options].sort((a, b) => a.index - b.index); if (index >= 0 && index < sorted.length) return sorted[index]?.label ?? BOOT_VOLUME_FALLBACK[index] ?? "—"; } return BOOT_VOLUME_FALLBACK[index] ?? "—"; } function bootVolumeOptions(dacText: LocaleDac["dac"], maxIndex: number): string[] { const labels: string[] = []; for (let i = 0; i <= maxIndex; i += 1) { labels.push(bootVolumeLabel(dacText, i)); } return labels; } function bootVolumeRowLabel(dacText: LocaleDac["dac"]): string { const boot = dacText?.bootVolume; if (boot && typeof boot === "object" && boot.label) return boot.label; if (typeof boot === "string") return boot; return "Boot volume"; } export default function AudioPage() { const [location, setLocation] = useLocation(); const { deviceState, updateSetting, refresh, isConnected, api } = useDevice(); const ds = deviceState; const vuSensor = (ds as ({ vuSensor?: number } | null))?.vuSensor; const langIdx = ds?.language ?? 2; const localeData = LOCALE_BY_LANG[langIdx] ?? (localeZh as LocaleDac); const dacText = localeData.dac ?? {}; // balance 从接口获取的是乘以 10 后的值,需要除以 10 显示(例如:-130 → -13.0) const balanceVal = ds?.balance !== undefined ? ds.balance / 10 : 0; // vuSensor 从接口获取的是放大 2 倍的值,需要除以 2 显示(例如:20 → +10dB) const vuSensVal = vuSensor !== undefined ? vuSensor / 2 : 0; const bootSoundFromApi = readBootSoundFromState(ds); const [localBootSound, setLocalBootSound] = useState(bootSoundFromApi); const [localBalance, setLocalBalance] = useState(balanceVal); const [localVuSens, setLocalVuSens] = useState(0); const isBalanceDragging = useRef(false); const isVuDragging = useRef(false); useEffect(() => { if (!isBalanceDragging.current) setLocalBalance(balanceVal); }, [balanceVal]); useEffect(() => { if (!isVuDragging.current) setLocalVuSens(vuSensVal); }, [vuSensVal]); useEffect(() => { setLocalBootSound(readBootSoundFromState(ds)); }, [ds?.bootSound]); useEffect(() => { if (location !== "/audio") return; setLocalBootSound(readBootSoundFromState(ds)); }, [location, ds?.bootSound, ds]); useEffect(() => { if (location !== "/audio") return; if (isConnected || api) { void refresh(); } }, [location, isConnected, api, refresh]); const gainIdx = ds?.dacGain ?? 0; const stepIdx = ds?.soundStep ?? 1; const filterIdx = ds?.pcm ?? 0; const xlrIdx = ds?.xlr ?? 0; const mutePolarIdx = (ds as ({ hdmimutepolar?: number } | null))?.hdmimutepolar ?? 0; const iisModeIdx = (ds as ({ hdmiType?: number } | null))?.hdmiType ?? 0; const bootSoundVal = normalizeBootSound(localBootSound); const firmwareVersion = parseFirmwareVersion(ds?.version); const bootSoundMaxIdx = getBootSoundMaxIndex(firmwareVersion); const bootSoundSelectIdx = Math.min(bootSoundVal, bootSoundMaxIdx); const BOOT_VOLUME_OPTIONS = bootVolumeOptions(dacText, bootSoundMaxIdx); const FILTER_OPTIONS = optionLabels(dacText.filters?.options, ["快速滚降", "慢速滚降", "短延迟快速滚降", "短延迟慢速滚降", "去重强调", "非过采样(NOS)"]); const GAIN_OPTIONS = optionLabels(dacText.dacGain?.options, ["低", "中", "高"]); const XLR_OPTIONS = optionLabels(dacText.xlr?.options, ["正常", "反转"]); const MUTE_POLAR_OPTIONS = optionLabels(dacText.mutePolar?.options, ["低电平", "高电平"]); const IIS_MODE_OPTIONS = optionLabels(dacText.IISMode?.options, ["模式1", "模式2", "模式3", "模式4", "模式5", "模式6", "模式7", "模式8"]); const goSelect = (title: string, options: string[], selected: number, key: string) => { navigateToSelect(title, options, selected, "/audio", key); setLocation("/select"); }; return (

{localeData.home?.audioSet ?? "音频设置"}

{/* ── 平衡与灵敏度 ── */}
{dacText.balance ?? "左右平衡"} {localBalance >= 0 ? `+${localBalance.toFixed(1)}` : localBalance.toFixed(1)} dB
{ isBalanceDragging.current = true; setLocalBalance(v); }} onRelease={(v) => { isBalanceDragging.current = false; setLocalBalance(v); updateSetting({ balance: Math.round(v * 10) }); }} />
{dacText.sensitivity ?? "VU 表灵敏度"} {localVuSens >= 0 ? `+${localVuSens}` : localVuSens} dB
{ isVuDragging.current = true; setLocalVuSens(v); }} onRelease={(v) => { isVuDragging.current = false; setLocalVuSens(v); updateSetting({ vuSensor: Math.round(v * 2) }); }} />
{/* ── 输出特性 ── */}
goSelect(dacText.filters?.label ?? "选择滤波特性", FILTER_OPTIONS, filterIdx, "filterCharacteristic")} /> goSelect(dacText.dacGain?.label ?? "选择耳机增益", GAIN_OPTIONS, gainIdx, "dacGain")} /> goSelect(dacText.volumeStep ?? "选择音量幅度", STEP_OPTIONS, stepIdx, "soundStep")} />
{/* ── 系统与接口 ── */}
goSelect( bootVolumeRowLabel(dacText), BOOT_VOLUME_OPTIONS, bootSoundSelectIdx, "bootSound", ) } /> goSelect(dacText.xlr?.label ?? "选择 XLR 极性", XLR_OPTIONS, xlrIdx, "xlr")} /> goSelect(dacText.mutePolar?.label ?? "选择 IIS 静音电平", MUTE_POLAR_OPTIONS, mutePolarIdx, "mutePolar")} /> goSelect(dacText.IISMode?.label ?? "选择 IIS 模式", IIS_MODE_OPTIONS, iisModeIdx, "IISMode")} />
); }