Weird hybrid

This commit is contained in:
2025-10-06 14:31:05 +02:00
parent ff5add97e8
commit 90f2f4209c
10 changed files with 405 additions and 103 deletions

View File

@ -1,6 +1,6 @@
import { useState, useRef } from 'react' import { useState, useRef } from 'react'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { Square, Archive, Dices, Sparkles } from 'lucide-react' import { Square, Archive, Dices, Sparkles, Blend } from 'lucide-react'
import { DownloadService } from './services/DownloadService' import { DownloadService } from './services/DownloadService'
import { generateRandomFormula } from './utils/bytebeatFormulas' import { generateRandomFormula } from './utils/bytebeatFormulas'
import { BytebeatTile } from './components/tile/BytebeatTile' import { BytebeatTile } from './components/tile/BytebeatTile'
@ -55,7 +55,7 @@ function App() {
} }
}) })
const { saveCurrentParams, loadParams, handleEngineChange, handleEffectChange, randomizeParams, randomizeAllParams } = const { saveCurrentParams, loadParams, handleEngineChange, handleEffectChange, randomizeParams, randomizeAllParams, interpolateParams } =
useParameterSync({ useParameterSync({
tiles, tiles,
setTiles, setTiles,
@ -171,9 +171,18 @@ function App() {
} }
} }
const handleRegenerate = (row: number, col: number) => {
const newTile = regenerateTile(row, col)
const tileId = getTileId(row, col)
if (playing === tileId) {
play(newTile.formula, tileId, newTile)
}
}
const handleKeyboardR = () => { const handleKeyboardR = () => {
if (focusedTile !== 'custom') { if (focusedTile !== 'custom') {
regenerateTile(focusedTile.row, focusedTile.col) handleRegenerate(focusedTile.row, focusedTile.col)
} }
} }
@ -200,6 +209,7 @@ function App() {
onShiftR: handleRandom, onShiftR: handleRandom,
onC: handleKeyboardC, onC: handleKeyboardC,
onShiftC: randomizeAllParams, onShiftC: randomizeAllParams,
onI: interpolateParams,
onEscape: exitMappingMode onEscape: exitMappingMode
}) })
@ -296,6 +306,12 @@ function App() {
> >
<Sparkles size={12} strokeWidth={2} className="mx-auto" /> <Sparkles size={12} strokeWidth={2} className="mx-auto" />
</button> </button>
<button
onClick={interpolateParams}
className="flex-1 px-2 py-2 bg-white text-black font-mono text-[9px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
>
<Blend size={12} strokeWidth={2} className="mx-auto" />
</button>
<button <button
onClick={handleDownloadAll} onClick={handleDownloadAll}
disabled={downloading} disabled={downloading}
@ -390,6 +406,13 @@ function App() {
<Sparkles size={12} strokeWidth={2} /> <Sparkles size={12} strokeWidth={2} />
CHAOS CHAOS
</button> </button>
<button
onClick={interpolateParams}
className="px-4 py-2 bg-white text-black font-mono text-[10px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-1"
>
<Blend size={12} strokeWidth={2} />
MORPH
</button>
<button <button
onClick={handleDownloadAll} onClick={handleDownloadAll}
disabled={downloading} disabled={downloading}
@ -442,7 +465,7 @@ function App() {
onPlay={handleTileClick} onPlay={handleTileClick}
onDoubleClick={handleTileDoubleClick} onDoubleClick={handleTileDoubleClick}
onDownload={handleDownloadFormula} onDownload={handleDownloadFormula}
onRegenerate={regenerateTile} onRegenerate={handleRegenerate}
/> />
) )
}) })

View File

@ -31,7 +31,8 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, sh
return getAlgorithmLabel(value) return getAlgorithmLabel(value)
default: { default: {
const param = ENGINE_CONTROLS[0].parameters.find(p => p.id === id) const param = ENGINE_CONTROLS[0].parameters.find(p => p.id === id)
return `${value}${param?.unit || ''}` const formattedValue = Number.isInteger(value) ? value.toString() : value.toFixed(1)
return `${formattedValue}${param?.unit || ''}`
} }
} }
} }

View File

