import { useEffect, useRef, type JSX } from 'react' import type { ScopeKind } from '../../types/scope' import type { ScopeSettings } from '../../types/settings' import type { PrismResolvedTheme, ResolvedAstraTheme, ResolvedLUFSMeterTheme, ResolvedOscilloscopeTheme, ResolvedSpectrogramTheme, ResolvedSpectrumTheme, ResolvedVectorscopeTheme, ResolvedVUMeterTheme, ResolvedWaveformTheme, } from '../../types/theme' import { useSettingsStore } from '../stores/settingsStore' import { useThemeStore } from '../stores/themeStore' import AstraScopeModule from './AstraScopeModule' import { SpectrumAnalyzer, type SpectrumAnalyzerDataSource } from '../visualizers/SpectrumAnalyzer' import { Oscilloscope, type OscilloscopeDataSource } from '../visualizers/Oscilloscope' import { Vectorscope, type VectorscopeDataSource } from '../visualizers/Vectorscope' import { Spectrogram, type SpectrogramDataSource } from '../visualizers/Spectrogram' import { VUMeter, type VUMeterDataSource } from '../visualizers/VUMeter' import { LUFSMeter, type LUFSMeterDataSource } from '../visualizers/LUFSMeter' import { Waveform, type WaveformDataSource } from '../visualizers/Waveform' import type { FrameScheduler } from '../visualizers/frameScheduler' type ScopeModuleTheme = | ResolvedSpectrumTheme | ResolvedOscilloscopeTheme | ResolvedVectorscopeTheme | ResolvedSpectrogramTheme | ResolvedVUMeterTheme | ResolvedLUFSMeterTheme | ResolvedWaveformTheme | ResolvedAstraTheme interface ScopeModuleProps { scopeKind: ScopeKind theme?: ScopeModuleTheme settings?: ScopeSettings[ScopeKind] frameScheduler?: FrameScheduler dataSource?: | SpectrumAnalyzerDataSource | OscilloscopeDataSource | VectorscopeDataSource | SpectrogramDataSource | VUMeterDataSource | LUFSMeterDataSource | WaveformDataSource } interface Visualizer { start(): void stop(): void dispose(): void resize(): void setOptions(options: Record): void } function getScopeTheme(theme: PrismResolvedTheme, kind: ScopeKind): ScopeModuleTheme { return theme[kind] as ScopeModuleTheme } export function scopeSettingsToOptions( kind: ScopeKind, settings: ScopeSettings[ScopeKind], theme: ScopeModuleTheme, ): Record { switch (kind) { case 'spectrum': { const s = settings as ScopeSettings['spectrum'] const t = theme as ResolvedSpectrumTheme return { lineColor: t.line, secondaryLineColor: t.sideLine, gradientColors: t.fillGradient, heatColors: t.heatColors, heatBaseColor: t.heatBase, backgroundColor: t.background, gridColor: t.guides, labelColor: t.labels, fftSize: s.fftSize, tiltDbPerOctave: s.tiltDbPerOctave, heatmapFill: s.heatmap, heatmapTiltDbPerOctave: s.heatmapTiltDbPerOctave, heatmapSmoothing: s.heatmapSmoothing, showGrid: s.showGrid, fillGradient: s.fillGradient, smoothing: s.smoothing, showSideLine: s.showSideLine, } } case 'oscilloscope': { const s = settings as ScopeSettings['oscilloscope'] const t = theme as ResolvedOscilloscopeTheme return { lineColor: t.line, backgroundColor: t.background, gridMajorColor: t.guides, gridMinorColor: t.guidesSecondary, underfillColor: t.fill, pitchLock: s.pitchLock, underfillEnabled: s.underfillEnabled, showGrid: s.showGrid, lineWidth: s.lineWidth, } } case 'vectorscope': { const s = settings as ScopeSettings['vectorscope'] const t = theme as ResolvedVectorscopeTheme return { lineColor: t.trace, backgroundColor: t.background, gridMajorColor: t.guides, gridMinorColor: t.guidesSecondary, labelColor: t.labels, bandColors: { low: t.bandLow, mid: t.bandMid, high: t.bandHigh, }, mode: s.mode, multiband: s.multiband, showGrid: s.showGrid, persistence: s.persistence, lineWidth: s.lineWidth, } } case 'spectrogram': { const s = settings as ScopeSettings['spectrogram'] const t = theme as ResolvedSpectrogramTheme return { lineColor: t.mono, heatColors: t.heatColors, backgroundColor: t.background, fftSize: s.fftSize, scrollSpeed: s.scrollSpeed, clarityMode: s.clarityMode, scaleMode: s.scaleMode, colorScheme: s.colorScheme, } } case 'vumeter': { const s = settings as ScopeSettings['vumeter'] const t = theme as ResolvedVUMeterTheme return { backgroundColor: t.background, lineColor: t.level, trackColor: t.track, peakColor: t.peak, clipColor: t.clip, scaleColor: t.scale, labelColor: t.labels, mode: s.mode, orientation: s.orientation, } } case 'lufsmeter': { const s = settings as ScopeSettings['lufsmeter'] const t = theme as ResolvedLUFSMeterTheme return { backgroundColor: t.background, lineColor: t.level, trackColor: t.track, targetColor: t.target, scaleColor: t.scale, labelColor: t.labels, mode: s.mode, } } case 'waveform': { const s = settings as ScopeSettings['waveform'] const t = theme as ResolvedWaveformTheme return { backgroundColor: t.background, lineColor: t.line, gridMajorColor: t.guides, gridMinorColor: t.guidesSecondary, bandColors: { low: t.bandLow, mid: t.bandMid, high: t.bandHigh, }, mode: s.mode, scrollSpeed: s.scrollSpeed, gainDb: s.gainDb, multiband: s.multiband, } } case 'astra': return {} default: return {} } } function createVisualizer( scopeKind: ScopeKind, canvas: HTMLCanvasElement, mySettings: ScopeSettings[ScopeKind], theme: ScopeModuleTheme, frameScheduler?: FrameScheduler, dataSource?: ScopeModuleProps['dataSource'], ): Visualizer | null { const opts = { ...scopeSettingsToOptions(scopeKind, mySettings, theme), frameScheduler } switch (scopeKind) { case 'spectrum': return new SpectrumAnalyzer(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as SpectrumAnalyzerDataSource } : {}), }) case 'oscilloscope': return new Oscilloscope(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as OscilloscopeDataSource } : {}), }) case 'vectorscope': return new Vectorscope(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as VectorscopeDataSource } : {}), }) case 'spectrogram': return new Spectrogram(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as SpectrogramDataSource } : {}), }) case 'vumeter': return new VUMeter(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as VUMeterDataSource } : {}), }) case 'lufsmeter': return new LUFSMeter(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as LUFSMeterDataSource } : {}), }) case 'waveform': return new Waveform(canvas, { ...opts, ...(dataSource ? { dataSource: dataSource as WaveformDataSource } : {}), }) case 'astra': return null default: return null } } export default function ScopeModule({ scopeKind, theme, settings, frameScheduler, dataSource, }: ScopeModuleProps): JSX.Element { const containerRef = useRef(null) const canvasRef = useRef(null) const visualizerRef = useRef(null) const initializedRef = useRef(false) const storeSettings = useSettingsStore((s) => s.scopeSettings[scopeKind]) const activeTheme = useThemeStore((s) => s.activeTheme) const mySettings = settings ?? storeSettings const myTheme = theme ?? getScopeTheme(activeTheme, scopeKind) if (scopeKind === 'astra') { return ( ) } useEffect(() => { const canvas = canvasRef.current if (!canvas) return initializedRef.current = false const viz = createVisualizer(scopeKind, canvas, mySettings, myTheme, frameScheduler, dataSource) if (!viz) return visualizerRef.current = viz viz.start() requestAnimationFrame(() => { initializedRef.current = true }) return () => { viz.dispose() visualizerRef.current = null initializedRef.current = false } }, [dataSource, frameScheduler, myTheme, mySettings, scopeKind]) useEffect(() => { if (!visualizerRef.current || !initializedRef.current) return const opts = { ...scopeSettingsToOptions(scopeKind, mySettings, myTheme), frameScheduler, ...(dataSource ? { dataSource } : {}), } visualizerRef.current.setOptions(opts) }, [dataSource, frameScheduler, mySettings, myTheme, scopeKind]) useEffect(() => { const container = containerRef.current const canvas = canvasRef.current if (!container || !canvas) return const resizeCanvas = (): void => { const rect = container.getBoundingClientRect() const width = Math.max(1, Math.floor(rect.width)) const height = Math.max(1, Math.floor(rect.height)) const dpr = window.devicePixelRatio || 1 canvas.style.width = `${width}px` canvas.style.height = `${height}px` canvas.width = Math.max(1, Math.floor(width * dpr)) canvas.height = Math.max(1, Math.floor(height * dpr)) visualizerRef.current?.resize() } const observer = new ResizeObserver(resizeCanvas) observer.observe(container) resizeCanvas() return () => observer.disconnect() }, []) return (
) }