From 81f5635dd6692f816ab37072b49c0908b91c3b5a Mon Sep 17 00:00:00 2001 From: eafonyang Date: Thu, 22 Oct 2026 18:40:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86=E8=80=B3=E6=9C=BA?= =?UTF-8?q?=E9=9F=B3=E9=87=8F=E4=BF=9D=E6=8A=A4=E5=92=8C=E6=A8=A1=E6=8B=9F?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E7=9A=84=E5=90=84=E9=A1=B9=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/contexts/DeviceContext.tsx | 11 +++- client/src/lib/luxsinApi.ts | 41 ++++++++++++ client/src/locales/data-de.json | 47 ++++++++++++++ client/src/locales/data-en.json | 47 ++++++++++++++ client/src/locales/data-es.json | 47 ++++++++++++++ client/src/locales/data-fr.json | 47 ++++++++++++++ client/src/locales/data-ko.json | 47 ++++++++++++++ client/src/locales/data-pl.json | 47 ++++++++++++++ client/src/locales/data-pt.json | 47 ++++++++++++++ client/src/locales/data-vi.json | 47 ++++++++++++++ client/src/locales/data-zh-HK.json | 51 ++++++++++++++- client/src/locales/data-zh.json | 51 ++++++++++++++- client/src/pages/AudioPage.tsx | 93 ++++++++++++++++++++++----- client/src/pages/Home.tsx | 55 +++++++++++----- client/src/pages/SelectPage.tsx | 10 +++ 15 files changed, 650 insertions(+), 38 deletions(-) diff --git a/client/src/contexts/DeviceContext.tsx b/client/src/contexts/DeviceContext.tsx index f2c64a9..0b1205e 100644 --- a/client/src/contexts/DeviceContext.tsx +++ b/client/src/contexts/DeviceContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import { DeviceState, + getHomeVolumeMax, LuxsinAPI, MOCK_DEVICE_STATE, MOCK_PEQ_STATE, @@ -262,12 +263,16 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) { }, [api, isDemoMode, applyPeqFiltersToState]); // Optimistic setters + const maxVolume = getHomeVolumeMax(deviceState); const setVolume = useCallback((v: number) => { - setDeviceState(prev => prev ? { ...prev, volume: v } : prev); + if (!Number.isFinite(v)) return; + // 指令和乐观状态使用同一上限,防止任何首页入口发送超限音量。 + const volume = Math.max(0, Math.min(maxVolume, Math.round(v))); + setDeviceState(prev => prev ? { ...prev, volume } : prev); if (!isDemoMode && api) { - api.setVolume(v).catch(console.error); + api.setVolume(volume).catch(console.error); } - }, [api, isDemoMode]); + }, [api, isDemoMode, maxVolume]); const setInput = useCallback((v: number) => { setDeviceState(prev => prev ? { ...prev, input: v } : prev); diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index 4c92312..20aeb3b 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -21,6 +21,32 @@ export function volumeToDb(vol: number): number { return (vol - 200) / 2; } +/** 耳机输出(协议索引 2)使用设置上限;其他输出及未设置时保持 0dB。 */ +export function getHomeVolumeMax( + state: Pick | null | undefined, +): number { + if (state?.output !== 2) return 200; + const rawIndex = Number(state.hpMaxVolume ?? 0); + const index = Number.isFinite(rawIndex) + ? Math.max(0, Math.min(10, Math.trunc(rawIndex))) + : 0; + // 每档降低 5dB,即 10 个设备音量单位。 + return dbToVolume(-5 * index); +} + +/** + * 耳机输出(协议索引 2)且用户设置了耳机最大音量(低于 0dB)时,返回该上限的 dB 值(负数); + * 其他输出或未设置(0 档)返回 null。 + */ +export function getHeadphoneMaxVolumeDb( + state: Pick | null | undefined, +): number | null { + if (state?.output !== 2) return null; + const rawIndex = Number(state.hpMaxVolume ?? 0); + if (!Number.isFinite(rawIndex) || Math.trunc(rawIndex) <= 0) return null; + return volumeToDb(getHomeVolumeMax(state)); +} + export function decodeCustomBase64(encoded: string): string { let translated = ""; for (let i = 0; i < encoded.length; i++) { @@ -95,8 +121,18 @@ export interface DeviceState { dacGain: number; dacArc: number; dacImpedance: number; + /** Headphone max volume: 0 = 0dB, 1 = -5dB … 10 = -50dB (5dB steps). */ + hpMaxVolume: number; + /** Headphone startup volume: 0 = default, 1 = -20dB … 7 = -50dB (5dB steps). */ + hpStartupVolume: number; + /** Headphone amp working mode: 0 = Class AB, 1 = Class A. */ + hpAmpMode: number; dacVolumeDirect: number; analogGain: number; + /** Analog input port: 0 = balanced, 1 = unbalanced. */ + analog_input: number; + /** Analog input bypass: 0 = off, 1 = on. */ + analog_bypass: number; effect_enable: number; effect_value: number; width_enable: number; @@ -582,8 +618,13 @@ export const MOCK_DEVICE_STATE: DeviceState = { dacGain: 0, dacArc: 0, dacImpedance: 0, + hpMaxVolume: 0, + hpStartupVolume: 0, + hpAmpMode: 0, dacVolumeDirect: 0, analogGain: 0, + analog_input: 0, + analog_bypass: 0, effect_enable: 0, effect_value: 50, width_enable: 0, diff --git a/client/src/locales/data-de.json b/client/src/locales/data-de.json index d16e0ca..45fb539 100644 --- a/client/src/locales/data-de.json +++ b/client/src/locales/data-de.json @@ -8,6 +8,7 @@ "failedHint": "Geben Sie das Gerät in der URL an, z.B. ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "Das maximale Kopfhörervolumen ist eingestellt. Maximum: {db}dB", "audioSet": "Audioeinstellung", "systemSet": "System", "power": "Stromversorgung", @@ -387,13 +388,59 @@ } }, "dac": { + "sectionHpSettings": "Kopfhörer-Einstellungen", + "hpMaxVolume": { + "label": "Max. Kopfhörerlautstärke", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Kopfhörer-Startlautstärke", + "options": [ + { "index": 0, "label": "Standard" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "Kopfhörerverstärker-Modus", + "options": [ + { "index": 0, "label": "Class AB" }, + { "index": 1, "label": "Class A" } + ] + }, "sectionBalanceSensitivity": "Balance & Empfindlichkeit", + "sectionAnalogInput": "Analogeingang", "sectionOutput": "Ausgangsmerkmale", "sectionIisSettings": "IIS-Einstellungen", "sectionSystemInterface": "System & Schnittstelle", "balance": "Links-Rechts-Balance", "sensitivity": "VU-Meter Empfindlichkeit", "analogInputGain": "Analoge Eingangsverstärkung", + "analogInput": { + "label": "Eingang", + "options": [ + { "index": 0, "label": "Symmetrisch" }, + { "index": 1, "label": "Asymmetrisch" } + ] + }, + "analogBypass": "Analogeingang-Bypass", "autoImpedanceDetection": "Automatische Impedanz-Erkennung", "dacVolumeDirect": { "label": "Vorverstärker-Lautstärke-Durchleitung", diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json index 49ab554..123960c 100644 --- a/client/src/locales/data-en.json +++ b/client/src/locales/data-en.json @@ -8,6 +8,7 @@ "failedHint": "Specify the device in the URL, e.g. ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "Headphone max volume is set. Maximum: {db}dB", "audioSet": "Audio setting", "systemSet": "System", "power": "Power", @@ -387,13 +388,59 @@ } }, "dac": { + "sectionHpSettings": "Headphone settings", + "hpMaxVolume": { + "label": "Max headphone volume", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Headphone startup volume", + "options": [ + { "index": 0, "label": "Default" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "HP amp mode", + "options": [ + { "index": 0, "label": "Class AB" }, + { "index": 1, "label": "Class A" } + ] + }, "sectionBalanceSensitivity": "Balance & sensitivity", + "sectionAnalogInput": "Analog input", "sectionOutput": "Output characteristics", "sectionIisSettings": "IIS settings", "sectionSystemInterface": "System & interface", "balance": "Left-right balance", "sensitivity": "VU Meter Sensitivity", "analogInputGain": "Analog input gain", + "analogInput": { + "label": "Input port", + "options": [ + { "index": 0, "label": "Balanced" }, + { "index": 1, "label": "Unbalanced" } + ] + }, + "analogBypass": "Analog input bypass", "autoImpedanceDetection": "Auto Impedance Detection", "dacVolumeDirect": { "label": "Pre-out Volume passthrough", diff --git a/client/src/locales/data-es.json b/client/src/locales/data-es.json index b58f9b4..bb6e87d 100644 --- a/client/src/locales/data-es.json +++ b/client/src/locales/data-es.json @@ -8,6 +8,7 @@ "failedHint": "Especifique el dispositivo en la URL, ej. ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "El volumen máximo de los auriculares está configurado. Máximo: {db}dB", "audioSet": "Configuración de audio", "systemSet": "Sistema", "power": "Alimentación", @@ -387,13 +388,59 @@ } }, "dac": { + "sectionHpSettings": "Ajustes de auriculares", + "hpMaxVolume": { + "label": "Volumen máx. de auriculares", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Volumen de inicio de auriculares", + "options": [ + { "index": 0, "label": "Predeterminado" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "Modo del amplificador de auriculares", + "options": [ + { "index": 0, "label": "Clase AB" }, + { "index": 1, "label": "Clase A" } + ] + }, "sectionBalanceSensitivity": "Balance y sensibilidad", + "sectionAnalogInput": "Entrada analógica", "sectionOutput": "Características de salida", "sectionIisSettings": "Configuración IIS", "sectionSystemInterface": "Sistema e interfaz", "balance": "Balance izquierda-derecha", "sensitivity": "Sensibilidad del VU-metro", "analogInputGain": "Ganancia de entrada analógica", + "analogInput": { + "label": "Puerto de entrada", + "options": [ + { "index": 0, "label": "Balanceada" }, + { "index": 1, "label": "No balanceada" } + ] + }, + "analogBypass": "Bypass de entrada analógica", "autoImpedanceDetection": "Detección automática de impedancia", "dacVolumeDirect": { "label": "Paso de volumen de pre-salida", diff --git a/client/src/locales/data-fr.json b/client/src/locales/data-fr.json index 44513a5..98064dc 100644 --- a/client/src/locales/data-fr.json +++ b/client/src/locales/data-fr.json @@ -8,6 +8,7 @@ "failedHint": "Spécifiez l'appareil dans l'URL, par ex. ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "Le volume maximal du casque est défini. Maximum : {db}dB", "audioSet": "Réglage audio", "systemSet": "Système", "power": "Alimentation", @@ -387,13 +388,59 @@ } }, "dac": { + "sectionHpSettings": "Réglages du casque", + "hpMaxVolume": { + "label": "Volume max. du casque", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Volume de démarrage du casque", + "options": [ + { "index": 0, "label": "Par défaut" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "Mode de l'ampli casque", + "options": [ + { "index": 0, "label": "Classe AB" }, + { "index": 1, "label": "Classe A" } + ] + }, "sectionBalanceSensitivity": "Balance et sensibilité", + "sectionAnalogInput": "Entrée analogique", "sectionOutput": "Caractéristiques de sortie", "sectionIisSettings": "Paramètres IIS", "sectionSystemInterface": "Système et interface", "balance": "Balance gauche-droite", "sensitivity": "Sensibilité VU-mètre", "analogInputGain": "Gain d'entrée analogique", + "analogInput": { + "label": "Port d'entrée", + "options": [ + { "index": 0, "label": "Symétrique" }, + { "index": 1, "label": "Asymétrique" } + ] + }, + "analogBypass": "Bypass d'entrée analogique", "autoImpedanceDetection": "Détection automatique d'impédance", "dacVolumeDirect": { "label": "Passage de volume pré-sortie", diff --git a/client/src/locales/data-ko.json b/client/src/locales/data-ko.json index bd05b5c..65f13d6 100644 --- a/client/src/locales/data-ko.json +++ b/client/src/locales/data-ko.json @@ -8,6 +8,7 @@ "failedHint": "URL에 장치를 지정하세요. 예: ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "헤드폰 최대 볼륨이 설정되었습니다. 최댓값: {db}dB", "audioSet": "오디오 설정", "systemSet": "시스템", "power": "전원", @@ -457,13 +458,59 @@ } }, "dac": { + "sectionHpSettings": "헤드폰 설정", + "hpMaxVolume": { + "label": "최대 헤드폰 볼륨", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "헤드폰 시작 볼륨", + "options": [ + { "index": 0, "label": "기본" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "헤드폰 앰프 모드", + "options": [ + { "index": 0, "label": "클래스 AB" }, + { "index": 1, "label": "클래스 A" } + ] + }, "sectionBalanceSensitivity": "밸런스 및 감도", + "sectionAnalogInput": "아날로그 입력", "sectionOutput": "출력 특성", "sectionIisSettings": "IIS 설정", "sectionSystemInterface": "시스템 및 인터페이스", "balance": "좌우 밸런스", "sensitivity": "VU 미터 감도", "analogInputGain": "아날로그 입력 게인", + "analogInput": { + "label": "입력 포트", + "options": [ + { "index": 0, "label": "밸런스" }, + { "index": 1, "label": "언밸런스" } + ] + }, + "analogBypass": "아날로그 입력 바이패스", "autoImpedanceDetection": "자동 임피던스 감지", "dacVolumeDirect": { "label": "프리아웃 볼륨 패스스루", diff --git a/client/src/locales/data-pl.json b/client/src/locales/data-pl.json index d992431..ac33f3e 100644 --- a/client/src/locales/data-pl.json +++ b/client/src/locales/data-pl.json @@ -8,6 +8,7 @@ "failedHint": "Określ urządzenie w adresie URL, np. ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "Ustawiono maksymalną głośność słuchawek. Maksimum: {db}dB", "audioSet": "Ustawienia dźwięku", "systemSet": "System", "power": "Zasilanie", @@ -457,13 +458,59 @@ } }, "dac": { + "sectionHpSettings": "Ustawienia słuchawek", + "hpMaxVolume": { + "label": "Maksymalna głośność słuchawek", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Głośność startowa słuchawek", + "options": [ + { "index": 0, "label": "Domyślna" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "Tryb wzmacniacza słuchawek", + "options": [ + { "index": 0, "label": "Klasa AB" }, + { "index": 1, "label": "Klasa A" } + ] + }, "sectionBalanceSensitivity": "Równowaga i czułość", + "sectionAnalogInput": "Wejście analogowe", "sectionOutput": "Charakterystyka wyjścia", "sectionIisSettings": "Ustawienia IIS", "sectionSystemInterface": "System i interfejs", "balance": "Równowaga lewo-prawo", "sensitivity": "Czułość miernika VU", "analogInputGain": "Wzmocnienie wejścia analogowego", + "analogInput": { + "label": "Port wejściowy", + "options": [ + { "index": 0, "label": "Zbalansowane" }, + { "index": 1, "label": "Niezbalansowane" } + ] + }, + "analogBypass": "Bypass wejścia analogowego", "autoImpedanceDetection": "Automatyczne wykrywanie impedancji", "dacVolumeDirect": { "label": "Przekazywanie głośności wyjścia przedwzmacniacza", diff --git a/client/src/locales/data-pt.json b/client/src/locales/data-pt.json index 77e8b61..f1b48c9 100644 --- a/client/src/locales/data-pt.json +++ b/client/src/locales/data-pt.json @@ -8,6 +8,7 @@ "failedHint": "Especifique o dispositivo no URL, por exemplo ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "O volume máximo dos auscultadores está definido. Máximo: {db}dB", "audioSet": "Definições de áudio", "systemSet": "Sistema", "power": "Energia", @@ -457,13 +458,59 @@ } }, "dac": { + "sectionHpSettings": "Definições de auscultadores", + "hpMaxVolume": { + "label": "Volume máximo dos auscultadores", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Volume de arranque dos auscultadores", + "options": [ + { "index": 0, "label": "Predefinido" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "Modo do amplificador dos auscultadores", + "options": [ + { "index": 0, "label": "Classe AB" }, + { "index": 1, "label": "Classe A" } + ] + }, "sectionBalanceSensitivity": "Equilíbrio e sensibilidade", + "sectionAnalogInput": "Entrada analógica", "sectionOutput": "Características de saída", "sectionIisSettings": "Definições IIS", "sectionSystemInterface": "Sistema e interface", "balance": "Equilíbrio esquerda-direita", "sensitivity": "Sensibilidade do medidor VU", "analogInputGain": "Ganho de entrada analógica", + "analogInput": { + "label": "Porta de entrada", + "options": [ + { "index": 0, "label": "Balanceada" }, + { "index": 1, "label": "Não balanceada" } + ] + }, + "analogBypass": "Bypass de entrada analógica", "autoImpedanceDetection": "Deteção automática de impedância", "dacVolumeDirect": { "label": "Bypass de volume de pré-saída", diff --git a/client/src/locales/data-vi.json b/client/src/locales/data-vi.json index b0905c6..7bd84a4 100644 --- a/client/src/locales/data-vi.json +++ b/client/src/locales/data-vi.json @@ -8,6 +8,7 @@ "failedHint": "Hãy chỉ định thiết bị trong URL, ví dụ ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "Đã đặt âm lượng tai nghe tối đa. Tối đa: {db}dB", "audioSet": "Cài đặt âm thanh", "systemSet": "Hệ thống", "power": "Nguồn", @@ -457,13 +458,59 @@ } }, "dac": { + "sectionHpSettings": "Cài đặt tai nghe", + "hpMaxVolume": { + "label": "Âm lượng tai nghe tối đa", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Âm lượng khởi động tai nghe", + "options": [ + { "index": 0, "label": "Mặc định" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "Chế độ ampli tai nghe", + "options": [ + { "index": 0, "label": "Class AB" }, + { "index": 1, "label": "Class A" } + ] + }, "sectionBalanceSensitivity": "Cân bằng & độ nhạy", + "sectionAnalogInput": "Đầu vào analog", "sectionOutput": "Đặc tính đầu ra", "sectionIisSettings": "Cài đặt IIS", "sectionSystemInterface": "Hệ thống & giao diện", "balance": "Cân bằng trái-phải", "sensitivity": "Độ nhạy đồng hồ VU", "analogInputGain": "Độ lợi đầu vào analog", + "analogInput": { + "label": "Cổng đầu vào", + "options": [ + { "index": 0, "label": "Cân bằng" }, + { "index": 1, "label": "Không cân bằng" } + ] + }, + "analogBypass": "Bypass đầu vào analog", "autoImpedanceDetection": "Tự động phát hiện trở kháng", "dacVolumeDirect": { "label": "Passthrough âm lượng Pre-out", diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json index cb0a393..94b031d 100644 --- a/client/src/locales/data-zh-HK.json +++ b/client/src/locales/data-zh-HK.json @@ -8,6 +8,7 @@ "failedHint": "透過網址列指定裝置,例如 ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "已設定耳機最大音量,最大音量為:{db}dB", "audioSet": "音訊設定", "systemSet": "系統設定", "power": "電源", @@ -56,9 +57,9 @@ "pageTitle": "輸入/輸出", "inputSection": "輸入", "outputSection": "輸出", - "inputOptions": ["USB-B", "USB-C", "同軸", "光纖", "藍牙", "IIS", "類比RCA", "本地輸入"], + "inputOptions": ["USB-B", "USB-C", "同軸", "光纖", "藍牙", "IIS", "模擬", "本地輸入"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "耳機"], - "input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "類比RCA" }, + "input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "模擬" }, "output": { "headset": "耳機" } }, "effect": { @@ -381,13 +382,59 @@ } }, "dac": { + "sectionHpSettings": "耳機設定", + "hpMaxVolume": { + "label": "耳機最大音量", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "耳機啟動音量", + "options": [ + { "index": 0, "label": "預設" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "耳放工作模式", + "options": [ + { "index": 0, "label": "甲乙類" }, + { "index": 1, "label": "甲類" } + ] + }, "sectionBalanceSensitivity": "平衡與靈敏度", + "sectionAnalogInput": "模擬輸入", "sectionOutput": "輸出特性", "sectionIisSettings": "IIS設定", "sectionSystemInterface": "系統與介面", "balance": "左右平衡", "sensitivity": "VU表靈敏度", "analogInputGain": "模擬輸入增益", + "analogInput": { + "label": "輸入口", + "options": [ + { "index": 0, "label": "平衡" }, + { "index": 1, "label": "非平衡" } + ] + }, + "analogBypass": "模擬輸入旁路", "autoImpedanceDetection": "自動檢測耳機阻抗", "dacVolumeDirect": { "label": "前級輸出音量直通模式", diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json index a63a1d4..641f487 100644 --- a/client/src/locales/data-zh.json +++ b/client/src/locales/data-zh.json @@ -8,6 +8,7 @@ "failedHint": "通过地址栏指定设备,例如 ?ip={ip}" }, "home": { + "hpMaxVolumeHint": "已设置耳机最大音量,最大音量为:{db}dB", "audioSet": "音频设置", "systemSet": "系统设置", "power": "电源", @@ -56,9 +57,9 @@ "pageTitle": "输入输出", "inputSection": "输入", "outputSection": "输出", - "inputOptions": ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "模拟RCA", "本地输入"], + "inputOptions": ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "模拟", "本地输入"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "耳机"], - "input": { "coaxial": "同轴", "optical": "光纤", "bluetooth": "蓝牙", "rca": "模拟RCA" }, + "input": { "coaxial": "同轴", "optical": "光纤", "bluetooth": "蓝牙", "rca": "模拟" }, "output": { "headset": "耳机" } }, "effect": { @@ -381,13 +382,59 @@ } }, "dac": { + "sectionHpSettings": "耳机设定", + "hpMaxVolume": { + "label": "耳机最大音量", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "耳机启动音量", + "options": [ + { "index": 0, "label": "默认" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, + "hpAmpMode": { + "label": "耳放工作模式", + "options": [ + { "index": 0, "label": "甲乙类" }, + { "index": 1, "label": "甲类" } + ] + }, "sectionBalanceSensitivity": "平衡与灵敏度", + "sectionAnalogInput": "模拟输入", "sectionOutput": "输出特性", "sectionIisSettings": "IIS设置", "sectionSystemInterface": "系统与接口", "balance": "左右平衡", "sensitivity": "vu表灵敏度", "analogInputGain": "模拟输入增益", + "analogInput": { + "label": "输入口", + "options": [ + { "index": 0, "label": "平衡" }, + { "index": 1, "label": "非平衡" } + ] + }, + "analogBypass": "模拟输入旁路", "autoImpedanceDetection": "自动检测耳机阻抗", "dacVolumeDirect": { "label": "前级输出音量直通模式", diff --git a/client/src/pages/AudioPage.tsx b/client/src/pages/AudioPage.tsx index 70403bc..d12bc5e 100644 --- a/client/src/pages/AudioPage.tsx +++ b/client/src/pages/AudioPage.tsx @@ -2,8 +2,10 @@ AUDIO PAGE — Audio Settings Design: Reference luxsin_x8_音频设置.png Sections: - - 平衡与灵敏度: 左右平衡, VU灵敏度, 模拟输入增益 sliders - - 输出特性: 滤波特性, 耳机增益, 音量幅度 + - 平衡与灵敏度: 左右平衡, VU灵敏度 sliders + - 模拟输入: 输入口, 模拟输入旁路, 模拟输入增益 + - 输出特性: 滤波特性, 前级输出音量直通模式, ARC模式, 音量幅度 + - 耳机设定: 耳机增益, 自动检测耳机阻抗, 耳机最大音量, 耳机启动音量, 耳放工作模式 - IIS设置: IIS静音电平, IIS模式 - 系统与接口: 开机音量, XLR极性 All list rows → navigate to /select page @@ -110,15 +112,22 @@ function ListRow({ label, value, onClick, toggle, checked, onToggle }: { type LocaleDac = { dac?: { sectionBalanceSensitivity?: string; + sectionAnalogInput?: string; sectionOutput?: string; + sectionHpSettings?: string; sectionIisSettings?: string; sectionSystemInterface?: string; balance?: string; sensitivity?: string; analogInputGain?: string; + analogInput?: { label?: string; options?: Array<{ index: number; label: string }> }; + analogBypass?: string; autoImpedanceDetection?: string; filters?: { label?: string; options?: Array<{ index: number; label: string }> }; dacGain?: { label?: string; options?: Array<{ index: number; label: string }> }; + hpMaxVolume?: { label?: string; options?: Array<{ index: number; label: string }> }; + hpStartupVolume?: { label?: string; options?: Array<{ index: number; label: string }> }; + hpAmpMode?: { label?: string; options?: Array<{ index: number; label: string }> }; dacVolumeDirect?: { label?: string; options?: Array<{ index: number; label: string }> }; dacArc?: { label?: string; options?: Array<{ index: number; label: string }> }; volumeStep?: string; @@ -225,7 +234,12 @@ export default function AudioPage() { const gainIdx = ds?.dacGain ?? 0; const autoImpedanceOn = (ds?.dacImpedance ?? 0) === 1; + const hpMaxVolumeIdx = Math.min(Math.max(ds?.hpMaxVolume ?? 0, 0), 10); + const hpStartupVolumeIdx = Math.min(Math.max(ds?.hpStartupVolume ?? 0, 0), 7); + const hpAmpModeIdx = Math.min(Math.max(ds?.hpAmpMode ?? 0, 0), 1); const dacVolumeDirectIdx = ds?.dacVolumeDirect ?? 0; + const analogInputIdx = Math.min(Math.max(ds?.analog_input ?? 0, 0), 1); + const analogBypassOn = (ds?.analog_bypass ?? 0) === 1; const dacArcIdx = ds?.dacArc ?? 0; const stepIdx = ds?.soundStep ?? 1; const filterIdx = ds?.pcm ?? 0; @@ -241,6 +255,14 @@ export default function AudioPage() { const FILTER_OPTIONS = optionLabels(dacText.filters?.options, ["快速滚降", "慢速滚降", "短延迟快速滚降", "短延迟慢速滚降", "超慢速滚降", "低离散短延迟"]); const GAIN_OPTIONS = optionLabels(dacText.dacGain?.options, ["低", "中", "高"]); + const HP_MAX_VOLUME_OPTIONS = optionLabels(dacText.hpMaxVolume?.options, [ + "0dB", "-5dB", "-10dB", "-15dB", "-20dB", "-25dB", "-30dB", "-35dB", "-40dB", "-45dB", "-50dB", + ]); + const HP_STARTUP_VOLUME_OPTIONS = optionLabels(dacText.hpStartupVolume?.options, [ + "默认", "-20dB", "-25dB", "-30dB", "-35dB", "-40dB", "-45dB", "-50dB", + ]); + const HP_AMP_MODE_OPTIONS = optionLabels(dacText.hpAmpMode?.options, ["甲乙类", "甲类"]); + const ANALOG_INPUT_OPTIONS = optionLabels(dacText.analogInput?.options, ["平衡", "非平衡"]); const DAC_VOLUME_DIRECT_OPTIONS = optionLabels(dacText.dacVolumeDirect?.options, ["关闭", "0dB", "-12dB"]); const DAC_ARC_OPTIONS = optionLabels(dacText.dacArc?.options, ["ARC", "EARC"]); const XLR_OPTIONS = optionLabels(dacText.xlr?.options, ["正常", "反转"]); @@ -318,8 +340,23 @@ export default function AudioPage() { }} /> -
-
+
+ + {/* ── 模拟输入 ── */} + +
+ goSelect(dacText.analogInput?.label ?? "输入口", ANALOG_INPUT_OPTIONS, analogInputIdx, "analogInput")} + /> + updateSetting({ analog_bypass: v ? 1 : 0 })} + /> +
{dacText.analogInputGain ?? "模拟输入增益"} @@ -354,17 +391,6 @@ export default function AudioPage() { onClick={() => goSelect(dacText.filters?.label ?? "选择滤波特性", FILTER_OPTIONS, filterIdx, "filterCharacteristic")} /> )} - goSelect(dacText.dacGain?.label ?? "选择耳机增益", GAIN_OPTIONS, gainIdx, "dacGain")} - /> - updateSetting({ dacImpedance: v ? 1 : 0 })} - />
+ {/* ── 耳机设定 ── */} + +
+ goSelect(dacText.dacGain?.label ?? "选择耳机增益", GAIN_OPTIONS, gainIdx, "dacGain")} + /> + updateSetting({ dacImpedance: v ? 1 : 0 })} + /> + + goSelect(dacText.hpMaxVolume?.label ?? "耳机最大音量", HP_MAX_VOLUME_OPTIONS, hpMaxVolumeIdx, "hpMaxVolume") + } + /> + + goSelect(dacText.hpStartupVolume?.label ?? "耳机启动音量", HP_STARTUP_VOLUME_OPTIONS, hpStartupVolumeIdx, "hpStartupVolume") + } + /> + + goSelect(dacText.hpAmpMode?.label ?? "耳放工作模式", HP_AMP_MODE_OPTIONS, hpAmpModeIdx, "hpAmpMode") + } + /> +
+ {/* ── IIS 设置 ── */}
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index be2d475..657123d 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -40,6 +40,8 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { + getHeadphoneMaxVolumeDb, + getHomeVolumeMax, INPUT_BLUETOOTH_INDEX, INPUT_LABELS, isHomeVolumeLockedByPassthrough, @@ -247,7 +249,8 @@ export default function Home() { return null; }, [volumeConfirm, homeText]); - const [localVol, setLocalVol] = useState(deviceState?.volume ?? 100); + const maxVolume = getHomeVolumeMax(deviceState); + const [localVol, setLocalVol] = useState(Math.min(maxVolume, deviceState?.volume ?? 100)); const isDragging = useRef(false); const knobDraggingRef = useRef(false); const knobActivePointerIdRef = useRef(null); @@ -257,9 +260,9 @@ export default function Home() { useEffect(() => { if (!isDragging.current && deviceState?.volume !== undefined) { - setLocalVol(deviceState.volume); + setLocalVol(Math.min(maxVolume, deviceState.volume)); } - }, [deviceState?.volume]); + }, [deviceState?.volume, maxVolume]); useEffect(() => { knobLastVolRef.current = localVol; @@ -450,16 +453,26 @@ export default function Home() { const vuLabel = `${homeText.vu ?? "VU表"}${(ds.vu ?? 0) + 1}`; const bypassOn = (ds.dsp_enable ?? 0) === 0; + // 耳机输出且已设置耳机最大音量时,提示当前上限,避免用户误以为还能加到 0dB。 + const hpMaxVolumeDb = getHeadphoneMaxVolumeDb(ds); + const hpMaxVolumeHint = + hpMaxVolumeDb === null + ? null + : (homeText.hpMaxVolumeHint ?? "已设置耳机最大音量,最大音量为:{db}dB").replace( + "{db}", + String(hpMaxVolumeDb), + ); + // Home 的 volume 取值为 0..200,对应 dB = (v - 200) / 2 // 因此 1 个 volume 单位 = 0.5dB // AudioPage 的 soundStep:0->0.5dB, 1->1dB, 2->2dB, 3->3dB // 换算为 volume 单位:0.5/1/2/3 dB -> 1/2/4/6 const fineVolumeStep = [1, 2, 4, 6][ds.soundStep ?? 1] ?? 1; - // Slider fill % (0-200 → 0-100%) - const fillPct = (localVol / 200) * 100; - // Knob angle (map 0..200 -> -135..+135 degrees) - const knobAngle = -135 + (localVol / 200) * 270; + // Slider fill % (0-maxVolume → 0-100%) + const fillPct = (localVol / maxVolume) * 100; + // Knob angle (map 0..maxVolume -> -135..+135 degrees) + const knobAngle = -135 + (localVol / maxVolume) * 270; const pointAngle = (clientX: number, clientY: number, rect: DOMRect) => { const cx = rect.left + rect.width / 2; @@ -494,9 +507,9 @@ export default function Home() { const rect = el.getBoundingClientRect(); const a = pointAngle(e.clientX, e.clientY, rect); const delta = normalizeDelta(a - knobStartAngleRef.current); - // 270deg travel -> full scale; 1 rad maps to ~200/(1.5*pi) volume steps - const stepsPerRad = 200 / (Math.PI * 1.5); - const next = Math.max(0, Math.min(200, Math.round(knobStartVolRef.current + delta * stepsPerRad))); + // 270deg travel -> full scale; 1 rad maps to ~maxVolume/(1.5*pi) volume steps + const stepsPerRad = maxVolume / (Math.PI * 1.5); + const next = Math.max(0, Math.min(maxVolume, Math.round(knobStartVolRef.current + delta * stepsPerRad))); setLocalVol(next); knobLastVolRef.current = next; }; @@ -700,7 +713,7 @@ export default function Home() { }} /> { if (blockVolumeAdjust()) return; - const next = Math.max(0, Math.min(200, localVol - fineVolumeStep)); + const next = Math.max(0, Math.min(maxVolume, localVol - fineVolumeStep)); setLocalVol(next); applyVolume(next); }} @@ -797,7 +810,7 @@ export default function Home() {
+ {hpMaxVolumeHint && ( +

+ {hpMaxVolumeHint} +

+ )} + {/* Bluetooth info — only when input source is Bluetooth */} {ds.input === INPUT_BLUETOOTH_INDEX && ds.bt_status !== 0 && (
@@ -986,8 +1009,8 @@ export default function Home() { setLocalVol(0); applyVolume(0); } else if (volumeConfirm === "max") { - setLocalVol(200); - applyVolume(200); + setLocalVol(maxVolume); + applyVolume(maxVolume); } setVolumeConfirm(null); }} diff --git a/client/src/pages/SelectPage.tsx b/client/src/pages/SelectPage.tsx index 147c8e3..b47b9a1 100644 --- a/client/src/pages/SelectPage.tsx +++ b/client/src/pages/SelectPage.tsx @@ -125,6 +125,16 @@ const KEY_TO_SETTING: Record = { dacImpedance: "dacImpedance", + hpMaxVolume: "hpMaxVolume", + + hpStartupVolume: "hpStartupVolume", + + hpAmpMode: "hpAmpMode", + + analogInput: "analog_input", + + analogBypass: "analog_bypass", + dacVolumeDirect: "dacVolumeDirect", dacArc: "dacArc",