diff --git a/client/index.html b/client/index.html index 32f124a..d34059e 100644 --- a/client/index.html +++ b/client/index.html @@ -3,7 +3,7 @@ - Luxsin X9 Controller + Luxsin X8 Controller @@ -11,9 +11,5 @@
- diff --git a/client/public/player/music.png b/client/public/player/music.png new file mode 100644 index 0000000..f88fe9e Binary files /dev/null and b/client/public/player/music.png differ diff --git a/client/public/player/pause.png b/client/public/player/pause.png new file mode 100644 index 0000000..3e180f6 Binary files /dev/null and b/client/public/player/pause.png differ diff --git a/client/public/player/play.png b/client/public/player/play.png new file mode 100644 index 0000000..a96b455 Binary files /dev/null and b/client/public/player/play.png differ diff --git a/client/public/player/skip-back.png b/client/public/player/skip-back.png new file mode 100644 index 0000000..f2da8d8 Binary files /dev/null and b/client/public/player/skip-back.png differ diff --git a/client/public/player/skip-forward.png b/client/public/player/skip-forward.png new file mode 100644 index 0000000..823bcbb Binary files /dev/null and b/client/public/player/skip-forward.png differ diff --git a/client/public/vu/vu10.png b/client/public/vu/vu10.png new file mode 100644 index 0000000..0f5c355 Binary files /dev/null and b/client/public/vu/vu10.png differ diff --git a/client/public/vu/vu11.png b/client/public/vu/vu11.png new file mode 100644 index 0000000..c0276a2 Binary files /dev/null and b/client/public/vu/vu11.png differ diff --git a/client/public/vu/vu12.png b/client/public/vu/vu12.png new file mode 100644 index 0000000..8bdd881 Binary files /dev/null and b/client/public/vu/vu12.png differ diff --git a/client/public/vu/vu13.png b/client/public/vu/vu13.png new file mode 100644 index 0000000..4179f7a Binary files /dev/null and b/client/public/vu/vu13.png differ diff --git a/client/public/vu/vu14.png b/client/public/vu/vu14.png new file mode 100644 index 0000000..00ee2f4 Binary files /dev/null and b/client/public/vu/vu14.png differ diff --git a/client/public/vu/vu15.png b/client/public/vu/vu15.png new file mode 100644 index 0000000..15f4999 Binary files /dev/null and b/client/public/vu/vu15.png differ diff --git a/client/public/vu/vu16.png b/client/public/vu/vu16.png new file mode 100644 index 0000000..95ce949 Binary files /dev/null and b/client/public/vu/vu16.png differ diff --git a/client/public/vu/vu3.png b/client/public/vu/vu3.png index 95ce949..25240f8 100644 Binary files a/client/public/vu/vu3.png and b/client/public/vu/vu3.png differ diff --git a/client/public/vu/vu4.png b/client/public/vu/vu4.png index 00ee2f4..6eeeaa7 100644 Binary files a/client/public/vu/vu4.png and b/client/public/vu/vu4.png differ diff --git a/client/public/vu/vu5.png b/client/public/vu/vu5.png index 4179f7a..b589d71 100644 Binary files a/client/public/vu/vu5.png and b/client/public/vu/vu5.png differ diff --git a/client/public/vu/vu6.png b/client/public/vu/vu6.png index 8bdd881..cc27ab8 100644 Binary files a/client/public/vu/vu6.png and b/client/public/vu/vu6.png differ diff --git a/client/public/vu/vu7.png b/client/public/vu/vu7.png new file mode 100644 index 0000000..8492d89 Binary files /dev/null and b/client/public/vu/vu7.png differ diff --git a/client/public/vu/vu8.png b/client/public/vu/vu8.png new file mode 100644 index 0000000..2ea57af Binary files /dev/null and b/client/public/vu/vu8.png differ diff --git a/client/public/vu/vu9.png b/client/public/vu/vu9.png new file mode 100644 index 0000000..ea2c067 Binary files /dev/null and b/client/public/vu/vu9.png differ diff --git a/client/src/App.tsx b/client/src/App.tsx index 4502b6a..880d234 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,6 @@ import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { Route, Switch } from "wouter"; +import { Route, Switch, Router } from "wouter"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "./contexts/ThemeContext"; import { DeviceProvider } from "./contexts/DeviceContext"; @@ -18,7 +18,7 @@ import IOPage from "./pages/IOPage"; import VUPage from "./pages/VUPage"; import SelectPage from "./pages/SelectPage"; -function Router() { +function AppRouter() { return ( @@ -46,7 +46,9 @@ function App() { - + + + diff --git a/client/src/components/ConnectScreen.tsx b/client/src/components/ConnectScreen.tsx index ba39759..81ce723 100644 --- a/client/src/components/ConnectScreen.tsx +++ b/client/src/components/ConnectScreen.tsx @@ -40,8 +40,8 @@ export default function ConnectScreen() { -

Luxsin X9

-

Pre-Amplifier Controller

+

Luxsin X8

+

Controller

{/* Connection card */} @@ -99,19 +99,6 @@ export default function ConnectScreen() { - {/* Demo mode */} - - -

- 演示模式使用模拟数据,无需实际设备 -

); -} +} diff --git a/client/src/lib/peqAudio.ts b/client/src/lib/peqAudio.ts new file mode 100644 index 0000000..2101636 --- /dev/null +++ b/client/src/lib/peqAudio.ts @@ -0,0 +1,444 @@ +// ============================================================ +// PEQ Audio Processing - Migrated from old/audio.js +// ============================================================ + +import * as echarts from 'echarts'; + +export const TYPE_PEAK = 4; +export const TYPE_LOWSHELF = 5; +export const TYPE_HIGHSHELF = 6; +export const TYPE_LOWPASS = 0; +export const TYPE_HIGHPASS = 1; +export const TYPE_BANDPASS = 2; +export const TYPE_NOTCH = 3; +export const TYPE_ALLPASS = 7; + +// Filter type mapping +export function getFilterType(typeName: string): number { + switch (typeName) { + case 'LPF': + case 'LOW_PASS': + return TYPE_LOWPASS; + case 'HPF': + case 'HIGH_PASS': + return TYPE_HIGHPASS; + case 'BPF': + case 'BAND_PASS': + return TYPE_BANDPASS; + case 'NOTCH': + return TYPE_NOTCH; + case 'PEAK': + case 'PEAKING': + return TYPE_PEAK; + case 'LSHELF': + case 'LOW_SHELF': + return TYPE_LOWSHELF; + case 'HSHELF': + case 'HIGH_SHELF': + return TYPE_HIGHSHELF; + case 'APF': + case 'ALL_PASS': + return TYPE_ALLPASS; + default: + return TYPE_PEAK; + } +} + +export function getFilterShortName(type: string): string { + switch (type) { + case 'LOW_PASS': + return 'LPF'; + case 'HIGH_PASS': + return 'HPF'; + case 'BAND_PASS': + return 'BPF'; + case 'LOW_SHELF': + return 'LSHELF'; + case 'HIGH_SHELF': + return 'HSHELF'; + case 'ALL_PASS': + return 'APF'; + case 'PEAKING': + return 'PEAK'; + default: + return type; + } +} + +// Coefficients class +class Coeff { + b0 = 0; + b1 = 0; + b2 = 0; + a0 = 0; + a1 = 0; + a2 = 0; + + setB0(v: number) { this.b0 = v; } + setB1(v: number) { this.b1 = v; } + setB2(v: number) { this.b2 = v; } + setA0(v: number) { this.a0 = v; } + setA1(v: number) { this.a1 = v; } + setA2(v: number) { this.a2 = v; } + getB0() { return this.b0; } + getB1() { return this.b1; } + getB2() { return this.b2; } + getA0() { return this.a0; } + getA1() { return this.a1; } + getA2() { return this.a2; } +} + +// Complex number class +class Complex { + constructor(public real: number, public image: number) {} + + add(a: Complex): Complex { + return new Complex(this.real + a.real, this.image + a.image); + } + + sub(a: Complex): Complex { + return new Complex(this.real - a.real, this.image - a.image); + } + + div(a: Complex): Complex { + const denominator = a.real * a.real + a.image * a.image; + const newReal = (this.real * a.real + this.image * a.image) / denominator; + const newImage = (this.image * a.real - this.real * a.image) / denominator; + return new Complex(newReal, newImage); + } + + mul(a: Complex): Complex { + const newReal = this.real * a.real - this.image * a.image; + const newImage = this.image * a.real + this.real * a.image; + return new Complex(newReal, newImage); + } + + mulScalar(a: number): Complex { + return new Complex(this.real * a, this.image * a); + } + + addScalar(a: number): Complex { + return new Complex(this.real + a, this.image); + } +} + +function getAmplitude(complex: Complex): number { + return Math.sqrt(Math.pow(complex.real, 2) + Math.pow(complex.image, 2)); +} + +function getFreqw(coeff: Coeff, w: number): Complex { + const complex = new Complex(Math.cos(w), -Math.sin(w)); + const b2 = (complex.mul(complex)).mulScalar(coeff.getB2()); + const b1 = complex.mulScalar(coeff.getB1()); + const hup = b2.add(b1).addScalar(coeff.getB0()); + const a2 = (complex.mul(complex)).mulScalar(coeff.getA2()); + const a1 = complex.mulScalar(coeff.getA1()); + const hdown = a2.add(a1).addScalar(coeff.getA0()); + return hup.div(hdown); +} + +function getFreqwList(coeffList: Coeff[], w: number): Complex { + let h = getFreqw(coeffList[0], w); + coeffList.slice(1).forEach((item) => { + h = h.mul(getFreqw(item, w)); + }); + return h; +} + +function getFreqzn(coeff: Coeff, fs: number, f: number[]): number[] { + const n = f.length; + const h = new Array(n); + for (let i = 0; i < n; i++) { + const w = (2 * Math.PI * f[i]) / fs; + const complex = getFreqw(coeff, w); + h[i] = 20 * Math.log10(getAmplitude(complex)); + } + return h; +} + +function getFreqznList(coeffList: Coeff[], fs: number, f: number[]): number[] { + const n = f.length; + const h = new Array(n); + for (let i = 0; i < n; i++) { + const w = (2 * Math.PI * f[i]) / fs; + const complex = getFreqwList(coeffList, w); + h[i] = 20 * Math.log10(getAmplitude(complex)); + } + return h; +} + +/** + * Calculate filter coefficients matrix + */ +export function getSectionsMatrix( + gain: number, + fc: number, + q: number, + type: number, + bypass: boolean, + fs: number +): Coeff { + const w = (2 * fc) / fs; + const bypassStatus = bypass ? 1 : 0; + const bqtype = type + bypassStatus * 10; + const sA = Math.pow(10, gain / 40); + const sqrt_sA = Math.sqrt(sA); + const sin_w = Math.sin(Math.PI * w); + const cos_w = Math.cos(Math.PI * w); + const alpha = sin_w / (2 * q); + + let b0 = 0, b1 = 0, b2 = 0, a0 = 0, a1 = 0, a2 = 0; + + switch (bqtype) { + case TYPE_PEAK: + b0 = 1 + alpha * sA; + b1 = -2 * cos_w; + b2 = 1 - alpha * sA; + a0 = 1 + alpha / sA; + a1 = b1; + a2 = 1 - alpha / sA; + break; + case TYPE_LOWSHELF: + b0 = sA * ((sA + 1) - (sA - 1) * cos_w + 2 * sqrt_sA * alpha); + b1 = 2 * sA * ((sA - 1) - (sA + 1) * cos_w); + b2 = sA * ((sA + 1) - (sA - 1) * cos_w - 2 * sqrt_sA * alpha); + a0 = (sA + 1) + (sA - 1) * cos_w + 2 * sqrt_sA * alpha; + a1 = -2 * ((sA - 1) + (sA + 1) * cos_w); + a2 = (sA + 1) + (sA - 1) * cos_w - 2 * sqrt_sA * alpha; + break; + case TYPE_HIGHSHELF: + b0 = sA * ((sA + 1) + (sA - 1) * cos_w + 2 * sqrt_sA * alpha); + b1 = -2 * sA * ((sA - 1) + (sA + 1) * cos_w); + b2 = sA * ((sA + 1) + (sA - 1) * cos_w - 2 * sqrt_sA * alpha); + a0 = (sA + 1) - (sA - 1) * cos_w + 2 * sqrt_sA * alpha; + a1 = 2 * ((sA - 1) - (sA + 1) * cos_w); + a2 = (sA + 1) - (sA - 1) * cos_w - 2 * sqrt_sA * alpha; + break; + case TYPE_LOWPASS: + b0 = (1 - cos_w) / 2; + b1 = 1 - cos_w; + b2 = b0; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_HIGHPASS: + b0 = (1 + cos_w) / 2; + b1 = -(1 + cos_w); + b2 = b0; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_BANDPASS: + b0 = alpha; + b1 = 0; + b2 = -alpha; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_NOTCH: + b0 = 1; + b1 = -2 * cos_w; + b2 = 1; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_ALLPASS: + b0 = 1 - alpha; + b1 = -2 * cos_w; + b2 = 1 + alpha; + a0 = b2; + a1 = b1; + a2 = b0; + break; + default: + b0 = 1; + b1 = 0; + b2 = 0; + a0 = 1; + a1 = 0; + a2 = 0; + break; + } + + const coeff = new Coeff(); + coeff.setB0(b0 / a0); + coeff.setB1(b1 / a0); + coeff.setB2(b2 / a0); + coeff.setA0(a0 / a0); + coeff.setA1(a1 / a0); + coeff.setA2(a2 / a0); + return coeff; +} + +/** + * Visualize frequency response + * Returns [xValues, yValues] for the overall response + */ +export function visualizeResponse(coeffList: Coeff[], fs: number): [number[], number[]] { + const n = 349; + const startF = 20; + const logStep = (Math.log10(20000) - Math.log10(20)) / n; + const f = new Array(n); + const step = Math.pow(10, logStep); + + for (let i = 0; i < n; i++) { + f[i] = startF * Math.pow(step, i); + } + + const semilogf = new Array(n); + // Only calculate first 200 points for x-axis + for (let i = 0; i < 200; i++) { + semilogf[i] = Math.log10(f[i]); + } + + const validCoeffList: Coeff[] = []; + coeffList.forEach((coeff) => { + if ( + !Number.isNaN(coeff.getB0()) && + !Number.isNaN(coeff.getB1()) && + !Number.isNaN(coeff.getB2()) && + !Number.isNaN(coeff.getA0()) && + !Number.isNaN(coeff.getA1()) && + !Number.isNaN(coeff.getA2()) + ) { + validCoeffList.push(coeff); + } + }); + + const overall = getFreqznList(validCoeffList, fs, f); + return [semilogf, overall]; +} + +/** + * Get ECharts options for frequency response chart + */ +export function getChartOps( + dataSet: [number[], number[]], + yMax: number, + yMin: number, + lineColor: string +) { + const labelsToShow = [0, 43, 85, 116, 160, 200, 233, 281, 320, 348]; + const labelMap: { [key: number]: string } = { + 0: '20', + 43: '50', + 85: '100', + 116: '200', + 160: '500', + 200: '1K', + 233: '2K', + 281: '5K', + 320: '10K', + 348: '20K', + }; + + return { + xAxis: { + type: 'category' as const, + data: dataSet[0], + axisLabel: { + formatter: (value: any, index: number) => { + if (labelsToShow.includes(index)) { + return labelMap[index] || ''; + } + return ''; + }, + interval: (index: number) => labelsToShow.includes(index), + fontSize: 14, + color: '#999', + }, + splitLine: { + show: true, + lineStyle: { + type: 'solid', + color: '#3A4348', + }, + }, + axisLine: { + lineStyle: { + color: '#3A4348', + }, + }, + }, + yAxis: { + type: 'value' as const, + min: yMin, + max: yMax, + interval: 5, + axisLabel: { + fontSize: 14, + color: '#999', + }, + splitLine: { + show: true, + lineStyle: { + type: 'solid', + color: '#3A4348', + }, + }, + axisLine: { + lineStyle: { + color: '#3A4348', + }, + }, + }, + legend: { + show: true, + top: 15, + data: [ + { + name: 'Equalizer', + icon: 'circle', + textStyle: { color: lineColor }, + itemStyle: { + color: lineColor, + borderColor: lineColor, + }, + }, + { + name: 'Raw', + icon: 'circle', + textStyle: { color: '#ffffff' }, + itemStyle: { + color: '#ffffff', + borderColor: '#ffffff', + }, + }, + { + name: 'Equalized', + icon: 'circle', + textStyle: { color: '#23d2fe' }, + itemStyle: { + color: '#23d2fe', + borderColor: '#23d2fe', + }, + }, + ], + textStyle: { + fontSize: 13, + }, + }, + series: [ + { + name: 'Equalizer', + data: dataSet[1], + type: 'line' as const, + showSymbol: false, + lineStyle: { + color: lineColor, + }, + areaStyle: { + opacity: 0.3, + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#578400' }, + { offset: 1, color: '#578400' }, + ]), + }, + }, + ], + }; +} diff --git a/client/src/old/Coeff.js b/client/src/old/Coeff.js new file mode 100644 index 0000000..73fa396 --- /dev/null +++ b/client/src/old/Coeff.js @@ -0,0 +1,48 @@ +class Coeff{ + constructor() { + this.b0 = 0; + this.b1 = 0; + this.b2 = 0; + this.a0 = 0; + this.a1 = 0; + this.a2 = 0; + } + + setB0(value){ + this.b0 = value; + } + + setB1(value){ + this.b1 = value; + } + + setB2(value){ + this.b2 = value; + } + + setA0(value){ + this.a0 = value; + } + + setA1(value){ + this.a1 = value; + } + + setA2(value){ + this.a2 = value; + } + + getSectionsMatrix(gain, fc, bw, type, bypass, fs){ + const w = 2 * fc / fs; + const q = fc / bw; + const bypassStatus = bypass ? 1 : 0; + const bqtype = type + bypassStatus * 10; + const sA = Math.pow(10, gain / 40); + const sqrt_sA = Math.sqrt(sA); + const sin_w = Math.sin(Math.PI * w); + const cos_w = Math.cos(Math.PI * w); + const alpha = sin_w / (2 * q); + + let b0, b1, b2, a0, a1, a2; + } +} \ No newline at end of file diff --git a/client/src/old/audio.js b/client/src/old/audio.js new file mode 100644 index 0000000..6816132 --- /dev/null +++ b/client/src/old/audio.js @@ -0,0 +1,512 @@ +import * as echarts from 'echarts' + +export const TYPE_PEAK = 0; +export const TYPE_LOWSHELF = 1; +export const TYPE_HIGHSHELF = 2; +export const TYPE_LOWPASS = 3; +export const TYPE_HIGHPASS = 4; +export const TYPE_BANDPASS = 5; +export const TYPE_NOTCH = 6; +export const TYPE_ALLPASS = 7; +export const LPF_1ST = 8; + + + +// Audio bean +export class AudioBean { + constructor(gain, fc, fs, type, bypass, q) { + this.gain = gain; + this.fc = fc; + this.fs = fs; + this.type = type; + this.bypass = bypass; + this.q = q; + } + + getGain(){return this.gain;} + getFc(){return this.fc;} + getFs(){return this.fs;} + getType(){return this.type;} + getBypass(){return this.bypass;} + getQ(){return this.q;} + setGain(gain){this.gain = gain;} + setFc(fc){this.fc = fc;} + setFs(fs){this.fs = fs;} + setType(type){this.type = type;} + setBypass(bypass){this.bypass = bypass;} + setQ(q){this.q = q;} + + toString(){ + return "AudioTunningEQ [gain=" + this.gain + ", fc=" + this.fc + ", fs=" + this.fs + ", type=" + this.type + ", bypass=" + this.bypass + ", q=" + this.q + "]"; + } +} +//Coeff bean +class Coeff { + constructor(props) { + this.b0 = 0; + this.b1 = 0; + this.b2 = 0; + this.a0 = 0; + this.a1 = 0; + this.a2 = 0; + + } + + setB0(b0) {this.b0 = b0;} + setB1(b1) {this.b1 = b1;} + setB2(b2) {this.b2 = b2;} + setA0(a0) {this.a0 = a0;} + setA1(a1) {this.a1 = a1;} + setA2(a2) {this.a2 = a2;} + getB0(){return this.b0;} + getB1(){return this.b1;} + getB2(){return this.b2;} + getA0(){return this.a0;} + getA1(){return this.a1;} + getA2(){return this.a2;} + + toString() { + + } +} + +//Complex bean +class Complex { + constructor(real, image) { + this.real = real; + this.image = image; + } + + getReal() {return this.real;} + getImage() {return this.image;} + setReal(real) {this.real = real;} + setImage(image) {this.image = image;} + + add(a) { + return new Complex(this.real + a.real, this.image + a.image); + } + + sub(a) { // 复数相减 + return new Complex(this.real - a.real, this.image - a.image); + } + + div(a) { // 复数相除 + const denominator = a.real * a.real + a.image * a.image; + const newReal = (this.real * a.real + this.image * a.image) / denominator; + const newImage = (this.image * a.real - this.real * a.image) / denominator; + return new Complex(newReal, newImage); + } + + mul(a) { // 复数相乘 + const newReal = this.real * a.real - this.image * a.image; + const newImage = this.image * a.real + this.real * a.image; + return new Complex(newReal, newImage); + } + + mulScalar(a) { // 复数与实数相乘 + return new Complex(this.real * a, this.image * a); + } + + addScalar(a) { // 复数与实数相加 + return new Complex(this.real + a, this.image); + } +} + +function getFreqznList(coeffList, fs, f) { + let n = f.length; + let h = new Array(n); + for (let i = 0; i < n; i++) { + let w = 2*Math.PI*(f[i])/fs; + let complex = getFreqwList(coeffList, w); + h[i] = 20*Math.log10(Math.abs(getAmplitude(complex))); + } + return h; +} + +function getFreqzn(coeff, fs, f) { + let n = f.length; + let h = new Array(n); + for (let i = 0; i < n; i++) { + let w = 2*Math.PI*(f[i])/fs; + let complex = getFreqw(coeff, w); + h[i] = 20*Math.log10(Math.abs(getAmplitude(complex))); + } + return h; +} + +function getAmplitude(complex) { + return Math.sqrt(Math.pow(complex.getReal(),2)+Math.pow(complex.getImage(),2)); +} + +function getFreqw(coeff, w) { + const complex = new Complex(Math.cos(w), -Math.sin(w)); + const b2 = (complex.mul(complex)).mulScalar(coeff.getB2()); + const b1 = complex.mulScalar(coeff.getB1()); + const hup = b2.add(b1).addScalar(coeff.getB0()); + const a2 = (complex.mul(complex)).mulScalar(coeff.getA2()); + const a1 = complex.mulScalar(coeff.getA1()); + const hdown = a2.add(a1).addScalar(coeff.getA0()); + return hup.div(hdown); +} + +function getFreqwList(coeffList, w){ + let h = getFreqw(coeffList[0], w); + coeffList.slice(1).forEach((item) => { + h = h.mul(getFreqw(item, w)); + }); + return h; +} + +function test(fc, q) { + let w = fc * 2 / (48000*1000); + let down = Math.sqrt(Math.pow((1 - w * w), 2) + w * w / (q * q)); + let gain = 1 / down; + let db = 20 * Math.log10(gain); + return db; +} + +export function visualizeResponse(coeffList, fs) { + let validCoeffList = [] + let n = 349 + let startF = 20 + let endF = 20000 + let logStep = (Math.log10(20000) - Math.log10(20)) / n + let f = new Array(n) + let step = Math.pow(10, logStep) + for (let i = 0; i < n; i++) { + f[i] = startF * Math.pow(step, i) + } + + let semilogf = new Array(n) + // 注意:此函数逻辑不能改动,保持与原项目一致,仅计算前 200 个点 + for (let i = 0; i < 200; i++) { + semilogf[i] = Math.log10(f[i]) + } + + let xValues = [] + let yValues = [] + let titleList = [] + coeffList.forEach((coeff, i) => { + if ( + !Number.isNaN(coeff.getB0()) && + !Number.isNaN(coeff.getB1()) && + !Number.isNaN(coeff.getB2()) && + !Number.isNaN(coeff.getA0()) && + !Number.isNaN(coeff.getA1()) && + !Number.isNaN(coeff.getA2()) + ) { + validCoeffList.push(coeff) + let h = getFreqzn(coeff, fs, f) + xValues.push(semilogf) + yValues.push(h) + titleList.push('Band' + i) + } + }) + + if (yValues.length > 1) { + let overall = getFreqznList(validCoeffList, fs, f) + xValues.push(semilogf) + yValues.push(overall) + titleList.push('overall') + } + + let testArray = new Array(n) + for (let i = 0; i < f.length; i++) { + testArray[i] = test(f[i], 1) + } + return [xValues[xValues.length - 1], yValues[yValues.length - 1]] +} + +export function getSectionsMatrix(gain, fc, q, type, bypass, fs) { + let w = 2*fc/fs; + let bypassStatus = bypass ? 1 : 0; + let bqtype = type + bypassStatus * 10; + const sA = Math.pow(10, gain / 40); + const sqrt_sA = Math.sqrt(sA); + const sin_w = Math.sin(Math.PI * w); + const cos_w = Math.cos(Math.PI * w); + const alpha = sin_w / (2*q); + let b0, b1, b2, a0, a1, a2; + switch (bqtype){ + case TYPE_PEAK: + b0 = 1 + alpha * sA; + b1 = -2 * cos_w; + b2 = 1 - alpha * sA; + a0 = 1 + alpha / sA; + a1 = b1; + a2 = 1 - alpha / sA; + break; + case TYPE_LOWSHELF: + b0 = sA * ((sA + 1) - (sA - 1) * cos_w + 2 * sqrt_sA * alpha); + b1 = 2 * sA * ((sA - 1) - (sA + 1) * cos_w); + b2 = sA * ((sA + 1) - (sA - 1) * cos_w - 2 * sqrt_sA * alpha); + a0 = (sA + 1) + (sA - 1) * cos_w + 2 * sqrt_sA * alpha; + a1 = -2 * ((sA - 1) + (sA + 1) * cos_w); + a2 = (sA + 1) + (sA - 1) * cos_w - 2 * sqrt_sA * alpha; + break; + case TYPE_HIGHSHELF: + b0 = sA * ((sA + 1) + (sA - 1) * cos_w + 2 * sqrt_sA * alpha); + b1 = -2 * sA * ((sA - 1) + (sA + 1) * cos_w); + b2 = sA * ((sA + 1) + (sA - 1) * cos_w - 2 * sqrt_sA * alpha); + a0 = (sA + 1) - (sA - 1) * cos_w + 2 * sqrt_sA * alpha; + a1 = 2 * ((sA - 1) - (sA + 1) * cos_w); + a2 = (sA + 1) - (sA - 1) * cos_w - 2 * sqrt_sA * alpha; + break; + case TYPE_LOWPASS: + b0 = (1 - cos_w) / 2; + b1 = 1 - cos_w; + b2 = b0; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_HIGHPASS: + b0 = (1 + cos_w) / 2; + b1 = -(1 + cos_w); + b2 = b0; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_BANDPASS: + b0 = alpha; + b1 = 0; + b2 = -alpha; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_NOTCH: + b0 = 1; + b1 = -2 * cos_w; + b2 = 1; + a0 = 1 + alpha; + a1 = -2 * cos_w; + a2 = 1 - alpha; + break; + case TYPE_ALLPASS: + b0 = 1 - alpha; + b1 = -2 * cos_w; + b2 = 1 + alpha; + a0 = b2; + a1 = b1; + a2 = b0; + break; + case LPF_1ST: + let alpha_1st = sin_w / (cos_w + 1); + b0 = alpha_1st / (1 + alpha_1st); + b1 = b0; + b2 = 0; + a0 = 1; + a1 = (alpha_1st - 1) / (1 + alpha_1st); + a2 = 0; + break; + default: + b0 = 1; + b1 = 0; + b2 = 0; + a0 = 1; + a1 = 0; + a2 = 0; + break; + } + let coeff = new Coeff(); + coeff.setB0(b0/a0); + coeff.setB1(b1/a0); + coeff.setB2(b2/a0); + coeff.setA0(a0/a0); + coeff.setA1(a1/a0); + coeff.setA2(a2/a0); + return coeff; +} + +export function getFilterShortName(type){ + switch (type){ + case "LOW_PASS": return "LPF"; + case "HIGH_PASS": return "HPF"; + case "BAND_PASS": return "BPF"; + case "LOW_SHELF": return "LSHELF"; + case "HIGH_SHELF": return "HSHELF"; + case "ALL_PASS": return "APF"; + case "PEAKING": return "PEAK"; + default: return type; + } +} + +function getFilterTypeName(val){ + switch(val){ + case TYPE_LOWPASS: return "LPF"; + case TYPE_HIGHPASS: return "HPF"; + case TYPE_BANDPASS: return "BPF"; + case TYPE_NOTCH: return "NOTCH"; + case TYPE_PEAK: return "PEAK"; + case TYPE_LOWSHELF: return "LSHELF"; + case TYPE_HIGHSHELF: return "HSHELF"; + case TYPE_ALLPASS: return "APF"; + } +} + +export function getFilterType(typeName) { + if(typeName === 'LPF' || typeName === 'LOWP_ASS'){ + return TYPE_LOWPASS; + } + else if(typeName === 'HPF' || typeName === 'HIGH_PASS'){ + return TYPE_HIGHPASS; + } + else if(typeName === 'BPF' || typeName === 'BAND_PASS'){ + return TYPE_BANDPASS; + } + else if(typeName === 'NOTCH'){ + return TYPE_NOTCH; + } + else if(typeName === 'PEAK' || typeName === 'PEAKING'){ + return TYPE_PEAK; + } + else if(typeName === 'LSHELF' || typeName === 'LOW_SHELF'){ + return TYPE_LOWSHELF; + } + else if(typeName === 'HSHELF' || typeName === 'HIGH_SHELF'){ + return TYPE_HIGHSHELF; + } + else if(typeName === 'APF' || typeName === 'ALL_PASS'){ + return TYPE_ALLPASS; + } +} + +export function getChartOps(dataSet, yMax, yMin, lineColor) { + //要显示的x轴刻度 + const labelsToShow = [0, 43, 85, 116, 160, 200, 233, 281, 320, 348] + + return { + xAxis: { + type: 'category', + data: dataSet[0], + axisLabel: { + formatter: function (value, index) { + // 指定只显示 index 在(0,47,93,115)的标签 + if (labelsToShow.includes(index)) { + switch (index) { + case 0: + return '20' + case 43: + return '50' + case 85: + return '100' + case 116: + return '200' + case 160: + return '500' + case 200: + return '1K' + case 233: + return '2K' + case 281: + return '5K' + case 320: + return '10K' + case 348: + return '20K' + } + } + }, + interval: function (index) { + return labelsToShow.includes(index) + }, + fontSize: 14, + color: '#999' + }, + splitLine: { + show: true, + lineStyle: { + type: 'solid', + color: '#3A4348' + } + }, + axisLine: { + lineStyle: { + color: '#3A4348' + } + } + }, + yAxis: { + type: 'value', + min: yMin, + max: yMax, + interval: 5, + + axisLabel: { + fontSize: 14, + color: '#999' + }, + splitLine: { + show: true, + lineStyle: { + type: 'solid', + color: '#3A4348' + } + }, + axisLine: { + lineStyle: { + color: '#3A4348' + } + } + }, + legend: { + show: true, + top: 15, + data: [ + { + name: 'Equalizer', + icon: 'circle', + textStyle: { color: lineColor }, // 单独设置颜色 + itemStyle: { + color: lineColor, // 设置图标颜色 + borderColor: lineColor // 如果需要边框 + } + }, + { + name: 'Raw', + icon: 'circle', + textStyle: { color: '#ffffff' }, + itemStyle: { + color: '#ffffff', // 设置图标颜色 + borderColor: '#ffffff' // 如果需要边框 + } + }, + { + name: 'Equalized', + icon: 'circle', + textStyle: { color: '#23d2fe' }, + itemStyle: { + color: '#23d2fe', // 设置图标颜色 + borderColor: '#23d2fe' // 如果需要边框 + } + } + ], + textStyle: { + fontSize: 13 + } + }, + series: [ + { + name: 'Equalizer', + data: dataSet[1], + type: 'line', + showSymbol: false, + lineStyle: { + color: lineColor + }, + areaStyle: { + opacity: 0.3, + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#578400' }, + { + offset: 1, + color: '#578400' + } + ]) + } + } + ] + } +} diff --git a/client/src/old/peq.js b/client/src/old/peq.js new file mode 100644 index 0000000..98a06cb --- /dev/null +++ b/client/src/old/peq.js @@ -0,0 +1,314 @@ +import axios from 'axios' +import { base64DecodedToString, base64Encoded, PEQAPI, setProperty, UPGRADEPEQ } from '@/api/common.js' +import { ref } from 'vue' + +import { + AudioBean, + getChartOps, + getFilterType, + getSectionsMatrix, + visualizeResponse +} from '@/api/audio.js' +import * as echarts from 'echarts' +import { i18n } from '@/main.js' +import { ElMessageBox } from 'element-plus' +export const getPeqData = async () => { + const peqItems = ref([]) + const peqSelect = ref(0) + const resp = await axios.get(PEQAPI) + const peqEnable = ref(0) + const msgCount = ref(0); + const data = JSON.parse(base64DecodedToString(resp.data)); + + console.log('peq 数据=', data); + if(data.peq.length > 0){ + data.peq.forEach((item, index) => { + item['index'] = index + item['filters'] = JSON.parse(item.filters) + }) + peqItems.value = data.peq + peqSelect.value = data.peqSelect + } + peqEnable.value = data.peqEnable + msgCount.value = data.msgCount + return { + peqItems, + peqSelect, + peqEnable, + msgCount + } +} + +export function changePeq(val, peqItems) { + setProperty('peqSelect', val) + + const selectedItem = peqItems.find((item) => item.index === val) + if (selectedItem) { + console.log('选中了:' + selectedItem.name + ' filters = ' + selectedItem.filters) + } + + return true +} + +export const renderCharts = (peq, raw, changeParam) => { + const list = [] + const fs = 48000 + peq.filters.forEach((item) => { + const bean = new AudioBean(item.gain, item.fc, fs, getFilterType(item.type), false, item.q) + list.push( + getSectionsMatrix( + bean.getGain(), + bean.getFc(), + bean.getQ(), + bean.getType(), + bean.getBypass(), + bean.getFs() + ) + ) + }) + const dataSet = visualizeResponse(list, fs) + const ops = getChartOps(dataSet, 20, -20, '#FFED00') + + // 统一获取DOM元素 + const chartDom = document.getElementById('main') + + // 尝试获取已存在的实例 + let myChart = echarts.getInstanceByDom(chartDom) + + // 如果实例不存在,则初始化 + if (!myChart) { + myChart = echarts.init(chartDom) + } + + if (changeParam && myChart) { + // 只有在changeParam为true且图表存在时才获取原始数据 + raw = myChart.getOption().series[1].data + } + + // 不再重新初始化,直接使用现有的myChart实例 + myChart.clear() + + if (raw) { + ops.series.push({ + name: 'Raw', + data: raw, + type: 'line', + showSymbol: false, + lineStyle: { + color: '#ffffff' + } + }) + + const equalized_raw = dataSet[1].map((value, index) => value + raw[index]) + ops.series.push({ + name: 'Equalized', + data: equalized_raw, + type: 'line', + showSymbol: false, + lineStyle: { + color: '#23d2fe', + width: 6, + opacity: 0.7 + } + }) + } + + myChart.setOption(ops, true) +} + +/** + * 切换auto开关 + * @param peq + */ +export const upgradeAutoPre = (peq, autoPre) => { + const filters = peq.filters.map((item) => ({ + type: getFilterVal(item.type), + fc: item.fc, + gain: item.gain, + q: item.q + })) + const data = { + peqChange: { + name: peq.name, + filters: filters, + autoPre: autoPre ? 1 : 0, + preamp: peq.preamp, + canDel: peq.canDel + } + } + upgradePeq(data) + peq.autoPre = autoPre ? 1 : 0 +} + +//修改总增益 +export const upgradePreamp = (peq, preamp) => { + const filters = peq.filters.map((item) => ({ + type: getFilterVal(item.type), + fc: item.fc, + gain: item.gain, + q: item.q + })) + const data = { + peqChange: { + name: peq.name, + filters: filters, + autoPre: peq.autoPre, + preamp: preamp, + canDel: peq.canDel + } + } + upgradePeq(data) + + peq.preamp = preamp +} + +export const upgradeEdit = (peq) => { + const filters = peq.filters.map((item) => ({ + type: getFilterVal(item.type), + fc: item.fc, + gain: item.gain, + q: item.q + })) + const data = { + peqChange: { + name: peq.name, + filters: filters, + autoPre: peq.autoPre, + preamp: peq.preamp, + canDel: peq.canDel + } + } + upgradePeq(data) +} + +//这里有个问题,当接口超时时,由于代码是同步,所以会一直阻塞等待 +export const upgradePeq = async (peq) => { + await axios.post( + UPGRADEPEQ, + { json: base64Encoded(JSON.stringify(peq)) }, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } } + ) +} + +export const getFilterVal = (filterName) => { + let result = -1 + if (filterName === 'HIGH_PASS' || filterName === 'HPF') { + result = 1 + } else if (filterName === 'BAND_PASS' || filterName === 'BPF') { + result = 2 + } else if (filterName === 'NOTCH') { + result = 3 + } else if (filterName === 'PEAKING' || filterName === 'PEAK') { + result = 4 + } else if (filterName === 'LOW_SHELF' || filterName === 'LSHELF') { + result = 5 + } else if (filterName === 'HIGH_SHELF' || filterName === 'HSHELF') { + result = 6 + } else if (filterName === 'ALL_PASS' || filterName === 'APF') { + result = 7 + } else if (filterName === 'LOW_PASS' || filterName === 'LPF') { + result = 0 + } + return result +} + +/** + * 改变滤波器的属性(频率,gain,q, 类型) + * @param currentFilter 当前选中的滤波器 + * @param val 值 + */ +export const changeFilterVal = (currentFilter, val, name, peq) => { + //currentFilter !== undefined 表示当前有某一个滤波器被选中,才需要更新下面的值 + if (currentFilter !== undefined) { + if (name === 'freq') { + currentFilter.fc = val + } else if (name === 'gain') { + currentFilter.gain = val + } else if (name === 'q') { + currentFilter.q = val + } else if (name === 'type') { + currentFilter.type = val + } + + //调用接口,更新peq + //需要将滤波器类型转换为数字,因为是引用类型,所以这里还需要遍历filters,声明新的filters,将值传过去,不能直接用peq.filters + const filters = peq.filters.map((item) => ({ + type: getFilterVal(item.type), + fc: item.fc, + gain: item.gain, + q: item.q + })) + + const data = { + peqChange: { + name: peq.name, + filters: filters, + autoPre: peq.autoPre, + preamp: peq.preamp, + canDel: peq.canDel + } + } + upgradePeq(data) + renderCharts(peq) + } +} + +/** + * 渲染添加耳机型号窗口数据 + */ +export const renderAddHeadPhone = () => { + const menus = [ + { value: 'Brands', label: 'Brands' }, + { value: 'Models', label: 'Models' }, + { value: 'Target', label: 'Target' } + ] + const selectedMenu = ref('Brands') + const targetList = ref([ + { name: 'Harman over-ear 2018', bassBoost: { fc: 105, q: 0.7, gain: 6 } , ear: 'over'}, + { name: 'HMS II.3 Harman over-ear 2018', bassBoost: { fc: 105, q: 0.7, gain: 6 }, ear: 'over'}, + { name: 'crinacle EARS + 711 Harman over-ear 2018', bassBoost: { fc: 105, q: 0.7, gain: 6 } , ear: 'over'}, + { name: 'Harman in-ear 2019', bassBoost: { fc: 105, q: 0.7, gain: 9.5 } ,ear: 'in'}, + { name: 'AutoEq in-ear', bassBoost: { fc: 105, q: 0.7, gain: 8 }, ear: 'in'}, + { name: 'HMS II.3 AutoEq in-ear', bassBoost: { fc: 105, q: 0.7, gain: 8 } ,ear: 'in'}, + { name: 'HMS II.3 Harman in-ear 2019', bassBoost: { fc: 105, q: 0.7, gain: 9.5 } ,ear: 'in'}, + { name: 'Diffuse Field 5128 (-1 dB/oct)', bassBoost: { fc: 105, q: 0.7, gain: 0 }, ear: 'over'}, + { name: 'LMG 5128 0.6 without bass', bassBoost: { fc: 105, q: 0.7, gain: 6 } ,ear: 'over'}, + { name: 'JM-1 with Harman filters', bassBoost: { fc: 105, q: 0.7, gain: 6.5 } ,ear: 'all'}, + { name: 'oratory1990 in-ear', bassBoost: { fc: 105, q: 0.7, gain: 9.5 } ,ear: 'in'}, + { name: 'oratory1990 over-ear', bassBoost: { fc: 105, q: 0.7, gain: 6 } ,ear: 'over'}, + { name: 'Harman over-ear 2013', bassBoost: { fc: 105, q: 0.7, gain: 6 } ,ear: 'over'}, + { name: 'Flat', bassBoost: { fc: 105, q: 0.7, gain: 0 } ,ear: 'all'} + ]); + return { menus, selectedMenu, targetList } +} + +export const getCurve = async (brand, name, target) =>{ + const resp = await axios.get('//api.luxsin.com.cn/audio/getCurve?brand='+encodeURIComponent(brand)+'&name='+encodeURIComponent(name)+'&target='+encodeURIComponent(target)); + const data = JSON.parse(base64DecodedToString(resp.data)); + + if(data.hasOwnProperty('parametric_eq')) { + return data.parametric_eq; + } + return null; +}; + +export const getModelCurve = async (brand, name) => { + const resp = await axios.get( + '//api.luxsin.com.cn/audio/modelCurve?brand=' + + encodeURIComponent(brand) + + '&name=' + + encodeURIComponent(name) + ) + const data = JSON.parse(base64DecodedToString(resp.data)) + console.log('型号的原始数据:', data) + if (data.hasOwnProperty('fr')) { + return data.fr + } + return null +} + +export const msg = (context) => { + ElMessageBox.alert(context, i18n.global.t('prompt.prompt'), { + confirmButtonText: i18n.global.t('prompt.confirm') + }) +} diff --git a/client/src/old/peq.vue b/client/src/old/peq.vue new file mode 100644 index 0000000..a7035bd --- /dev/null +++ b/client/src/old/peq.vue @@ -0,0 +1,1830 @@ + + + + + diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 4bd4313..0dd798d 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -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([]); + const [peqItems, setPeqItems] = useState>([]); 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(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) => { 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); diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index ba6edfa..6515ae6 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -24,6 +24,7 @@ import { ChevronDown, Zap, Wifi, + Bluetooth, } from "lucide-react"; import { useLocation } from "wouter"; import { useRef, useState, useEffect, useCallback } from "react"; @@ -110,17 +111,13 @@ export default function Home() { const vuLabel = `VU表${(ds.vu ?? 0) + 1}`; const bypassOn = (ds.dsp_enable ?? 0) === 0; - // Slider fill % - const fillPct = localVol; + // Slider fill % (0-200 → 0-100%) + const fillPct = (localVol / 200) * 100; return (
{/* ── Top bar ── */} -
- +
- {/* Track fill */} + {/* Track fill - 根据按钮位置调整 */}
+ style={{ + width: `${fillPct}%`, + background: "#00FFF6", + boxShadow: "0 0 8px rgba(0,255,246,0.5)" + }} />
+ + {/* Bluetooth info */} + {ds.bt_status !== 0 && ( +
+
+ + {ds.bt_srcname} +
+ {ds.bt_title && ( +

+ {ds.bt_title} + {ds.bt_artist && - {ds.bt_artist}} +

+ )} + {/* Playback controls */} +
+ + + +
+
+ )}
{/* ── 4 quick-action circle buttons ── */} diff --git a/client/src/pages/VUPage.tsx b/client/src/pages/VUPage.tsx index e2d8f9c..46ac04e 100644 --- a/client/src/pages/VUPage.tsx +++ b/client/src/pages/VUPage.tsx @@ -13,48 +13,14 @@ import { useLocation } from "wouter"; interface VUStyle { index: number; name: string; - description: string; preview: string; } -const VU_STYLES: VUStyle[] = [ - { - index: 0, - name: "经典复古", - description: "琥珀色指针表盘,经典模拟风格", - preview: "/vu/vu1.png", - }, - { - index: 1, - name: "现代平面", - description: "LED 电平条,简洁现代风格", - preview: "/vu/vu2.png", - }, - { - index: 2, - name: "数字显示", - description: "数字电平显示,精准直观", - preview: "/vu/vu3.png", - }, - { - index: 3, - name: "条形图", - description: "垂直条形图,专业风格", - preview: "/vu/vu4.png", - }, - { - index: 4, - name: "渐变光谱", - description: "彩虹渐变光谱,炫酷动感", - preview: "/vu/vu5.png", - }, - { - index: 5, - name: "频谱分析", - description: "实时频谱分析,专业监测", - preview: "/vu/vu6.png", - }, -]; +const VU_STYLES: VUStyle[] = Array.from({ length: 16 }, (_, i) => ({ + index: i, + name: `VU ${i + 1}`, + preview: `/vu/vu${i + 1}.png`, +})); export default function VUPage() { const [, setLocation] = useLocation(); @@ -112,7 +78,6 @@ export default function VUPage() {

{style.name}

-

{style.description}

); diff --git a/package.json b/package.json index 9a6ee1b..cc53c93 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "echarts": "^6.0.0", "embla-carousel-react": "^8.6.0", "express": "^4.21.2", "framer-motion": "^12.23.22", @@ -81,6 +82,7 @@ "postcss": "^8.4.47", "prettier": "^3.6.2", "tailwindcss": "^4.1.14", + "terser": "^5.46.1", "tsx": "^4.19.1", "tw-animate-css": "^1.4.0", "typescript": "5.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82db073..fdc7c27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,9 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + echarts: + specifier: ^6.0.0 + version: 6.0.0 embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.2.1) @@ -172,13 +175,13 @@ importers: devDependencies: '@builder.io/vite-plugin-jsx-loc': specifier: ^0.1.1 - version: 0.1.1(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) + version: 0.1.1(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6)) '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.19(tailwindcss@4.1.14) '@tailwindcss/vite': specifier: ^4.1.3 - version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) + version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6)) '@types/express': specifier: 4.17.21 version: 4.17.21 @@ -196,7 +199,7 @@ importers: version: 19.2.1(@types/react@19.2.1) '@vitejs/plugin-react': specifier: ^5.0.4 - version: 5.0.4(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) + version: 5.0.4(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6)) add: specifier: ^2.0.6 version: 2.0.6 @@ -218,6 +221,9 @@ importers: tailwindcss: specifier: ^4.1.14 version: 4.1.14 + terser: + specifier: ^5.46.1 + version: 5.46.1 tsx: specifier: ^4.19.1 version: 4.20.6 @@ -229,13 +235,13 @@ importers: version: 5.6.3 vite: specifier: ^7.1.7 - version: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + version: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6) vite-plugin-manus-runtime: specifier: ^0.0.57 version: 0.0.57 vitest: specifier: ^2.1.4 - version: 2.1.9(@types/node@24.7.0)(lightningcss@1.30.1) + version: 2.1.9(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1) packages: @@ -695,6 +701,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1826,6 +1835,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1900,6 +1912,9 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -2177,6 +2192,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3176,6 +3194,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -3225,6 +3250,11 @@ packages: resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3267,6 +3297,9 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3532,6 +3565,9 @@ packages: zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3666,10 +3702,10 @@ snapshots: estree-walker: 2.0.2 magic-string: 0.30.19 - '@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': + '@builder.io/vite-plugin-jsx-loc@0.1.1(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6))': dependencies: '@builder.io/jsx-loc-internals': 0.0.1 - vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6) '@chevrotain/cst-dts-gen@11.0.3': dependencies: @@ -3890,6 +3926,11 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -4728,12 +4769,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.14 - '@tailwindcss/vite@4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': + '@tailwindcss/vite@4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6))': dependencies: '@tailwindcss/node': 4.1.14 '@tailwindcss/oxide': 4.1.14 tailwindcss: 4.1.14 - vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6) '@types/babel__core@7.20.5': dependencies: @@ -4966,7 +5007,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': + '@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -4974,7 +5015,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.38 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6) transitivePeerDependencies: - supports-color @@ -4985,13 +5026,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1))': + '@vitest/mocker@2.1.9(vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1) + vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1) '@vitest/pretty-format@2.1.9': dependencies: @@ -5084,6 +5125,8 @@ snapshots: node-releases: 2.0.23 update-browserslist-db: 1.1.3(browserslist@4.26.3) + buffer-from@1.1.2: {} + bytes@3.1.2: {} cac@6.7.14: {} @@ -5160,6 +5203,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@2.20.3: {} + commander@7.2.0: {} commander@8.3.0: {} @@ -5433,6 +5478,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + ee-first@1.1.1: {} electron-to-chromium@1.5.232: {} @@ -6835,6 +6885,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} stackback@0.0.2: {} @@ -6896,6 +6953,13 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + terser@5.46.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -6923,6 +6987,8 @@ snapshots: ts-dedent@2.2.0: {} + tslib@2.3.0: {} + tslib@2.8.1: {} tsx@4.20.6: @@ -7064,13 +7130,13 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@2.1.9(@types/node@24.7.0)(lightningcss@1.30.1): + vite-node@2.1.9(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1) + vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1) transitivePeerDependencies: - '@types/node' - less @@ -7092,7 +7158,7 @@ snapshots: react-dom: 19.2.1(react@19.2.1) tailwind-merge: 3.3.1 - vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1): + vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -7101,8 +7167,9 @@ snapshots: '@types/node': 24.7.0 fsevents: 2.3.3 lightningcss: 1.30.1 + terser: 5.46.1 - vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6): + vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.1)(tsx@4.20.6): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -7115,12 +7182,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.1 + terser: 5.46.1 tsx: 4.20.6 - vitest@2.1.9(@types/node@24.7.0)(lightningcss@1.30.1): + vitest@2.1.9(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)) + '@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -7136,8 +7204,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 1.2.0 - vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1) - vite-node: 2.1.9(@types/node@24.7.0)(lightningcss@1.30.1) + vite: 5.4.20(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1) + vite-node: 2.1.9(@types/node@24.7.0)(lightningcss@1.30.1)(terser@5.46.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.7.0 @@ -7189,4 +7257,8 @@ snapshots: zod@4.1.12: {} + zrender@6.0.0: + dependencies: + tslib: 2.3.0 + zwitch@2.0.4: {} diff --git a/vite.config.ts b/vite.config.ts index 9c60e63..2322e3b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -154,6 +154,7 @@ const plugins = [react(), tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime( export default defineConfig({ plugins, + base: process.env.NODE_ENV === 'production' ? '/x8/v2/public/' : '/', resolve: { alias: { "@": path.resolve(import.meta.dirname, "client", "src"), @@ -163,9 +164,31 @@ export default defineConfig({ }, envDir: path.resolve(import.meta.dirname), root: path.resolve(import.meta.dirname, "client"), + publicDir: path.resolve(import.meta.dirname, "client/public"), + // 性能优化 build: { + target: 'esnext', + cssTarget: 'esnext', outDir: path.resolve(import.meta.dirname, "dist/public"), emptyOutDir: true, + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + }, + rollupOptions: { + output: { + manualChunks: { + 'react-vendor': ['react', 'react-dom'], + 'router-vendor': ['wouter'], + 'ui-vendor': ['lucide-react', 'class-variance-authority', 'clsx', 'tailwind-merge'], + }, + }, + }, + chunkSizeWarningLimit: 500, + cssCodeSplit: true, }, server: { port: 3000,