import { useState, useRef } from 'react' import { useStore } from '@nanostores/react' import { Square, Archive, Dices, Sparkles, Blend } from 'lucide-react' import { DownloadService } from './services/DownloadService' import { generateRandomFormula } from './utils/bytebeatFormulas' import { BytebeatTile } from './components/tile/BytebeatTile' import { EffectsBar } from './components/controls/EffectsBar' import { EngineControls } from './components/controls/EngineControls' import { FormulaEditor } from './components/tile/FormulaEditor' import { LFOPanel } from './components/controls/LFOPanel' import { AudioContextWarning } from './components/modals/AudioContextWarning' import { HelpModal } from './components/modals/HelpModal' import { engineSettings, effectSettings } from './stores/settings' import { exitMappingMode } from './stores/mappingMode' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useTileGrid } from './hooks/useTileGrid' import { usePlaybackControl } from './hooks/usePlaybackControl' import { useFocusNavigation } from './hooks/useFocusNavigation' import { useParameterSync } from './hooks/useParameterSync' import { useLFOMapping } from './hooks/useLFOMapping' import type { TileState } from './types/tiles' import { createTileStateFromCurrent } from './utils/tileState' import { DEFAULT_DOWNLOAD_OPTIONS, PLAYBACK_ID } from './constants/defaults' import { getTileId, getTileFromGrid } from './utils/tileHelpers' function App() { const engineValues = useStore(engineSettings) const effectValues = useStore(effectSettings) const [downloading, setDownloading] = useState(false) const [customTile, setCustomTile] = useState(() => createTileStateFromCurrent('t*(8&t>>9)')) const [showWarning, setShowWarning] = useState(true) const [showHelp, setShowHelp] = useState(false) const [mobileHeaderTab, setMobileHeaderTab] = useState<'global' | 'options' | 'modulate'>('global') const downloadServiceRef = useRef(new DownloadService()) const { tiles, setTiles, mode, regenerateAll, regenerateTile, switchMode } = useTileGrid() const { playing, queued, playbackPosition, playbackManager, play, stop, queue, cancelQueue, updateMode } = usePlaybackControl({ mode }) const { focusedTile, setFocus, moveFocus } = useFocusNavigation({ tiles, onFocusChange: (tile) => { if (tile !== 'custom') { const tileData = getTileFromGrid(tiles, tile.row, tile.col) if (tileData) { saveCurrentParams() loadParams(tileData) } } else { saveCurrentParams() loadParams(customTile) } } }) const { saveCurrentParams, loadParams, handleEngineChange, handleEffectChange, randomizeParams, randomizeAllParams, interpolateParams } = useParameterSync({ tiles, setTiles, customTile, setCustomTile, focusedTile, playbackManager, playing, playbackId: PLAYBACK_ID.CUSTOM }) const { handleLFOChange, handleParameterMapClick, handleUpdateMappingDepth, handleRemoveMapping, getMappedLFOs } = useLFOMapping({ playbackManager, saveCurrentParams }) const handleRandom = () => { cancelQueue() regenerateAll() } const handleModeToggle = () => { const newMode = mode === 'bytebeat' ? 'fm' : 'bytebeat' stop() switchMode(newMode) updateMode(newMode) } 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 setFocus({ row, col }) if (isDoubleClick || playing === null) { play(tile.formula, id, tile) } else { queue(id, () => { const queuedTile = getTileFromGrid(tiles, row, col) if (queuedTile) { play(queuedTile.formula, id, queuedTile) } }) } } const handleTileDoubleClick = (formula: string, row: number, col: number) => { handleTileClick(formula, row, col, true) } 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 handleCustomEvaluate = (formula: string) => { setFocus('custom') setCustomTile({ ...customTile, formula }) play(formula, PLAYBACK_ID.CUSTOM, { ...customTile, formula }) } const handleCustomStop = () => { if (playing === PLAYBACK_ID.CUSTOM) { stop() } } const handleCustomRandom = () => { return generateRandomFormula(engineValues.complexity) } const handleKeyboardSpace = () => { if (playing) { stop() } 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 handleRegenerate = (row: number, col: number) => { const newTile = regenerateTile(row, col) const tileId = getTileId(row, col) if (playing === tileId) { play(newTile.formula, tileId, newTile) } } const handleKeyboardR = () => { if (focusedTile !== 'custom') { handleRegenerate(focusedTile.row, focusedTile.col) } } const handleKeyboardC = () => { const tileId = focusedTile === 'custom' ? PLAYBACK_ID.CUSTOM : getTileId(focusedTile.row, focusedTile.col) randomizeParams(tileId) } 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: handleRandom, onC: handleKeyboardC, onShiftC: randomizeAllParams, onI: interpolateParams, onEscape: exitMappingMode }) return (
{showWarning && } {showHelp && setShowHelp(false)} />}
{/* Mobile header */}

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

{mobileHeaderTab === 'global' && (
)} {mobileHeaderTab === 'options' && ( )} {mobileHeaderTab === 'modulate' && ( )}
{/* Desktop header */}

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

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