diff --git a/client/src/pages/EffectsPage.tsx b/client/src/pages/EffectsPage.tsx
index c3d9f96..e823a42 100644
--- a/client/src/pages/EffectsPage.tsx
+++ b/client/src/pages/EffectsPage.tsx
@@ -11,8 +11,10 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
import { useLocation } from "wouter";
import { useState, useEffect, useRef, useMemo } from "react";
import BottomNav from "@/components/BottomNav";
+import SubwooferLpfChart from "@/components/SubwooferLpfChart";
import { FeatureGate } from "@/components/FeatureGate";
import { navigateToSelect } from "./SelectPage";
+import { parseFirmwareVersion } 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";
@@ -24,6 +26,115 @@ function clampPickIndex(i: number, len: number) {
return Math.min(Math.max(i, 0), len - 1);
}
+const SUBWOOFER_DELAY_MS_DIVISOR = 48;
+const SUBWOOFER_DELAY_CM_NUMERATOR = 34;
+const SUBWOOFER_DELAY_MAX = 1920;
+
+function clampSubwooferDelayValue(v: number) {
+ return Math.round(Math.min(SUBWOOFER_DELAY_MAX, Math.max(0, v)));
+}
+
+function subwooferDelayToMs(value: number) {
+ return value / SUBWOOFER_DELAY_MS_DIVISOR;
+}
+
+function subwooferDelayToCm(value: number) {
+ return (value * SUBWOOFER_DELAY_CM_NUMERATOR) / SUBWOOFER_DELAY_MS_DIVISOR;
+}
+
+function formatSubwooferDelayValue(value: number) {
+ const ms = subwooferDelayToMs(value);
+ const cm = subwooferDelayToCm(value);
+ return `${ms.toFixed(2)}ms(${cm.toFixed(1)}cm)`;
+}
+
+type DelayChannel = "L" | "R";
+
+function DelaySpeakerIcon({ direction }: { direction: "left" | "right" }) {
+ return (
+
+ );
+}
+
+function FullRangeToggleButton({
+ label,
+ active,
+ onClick,
+}: {
+ label: string;
+ active: boolean;
+ onClick: () => void;
+}) {
+ return (
+
+ );
+}
+
+function DelayChannelToggle({
+ channel,
+ onChange,
+ leftLabel,
+ rightLabel,
+}: {
+ channel: DelayChannel;
+ onChange: (ch: DelayChannel) => void;
+ leftLabel: string;
+ rightLabel: string;
+}) {
+ const btnClass = (active: boolean) =>
+ `flex items-center justify-center w-10 h-10 rounded-lg transition-colors ${
+ active ? "bg-[#00FFF6]/20 text-[#00FFF6]" : "text-white/35 active:bg-white/10"
+ }`;
+
+ return (
+
+
+
+
+ );
+}
+
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
return (
+
+ {/* ── 低音炮输出 ── */}
+