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:
@@ -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' },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user