diff --git a/src/App.tsx b/src/App.tsx
index 53a48d63..ac0c882e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,6 @@
import { useState, useRef } from '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 { generateRandomFormula } from './utils/bytebeatFormulas'
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({
tiles,
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 = () => {
if (focusedTile !== 'custom') {
- regenerateTile(focusedTile.row, focusedTile.col)
+ handleRegenerate(focusedTile.row, focusedTile.col)
}
}
@@ -200,6 +209,7 @@ function App() {
onShiftR: handleRandom,
onC: handleKeyboardC,
onShiftC: randomizeAllParams,
+ onI: interpolateParams,
onEscape: exitMappingMode
})
@@ -296,6 +306,12 @@ function App() {
>
+
CHAOS
+
)
})
diff --git a/src/components/controls/EngineControls.tsx b/src/components/controls/EngineControls.tsx
index baba7712..8aeb343b 100644
--- a/src/components/controls/EngineControls.tsx
+++ b/src/components/controls/EngineControls.tsx
@@ -31,7 +31,8 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, sh
return getAlgorithmLabel(value)
default: {
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 || ''}`
}
}
}
diff --git a/src/components/modals/HelpModal.tsx b/src/components/modals/HelpModal.tsx
index 3d4bff9a..d64ccec8 100644
--- a/src/components/modals/HelpModal.tsx
+++ b/src/components/modals/HelpModal.tsx
@@ -77,6 +77,10 @@ export function HelpModal({ onClose, showStartButton = false }: HelpModalProps)
SHIFT + C |
Randomize all params (CHAOS) |
+
+ | I |
+ Interpolate params (MORPH) |
+
| ESC |
Exit mapping mode |
diff --git a/src/components/ui/Knob.tsx b/src/components/ui/Knob.tsx
index 56b6bff1..2d6305bc 100644
--- a/src/components/ui/Knob.tsx
+++ b/src/components/ui/Knob.tsx
@@ -38,7 +38,14 @@ export function Knob({
const startValueRef = useRef(0)
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 hasMappings = mappedLFOs.length > 0
diff --git a/src/components/ui/Slider.tsx b/src/components/ui/Slider.tsx
index 1f5eaa22..c9631a25 100644
--- a/src/components/ui/Slider.tsx
+++ b/src/components/ui/Slider.tsx
@@ -31,7 +31,15 @@ export function Slider({
mappedLFOs = []
}: SliderProps) {
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 hasMappings = mappedLFOs.length > 0
diff --git a/src/config/parameters.ts b/src/config/parameters.ts
index a01d903a..4d8d6e90 100644
--- a/src/config/parameters.ts
+++ b/src/config/parameters.ts
@@ -217,8 +217,8 @@ export const EFFECTS: ParameterGroup[] = [
id: 'delayTime',
label: 'Time',
min: 10,
- max: 2000,
- default: 250,
+ max: 10000,
+ default: 500,
step: 10,
unit: 'ms'
},
@@ -250,20 +250,20 @@ export const EFFECTS: ParameterGroup[] = [
unit: '%'
},
{
- id: 'delaySaturation',
- label: 'Saturation',
+ id: 'delayPingPong',
+ label: 'Ping-Pong',
min: 0,
max: 100,
- default: 20,
+ default: 0,
step: 1,
unit: '%'
},
{
- id: 'delayFlutter',
- label: 'Flutter',
+ id: 'delayDiffusion',
+ label: 'Diffusion',
min: 0,
max: 100,
- default: 15,
+ default: 0,
step: 1,
unit: '%'
}
diff --git a/src/domain/audio/effects/DelayEffect.ts b/src/domain/audio/effects/DelayEffect.ts
index e6674538..293eb1bc 100644
--- a/src/domain/audio/effects/DelayEffect.ts
+++ b/src/domain/audio/effects/DelayEffect.ts
@@ -6,71 +6,118 @@ export class DelayEffect implements Effect {
private audioContext: AudioContext
private inputNode: GainNode
private outputNode: GainNode
- private delayNode: DelayNode
- private feedbackNode: GainNode
- private wetNode: GainNode
private dryNode: GainNode
+ private wetNode: GainNode
+
+ private leftDelayNode: DelayNode
+ private rightDelayNode: DelayNode
+ private leftFeedbackNode: GainNode
+ private rightFeedbackNode: GainNode
+
private filterNode: BiquadFilterNode
- private saturatorNode: WaveShaperNode
- private lfoNode: OscillatorNode
- private lfoGainNode: GainNode
+ private dcBlockerNode: BiquadFilterNode
+
+ private splitterNode: ChannelSplitterNode
+ private mergerNode: ChannelMergerNode
+
+ private diffusionNodes: BiquadFilterNode[] = []
+ private diffusionMixNode: GainNode
+ private diffusionWetNode: GainNode
+ private diffusionDryNode: GainNode
+
private bypassed: boolean = false
private currentWetValue: number = 0.5
private currentDryValue: number = 0.5
+ private currentPingPong: number = 0
constructor(audioContext: AudioContext) {
this.audioContext = audioContext
+
this.inputNode = 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.filterNode = audioContext.createBiquadFilter()
- this.saturatorNode = audioContext.createWaveShaper()
+ this.wetNode = audioContext.createGain()
- 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.wetNode.gain.value = 0.5
- this.feedbackNode.gain.value = 0.5
this.filterNode.type = 'lowpass'
this.filterNode.frequency.value = 5000
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.lfoGainNode = audioContext.createGain()
- 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.diffusionWetNode.gain.value = 0
+ this.diffusionDryNode.gain.value = 1
- this.inputNode.connect(this.dryNode)
- this.inputNode.connect(this.delayNode)
- this.delayNode.connect(this.saturatorNode)
- this.saturatorNode.connect(this.filterNode)
- this.filterNode.connect(this.feedbackNode)
- this.feedbackNode.connect(this.delayNode)
- this.delayNode.connect(this.wetNode)
- 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)
+ const diffusionDelays = [5.3, 11.7, 19.3, 29.1]
+ for (let i = 0; i < diffusionDelays.length; i++) {
+ const allpass = audioContext.createBiquadFilter()
+ allpass.type = 'allpass'
+ allpass.frequency.value = 1000 / (diffusionDelays[i] / 1000)
+ allpass.Q.value = 0.707
+ this.diffusionNodes.push(allpass)
}
- this.saturatorNode.curve = curve
- this.saturatorNode.oversample = '2x'
+ this.setupRouting()
+ }
+
+ 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 {
@@ -86,27 +133,39 @@ export class DelayEffect implements Effect {
if (bypass) {
this.wetNode.gain.value = 0
this.dryNode.gain.value = 1
+ this.leftFeedbackNode.disconnect()
+ this.rightFeedbackNode.disconnect()
} else {
this.wetNode.gain.value = this.currentWetValue
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): void {
+ const now = this.audioContext.currentTime
+
if (values.delayTime !== undefined && typeof values.delayTime === 'number') {
const time = values.delayTime / 1000
- this.delayNode.delayTime.setTargetAtTime(
- time,
- this.audioContext.currentTime,
- 0.01
- )
+ this.leftDelayNode.delayTime.setTargetAtTime(time, now, 0.01)
+ this.rightDelayNode.delayTime.setTargetAtTime(time, now, 0.01)
}
if (values.delayFeedback !== undefined && typeof values.delayFeedback === 'number') {
const feedback = values.delayFeedback / 100
- this.feedbackNode.gain.setTargetAtTime(
- feedback * 0.95,
- this.audioContext.currentTime,
+ const pingPongFactor = Math.max(0.5, this.currentPingPong / 100)
+
+ this.leftFeedbackNode.gain.setTargetAtTime(
+ feedback * 0.95 * pingPongFactor,
+ now,
+ 0.01
+ )
+ this.rightFeedbackNode.gain.setTargetAtTime(
+ feedback * 0.95 * pingPongFactor,
+ now,
0.01
)
}
@@ -117,57 +176,51 @@ export class DelayEffect implements Effect {
this.currentDryValue = 1 - wet
if (!this.bypassed) {
- this.wetNode.gain.setTargetAtTime(
- this.currentWetValue,
- this.audioContext.currentTime,
- 0.01
- )
- this.dryNode.gain.setTargetAtTime(
- this.currentDryValue,
- this.audioContext.currentTime,
- 0.01
- )
+ this.wetNode.gain.setTargetAtTime(this.currentWetValue, now, 0.01)
+ this.dryNode.gain.setTargetAtTime(this.currentDryValue, now, 0.01)
}
}
if (values.delayTone !== undefined && typeof values.delayTone === 'number') {
const tone = values.delayTone / 100
const freq = 200 + tone * 7800
- this.filterNode.frequency.setTargetAtTime(
- freq,
- this.audioContext.currentTime,
- 0.01
- )
+ this.filterNode.frequency.setTargetAtTime(freq, now, 0.01)
}
- if (values.delaySaturation !== undefined && typeof values.delaySaturation === 'number') {
- const saturation = values.delaySaturation / 100
- this.createSaturationCurve(saturation)
+ if (values.delayPingPong !== undefined && typeof values.delayPingPong === 'number') {
+ this.currentPingPong = values.delayPingPong
}
- if (values.delayFlutter !== undefined && typeof values.delayFlutter === 'number') {
- const flutter = values.delayFlutter / 100
- const baseDelay = this.delayNode.delayTime.value
- const modDepth = baseDelay * flutter * 0.1
- this.lfoGainNode.gain.setTargetAtTime(
- modDepth,
- this.audioContext.currentTime,
- 0.01
- )
+ if (values.delayDiffusion !== undefined && typeof values.delayDiffusion === 'number') {
+ const diffusion = values.delayDiffusion / 100
+ this.diffusionWetNode.gain.setTargetAtTime(diffusion, now, 0.01)
+ this.diffusionDryNode.gain.setTargetAtTime(1 - diffusion, now, 0.01)
+
+ for (const filter of this.diffusionNodes) {
+ filter.Q.setTargetAtTime(0.707 + diffusion * 4, now, 0.01)
+ }
}
}
dispose(): void {
- this.lfoNode.stop()
- this.lfoNode.disconnect()
- this.lfoGainNode.disconnect()
this.inputNode.disconnect()
this.outputNode.disconnect()
- this.delayNode.disconnect()
- this.feedbackNode.disconnect()
- this.wetNode.disconnect()
this.dryNode.disconnect()
+ this.wetNode.disconnect()
+ this.leftDelayNode.disconnect()
+ this.rightDelayNode.disconnect()
+ this.leftFeedbackNode.disconnect()
+ this.rightFeedbackNode.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()
+ }
}
}
diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts
index 8b4cad8f..36d84cd9 100644
--- a/src/hooks/useKeyboardShortcuts.ts
+++ b/src/hooks/useKeyboardShortcuts.ts
@@ -12,6 +12,7 @@ export interface KeyboardShortcutHandlers {
onShiftR?: () => void
onC?: () => void
onShiftC?: () => void
+ onI?: () => void
onEscape?: () => void
}
@@ -92,6 +93,12 @@ export function useKeyboardShortcuts(handlers: KeyboardShortcutHandlers) {
}
break
+ case 'i':
+ case 'I':
+ e.preventDefault()
+ h.onI?.()
+ break
+
case 'Escape':
e.preventDefault()
h.onEscape?.()
diff --git a/src/hooks/useParameterSync.ts b/src/hooks/useParameterSync.ts
index cabecdb3..62f4a592 100644
--- a/src/hooks/useParameterSync.ts
+++ b/src/hooks/useParameterSync.ts
@@ -1,11 +1,13 @@
-import { useCallback } from 'react'
+import { useCallback, useRef } from 'react'
import type { TileState } from '../types/tiles'
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 { getTileFromGrid } from '../utils/tileHelpers'
import { DEFAULT_VARIABLES } from '../constants/defaults'
import type { PlaybackManager } from '../services/PlaybackManager'
+import { ENGINE_CONTROLS, EFFECTS } from '../config/parameters'
+import type { LFOWaveform } from '../domain/modulation/LFO'
interface UseParameterSyncProps {
tiles: TileState[][]
@@ -28,6 +30,7 @@ export function useParameterSync({
playing,
playbackId
}: UseParameterSyncProps) {
+ const interpolationRef = useRef(null)
const saveCurrentParams = useCallback(() => {
if (focusedTile === 'custom') {
@@ -201,12 +204,206 @@ export function useParameterSync({
}
}, [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 {
saveCurrentParams,
loadParams,
handleEngineChange,
handleEffectChange,
randomizeParams,
- randomizeAllParams
+ randomizeAllParams,
+ interpolateParams
}
}
diff --git a/src/hooks/useTileGrid.ts b/src/hooks/useTileGrid.ts
index 3ec50a1f..9f200044 100644
--- a/src/hooks/useTileGrid.ts
+++ b/src/hooks/useTileGrid.ts
@@ -26,7 +26,7 @@ export function useTileGrid() {
}
}, [mode, engineValues.complexity])
- const regenerateTile = useCallback((row: number, col: number) => {
+ const regenerateTile = useCallback((row: number, col: number): TileState => {
let newTile: TileState
if (mode === 'fm') {
@@ -43,6 +43,8 @@ export function useTileGrid() {
newTiles[row][col] = newTile
return newTiles
})
+
+ return newTile
}, [mode, engineValues.complexity])
const switchMode = useCallback((newMode: SynthesisMode) => {