@ -77,6 +77,10 @@ export function HelpModal({ onClose, showStartButton = false }: HelpModalProps)
<td className="p-2 md:p-3 border-r border-white">SHIFT + C</td> <td className="p-2 md:p-3 border-r border-white">SHIFT + C</td>
<td className="p-2 md:p-3">Randomize all params (CHAOS)</td> <td className="p-2 md:p-3">Randomize all params (CHAOS)</td>
</tr> </tr>
<tr className="border-b border-white">
<td className="p-2 md:p-3 border-r border-white">I</td>
<td className="p-2 md:p-3">Interpolate params (MORPH)</td>
</tr>
<tr> <tr>
<td className="p-2 md:p-3 border-r border-white">ESC</td> <td className="p-2 md:p-3 border-r border-white">ESC</td>
<td className="p-2 md:p-3">Exit mapping mode</td> <td className="p-2 md:p-3">Exit mapping mode</td>

View File

@ -38,7 +38,14 @@ export function Knob({
const startValueRef = useRef<number>(0) const startValueRef = useRef<number>(0)
const mappingModeState = useStore(mappingMode) const mappingModeState = useStore(mappingMode)
const displayValue = formatValue && valueId ? formatValue(valueId, value) : `${value}${unit || ''}` const formatNumber = (num: number) => {
if (Number.isInteger(num)) return num.toString()
return num.toFixed(1)
}
const displayValue = formatValue && valueId
? formatValue(valueId, value)
: `${formatNumber(value)}${unit || ''}`
const isInMappingMode = mappingModeState.isActive && paramId const isInMappingMode = mappingModeState.isActive && paramId
const hasMappings = mappedLFOs.length > 0 const hasMappings = mappedLFOs.length > 0

View File

@ -31,7 +31,15 @@ export function Slider({
mappedLFOs = [] mappedLFOs = []
}: SliderProps) { }: SliderProps) {
const mappingModeState = useStore(mappingMode) const mappingModeState = useStore(mappingMode)
const displayValue = formatValue && valueId ? formatValue(valueId, value) : `${value}${unit || ''}`
const formatNumber = (num: number) => {
if (Number.isInteger(num)) return num.toString()
return num.toFixed(1)
}
const displayValue = formatValue && valueId
? formatValue(valueId, value)
: `${formatNumber(value)}${unit || ''}`
const isInMappingMode = !!(mappingModeState.isActive && paramId) const isInMappingMode = !!(mappingModeState.isActive && paramId)
const hasMappings = mappedLFOs.length > 0 const hasMappings = mappedLFOs.length > 0

View File

@ -217,8 +217,8 @@ export const EFFECTS: ParameterGroup[] = [
id: 'delayTime', id: 'delayTime',
label: 'Time', label: 'Time',
min: 10, min: 10,
max: 2000, max: 10000,
default: 250, default: 500,
step: 10, step: 10,
unit: 'ms' unit: 'ms'
}, },
@ -250,20 +250,20 @@ export const EFFECTS: ParameterGroup[] = [
unit: '%' unit: '%'
}, },
{ {
id: 'delaySaturation', id: 'delayPingPong',
label: 'Saturation', label: 'Ping-Pong',
min: 0, min: 0,
max: 100, max: 100,
default: 20, default: 0,
step: 1, step: 1,
unit: '%' unit: '%'
}, },
{ {
id: 'delayFlutter', id: 'delayDiffusion',
label: 'Flutter', label: 'Diffusion',
min: 0, min: 0,
max: 100, max: 100,
default: 15, default: 0,
step: 1, step: 1,
unit: '%' unit: '%'
} }

View File

