新增听力补偿,将有版本限制的功能,提取到配置文件,不再硬编码,优化部署文件参数
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Firmware version gates for UI features.
|
||||
*
|
||||
* Versions are compared using `parseFirmwareVersion` (e.g. 2007 → 2.0.0.7).
|
||||
* Adjust thresholds here instead of hardcoding in page components.
|
||||
*/
|
||||
export const FIRMWARE_FEATURE_MIN_VERSION = {
|
||||
/** Boot sound: indices 7–10 (-35…-50 dB); legacy firmware caps at index 6. */
|
||||
bootSoundExtendedSteps: 26,
|
||||
/** Subwoofer LPF/HPF full-range toggles on the Effects page. */
|
||||
subwooferFullRange: 2001,
|
||||
/** Hearing compensation section on the Effects page. */
|
||||
hearingCompensation: 2007,
|
||||
} as const;
|
||||
|
||||
export type FirmwareFeatureKey = keyof typeof FIRMWARE_FEATURE_MIN_VERSION;
|
||||
|
||||
export function isFirmwareFeatureAvailable(
|
||||
feature: FirmwareFeatureKey,
|
||||
firmwareVersion: number,
|
||||
): boolean {
|
||||
return firmwareVersion >= FIRMWARE_FEATURE_MIN_VERSION[feature];
|
||||
}
|
||||
|
||||
/** Max `bootSound` select index (0–6 legacy, 0–10 when extended steps are supported). */
|
||||
export function getBootSoundMaxIndex(firmwareVersion: number): number {
|
||||
return isFirmwareFeatureAvailable("bootSoundExtendedSteps", firmwareVersion) ? 10 : 6;
|
||||
}
|
||||
@@ -120,6 +120,9 @@ export interface DeviceState {
|
||||
loudness_bass_gain: number;
|
||||
loudness_treble_gain: number;
|
||||
loudness_threshold_gain: number;
|
||||
hearing_enable: number;
|
||||
hearing_select: number;
|
||||
hearing_data: HearingProfile[] | string;
|
||||
bt_status: number;
|
||||
bt_srcname: string;
|
||||
bt_title: string;
|
||||
@@ -127,6 +130,37 @@ export interface DeviceState {
|
||||
msgCount: number;
|
||||
}
|
||||
|
||||
export interface HearingProfile {
|
||||
n: string;
|
||||
l: number[];
|
||||
r: number[];
|
||||
}
|
||||
|
||||
/** Parse `hearing_data` from syncData (array or JSON string). */
|
||||
export function parseHearingData(raw: unknown): HearingProfile[] {
|
||||
if (raw == null || raw === "") return [];
|
||||
let value: unknown = raw;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
value = JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== "object") return null;
|
||||
const row = item as Record<string, unknown>;
|
||||
const n = typeof row.n === "string" ? row.n.trim() : String(row.n ?? "").trim();
|
||||
if (!n) return null;
|
||||
const l = Array.isArray(row.l) ? row.l.map((v) => Number(v)) : [];
|
||||
const r = Array.isArray(row.r) ? row.r.map((v) => Number(v)) : [];
|
||||
return { n, l, r };
|
||||
})
|
||||
.filter((item): item is HearingProfile => item !== null);
|
||||
}
|
||||
|
||||
export interface PeqFilter {
|
||||
fc: number;
|
||||
gain: number;
|
||||
@@ -375,7 +409,7 @@ export class LuxsinAPI {
|
||||
setCrossfeedEnable(enable: boolean) { return this.setSetting({ crossfeed_enable: enable ? 1 : 0 }); }
|
||||
setXlrPolarity(reverse: boolean) { return this.setSetting({ xlr: reverse ? 1 : 0 }); }
|
||||
setDacArc(earc: boolean) { return this.setSetting({ dacArc: earc ? 1 : 0 }); }
|
||||
/** Boot volume level: 0 = default, 1..6 = -5..-30 dB, 7..10 = -35..-50 dB (firmware >= 26). */
|
||||
/** Boot volume level: 0 = default, 1..6 = -5..-30 dB, 7..10 = -35..-50 dB (see firmwareFeatures.bootSoundExtendedSteps). */
|
||||
setBootSound(level: number) {
|
||||
return this.setSetting({ bootSound: Math.max(0, Math.min(10, Math.round(level))) });
|
||||
}
|
||||
@@ -383,7 +417,7 @@ export class LuxsinAPI {
|
||||
powerOff() { return this.setSetting({ power: 0 }); }
|
||||
}
|
||||
|
||||
/** Parse firmware `version` for feature gating (e.g. bootSound extended steps). */
|
||||
/** Parse firmware `version` for feature gating (see `config/firmwareFeatures.ts`). */
|
||||
export function parseFirmwareVersion(version: unknown): number {
|
||||
if (version === null || version === undefined) return 0;
|
||||
if (typeof version === "number" && Number.isFinite(version)) return version;
|
||||
@@ -423,10 +457,7 @@ export function isHomeVolumeLockedByPassthrough(
|
||||
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;
|
||||
}
|
||||
export { getBootSoundMaxIndex } from "@/config/firmwareFeatures";
|
||||
|
||||
/** Normalize `bootSound` from syncData (0 = default … 10 = -50 dB). */
|
||||
export function normalizeBootSound(value: unknown): number {
|
||||
@@ -502,6 +533,15 @@ export const MOCK_DEVICE_STATE: DeviceState = {
|
||||
loudness_bass_gain: 3,
|
||||
loudness_treble_gain: 2,
|
||||
loudness_threshold_gain: 60,
|
||||
hearing_enable: 0,
|
||||
hearing_select: 0,
|
||||
hearing_data: [
|
||||
{
|
||||
n: "testing",
|
||||
l: [1.7, 0.8, 0.5, 0.5, 0.5, 0.5, 0.4],
|
||||
r: [0.4, 0.4, 0.3, 0.3, 0.2, 0.4, 0.3],
|
||||
},
|
||||
],
|
||||
bt_status: 1,
|
||||
bt_srcname: "iPhone 15 Pro",
|
||||
bt_title: "Bohemian Rhapsody",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Effekte",
|
||||
"selectStyleTitle": "Klangstil auswählen",
|
||||
"selectCrossfeedTitle": "Crossfeed-Voreinstellung auswählen",
|
||||
"selectHearingTitle": "Hörkompensationsprofil auswählen",
|
||||
"lowBass": "Bass",
|
||||
"enterBass": "Mitten",
|
||||
"highBass": "Höhen",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B Entspannt (650 Hz, 9,5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Hörkompensation",
|
||||
"emptyProfile": "Kein Profil verfügbar"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Subwoofer",
|
||||
"cutOffFrequency": "Trennfrequenz",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Effects",
|
||||
"selectStyleTitle": "Select sound style",
|
||||
"selectCrossfeedTitle": "Select crossfeed preset",
|
||||
"selectHearingTitle": "Select hearing compensation profile",
|
||||
"lowBass": "Bass",
|
||||
"enterBass": "Mid",
|
||||
"highBass": "Treble",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B relax (650 Hz, 9.5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Hearing compensation",
|
||||
"emptyProfile": "No profile available"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Subwoofer",
|
||||
"cutOffFrequency": "Cut off frequency",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Efectos",
|
||||
"selectStyleTitle": "Seleccionar estilo de sonido",
|
||||
"selectCrossfeedTitle": "Seleccionar preset de crossfeed",
|
||||
"selectHearingTitle": "Seleccionar perfil de compensación auditiva",
|
||||
"lowBass": "Graves",
|
||||
"enterBass": "Medios",
|
||||
"highBass": "Agudos",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B relajado (650 Hz, 9,5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Compensación auditiva",
|
||||
"emptyProfile": "No hay perfil disponible"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Subwoofer",
|
||||
"cutOffFrequency": "Frecuencia de corte",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Effets",
|
||||
"selectStyleTitle": "Sélectionner le style sonore",
|
||||
"selectCrossfeedTitle": "Sélectionner le préréglage crossfeed",
|
||||
"selectHearingTitle": "Sélectionner le profil de compensation auditive",
|
||||
"lowBass": "Basses",
|
||||
"enterBass": "Médiums",
|
||||
"highBass": "Aigus",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B relax (650 Hz, 9,5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Compensation auditive",
|
||||
"emptyProfile": "Aucun profil disponible"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Caisson de basses",
|
||||
"cutOffFrequency": "Fréquence de coupure",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"pageTitle": "音效",
|
||||
"selectStyleTitle": "選擇音效風格",
|
||||
"selectCrossfeedTitle": "選擇交叉回授模式",
|
||||
"selectHearingTitle": "選擇聽力補償配置",
|
||||
"lowBass": "低頻",
|
||||
"enterBass": "中頻",
|
||||
"highBass": "高頻",
|
||||
@@ -103,6 +104,10 @@
|
||||
"BS2B relax(650Hz,9.5dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "聽力補償",
|
||||
"emptyProfile": "無可用配置"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "低音炮輸出",
|
||||
"cutOffFrequency": "截止頻率",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"pageTitle": "音效",
|
||||
"selectStyleTitle": "选择音效风格",
|
||||
"selectCrossfeedTitle": "选择交叉反馈模式",
|
||||
"selectHearingTitle": "选择听力补偿配置",
|
||||
"lowBass": "低音",
|
||||
"enterBass": "中音",
|
||||
"highBass": "高音",
|
||||
@@ -103,6 +104,10 @@
|
||||
"BS2B relax(650Hz,9.5dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "听力补偿",
|
||||
"emptyProfile": "无可用配置"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "低音炮输出",
|
||||
"cutOffFrequency": "截止频率",
|
||||
|
||||
@@ -14,7 +14,8 @@ import BottomNav from "@/components/BottomNav";
|
||||
import SubwooferLpfChart from "@/components/SubwooferLpfChart";
|
||||
import { FeatureGate } from "@/components/FeatureGate";
|
||||
import { navigateToSelect } from "./SelectPage";
|
||||
import { parseFirmwareVersion } from "@/lib/luxsinApi";
|
||||
import { parseFirmwareVersion, parseHearingData } from "@/lib/luxsinApi";
|
||||
import { isFirmwareFeatureAvailable } from "@/config/firmwareFeatures";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
@@ -247,6 +248,16 @@ export default function EffectsPage() {
|
||||
const widthVal = ds?.width_value ?? 50;
|
||||
const crossfeedOn = (ds?.crossfeed_enable ?? 0) === 1;
|
||||
const crossfeedVal = ds?.crossfeed_value ?? 0;
|
||||
const hearingOn = (ds?.hearing_enable ?? 0) === 1;
|
||||
const hearingSelect = ds?.hearing_select ?? 0;
|
||||
const hearingProfiles = useMemo(
|
||||
() => parseHearingData(ds?.hearing_data),
|
||||
[ds?.hearing_data],
|
||||
);
|
||||
const hearingLabels = useMemo(
|
||||
() => hearingProfiles.map((profile) => profile.n),
|
||||
[hearingProfiles],
|
||||
);
|
||||
const sceneOn = (ds?.effect_enable ?? 0) === 1;
|
||||
const sceneVal = ds?.effect_value ?? 0;
|
||||
const colorOn = (ds?.color_enable ?? 0) === 1;
|
||||
@@ -265,7 +276,8 @@ export default function EffectsPage() {
|
||||
const subwooferLpfOn = (ds?.subwoofer_lpf_enable ?? 0) === 1;
|
||||
const subwooferHpfOn = (ds?.subwoofer_hpf_enable ?? 0) === 1;
|
||||
const firmwareVersion = parseFirmwareVersion(ds?.version);
|
||||
const showSubwooferFullRange = firmwareVersion >= 2001;
|
||||
const showSubwooferFullRange = isFirmwareFeatureAvailable("subwooferFullRange", firmwareVersion);
|
||||
const showHearingCompensation = isFirmwareFeatureAvailable("hearingCompensation", firmwareVersion);
|
||||
|
||||
const subwooferDelayMainL = clampSubwooferDelayValue(ds?.subwoofer_delay_main ?? 0);
|
||||
const subwooferDelaySubL = clampSubwooferDelayValue(ds?.subwoofer_delay ?? 0);
|
||||
@@ -393,6 +405,9 @@ export default function EffectsPage() {
|
||||
const sceneDisplay = sceneLabels[sceneIdx] ?? "";
|
||||
const crossIdx = clampPickIndex(crossfeedVal, crossfeedLabels.length);
|
||||
const crossfeedDisplay = crossfeedLabels[crossIdx] ?? "";
|
||||
const hearingIdx = clampPickIndex(hearingSelect, hearingLabels.length);
|
||||
const hearingDisplay =
|
||||
hearingLabels[hearingIdx] ?? effectText.hearing?.emptyProfile ?? "—";
|
||||
const subRateIdx = clampPickIndex(subwooferRate, subwooferRateLabels.length);
|
||||
const subRateDisplay = subwooferRateLabels[subRateIdx] ?? "";
|
||||
const subMixIdx = clampPickIndex(subwooferMixType, subwooferOutputLabels.length);
|
||||
@@ -870,6 +885,35 @@ export default function EffectsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── 听力补偿 ── */}
|
||||
{showHearingCompensation && (
|
||||
<div className="ios-list-group p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[16px] font-medium text-white">{effectText.hearing?.label ?? "听力补偿"}</span>
|
||||
<IOSToggle checked={hearingOn} onChange={(v) => updateSetting({ hearing_enable: v ? 1 : 0 })} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hearingOn || hearingLabels.length === 0}
|
||||
className={`flex items-center justify-between w-full transition-opacity ${
|
||||
hearingOn && hearingLabels.length > 0 ? "active:opacity-70" : "cursor-default opacity-50"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!hearingOn || hearingLabels.length === 0) return;
|
||||
goSelect(
|
||||
effectText.selectHearingTitle ?? effectText.hearing?.label ?? "听力补偿",
|
||||
hearingLabels,
|
||||
hearingSelect,
|
||||
"hearing_select",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="text-[14px] text-white/45">{hearingDisplay}</span>
|
||||
<ChevronRight size={16} className="ios-chevron" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</FeatureGate>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ const KEY_TO_SETTING: Record<string, string> = {
|
||||
effect_value: "effect_value",
|
||||
|
||||
crossfeed_value: "crossfeed_value",
|
||||
|
||||
hearing_select: "hearing_select",
|
||||
subwoofer_rate: "subwoofer_rate",
|
||||
|
||||
subwoofer_mix_type: "subwoofer_mix_type",
|
||||
|
||||
Reference in New Issue
Block a user