From 6cce6d1f73e8d91660a369f31b9ba32dafd2338d Mon Sep 17 00:00:00 2001 From: yangy Date: Thu, 23 Apr 2026 18:00:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96eq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/public/home/bypass.png | Bin 0 -> 439 bytes client/src/pages/EQPage.tsx | 270 +++++++++++++++++++++++----------- client/src/pages/Home.tsx | 8 +- 3 files changed, 188 insertions(+), 90 deletions(-) create mode 100644 client/public/home/bypass.png diff --git a/client/public/home/bypass.png b/client/public/home/bypass.png new file mode 100644 index 0000000000000000000000000000000000000000..35734024cd03bb415b9fadb28622cf95211158d2 GIT binary patch literal 439 zcmV;o0Z9IdP)8$xyRg8002yZSCn@M=n72*PW z`nG!vb4l6=Y}`x0*FHy}#p}%_m{KrKI&jZC1KfMZew{h@k8ujtrIj+{t)ZEtxVQ}` zw)Jt$YNK%v3gE)6y*j9ce-F`cr1HXkI4Byh8~fim--%;1|HpwU$|QPoP*({{llWD! zU5`|tgrzBdEyO(^0eog0n!jxYScoIAo>wLa-PjW|r?t^X2i28x)r*6g7&|1&i?^hrE;;uZl=2XFjT*#sYqBFfH3Ox hX42~1`&A_XegO}+FK%bPZtnm9002ovPDHLkV1h01y@~(; literal 0 HcmV?d00001 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 -
-
-
- Raw -
-
+ + +
{/* 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 Bypass