@ -6,71 +6,118 @@ export class DelayEffect implements Effect {
private audioContext: AudioContext private audioContext: AudioContext
private inputNode: GainNode private inputNode: GainNode
private outputNode: GainNode private outputNode: GainNode
private delayNode: DelayNode
private feedbackNode: GainNode
private wetNode: GainNode
private dryNode: GainNode private dryNode: GainNode
private wetNode: GainNode
private leftDelayNode: DelayNode
private rightDelayNode: DelayNode
private leftFeedbackNode: GainNode
private rightFeedbackNode: GainNode
private filterNode: BiquadFilterNode private filterNode: BiquadFilterNode
private saturatorNode: WaveShaperNode private dcBlockerNode: BiquadFilterNode
private lfoNode: OscillatorNode
private lfoGainNode: GainNode private splitterNode: ChannelSplitterNode
private mergerNode: ChannelMergerNode
private diffusionNodes: BiquadFilterNode[] = []
private diffusionMixNode: GainNode
private diffusionWetNode: GainNode
private diffusionDryNode: GainNode
private bypassed: boolean = false private bypassed: boolean = false
private currentWetValue: number = 0.5 private currentWetValue: number = 0.5
private currentDryValue: number = 0.5 private currentDryValue: number = 0.5
private currentPingPong: number = 0
constructor(audioContext: AudioContext) { constructor(audioContext: AudioContext) {
this.audioContext = audioContext this.audioContext = audioContext
this.inputNode = audioContext.createGain() this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain() this.outputNode = audioContext.createGain()
this.delayNode = audioContext.createDelay(2.0)
this.feedbackNode = audioContext.createGain()
this.wetNode = audioContext.createGain()
this.dryNode = audioContext.createGain() this.dryNode = audioContext.createGain()
this.filterNode = audioContext.createBiquadFilter() this.wetNode = audioContext.createGain()
this.saturatorNode = audioContext.createWaveShaper()
this.delayNode.delayTime.value = 0.25 this.splitterNode = audioContext.createChannelSplitter(2)
this.mergerNode = audioContext.createChannelMerger(2)
this.leftDelayNode = audioContext.createDelay(11)
this.rightDelayNode = audioContext.createDelay(11)
this.leftFeedbackNode = audioContext.createGain()
this.rightFeedbackNode = audioContext.createGain()
this.filterNode = audioContext.createBiquadFilter()
this.dcBlockerNode = audioContext.createBiquadFilter()
this.diffusionMixNode = audioContext.createGain()
this.diffusionWetNode = audioContext.createGain()
this.diffusionDryNode = audioContext.createGain()
this.leftDelayNode.delayTime.value = 0.5
this.rightDelayNode.delayTime.value = 0.5
this.leftFeedbackNode.gain.value = 0.5
this.rightFeedbackNode.gain.value = 0.5
this.dryNode.gain.value = 0.5 this.dryNode.gain.value = 0.5
this.wetNode.gain.value = 0.5 this.wetNode.gain.value = 0.5
this.feedbackNode.gain.value = 0.5
this.filterNode.type = 'lowpass' this.filterNode.type = 'lowpass'
this.filterNode.frequency.value = 5000 this.filterNode.frequency.value = 5000
this.filterNode.Q.value = 0.7 this.filterNode.Q.value = 0.7
this.createSaturationCurve(0.2) this.dcBlockerNode.type = 'highpass'
this.dcBlockerNode.frequency.value = 5
this.dcBlockerNode.Q.value = 0.707
this.lfoNode = audioContext.createOscillator() this.diffusionWetNode.gain.value = 0
this.lfoGainNode = audioContext.createGain() this.diffusionDryNode.gain.value = 1
this.lfoNode.frequency.value = 2.5
this.lfoGainNode.gain.value = 0.0015
this.lfoNode.connect(this.lfoGainNode)
this.lfoGainNode.connect(this.delayNode.delayTime)
this.lfoNode.start()
this.inputNode.connect(this.dryNode) const diffusionDelays = [5.3, 11.7, 19.3, 29.1]
this.inputNode.connect(this.delayNode) for (let i = 0; i < diffusionDelays.length; i++) {
this.delayNode.connect(this.saturatorNode) const allpass = audioContext.createBiquadFilter()
this.saturatorNode.connect(this.filterNode) allpass.type = 'allpass'
this.filterNode.connect(this.feedbackNode) allpass.frequency.value = 1000 / (diffusionDelays[i] / 1000)
this.feedbackNode.connect(this.delayNode) allpass.Q.value = 0.707
this.delayNode.connect(this.wetNode) this.diffusionNodes.push(allpass)
this.dryNode.connect(this.outputNode)
this.wetNode.connect(this.outputNode)
}
private createSaturationCurve(amount: number): void {
const samples = 2048
const curve = new Float32Array(samples)
const drive = 1 + amount * 9
for (let i = 0; i < samples; i++) {
const x = (i * 2) / samples - 1
curve[i] = Math.tanh(x * drive) / Math.tanh(drive)
} }
this.saturatorNode.curve = curve this.setupRouting()
this.saturatorNode.oversample = '2x' }
private setupRouting(): void {
this.inputNode.connect(this.dryNode)
this.dryNode.connect(this.outputNode)
this.inputNode.connect(this.splitterNode)
this.splitterNode.connect(this.leftDelayNode, 0)
this.splitterNode.connect(this.rightDelayNode, 1)
this.leftDelayNode.connect(this.filterNode)
this.filterNode.connect(this.dcBlockerNode)
this.dcBlockerNode.connect(this.diffusionMixNode)
this.diffusionMixNode.connect(this.diffusionDryNode)
this.diffusionMixNode.connect(this.diffusionNodes[0])
let lastNode: AudioNode = this.diffusionNodes[0]
for (let i = 1; i < this.diffusionNodes.length; i++) {
lastNode.connect(this.diffusionNodes[i])
lastNode = this.diffusionNodes[i]
}
lastNode.connect(this.diffusionWetNode)
this.diffusionDryNode.connect(this.leftFeedbackNode)
this.diffusionWetNode.connect(this.leftFeedbackNode)
this.leftFeedbackNode.connect(this.rightDelayNode)
this.rightDelayNode.connect(this.rightFeedbackNode)
this.rightFeedbackNode.connect(this.leftDelayNode)
this.diffusionDryNode.connect(this.mergerNode, 0, 0)
this.diffusionWetNode.connect(this.mergerNode, 0, 0)
this.rightDelayNode.connect(this.mergerNode, 0, 1)
this.mergerNode.connect(this.wetNode)
this.wetNode.connect(this.outputNode)
} }
getInputNode(): AudioNode { getInputNode(): AudioNode {
@ -86,27 +133,39 @@ export class DelayEffect implements Effect {
if (bypass) { if (bypass) {
this.wetNode.gain.value = 0 this.wetNode.gain.value = 0
this.dryNode.gain.value = 1 this.dryNode.gain.value = 1
this.leftFeedbackNode.disconnect()
this.rightFeedbackNode.disconnect()
} else { } else {
this.wetNode.gain.value = this.currentWetValue this.wetNode.gain.value = this.currentWetValue
this.dryNode.gain.value = this.currentDryValue this.dryNode.gain.value = this.currentDryValue
this.leftFeedbackNode.disconnect()
this.rightFeedbackNode.disconnect()
this.leftFeedbackNode.connect(this.rightDelayNode)
this.rightFeedbackNode.connect(this.leftDelayNode)
} }
} }
updateParams(values: Record<string, number | string>): void { updateParams(values: Record<string, number | string>): void {
const now = this.audioContext.currentTime
if (values.delayTime !== undefined && typeof values.delayTime === 'number') { if (values.delayTime !== undefined && typeof values.delayTime === 'number') {
const time = values.delayTime / 1000 const time = values.delayTime / 1000
this.delayNode.delayTime.setTargetAtTime( this.leftDelayNode.delayTime.setTargetAtTime(time, now, 0.01)
time, this.rightDelayNode.delayTime.setTargetAtTime(time, now, 0.01)
this.audioContext.currentTime,
0.01
)
} }
if (values.delayFeedback !== undefined && typeof values.delayFeedback === 'number') { if (values.delayFeedback !== undefined && typeof values.delayFeedback === 'number') {
const feedback = values.delayFeedback / 100 const feedback = values.delayFeedback / 100
this.feedbackNode.gain.setTargetAtTime( const pingPongFactor = Math.max(0.5, this.currentPingPong / 100)
feedback * 0.95,
this.audioContext.currentTime, this.leftFeedbackNode.gain.setTargetAtTime(
feedback * 0.95 * pingPongFactor,
now,
0.01
)
this.rightFeedbackNode.gain.setTargetAtTime(
feedback * 0.95 * pingPongFactor,
now,
0.01 0.01
) )
} }
@ -117,57 +176,51 @@ export class DelayEffect implements Effect {
this.currentDryValue = 1 - wet this.currentDryValue = 1 - wet
if (!this.bypassed) { if (!this.bypassed) {
this.wetNode.gain.setTargetAtTime( this.wetNode.gain.setTargetAtTime(this.currentWetValue, now, 0.01)
this.currentWetValue, this.dryNode.gain.setTargetAtTime(this.currentDryValue, now, 0.01)
this.audioContext.currentTime,
0.01
)
this.dryNode.gain.setTargetAtTime(
this.currentDryValue,
this.audioContext.currentTime,
0.01
)
} }
} }
if (values.delayTone !== undefined && typeof values.delayTone === 'number') { if (values.delayTone !== undefined && typeof values.delayTone === 'number') {
const tone = values.delayTone / 100 const tone = values.delayTone / 100
const freq = 200 + tone * 7800 const freq = 200 + tone * 7800
this.filterNode.frequency.setTargetAtTime( this.filterNode.frequency.setTargetAtTime(freq, now, 0.01)
freq,
this.audioContext.currentTime,
0.01
)
} }
if (values.delaySaturation !== undefined && typeof values.delaySaturation === 'number') { if (values.delayPingPong !== undefined && typeof values.delayPingPong === 'number') {
const saturation = values.delaySaturation / 100 this.currentPingPong = values.delayPingPong
this.createSaturationCurve(saturation)
} }
if (values.delayFlutter !== undefined && typeof values.delayFlutter === 'number') { if (values.delayDiffusion !== undefined && typeof values.delayDiffusion === 'number') {
const flutter = values.delayFlutter / 100 const diffusion = values.delayDiffusion / 100
const baseDelay = this.delayNode.delayTime.value this.diffusionWetNode.gain.setTargetAtTime(diffusion, now, 0.01)
const modDepth = baseDelay * flutter * 0.1 this.diffusionDryNode.gain.setTargetAtTime(1 - diffusion, now, 0.01)
this.lfoGainNode.gain.setTargetAtTime(
modDepth, for (const filter of this.diffusionNodes) {
this.audioContext.currentTime, filter.Q.setTargetAtTime(0.707 + diffusion * 4, now, 0.01)
0.01 }
)
} }
} }
dispose(): void { dispose(): void {
this.lfoNode.stop()
this.lfoNode.disconnect()
this.lfoGainNode.disconnect()
this.inputNode.disconnect() this.inputNode.disconnect()
this.outputNode.disconnect() this.outputNode.disconnect()
this.delayNode.disconnect()
this.feedbackNode.disconnect()
this.wetNode.disconnect()
this.dryNode.disconnect() this.dryNode.disconnect()
this.wetNode.disconnect()
this.leftDelayNode.disconnect()
this.rightDelayNode.disconnect()
this.leftFeedbackNode.disconnect()
this.rightFeedbackNode.disconnect()
this.filterNode.disconnect() this.filterNode.disconnect()
this.saturatorNode.disconnect() this.dcBlockerNode.disconnect()
this.splitterNode.disconnect()
this.mergerNode.disconnect()
this.diffusionMixNode.disconnect()
this.diffusionWetNode.disconnect()
this.diffusionDryNode.disconnect()
for (const filter of this.diffusionNodes) {
filter.disconnect()
}
} }
} }

