import { useState, useRef, useEffect } from 'react' import { useStore } from '@nanostores/react' import { Square, Archive, Dices, Sparkles } from 'lucide-react' import { PlaybackManager } from './services/PlaybackManager' import { DownloadService } from './services/DownloadService' import { generateTileGrid, generateRandomFormula } from './utils/bytebeatFormulas' import { BytebeatTile } from './components/BytebeatTile' import { EffectsBar } from './components/EffectsBar' import { EngineControls } from './components/EngineControls' import { FormulaEditor } from './components/FormulaEditor' import { LFOPanel } from './components/LFOPanel' import { AudioContextWarning } from './components/AudioContextWarning' import { HelpModal } from './components/HelpModal' import { getSampleRateFromIndex } from './config/effects' import { engineSettings, effectSettings, lfoSettings } from './stores/settings' import { exitMappingMode } from './stores/mappingMode' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useTileParams } from './hooks/useTileParams' import type { TileState } from './types/tiles' import { createTileStateFromCurrent, loadTileParams, randomizeTileParams } from './utils/tileState' import { DEFAULT_VARIABLES, PLAYBACK_ID, TILE_GRID, DEFAULT_DOWNLOAD_OPTIONS, LOOP_DURATION } from './constants/defaults' import { getTileId, getTileFromGrid, type FocusedTile } from './utils/tileHelpers' function App() { const engineValues = useStore(engineSettings) const effectValues = useStore(effectSettings) const [tiles, setTiles] = useState(() => generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity) ) const [playing, setPlaying] = useState(null) const [queued, setQueued] = useState(null) const [playbackPosition, setPlaybackPosition] = useState(0) const [downloading, setDownloading] = useState(false) const [focusedTile, setFocusedTile] = useState({ row: 0, col: 0 }) const [customTile, setCustomTile] = useState(() => createTileStateFromCurrent('t*(8&t>>9)')) const [showWarning, setShowWarning] = useState(true) const [showHelp, setShowHelp] = useState(false) const playbackManagerRef = useRef(null) const downloadServiceRef = useRef(new DownloadService()) const switchTimerRef = useRef(null) const { saveCurrentTileParams } = useTileParams({ tiles, setTiles, customTile, setCustomTile, focusedTile }) useEffect(() => { if (playbackManagerRef.current) { playbackManagerRef.current.setPlaybackPositionCallback(setPlaybackPosition) } }, []) useEffect(() => { effectSettings.setKey('masterVolume', engineValues.masterVolume) }, [engineValues.masterVolume]) const clearSwitchTimer = () => { if (switchTimerRef.current !== null) { clearTimeout(switchTimerRef.current) switchTimerRef.current = null } } const startSwitchTimer = (queuedId: string) => { clearSwitchTimer() switchTimerRef.current = window.setTimeout(() => { const [rowStr, colStr] = queuedId.split('-') const row = parseInt(rowStr, 10) const col = parseInt(colStr, 10) const tile = getTileFromGrid(tiles, row, col) if (tile) { playFormula(tile.formula, queuedId) } }, engineValues.loopCount * 1000) } useEffect(() => { return () => clearSwitchTimer() }, []) const handleRandom = () => { clearSwitchTimer() setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity)) setQueued(null) } const handleRandomizeAllParams = () => { clearSwitchTimer() let newRandomized: TileState | null = null if (playing === PLAYBACK_ID.CUSTOM) { setCustomTile(prev => { const randomized = randomizeTileParams(prev) newRandomized = randomized return randomized }) } else { setTiles(prevTiles => { const newTiles = prevTiles.map((row, rowIdx) => row.map((tile, colIdx) => { const randomized = randomizeTileParams(tile) if (playing && focusedTile !== 'custom') { const tileId = getTileId(focusedTile.row, focusedTile.col) if (playing === tileId && rowIdx === focusedTile.row && colIdx === focusedTile.col) { newRandomized = randomized } } return randomized }) ) return newTiles }) setCustomTile(prev => randomizeTileParams(prev)) } if (newRandomized && playbackManagerRef.current) { const params = newRandomized as TileState loadTileParams(params) playbackManagerRef.current.setEffects(params.effectParams as any) playbackManagerRef.current.setVariables( params.engineParams.a ?? DEFAULT_VARIABLES.a, params.engineParams.b ?? DEFAULT_VARIABLES.b, params.engineParams.c ?? DEFAULT_VARIABLES.c, params.engineParams.d ?? DEFAULT_VARIABLES.d ) playbackManagerRef.current.setPitch(params.engineParams.pitch ?? 1.0) if (params.lfoConfigs) { playbackManagerRef.current.setLFOConfig(0, params.lfoConfigs.lfo1) playbackManagerRef.current.setLFOConfig(1, params.lfoConfigs.lfo2) playbackManagerRef.current.setLFOConfig(2, params.lfoConfigs.lfo3) playbackManagerRef.current.setLFOConfig(3, params.lfoConfigs.lfo4) } } setQueued(null) } const playFormula = async (formula: string, id: string) => { const sampleRate = getSampleRateFromIndex(engineValues.sampleRate) const duration = LOOP_DURATION if (!playbackManagerRef.current) { playbackManagerRef.current = new PlaybackManager({ sampleRate, duration }) } else { await playbackManagerRef.current.updateOptions({ sampleRate, duration }) } playbackManagerRef.current.stop() playbackManagerRef.current.setEffects(effectValues) playbackManagerRef.current.setVariables( engineValues.a ?? DEFAULT_VARIABLES.a, engineValues.b ?? DEFAULT_VARIABLES.b, engineValues.c ?? DEFAULT_VARIABLES.c, engineValues.d ?? DEFAULT_VARIABLES.d ) playbackManagerRef.current.setPitch(engineValues.pitch ?? 1.0) await playbackManagerRef.current.play(formula) setPlaying(id) setQueued(null) return true } const handleTileClick = (_formula: string, row: number, col: number, isDoubleClick: boolean = false) => { const id = getTileId(row, col) const tile = getTileFromGrid(tiles, row, col) if (!tile) return if (focusedTile === 'custom' || (focusedTile.row !== row || focusedTile.col !== col)) { saveCurrentTileParams() } if (tile) { loadTileParams(tile) } setFocusedTile({ row, col }) if (playing === id) { handleStop() return } if (isDoubleClick || playing === null) { clearSwitchTimer() playFormula(tile.formula, id) } else { setQueued(id) startSwitchTimer(id) } } const handleTileDoubleClick = (formula: string, row: number, col: number) => { handleTileClick(formula, row, col, true) } const handleEngineChange = async (parameterId: string, value: number) => { engineSettings.setKey(parameterId as keyof typeof engineValues, value) saveCurrentTileParams() if (parameterId === 'masterVolume' && playbackManagerRef.current) { playbackManagerRef.current.setEffects({ ...effectValues, masterVolume: value }) } if (parameterId === 'pitch' && playbackManagerRef.current && playing) { playbackManagerRef.current.setPitch(value) } if (['a', 'b', 'c', 'd'].includes(parameterId) && playbackManagerRef.current && playing) { const updatedValues = { ...engineValues, [parameterId]: value } playbackManagerRef.current.setVariables( updatedValues.a ?? DEFAULT_VARIABLES.a, updatedValues.b ?? DEFAULT_VARIABLES.b, updatedValues.c ?? DEFAULT_VARIABLES.c, updatedValues.d ?? DEFAULT_VARIABLES.d ) } } const handleEffectChange = (parameterId: string, value: number | boolean | string) => { effectSettings.setKey(parameterId as any, value as any) saveCurrentTileParams() if (playbackManagerRef.current) { playbackManagerRef.current.setEffects(effectValues) } } const handleLFOChange = (lfoIndex: number, config: any) => { if (playbackManagerRef.current) { playbackManagerRef.current.setLFOConfig(lfoIndex, config) } } const handleParameterMapClick = (paramId: string, lfoIndex: number) => { const lfoKey = `lfo${lfoIndex + 1}` as 'lfo1' | 'lfo2' | 'lfo3' | 'lfo4' const currentLFO = lfoSettings.get()[lfoKey] const existingMappingIndex = currentLFO.mappings.findIndex(m => m.targetParam === paramId) let updatedMappings if (existingMappingIndex >= 0) { updatedMappings = currentLFO.mappings.filter((_, i) => i !== existingMappingIndex) } else { updatedMappings = [...currentLFO.mappings, { targetParam: paramId, depth: 50 }] } const updatedLFO = { ...currentLFO, mappings: updatedMappings } lfoSettings.setKey(lfoKey, updatedLFO) if (playbackManagerRef.current) { playbackManagerRef.current.setLFOConfig(lfoIndex, updatedLFO) } saveCurrentTileParams() if (updatedMappings.length === 0 || existingMappingIndex >= 0) { exitMappingMode() } } const handleUpdateMappingDepth = (lfoIndex: number, paramId: string, depth: number) => { const lfoKey = `lfo${lfoIndex + 1}` as 'lfo1' | 'lfo2' | 'lfo3' | 'lfo4' const currentLFO = lfoSettings.get()[lfoKey] const updatedMappings = currentLFO.mappings.map(m => m.targetParam === paramId ? { ...m, depth } : m ) const updatedLFO = { ...currentLFO, mappings: updatedMappings } lfoSettings.setKey(lfoKey, updatedLFO) if (playbackManagerRef.current) { playbackManagerRef.current.setLFOConfig(lfoIndex, updatedLFO) } saveCurrentTileParams() } const handleRemoveMapping = (lfoIndex: number, paramId: string) => { const lfoKey = `lfo${lfoIndex + 1}` as 'lfo1' | 'lfo2' | 'lfo3' | 'lfo4' const currentLFO = lfoSettings.get()[lfoKey] const updatedMappings = currentLFO.mappings.filter(m => m.targetParam !== paramId) const updatedLFO = { ...currentLFO, mappings: updatedMappings } lfoSettings.setKey(lfoKey, updatedLFO) if (playbackManagerRef.current) { playbackManagerRef.current.setLFOConfig(lfoIndex, updatedLFO) } saveCurrentTileParams() } const getMappedLFOs = (paramId: string): number[] => { const lfos = lfoSettings.get() const mapped: number[] = [] Object.entries(lfos).forEach(([, lfo], index) => { if (lfo.mappings.some((m: { targetParam: string }) => m.targetParam === paramId)) { mapped.push(index) } }) return mapped } const handleDownloadAll = async () => { setDownloading(true) const formulas = tiles.map(row => row.map(tile => tile.formula)) await downloadServiceRef.current.downloadAll(formulas, { duration: DEFAULT_DOWNLOAD_OPTIONS.DURATION, bitDepth: DEFAULT_DOWNLOAD_OPTIONS.BIT_DEPTH }) setDownloading(false) } const handleDownloadFormula = (formula: string, filename: string) => { downloadServiceRef.current.downloadFormula(formula, filename, { duration: DEFAULT_DOWNLOAD_OPTIONS.DURATION, bitDepth: DEFAULT_DOWNLOAD_OPTIONS.BIT_DEPTH }) } const handleRegenerate = (row: number, col: number) => { const newFormula = generateRandomFormula(engineValues.complexity) const newTile = createTileStateFromCurrent(newFormula) setTiles(prevTiles => { const newTiles = [...prevTiles] newTiles[row] = [...newTiles[row]] newTiles[row][col] = newTile return newTiles }) } const handleStop = () => { clearSwitchTimer() playbackManagerRef.current?.stop() setPlaying(null) setQueued(null) setPlaybackPosition(0) } const handleCustomEvaluate = (formula: string) => { if (focusedTile !== 'custom') { saveCurrentTileParams() loadTileParams(customTile) } setFocusedTile('custom') setCustomTile({ ...customTile, formula }) playFormula(formula, PLAYBACK_ID.CUSTOM) } const handleCustomStop = () => { if (playing === PLAYBACK_ID.CUSTOM) { handleStop() } } const handleCustomRandom = () => { return generateRandomFormula(engineValues.complexity) } const moveFocus = (direction: 'up' | 'down' | 'left' | 'right', step: number = 1) => { saveCurrentTileParams() setFocusedTile(prev => { if (prev === 'custom') return prev let { row, col } = prev const maxRow = tiles.length - 1 const maxCol = (tiles[row]?.length || 1) - 1 switch (direction) { case 'up': row = Math.max(0, row - step) break case 'down': row = Math.min(maxRow, row + step) break case 'left': col = Math.max(0, col - step) break case 'right': col = Math.min(maxCol, col + step) break } const newTile = tiles[row]?.[col] if (newTile) { loadTileParams(newTile) return { row, col } } return prev }) } const handleKeyboardSpace = () => { if (playing) { handleStop() } else if (focusedTile !== 'custom') { const tile = tiles[focusedTile.row]?.[focusedTile.col] if (tile) { handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true) } } } const handleKeyboardEnter = () => { if (focusedTile !== 'custom') { const tile = tiles[focusedTile.row]?.[focusedTile.col] if (tile) { handleTileClick(tile.formula, focusedTile.row, focusedTile.col, false) } } } const handleKeyboardDoubleEnter = () => { if (focusedTile !== 'custom') { const tile = tiles[focusedTile.row]?.[focusedTile.col] if (tile) { handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true) } } } const handleKeyboardR = () => { if (focusedTile !== 'custom') { handleRegenerate(focusedTile.row, focusedTile.col) } } const handleKeyboardShiftR = () => { handleRandom() } const handleEscape = () => { exitMappingMode() } const handleKeyboardC = () => { if (focusedTile === 'custom') { setCustomTile(prev => { const randomized = randomizeTileParams(prev) loadTileParams(randomized) if (playing === PLAYBACK_ID.CUSTOM && playbackManagerRef.current) { playbackManagerRef.current.setEffects(randomized.effectParams as any) playbackManagerRef.current.setVariables( randomized.engineParams.a ?? DEFAULT_VARIABLES.a, randomized.engineParams.b ?? DEFAULT_VARIABLES.b, randomized.engineParams.c ?? DEFAULT_VARIABLES.c, randomized.engineParams.d ?? DEFAULT_VARIABLES.d ) playbackManagerRef.current.setPitch(randomized.engineParams.pitch ?? 1.0) if (randomized.lfoConfigs) { playbackManagerRef.current.setLFOConfig(0, randomized.lfoConfigs.lfo1) playbackManagerRef.current.setLFOConfig(1, randomized.lfoConfigs.lfo2) playbackManagerRef.current.setLFOConfig(2, randomized.lfoConfigs.lfo3) playbackManagerRef.current.setLFOConfig(3, randomized.lfoConfigs.lfo4) } } return randomized }) } else { const tileId = getTileId(focusedTile.row, focusedTile.col) setTiles(prevTiles => { const newTiles = [...prevTiles] newTiles[focusedTile.row] = [...newTiles[focusedTile.row]] const randomized = randomizeTileParams(newTiles[focusedTile.row][focusedTile.col]) newTiles[focusedTile.row][focusedTile.col] = randomized loadTileParams(randomized) if (playing === tileId && playbackManagerRef.current) { playbackManagerRef.current.setEffects(randomized.effectParams as any) playbackManagerRef.current.setVariables( randomized.engineParams.a ?? DEFAULT_VARIABLES.a, randomized.engineParams.b ?? DEFAULT_VARIABLES.b, randomized.engineParams.c ?? DEFAULT_VARIABLES.c, randomized.engineParams.d ?? DEFAULT_VARIABLES.d ) playbackManagerRef.current.setPitch(randomized.engineParams.pitch ?? 1.0) if (randomized.lfoConfigs) { playbackManagerRef.current.setLFOConfig(0, randomized.lfoConfigs.lfo1) playbackManagerRef.current.setLFOConfig(1, randomized.lfoConfigs.lfo2) playbackManagerRef.current.setLFOConfig(2, randomized.lfoConfigs.lfo3) playbackManagerRef.current.setLFOConfig(3, randomized.lfoConfigs.lfo4) } } return newTiles }) } } const handleKeyboardShiftC = () => { handleRandomizeAllParams() } const handleDismissWarning = () => { setShowWarning(false) } useKeyboardShortcuts({ onSpace: handleKeyboardSpace, onArrowUp: (shift) => moveFocus('up', shift ? 10 : 1), onArrowDown: (shift) => moveFocus('down', shift ? 10 : 1), onArrowLeft: (shift) => moveFocus('left', shift ? 10 : 1), onArrowRight: (shift) => moveFocus('right', shift ? 10 : 1), onEnter: handleKeyboardEnter, onDoubleEnter: handleKeyboardDoubleEnter, onR: handleKeyboardR, onShiftR: handleKeyboardShiftR, onC: handleKeyboardC, onShiftC: handleKeyboardShiftC, onEscape: handleEscape }) useEffect(() => { if (focusedTile !== 'custom') { const element = document.querySelector(`[data-tile-id="${focusedTile.row}-${focusedTile.col}"]`) if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) } } }, [focusedTile]) return (
{showWarning && } {showHelp && setShowHelp(false)} />}

setShowHelp(true)} className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity" > BRUITISTE

{tiles.map((row, i) => row.map((tile, j) => { const id = getTileId(i, j) return ( ) }) )}
) } export default App