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:
+191
-8
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user