修复&优化听力补偿模块功能

This commit is contained in:
eafonyang
2026-09-09 18:26:26 +08:00
parent dc69999890
commit 46c1226d29
7 changed files with 75 additions and 14 deletions
+25 -3
View File
@@ -1,7 +1,7 @@
import { lazy, Suspense } from "react"; import { lazy, Suspense, useEffect, useRef } from "react";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { Route, Switch, Router } from "wouter"; import { Route, Switch, Router, useLocation } from "wouter";
import { useHashLocation } from "wouter/use-hash-location"; import { useHashLocation } from "wouter/use-hash-location";
import ErrorBoundary from "./components/ErrorBoundary"; import ErrorBoundary from "./components/ErrorBoundary";
import PageLoader from "./components/PageLoader"; import PageLoader from "./components/PageLoader";
@@ -14,7 +14,7 @@ import AudioPage from "./pages/AudioPage";
import SystemPage from "./pages/SystemPage"; import SystemPage from "./pages/SystemPage";
import IOPage from "./pages/IOPage"; import IOPage from "./pages/IOPage";
import VUPage from "./pages/VUPage"; import VUPage from "./pages/VUPage";
import SelectPage from "./pages/SelectPage"; import SelectPage, { consumeSelectReturnScroll } from "./pages/SelectPage";
import CommunityPage from "./pages/CommunityPage"; import CommunityPage from "./pages/CommunityPage";
import ChangelogPage from "./pages/ChangelogPage"; import ChangelogPage from "./pages/ChangelogPage";
import { AIDrawerProvider } from "./contexts/AIDrawerContext"; import { AIDrawerProvider } from "./contexts/AIDrawerContext";
@@ -53,6 +53,27 @@ function EffectsPageRoute() {
); );
} }
/**
* Restores the calling page's scroll position when returning from /select.
* The select page is short, so the browser clamps window scroll to 0 while on
* it; navigateToSelect() captures the offset before navigating and we re-apply
* it once the calling page (e.g. Effects) has remounted.
*/
function SelectScrollRestore() {
const [location] = useLocation();
const prevRef = useRef(location);
useEffect(() => {
const prev = prevRef.current;
prevRef.current = location;
if (prev !== "/select" || location === "/select") return;
const y = consumeSelectReturnScroll();
if (y == null || y <= 0) return;
// Wait a frame so the returning page has rendered and the document has height.
requestAnimationFrame(() => window.scrollTo(0, y));
}, [location]);
return null;
}
function AppRouter() { function AppRouter() {
return ( return (
<Switch> <Switch>
@@ -83,6 +104,7 @@ function App() {
<Toaster /> <Toaster />
<Router hook={useHashLocation}> <Router hook={useHashLocation}>
<AIDrawerProvider> <AIDrawerProvider>
<SelectScrollRestore />
<div className={appShellOuterClass}> <div className={appShellOuterClass}>
<div className={appShellColumnClass}> <div className={appShellColumnClass}>
<DesktopAIShell> <DesktopAIShell>
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { import {
HEARING_FREQ_LABELS,
buildHearingSplineSeries, buildHearingSplineSeries,
resolveHearingFreqLabels,
} from "@/lib/hearingCurve"; } from "@/lib/hearingCurve";
const LEFT_COLOR = "rgb(0, 255, 246)"; const LEFT_COLOR = "rgb(0, 255, 246)";
@@ -29,6 +29,11 @@ export default function HearingCompensationChart({
const leftSeries = useMemo(() => buildHearingSplineSeries(left), [left]); const leftSeries = useMemo(() => buildHearingSplineSeries(left), [left]);
const rightSeries = useMemo(() => buildHearingSplineSeries(right), [right]); const rightSeries = useMemo(() => buildHearingSplineSeries(right), [right]);
// X axis adapts to the control-point count: 7-band (legacy) or 10-band (adds 3k/5k/6k).
const pointCount = Math.max(left.length, right.length);
const freqLabels = useMemo(() => resolveHearingFreqLabels(pointCount), [pointCount]);
const xMax = Math.max(0, pointCount - 1);
useEffect(() => { useEffect(() => {
const el = containerRef.current; const el = containerRef.current;
if (!el) return; if (!el) return;
@@ -76,13 +81,13 @@ export default function HearingCompensationChart({
xAxis: { xAxis: {
type: "value", type: "value",
min: 0, min: 0,
max: HEARING_FREQ_LABELS.length - 1, max: xMax,
interval: 1, interval: 1,
axisLabel: { axisLabel: {
formatter: (value: number) => { formatter: (value: number) => {
const idx = Math.round(value); const idx = Math.round(value);
if (Math.abs(value - idx) > 1e-6) return ""; if (Math.abs(value - idx) > 1e-6) return "";
return HEARING_FREQ_LABELS[idx] ?? ""; return freqLabels[idx] ?? "";
}, },
fontSize: 14, fontSize: 14,
color: "#999", color: "#999",
@@ -98,7 +103,9 @@ export default function HearingCompensationChart({
yAxis: { yAxis: {
type: "value", type: "value",
min: -80, min: -80,
max: 0, // Top raised to +20 dB: levels above 70 map to positive dB and were
// clipped at the old max of 0, leaving gaps at the top of the curve.
max: 20,
interval: 10, interval: 10,
name: "dB", name: "dB",
nameLocation: "end", nameLocation: "end",
@@ -157,7 +164,7 @@ export default function HearingCompensationChart({
ro.disconnect(); ro.disconnect();
window.removeEventListener("resize", onResize); window.removeEventListener("resize", onResize);
}; };
}, [leftSeries, rightSeries, leftLabel, rightLabel]); }, [leftSeries, rightSeries, leftLabel, rightLabel, freqLabels, xMax]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -171,7 +178,7 @@ export default function HearingCompensationChart({
ref={containerRef} ref={containerRef}
className={ className={
className className
?? "w-full h-[min(42vw,200px)] min-h-[140px] md:h-[260px] lg:h-[300px] rounded-lg bg-black/30" ?? "w-full h-[min(52vw,240px)] min-h-[180px] md:h-[320px] lg:h-[380px] rounded-lg bg-black/30"
} }
aria-hidden aria-hidden
/> />
-2
View File
@@ -9,8 +9,6 @@ export const FIRMWARE_FEATURE_MIN_VERSION = {
bootSoundExtendedSteps: 26, bootSoundExtendedSteps: 26,
/** Subwoofer LPF/HPF full-range toggles on the Effects page. */ /** Subwoofer LPF/HPF full-range toggles on the Effects page. */
subwooferFullRange: 2001, subwooferFullRange: 2001,
/** Hearing compensation section on the Effects page. */
hearingCompensation: 2007,
/** PEQ preset rename (peqRename) and reorder (peqSoft) in manage dialog. */ /** PEQ preset rename (peqRename) and reorder (peqSoft) in manage dialog. */
peqPresetRenameAndSort: 2008, peqPresetRenameAndSort: 2008,
} as const; } as const;
+17 -1
View File
@@ -1,7 +1,22 @@
/** Hearing compensation curve helpers: map levels → dB and cubic-spline smooth. */ /** Hearing compensation curve helpers: map levels → dB and cubic-spline smooth. */
/** 7-band control points (legacy): 125/250/500/1k/2k/4k/8k. */
export const HEARING_FREQ_LABELS = ["125", "250", "500", "1k", "2k", "4k", "8k"] as const; export const HEARING_FREQ_LABELS = ["125", "250", "500", "1k", "2k", "4k", "8k"] as const;
/** 10-band control points: adds 3k/5k/6k → 125/250/500/1k/2k/3k/4k/5k/6k/8k (ascending). */
export const HEARING_FREQ_LABELS_10 = [
"125", "250", "500", "1k", "2k", "3k", "4k", "5k", "6k", "8k",
] as const;
/**
* Resolve the X-axis frequency labels for a given control-point `count`.
* 10 points → the extended 3k/5k/6k set; any other count → the legacy 7-band
* set. Keeps the chart compatible with both 7-point and 10-point hearing data.
*/
export function resolveHearingFreqLabels(count: number): readonly string[] {
return count === HEARING_FREQ_LABELS_10.length ? HEARING_FREQ_LABELS_10 : HEARING_FREQ_LABELS;
}
/** Y = n + (-70). Values outside [-80, 0] are kept as-is (axis still -80…0). */ /** Y = n + (-70). Values outside [-80, 0] are kept as-is (axis still -80…0). */
export function mapHearingLevelToDb(n: number): number { export function mapHearingLevelToDb(n: number): number {
return n - 70; return n - 70;
@@ -92,7 +107,8 @@ export function createNaturalCubicSpline(
/** /**
* Build densely sampled [xIndex, dB] pairs for a smooth ECharts line. * Build densely sampled [xIndex, dB] pairs for a smooth ECharts line.
* X uses equal spacing 0…6 matching HEARING_FREQ_LABELS. * X uses equal spacing 0…N-1 where N = `levels.length`, so it adapts to both
* 7-point and 10-point hearing data (see `resolveHearingFreqLabels`).
*/ */
export function buildHearingSplineSeries( export function buildHearingSplineSeries(
levels: number[], levels: number[],
+3 -2
View File
@@ -280,7 +280,8 @@ export default function EffectsPage() {
const subwooferHpfOn = (ds?.subwoofer_hpf_enable ?? 0) === 1; const subwooferHpfOn = (ds?.subwoofer_hpf_enable ?? 0) === 1;
const firmwareVersion = parseFirmwareVersion(ds?.version); const firmwareVersion = parseFirmwareVersion(ds?.version);
const showSubwooferFullRange = isFirmwareFeatureAvailable("subwooferFullRange", firmwareVersion); const showSubwooferFullRange = isFirmwareFeatureAvailable("subwooferFullRange", firmwareVersion);
const showHearingCompensation = isFirmwareFeatureAvailable("hearingCompensation", firmwareVersion); // B9: hearing compensation has no firmware version gate — always shown.
const showHearingCompensation = true;
const subwooferDelayMainL = clampSubwooferDelayValue(ds?.subwoofer_delay_main ?? 0); const subwooferDelayMainL = clampSubwooferDelayValue(ds?.subwoofer_delay_main ?? 0);
const subwooferDelaySubL = clampSubwooferDelayValue(ds?.subwoofer_delay ?? 0); const subwooferDelaySubL = clampSubwooferDelayValue(ds?.subwoofer_delay ?? 0);
@@ -917,7 +918,7 @@ export default function EffectsPage() {
</button> </button>
{hearingOn && hearingProfiles[hearingIdx] && ( {hearingOn && hearingProfiles[hearingIdx] && (
<HearingCompensationChart <HearingCompensationChart
className="mt-3 w-full h-[min(42vw,200px)] min-h-[140px] md:h-[260px] lg:h-[300px] rounded-lg bg-black/30" className="mt-3 w-full h-[min(52vw,240px)] min-h-[180px] md:h-[320px] lg:h-[380px] rounded-lg bg-black/30"
left={hearingProfiles[hearingIdx].l} left={hearingProfiles[hearingIdx].l}
right={hearingProfiles[hearingIdx].r} right={hearingProfiles[hearingIdx].r}
leftLabel={effectText.hearing?.left ?? "Left"} leftLabel={effectText.hearing?.left ?? "Left"}
+13
View File
@@ -187,10 +187,23 @@ export function consumeSelectPageResult() {
// Scroll position of the calling page, captured before navigating to /select.
// The select page is short, so the browser clamps window scroll to 0 while on
// it; without restoring, the calling page (e.g. Effects) reopens at the top.
let selectReturnScroll: number | null = null;
/** Consume the stored scroll position (once). Returns null when absent. */
export function consumeSelectReturnScroll(): number | null {
const y = selectReturnScroll;
selectReturnScroll = null;
return y;
}
// Helper function to set select page state // Helper function to set select page state
export function navigateToSelect(title: string, options: string[], selected: number, back: string, key: string) { export function navigateToSelect(title: string, options: string[], selected: number, back: string, key: string) {
selectReturnScroll = window.scrollY;
selectPageState = { title, options, selected, back, key }; selectPageState = { title, options, selected, back, key };
} }
+4
View File
@@ -6,6 +6,10 @@
set -e set -e
# AWS CLI v2 默认把命令输出送入分页器(less)create-invalidation 结束后会停在
# (END) 提示符等待按键,导致脚本无法返回 shell。禁用分页器直接打印输出。
export AWS_PAGER=""
show_help() { show_help() {
cat <<EOF cat <<EOF
用法: ./scripts/deploy.sh [环境] [选项] 用法: ./scripts/deploy.sh [环境] [选项]