The audio visualizer integrates with LedFx-Frontend-v2 via react-dynamic-module, enabling runtime loading and direct Zustand store sharing.
┌─────────────────────────────────────────────────────────────┐
│ Build: Audio-Visualizer │
│ pnpm build → dist/yz-audio-visualiser.js (733 kB gzipped) │
└─────────────────────────────────────────────────────────────┘
↓ (copy to)
┌─────────────────────────────────────────────────────────────┐
│ Frontend: public/modules/yz-audio-visualiser.js │
└─────────────────────────────────────────────────────────────┘
↓ (loaded via)
┌─────────────────────────────────────────────────────────────┐
│ react-dynamic-module (runtime) │
│ useDynamicModule({ url, scope, module }) │
└─────────────────────────────────────────────────────────────┘
↓ (exports)
┌─────────────────────────────────────────────────────────────┐
│ { AudioVisualiser, useStore, types } │
│ window.YzAudioVisualiser = { useStore, ... } │
└─────────────────────────────────────────────────────────────┘
// frontend/src/components/AudioVisualiser/AudioVisualiser.tsx
import { useDynamicModule } from '@yz-dev/react-dynamic-module'
const AudioVisualiserWrapper = () => {
const module = useDynamicModule({
url: '/modules/yz-audio-visualiser.js',
scope: 'YzAudioVisualiser',
module: '.'
})
if (module.loading) return <Loading />
if (module.error) return <Error error={module.error} />
const { AudioVisualiser, useStore } = module.exports || {}
return <AudioVisualiser />
}// Extract useStore from module
const { useStore } = module.exports || {}
// Alias as useVstore (convention)
const useVstore = useStore
// READ STATE (reactive!)
const visualType = useVstore?.(state => state.visualType)
const isPlaying = useVstore?.(state => state.isPlaying)
const showOverlays = useVstore?.(state => state.showOverlays)
const ppConfig = useVstore?.(state => state.ppConfig)
// CALL ACTIONS (type-safe!)
const setVisualType = useVstore?.(state => state.setVisualType)
const togglePlay = useVstore?.(state => state.togglePlay)
const toggleOverlays = useVstore?.(state => state.toggleOverlays)
// USE IN UI
<Button onClick={() => setVisualType?.('butterchurn')}>
Butterchurn
</Button>
<IconButton onClick={() => togglePlay?.()}>
{isPlaying ? <Pause /> : <Play />}
</IconButton>// Single setState call = single re-render
useVstore?.setState({
visualType: 'fluid',
isPlaying: true,
showOverlays: false
})// Get current state (non-reactive)
const currentState = useVstore?.getState()
console.log('Current visual:', currentState?.visualType)
// Subscribe to changes
useEffect(() => {
const unsubscribe = useVstore?.subscribe(
state => state.isPlaying,
(playing) => {
console.log('Playing changed:', playing)
}
)
return unsubscribe
}, [useVstore])Some methods still use window.visualiserApi for ref-dependent operations:
// Still available for specific operations
window.visualiserApi?.loadPreset(5)
window.visualiserApi?.toggleFullscreen()
window.visualiserApi?.getVisualizerIds()
// But most state/actions now via useVstore!// State
visualType: VisualisationType
audioSource: 'backend' | 'mic'
autoChange: boolean
isPlaying: boolean
// Actions
setVisualType(type: VisualisationType)
setAudioSource(source: 'backend' | 'mic')
setAutoChange(enabled: boolean)
setIsPlaying(playing: boolean)
togglePlay()// State
showOverlays: boolean
fullScreen: boolean
showFxPanel: boolean
saveError: string | null
// Actions
setShowOverlays(show: boolean)
toggleOverlays()
setFullScreen(fullScreen: boolean)
setShowFxPanel(show: boolean)
setSaveError(error: string | null)// State
fxEnabled: boolean
ppConfig: PostProcessingConfig
// Actions
toggleFx()
updatePpConfig(updates: Partial<PostProcessingConfig>)// State
butterchurnConfig: ButterchurnConfig
astrofoxConfig: AstrofoxConfig
visualizerConfigs: Record<string, any>
// Actions
updateButterchurnConfig(updates: Partial<ButterchurnConfig>)
updateAstrofoxConfig(updates: Partial<AstrofoxConfig>)
setVisualizerConfig(id: string, config: any)// State
showCode: boolean
shaderCode: string
activeCustomShader: string | undefined
// Actions
setShaderCode(code: string)
setActiveCustomShader(id: string | undefined)
toggleShaderEditor()// ❌ Bad - subscribes to all state changes
const store = useVstore?.()
// ✅ Good - only subscribes to visualType
const visualType = useVstore?.(state => state.visualType)// ❌ Bad - three re-renders
setVisualType('fluid')
setIsPlaying(true)
toggleOverlays()
// ✅ Good - one re-render
useVstore?.setState({
visualType: 'fluid',
isPlaying: true,
showOverlays: false
})// Always use ?. since module might not be loaded
useVstore?.(state => state.visualType)
setVisualType?.('butterchurn')// Import types from visualizer
import type { VisualisationType } from '@/types/visualiser'
// Or infer from store
type StoreState = ReturnType<typeof useVstore.getState>New Feature: Automatic configuration via URL parameters
The visualizer supports URL query parameters for automatic configuration, enabling OBS integration, preset sharing, and deep linking.
- ✅ Auto-generated support - All visualizer configs automatically supported via schemas
- ✅ Type conversion - Automatic string → boolean/number/integer conversion
- ✅ Validation - Min/max constraints and enum checking from schemas
- ✅ HashRouter compatible - Works in both standalone and embedded modes
- ✅ Race condition free - Synchronous parsing during store initialization
# Standalone Mode (port 3001)
http://localhost:3001/?visual=butterchurn¤tPresetIndex=42
# Integrated Mode (HashRouter)
http://localhost:3000/#/visualiser?visual=butterchurn¤tPresetIndex=42
# Display Mode (OBS-friendly - no UI chrome)
http://localhost:3000/#/visualiser?display=true&visual=butterchurn¤tPresetIndex=42
?visual=<name> # Sets active visualizer (required)
# Examples: butterchurn, fluid, frequencyrings
?currentPresetIndex=<0-394> # Preset by index
?currentPresetName=<name> # Preset by name
?cycleInterval=<seconds> # Auto-cycle interval (default: 25)
?blendTime=<seconds> # Blend duration (default: 2.7)
?shufflePresets=<bool> # Random preset order
All schema properties automatically supported:
# Fluid
?fluidDensity=0.98&particleCount=8000
# Frequency Rings
?ringCount=10&rotationSpeed=2
# Aurora Borealis
?waveSpeed=1.5&particleIntensity=0.8
// Boolean: true/false, 1/0, yes/no
?shufflePresets=true → true
// Integer: with min/max validation
?currentPresetIndex=42 → 42
// Number: float with constraints
?blendTime=2.5 → 2.5
// Array: JSON or comma-separated
?colors=red,blue → ["red", "blue"]
// Object: JSON only
?position={"x":1} → {x: 1}URL: http://localhost:3000/#/visualiser
?display=true
&visual=butterchurn
¤tPresetIndex=42
&cycleInterval=30
&shufflePresets=true
Width: 1920
Height: 1080
FPS: 60
Query params are parsed synchronously during store initialization (src/store/queryParamInit.ts), ensuring the configuration is applied before the first component render, eliminating race conditions.
// Standalone: window.location.search
// HashRouter: window.location.hash (after '?')
// Automatic schema-based type conversion
// Butterchurn: initialPresetIndex → loaded on mount
// Other visualizers: updateVisualizerConfig(type, params)// Open Redux DevTools to see:
// - Current state tree
// - Action history
// - State diffs
// - Time-travel debugging// Get current state snapshot
console.log('Store state:', useVstore?.getState())
// Subscribe to all changes
useVstore?.subscribe(
state => state,
(state) => console.log('State changed:', state)
)cd _audio-visualiser
pnpm build
# Output: dist/yz-audio-visualiser.js (733 kB gzipped)# Manual
cp dist/yz-audio-visualiser.js ../frontend/public/modules/
# Or automated (if script exists)
pnpm deploy:frontendcd ../frontend
pnpm dev
# Module will be loaded at runtime- 🔴 Polling required (100ms intervals)
- 🔴 Desync possible (localStorage races)
- 🔴 No type safety
- 🔴 Limited to predefined methods
- ✅ No polling (reactive Zustand hooks)
- ✅ No desync (shared store)
- ✅ Full type safety
- ✅ Full state + actions access
- ✅ Redux DevTools support
- ✅ Automatic re-renders
- ✅ Performance optimized
- COMMUNICATION.md - Communication evolution details
- ZUSTAND_STORE.md - Store structure
- ARCHITECTURE.md - Overall architecture