Weird hybrid
This commit is contained in:
31
src/App.tsx
31
src/App.tsx
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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 || ''}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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: '%'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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?.()
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user