Files
Psychotoxical-psysonic/src/features/settings/components/AudioTab.tsx
T
Psychotoxical 209dd61442 refactor(lib): consolidate generic infra into src/lib
Move domain-agnostic, feature-free helpers out of the flat utils/ and store/
roots into src/lib/ (plan M4, §3 lib/ layer):
- lib/format/: formatBytes, formatClockTime, formatDuration, formatHumanDuration,
  relativeTime (pure format/date/byte/clock helpers)
- lib/i18n.ts: i18next bootstrap (global app infra)
- lib/util/: sanitizeHtml, platform, dedupeById, safeStorage (pure helpers +
  the zustand storage adapter)

playbackScheduleFormat stays in utils/format (runtime usePlayerStore dep =
audio-core coupling, not generic). formatClockTime keeps a type-only
@/store/authStoreTypes import (erased, no runtime inversion — accepted per the
deviceSync precedent).

Pure move via deep @/lib/* specifiers (no barrel → no mock-collapse surface).
tsc 0, lint 0/0, full suite 319 files / 2353 tests green.

Tooling note: lib_move.py regex extended to also rewrite side-effect imports
(import './i18n') and vi.importActual paths — both were silent gaps.
2026-06-30 08:07:04 +02:00

129 lines
4.5 KiB
TypeScript

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Blend, Gauge, Sliders, Volume2, Waves } from 'lucide-react';
import { useAuthStore } from '@/store/authStore';
import Equalizer from '@/components/Equalizer';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/utils/audio/loudnessPreAnalysisSlider';
import { useAudioDevicesProbe } from '@/hooks/useAudioDevicesProbe';
import { IS_MACOS } from '@/lib/util/platform';
import { AudioOutputDeviceSection } from '@/features/settings/components/audio/AudioOutputDeviceSection';
import { NormalizationBlock } from '@/features/settings/components/audio/NormalizationBlock';
import { PlaybackRateBlock } from '@/features/settings/components/audio/PlaybackRateBlock';
import { TrackTransitionsBlock } from '@/features/settings/components/audio/TrackTransitionsBlock';
import { TrackPreviewsSection } from '@/features/settings/components/audio/TrackPreviewsSection';
import { HiResCrossfadeResampleBlock } from '@/features/settings/components/audio/HiResCrossfadeResampleBlock';
export function AudioTab() {
const { t } = useTranslation();
const auth = useAuthStore();
const {
audioDevices,
osDefaultAudioDeviceId,
deviceSwitching,
devicesLoading,
setDeviceSwitching,
refreshAudioDevices,
} = useAudioDevicesProbe(t);
const preAnalysisEffectiveDb = useMemo(
() => effectiveLoudnessPreAnalysisAttenuationDb(
auth.loudnessPreAnalysisAttenuationDb,
auth.loudnessTargetLufs,
),
[auth.loudnessPreAnalysisAttenuationDb, auth.loudnessTargetLufs],
);
return (
<>
{/* Output-device picker is hidden on macOS — the stream is pinned to the
system default there, so the whole category is gated out. */}
{!IS_MACOS && (
<AudioOutputDeviceSection
audioDevices={audioDevices}
osDefaultAudioDeviceId={osDefaultAudioDeviceId}
deviceSwitching={deviceSwitching}
devicesLoading={devicesLoading}
setDeviceSwitching={setDeviceSwitching}
refreshAudioDevices={refreshAudioDevices}
t={t}
/>
)}
{/* Normalization — loudness levelling (own category) */}
<SettingsSubSection
title={t('settings.normalization')}
description={t('settings.normalizationDesc')}
icon={<Volume2 size={16} />}
>
<div className="settings-card">
<NormalizationBlock preAnalysisEffectiveDb={preAnalysisEffectiveDb} t={t} />
</div>
</SettingsSubSection>
{/* Track transitions — crossfade / gapless / AutoDJ (own category) */}
<SettingsSubSection
title={t('settings.transitionsTitle')}
description={t('settings.transitionsDesc')}
icon={<Blend size={16} />}
>
<div className="settings-card">
<TrackTransitionsBlock t={t} />
</div>
</SettingsSubSection>
{/* Native Hi-Res Playback */}
<SettingsSubSection
title={t('settings.hiResTitle')}
icon={<Waves size={16} />}
>
<div className="settings-card">
<SettingsGroup>
<SettingsToggle
desc={t('settings.hiResDesc')}
ariaLabel={t('settings.hiResEnabled')}
id="hires-enabled-toggle"
checked={auth.enableHiRes}
onChange={auth.setEnableHiRes}
/>
<HiResCrossfadeResampleBlock
enabled={auth.enableHiRes}
resampleHz={auth.hiResCrossfadeResampleHz}
onResampleHzChange={auth.setHiResCrossfadeResampleHz}
t={t}
/>
</SettingsGroup>
</div>
</SettingsSubSection>
{/* Equalizer */}
<SettingsSubSection
title={t('settings.eqTitle')}
icon={<Sliders size={16} />}
>
<div className="settings-card">
<SettingsGroup>
<Equalizer />
</SettingsGroup>
</div>
</SettingsSubSection>
{/* Playback speed */}
<SettingsSubSection
title={t('settings.playbackRateTitle')}
icon={<Gauge size={16} />}
>
<div className="settings-card">
<SettingsGroup>
<PlaybackRateBlock t={t} />
</SettingsGroup>
</div>
</SettingsSubSection>
<TrackPreviewsSection t={t} />
</>
);
}