Adding new FM synthesis mode
This commit is contained in:
203
src/App.tsx
203
src/App.tsx
@ -4,6 +4,7 @@ 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 { generateFMTileGrid, generateRandomFMPatch, createFMTileState } from './utils/fmPatches'
|
||||
import { BytebeatTile } from './components/BytebeatTile'
|
||||
import { EffectsBar } from './components/EffectsBar'
|
||||
import { EngineControls } from './components/EngineControls'
|
||||
@ -13,6 +14,7 @@ import { AudioContextWarning } from './components/AudioContextWarning'
|
||||
import { HelpModal } from './components/HelpModal'
|
||||
import { getSampleRateFromIndex } from './config/effects'
|
||||
import { engineSettings, effectSettings, lfoSettings, type LFOConfig } from './stores/settings'
|
||||
import { synthesisMode, setSynthesisMode } from './stores/synthesisMode'
|
||||
import { exitMappingMode } from './stores/mappingMode'
|
||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
||||
import { useTileParams } from './hooks/useTileParams'
|
||||
@ -24,9 +26,12 @@ import { getTileId, getTileFromGrid, type FocusedTile } from './utils/tileHelper
|
||||
function App() {
|
||||
const engineValues = useStore(engineSettings)
|
||||
const effectValues = useStore(effectSettings)
|
||||
const mode = useStore(synthesisMode)
|
||||
|
||||
const [tiles, setTiles] = useState<TileState[][]>(() =>
|
||||
generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity)
|
||||
mode === 'fm'
|
||||
? generateFMTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, 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)
|
||||
@ -81,10 +86,32 @@ function App() {
|
||||
|
||||
const handleRandom = () => {
|
||||
clearSwitchTimer()
|
||||
setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||
if (mode === 'fm') {
|
||||
setTiles(generateFMTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||
} else {
|
||||
setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||
}
|
||||
setQueued(null)
|
||||
}
|
||||
|
||||
const handleModeToggle = () => {
|
||||
const newMode = mode === 'bytebeat' ? 'fm' : 'bytebeat'
|
||||
handleStop()
|
||||
setSynthesisMode(newMode)
|
||||
|
||||
if (playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setMode(newMode)
|
||||
playbackManagerRef.current.setAlgorithm(engineValues.fmAlgorithm ?? 0)
|
||||
playbackManagerRef.current.setFeedback(engineValues.fmFeedback ?? 0)
|
||||
}
|
||||
|
||||
if (newMode === 'fm') {
|
||||
setTiles(generateFMTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||
} else {
|
||||
setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||
}
|
||||
}
|
||||
|
||||
const handleRandomizeAllParams = () => {
|
||||
clearSwitchTimer()
|
||||
let newRandomized: TileState | null = null
|
||||
@ -145,6 +172,7 @@ function App() {
|
||||
|
||||
if (!playbackManagerRef.current) {
|
||||
playbackManagerRef.current = new PlaybackManager({ sampleRate, duration })
|
||||
playbackManagerRef.current.setMode(mode)
|
||||
} else {
|
||||
await playbackManagerRef.current.updateOptions({ sampleRate, duration })
|
||||
}
|
||||
@ -158,6 +186,10 @@ function App() {
|
||||
engineValues.d ?? DEFAULT_VARIABLES.d
|
||||
)
|
||||
playbackManagerRef.current.setPitch(engineValues.pitch ?? 1.0)
|
||||
const fmPatch = mode === 'fm' ? JSON.parse(formula) : null
|
||||
const lfoRates = fmPatch?.lfoRates || undefined
|
||||
playbackManagerRef.current.setAlgorithm(engineValues.fmAlgorithm ?? 0, lfoRates)
|
||||
playbackManagerRef.current.setFeedback(engineValues.fmFeedback ?? 0)
|
||||
|
||||
await playbackManagerRef.current.play(formula)
|
||||
setPlaying(id)
|
||||
@ -206,6 +238,14 @@ function App() {
|
||||
playbackManagerRef.current.setPitch(value)
|
||||
}
|
||||
|
||||
if (parameterId === 'fmAlgorithm' && playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setAlgorithm(value)
|
||||
}
|
||||
|
||||
if (parameterId === 'fmFeedback' && playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setFeedback(value)
|
||||
}
|
||||
|
||||
if (['a', 'b', 'c', 'd'].includes(parameterId) && playbackManagerRef.current && playing) {
|
||||
const updatedValues = { ...engineValues, [parameterId]: value }
|
||||
playbackManagerRef.current.setVariables(
|
||||
@ -324,8 +364,15 @@ function App() {
|
||||
}
|
||||
|
||||
const handleRegenerate = (row: number, col: number) => {
|
||||
const newFormula = generateRandomFormula(engineValues.complexity)
|
||||
const newTile = createTileStateFromCurrent(newFormula)
|
||||
let newTile: TileState
|
||||
|
||||
if (mode === 'fm') {
|
||||
const patch = generateRandomFMPatch(engineValues.complexity)
|
||||
newTile = createFMTileState(patch)
|
||||
} else {
|
||||
const newFormula = generateRandomFormula(engineValues.complexity)
|
||||
newTile = createTileStateFromCurrent(newFormula)
|
||||
}
|
||||
|
||||
setTiles(prevTiles => {
|
||||
const newTiles = [...prevTiles]
|
||||
@ -547,37 +594,61 @@ function App() {
|
||||
>
|
||||
BRUITISTE
|
||||
</h1>
|
||||
<div className="flex gap-1 border-2 border-white">
|
||||
<button
|
||||
onClick={() => setMobileHeaderTab('global')}
|
||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||
mobileHeaderTab === 'global'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
GLOBAL
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMobileHeaderTab('options')}
|
||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||
mobileHeaderTab === 'options'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
OPTIONS
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMobileHeaderTab('modulate')}
|
||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||
mobileHeaderTab === 'modulate'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
MODULATE
|
||||
</button>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex border-2 border-white">
|
||||
<button
|
||||
onClick={handleModeToggle}
|
||||
className={`px-2 py-1 font-mono text-[7px] tracking-[0.15em] transition-colors ${
|
||||
mode === 'bytebeat'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
1-BIT
|
||||
</button>
|
||||
<button
|
||||
onClick={handleModeToggle}
|
||||
className={`px-2 py-1 font-mono text-[7px] tracking-[0.15em] transition-colors ${
|
||||
mode === 'fm'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
4OP-FM
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex border-2 border-white">
|
||||
<button
|
||||
onClick={() => setMobileHeaderTab('global')}
|
||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||
mobileHeaderTab === 'global'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
GLOBAL
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMobileHeaderTab('options')}
|
||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||
mobileHeaderTab === 'options'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
OPTIONS
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMobileHeaderTab('modulate')}
|
||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||
mobileHeaderTab === 'modulate'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
MODULATE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -635,12 +706,36 @@ function App() {
|
||||
|
||||
{/* Desktop header */}
|
||||
<div className="hidden lg:flex items-center gap-4">
|
||||
<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>
|
||||
<div className="flex flex-col gap-1 flex-shrink-0">
|
||||
<h1
|
||||
onClick={() => setShowHelp(true)}
|
||||
className="font-mono text-sm tracking-[0.3em] text-white cursor-pointer hover:opacity-70 transition-opacity"
|
||||
>
|
||||
BRUITISTE
|
||||
</h1>
|
||||
<div className="flex border-2 border-white w-fit">
|
||||
<button
|
||||
onClick={handleModeToggle}
|
||||
className={`px-2 py-0.5 font-mono text-[7px] tracking-[0.15em] transition-colors whitespace-nowrap ${
|
||||
mode === 'bytebeat'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
1-BIT
|
||||
</button>
|
||||
<button
|
||||
onClick={handleModeToggle}
|
||||
className={`px-2 py-0.5 font-mono text-[7px] tracking-[0.15em] transition-colors whitespace-nowrap ${
|
||||
mode === 'fm'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-black text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
4OP-FM
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<EngineControls
|
||||
values={engineValues}
|
||||
@ -687,19 +782,21 @@ function App() {
|
||||
<LFOPanel onChange={handleLFOChange} onUpdateDepth={handleUpdateMappingDepth} onRemoveMapping={handleRemoveMapping} />
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-auto bg-white">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-[1px] bg-white p-[1px]">
|
||||
<div className="col-span-1 lg:col-span-4">
|
||||
<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}
|
||||
/>
|
||||
{mode === 'bytebeat' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-[1px] bg-white p-[1px]">
|
||||
<div className="col-span-1 lg:col-span-4">
|
||||
<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>
|
||||
)}
|
||||
|
||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-4 auto-rows-min gap-[1px] bg-white p-[1px]">
|
||||
{tiles.map((row, i) =>
|
||||
|
||||
Reference in New Issue
Block a user