import { useState, type CSSProperties, type JSX, type ReactNode } from 'react' import type { ScopeKind } from '../../types/scope' import { SCOPE_LABELS, isTransformableScopeKind } from '../../types/scope' import type { ScopeSettings } from '../../types/settings' import { SCOPE_DISPLAY_ROTATIONS, type ScopeDisplayRotation } from '../../types/scopeTransform' import { MAX_SPECTROGRAM_CONTRAST, MAX_SPECTROGRAM_TILT_DB_PER_OCTAVE, MIN_SPECTROGRAM_CONTRAST, MIN_SPECTROGRAM_TILT_DB_PER_OCTAVE, SPECTROGRAM_CONTRAST_STEP, SPECTROGRAM_TILT_STEP, } from '../../types/spectrogram' import { DEFAULT_VU_REFERENCE_DBFS, VU_REFERENCE_MAX_DBFS, VU_REFERENCE_MIN_DBFS, VU_REFERENCE_PRESETS, findVUReferencePreset, sanitizeVUReferenceDbfs, } from '../../types/vumeter' import { MAX_WAVEFORM_SCROLL_SPEED, MIN_WAVEFORM_SCROLL_SPEED, WAVEFORM_SCROLL_SPEED_STEP, } from '../../types/waveform' import ThemedSelect from './ThemedSelect' function vectorscopeModeLabel(mode: ScopeSettings['vectorscope']['mode']): string { switch (mode) { case 'lissajous': return 'Lissajous' case 'polar-unipolar': return 'Polar Uni' case 'polar-bipolar': return 'Polar Bi' case 'linear-unipolar': return 'Linear Uni' case 'linear-bipolar': return 'Linear Bi' } } function lufsReadoutLabel(readout: ScopeSettings['lufsmeter']['readout']): string { switch (readout) { case 'integrated': return 'Integrated' case 'shortTerm': return 'Short-term' case 'momentary': return 'Momentary' } } function nowPlayingVisibleLabels(settings: ScopeSettings['nowPlaying']): string[] { const labels: string[] = [] if (settings.showCoverArt) labels.push('Cover') if (settings.showTitle) labels.push('Title') if (settings.showArtist) labels.push('Artist') if (settings.showProgress) labels.push('Bar') if (settings.showTime) labels.push('Time') if (settings.showControls) labels.push('Controls') return labels } function appendTransformSummary( parts: string[], settings: { rotation: ScopeDisplayRotation; mirrorHorizontal: boolean }, ): string { if (settings.rotation !== 0) parts.push(`R${settings.rotation}°`) if (settings.mirrorHorizontal) parts.push('Mirror') return parts.join(' · ') } export function scopeSummary(kind: ScopeKind, settings: ScopeSettings[ScopeKind]): string { switch (kind) { case 'spectrum': { const scopeSettings = settings as ScopeSettings['spectrum'] const summary = `${scopeSettings.heatmap ? 'Heat' : 'Fill'} · FFT ${scopeSettings.fftSize}` const parts = [summary] if (scopeSettings.showSideLine) { parts.push('Side') } if (scopeSettings.peakInfoMode === 'on') { parts.push('Peak') } else if (scopeSettings.peakInfoMode === 'following') { parts.push('Peak Follow') } return appendTransformSummary(parts, scopeSettings) } case 'oscilloscope': { const scopeSettings = settings as ScopeSettings['oscilloscope'] const mode = scopeSettings.pitchLock ? 'Pitch Lock' : 'Free Run' return appendTransformSummary( scopeSettings.underfillEnabled ? [mode, 'Fill'] : [mode], scopeSettings, ) } case 'vectorscope': { const scopeSettings = settings as ScopeSettings['vectorscope'] return scopeSettings.multiband ? `${vectorscopeModeLabel(scopeSettings.mode)} · RGB` : vectorscopeModeLabel(scopeSettings.mode) } case 'spectrogram': { const scopeSettings = settings as ScopeSettings['spectrogram'] return [ `${scopeSettings.rotation}°`, scopeSettings.scaleMode.toUpperCase(), scopeSettings.clarityMode, ...(scopeSettings.mirrorHorizontal ? ['Mirror'] : []), ].join(' · ') } case 'vumeter': { const scopeSettings = settings as ScopeSettings['vumeter'] const matchedPreset = findVUReferencePreset(scopeSettings.referenceDb) const refLabel = matchedPreset ? matchedPreset.label : `${scopeSettings.referenceDb.toFixed(1)} dBFS` const base = scopeSettings.mode === 'needle' ? `${scopeSettings.mode.toUpperCase()} · ${scopeSettings.needleChannels.toUpperCase()}` : `${scopeSettings.mode.toUpperCase()} · ${scopeSettings.orientation.toUpperCase()}` return `${base} · ${refLabel}` } case 'lufsmeter': return `${lufsReadoutLabel((settings as ScopeSettings['lufsmeter']).readout)} LUFS` case 'waveform': { const scopeSettings = settings as ScopeSettings['waveform'] const summary = [scopeSettings.mode === 'stereo' ? 'Stereo' : 'Mono'] if (scopeSettings.multiband) { summary.push('RGB') } return appendTransformSummary(summary, scopeSettings) } case 'nowPlaying': { const visible = nowPlayingVisibleLabels(settings as ScopeSettings['nowPlaying']) return visible.length > 0 ? visible.join(' · ') : 'Hidden' } } } function ToggleChip({ label, active, onClick, }: { label: string active: boolean onClick: () => void }): JSX.Element { return ( ) } function ToggleGroup({ label, children, }: { label: string children: ReactNode }): JSX.Element { return (
{label}
{children}
) } function SelectControl({ label, value, children, onChange, }: { label: string value: string | number children: ReactNode onChange: (value: string) => void }): JSX.Element { return ( ) } function RangeControl({ label, value, valueLabel, min, max, step, fullWidth = true, disabled = false, onChange, }: { label: string value: number valueLabel: string min: number max: number step: number fullWidth?: boolean disabled?: boolean onChange: (value: number) => void }): JSX.Element { const percent = max > min ? Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100)) : 0 return ( ) } function VUReferenceControl({ value, onChange, }: { value: number onChange: (value: number) => void }): JSX.Element { const matchedPreset = findVUReferencePreset(value) const [customExpanded, setCustomExpanded] = useState(matchedPreset === null) const showCustom = customExpanded || matchedPreset === null const selectValue = showCustom ? 'custom' : matchedPreset!.id return ( <> { if (next === 'custom') { setCustomExpanded(true) return } const preset = VU_REFERENCE_PRESETS.find((entry) => entry.id === next) if (preset) { setCustomExpanded(false) onChange(preset.dbfs) } }} > {VU_REFERENCE_PRESETS.map((preset) => ( ))} {showCustom && ( onChange(sanitizeVUReferenceDbfs(next))} /> )} ) } interface ScopeSettingsSectionProps { kind: ScopeKind settings: ScopeSettings[ScopeKind] onUpdate: (kind: K, partial: Partial) => void } export default function ScopeSettingsSection({ kind, settings, onUpdate, }: ScopeSettingsSectionProps): JSX.Element { return (
{SCOPE_LABELS[kind]}
{scopeSummary(kind, settings)}
{kind === 'spectrum' && (() => { const current = settings as ScopeSettings['spectrum'] return ( <> onUpdate('spectrum', { fftSize: Number(value) })} > {[1024, 2048, 4096, 8192, 16384].map((option) => ( ))} onUpdate('spectrum', { peakInfoMode: value as ScopeSettings['spectrum']['peakInfoMode'], })} > onUpdate('spectrum', { fillGradient: !current.fillGradient })} /> onUpdate('spectrum', { heatmap: !current.heatmap })} /> onUpdate('spectrum', { showGrid: !current.showGrid })} /> onUpdate('spectrum', { showSideLine: !current.showSideLine })} /> onUpdate('spectrum', { tiltDbPerOctave: value })} /> onUpdate('spectrum', { heatmapTiltDbPerOctave: value })} /> onUpdate('spectrum', { heatmapSmoothing: value })} /> onUpdate('spectrum', { smoothing: value })} /> ) })()} {kind === 'oscilloscope' && (() => { const current = settings as ScopeSettings['oscilloscope'] return ( <> onUpdate('oscilloscope', { pitchLock: !current.pitchLock })} /> onUpdate('oscilloscope', { underfillEnabled: !current.underfillEnabled })} /> onUpdate('oscilloscope', { showGrid: !current.showGrid })} /> onUpdate('oscilloscope', { lineWidth: value })} /> ) })()} {kind === 'vectorscope' && (() => { const current = settings as ScopeSettings['vectorscope'] return ( <> onUpdate('vectorscope', { mode: value as ScopeSettings['vectorscope']['mode'] })} > onUpdate('vectorscope', { multiband: !current.multiband })} /> onUpdate('vectorscope', { showGrid: !current.showGrid })} /> onUpdate('vectorscope', { persistence: value })} /> onUpdate('vectorscope', { lineWidth: value })} /> ) })()} {kind === 'spectrogram' && (() => { const current = settings as ScopeSettings['spectrogram'] return ( <> onUpdate('spectrogram', { fftSize: Number(value) })} > {[512, 1024, 2048, 4096, 8192].map((option) => ( ))} onUpdate('spectrogram', { scaleMode: value as ScopeSettings['spectrogram']['scaleMode'] })} > onUpdate('spectrogram', { clarityMode: value as ScopeSettings['spectrogram']['clarityMode'] })} > onUpdate('spectrogram', { colorScheme: value as ScopeSettings['spectrogram']['colorScheme'] })} > onUpdate('spectrogram', { scrollSpeed: value })} /> onUpdate('spectrogram', { contrast: value })} /> onUpdate('spectrogram', { tiltDbPerOctave: value })} /> ) })()} {kind === 'vumeter' && (() => { const current = settings as ScopeSettings['vumeter'] return ( <> onUpdate('vumeter', { mode: value as ScopeSettings['vumeter']['mode'] })} > {current.mode === 'bar' ? ( onUpdate('vumeter', { orientation: value as ScopeSettings['vumeter']['orientation'] })} > ) : ( onUpdate('vumeter', { needleChannels: value as ScopeSettings['vumeter']['needleChannels'] })} > )} onUpdate('vumeter', { referenceDb: value })} /> {current.referenceDb !== DEFAULT_VU_REFERENCE_DBFS && ( onUpdate('vumeter', { referenceDb: DEFAULT_VU_REFERENCE_DBFS })} /> )} ) })()} {kind === 'lufsmeter' && (() => { const current = settings as ScopeSettings['lufsmeter'] return ( onUpdate('lufsmeter', { readout: value as ScopeSettings['lufsmeter']['readout'] })} > ) })()} {kind === 'waveform' && (() => { const current = settings as ScopeSettings['waveform'] return ( <> onUpdate('waveform', { mode: 'mono' })} /> onUpdate('waveform', { mode: 'stereo' })} /> onUpdate('waveform', { multiband: !current.multiband })} /> onUpdate('waveform', { scrollSpeed: value })} /> ) })()} {kind === 'nowPlaying' && (() => { const current = settings as ScopeSettings['nowPlaying'] return ( onUpdate('nowPlaying', { showCoverArt: !current.showCoverArt })} /> onUpdate('nowPlaying', { showTitle: !current.showTitle })} /> onUpdate('nowPlaying', { showArtist: !current.showArtist })} /> onUpdate('nowPlaying', { showProgress: !current.showProgress })} /> onUpdate('nowPlaying', { showTime: !current.showTime })} /> onUpdate('nowPlaying', { showControls: !current.showControls })} /> ) })()} {isTransformableScopeKind(kind) && (() => { const current = settings as ScopeSettings[typeof kind] return ( <> onUpdate(kind, { rotation: Number(value) as ScopeDisplayRotation })} > {SCOPE_DISPLAY_ROTATIONS.map((rotation) => ( ))} onUpdate(kind, { mirrorHorizontal: !current.mirrorHorizontal })} /> ) })()}
) }