Better code quality
This commit is contained in:
272
src/App.tsx
272
src/App.tsx
@ -7,40 +7,46 @@ import { generateTileGrid, generateRandomFormula } from './utils/bytebeatFormula
|
||||
import { BytebeatTile } from './components/BytebeatTile'
|
||||
import { EffectsBar } from './components/EffectsBar'
|
||||
import { EngineControls } from './components/EngineControls'
|
||||
import { FormulaEditor } from './components/FormulaEditor'
|
||||
import { getSampleRateFromIndex } from './config/effects'
|
||||
import { engineSettings, effectSettings } from './stores/settings'
|
||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
||||
import { useTileParams } from './hooks/useTileParams'
|
||||
import type { TileState } from './types/tiles'
|
||||
import { createTileStateFromCurrent, loadTileParams, saveTileParams } from './utils/tileState'
|
||||
import { createTileStateFromCurrent, loadTileParams } from './utils/tileState'
|
||||
import { DEFAULT_VARIABLES, PLAYBACK_ID, TILE_GRID, DEFAULT_DOWNLOAD_OPTIONS } 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<TileState[][]>(() =>
|
||||
generateTileGrid(100, 2, engineValues.complexity)
|
||||
generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity)
|
||||
)
|
||||
const [playing, setPlaying] = useState<string | null>(null)
|
||||
const [queued, setQueued] = useState<string | null>(null)
|
||||
const [regenerating, setRegenerating] = useState<string | null>(null)
|
||||
const [playbackPosition, setPlaybackPosition] = useState<number>(0)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [focusedTile, setFocusedTile] = useState<{ row: number; col: number }>({ row: 0, col: 0 })
|
||||
const [focusedTile, setFocusedTile] = useState<FocusedTile>({ row: 0, col: 0 })
|
||||
const [customTile, setCustomTile] = useState<TileState>(() => createTileStateFromCurrent('t*(8&t>>9)'))
|
||||
const playbackManagerRef = useRef<PlaybackManager | null>(null)
|
||||
const downloadServiceRef = useRef<DownloadService>(new DownloadService())
|
||||
const animationFrameRef = useRef<number | null>(null)
|
||||
const tilesRef = useRef<TileState[][]>(tiles)
|
||||
|
||||
const { saveCurrentTileParams } = useTileParams({ tiles, setTiles, customTile, setCustomTile, focusedTile })
|
||||
|
||||
useEffect(() => {
|
||||
tilesRef.current = tiles
|
||||
}, [tiles])
|
||||
if (playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setPlaybackPositionCallback(setPlaybackPosition)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
effectSettings.setKey('masterVolume', engineValues.masterVolume)
|
||||
}, [engineValues.masterVolume])
|
||||
|
||||
const handleRandom = () => {
|
||||
setTiles(generateTileGrid(100, 2, engineValues.complexity))
|
||||
setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||
setQueued(null)
|
||||
}
|
||||
|
||||
@ -56,58 +62,42 @@ function App() {
|
||||
|
||||
playbackManagerRef.current.stop()
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
playbackManagerRef.current.setPitch(engineValues.pitch ?? 1)
|
||||
playbackManagerRef.current.setVariables(
|
||||
engineValues.a ?? DEFAULT_VARIABLES.a,
|
||||
engineValues.b ?? DEFAULT_VARIABLES.b,
|
||||
engineValues.c ?? DEFAULT_VARIABLES.c,
|
||||
engineValues.d ?? DEFAULT_VARIABLES.d
|
||||
)
|
||||
|
||||
const success = await playbackManagerRef.current.play(formula, sampleRate, duration)
|
||||
const success = await playbackManagerRef.current.play(formula)
|
||||
|
||||
if (success) {
|
||||
setPlaying(id)
|
||||
setQueued(null)
|
||||
startPlaybackTracking()
|
||||
return true
|
||||
} else {
|
||||
console.error('Failed to play formula')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const startPlaybackTracking = () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current)
|
||||
}
|
||||
|
||||
const updatePosition = () => {
|
||||
if (playbackManagerRef.current) {
|
||||
const position = playbackManagerRef.current.getPlaybackPosition()
|
||||
setPlaybackPosition(position)
|
||||
animationFrameRef.current = requestAnimationFrame(updatePosition)
|
||||
}
|
||||
}
|
||||
updatePosition()
|
||||
}
|
||||
|
||||
const handleTileClick = (_formula: string, row: number, col: number, isDoubleClick: boolean = false) => {
|
||||
const id = `${row}-${col}`
|
||||
const tile = tiles[row]?.[col]
|
||||
const id = getTileId(row, col)
|
||||
const tile = getTileFromGrid(tiles, row, col)
|
||||
|
||||
if (!tile) return
|
||||
|
||||
if (focusedTile.row !== row || focusedTile.col !== col) {
|
||||
const currentTile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (currentTile) {
|
||||
saveTileParams(currentTile)
|
||||
}
|
||||
loadTileParams(tile)
|
||||
if (focusedTile === 'custom' || (focusedTile.row !== row || focusedTile.col !== col)) {
|
||||
saveCurrentTileParams()
|
||||
}
|
||||
|
||||
if (tile) {
|
||||
loadTileParams(tile)
|
||||
}
|
||||
setFocusedTile({ row, col })
|
||||
|
||||
if (playing === id) {
|
||||
playbackManagerRef.current?.stop()
|
||||
setPlaying(null)
|
||||
setQueued(null)
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current)
|
||||
animationFrameRef.current = null
|
||||
}
|
||||
handleStop()
|
||||
return
|
||||
}
|
||||
|
||||
@ -115,15 +105,6 @@ function App() {
|
||||
playFormula(tile.formula, id)
|
||||
} else {
|
||||
setQueued(id)
|
||||
if (playbackManagerRef.current) {
|
||||
playbackManagerRef.current.scheduleNextTrack(() => {
|
||||
const queuedTile = tilesRef.current[row]?.[col]
|
||||
if (queuedTile) {
|
||||
loadTileParams(queuedTile)
|
||||
playFormula(queuedTile.formula, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,13 +113,9 @@ function App() {
|
||||
}
|
||||
|
||||
|
||||
const handleEngineChange = (parameterId: string, value: number) => {
|
||||
const handleEngineChange = async (parameterId: string, value: number) => {
|
||||
engineSettings.setKey(parameterId as keyof typeof engineValues, value)
|
||||
|
||||
const currentTile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (currentTile) {
|
||||
saveTileParams(currentTile)
|
||||
}
|
||||
saveCurrentTileParams()
|
||||
|
||||
if (parameterId === 'masterVolume' && playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setEffects({ ...effectValues, masterVolume: value })
|
||||
@ -147,15 +124,21 @@ function App() {
|
||||
if (parameterId === 'pitch' && playbackManagerRef.current) {
|
||||
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)
|
||||
|
||||
const currentTile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (currentTile) {
|
||||
saveTileParams(currentTile)
|
||||
}
|
||||
saveCurrentTileParams()
|
||||
|
||||
if (playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
@ -165,59 +148,66 @@ function App() {
|
||||
const handleDownloadAll = async () => {
|
||||
setDownloading(true)
|
||||
const formulas = tiles.map(row => row.map(tile => tile.formula))
|
||||
await downloadServiceRef.current.downloadAll(formulas, { duration: 10, bitDepth: 8 })
|
||||
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: 10, bitDepth: 8 })
|
||||
downloadServiceRef.current.downloadFormula(formula, filename, {
|
||||
duration: DEFAULT_DOWNLOAD_OPTIONS.DURATION,
|
||||
bitDepth: DEFAULT_DOWNLOAD_OPTIONS.BIT_DEPTH
|
||||
})
|
||||
}
|
||||
|
||||
const handleRegenerate = (row: number, col: number) => {
|
||||
const id = `${row}-${col}`
|
||||
const newFormula = generateRandomFormula(engineValues.complexity)
|
||||
const newTile = createTileStateFromCurrent(newFormula)
|
||||
|
||||
if (playing === id && playbackManagerRef.current) {
|
||||
setRegenerating(id)
|
||||
playbackManagerRef.current.scheduleNextTrack(() => {
|
||||
setTiles(prevTiles => {
|
||||
const newTiles = [...prevTiles]
|
||||
newTiles[row] = [...newTiles[row]]
|
||||
newTiles[row][col] = newTile
|
||||
return newTiles
|
||||
})
|
||||
loadTileParams(newTile)
|
||||
playFormula(newTile.formula, id)
|
||||
setRegenerating(null)
|
||||
})
|
||||
} else {
|
||||
setTiles(prevTiles => {
|
||||
const newTiles = [...prevTiles]
|
||||
newTiles[row] = [...newTiles[row]]
|
||||
newTiles[row][col] = newTile
|
||||
return newTiles
|
||||
})
|
||||
}
|
||||
setTiles(prevTiles => {
|
||||
const newTiles = [...prevTiles]
|
||||
newTiles[row] = [...newTiles[row]]
|
||||
newTiles[row][col] = newTile
|
||||
return newTiles
|
||||
})
|
||||
}
|
||||
|
||||
const handleStop = () => {
|
||||
playbackManagerRef.current?.stop()
|
||||
setPlaying(null)
|
||||
setQueued(null)
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current)
|
||||
animationFrameRef.current = 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) => {
|
||||
const currentTile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (currentTile) {
|
||||
saveTileParams(currentTile)
|
||||
}
|
||||
saveCurrentTileParams()
|
||||
|
||||
setFocusedTile(prev => {
|
||||
if (prev === 'custom') return prev
|
||||
|
||||
let { row, col } = prev
|
||||
const maxRow = tiles.length - 1
|
||||
const maxCol = (tiles[row]?.length || 1) - 1
|
||||
@ -240,16 +230,17 @@ function App() {
|
||||
const newTile = tiles[row]?.[col]
|
||||
if (newTile) {
|
||||
loadTileParams(newTile)
|
||||
return { row, col }
|
||||
}
|
||||
|
||||
return { row, col }
|
||||
return prev
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeyboardSpace = () => {
|
||||
if (playing) {
|
||||
handleStop()
|
||||
} else {
|
||||
} else if (focusedTile !== 'custom') {
|
||||
const tile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (tile) {
|
||||
handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true)
|
||||
@ -258,21 +249,27 @@ function App() {
|
||||
}
|
||||
|
||||
const handleKeyboardEnter = () => {
|
||||
const tile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (tile) {
|
||||
handleTileClick(tile.formula, focusedTile.row, focusedTile.col, false)
|
||||
if (focusedTile !== 'custom') {
|
||||
const tile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (tile) {
|
||||
handleTileClick(tile.formula, focusedTile.row, focusedTile.col, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyboardDoubleEnter = () => {
|
||||
const tile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (tile) {
|
||||
handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true)
|
||||
if (focusedTile !== 'custom') {
|
||||
const tile = tiles[focusedTile.row]?.[focusedTile.col]
|
||||
if (tile) {
|
||||
handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyboardR = () => {
|
||||
handleRegenerate(focusedTile.row, focusedTile.col)
|
||||
if (focusedTile !== 'custom') {
|
||||
handleRegenerate(focusedTile.row, focusedTile.col)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyboardShiftR = () => {
|
||||
@ -292,9 +289,11 @@ function App() {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.querySelector(`[data-tile-id="${focusedTile.row}-${focusedTile.col}"]`)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
if (focusedTile !== 'custom') {
|
||||
const element = document.querySelector(`[data-tile-id="${focusedTile.row}-${focusedTile.col}"]`)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
}, [focusedTile])
|
||||
|
||||
@ -326,35 +325,50 @@ function App() {
|
||||
className="px-6 py-2 bg-black text-white border-2 border-white font-mono text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Archive size={14} strokeWidth={2} />
|
||||
{downloading ? 'DOWNLOADING...' : 'DOWNLOAD ALL'}
|
||||
{downloading ? 'DOWNLOADING...' : 'PACK'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 grid grid-cols-2 auto-rows-min gap-[1px] bg-white p-[1px] overflow-auto">
|
||||
{tiles.map((row, i) =>
|
||||
row.map((tile, j) => {
|
||||
const id = `${i}-${j}`
|
||||
return (
|
||||
<BytebeatTile
|
||||
key={id}
|
||||
formula={tile.formula}
|
||||
row={i}
|
||||
col={j}
|
||||
isPlaying={playing === id}
|
||||
isQueued={queued === id}
|
||||
isRegenerating={regenerating === id}
|
||||
isFocused={focusedTile.row === i && focusedTile.col === j}
|
||||
playbackPosition={playing === id ? playbackPosition : 0}
|
||||
onPlay={handleTileClick}
|
||||
onDoubleClick={handleTileDoubleClick}
|
||||
onDownload={handleDownloadFormula}
|
||||
onRegenerate={handleRegenerate}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
<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">
|
||||
<FormulaEditor
|
||||
formula={customTile.formula}
|
||||
isPlaying={playing === PLAYBACK_ID.CUSTOM}
|
||||
isFocused={focusedTile === 'custom'}
|
||||
playbackPosition={playing === PLAYBACK_ID.CUSTOM ? playbackPosition : 0}
|
||||
onEvaluate={handleCustomEvaluate}
|
||||
onStop={handleCustomStop}
|
||||
onRandom={handleCustomRandom}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 grid grid-cols-2 auto-rows-min gap-[1px] bg-white p-[1px]">
|
||||
{tiles.map((row, i) =>
|
||||
row.map((tile, j) => {
|
||||
const id = getTileId(i, j)
|
||||
return (
|
||||
<BytebeatTile
|
||||
key={id}
|
||||
formula={tile.formula}
|
||||
row={i}
|
||||
col={j}
|
||||
isPlaying={playing === id}
|
||||
isQueued={queued === id}
|
||||
isFocused={focusedTile !== 'custom' && focusedTile.row === i && focusedTile.col === j}
|
||||
playbackPosition={playing === id ? playbackPosition : 0}
|
||||
onPlay={handleTileClick}
|
||||
onDoubleClick={handleTileDoubleClick}
|
||||
onDownload={handleDownloadFormula}
|
||||
onRegenerate={handleRegenerate}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EffectsBar values={effectValues} onChange={handleEffectChange} />
|
||||
|
||||
Reference in New Issue
Block a user