多语言,EQ 频响曲线 Y 轴可调,滤波器类型新增 Bypass

This commit is contained in:
eafonyang
2026-11-03 11:22:35 +08:00
parent a276beb1c7
commit 2d802f7844
11 changed files with 2381 additions and 91 deletions
+7
View File
@@ -2,6 +2,13 @@
> This document records feature updates and bug fixes for the Luxsin X8 controller. Entries are listed in reverse chronological order — the newest release appears first.
## v2026.11.03
### New Features
- EQ frequency-response chart: the Y-axis (gain) range is now adjustable. Use the segmented control beside the legend to switch between ±6 / ±12 / ±15 / ±20 dB; your choice is remembered next time.
- EQ filter type: added "BYPASS" as the first option in the filter-type menu. Selecting it bypasses the band and locks its FREQ / GAIN / Q controls, while the underlying filter values stay unchanged.
- Added more interface languages, including Russian and Japanese. These languages apply to the app UI only and are not sent to the device.
## v2026.09.22
### New Features
+9 -2
View File
@@ -12,6 +12,11 @@ export const TYPE_HIGHPASS = 1;
export const TYPE_BANDPASS = 2;
export const TYPE_NOTCH = 3;
export const TYPE_ALLPASS = 7;
/**
* 旁通标记:设备协议用 type + 10 表示该滤波器被 bypass(见 getSectionsMatrix)。
* UI 固定下发 10,系数落入 default 分支→unity(0 dB 直通)。
*/
export const TYPE_BYPASS = 10;
// Filter type mapping (device uses numeric type; pass-through when already a number)
export function getFilterType(typeName: string | number): number {
@@ -340,6 +345,8 @@ export type PeqBandForResponse = {
freq: number;
q: number;
type: string | number;
/** 旁通:该带不参与级联响应(曲线贡献 0 dB),type 仍保留原滤波器类型。 */
bypassed?: boolean;
};
/**
@@ -351,7 +358,7 @@ export function computePeqMagnitudeDb(bands: PeqBandForResponse[], fs: number):
const list: Coeff[] = [];
bands.forEach((b) => {
if (!b.enabled) return;
list.push(getSectionsMatrix(b.gain, b.freq, b.q, getFilterType(b.type), false, fs));
list.push(getSectionsMatrix(b.gain, b.freq, b.q, getFilterType(b.type), !!b.bypassed, fs));
});
if (list.length === 0) {
return f.map(() => 0);
@@ -372,7 +379,7 @@ export function sampleCombinedPeqMagnitudeDb(
const list: Coeff[] = [];
bands.forEach((b) => {
if (!b.enabled) return;
list.push(getSectionsMatrix(b.gain, b.freq, b.q, getFilterType(b.type), false, fs));
list.push(getSectionsMatrix(b.gain, b.freq, b.q, getFilterType(b.type), !!b.bypassed, fs));
});
if (list.length === 0) return 0;
const out = getFreqznList(list, fs, [fc]);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+11 -4
View File
@@ -8,11 +8,14 @@ import localeKo from "./data-ko.json";
import localeVi from "./data-vi.json";
import localePt from "./data-pt.json";
import localePl from "./data-pl.json";
import localeRu from "./data-ru.json";
import localeJa from "./data-ja.json";
/**
* 语言索引映射:
* 0=en, 1=zh-HK, 2=zh-CN → 设备固件支持,选择后会下发 dev/info.cgi?action=setting&language=
* 3=de, 4=es, 5=fr, 6=ko, 7=vi, 8=pt, 9=pl → 设备不支持,仅在 controller 前端做界面语言切换,绝不下发设备
* 3=de, 4=es, 5=fr, 6=ko, 7=vi, 8=pt, 9=pl,
* 10=ru, 11=ja → 设备不支持,仅在 controller 前端做界面语言切换,绝不下发设备
*/
export const DEFAULT_DEVICE_LANGUAGE = 0;
@@ -32,6 +35,8 @@ const LOCALE_PACKS = [
localeVi,
localePt,
localePl,
localeRu,
localeJa,
] as LocalePack[];
/** 语言选择器可展示的全部语言数量(含仅前端语言)。 */
@@ -39,7 +44,7 @@ export const LANGUAGE_COUNT = LOCALE_PACKS.length;
/**
* 语言选择器固定显示的「母语名称」,与当前界面语言无关。
* 数组下标即语言索引(0=en … 9=pl),选择时直接作为语言值使用。
* 数组下标即语言索引(0=en … 11=ja),选择时直接作为语言值使用。
*/
export const LANGUAGE_NATIVE_LABELS = [
"English",
@@ -52,6 +57,8 @@ export const LANGUAGE_NATIVE_LABELS = [
"Tiếng Việt",
"Português (Portugal)",
"Polski",
"Русский",
"日本語",
];
/** 是否为设备支持的语言(0/1/2),只有这些才会下发设备。 */
@@ -69,7 +76,7 @@ export function resolveDeviceLanguage(language: number | undefined): number {
return language ?? DEFAULT_DEVICE_LANGUAGE;
}
/* ── 前端语言覆盖(仅非设备语言 3..9,持久化到 localStorage,绝不下发设备) ── */
/* ── 前端语言覆盖(仅非设备语言 3..11,持久化到 localStorage,绝不下发设备) ── */
const UI_LANGUAGE_OVERRIDE_KEY = "luxsin.uiLanguageOverride";
function readStoredOverride(): number | null {
@@ -91,7 +98,7 @@ export function getUiLanguageOverride(): number | null {
}
/**
* 设置前端语言覆盖。传入仅前端语言(3..9)时生效并持久化;
* 设置前端语言覆盖。传入仅前端语言(3..11)时生效并持久化;
* 传入设备语言(0/1/2)或 null 时清除覆盖,恢复跟随设备语言。
*/
export function setUiLanguageOverride(index: number | null): number | null {
+70 -18
View File
@@ -83,6 +83,7 @@ import {
fetchEqSyncPeq,
getUniquePresetName,
importedFiltersToPeqFilters,
isBypassFilterType,
} from "./eq/peqMappers";
import type { BandParamKind, PeqCatalogItem, PeqEqUi, PeqPresetLocalCache } from "./eq/types";
@@ -355,12 +356,15 @@ export default function EQPage() {
return filters.map((f: any) => {
const rawFreq = f.fc ?? f.freq ?? f.frequency ?? 1000;
const freq = normalizeBandFreqHz(rawFreq) ?? 1000;
// 设备用 type=10 表示旁通,原类型已丢失;回退 PEAK 供重新打开时使用
const bypassed = isBypassFilterType(f.type);
return {
freq: Math.round(freq),
gain: Number(f.gain || 0),
q: Number(f.q || 1),
type: normalizeFilterType(f.type),
type: bypassed ? "PEAK" : normalizeFilterType(f.type),
enabled: f.enabled !== undefined ? f.enabled : true,
bypassed,
};
});
};
@@ -415,6 +419,7 @@ export default function EQPage() {
freq: number;
gain: number;
q: number;
bypassed: boolean;
}>(10);
for (let i = 1; i <= 10; i++) {
@@ -449,6 +454,7 @@ export default function EQPage() {
freq: Number(freq.toFixed(2)),
gain: Number(gain.toFixed(2)),
q: Number(q.toFixed(2)),
bypassed: false,
};
}
@@ -1243,6 +1249,7 @@ export default function EQPage() {
}, []);
const band = bands[selectedBand] ?? DEFAULT_BANDS[selectedBand] ?? DEFAULT_BANDS[0];
const bandBypassed = !!band.bypassed;
useEffect(() => {
setBandSliderPreview(null);
@@ -1682,29 +1689,44 @@ export default function EQPage() {
</div>
{/* ── Band detail card ── */}
<div className="ios-list-group p-4 space-y-5 relative z-30 overflow-visible">
{/* Filter type selector */}
<div className="flex items-center justify-between">
<span className="text-[14px] text-white/50 font-medium">{peqCardLabels.filters?.label ?? "滤波器"}</span>
<div
className={cn(
"ios-list-group p-4 space-y-5 relative overflow-visible",
// 下拉展开时抬升整卡层级,避免末项被固定底部导航(z-50)压住
isFilterMenuOpen ? "z-[60]" : "z-30"
)}
>
{/* Filter type selector:BYPASS 作为下拉首个可选类型 */}
<div className="flex items-center justify-between gap-2">
<span className="text-[14px] text-white/50 font-medium shrink-0">
{peqCardLabels.filters?.label ?? "滤波器"}
</span>
<div
ref={filterMenuRef}
className="relative z-40 w-[136px] rounded-[12px] overflow-visible backdrop-blur-md transition-all"
className="relative z-40 w-[136px] shrink-0 rounded-[12px] overflow-visible backdrop-blur-md transition-all"
style={{
background: "linear-gradient(180deg, rgba(36,38,42,0.92) 0%, rgba(24,26,30,0.92) 100%)",
border: "1px solid rgba(0,255,246,0.52)",
boxShadow: "0 0 14px rgba(0,255,246,0.12), inset 0 1px 0 rgba(255,255,255,0.08)",
border: bandBypassed
? "1px solid rgba(255,255,255,0.18)"
: "1px solid rgba(0,255,246,0.52)",
boxShadow: bandBypassed
? "none"
: "0 0 14px rgba(0,255,246,0.12), inset 0 1px 0 rgba(255,255,255,0.08)",
}}
>
<button
type="button"
className="w-full pl-3 pr-8 py-2 text-left text-[13px] font-semibold tracking-wide text-[#E9FBFF] bg-transparent outline-none cursor-pointer active:opacity-80 transition-opacity rounded-[12px]"
className={cn(
"w-full pl-3 pr-8 py-2 text-left text-[13px] font-semibold tracking-wide bg-transparent outline-none cursor-pointer active:opacity-80 transition-opacity rounded-[12px]",
bandBypassed ? "text-white/50" : "text-[#E9FBFF]"
)}
onClick={() => setIsFilterMenuOpen((v) => !v)}
>
{normalizeFilterType(band.type)}
{bandBypassed ? "BYPASS" : normalizeFilterType(band.type)}
</button>
<ChevronDown
size={13}
className={`absolute right-2.5 top-1/2 -translate-y-1/2 text-[#00FFF6] pointer-events-none transition-transform ${isFilterMenuOpen ? "rotate-180" : ""}`}
className={`absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none transition-transform ${bandBypassed ? "text-white/40" : "text-[#00FFF6]"} ${isFilterMenuOpen ? "rotate-180" : ""}`}
/>
{isFilterMenuOpen && (
<div
@@ -1715,23 +1737,47 @@ export default function EQPage() {
boxShadow: "0 8px 20px rgba(0,0,0,0.45)",
}}
>
{/* 本机型无固件版本门槛,Bypass 始终可用 */}
<button
type="button"
className={`w-full px-3 py-2 text-left text-[13px] transition-colors border-b border-white/10 ${
bandBypassed
? "text-black font-semibold"
: "text-white/90 hover:bg-white/10"
}`}
style={bandBypassed ? { background: "#00FFF6" } : undefined}
onClick={() => {
if (band.bypassed) {
setIsFilterMenuOpen(false);
return;
}
captureBefore(takeUndoSnapshot());
// 选中 BYPASS:该带旁通,提交时固定下发 type=10
updateBand(selectedBand, { bypassed: true });
setIsFilterMenuOpen(false);
}}
>
BYPASS
</button>
{FILTER_TYPES.map((type) => {
const active = normalizeFilterType(band.type) === type;
const active = !bandBypassed && normalizeFilterType(band.type) === type;
return (
<button
key={type}
type="button"
className={`w-full px-3 py-2 text-left text-[13px] transition-colors ${
active ? "text-black font-semibold" : "text-white/90 hover:bg-white/10"
active
? "text-black font-semibold"
: "text-white/90 hover:bg-white/10"
}`}
style={active ? { background: "#00FFF6" } : undefined}
onClick={() => {
if (normalizeFilterType(band.type) === type) {
if (!band.bypassed && normalizeFilterType(band.type) === type) {
setIsFilterMenuOpen(false);
return;
}
captureBefore(takeUndoSnapshot());
updateBand(selectedBand, { type });
updateBand(selectedBand, { type, bypassed: false });
setIsFilterMenuOpen(false);
}}
>
@@ -1751,7 +1797,8 @@ export default function EQPage() {
<button
type="button"
onClick={() => openBandParamDialog("freq")}
className="notranslate text-[13px] font-semibold tabular-nums px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
disabled={bandBypassed}
className={`notranslate text-[13px] font-semibold tabular-nums px-2.5 py-1 rounded-[8px] transition-transform ${bandBypassed ? "text-white/40 cursor-not-allowed" : "text-white active:scale-95"}`}
style={BAND_PARAM_VALUE_BOX_STYLE}
translate="no"
>
@@ -1763,6 +1810,7 @@ export default function EQPage() {
min={Math.log10(BAND_FREQ_MIN)}
max={Math.log10(BAND_FREQ_MAX)}
step={0.005}
disabled={bandBypassed}
onInteractionStart={() => beginBandSliderDrag("freq")}
onInteractionEnd={() => endBandSliderDrag("freq")}
onChange={(v) => handleBandSliderChange("freq", Math.round(Math.pow(10, v)))}
@@ -1776,7 +1824,8 @@ export default function EQPage() {
<button
type="button"
onClick={() => openBandParamDialog("gain")}
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
disabled={bandBypassed}
className={`text-[13px] font-semibold px-2.5 py-1 rounded-[8px] transition-transform ${bandBypassed ? "text-white/40 cursor-not-allowed" : "text-white active:scale-95"}`}
style={BAND_PARAM_VALUE_BOX_STYLE}
>
{formatBandGainDisplay(bandSliderPreview?.gain ?? band.gain)}
@@ -1787,6 +1836,7 @@ export default function EQPage() {
min={BAND_GAIN_MIN}
max={BAND_GAIN_MAX}
step={0.1}
disabled={bandBypassed}
onInteractionStart={() => beginBandSliderDrag("gain")}
onInteractionEnd={() => endBandSliderDrag("gain")}
onChange={(v) => handleBandSliderChange("gain", v)}
@@ -1800,7 +1850,8 @@ export default function EQPage() {
<button
type="button"
onClick={() => openBandParamDialog("q")}
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
disabled={bandBypassed}
className={`text-[13px] font-semibold px-2.5 py-1 rounded-[8px] transition-transform ${bandBypassed ? "text-white/40 cursor-not-allowed" : "text-white active:scale-95"}`}
style={BAND_PARAM_VALUE_BOX_STYLE}
>
{formatBandQDisplay(bandSliderPreview?.q ?? band.q)}
@@ -1811,6 +1862,7 @@ export default function EQPage() {
min={BAND_Q_MIN}
max={BAND_Q_MAX}
step={0.01}
disabled={bandBypassed}
onInteractionStart={() => beginBandSliderDrag("q")}
onInteractionEnd={() => endBandSliderDrag("q")}
onChange={(v) => handleBandSliderChange("q", v)}
+116 -45
View File
@@ -6,6 +6,24 @@ import { eqInterp } from "../eqFormatters";
import type { EqBand, PeqEqUi } from "../types";
import { PeqUndoButton } from "./PeqUndoButton";
/* ── Y 轴刻度范围(±dB)可选项与持久化 ── */
const Y_RANGE_OPTIONS = [6, 12, 15, 20] as const;
type YRangeDb = (typeof Y_RANGE_OPTIONS)[number];
const DEFAULT_Y_RANGE_DB: YRangeDb = 12;
const Y_RANGE_STORAGE_KEY = "x8-peq-chart-y-db-max";
/** 每档范围的网格整数步长(均可整除边界,确保标签为整数) */
const Y_RANGE_STEP: Record<YRangeDb, number> = { 6: 2, 12: 3, 15: 5, 20: 5 };
function loadYRangeDb(): YRangeDb {
try {
const raw = Number(localStorage.getItem(Y_RANGE_STORAGE_KEY));
if (Y_RANGE_OPTIONS.includes(raw as YRangeDb)) return raw as YRangeDb;
} catch {
/* localStorage 不可用时回退默认值 */
}
return DEFAULT_Y_RANGE_DB;
}
function LegendCurveIndicator({
loading,
color,
@@ -73,6 +91,16 @@ export function FreqChart({
const [chartWidth, setChartWidth] = useState(340);
const W = chartWidth;
const [isBandDragging, setIsBandDragging] = useState(false);
const [yDbMax, setYDbMax] = useState<YRangeDb>(loadYRangeDb);
const handleYRangeChange = (range: YRangeDb) => {
setYDbMax(range);
try {
localStorage.setItem(Y_RANGE_STORAGE_KEY, String(range));
} catch {
/* 忽略持久化失败,仅影响下次进入页面的默认值 */
}
};
useEffect(() => {
const node = chartContainerRef.current;
@@ -89,7 +117,7 @@ export function FreqChart({
return () => observer.disconnect();
}, []);
const Y_DB_MAX = 20;
const Y_DB_MAX = yDbMax;
const freqToX = (f: number) => {
const logMin = Math.log10(20),
@@ -157,7 +185,7 @@ export function FreqChart({
maxFreq: 20000,
paddingY: 10,
}),
[bands, W, H],
[bands, W, H, Y_DB_MAX],
);
const pathD = curveData.pathD;
const fillD = curveData.fillD;
@@ -225,7 +253,20 @@ export function FreqChart({
const copyButtonText = eqInterp(eqUi.chartCopyTo, { mode: targetMode });
const freqLabels = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
const gainLabels = [20, 15, 10, 5, 0, -5, -10, -15, -20];
// 按当前范围的整数步长生成网格标签(如 ±12 → 12,9,6,3,0,-3,-6,-9,-12)
const gainLabels = useMemo(() => {
const step = Y_RANGE_STEP[Y_DB_MAX];
const intervals = Math.round((2 * Y_DB_MAX) / step);
const labels: number[] = [];
for (let i = 0; i <= intervals; i++) {
labels.push(Number((Y_DB_MAX - i * step).toFixed(2)));
}
return labels;
}, [Y_DB_MAX]);
const formatGainLabel = (g: number) => {
const v = Number(g.toFixed(2));
return v > 0 ? `+${v}` : String(v);
};
return (
<div className="ios-list-group p-3 mb-3">
@@ -269,6 +310,27 @@ export function FreqChart({
Equalized
</span>
</button>
{/* Y 轴刻度范围切换 */}
<div
className="ml-auto flex shrink-0 items-center overflow-hidden rounded-[8px]"
style={{ background: "rgba(44,44,46,0.7)", border: "1px solid rgba(255,255,255,0.08)" }}
>
{Y_RANGE_OPTIONS.map((r) => (
<button
key={r}
type="button"
className="px-2 py-1 text-[10px] transition-all duration-150"
style={
yDbMax === r
? { background: "#00FFF6", color: "#000", fontWeight: 600 }
: { color: "rgba(255,255,255,0.4)" }
}
onClick={() => handleYRangeChange(r)}
>
±{r}
</button>
))}
</div>
</div>
{/* A/B + DIFF controls */}
@@ -354,12 +416,18 @@ export function FreqChart({
onPointerCancel={stopDragging}
onPointerLeave={stopDragging}
>
<defs>
{/* 绘图区裁剪:超出当前 Y 轴刻度范围的曲线部分直接截断不显示 */}
<clipPath id="freq-chart-plot-clip">
<rect x="0" y="10" width={W} height={H - 20} />
</clipPath>
</defs>
{/* dB grid */}
{gainLabels.map((g) => (
<g key={g}>
<line x1="0" y1={gainToY(g)} x2={W} y2={gainToY(g)} stroke="rgba(255,255,255,0.05)" strokeWidth="1" />
<text x="3" y={gainToY(g) - 2} fontSize="7" fill="rgba(255,255,255,0.2)">
{g > 0 ? `+${g}` : g}
{formatGainLabel(g)}
</text>
</g>
))}
@@ -377,46 +445,48 @@ export function FreqChart({
))}
{/* Zero line */}
<line x1="0" y1={H / 2} x2={W} y2={H / 2} stroke="rgba(255,255,255,0.15)" strokeWidth="1" />
{/* Fill */}
{showEq && <path d={fillD} fill="rgba(255, 237, 0, 0.08)" />}
{/* EQ curve */}
{showEq && (
<path
ref={curvePathRef}
d={pathD}
fill="none"
stroke="#FFED00"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
{/* Raw */}
{rawPathD && showRaw && (
<path
ref={rawPathRef}
d={rawPathD}
fill="none"
stroke="#ffffff"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.9"
/>
)}
{/* Equalized = EQ + Raw */}
{equalizedPathD && showEqualized && (
<path
ref={equalizedPathRef}
d={equalizedPathD}
fill="none"
stroke="#23d2fe"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.95"
/>
)}
<g clipPath="url(#freq-chart-plot-clip)">
{/* Fill */}
{showEq && <path d={fillD} fill="rgba(255, 237, 0, 0.08)" />}
{/* EQ curve */}
{showEq && (
<path
ref={curvePathRef}
d={pathD}
fill="none"
stroke="#FFED00"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
{/* Raw */}
{rawPathD && showRaw && (
<path
ref={rawPathRef}
d={rawPathD}
fill="none"
stroke="#ffffff"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.9"
/>
)}
{/* Equalized = EQ + Raw */}
{equalizedPathD && showEqualized && (
<path
ref={equalizedPathRef}
d={equalizedPathD}
fill="none"
stroke="#23d2fe"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.95"
/>
)}
</g>
{/* Band nodes with index */}
{showEq &&
bands.map((band, i) => (
@@ -427,7 +497,8 @@ export function FreqChart({
const fillColor = isSelected ? "#FFED00" : "rgba(0,0,0,0.5)";
const strokeColor = isSelected ? "#FFED00" : "#FFED00";
const textColor = isSelected ? "#000000" : "#FFED00";
const handleY = gainToY(combinedMagDbAtHandles[i] ?? 0);
// 级联响应超出当前刻度范围时,手柄钳制在绘图区边缘保持可见可拖
const handleY = Math.max(10, Math.min(H - 10, gainToY(combinedMagDbAtHandles[i] ?? 0)));
return (
<>
<circle cx={freqToX(band.freq)} cy={handleY} r="14" fill="transparent" />
+20 -20
View File
@@ -24,30 +24,30 @@ export const BAND_PARAM_VALUE_BOX_STYLE: CSSProperties = {
/** Default bands matching reference image */
export const DEFAULT_BANDS: EqBand[] = [
{ freq: 9500, gain: 0, q: 1.41, type: "LSHELF", enabled: true },
{ freq: 9200, gain: -2, q: 1.41, type: "PEAK", enabled: true },
{ freq: 220, gain: 1, q: 1.41, type: "PEAK", enabled: true },
{ freq: 500, gain: -3, q: 1.41, type: "PEAK", enabled: true },
{ freq: 1200, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 13800, gain: -1, q: 1.41, type: "NOTCH", enabled: true },
{ freq: 11000, gain: -2, q: 1.41, type: "PEAK", enabled: true },
{ freq: 7400, gain: 1, q: 1.41, type: "PEAK", enabled: true },
{ freq: 8200, gain: -1, q: 1.41, type: "PEAK", enabled: true },
{ freq: 10000, gain: 0, q: 1.41, type: "HSHELF", enabled: true },
{ freq: 9500, gain: 0, q: 1.41, type: "LSHELF", enabled: true, bypassed: false },
{ freq: 9200, gain: -2, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 220, gain: 1, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 500, gain: -3, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 1200, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 13800, gain: -1, q: 1.41, type: "NOTCH", enabled: true, bypassed: false },
{ freq: 11000, gain: -2, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 7400, gain: 1, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 8200, gain: -1, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 10000, gain: 0, q: 1.41, type: "HSHELF", enabled: true, bypassed: false },
];
/** Flat bands:无预设/未加载时展示的平直曲线(10 个 gain=0 的 PEAK) */
export const FLAT_BANDS: EqBand[] = [
{ freq: 80, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 150, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 350, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 750, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 1500, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 3000, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 6000, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 10000, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 14000, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 18000, gain: 0, q: 1.41, type: "PEAK", enabled: true },
{ freq: 80, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 150, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 350, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 750, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 1500, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 3000, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 6000, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 10000, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 14000, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
{ freq: 18000, gain: 0, q: 1.41, type: "PEAK", enabled: true, bypassed: false },
];
export const FLAT_PRESET_FILTERS: PeqFilter[] = [
@@ -9,6 +9,7 @@ export type PeqUndoBand = {
q: number;
type: string;
enabled: boolean;
bypassed: boolean;
};
export type PeqUndoSnapshot = {
+9 -2
View File
@@ -1,4 +1,4 @@
import { getFilterType } from "@/lib/peqAudio";
import { getFilterType, TYPE_BYPASS } from "@/lib/peqAudio";
import { normalizePeqFiltersForSubmit, type PeqFilter, type PeqState } from "@/lib/luxsinApi";
import type { EqBand } from "./types";
import { clampPeqPresetName } from "./peqPresetName";
@@ -48,15 +48,22 @@ export function bandToPeqFilter(b: {
gain: number;
q: number;
type: string | number;
bypassed?: boolean;
}): PeqFilter {
return {
fc: b.freq,
gain: b.gain,
q: b.q,
type: getFilterType(b.type),
// 旁通时固定下发 type=10;原类型保留在 UI 侧,重新打开时直接恢复
type: b.bypassed ? TYPE_BYPASS : getFilterType(b.type),
};
}
/** 设备回读的 type=10 即旁通标记(原类型已丢失,UI 侧回退 PEAK)。 */
export function isBypassFilterType(type: unknown): boolean {
return Number(type) === TYPE_BYPASS;
}
export function importedFiltersToPeqFilters(filters: unknown): PeqFilter[] {
return normalizePeqFiltersForSubmit(filters).map((f) => {
const item = f as unknown as Record<string, unknown>;
+2
View File
@@ -11,6 +11,8 @@ export type EqBand = {
q: number;
type: string;
enabled: boolean;
/** 旁通:提交时该带固定下发 type=10,type 仍保留原滤波器类型供重新打开时恢复 */
bypassed: boolean;
};
export type PeqCatalogItem = NonNullable<PeqState["peq"]>[number];