slightly better

This commit is contained in:
2025-10-06 02:16:23 +02:00
parent ba37b94908
commit ac772054c9
35 changed files with 1874 additions and 390 deletions

View File

@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react'
import { useStore } from '@nanostores/react'
import { Square, Archive, Dices } from 'lucide-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'
@ -8,12 +8,16 @@ 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 } from './stores/settings'
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 } from './utils/tileState'
import { createTileStateFromCurrent, loadTileParams, randomizeTileParams } from './utils/tileState'
import { DEFAULT_VARIABLES, PLAYBACK_ID, TILE_GRID, DEFAULT_DOWNLOAD_OPTIONS } from './constants/defaults'
import { getTileId, getTileFromGrid, type FocusedTile } from './utils/tileHelpers'
@ -30,6 +34,8 @@ function App() {
const [downloading, setDownloading] = useState(false)
const [focusedTile, setFocusedTile] = useState<FocusedTile>({ row: 0, col: 0 })
const [customTile, setCustomTile] = useState<TileState>(() => createTileStateFromCurrent('t*(8&t>>9)'))
const [showWarning, setShowWarning] = useState(true)
const [showHelp, setShowHelp] = useState(false)
const playbackManagerRef = useRef<PlaybackManager | null>(null)
const downloadServiceRef = useRef<DownloadService>(new DownloadService())
@ -50,6 +56,59 @@ function App() {
setQueued(null)
}
const handleRandomizeAllParams = () => {
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 = engineValues.loopDuration
@ -68,17 +127,12 @@ function App() {
engineValues.c ?? DEFAULT_VARIABLES.c,
engineValues.d ?? DEFAULT_VARIABLES.d
)
playbackManagerRef.current.setPitch(engineValues.pitch ?? 1.0)
const success = await playbackManagerRef.current.play(formula)
if (success) {
setPlaying(id)
setQueued(null)
return true
} else {
console.error('Failed to play formula')
return false
}
await playbackManagerRef.current.play(formula)
setPlaying(id)
setQueued(null)
return true
}
const handleTileClick = (_formula: string, row: number, col: number, isDoubleClick: boolean = false) => {
@ -121,7 +175,7 @@ function App() {
playbackManagerRef.current.setEffects({ ...effectValues, masterVolume: value })
}
if (parameterId === 'pitch' && playbackManagerRef.current) {
if (parameterId === 'pitch' && playbackManagerRef.current && playing) {
playbackManagerRef.current.setPitch(value)
}
@ -145,6 +199,86 @@ function App() {
}
}
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))
@ -276,6 +410,77 @@ function App() {
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),
@ -285,7 +490,10 @@ function App() {
onEnter: handleKeyboardEnter,
onDoubleEnter: handleKeyboardDoubleEnter,
onR: handleKeyboardR,
onShiftR: handleKeyboardShiftR
onShiftR: handleKeyboardShiftR,
onC: handleKeyboardC,
onShiftC: handleKeyboardShiftC,
onEscape: handleEscape
})
useEffect(() => {
@ -299,10 +507,22 @@ function App() {
return (
<div className="w-screen h-screen flex flex-col bg-black overflow-hidden">
{showWarning && <AudioContextWarning onDismiss={handleDismissWarning} />}
{showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
<header className="bg-black border-b-2 border-white px-6 py-3">
<div className="flex items-center justify-between gap-6">
<h1 className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0">BRUITISTE</h1>
<EngineControls values={engineValues} onChange={handleEngineChange} />
<h1
onClick={() => setShowHelp(true)}
className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
>
BRUITISTE
</h1>
<EngineControls
values={engineValues}
onChange={handleEngineChange}
onMapClick={handleParameterMapClick}
getMappedLFOs={getMappedLFOs}
/>
<div className="flex gap-4 flex-shrink-0">
<button
onClick={handleStop}
@ -319,6 +539,13 @@ function App() {
<Dices size={14} strokeWidth={2} />
RANDOM
</button>
<button
onClick={handleRandomizeAllParams}
className="px-6 py-2 bg-white text-black font-mono text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-2"
>
<Sparkles size={14} strokeWidth={2} />
CHAOS
</button>
<button
onClick={handleDownloadAll}
disabled={downloading}
@ -331,9 +558,11 @@ function App() {
</div>
</header>
<LFOPanel onChange={handleLFOChange} onUpdateDepth={handleUpdateMappingDepth} onRemoveMapping={handleRemoveMapping} />
<div className="flex-1 flex flex-col overflow-auto bg-white">
<div className="grid grid-cols-2 gap-[1px] bg-white p-[1px]">
<div className="col-span-2">
<div className="grid grid-cols-4 gap-[1px] bg-white p-[1px]">
<div className="col-span-4">
<FormulaEditor
formula={customTile.formula}
isPlaying={playing === PLAYBACK_ID.CUSTOM}
@ -346,7 +575,7 @@ function App() {
</div>
</div>
<div className="flex-1 grid grid-cols-2 auto-rows-min gap-[1px] bg-white p-[1px]">
<div className="flex-1 grid grid-cols-4 auto-rows-min gap-[1px] bg-white p-[1px]">
{tiles.map((row, i) =>
row.map((tile, j) => {
const id = getTileId(i, j)
@ -360,6 +589,10 @@ function App() {
isQueued={queued === id}
isFocused={focusedTile !== 'custom' && focusedTile.row === i && focusedTile.col === j}
playbackPosition={playing === id ? playbackPosition : 0}
a={tile.engineParams.a ?? 8}
b={tile.engineParams.b ?? 16}
c={tile.engineParams.c ?? 32}
d={tile.engineParams.d ?? 64}
onPlay={handleTileClick}
onDoubleClick={handleTileDoubleClick}
onDownload={handleDownloadFormula}
@ -371,7 +604,12 @@ function App() {
</div>
</div>
<EffectsBar values={effectValues} onChange={handleEffectChange} />
<EffectsBar
values={effectValues}
onChange={handleEffectChange}
onMapClick={handleParameterMapClick}
getMappedLFOs={getMappedLFOs}
/>
</div>
)
}