228 lines
6.3 KiB
TypeScript
228 lines
6.3 KiB
TypeScript
import type { PeqFilter, PeqState } from "@/lib/luxsinApi";
|
|
import { normalizePeqFiltersForSubmit } from "@/lib/luxsinApi";
|
|
import { getFilterShortName, getFilterType } from "@/lib/peqAudio";
|
|
import {
|
|
BAND_FREQ_MIN,
|
|
BAND_FREQ_MAX,
|
|
BAND_GAIN_MIN,
|
|
BAND_GAIN_MAX,
|
|
BAND_Q_MIN,
|
|
BAND_Q_MAX,
|
|
type BandParamKind,
|
|
type PeqCatalogItem,
|
|
} from "./constants";
|
|
|
|
/* ── Template interpolation ── */
|
|
export function eqInterp(
|
|
template: string | undefined,
|
|
vars: Record<string, string | number>,
|
|
): string {
|
|
if (!template) return "";
|
|
let s = template;
|
|
for (const [k, v] of Object.entries(vars)) {
|
|
s = s.split(`{{${k}}}`).join(String(v));
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/* ── Band frequency display formatting ── */
|
|
export function formatBandFreqDisplay(freq: number) {
|
|
return freq >= 1000
|
|
? `${(freq / 1000).toFixed(2).replace(/\.?0+$/, "")} kHz`
|
|
: `${freq} Hz`;
|
|
}
|
|
|
|
export function formatBandGainDisplay(gain: unknown) {
|
|
const db = typeof gain === "number" && Number.isFinite(gain) ? gain : Number(gain);
|
|
if (!Number.isFinite(db)) return "0.0 dB";
|
|
const text = db.toFixed(1);
|
|
return `${db >= 0 ? `+${text}` : text} dB`;
|
|
}
|
|
|
|
export function formatBandQDisplay(q: unknown) {
|
|
const n = typeof q === "number" && Number.isFinite(q) ? q : Number(q);
|
|
return Number.isFinite(n) ? n.toFixed(2) : "—";
|
|
}
|
|
|
|
/* ── Band param value for input field ── */
|
|
export function formatBandParamForInput(
|
|
kind: BandParamKind,
|
|
band: { freq: number; gain: number; q: number },
|
|
) {
|
|
switch (kind) {
|
|
case "freq":
|
|
return String(band.freq);
|
|
case "gain":
|
|
return band.gain.toFixed(1);
|
|
case "q":
|
|
return band.q.toFixed(2);
|
|
}
|
|
}
|
|
|
|
/* ── Parse band param input with validation ── */
|
|
export function parseBandParamInput(
|
|
kind: BandParamKind,
|
|
raw: string,
|
|
messages: { invalid: string; outOfRange: string },
|
|
): { ok: true; value: number } | { ok: false; message: string } {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) return { ok: false, message: messages.invalid };
|
|
|
|
if (kind === "freq") {
|
|
let s = trimmed.replace(/\s+/g, "").toLowerCase().replace(/hz$/, "");
|
|
const kHz = /k(hz)?$/.test(s);
|
|
if (kHz) s = s.replace(/k(hz)?$/, "");
|
|
const num = Number.parseFloat(s);
|
|
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
|
|
const hz = Math.round(kHz ? num * 1000 : num);
|
|
if (hz < BAND_FREQ_MIN || hz > BAND_FREQ_MAX) {
|
|
return { ok: false, message: messages.outOfRange };
|
|
}
|
|
return { ok: true, value: hz };
|
|
}
|
|
|
|
if (kind === "gain") {
|
|
const s = trimmed.replace(/\s*dB\s*$/i, "").trim();
|
|
const num = Number.parseFloat(s);
|
|
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
|
|
const gain = Number(num.toFixed(1));
|
|
if (gain < BAND_GAIN_MIN || gain > BAND_GAIN_MAX) {
|
|
return { ok: false, message: messages.outOfRange };
|
|
}
|
|
return { ok: true, value: gain };
|
|
}
|
|
|
|
const num = Number.parseFloat(trimmed);
|
|
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
|
|
const q = Number(num.toFixed(2));
|
|
if (q < BAND_Q_MIN || q > BAND_Q_MAX) {
|
|
return { ok: false, message: messages.outOfRange };
|
|
}
|
|
return { ok: true, value: q };
|
|
}
|
|
|
|
/* ── Normalize filter type from string or number ── */
|
|
export function normalizeFilterType(
|
|
type: string | number | undefined,
|
|
): string {
|
|
if (type === undefined || type === null) return "PEAK";
|
|
if (typeof type === "number") {
|
|
switch (type) {
|
|
case 0:
|
|
return "LPF";
|
|
case 1:
|
|
return "HPF";
|
|
case 2:
|
|
return "BPF";
|
|
case 3:
|
|
return "NOTCH";
|
|
case 4:
|
|
return "PEAK";
|
|
case 5:
|
|
return "LSHELF";
|
|
case 6:
|
|
return "HSHELF";
|
|
case 7:
|
|
return "APF";
|
|
default:
|
|
return "PEAK";
|
|
}
|
|
}
|
|
return getFilterShortName(type);
|
|
}
|
|
|
|
/* ── Clone bands array ── */
|
|
export function cloneBands(
|
|
source: Array<{
|
|
freq: number;
|
|
gain: number;
|
|
q: number;
|
|
type: string;
|
|
enabled: boolean;
|
|
}>,
|
|
) {
|
|
return source.map((band) => ({ ...band }));
|
|
}
|
|
|
|
/* ── Frequency label for band grid (numeric only; avoid browser translate → words) ── */
|
|
export function freqLabel(freq: unknown) {
|
|
const hz =
|
|
typeof freq === "number" && Number.isFinite(freq)
|
|
? freq
|
|
: Number(freq);
|
|
if (!Number.isFinite(hz)) return "—";
|
|
if (hz >= 1000) {
|
|
const k = hz / 1000;
|
|
const text = k >= 10 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, "");
|
|
return `${text}K`;
|
|
}
|
|
return String(Math.round(hz));
|
|
}
|
|
|
|
/** Dev: log decoded syncPeq JSON after EQ page fetches device PEQ state. */
|
|
export function logEqSyncPeqDev(source: string, data: PeqState): void {
|
|
if (!import.meta.env.DEV) return;
|
|
console.log(`[EQ syncPeq] ${source}`, data);
|
|
}
|
|
|
|
export async function fetchEqSyncPeq(
|
|
api: { getPeqState: () => Promise<PeqState> },
|
|
source: string,
|
|
): Promise<PeqState> {
|
|
const data = await api.getPeqState();
|
|
logEqSyncPeqDev(source, data);
|
|
return data;
|
|
}
|
|
|
|
/* ── Build PEQ catalog sync key ── */
|
|
export function buildPeqCatalogSyncKey(
|
|
remote: Pick<PeqState, "peq" | "peqSelect"> | null | undefined,
|
|
devicePeqSelect?: number,
|
|
): string {
|
|
const items = remote?.peq;
|
|
if (!items?.length) return items ? "empty" : "";
|
|
const select = devicePeqSelect ?? remote?.peqSelect ?? 0;
|
|
return `${select}|${items.map((p) => p.name).join("\u0001")}`;
|
|
}
|
|
|
|
/* ── Generate unique preset name ── */
|
|
export function getUniquePresetName(
|
|
base: string,
|
|
existingNames: string[],
|
|
) {
|
|
if (!existingNames.includes(base)) return base;
|
|
let index = 1;
|
|
while (existingNames.includes(`${base}_${index}`)) {
|
|
index += 1;
|
|
}
|
|
return `${base}_${index}`;
|
|
}
|
|
|
|
/** Normalize shared/imported EQ filters into device PeqFilter[]. */
|
|
export function importedFiltersToPeqFilters(filters: unknown): PeqFilter[] {
|
|
return normalizePeqFiltersForSubmit(filters).map((f) => {
|
|
const item = f as Record<string, unknown>;
|
|
return {
|
|
fc: Number(item.fc ?? item.freq ?? item.frequency ?? 1000),
|
|
gain: Number(item.gain ?? 0),
|
|
q: Number(item.q ?? 1),
|
|
type: getFilterType(item.type as string | number),
|
|
};
|
|
});
|
|
}
|
|
|
|
/* ── Convert UI band → device PeqFilter ── */
|
|
export function bandToPeqFilter(b: {
|
|
freq: number;
|
|
gain: number;
|
|
q: number;
|
|
type: string | number;
|
|
}): PeqFilter {
|
|
return {
|
|
fc: b.freq,
|
|
gain: b.gain,
|
|
q: b.q,
|
|
type: getFilterType(b.type),
|
|
};
|
|
}
|