优化eq
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 439 B |
+173
-79
@@ -94,9 +94,10 @@ function normalizeFilterType(type: string | number | undefined): string {
|
|||||||
|
|
||||||
/* ── Frequency Response Chart ── */
|
/* ── Frequency Response Chart ── */
|
||||||
function FreqChart({
|
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 }>;
|
bands: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>;
|
||||||
|
rawCurve: number[] | null;
|
||||||
selectedBand: number;
|
selectedBand: number;
|
||||||
abMode: "A" | "B";
|
abMode: "A" | "B";
|
||||||
onAbToggle: (m: "A" | "B") => void;
|
onAbToggle: (m: "A" | "B") => void;
|
||||||
@@ -194,6 +195,26 @@ function FreqChart({
|
|||||||
);
|
);
|
||||||
const pathD = curveData.pathD;
|
const pathD = curveData.pathD;
|
||||||
const fillD = curveData.fillD;
|
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).
|
// Reusable left-to-right stroke draw animation (skip while dragging a band).
|
||||||
useStrokeDrawAnimation(curvePathRef, pathD, {
|
useStrokeDrawAnimation(curvePathRef, pathD, {
|
||||||
@@ -208,18 +229,32 @@ function FreqChart({
|
|||||||
<div className="ios-list-group p-3 mb-3">
|
<div className="ios-list-group p-3 mb-3">
|
||||||
{/* Legend row */}
|
{/* Legend row */}
|
||||||
<div className="flex items-center gap-3 mb-2 px-1">
|
<div className="flex items-center gap-3 mb-2 px-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<button
|
||||||
<div className="w-3 h-[2px] rounded" style={{ background: "#f59e0b" }} />
|
type="button"
|
||||||
<span className="text-[10px] text-white/45">Equalizer</span>
|
className="flex items-center gap-1.5 active:opacity-80 transition-opacity"
|
||||||
</div>
|
onClick={() => setShowEq((v) => !v)}
|
||||||
<div className="flex items-center gap-1.5">
|
>
|
||||||
<div className="w-3 h-[2px] rounded bg-white/25" />
|
|
||||||
<span className="text-[10px] text-white/45">Raw</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<div className="w-3 h-[2px] rounded" style={{ background: "#FFED00" }} />
|
<div className="w-3 h-[2px] rounded" style={{ background: "#FFED00" }} />
|
||||||
<span className="text-[10px] text-white/45">Equalized</span>
|
<span className="text-[10px]" style={{ color: "#FFED00", opacity: showEq ? 1 : 0.35 }}>Equalizer</span>
|
||||||
</div>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
onClick={() => setShowRaw((v) => !v)}
|
||||||
|
disabled={!hasRawCurve}
|
||||||
|
>
|
||||||
|
<div className="w-3 h-[2px] rounded" style={{ background: "#ffffff" }} />
|
||||||
|
<span className="text-[10px]" style={{ color: "#ffffff", opacity: showRaw ? 1 : 0.35 }}>Raw</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
onClick={() => setShowEqualized((v) => !v)}
|
||||||
|
disabled={!hasEqualizedCurve}
|
||||||
|
>
|
||||||
|
<div className="w-3 h-[2px] rounded" style={{ background: "#23d2fe" }} />
|
||||||
|
<span className="text-[10px]" style={{ color: "#23d2fe", opacity: showEqualized ? 1 : 0.35 }}>Equalized</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* A/B + DIFF controls */}
|
{/* A/B + DIFF controls */}
|
||||||
@@ -286,8 +321,9 @@ function FreqChart({
|
|||||||
{/* Zero line */}
|
{/* Zero line */}
|
||||||
<line x1="0" y1={H / 2} x2={W} y2={H / 2} stroke="rgba(255,255,255,0.15)" strokeWidth="1" />
|
<line x1="0" y1={H / 2} x2={W} y2={H / 2} stroke="rgba(255,255,255,0.15)" strokeWidth="1" />
|
||||||
{/* Fill */}
|
{/* Fill */}
|
||||||
<path d={fillD} fill="rgba(255, 237, 0, 0.08)" />
|
{showEq && <path d={fillD} fill="rgba(255, 237, 0, 0.08)" />}
|
||||||
{/* Equalized curve (stroke-dash anim via useEffect) */}
|
{/* EQ curve */}
|
||||||
|
{showEq && (
|
||||||
<path
|
<path
|
||||||
ref={curvePathRef}
|
ref={curvePathRef}
|
||||||
d={pathD}
|
d={pathD}
|
||||||
@@ -297,10 +333,33 @@ function FreqChart({
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
{/* Raw curve (flat) */}
|
)}
|
||||||
<line x1="0" y1={H / 2} x2={W} y2={H / 2} stroke="rgba(255,255,255,0.2)" strokeWidth="1.5" strokeDasharray="4,3" />
|
{/* Raw */}
|
||||||
|
{rawPathD && showRaw && (
|
||||||
|
<path
|
||||||
|
d={rawPathD}
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffffff"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
opacity="0.9"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Equalized = EQ + Raw */}
|
||||||
|
{equalizedPathD && showEqualized && (
|
||||||
|
<path
|
||||||
|
d={equalizedPathD}
|
||||||
|
fill="none"
|
||||||
|
stroke="#23d2fe"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
opacity="0.95"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Band nodes with index */}
|
{/* Band nodes with index */}
|
||||||
{bands.map((band, i) => (
|
{showEq && bands.map((band, i) => (
|
||||||
<g
|
<g
|
||||||
key={i}
|
key={i}
|
||||||
style={{ cursor: "grab" }}
|
style={{ cursor: "grab" }}
|
||||||
@@ -479,6 +538,7 @@ export default function EQPage() {
|
|||||||
const [selectedCatalogModelForm, setSelectedCatalogModelForm] = useState<string | undefined>(undefined);
|
const [selectedCatalogModelForm, setSelectedCatalogModelForm] = useState<string | undefined>(undefined);
|
||||||
const [selectedCatalogTarget, setSelectedCatalogTarget] = useState<string>("");
|
const [selectedCatalogTarget, setSelectedCatalogTarget] = useState<string>("");
|
||||||
const [isConfirmingTarget, setIsConfirmingTarget] = useState(false);
|
const [isConfirmingTarget, setIsConfirmingTarget] = useState(false);
|
||||||
|
const [currentRawCurve, setCurrentRawCurve] = useState<number[] | null>(null);
|
||||||
const allowPeqRemoteSyncRef = useRef(false);
|
const allowPeqRemoteSyncRef = useRef(false);
|
||||||
const syncingHeadphoneRef = useRef(false);
|
const syncingHeadphoneRef = useRef(false);
|
||||||
const peqSyncTimerRef = useRef<number | null>(null);
|
const peqSyncTimerRef = useRef<number | null>(null);
|
||||||
@@ -786,8 +846,7 @@ export default function EQPage() {
|
|||||||
const latest = await api.getPeqState();
|
const latest = await api.getPeqState();
|
||||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||||
toast.success("已删除耳机");
|
toast.success("已删除耳机");
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("removePeq", error);
|
|
||||||
toast.error("删除耳机失败");
|
toast.error("删除耳机失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -863,8 +922,7 @@ export default function EQPage() {
|
|||||||
}
|
}
|
||||||
setIsAddPresetDialogOpen(false);
|
setIsAddPresetDialogOpen(false);
|
||||||
toast.success("已新增预设");
|
toast.success("已新增预设");
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("saveAddPreset", error);
|
|
||||||
toast.error("新增预设失败");
|
toast.error("新增预设失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -879,6 +937,7 @@ export default function EQPage() {
|
|||||||
// 获取耳机原始曲线
|
// 获取耳机原始曲线
|
||||||
const getModelCurve = async (brand: string, name: string) => {
|
const getModelCurve = async (brand: string, name: string) => {
|
||||||
try {
|
try {
|
||||||
|
console.log("[modelCurve] request", { brand, name });
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`//api.luxsin.com.cn/audio/modelCurve?brand=${encodeURIComponent(brand)}&name=${encodeURIComponent(name)}`
|
`//api.luxsin.com.cn/audio/modelCurve?brand=${encodeURIComponent(brand)}&name=${encodeURIComponent(name)}`
|
||||||
);
|
);
|
||||||
@@ -886,17 +945,74 @@ export default function EQPage() {
|
|||||||
// 使用自定义 Base64 解码
|
// 使用自定义 Base64 解码
|
||||||
const decoded = decodeCustomBase64(data);
|
const decoded = decodeCustomBase64(data);
|
||||||
const parsed = JSON.parse(decoded);
|
const parsed = JSON.parse(decoded);
|
||||||
console.log('型号的原始数据:', parsed);
|
console.log("[modelCurve] decoded", {
|
||||||
if (parsed.hasOwnProperty('fr')) {
|
hasFrRaw:
|
||||||
return parsed.fr;
|
!!(parsed &&
|
||||||
}
|
typeof parsed === "object" &&
|
||||||
return null;
|
"fr" in (parsed as Record<string, unknown>) &&
|
||||||
|
(parsed as { fr?: unknown }).fr &&
|
||||||
|
typeof (parsed as { fr?: unknown }).fr === "object" &&
|
||||||
|
"raw" in ((parsed as { fr?: Record<string, unknown> }).fr ?? {})),
|
||||||
|
});
|
||||||
|
console.log("[modelCurve] fr.raw", (parsed as { fr?: { raw?: unknown } }).fr?.raw);
|
||||||
|
return parsed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get model curve:", error);
|
console.log("[modelCurve] request failed", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadRawCurveForPeq = useCallback(
|
||||||
|
async (peq: { brand?: string; model?: string; name?: string } | undefined): Promise<number[] | null> => {
|
||||||
|
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<string, unknown>).y
|
||||||
|
?? (point as Record<string, unknown>).value
|
||||||
|
?? (point as Record<string, unknown>).db
|
||||||
|
?? (point as Record<string, unknown>).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(() => {
|
useEffect(() => {
|
||||||
async function loadHeadphones() {
|
async function loadHeadphones() {
|
||||||
@@ -914,6 +1030,7 @@ export default function EQPage() {
|
|||||||
];
|
];
|
||||||
setHeadphoneModels(defaultModels);
|
setHeadphoneModels(defaultModels);
|
||||||
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
||||||
|
setCurrentRawCurve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -928,33 +1045,14 @@ export default function EQPage() {
|
|||||||
// 修复 filters 数据(兼容不同的字段名)
|
// 修复 filters 数据(兼容不同的字段名)
|
||||||
const fixedFilters = normalizeFiltersFromPeq(currentPeq);
|
const fixedFilters = normalizeFiltersFromPeq(currentPeq);
|
||||||
|
|
||||||
console.log("Fixed filters:", fixedFilters);
|
|
||||||
|
|
||||||
// 更新 bands 状态
|
// 更新 bands 状态
|
||||||
if (fixedFilters && fixedFilters.length > 0) {
|
if (fixedFilters && fixedFilters.length > 0) {
|
||||||
setBands(fixedFilters);
|
setBands(fixedFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有 brand 和 model,获取原始曲线
|
// raw 与图表渲染统一交给 headphoneIdx/peqItems 监听逻辑处理,
|
||||||
let raw: number[] | null = null;
|
// 避免首次进入页面时重复请求 modelCurve。
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染图表(使用修复后的 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 {
|
} else {
|
||||||
// 如果没有数据,使用默认列表
|
// 如果没有数据,使用默认列表
|
||||||
@@ -968,9 +1066,9 @@ export default function EQPage() {
|
|||||||
];
|
];
|
||||||
setHeadphoneModels(defaultModels);
|
setHeadphoneModels(defaultModels);
|
||||||
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
||||||
|
setCurrentRawCurve(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("Failed to load headphone models:", error);
|
|
||||||
// 出错时使用默认列表
|
// 出错时使用默认列表
|
||||||
const defaultModels = [
|
const defaultModels = [
|
||||||
"Sennheiser HD 650",
|
"Sennheiser HD 650",
|
||||||
@@ -982,13 +1080,14 @@ export default function EQPage() {
|
|||||||
];
|
];
|
||||||
setHeadphoneModels(defaultModels);
|
setHeadphoneModels(defaultModels);
|
||||||
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
||||||
|
setCurrentRawCurve(null);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
allowPeqRemoteSyncRef.current = true;
|
allowPeqRemoteSyncRef.current = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadHeadphones();
|
loadHeadphones();
|
||||||
}, [api, isDemoMode]);
|
}, [api, isDemoMode, loadRawCurveForPeq]);
|
||||||
|
|
||||||
// 切换耳机型号后,立即用该型号 filters 刷新 10 个滤波器与曲线(暂停一次上报避免错写)
|
// 切换耳机型号后,立即用该型号 filters 刷新 10 个滤波器与曲线(暂停一次上报避免错写)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -999,10 +1098,20 @@ export default function EQPage() {
|
|||||||
syncingHeadphoneRef.current = true;
|
syncingHeadphoneRef.current = true;
|
||||||
setBands(nextBands);
|
setBands(nextBands);
|
||||||
setSelectedBand(0);
|
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(() => {
|
requestAnimationFrame(() => {
|
||||||
syncingHeadphoneRef.current = false;
|
syncingHeadphoneRef.current = false;
|
||||||
});
|
});
|
||||||
}, [headphoneIdx, peqItems]);
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [headphoneIdx, peqItems, loadRawCurveForPeq]);
|
||||||
|
|
||||||
const schedulePeqSync = (
|
const schedulePeqSync = (
|
||||||
peq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined,
|
peq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined,
|
||||||
@@ -1028,9 +1137,7 @@ export default function EQPage() {
|
|||||||
canDel: peq.canDel ?? 1,
|
canDel: peq.canDel ?? 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
console.log("upgradePeqChange payload:", payload);
|
|
||||||
upgradePeqChange(payload).catch((err) => {
|
upgradePeqChange(payload).catch((err) => {
|
||||||
console.error("upgradePeqChange", err);
|
|
||||||
toast.error("EQ 保存失败");
|
toast.error("EQ 保存失败");
|
||||||
});
|
});
|
||||||
}, delay);
|
}, delay);
|
||||||
@@ -1072,9 +1179,8 @@ export default function EQPage() {
|
|||||||
const fs = 48000;
|
const fs = 48000;
|
||||||
|
|
||||||
// Calculate coefficient matrix for each filter
|
// Calculate coefficient matrix for each filter
|
||||||
peqFilters.forEach((item, index) => {
|
peqFilters.forEach((item) => {
|
||||||
const filterType = getFilterType(item.type);
|
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(
|
const coeff = getSectionsMatrix(
|
||||||
item.gain,
|
item.gain,
|
||||||
@@ -1086,14 +1192,12 @@ export default function EQPage() {
|
|||||||
);
|
);
|
||||||
if (coeff) {
|
if (coeff) {
|
||||||
list.push(coeff);
|
list.push(coeff);
|
||||||
} else {
|
|
||||||
console.error(`Filter ${index} returned null/undefined coeff!`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get frequency response data
|
// Get frequency response data
|
||||||
const dataSet = visualizeResponse(list, fs);
|
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
|
// Get or create chart instance
|
||||||
const chartDom = document.getElementById('freq-chart');
|
const chartDom = document.getElementById('freq-chart');
|
||||||
@@ -1107,7 +1211,7 @@ export default function EQPage() {
|
|||||||
|
|
||||||
// Handle changeParam (get raw data from existing chart)
|
// Handle changeParam (get raw data from existing chart)
|
||||||
if (changeParam && myChart) {
|
if (changeParam && myChart) {
|
||||||
const option = myChart.getOption();
|
const option = myChart.getOption() as { series?: Array<{ data?: number[] }> };
|
||||||
if (option.series && option.series.length > 1) {
|
if (option.series && option.series.length > 1) {
|
||||||
raw = (option.series[1] as any).data;
|
raw = (option.series[1] as any).data;
|
||||||
}
|
}
|
||||||
@@ -1116,8 +1220,8 @@ export default function EQPage() {
|
|||||||
// Clear and rebuild chart
|
// Clear and rebuild chart
|
||||||
myChart.clear();
|
myChart.clear();
|
||||||
|
|
||||||
// Add Raw and Equalized curves if raw data is available
|
// Add Raw and Equalized curves if raw data is available (expects same 349 points as EQ curve)
|
||||||
if (raw) {
|
if (Array.isArray(raw) && raw.length === dataSet[1].length) {
|
||||||
ops.series.push({
|
ops.series.push({
|
||||||
name: 'Raw',
|
name: 'Raw',
|
||||||
data: raw,
|
data: raw,
|
||||||
@@ -1129,7 +1233,7 @@ export default function EQPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Calculate equalized curve
|
// 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({
|
ops.series.push({
|
||||||
name: 'Equalized',
|
name: 'Equalized',
|
||||||
data: equalizedRaw,
|
data: equalizedRaw,
|
||||||
@@ -1147,11 +1251,11 @@ export default function EQPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateBand = (idx: number, patch: Partial<typeof bands[0]>) => {
|
const updateBand = (idx: number, patch: Partial<typeof bands[0]>) => {
|
||||||
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
|
// Re-render chart when band changes
|
||||||
if (peqItems.length > 0 && headphoneIdx < peqItems.length) {
|
if (peqItems.length > 0 && headphoneIdx < peqItems.length) {
|
||||||
const currentPeq = peqItems[headphoneIdx];
|
renderCharts(nextBands, currentRawCurve, false);
|
||||||
renderCharts(bands, null, false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1286,6 +1390,7 @@ export default function EQPage() {
|
|||||||
{/* ── Frequency response chart ── */}
|
{/* ── Frequency response chart ── */}
|
||||||
<FreqChart
|
<FreqChart
|
||||||
bands={bands}
|
bands={bands}
|
||||||
|
rawCurve={currentRawCurve}
|
||||||
selectedBand={selectedBand}
|
selectedBand={selectedBand}
|
||||||
abMode={abMode}
|
abMode={abMode}
|
||||||
onAbToggle={setAbMode}
|
onAbToggle={setAbMode}
|
||||||
@@ -1767,10 +1872,7 @@ export default function EQPage() {
|
|||||||
|
|
||||||
const parsedObj = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
|
const parsedObj = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
|
||||||
const parametricEqRaw = parsedObj?.parametric_eq;
|
const parametricEqRaw = parsedObj?.parametric_eq;
|
||||||
if (!parametricEqRaw || typeof parametricEqRaw !== "object") {
|
if (!parametricEqRaw || typeof parametricEqRaw !== "object") return;
|
||||||
console.warn("getCurve decoded: missing parametric_eq", { brand, name, target, data: parsed });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parametricEq = parametricEqRaw as {
|
const parametricEq = parametricEqRaw as {
|
||||||
filters?: Array<{ type: string | number; fc: number; gain: number; q: number }>;
|
filters?: Array<{ type: string | number; fc: number; gain: number; q: number }>;
|
||||||
@@ -1834,16 +1936,8 @@ export default function EQPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsBrandDrawerOpen(false);
|
setIsBrandDrawerOpen(false);
|
||||||
console.log("getCurve decoded + peqChange posted:", {
|
|
||||||
brand,
|
|
||||||
name,
|
|
||||||
target,
|
|
||||||
data: parsed,
|
|
||||||
postPeq,
|
|
||||||
});
|
|
||||||
toast.success("新耳机 EQ 已上报");
|
toast.success("新耳机 EQ 已上报");
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("getCurve failed:", error);
|
|
||||||
toast.error("获取曲线失败");
|
toast.error("获取曲线失败");
|
||||||
} finally {
|
} finally {
|
||||||
setIsConfirmingTarget(false);
|
setIsConfirmingTarget(false);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Gauge,
|
Gauge,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Zap,
|
|
||||||
Wifi,
|
Wifi,
|
||||||
Bluetooth,
|
Bluetooth,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -462,7 +461,12 @@ export default function Home() {
|
|||||||
className={cn("icon-circle-btn", bypassOn && "active")}
|
className={cn("icon-circle-btn", bypassOn && "active")}
|
||||||
onClick={() => updateSetting({ dsp_enable: bypassOn ? 1 : 0 })}
|
onClick={() => updateSetting({ dsp_enable: bypassOn ? 1 : 0 })}
|
||||||
>
|
>
|
||||||
<Zap size={22} className={bypassOn ? "text-[#00FFF6]" : "text-white/60"} />
|
<img
|
||||||
|
src={`${import.meta.env.BASE_URL}home/bypass.png`}
|
||||||
|
alt="Bypass"
|
||||||
|
className="w-[25px] h-[25px] object-contain"
|
||||||
|
style={{ opacity: bypassOn ? 1 : 0.6, transform: "translateX(1.5px)" }}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<span className={cn("text-[11px]", bypassOn ? "text-[#00FFF6]" : "text-white/35")}>Bypass</span>
|
<span className={cn("text-[11px]", bypassOn ? "text-[#00FFF6]" : "text-white/35")}>Bypass</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user