diff --git a/client/public/home/bypass.png b/client/public/home/bypass.png
new file mode 100644
index 0000000..3573402
Binary files /dev/null and b/client/public/home/bypass.png differ
diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx
index 2ac6097..c6a6791 100644
--- a/client/src/pages/EQPage.tsx
+++ b/client/src/pages/EQPage.tsx
@@ -94,9 +94,10 @@ function normalizeFilterType(type: string | number | undefined): string {
/* ── Frequency Response Chart ── */
function FreqChart({
- bands, selectedBand, abMode, onAbToggle, onBandDrag, onBandSelect,
+ bands, rawCurve, selectedBand, abMode, onAbToggle, onBandDrag, onBandSelect,
}: {
bands: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>;
+ rawCurve: number[] | null;
selectedBand: number;
abMode: "A" | "B";
onAbToggle: (m: "A" | "B") => void;
@@ -194,6 +195,26 @@ function FreqChart({
);
const pathD = curveData.pathD;
const fillD = curveData.fillD;
+ const [showEq, setShowEq] = useState(true);
+ const [showRaw, setShowRaw] = useState(true);
+ const [showEqualized, setShowEqualized] = useState(true);
+ const rawPathD = useMemo(() => {
+ if (!Array.isArray(rawCurve) || rawCurve.length !== curveData.points.length) return "";
+ return curveData.points
+ .map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${gainToY(rawCurve[i] ?? 0).toFixed(1)}`)
+ .join(" ");
+ }, [rawCurve, curveData.points]);
+ const equalizedPathD = useMemo(() => {
+ if (!Array.isArray(rawCurve) || rawCurve.length !== curveData.points.length) return "";
+ return curveData.points
+ .map((p, i) => {
+ const y = gainToY((p.gainDb ?? 0) + (rawCurve[i] ?? 0));
+ return `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${y.toFixed(1)}`;
+ })
+ .join(" ");
+ }, [rawCurve, curveData.points]);
+ const hasRawCurve = !!rawPathD;
+ const hasEqualizedCurve = !!equalizedPathD;
// Reusable left-to-right stroke draw animation (skip while dragging a band).
useStrokeDrawAnimation(curvePathRef, pathD, {
@@ -208,18 +229,32 @@ function FreqChart({
{/* Legend row */}
-
-
-
+
+
Equalizer
+
+
+
{/* A/B + DIFF controls */}
@@ -286,21 +321,45 @@ function FreqChart({
{/* Zero line */}
{/* Fill */}
-
- {/* Equalized curve (stroke-dash anim via useEffect) */}
-
- {/* Raw curve (flat) */}
-
+ {showEq &&
}
+ {/* EQ curve */}
+ {showEq && (
+
+ )}
+ {/* Raw */}
+ {rawPathD && showRaw && (
+
+ )}
+ {/* Equalized = EQ + Raw */}
+ {equalizedPathD && showEqualized && (
+
+ )}
{/* Band nodes with index */}
- {bands.map((band, i) => (
+ {showEq && bands.map((band, i) => (
(undefined);
const [selectedCatalogTarget, setSelectedCatalogTarget] = useState("");
const [isConfirmingTarget, setIsConfirmingTarget] = useState(false);
+ const [currentRawCurve, setCurrentRawCurve] = useState(null);
const allowPeqRemoteSyncRef = useRef(false);
const syncingHeadphoneRef = useRef(false);
const peqSyncTimerRef = useRef(null);
@@ -786,8 +846,7 @@ export default function EQPage() {
const latest = await api.getPeqState();
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
toast.success("已删除耳机");
- } catch (error) {
- console.error("removePeq", error);
+ } catch {
toast.error("删除耳机失败");
}
};
@@ -863,8 +922,7 @@ export default function EQPage() {
}
setIsAddPresetDialogOpen(false);
toast.success("已新增预设");
- } catch (error) {
- console.error("saveAddPreset", error);
+ } catch {
toast.error("新增预设失败");
}
};
@@ -879,6 +937,7 @@ export default function EQPage() {
// 获取耳机原始曲线
const getModelCurve = async (brand: string, name: string) => {
try {
+ console.log("[modelCurve] request", { brand, name });
const resp = await fetch(
`//api.luxsin.com.cn/audio/modelCurve?brand=${encodeURIComponent(brand)}&name=${encodeURIComponent(name)}`
);
@@ -886,17 +945,74 @@ export default function EQPage() {
// 使用自定义 Base64 解码
const decoded = decodeCustomBase64(data);
const parsed = JSON.parse(decoded);
- console.log('型号的原始数据:', parsed);
- if (parsed.hasOwnProperty('fr')) {
- return parsed.fr;
- }
- return null;
+ console.log("[modelCurve] decoded", {
+ hasFrRaw:
+ !!(parsed &&
+ typeof parsed === "object" &&
+ "fr" in (parsed as Record) &&
+ (parsed as { fr?: unknown }).fr &&
+ typeof (parsed as { fr?: unknown }).fr === "object" &&
+ "raw" in ((parsed as { fr?: Record }).fr ?? {})),
+ });
+ console.log("[modelCurve] fr.raw", (parsed as { fr?: { raw?: unknown } }).fr?.raw);
+ return parsed;
} catch (error) {
- console.error("Failed to get model curve:", error);
+ console.log("[modelCurve] request failed", error);
return null;
}
};
+ const loadRawCurveForPeq = useCallback(
+ async (peq: { brand?: string; model?: string; name?: string } | undefined): Promise => {
+ let brand = peq?.brand?.trim() ?? "";
+ let model = peq?.model?.trim() ?? "";
+
+ // Some presets only have `name` (e.g. "Apple AirPods Pro") and miss explicit brand/model.
+ if ((!brand || !model) && peq?.name) {
+ const [first, ...rest] = peq.name.trim().split(/\s+/);
+ if (!brand && first) brand = first;
+ if (!model && rest.length > 0) model = rest.join(" ");
+ }
+ if (!brand || !model) return null;
+
+ const modelCurve = await getModelCurve(brand, model);
+ if (modelCurve && typeof modelCurve === "object") {
+ const payload = modelCurve as {
+ fr?: { raw?: unknown } | unknown;
+ };
+ const rawCandidate =
+ payload.fr && typeof payload.fr === "object" && Array.isArray((payload.fr as { raw?: unknown }).raw)
+ ? (payload.fr as { raw?: unknown }).raw
+ : null;
+ const raw = Array.isArray(rawCandidate)
+ ? rawCandidate
+ .map((point) => {
+ if (typeof point === "number" && Number.isFinite(point)) return point;
+ if (Array.isArray(point) && point.length >= 2 && typeof point[1] === "number") return point[1];
+ if (point && typeof point === "object") {
+ const y = (point as Record).y
+ ?? (point as Record).value
+ ?? (point as Record).db
+ ?? (point as Record).gain;
+ if (typeof y === "number" && Number.isFinite(y)) return y;
+ }
+ return NaN;
+ })
+ : null;
+ const cleanedRaw = raw && raw.every((v) => Number.isFinite(v)) ? (raw as number[]) : null;
+ console.log("[modelCurve] raw check", {
+ brand,
+ model,
+ hasRaw: !!cleanedRaw,
+ rawLength: cleanedRaw?.length ?? 0,
+ });
+ return cleanedRaw;
+ }
+ return null;
+ },
+ [],
+ );
+
// 加载耳机列表并初始化曲线
useEffect(() => {
async function loadHeadphones() {
@@ -914,6 +1030,7 @@ export default function EQPage() {
];
setHeadphoneModels(defaultModels);
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
+ setCurrentRawCurve(null);
return;
}
try {
@@ -928,33 +1045,14 @@ export default function EQPage() {
// 修复 filters 数据(兼容不同的字段名)
const fixedFilters = normalizeFiltersFromPeq(currentPeq);
- console.log("Fixed filters:", fixedFilters);
-
// 更新 bands 状态
if (fixedFilters && fixedFilters.length > 0) {
setBands(fixedFilters);
}
- // 如果有 brand 和 model,获取原始曲线
- let raw: number[] | null = null;
- if ((currentPeq as any).brand && (currentPeq as any).model) {
- try {
- const modelCurve = await getModelCurve((currentPeq as any).brand, (currentPeq as any).model);
- if (modelCurve && 'raw' in modelCurve) {
- raw = modelCurve.raw;
- }
- } catch (error) {
- console.warn("Failed to load model curve:", error);
- }
- }
+ // raw 与图表渲染统一交给 headphoneIdx/peqItems 监听逻辑处理,
+ // 避免首次进入页面时重复请求 modelCurve。
- // 渲染图表(使用修复后的 fixedFilters)
- // 使用 setTimeout 确保 DOM 已经渲染
- setTimeout(() => {
- renderCharts(fixedFilters.length > 0 ? fixedFilters : bands, raw, false);
- }, 100);
-
- console.log("Initialized PEQ:", currentPeq.name, "Filters count:", fixedFilters.length, "Raw curve:", raw ? "loaded" : "not available");
}
} else {
// 如果没有数据,使用默认列表
@@ -968,9 +1066,9 @@ export default function EQPage() {
];
setHeadphoneModels(defaultModels);
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
+ setCurrentRawCurve(null);
}
- } catch (error) {
- console.error("Failed to load headphone models:", error);
+ } catch {
// 出错时使用默认列表
const defaultModels = [
"Sennheiser HD 650",
@@ -982,13 +1080,14 @@ export default function EQPage() {
];
setHeadphoneModels(defaultModels);
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
+ setCurrentRawCurve(null);
}
} finally {
allowPeqRemoteSyncRef.current = true;
}
}
loadHeadphones();
- }, [api, isDemoMode]);
+ }, [api, isDemoMode, loadRawCurveForPeq]);
// 切换耳机型号后,立即用该型号 filters 刷新 10 个滤波器与曲线(暂停一次上报避免错写)
useEffect(() => {
@@ -999,10 +1098,20 @@ export default function EQPage() {
syncingHeadphoneRef.current = true;
setBands(nextBands);
setSelectedBand(0);
+ let cancelled = false;
+ void (async () => {
+ const raw = await loadRawCurveForPeq(peq as { brand?: string; model?: string });
+ if (cancelled) return;
+ setCurrentRawCurve(raw);
+ renderCharts(nextBands, raw, false);
+ })();
requestAnimationFrame(() => {
syncingHeadphoneRef.current = false;
});
- }, [headphoneIdx, peqItems]);
+ return () => {
+ cancelled = true;
+ };
+ }, [headphoneIdx, peqItems, loadRawCurveForPeq]);
const schedulePeqSync = (
peq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined,
@@ -1028,9 +1137,7 @@ export default function EQPage() {
canDel: peq.canDel ?? 1,
},
};
- console.log("upgradePeqChange payload:", payload);
upgradePeqChange(payload).catch((err) => {
- console.error("upgradePeqChange", err);
toast.error("EQ 保存失败");
});
}, delay);
@@ -1072,9 +1179,8 @@ export default function EQPage() {
const fs = 48000;
// Calculate coefficient matrix for each filter
- peqFilters.forEach((item, index) => {
+ peqFilters.forEach((item) => {
const filterType = getFilterType(item.type);
- console.log(`Filter ${index}: type="${item.type}" -> mapped to ${filterType}, freq=${item.freq}, gain=${item.gain}, q=${item.q}`);
const coeff = getSectionsMatrix(
item.gain,
@@ -1086,14 +1192,12 @@ export default function EQPage() {
);
if (coeff) {
list.push(coeff);
- } else {
- console.error(`Filter ${index} returned null/undefined coeff!`);
}
});
// Get frequency response data
const dataSet = visualizeResponse(list, fs);
- const ops = getChartOps(dataSet, 20, -20, '#FFED00');
+ const ops = getChartOps(dataSet, 20, -20, '#FFED00') as any;
// Get or create chart instance
const chartDom = document.getElementById('freq-chart');
@@ -1107,7 +1211,7 @@ export default function EQPage() {
// Handle changeParam (get raw data from existing chart)
if (changeParam && myChart) {
- const option = myChart.getOption();
+ const option = myChart.getOption() as { series?: Array<{ data?: number[] }> };
if (option.series && option.series.length > 1) {
raw = (option.series[1] as any).data;
}
@@ -1116,8 +1220,8 @@ export default function EQPage() {
// Clear and rebuild chart
myChart.clear();
- // Add Raw and Equalized curves if raw data is available
- if (raw) {
+ // Add Raw and Equalized curves if raw data is available (expects same 349 points as EQ curve)
+ if (Array.isArray(raw) && raw.length === dataSet[1].length) {
ops.series.push({
name: 'Raw',
data: raw,
@@ -1129,7 +1233,7 @@ export default function EQPage() {
});
// Calculate equalized curve
- const equalizedRaw = dataSet[1].map((value, index) => value + (raw as any)[index]);
+ const equalizedRaw = dataSet[1].map((value, index) => value + raw[index]);
ops.series.push({
name: 'Equalized',
data: equalizedRaw,
@@ -1147,11 +1251,11 @@ export default function EQPage() {
};
const updateBand = (idx: number, patch: Partial) => {
- setBands((prev) => prev.map((b, i) => i === idx ? { ...b, ...patch } : b));
+ const nextBands = bands.map((b, i) => (i === idx ? { ...b, ...patch } : b));
+ setBands(nextBands);
// Re-render chart when band changes
if (peqItems.length > 0 && headphoneIdx < peqItems.length) {
- const currentPeq = peqItems[headphoneIdx];
- renderCharts(bands, null, false);
+ renderCharts(nextBands, currentRawCurve, false);
}
};
@@ -1286,6 +1390,7 @@ export default function EQPage() {
{/* ── Frequency response chart ── */}
) : null;
const parametricEqRaw = parsedObj?.parametric_eq;
- if (!parametricEqRaw || typeof parametricEqRaw !== "object") {
- console.warn("getCurve decoded: missing parametric_eq", { brand, name, target, data: parsed });
- return;
- }
+ if (!parametricEqRaw || typeof parametricEqRaw !== "object") return;
const parametricEq = parametricEqRaw as {
filters?: Array<{ type: string | number; fc: number; gain: number; q: number }>;
@@ -1834,16 +1936,8 @@ export default function EQPage() {
}
setIsBrandDrawerOpen(false);
- console.log("getCurve decoded + peqChange posted:", {
- brand,
- name,
- target,
- data: parsed,
- postPeq,
- });
toast.success("新耳机 EQ 已上报");
- } catch (error) {
- console.error("getCurve failed:", error);
+ } catch {
toast.error("获取曲线失败");
} finally {
setIsConfirmingTarget(false);
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx
index 97379f8..10c2e96 100644
--- a/client/src/pages/Home.tsx
+++ b/client/src/pages/Home.tsx
@@ -32,7 +32,6 @@ import {
Activity,
Gauge,
ChevronDown,
- Zap,
Wifi,
Bluetooth,
} from "lucide-react";
@@ -462,7 +461,12 @@ export default function Home() {
className={cn("icon-circle-btn", bypassOn && "active")}
onClick={() => updateSetting({ dsp_enable: bypassOn ? 1 : 0 })}
>
-
+
Bypass