View File

@ -12,6 +12,7 @@ export interface KeyboardShortcutHandlers {
onShiftR?: () => void onShiftR?: () => void
onC?: () => void onC?: () => void
onShiftC?: () => void onShiftC?: () => void
onI?: () => void
onEscape?: () => void onEscape?: () => void
} }
@ -92,6 +93,12 @@ export function useKeyboardShortcuts(handlers: KeyboardShortcutHandlers) {
} }
break break
case 'i':
case 'I':
e.preventDefault()
h.onI?.()
break
case 'Escape': case 'Escape':
e.preventDefault() e.preventDefault()
h.onEscape?.() h.onEscape?.()

View File

@ -1,11 +1,13 @@
import { useCallback } from 'react' import { useCallback, useRef } from 'react'
import type { TileState } from '../types/tiles' import type { TileState } from '../types/tiles'
import type { FocusedTile } from '../utils/tileHelpers' import type { FocusedTile } from '../utils/tileHelpers'
import { engineSettings, effectSettings } from '../stores/settings' import { engineSettings, effectSettings, lfoSettings } from '../stores/settings'
import { loadTileParams, saveTileParams, randomizeTileParams } from '../utils/tileState' import { loadTileParams, saveTileParams, randomizeTileParams } from '../utils/tileState'
import { getTileFromGrid } from '../utils/tileHelpers' import { getTileFromGrid } from '../utils/tileHelpers'
import { DEFAULT_VARIABLES } from '../constants/defaults' import { DEFAULT_VARIABLES } from '../constants/defaults'
import type { PlaybackManager } from '../services/PlaybackManager' import type { PlaybackManager } from '../services/PlaybackManager'
import { ENGINE_CONTROLS, EFFECTS } from '../config/parameters'
import type { LFOWaveform } from '../domain/modulation/LFO'
interface UseParameterSyncProps { interface UseParameterSyncProps {
tiles: TileState[][] tiles: TileState[][]
@ -28,6 +30,7 @@ export function useParameterSync({
playing, playing,
playbackId playbackId
}: UseParameterSyncProps) { }: UseParameterSyncProps) {
const interpolationRef = useRef<number | null>(null)
const saveCurrentParams = useCallback(() => { const saveCurrentParams = useCallback(() => {
if (focusedTile === 'custom') { if (focusedTile === 'custom') {
@ -201,12 +204,206 @@ export function useParameterSync({
} }
}, [playing, playbackId, setCustomTile, setTiles, focusedTile, playbackManager]) }, [playing, playbackId, setCustomTile, setTiles, focusedTile, playbackManager])
const interpolateParams = useCallback(() => {
if (interpolationRef.current !== null) {
return
}
const startTime = Date.now()
const currentEngine = engineSettings.get()
const currentEffect = effectSettings.get()
const currentLFOs = lfoSettings.get()
interface ParamTransition {
start: number
end: number
duration: number
paramId: string
paramType: 'engine' | 'effect' | 'lfo'
isBoolean?: boolean
isString?: boolean
isInteger?: boolean
lfoIndex?: number
}
const transitions: ParamTransition[] = []
EFFECTS.forEach(effect => {
if (effect.bypassable) {
const bypassKey = `${effect.id}Bypass`
const targetBypass = Math.random() > 0.5
setTimeout(() => {
handleEffectChange(bypassKey, targetBypass)
}, 500 + Math.random() * 4500)
}
})
const waveforms: LFOWaveform[] = ['sine', 'triangle', 'square', 'sawtooth']
const lfoKeys = ['lfo1', 'lfo2', 'lfo3', 'lfo4'] as const
lfoKeys.forEach((lfoKey, index) => {
const currentLFO = currentLFOs[lfoKey]
const targetFrequency = Math.random() * 10
transitions.push({
start: currentLFO.frequency,
end: targetFrequency,
duration: 500 + Math.random() * 4500,
paramId: 'frequency',
paramType: 'lfo',
lfoIndex: index
})
const targetWaveform = waveforms[Math.floor(Math.random() * waveforms.length)]
const targetPhase = Math.random() * 360
setTimeout(() => {
if (playbackManager.current) {
const updatedLFO = {
...currentLFO,
waveform: targetWaveform,
phase: targetPhase,
frequency: targetFrequency
}
lfoSettings.setKey(lfoKey, updatedLFO)
playbackManager.current.setLFOConfig(index, updatedLFO)
}
}, 500 + Math.random() * 4500)
})
ENGINE_CONTROLS.forEach(control => {
control.parameters.forEach(param => {
const currentValue = currentEngine[param.id as keyof typeof currentEngine] as number
let targetValue: number
const isIntegerParam = param.step === 1 && Number.isInteger(param.min as number)
if (param.id === 'sampleRate' || param.id === 'bitDepth') {
return
} else if (param.id === 'pitch') {
targetValue = 0.1 + Math.random() * 1.4
} else if (param.id === 'a' || param.id === 'b' || param.id === 'c' || param.id === 'd') {
targetValue = Math.floor(Math.random() * 256)
} else {
const range = (param.max as number) - (param.min as number)
targetValue = (param.min as number) + Math.random() * range
}
transitions.push({
start: currentValue,
end: targetValue,
duration: 500 + Math.random() * 4500,
paramId: param.id,
paramType: 'engine',
isInteger: isIntegerParam
})
})
})
EFFECTS.forEach(effect => {
effect.parameters.forEach(param => {
const currentValue = currentEffect[param.id as keyof typeof currentEffect]
const isIntegerParam = param.step === 1 && Number.isInteger(param.min as number)
if (param.options) {
const options = param.options
const randomOption = options[Math.floor(Math.random() * options.length)]
transitions.push({
start: 0,
end: 0,
duration: 500 + Math.random() * 4500,
paramId: param.id,
paramType: 'effect',
isString: true
})
setTimeout(() => {
handleEffectChange(param.id, randomOption.value)
}, 500 + Math.random() * 4500)
} else if (typeof param.default === 'boolean') {
const targetValue = Math.random() > 0.5
transitions.push({
start: 0,
end: 0,
duration: 500 + Math.random() * 4500,
paramId: param.id,
paramType: 'effect',
isBoolean: true
})
setTimeout(() => {
handleEffectChange(param.id, targetValue)
}, 500 + Math.random() * 4500)
} else {
const range = (param.max as number) - (param.min as number)
const targetValue = (param.min as number) + Math.random() * range
transitions.push({
start: currentValue as number,
end: targetValue,
duration: 500 + Math.random() * 4500,
paramId: param.id,
paramType: 'effect',
isInteger: isIntegerParam
})
}
})
})
const animate = () => {
const elapsed = Date.now() - startTime
let allComplete = true
transitions.forEach(transition => {
if (transition.isBoolean || transition.isString) {
return
}
const progress = Math.min(elapsed / transition.duration, 1)
const eased = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2
let currentValue = transition.start + (transition.end - transition.start) * eased
if (transition.isInteger) {
currentValue = Math.round(currentValue)
}
if (transition.paramType === 'engine') {
handleEngineChange(transition.paramId, currentValue)
} else if (transition.paramType === 'effect') {
handleEffectChange(transition.paramId, currentValue)
} else if (transition.paramType === 'lfo' && transition.lfoIndex !== undefined) {
if (playbackManager.current) {
const lfoKey = ['lfo1', 'lfo2', 'lfo3', 'lfo4'][transition.lfoIndex] as 'lfo1' | 'lfo2' | 'lfo3' | 'lfo4'
const currentLFO = lfoSettings.get()[lfoKey]
const updatedLFO = {
...currentLFO,
frequency: currentValue
}
lfoSettings.setKey(lfoKey, updatedLFO)
playbackManager.current.setLFOConfig(transition.lfoIndex, updatedLFO)
}
}
if (progress < 1) {
allComplete = false
}
})
if (!allComplete) {
interpolationRef.current = requestAnimationFrame(animate)
} else {
interpolationRef.current = null
}
}
interpolationRef.current = requestAnimationFrame(animate)
}, [handleEngineChange, handleEffectChange, playbackManager])
return { return {
saveCurrentParams, saveCurrentParams,
loadParams, loadParams,
handleEngineChange, handleEngineChange,
handleEffectChange, handleEffectChange,
randomizeParams, randomizeParams,
randomizeAllParams randomizeAllParams,
interpolateParams
} }
} }

View File

@ -26,7 +26,7 @@ export function useTileGrid() {
} }
}, [mode, engineValues.complexity]) }, [mode, engineValues.complexity])
const regenerateTile = useCallback((row: number, col: number) => { const regenerateTile = useCallback((row: number, col: number): TileState => {
let newTile: TileState let newTile: TileState
if (mode === 'fm') { if (mode === 'fm') {
@ -43,6 +43,8 @@ export function useTileGrid() {
newTiles[row][col] = newTile newTiles[row][col] = newTile
return newTiles return newTiles
}) })
return newTile
}, [mode, engineValues.complexity]) }, [mode, engineValues.complexity])
const switchMode = useCallback((newMode: SynthesisMode) => { const switchMode = useCallback((newMode: SynthesisMode) => {