{{ obj.fc }}
+{{ getFilterShortName(obj.type) }}
+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 @@
-Pre-Amplifier Controller
+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 @@ + +
+ {{ $t('peq.edit') }}
+{{ $t('peq.headset') }}
+{{ obj.fc }}
+{{ getFilterShortName(obj.type) }}
+{{ $t('peq.filters.label') }}
+
+ {{ dropdownValue }}
+
{{ $t('peq.filters.options[0].desc1') }}
+{{ $t('peq.filters.options[0].desc2') }}
+{{ $t('peq.filters.options[0].desc3') }}
+{{ $t('peq.filters.options[1].desc1') }}
+{{ $t('peq.filters.options[1].desc2') }}
+{{ $t('peq.filters.options[1].desc3') }}
+{{ $t('peq.filters.options[2].desc1') }}
+{{ $t('peq.filters.options[2].desc2') }}
+{{ $t('peq.filters.options[2].desc3') }}
+{{ $t('peq.filters.options[3].desc1') }}
+{{ $t('peq.filters.options[3].desc2') }}
+{{ $t('peq.filters.options[4].desc1') }}
+{{ $t('peq.filters.options[4].desc2') }}
+{{ $t('peq.filters.options[5].desc1') }}
+{{ $t('peq.filters.options[5].desc2') }}
+{{ $t('peq.filters.options[6].desc1') }}
+{{ $t('peq.filters.options[6].desc2') }}
+{{ $t('peq.filters.options[7].desc1') }}
+{{ $t('peq.filters.options[7].desc2') }}
++ FREQ: {{ elSliderfreq }}Hz +
++ GAIN: {{ elSlidergain }}dB +
++ {{ $t('peq.q') }}: {{ elSliderQzhi }}dB +
+{{ $t('peq.preamp') }}:{{ preamp }}dB
++ {{ item.name }} +
++ {{ item.name }} +
++ {{ item.name }} +
++ {{ it.brand_name }} {{ it.name }} +
+{{ $t('prompt.range') }}:{{ dialogScope }}
++ {ds.bt_title} + {ds.bt_artist && - {ds.bt_artist}} +
+ )} + {/* Playback controls */} +{style.name}
-{style.description}