Adding new FM synthesis mode

This commit is contained in:
2025-10-06 13:08:59 +02:00
parent 0110a9760b
commit 324cf9d2ed
13 changed files with 1233 additions and 69 deletions

View File

@ -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) =>