mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
356 lines
10 KiB
TypeScript
356 lines
10 KiB
TypeScript
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<string, unknown>): void
|
|
}
|
|
|
|
function getScopeTheme(theme: PrismResolvedTheme, kind: ScopeKind): ScopeModuleTheme {
|
|
return theme[kind] as ScopeModuleTheme
|
|
}
|
|
|
|
export function scopeSettingsToOptions(
|
|
kind: ScopeKind,
|
|
settings: ScopeSettings[ScopeKind],
|
|
theme: ScopeModuleTheme,
|
|
): Record<string, unknown> {
|
|
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<HTMLDivElement>(null)
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
const visualizerRef = useRef<Visualizer | null>(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 (
|
|
<AstraScopeModule
|
|
theme={myTheme as ResolvedAstraTheme}
|
|
settings={mySettings as ScopeSettings['astra']}
|
|
/>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div
|
|
ref={containerRef}
|
|
style={{
|
|
minWidth: 0,
|
|
height: '100%',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<canvas
|
|
ref={canvasRef}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|