Update project dependencies and configuration: Added ECharts for data visualization, integrated Terser for minification, and adjusted Vite configuration for production. Updated app title and ConnectScreen content to reflect new version. Enhanced EQPage with headphone model loading and frequency response chart rendering.

This commit is contained in:
yangy
2026-04-09 17:00:44 +08:00
parent 502f708336
commit 53f24d5bb4
33 changed files with 3522 additions and 102 deletions
+191 -8
View File
@@ -13,11 +13,19 @@
import { useDevice } from "@/contexts/DeviceContext";
import { ChevronLeft, ChevronDown, Minus, Plus, Edit3, Headphones } from "lucide-react";
import { useLocation } from "wouter";
import { useState, useMemo, useEffect } from "react";
import { useState, useMemo, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import BottomNav from "@/components/BottomNav";
import { toast } from "sonner";
import { navigateToSelect } from "./SelectPage";
import { decodeCustomBase64 } from "@/lib/luxsinApi";
import * as echarts from "echarts";
import {
getSectionsMatrix,
visualizeResponse,
getChartOps,
getFilterType,
} from "@/lib/peqAudio";
/* ── iOS Toggle ── */
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
@@ -210,6 +218,7 @@ export default function EQPage() {
const [selectedBand, setSelectedBand] = useState(3);
const [headphoneIdx, setHeadphoneIdx] = useState(deviceState?.peqSelect ?? 0);
const [headphoneModels, setHeadphoneModels] = useState<string[]>([]);
const [peqItems, setPeqItems] = useState<Array<{ name: string; brand?: string; model?: string; filters: any[] }>>([]);
const [abMode, setAbMode] = useState<"A" | "B">("A");
const [autoGain, setAutoGain] = useState(false);
@@ -220,56 +229,230 @@ export default function EQPage() {
}
}, [deviceState?.peqSelect]);
// 加载耳机列表
// 获取耳机原始曲线
const getModelCurve = async (brand: string, name: string) => {
try {
const resp = await fetch(
`//api.luxsin.com.cn/audio/modelCurve?brand=${encodeURIComponent(brand)}&name=${encodeURIComponent(name)}`
);
const data = await resp.text();
// 使用自定义 Base64 解码
const decoded = decodeCustomBase64(data);
const parsed = JSON.parse(decoded);
console.log('型号的原始数据:', parsed);
if (parsed.hasOwnProperty('fr')) {
return parsed.fr;
}
return null;
} catch (error) {
console.error("Failed to get model curve:", error);
return null;
}
};
// 加载耳机列表并初始化曲线
useEffect(() => {
async function loadHeadphones() {
if (isDemoMode || !api) {
// 演示模式使用默认列表
setHeadphoneModels([
const defaultModels = [
"Sennheiser HD 650",
"Sennheiser HD 800",
"Sony WH-1000XM5",
"AKG K701",
"Beyerdynamic DT 990",
"Audio-Technica ATH-M50x",
]);
];
setHeadphoneModels(defaultModels);
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
return;
}
try {
const peqState = await api.getPeqState();
if (peqState.peq && peqState.peq.length > 0) {
setHeadphoneModels(peqState.peq.map(h => h.name));
setPeqItems(peqState.peq);
// 初始化当前选中的耳机曲线
const currentPeq = peqState.peq[peqState.peqSelect || 0];
if (currentPeq) {
// 解析 filters(可能是 JSON 字符串或数组)
let filters: any[] = [];
if (typeof currentPeq.filters === 'string') {
try {
filters = JSON.parse(currentPeq.filters);
} catch (error) {
console.error("Failed to parse filters:", error);
filters = [];
}
} else if (Array.isArray(currentPeq.filters)) {
filters = currentPeq.filters;
}
console.log("Raw filters from API:", filters);
// 修复 filters 数据(兼容不同的字段名)
const fixedFilters = filters.map((f: any) => {
// 兼容不同的字段名:fc, freq, frequency
const freq = f.fc || f.freq || f.frequency || 1000;
return {
freq: Number(freq),
gain: Number(f.gain || 0),
q: Number(f.q || 1),
type: f.type || 'PEAK',
enabled: f.enabled !== undefined ? f.enabled : true,
};
});
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);
}
}
// 渲染图表(使用修复后的 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 {
// 如果没有数据,使用默认列表
setHeadphoneModels([
const defaultModels = [
"Sennheiser HD 650",
"Sennheiser HD 800",
"Sony WH-1000XM5",
"AKG K701",
"Beyerdynamic DT 990",
"Audio-Technica ATH-M50x",
]);
];
setHeadphoneModels(defaultModels);
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
}
} catch (error) {
console.error("Failed to load headphone models:", error);
// 出错时使用默认列表
setHeadphoneModels([
const defaultModels = [
"Sennheiser HD 650",
"Sennheiser HD 800",
"Sony WH-1000XM5",
"AKG K701",
"Beyerdynamic DT 990",
"Audio-Technica ATH-M50x",
]);
];
setHeadphoneModels(defaultModels);
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
}
}
loadHeadphones();
}, [api, isDemoMode]);
const chartRef = useRef<echarts.ECharts | null>(null);
const band = bands[selectedBand];
// Render frequency response chart using ECharts
const renderCharts = (peqFilters: typeof bands, raw: number[] | null, changeParam: boolean) => {
const list: any[] = [];
const fs = 48000;
// Calculate coefficient matrix for each filter
peqFilters.forEach((item, index) => {
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,
item.freq,
item.q,
filterType,
false,
fs
);
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');
// Get or create chart instance
const chartDom = document.getElementById('freq-chart');
if (!chartDom) return;
if (!chartRef.current) {
chartRef.current = echarts.init(chartDom);
}
const myChart = chartRef.current;
// Handle changeParam (get raw data from existing chart)
if (changeParam && myChart) {
const option = myChart.getOption();
if (option.series && option.series.length > 1) {
raw = (option.series[1] as any).data;
}
}
// Clear and rebuild chart
myChart.clear();
// Add Raw and Equalized curves if raw data is available
if (raw) {
ops.series.push({
name: 'Raw',
data: raw,
type: 'line',
showSymbol: false,
lineStyle: {
color: '#ffffff',
},
});
// Calculate equalized curve
const equalizedRaw = dataSet[1].map((value, index) => value + (raw as any)[index]);
ops.series.push({
name: 'Equalized',
data: equalizedRaw,
type: 'line',
showSymbol: false,
lineStyle: {
color: '#23d2fe',
width: 6,
opacity: 0.7,
},
});
}
myChart.setOption(ops, true);
};
const updateBand = (idx: number, patch: Partial<typeof bands[0]>) => {
setBands((prev) => prev.map((b, i) => i === idx ? { ...b, ...patch } : b));
// Re-render chart when band changes
if (peqItems.length > 0 && headphoneIdx < peqItems.length) {
const currentPeq = peqItems[headphoneIdx];
renderCharts(bands, null, false);
}
};
const totalGain = bands.reduce((sum, b) => sum + (b.enabled ? b.gain : 0), 0);