186 lines
6.4 KiB
TypeScript
186 lines
6.4 KiB
TypeScript
import { useState, useRef, useEffect } from 'react'
|
|
import { useStore } from '@nanostores/react'
|
|
import { PlaybackManager } from './services/PlaybackManager'
|
|
import { DownloadService } from './services/DownloadService'
|
|
import { generateFormulaGrid } from './utils/bytebeatFormulas'
|
|
import { BytebeatTile } from './components/BytebeatTile'
|
|
import { EffectsBar } from './components/EffectsBar'
|
|
import { EngineControls } from './components/EngineControls'
|
|
import { getSampleRateFromIndex } from './config/effects'
|
|
import { engineSettings, effectSettings } from './stores/settings'
|
|
|
|
function App() {
|
|
const engineValues = useStore(engineSettings)
|
|
const effectValues = useStore(effectSettings)
|
|
|
|
const [formulas, setFormulas] = useState<string[][]>(() =>
|
|
generateFormulaGrid(100, 2, engineValues.complexity)
|
|
)
|
|
const [playing, setPlaying] = useState<string | null>(null)
|
|
const [queued, setQueued] = useState<string | null>(null)
|
|
const [playbackPosition, setPlaybackPosition] = useState<number>(0)
|
|
const [downloading, setDownloading] = useState(false)
|
|
const playbackManagerRef = useRef<PlaybackManager | null>(null)
|
|
const downloadServiceRef = useRef<DownloadService>(new DownloadService())
|
|
const animationFrameRef = useRef<number | null>(null)
|
|
|
|
useEffect(() => {
|
|
effectSettings.setKey('masterVolume', engineValues.masterVolume)
|
|
}, [engineValues.masterVolume])
|
|
|
|
const handleRandom = () => {
|
|
setFormulas(generateFormulaGrid(100, 2, engineValues.complexity))
|
|
setQueued(null)
|
|
}
|
|
|
|
const playFormula = (formula: string, id: string) => {
|
|
const sampleRate = getSampleRateFromIndex(engineValues.sampleRate)
|
|
const duration = engineValues.loopDuration
|
|
|
|
if (!playbackManagerRef.current) {
|
|
playbackManagerRef.current = new PlaybackManager({ sampleRate, duration })
|
|
} else {
|
|
playbackManagerRef.current.updateOptions({ sampleRate, duration })
|
|
}
|
|
|
|
playbackManagerRef.current.stop()
|
|
playbackManagerRef.current.setEffects(effectValues)
|
|
|
|
const success = playbackManagerRef.current.play(formula, sampleRate, duration)
|
|
|
|
if (success) {
|
|
setPlaying(id)
|
|
setQueued(null)
|
|
startPlaybackTracking()
|
|
} else {
|
|
console.error('Failed to play formula')
|
|
}
|
|
}
|
|
|
|
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}`
|
|
|
|
if (playing === id) {
|
|
playbackManagerRef.current?.stop()
|
|
setPlaying(null)
|
|
setQueued(null)
|
|
if (animationFrameRef.current) {
|
|
cancelAnimationFrame(animationFrameRef.current)
|
|
animationFrameRef.current = null
|
|
}
|
|
return
|
|
}
|
|
|
|
if (isDoubleClick || playing === null) {
|
|
playFormula(formula, id)
|
|
} else {
|
|
setQueued(id)
|
|
if (playbackManagerRef.current) {
|
|
playbackManagerRef.current.scheduleNextTrack(() => {
|
|
const queuedFormula = formulas.flat()[parseInt(id.split('-')[0]) * 2 + parseInt(id.split('-')[1])]
|
|
if (queuedFormula) {
|
|
playFormula(queuedFormula, id)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleTileDoubleClick = (formula: string, row: number, col: number) => {
|
|
handleTileClick(formula, row, col, true)
|
|
}
|
|
|
|
|
|
const handleEngineChange = (parameterId: string, value: number) => {
|
|
engineSettings.setKey(parameterId as keyof typeof engineValues, value)
|
|
|
|
if (parameterId === 'masterVolume' && playbackManagerRef.current) {
|
|
playbackManagerRef.current.setEffects(effectValues)
|
|
}
|
|
}
|
|
|
|
const handleEffectChange = (parameterId: string, value: number | boolean) => {
|
|
effectSettings.setKey(parameterId as any, value as any)
|
|
if (playbackManagerRef.current) {
|
|
playbackManagerRef.current.setEffects(effectValues)
|
|
}
|
|
}
|
|
|
|
const handleDownloadAll = async () => {
|
|
setDownloading(true)
|
|
await downloadServiceRef.current.downloadAll(formulas, { duration: 10, bitDepth: 8 })
|
|
setDownloading(false)
|
|
}
|
|
|
|
const handleDownloadFormula = (formula: string, filename: string) => {
|
|
downloadServiceRef.current.downloadFormula(formula, filename, { duration: 10, bitDepth: 8 })
|
|
}
|
|
|
|
return (
|
|
<div className="w-screen h-screen flex flex-col bg-black overflow-hidden">
|
|
<header className="bg-black border-b-2 border-white px-6 py-3">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h1 className="font-mono text-sm tracking-[0.3em] text-white">BRUITISTE</h1>
|
|
<div className="flex gap-4">
|
|
<button
|
|
onClick={handleRandom}
|
|
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"
|
|
>
|
|
RANDOM
|
|
</button>
|
|
<button
|
|
onClick={handleDownloadAll}
|
|
disabled={downloading}
|
|
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"
|
|
>
|
|
{downloading ? 'DOWNLOADING...' : 'DOWNLOAD ALL'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<EngineControls values={engineValues} onChange={handleEngineChange} />
|
|
</header>
|
|
|
|
<div className="flex-1 grid grid-cols-2 auto-rows-min gap-[1px] bg-white p-[1px] overflow-auto">
|
|
{formulas.map((row, i) =>
|
|
row.map((formula, j) => {
|
|
const id = `${i}-${j}`
|
|
return (
|
|
<BytebeatTile
|
|
key={id}
|
|
formula={formula}
|
|
row={i}
|
|
col={j}
|
|
isPlaying={playing === id}
|
|
isQueued={queued === id}
|
|
playbackPosition={playing === id ? playbackPosition : 0}
|
|
onPlay={handleTileClick}
|
|
onDoubleClick={handleTileDoubleClick}
|
|
onDownload={handleDownloadFormula}
|
|
/>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
<EffectsBar values={effectValues} onChange={handleEffectChange} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default App
|