This commit is contained in:
yangy
2026-05-26 18:46:41 +08:00
parent 0c9e42b91c
commit 47ea7d7b56
13 changed files with 1370 additions and 121 deletions
+34 -1
View File
@@ -103,6 +103,13 @@ export interface DeviceState {
subwoofer_value: number;
subwoofer_rate: number;
subwoofer_gain: number;
subwoofer_mix_type: number;
subwoofer_delay: number;
subwoofer_delay_main: number;
subwoofer_delay_r: number;
subwoofer_delay_main_r: number;
subwoofer_lpf_enable: number;
subwoofer_hpf_enable: number;
loudness_enable: number;
loudness_bass_gain: number;
loudness_treble_gain: number;
@@ -163,6 +170,9 @@ export interface PeqApplyPayload {
// ============================================================
export const INPUT_LABELS = ["USB", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA"];
export const OUTPUT_LABELS = ["XLR", "RCA", "Headset", "XLR/RCA"];
/** Device `output` index for headphone / headset. */
export const OUTPUT_HEADSET_INDEX = 2;
export const LANGUAGE_LABELS = ["English", "繁體中文", "简体中文"];
export const SCREEN_LIGHT_LABELS = ["Bright", "Medium", "Dark"];
export const KNOB_LIGHT_LABELS = ["Off", "Bright", "Medium", "Dark"];
@@ -213,7 +223,9 @@ export class LuxsinAPI {
const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncData`);
const text = await response.text();
const decoded = decodeCustomBase64(text.trim());
return JSON.parse(decoded) as DeviceState;
const state = JSON.parse(decoded) as DeviceState;
console.log("[syncData] decoded", state);
return state;
} catch (error) {
this.maybeRedirectForHttpsCert(error);
throw error;
@@ -333,6 +345,20 @@ export function parseFirmwareVersion(version: unknown): number {
return Number(parts[parts.length - 1]);
}
/** Pre-out volume passthrough mode selected (0dB or -12dB). */
export function isVolumePassthroughActive(dacVolumeDirect: number | undefined): boolean {
return dacVolumeDirect === 1 || dacVolumeDirect === 2;
}
/** Lock home volume controls when passthrough is on, except output is headphone. */
export function isHomeVolumeLockedByPassthrough(
dacVolumeDirect: number | undefined,
output: number | undefined,
): boolean {
if (!isVolumePassthroughActive(dacVolumeDirect)) return false;
return (output ?? -1) !== OUTPUT_HEADSET_INDEX;
}
/** Max `bootSound` index available for the current firmware. */
export function getBootSoundMaxIndex(firmwareVersion: number): number {
return firmwareVersion >= 26 ? 10 : 6;
@@ -401,6 +427,13 @@ export const MOCK_DEVICE_STATE: DeviceState = {
subwoofer_value: 80,
subwoofer_rate: 0,
subwoofer_gain: 0,
subwoofer_mix_type: 0,
subwoofer_delay: 582,
subwoofer_delay_main: 571,
subwoofer_delay_r: 582,
subwoofer_delay_main_r: 571,
subwoofer_lpf_enable: 0,
subwoofer_hpf_enable: 0,
loudness_enable: 0,
loudness_bass_gain: 3,
loudness_treble_gain: 2,
+53 -32
View File
@@ -12,6 +12,8 @@ export const TYPE_HIGHPASS = 1;
export const TYPE_BANDPASS = 2;
export const TYPE_NOTCH = 3;
export const TYPE_ALLPASS = 7;
/** First-order low-pass (subwoofer LPF curve). */
export const LPF_1ST = 8;
// Filter type mapping (device uses numeric type; pass-through when already a number)
export function getFilterType(typeName: string | number): number {
@@ -257,6 +259,16 @@ export function getSectionsMatrix(
a1 = b1;
a2 = b0;
break;
case LPF_1ST: {
const alpha1st = sin_w / (cos_w + 1);
b0 = alpha1st / (1 + alpha1st);
b1 = b0;
b2 = 0;
a0 = 1;
a1 = (alpha1st - 1) / (1 + alpha1st);
a2 = 0;
break;
}
default:
b0 = 1;
b1 = 0;
@@ -440,7 +452,8 @@ export function getChartOps(
dataSet: [number[], number[]],
yMax: number,
yMin: number,
lineColor: string
lineColor: string,
withPeqLegend = true,
) {
const labelsToShow = [0, 43, 85, 116, 160, 200, 233, 281, 320, 348];
const labelMap: { [key: number]: string } = {
@@ -507,44 +520,52 @@ export function getChartOps(
},
},
legend: {
show: true,
show: withPeqLegend,
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',
},
},
],
data: withPeqLegend
? [
{
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,
},
},
grid: {
left: 48,
right: 16,
top: withPeqLegend ? 40 : 12,
bottom: 28,
},
series: [
{
name: 'Equalizer',
name: withPeqLegend ? 'Equalizer' : 'LPF',
data: dataSet[1],
type: 'line' as const,
showSymbol: false,
+66
View File
@@ -0,0 +1,66 @@
/**
* Subwoofer low-pass frequency response — ported from olds/effect.js (painting, getLps).
* Uses peqAudio getSectionsMatrix + visualizeResponse (same as EQ page).
*/
import {
getSectionsMatrix,
LPF_1ST,
TYPE_LOWPASS,
visualizeResponse,
} from "@/lib/peqAudio";
export const SUBWOOFER_LPF_FS = 48000;
const SUBWOOFER_PARAM = [1, 1, 1.35, 1.28, 1.5, 1.4, 1.65, 1.55];
function pushSection(list: ReturnType<typeof getSectionsMatrix>[], fc: number, type: number) {
list.push(getSectionsMatrix(0, fc, 0.707, type, false, SUBWOOFER_LPF_FS));
}
/**
* Slope layout: [2nd-order count, 1st-order count] — see olds/effect.js getLps.
*/
export function getSubwooferLps(lpsIndex: number, lpsVal: number): [number, number] {
let count12 = 0;
let count6 = 1;
if (lpsIndex > 1) {
const tmp = lpsVal / 12;
if (!Number.isInteger(tmp)) {
count12 = Math.floor(tmp);
} else {
count12 = tmp;
count6 = 0;
}
} else if (lpsIndex === 1) {
count12 = 1;
count6 = 0;
}
return [count12, count6];
}
/**
* Build cascaded LPF coeffs and return log-frequency magnitude plot data.
*/
export function buildSubwooferLpfResponse(
fc: number,
lpsIndex: number,
): [number[], number[]] {
const lpsVal = (lpsIndex + 1) * 6;
const lpsArray = getSubwooferLps(lpsIndex, lpsVal);
const fcAdj = fc * SUBWOOFER_PARAM[lpsIndex];
const list: ReturnType<typeof getSectionsMatrix>[] = [];
if (lpsArray[0] === 0) {
pushSection(list, fcAdj, LPF_1ST);
} else {
for (let i = 0; i < lpsArray[0]; i += 1) {
pushSection(list, fcAdj, TYPE_LOWPASS);
}
if (lpsArray[1] > 0) {
for (let i = 0; i < lpsArray[1]; i += 1) {
pushSection(list, fcAdj, LPF_1ST);
}
}
}
return visualizeResponse(list, SUBWOOFER_LPF_FS);
}