Lightningbeam/src/nodeTypes.js

1510 lines
60 KiB
JavaScript

// Node type definitions for the audio node graph editor
// Each node type defines its inputs, outputs, parameters, and HTML template
/**
* Signal types for node ports
* These match the backend SignalType enum
*/
export const SignalType = {
AUDIO: 'audio', // Blue circles
MIDI: 'midi', // Green squares
CV: 'cv' // Orange diamonds
};
/**
* Node category for organization in the palette
*/
export const NodeCategory = {
INPUT: 'input',
GENERATOR: 'generator',
EFFECT: 'effect',
UTILITY: 'utility',
OUTPUT: 'output'
};
/**
* Get CSS class for a port based on its signal type
*/
export function getPortClass(signalType) {
return `connector-${signalType}`;
}
/**
* Node type catalog
* Maps node type names to their definitions
*/
export const nodeTypes = {
Oscillator: {
name: 'Oscillator',
category: NodeCategory.GENERATOR,
description: 'Oscillator with multiple waveforms and CV modulation',
inputs: [
{ name: 'V/Oct', type: SignalType.CV, index: 0 },
{ name: 'FM', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'Audio', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'frequency', label: 'Frequency', min: 20, max: 20000, default: 440, unit: 'Hz' },
{ id: 1, name: 'amplitude', label: 'Amplitude', min: 0, max: 1, default: 0.5, unit: '' },
{ id: 2, name: 'waveform', label: 'Waveform', min: 0, max: 3, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Oscillator</div>
<div class="node-param">
<label>Waveform: <span id="wave-${nodeId}">Sine</span></label>
<input type="range"
data-node="${nodeId}"
data-param="2"
min="0"
max="3"
value="0"
step="1">
</div>
<div class="node-param">
<label>Frequency: <span id="freq-${nodeId}">440</span> Hz</label>
<input type="range"
data-node="${nodeId}"
data-param="0"
min="20"
max="20000"
value="440"
step="1">
</div>
<div class="node-param">
<label>Amplitude: <span id="amp-${nodeId}">0.5</span></label>
<input type="range"
data-node="${nodeId}"
data-param="1"
min="0"
max="1"
value="0.5"
step="0.01">
</div>
</div>
`
},
Gain: {
name: 'Gain',
category: NodeCategory.UTILITY,
description: 'VCA (voltage-controlled amplifier) - CV multiplies gain',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 },
{ name: 'CV', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'gain', label: 'Gain', min: 0, max: 2, default: 1, unit: 'x' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Gain</div>
<div class="node-param">
<label>Gain: <span id="gain-${nodeId}">1.0</span>x</label>
<input type="range"
data-node="${nodeId}"
data-param="0"
min="0"
max="2"
value="1"
step="0.01">
</div>
</div>
`
},
Mixer: {
name: 'Mixer',
category: NodeCategory.UTILITY,
description: 'Mix up to 4 audio inputs with independent gain controls',
inputs: [
{ name: 'Input 1', type: SignalType.AUDIO, index: 0 },
{ name: 'Input 2', type: SignalType.AUDIO, index: 1 },
{ name: 'Input 3', type: SignalType.AUDIO, index: 2 },
{ name: 'Input 4', type: SignalType.AUDIO, index: 3 }
],
outputs: [
{ name: 'Mixed Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'gain1', label: 'Gain 1', min: 0, max: 2, default: 1, unit: 'x' },
{ id: 1, name: 'gain2', label: 'Gain 2', min: 0, max: 2, default: 1, unit: 'x' },
{ id: 2, name: 'gain3', label: 'Gain 3', min: 0, max: 2, default: 1, unit: 'x' },
{ id: 3, name: 'gain4', label: 'Gain 4', min: 0, max: 2, default: 1, unit: 'x' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Mixer</div>
<div class="node-param">
<label>Gain 1: <span id="g1-${nodeId}">1.0</span>x</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0" max="2" value="1" step="0.01">
</div>
<div class="node-param">
<label>Gain 2: <span id="g2-${nodeId}">1.0</span>x</label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="2" value="1" step="0.01">
</div>
<div class="node-param">
<label>Gain 3: <span id="g3-${nodeId}">1.0</span>x</label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="2" value="1" step="0.01">
</div>
<div class="node-param">
<label>Gain 4: <span id="g4-${nodeId}">1.0</span>x</label>
<input type="range" data-node="${nodeId}" data-param="3" min="0" max="2" value="1" step="0.01">
</div>
</div>
`
},
Filter: {
name: 'Filter',
category: NodeCategory.EFFECT,
description: 'Biquad filter with lowpass/highpass modes',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 },
{ name: 'Cutoff CV', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'cutoff', label: 'Cutoff', min: 20, max: 20000, default: 1000, unit: 'Hz' },
{ id: 1, name: 'resonance', label: 'Resonance', min: 0.1, max: 10, default: 0.707, unit: 'Q' },
{ id: 2, name: 'type', label: 'Type', min: 0, max: 1, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Filter</div>
<div class="node-param">
<label>Cutoff: <span id="cutoff-${nodeId}">1000</span> Hz</label>
<input type="range" data-node="${nodeId}" data-param="0" min="20" max="20000" value="1000" step="1">
</div>
<div class="node-param">
<label>Resonance: <span id="res-${nodeId}">0.707</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.1" max="10" value="0.707" step="0.01">
</div>
<div class="node-param">
<label>Type: <span id="ftype-${nodeId}">LP</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="0" step="1">
</div>
</div>
`
},
ADSR: {
name: 'ADSR',
category: NodeCategory.UTILITY,
description: 'Attack-Decay-Sustain-Release envelope generator',
inputs: [
{ name: 'Gate', type: SignalType.CV, index: 0 }
],
outputs: [
{ name: 'Envelope', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'attack', label: 'Attack', min: 0.001, max: 5, default: 0.01, unit: 's' },
{ id: 1, name: 'decay', label: 'Decay', min: 0.001, max: 5, default: 0.1, unit: 's' },
{ id: 2, name: 'sustain', label: 'Sustain', min: 0, max: 1, default: 0.7, unit: '' },
{ id: 3, name: 'release', label: 'Release', min: 0.001, max: 5, default: 0.2, unit: 's' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">ADSR</div>
<div class="node-param">
<label>A: <span id="a-${nodeId}">0.01</span>s</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.001" max="5" value="0.01" step="0.001">
</div>
<div class="node-param">
<label>D: <span id="d-${nodeId}">0.1</span>s</label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.001" max="5" value="0.1" step="0.001">
</div>
<div class="node-param">
<label>S: <span id="s-${nodeId}">0.7</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="0.7" step="0.01">
</div>
<div class="node-param">
<label>R: <span id="r-${nodeId}">0.2</span>s</label>
<input type="range" data-node="${nodeId}" data-param="3" min="0.001" max="5" value="0.2" step="0.001">
</div>
</div>
`
},
MidiInput: {
name: 'MidiInput',
category: NodeCategory.INPUT,
description: 'MIDI input - receives MIDI events from track',
inputs: [],
outputs: [
{ name: 'MIDI Out', type: SignalType.MIDI, index: 0 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">MIDI Input</div>
<div class="node-info">Receives MIDI from track</div>
</div>
`
},
MidiToCV: {
name: 'MidiToCV',
category: NodeCategory.UTILITY,
description: 'Convert MIDI notes to CV signals',
inputs: [
{ name: 'MIDI In', type: SignalType.MIDI, index: 0 }
],
outputs: [
{ name: 'V/Oct', type: SignalType.CV, index: 0 },
{ name: 'Gate', type: SignalType.CV, index: 1 },
{ name: 'Velocity', type: SignalType.CV, index: 2 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">MIDI→CV</div>
<div class="node-info">Converts MIDI to CV signals</div>
</div>
`
},
AudioToCV: {
name: 'AudioToCV',
category: NodeCategory.UTILITY,
description: 'Envelope follower - converts audio amplitude to CV',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'attack', label: 'Attack', min: 0.001, max: 1.0, default: 0.01, unit: 's' },
{ id: 1, name: 'release', label: 'Release', min: 0.001, max: 1.0, default: 0.1, unit: 's' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Audio→CV</div>
<div class="node-param">
<label>Attack: <span id="att-${nodeId}">0.01</span>s</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.001" max="1.0" value="0.01" step="0.001">
</div>
<div class="node-param">
<label>Release: <span id="rel-${nodeId}">0.1</span>s</label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.001" max="1.0" value="0.1" step="0.001">
</div>
</div>
`
},
Oscilloscope: {
name: 'Oscilloscope',
category: NodeCategory.UTILITY,
description: 'Visual audio signal monitor (pass-through)',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 },
{ name: 'V/oct', type: SignalType.CV, index: 1 },
{ name: 'CV In', type: SignalType.CV, index: 2 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'time_scale', label: 'Time Scale', min: 10, max: 1000, default: 100, unit: 'ms' },
{ id: 1, name: 'trigger_mode', label: 'Trigger', min: 0, max: 3, default: 0, unit: '' },
{ id: 2, name: 'trigger_level', label: 'Trigger Level', min: -1, max: 1, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Oscilloscope</div>
<canvas id="oscilloscope-canvas-${nodeId}" width="200" height="80" style="width: 200px; height: 80px; background: #1a1a1a; border: 1px solid #444; border-radius: 2px; display: block; margin: 4px 0;"></canvas>
<div class="node-param">
<label>Time: <span id="time_scale-${nodeId}">100</span>ms</label>
<input type="range" class="node-slider" data-node="${nodeId}" data-param="0" min="10" max="1000" value="100" step="10">
</div>
<div class="node-param">
<label>Trigger: <span id="trigger_mode-${nodeId}">Free</span></label>
<input type="range" class="node-slider" data-node="${nodeId}" data-param="1" min="0" max="3" value="0" step="1">
</div>
</div>
`
},
VoiceAllocator: {
name: 'VoiceAllocator',
category: NodeCategory.UTILITY,
description: 'Polyphonic voice allocator - creates N instances of internal graph',
inputs: [
{ name: 'MIDI In', type: SignalType.MIDI, index: 0 }
],
outputs: [
{ name: 'Mixed Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'voices', label: 'Voices', min: 1, max: 16, default: 8, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="voice-allocator-header">
<div class="node-title">Voice Allocator</div>
<div class="node-param">
<label>Voices: <span id="voices-${nodeId}">8</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="1" max="16" value="8" step="1">
</div>
<div class="node-info" style="margin-top: 4px; font-size: 10px;">Double-click to edit</div>
</div>
<div class="voice-allocator-contents" id="voice-allocator-contents-${nodeId}" style="display: none;">
<div class="node-info" style="font-size: 10px; color: #aaa; margin-bottom: 8px;">Build a synth voice template:</div>
<div class="node-info" style="font-size: 9px; color: #c77dff;">Purple nodes are Template Input/Output (non-deletable)</div>
<div class="node-info" style="font-size: 9px; color: #888;">Connect MIDI from Template Input → MidiToCV</div>
<div class="node-info" style="font-size: 9px; color: #888;">Add synth nodes: Oscillator, ADSR, Gain, etc.</div>
<div class="node-info" style="font-size: 9px; color: #888;">Connect final audio → Template Output</div>
<div class="node-info" style="font-size: 9px; color: #666; margin-top: 8px;">Drag nodes from palette • Container auto-resizes</div>
</div>
</div>
`
},
AudioInput: {
name: 'AudioInput',
category: NodeCategory.INPUT,
description: 'Audio track clip input - receives audio from timeline clips',
inputs: [],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Audio Input</div>
<div class="node-info">Audio from clips</div>
</div>
`
},
AudioOutput: {
name: 'AudioOutput',
category: NodeCategory.OUTPUT,
description: 'Final audio output',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Audio Output</div>
<div class="node-info">Final output to speakers</div>
</div>
`
},
TemplateInput: {
name: 'TemplateInput',
category: NodeCategory.INPUT,
description: 'VoiceAllocator template input - receives MIDI for one voice',
inputs: [],
outputs: [
{ name: 'MIDI Out', type: SignalType.MIDI, index: 0 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Template Input</div>
<div class="node-info" style="font-size: 9px;">MIDI for one voice</div>
</div>
`
},
TemplateOutput: {
name: 'TemplateOutput',
category: NodeCategory.OUTPUT,
description: 'VoiceAllocator template output - sends audio to voice mixer',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Template Output</div>
<div class="node-info" style="font-size: 9px;">Audio to mixer</div>
</div>
`
},
LFO: {
name: 'LFO',
category: NodeCategory.UTILITY,
description: 'Low frequency oscillator for modulation',
inputs: [],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'frequency', label: 'Frequency', min: 0.01, max: 20, default: 1.0, unit: 'Hz' },
{ id: 1, name: 'amplitude', label: 'Amplitude', min: 0, max: 1, default: 1.0, unit: '' },
{ id: 2, name: 'waveform', label: 'Waveform', min: 0, max: 4, default: 0, unit: '' },
{ id: 3, name: 'phase', label: 'Phase', min: 0, max: 1, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">LFO</div>
<div class="node-param">
<label>Wave: <span id="lfowave-${nodeId}">Sine</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="4" value="0" step="1">
</div>
<div class="node-param">
<label>Freq: <span id="lfofreq-${nodeId}">1.0</span> Hz</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.01" max="20" value="1.0" step="0.01">
</div>
<div class="node-param">
<label>Depth: <span id="lfoamp-${nodeId}">1.0</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="1" value="1.0" step="0.01">
</div>
</div>
`
},
NoiseGenerator: {
name: 'NoiseGenerator',
category: NodeCategory.GENERATOR,
description: 'White and pink noise generator',
inputs: [],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'amplitude', label: 'Amplitude', min: 0, max: 1, default: 0.5, unit: '' },
{ id: 1, name: 'color', label: 'Color', min: 0, max: 1, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Noise</div>
<div class="node-param">
<label>Color: <span id="noisecolor-${nodeId}">White</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="1" value="0" step="1">
</div>
<div class="node-param">
<label>Level: <span id="noiselevel-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="0" max="1" value="0.5" step="0.01">
</div>
</div>
`
},
Splitter: {
name: 'Splitter',
category: NodeCategory.UTILITY,
description: 'Split audio signal to multiple outputs for parallel routing',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Out 1', type: SignalType.AUDIO, index: 0 },
{ name: 'Out 2', type: SignalType.AUDIO, index: 1 },
{ name: 'Out 3', type: SignalType.AUDIO, index: 2 },
{ name: 'Out 4', type: SignalType.AUDIO, index: 3 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Splitter</div>
<div class="node-info" style="font-size: 10px;">1→4 split</div>
</div>
`
},
Pan: {
name: 'Pan',
category: NodeCategory.UTILITY,
description: 'Stereo panning with CV modulation',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 },
{ name: 'Pan CV', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'pan', label: 'Pan', min: -1, max: 1, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Pan</div>
<div class="node-param">
<label>Position: <span id="panpos-${nodeId}">0.0</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="-1" max="1" value="0" step="0.01">
</div>
</div>
`
},
Delay: {
name: 'Delay',
category: NodeCategory.EFFECT,
description: 'Stereo delay with feedback',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'delay_time', label: 'Delay Time', min: 0.001, max: 2.0, default: 0.5, unit: 's' },
{ id: 1, name: 'feedback', label: 'Feedback', min: 0, max: 0.95, default: 0.5, unit: '' },
{ id: 2, name: 'wet_dry', label: 'Wet/Dry', min: 0, max: 1, default: 0.5, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Delay</div>
<div class="node-param">
<label>Time: <span id="delaytime-${nodeId}">0.5</span>s</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.001" max="2" value="0.5" step="0.001">
</div>
<div class="node-param">
<label>Feedback: <span id="feedback-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="0.95" value="0.5" step="0.01">
</div>
<div class="node-param">
<label>Wet/Dry: <span id="wetdry-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="0.5" step="0.01">
</div>
</div>
`
},
Reverb: {
name: 'Reverb',
category: NodeCategory.EFFECT,
description: 'Schroeder reverb with room size and damping',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'room_size', label: 'Room Size', min: 0, max: 1, default: 0.5, unit: '' },
{ id: 1, name: 'damping', label: 'Damping', min: 0, max: 1, default: 0.5, unit: '' },
{ id: 2, name: 'wet_dry', label: 'Wet/Dry', min: 0, max: 1, default: 0.3, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Reverb</div>
<div class="node-param">
<label>Room Size: <span id="roomsize-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="0" max="1" value="0.5" step="0.01">
</div>
<div class="node-param">
<label>Damping: <span id="damping-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="1" value="0.5" step="0.01">
</div>
<div class="node-param">
<label>Wet/Dry: <span id="wetdry-${nodeId}">0.3</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="0.3" step="0.01">
</div>
</div>
`
},
Chorus: {
name: 'Chorus',
category: NodeCategory.EFFECT,
description: 'Chorus effect with modulated delay',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'rate', label: 'Rate', min: 0.1, max: 5.0, default: 1.0, unit: 'Hz' },
{ id: 1, name: 'depth', label: 'Depth', min: 0, max: 1, default: 0.5, unit: '' },
{ id: 2, name: 'wet_dry', label: 'Wet/Dry', min: 0, max: 1, default: 0.5, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Chorus</div>
<div class="node-param">
<label>Rate: <span id="chorusrate-${nodeId}">1.0</span>Hz</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.1" max="5" value="1.0" step="0.1">
</div>
<div class="node-param">
<label>Depth: <span id="chorusdepth-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="1" value="0.5" step="0.01">
</div>
<div class="node-param">
<label>Wet/Dry: <span id="choruswetdry-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="0.5" step="0.01">
</div>
</div>
`
},
Flanger: {
name: 'Flanger',
category: NodeCategory.EFFECT,
description: 'Flanger effect with feedback',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'rate', label: 'Rate', min: 0.1, max: 10.0, default: 0.5, unit: 'Hz' },
{ id: 1, name: 'depth', label: 'Depth', min: 0, max: 1, default: 0.7, unit: '' },
{ id: 2, name: 'feedback', label: 'Feedback', min: -0.95, max: 0.95, default: 0.5, unit: '' },
{ id: 3, name: 'wet_dry', label: 'Wet/Dry', min: 0, max: 1, default: 0.5, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Flanger</div>
<div class="node-param">
<label>Rate: <span id="flangerrate-${nodeId}">0.5</span>Hz</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.1" max="10" value="0.5" step="0.1">
</div>
<div class="node-param">
<label>Depth: <span id="flangerdepth-${nodeId}">0.7</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="1" value="0.7" step="0.01">
</div>
<div class="node-param">
<label>Feedback: <span id="flangerfeedback-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="-0.95" max="0.95" value="0.5" step="0.01">
</div>
<div class="node-param">
<label>Wet/Dry: <span id="flangerwetdry-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="3" min="0" max="1" value="0.5" step="0.01">
</div>
</div>
`
},
FMSynth: {
name: 'FM Synth',
category: NodeCategory.GENERATOR,
description: '4-operator FM synthesizer',
inputs: [
{ name: 'V/Oct', type: SignalType.CV, index: 0 },
{ name: 'Gate', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'algorithm', label: 'Algorithm', min: 0, max: 3, default: 0, unit: '' },
{ id: 1, name: 'op1_ratio', label: 'Op1 Ratio', min: 0.25, max: 16, default: 1.0, unit: '' },
{ id: 2, name: 'op1_level', label: 'Op1 Level', min: 0, max: 1, default: 1.0, unit: '' },
{ id: 3, name: 'op2_ratio', label: 'Op2 Ratio', min: 0.25, max: 16, default: 2.0, unit: '' },
{ id: 4, name: 'op2_level', label: 'Op2 Level', min: 0, max: 1, default: 0.8, unit: '' },
{ id: 5, name: 'op3_ratio', label: 'Op3 Ratio', min: 0.25, max: 16, default: 3.0, unit: '' },
{ id: 6, name: 'op3_level', label: 'Op3 Level', min: 0, max: 1, default: 0.6, unit: '' },
{ id: 7, name: 'op4_ratio', label: 'Op4 Ratio', min: 0.25, max: 16, default: 4.0, unit: '' },
{ id: 8, name: 'op4_level', label: 'Op4 Level', min: 0, max: 1, default: 0.4, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">FM Synth</div>
<div class="node-param">
<label>Algorithm: <span id="fmalgo-${nodeId}">0</span></label>
<select data-node="${nodeId}" data-param="0" style="width: 100%; padding: 2px;">
<option value="0">Stack (1→2→3→4)</option>
<option value="1">Parallel</option>
<option value="2">Bell (1→2, 3→4)</option>
<option value="3">Dual (1→2, 3→4)</option>
</select>
</div>
<div style="font-size: 10px; margin-top: 4px; font-weight: bold;">Operator 1</div>
<div class="node-param">
<label>Ratio: <span id="op1ratio-${nodeId}">1.0</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.25" max="16" value="1.0" step="0.25">
</div>
<div class="node-param">
<label>Level: <span id="op1level-${nodeId}">1.0</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="1.0" step="0.01">
</div>
<div style="font-size: 10px; margin-top: 4px; font-weight: bold;">Operator 2</div>
<div class="node-param">
<label>Ratio: <span id="op2ratio-${nodeId}">2.0</span></label>
<input type="range" data-node="${nodeId}" data-param="3" min="0.25" max="16" value="2.0" step="0.25">
</div>
<div class="node-param">
<label>Level: <span id="op2level-${nodeId}">0.8</span></label>
<input type="range" data-node="${nodeId}" data-param="4" min="0" max="1" value="0.8" step="0.01">
</div>
<div style="font-size: 10px; margin-top: 4px; font-weight: bold;">Operator 3</div>
<div class="node-param">
<label>Ratio: <span id="op3ratio-${nodeId}">3.0</span></label>
<input type="range" data-node="${nodeId}" data-param="5" min="0.25" max="16" value="3.0" step="0.25">
</div>
<div class="node-param">
<label>Level: <span id="op3level-${nodeId}">0.6</span></label>
<input type="range" data-node="${nodeId}" data-param="6" min="0" max="1" value="0.6" step="0.01">
</div>
<div style="font-size: 10px; margin-top: 4px; font-weight: bold;">Operator 4</div>
<div class="node-param">
<label>Ratio: <span id="op4ratio-${nodeId}">4.0</span></label>
<input type="range" data-node="${nodeId}" data-param="7" min="0.25" max="16" value="4.0" step="0.25">
</div>
<div class="node-param">
<label>Level: <span id="op4level-${nodeId}">0.4</span></label>
<input type="range" data-node="${nodeId}" data-param="8" min="0" max="1" value="0.4" step="0.01">
</div>
</div>
`
},
WavetableOscillator: {
name: 'Wavetable',
category: NodeCategory.GENERATOR,
description: 'Wavetable oscillator with preset waveforms',
inputs: [
{ name: 'V/Oct', type: SignalType.CV, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'wavetable', label: 'Wavetable', min: 0, max: 7, default: 0, unit: '' },
{ id: 1, name: 'fine_tune', label: 'Fine Tune', min: -1, max: 1, default: 0, unit: '' },
{ id: 2, name: 'position', label: 'Position', min: 0, max: 1, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Wavetable</div>
<div class="node-param">
<label>Waveform: <span id="wavetable-${nodeId}">Sine</span></label>
<select data-node="${nodeId}" data-param="0" style="width: 100%; padding: 2px;">
<option value="0">Sine</option>
<option value="1">Saw</option>
<option value="2">Square</option>
<option value="3">Triangle</option>
<option value="4">PWM</option>
<option value="5">Harmonic</option>
<option value="6">Inharmonic</option>
<option value="7">Digital</option>
</select>
</div>
<div class="node-param">
<label>Fine: <span id="finetune-${nodeId}">0.00</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="-1" max="1" value="0" step="0.01">
</div>
<div class="node-param">
<label>Position: <span id="position-${nodeId}">0.00</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="0" step="0.01">
</div>
</div>
`
},
SimpleSampler: {
name: 'Sampler',
category: NodeCategory.GENERATOR,
description: 'Simple sample playback with pitch shifting',
inputs: [
{ name: 'V/Oct', type: SignalType.CV, index: 0 },
{ name: 'Gate', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'gain', label: 'Gain', min: 0, max: 2, default: 1.0, unit: '' },
{ id: 1, name: 'loop', label: 'Loop', min: 0, max: 1, default: 0, unit: '' },
{ id: 2, name: 'pitch_shift', label: 'Pitch Shift', min: -12, max: 12, default: 0, unit: 'semi' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Sampler</div>
<div class="node-param">
<label>Gain: <span id="gain-${nodeId}">1.00</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="0" max="2" value="1.0" step="0.01">
</div>
<div class="node-param">
<label>Loop: <span id="loop-${nodeId}">Off</span></label>
<input type="checkbox" class="node-checkbox" data-node="${nodeId}" data-param="1">
</div>
<div class="node-param">
<label>Pitch: <span id="pitch-${nodeId}">0</span> semi</label>
<input type="range" data-node="${nodeId}" data-param="2" min="-12" max="12" value="0" step="1">
</div>
<div class="node-param" style="margin-top: 4px;">
<button class="load-sample-btn" data-node="${nodeId}" style="width: 100%; padding: 4px; font-size: 10px;">Load Sample</button>
</div>
<div id="sample-info-${nodeId}" style="font-size: 9px; color: #888; margin-top: 2px; text-align: center;">No sample loaded</div>
</div>
`
},
MultiSampler: {
name: 'Multi Sampler',
category: NodeCategory.GENERATOR,
description: 'Multi-sample instrument with velocity layers and key zones',
inputs: [
{ name: 'MIDI In', type: SignalType.MIDI, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'gain', label: 'Gain', min: 0, max: 2, default: 1.0, unit: '' },
{ id: 1, name: 'attack', label: 'Attack', min: 0.001, max: 1, default: 0.01, unit: 's' },
{ id: 2, name: 'release', label: 'Release', min: 0.01, max: 5, default: 0.1, unit: 's' },
{ id: 3, name: 'transpose', label: 'Transpose', min: -24, max: 24, default: 0, unit: 'semi' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Multi Sampler</div>
<div class="node-param">
<label>Gain: <span id="gain-${nodeId}">1.00</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="0" max="2" value="1.0" step="0.01">
</div>
<div class="node-param">
<label>Attack: <span id="attack-${nodeId}">0.01</span>s</label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.001" max="1" value="0.01" step="0.001">
</div>
<div class="node-param">
<label>Release: <span id="release-${nodeId}">0.10</span>s</label>
<input type="range" data-node="${nodeId}" data-param="2" min="0.01" max="5" value="0.1" step="0.01">
</div>
<div class="node-param">
<label>Transpose: <span id="transpose-${nodeId}">0</span> semi</label>
<input type="range" data-node="${nodeId}" data-param="3" min="-24" max="24" value="0" step="1">
</div>
<div class="node-param" style="margin-top: 4px;">
<button class="add-layer-btn" data-node="${nodeId}" style="width: 100%; padding: 4px; font-size: 10px;">Add Sample Layer</button>
</div>
<div id="sample-layers-container-${nodeId}" class="sample-layers-container">
<table id="sample-layers-table-${nodeId}" class="sample-layers-table">
<thead>
<tr>
<th>File</th>
<th>Range</th>
<th>Root</th>
<th>Vel</th>
<th></th>
</tr>
</thead>
<tbody id="sample-layers-list-${nodeId}">
<tr><td colspan="5" class="sample-layers-empty">No layers loaded</td></tr>
</tbody>
</table>
</div>
</div>
`
},
Compressor: {
name: 'Compressor',
category: NodeCategory.EFFECT,
description: 'Dynamic range compressor with soft-knee',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'threshold', label: 'Threshold', min: -60, max: 0, default: -20, unit: 'dB' },
{ id: 1, name: 'ratio', label: 'Ratio', min: 1, max: 20, default: 4, unit: ':1' },
{ id: 2, name: 'attack', label: 'Attack', min: 0.1, max: 100, default: 5, unit: 'ms' },
{ id: 3, name: 'release', label: 'Release', min: 10, max: 1000, default: 100, unit: 'ms' },
{ id: 4, name: 'makeup_gain', label: 'Makeup Gain', min: 0, max: 20, default: 0, unit: 'dB' },
{ id: 5, name: 'knee', label: 'Knee', min: 0, max: 12, default: 6, unit: 'dB' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Compressor</div>
<div class="node-param">
<label>Threshold: <span id="threshold-${nodeId}">-20</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="0" min="-60" max="0" value="-20" step="0.1">
</div>
<div class="node-param">
<label>Ratio: <span id="ratio-${nodeId}">4.0</span>:1</label>
<input type="range" data-node="${nodeId}" data-param="1" min="1" max="20" value="4" step="0.1">
</div>
<div class="node-param">
<label>Attack: <span id="attack-${nodeId}">5</span> ms</label>
<input type="range" data-node="${nodeId}" data-param="2" min="0.1" max="100" value="5" step="0.1">
</div>
<div class="node-param">
<label>Release: <span id="release-${nodeId}">100</span> ms</label>
<input type="range" data-node="${nodeId}" data-param="3" min="10" max="1000" value="100" step="1">
</div>
<div class="node-param">
<label>Makeup: <span id="makeup-${nodeId}">0</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="4" min="0" max="20" value="0" step="0.1">
</div>
<div class="node-param">
<label>Knee: <span id="knee-${nodeId}">6</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="5" min="0" max="12" value="6" step="0.1">
</div>
</div>
`
},
Limiter: {
name: 'Limiter',
category: NodeCategory.EFFECT,
description: 'Peak limiter with ceiling control',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'threshold', label: 'Threshold', min: -60, max: 0, default: -10, unit: 'dB' },
{ id: 1, name: 'release', label: 'Release', min: 10, max: 1000, default: 50, unit: 'ms' },
{ id: 2, name: 'ceiling', label: 'Ceiling', min: -20, max: 0, default: 0, unit: 'dB' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Limiter</div>
<div class="node-param">
<label>Threshold: <span id="limthreshold-${nodeId}">-10</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="0" min="-60" max="0" value="-10" step="0.1">
</div>
<div class="node-param">
<label>Release: <span id="limrelease-${nodeId}">50</span> ms</label>
<input type="range" data-node="${nodeId}" data-param="1" min="10" max="1000" value="50" step="1">
</div>
<div class="node-param">
<label>Ceiling: <span id="ceiling-${nodeId}">0</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="2" min="-20" max="0" value="0" step="0.1">
</div>
</div>
`
},
Distortion: {
name: 'Distortion',
category: NodeCategory.EFFECT,
description: 'Waveshaping distortion with multiple algorithms',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'drive', label: 'Drive', min: 0.01, max: 20, default: 1, unit: '' },
{ id: 1, name: 'type', label: 'Type', min: 0, max: 3, default: 0, unit: '' },
{ id: 2, name: 'tone', label: 'Tone', min: 0, max: 1, default: 0.7, unit: '' },
{ id: 3, name: 'mix', label: 'Mix', min: 0, max: 1, default: 1, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Distortion</div>
<div class="node-param">
<label>Type: <span id="disttype-${nodeId}">Soft Clip</span></label>
<select data-node="${nodeId}" data-param="1" style="width: 100%; padding: 2px;">
<option value="0">Soft Clip</option>
<option value="1">Hard Clip</option>
<option value="2">Tanh</option>
<option value="3">Asymmetric</option>
</select>
</div>
<div class="node-param">
<label>Drive: <span id="drive-${nodeId}">1.00</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.01" max="20" value="1" step="0.01">
</div>
<div class="node-param">
<label>Tone: <span id="tone-${nodeId}">0.70</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0" max="1" value="0.7" step="0.01">
</div>
<div class="node-param">
<label>Mix: <span id="mix-${nodeId}">1.00</span></label>
<input type="range" data-node="${nodeId}" data-param="3" min="0" max="1" value="1" step="0.01">
</div>
</div>
`
},
Constant: {
name: 'Constant',
category: NodeCategory.UTILITY,
description: 'Constant CV source - outputs a fixed voltage',
inputs: [],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'value', label: 'Value', min: -10, max: 10, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Constant</div>
<div class="node-param">
<label>Value:</label>
<input type="number" class="node-number-input" data-node="${nodeId}" data-param="0" min="-10" max="10" value="0" step="0.01" style="width: 60px; padding: 2px;">
<input type="range" data-node="${nodeId}" data-param="0" min="-10" max="10" value="0" step="0.01">
</div>
</div>
`
},
AutomationInput: {
name: 'AutomationInput',
category: NodeCategory.UTILITY,
description: 'Timeline automation - outputs CV signal controlled by timeline curves',
inputs: [],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Automation</div>
<div class="node-info" style="font-size: 10px; padding: 8px; color: #888;">
Timeline-based automation
</div>
<div id="automation-name-${nodeId}" style="font-size: 9px; color: #aaa; text-align: center; padding: 4px;">
Not connected
</div>
<div style="font-size: 9px; color: #666; text-align: center; padding: 4px;">
Edit curves in timeline
</div>
</div>
`
},
Math: {
name: 'Math',
category: NodeCategory.UTILITY,
description: 'Mathematical and logical operations on CV signals',
inputs: [
{ name: 'CV In A', type: SignalType.CV, index: 0 },
{ name: 'CV In B', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'operation', label: 'Operation', min: 0, max: 13, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Math</div>
<div class="node-param">
<label>Op: <span id="operation-${nodeId}">Add</span></label>
<select class="node-select" data-node="${nodeId}" data-param="0" style="width: 100%; padding: 2px;">
<option value="0">Add</option>
<option value="1">Subtract</option>
<option value="2">Multiply</option>
<option value="3">Divide</option>
<option value="4">Min</option>
<option value="5">Max</option>
<option value="6">Average</option>
<option value="7">Invert</option>
<option value="8">Abs</option>
<option value="9">Clamp</option>
<option value="10">Wrap</option>
<option value="11">Greater</option>
<option value="12">Less</option>
<option value="13">Equal</option>
</select>
</div>
</div>
`
},
Quantizer: {
name: 'Quantizer',
category: NodeCategory.UTILITY,
description: 'Quantize CV to musical scales',
inputs: [
{ name: 'CV In', type: SignalType.CV, index: 0 }
],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 },
{ name: 'Gate Out', type: SignalType.CV, index: 1 }
],
parameters: [
{ id: 0, name: 'scale', label: 'Scale', min: 0, max: 10, default: 0, unit: '' },
{ id: 1, name: 'root', label: 'Root', min: 0, max: 11, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Quantizer</div>
<div class="node-param">
<label>Scale: <span id="scale-${nodeId}">Chromatic</span></label>
<select class="node-select" data-node="${nodeId}" data-param="0" style="width: 100%; padding: 2px;">
<option value="0">Chromatic</option>
<option value="1">Major</option>
<option value="2">Minor</option>
<option value="3">Pent. Major</option>
<option value="4">Pent. Minor</option>
<option value="5">Dorian</option>
<option value="6">Phrygian</option>
<option value="7">Lydian</option>
<option value="8">Mixolydian</option>
<option value="9">Whole Tone</option>
<option value="10">Octaves</option>
</select>
</div>
<div class="node-param">
<label>Root: <span id="root-${nodeId}">C</span></label>
<select class="node-select" data-node="${nodeId}" data-param="1" style="width: 100%; padding: 2px;">
<option value="0">C</option>
<option value="1">C#</option>
<option value="2">D</option>
<option value="3">D#</option>
<option value="4">E</option>
<option value="5">F</option>
<option value="6">F#</option>
<option value="7">G</option>
<option value="8">G#</option>
<option value="9">A</option>
<option value="10">A#</option>
<option value="11">B</option>
</select>
</div>
</div>
`
},
SlewLimiter: {
name: 'SlewLimiter',
category: NodeCategory.UTILITY,
description: 'Limit rate of change for portamento/glide effects',
inputs: [
{ name: 'CV In', type: SignalType.CV, index: 0 }
],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'rise_time', label: 'Rise Time', min: 0, max: 5, default: 0.01, unit: 's' },
{ id: 1, name: 'fall_time', label: 'Fall Time', min: 0, max: 5, default: 0.01, unit: 's' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Slew Limiter</div>
<div class="node-param">
<label>Rise: <span id="slewrise-${nodeId}">0.01</span>s</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0" max="5" value="0.01" step="0.001">
</div>
<div class="node-param">
<label>Fall: <span id="slewfall-${nodeId}">0.01</span>s</label>
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="5" value="0.01" step="0.001">
</div>
</div>
`
},
EQ: {
name: 'EQ',
category: NodeCategory.EFFECT,
description: '3-band parametric EQ',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'low_freq', label: 'Low Freq', min: 20, max: 500, default: 100, unit: 'Hz' },
{ id: 1, name: 'low_gain', label: 'Low Gain', min: -24, max: 24, default: 0, unit: 'dB' },
{ id: 2, name: 'mid_freq', label: 'Mid Freq', min: 200, max: 5000, default: 1000, unit: 'Hz' },
{ id: 3, name: 'mid_gain', label: 'Mid Gain', min: -24, max: 24, default: 0, unit: 'dB' },
{ id: 4, name: 'mid_q', label: 'Mid Q', min: 0.1, max: 10, default: 0.707, unit: '' },
{ id: 5, name: 'high_freq', label: 'High Freq', min: 2000, max: 20000, default: 8000, unit: 'Hz' },
{ id: 6, name: 'high_gain', label: 'High Gain', min: -24, max: 24, default: 0, unit: 'dB' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">EQ</div>
<div style="font-size: 10px; margin-top: 4px; font-weight: bold;">Low Band</div>
<div class="node-param">
<label>Freq: <span id="lowfreq-${nodeId}">100</span> Hz</label>
<input type="range" data-node="${nodeId}" data-param="0" min="20" max="500" value="100" step="1">
</div>
<div class="node-param">
<label>Gain: <span id="lowgain-${nodeId}">0</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="1" min="-24" max="24" value="0" step="0.1">
</div>
<div style="font-size: 10px; margin-top: 4px; font-weight: bold;">Mid Band</div>
<div class="node-param">
<label>Freq: <span id="midfreq-${nodeId}">1000</span> Hz</label>
<input type="range" data-node="${nodeId}" data-param="2" min="200" max="5000" value="1000" step="10">
</div>
<div class="node-param">
<label>Gain: <span id="midgain-${nodeId}">0</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="3" min="-24" max="24" value="0" step="0.1">
</div>
<div class="node-param">
<label>Q: <span id="midq-${nodeId}">0.71</span></label>
<input type="range" data-node="${nodeId}" data-param="4" min="0.1" max="10" value="0.707" step="0.01">
</div>
<div style="font-size: 10px; margin-top: 4px; font-weight: bold;">High Band</div>
<div class="node-param">
<label>Freq: <span id="highfreq-${nodeId}">8000</span> Hz</label>
<input type="range" data-node="${nodeId}" data-param="5" min="2000" max="20000" value="8000" step="100">
</div>
<div class="node-param">
<label>Gain: <span id="highgain-${nodeId}">0</span> dB</label>
<input type="range" data-node="${nodeId}" data-param="6" min="-24" max="24" value="0" step="0.1">
</div>
</div>
`
},
SampleHold: {
name: 'Sample & Hold',
category: NodeCategory.UTILITY,
description: 'Samples CV input when gate signal goes high',
inputs: [
{ name: 'CV In', type: SignalType.CV, index: 0 },
{ name: 'Gate In', type: SignalType.CV, index: 1 }
],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Sample & Hold</div>
<div style="padding: 8px; font-size: 11px; color: #888;">
Samples CV input<br>on gate rising edge
</div>
</div>
`
},
BpmDetector: {
name: 'BPM Detector',
category: NodeCategory.UTILITY,
description: 'Detects tempo from audio and outputs BPM as CV',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'BPM CV', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'smoothing', label: 'Smoothing', min: 0.0, max: 1.0, default: 0.9, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">BPM Detector</div>
<div class="node-param">
<label>Smoothing: <span id="smoothing-${nodeId}">0.90</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.0" max="1.0" value="0.9" step="0.01">
</div>
<div class="node-info" style="font-size: 10px; color: #888; margin-top: 5px;">
Analyzes incoming audio and outputs detected BPM as CV signal
</div>
</div>
`
},
EnvelopeFollower: {
name: 'Envelope Follower',
category: NodeCategory.UTILITY,
description: 'Extracts amplitude envelope from audio signal',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'CV Out', type: SignalType.CV, index: 0 }
],
parameters: [
{ id: 0, name: 'attack', label: 'Attack', min: 0.001, max: 1.0, default: 0.01, unit: 's' },
{ id: 1, name: 'release', label: 'Release', min: 0.001, max: 1.0, default: 0.1, unit: 's' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Envelope Follower</div>
<div class="node-param">
<label>Attack: <span id="attack-${nodeId}">0.01</span> s</label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.001" max="1.0" value="0.01" step="0.001">
</div>
<div class="node-param">
<label>Release: <span id="release-${nodeId}">0.1</span> s</label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.001" max="1.0" value="0.1" step="0.001">
</div>
</div>
`
},
RingModulator: {
name: 'Ring Modulator',
category: NodeCategory.EFFECT,
description: 'Multiplies carrier and modulator for metallic timbres',
inputs: [
{ name: 'Carrier', type: SignalType.AUDIO, index: 0 },
{ name: 'Modulator', type: SignalType.AUDIO, index: 1 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'mix', label: 'Mix', min: 0.0, max: 1.0, default: 1.0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Ring Modulator</div>
<div class="node-param">
<label>Mix: <span id="mix-${nodeId}">1.00</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="0.0" max="1.0" value="1.0" step="0.01">
</div>
</div>
`
},
Phaser: {
name: 'Phaser',
category: NodeCategory.EFFECT,
description: 'Sweeping all-pass filters for phase shifting effect',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'rate', label: 'Rate', min: 0.1, max: 10.0, default: 0.5, unit: 'Hz' },
{ id: 1, name: 'depth', label: 'Depth', min: 0.0, max: 1.0, default: 0.7, unit: '' },
{ id: 2, name: 'stages', label: 'Stages', min: 2, max: 8, default: 6, unit: '' },
{ id: 3, name: 'feedback', label: 'Feedback', min: -0.95, max: 0.95, default: 0.5, unit: '' },
{ id: 4, name: 'wetdry', label: 'Wet/Dry', min: 0.0, max: 1.0, default: 0.5, unit: '' },
{ id: 5, name: 'sync', label: 'Sync to BPM', min: 0, max: 1, default: 0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Phaser</div>
<div class="node-param">
<label>
<input type="checkbox" id="sync-${nodeId}" data-node="${nodeId}" data-param="5">
Sync to BPM
</label>
</div>
<div class="node-param">
<label>Rate: <span id="rate-${nodeId}">0.5</span><span id="rate-unit-${nodeId}"> Hz</span></label>
<input type="range" id="rate-slider-${nodeId}" data-node="${nodeId}" data-param="0" min="0.1" max="10.0" value="0.5" step="0.1">
</div>
<div class="node-param">
<label>Depth: <span id="depth-${nodeId}">0.7</span></label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.0" max="1.0" value="0.7" step="0.01">
</div>
<div class="node-param">
<label>Stages: <span id="stages-${nodeId}">6</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="2" max="8" value="6" step="2">
</div>
<div class="node-param">
<label>Feedback: <span id="feedback-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="3" min="-0.95" max="0.95" value="0.5" step="0.01">
</div>
<div class="node-param">
<label>Wet/Dry: <span id="wetdry-${nodeId}">0.5</span></label>
<input type="range" data-node="${nodeId}" data-param="4" min="0.0" max="1.0" value="0.5" step="0.01">
</div>
</div>
`
},
BitCrusher: {
name: 'Bit Crusher',
category: NodeCategory.EFFECT,
description: 'Lo-fi effect via bit depth and sample rate reduction',
inputs: [
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'bitdepth', label: 'Bit Depth', min: 1, max: 16, default: 8, unit: 'bits' },
{ id: 1, name: 'samplerate', label: 'Sample Rate', min: 100, max: 48000, default: 8000, unit: 'Hz' },
{ id: 2, name: 'mix', label: 'Mix', min: 0.0, max: 1.0, default: 1.0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Bit Crusher</div>
<div class="node-param">
<label>Bit Depth: <span id="bitdepth-${nodeId}">8</span> bits</label>
<input type="range" data-node="${nodeId}" data-param="0" min="1" max="16" value="8" step="1">
</div>
<div class="node-param">
<label>Sample Rate: <span id="samplerate-${nodeId}">8000</span> Hz</label>
<input type="range" data-node="${nodeId}" data-param="1" min="100" max="48000" value="8000" step="100">
</div>
<div class="node-param">
<label>Mix: <span id="mix-${nodeId}">1.0</span></label>
<input type="range" data-node="${nodeId}" data-param="2" min="0.0" max="1.0" value="1.0" step="0.01">
</div>
</div>
`
},
Vocoder: {
name: 'Vocoder',
category: NodeCategory.EFFECT,
description: 'Multi-band vocoder - modulator controls carrier spectrum',
inputs: [
{ name: 'Modulator', type: SignalType.AUDIO, index: 0 },
{ name: 'Carrier', type: SignalType.AUDIO, index: 1 }
],
outputs: [
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
],
parameters: [
{ id: 0, name: 'bands', label: 'Bands', min: 8, max: 32, default: 16, unit: '' },
{ id: 1, name: 'attack', label: 'Attack', min: 0.001, max: 0.1, default: 0.01, unit: 's' },
{ id: 2, name: 'release', label: 'Release', min: 0.001, max: 1.0, default: 0.05, unit: 's' },
{ id: 3, name: 'mix', label: 'Mix', min: 0.0, max: 1.0, default: 1.0, unit: '' }
],
getHTML: (nodeId) => `
<div class="node-content">
<div class="node-title">Vocoder</div>
<div class="node-param">
<label>Bands: <span id="bands-${nodeId}">16</span></label>
<input type="range" data-node="${nodeId}" data-param="0" min="8" max="32" value="16" step="1">
</div>
<div class="node-param">
<label>Attack: <span id="attack-${nodeId}">0.01</span> s</label>
<input type="range" data-node="${nodeId}" data-param="1" min="0.001" max="0.1" value="0.01" step="0.001">
</div>
<div class="node-param">
<label>Release: <span id="release-${nodeId}">0.05</span> s</label>
<input type="range" data-node="${nodeId}" data-param="2" min="0.001" max="1.0" value="0.05" step="0.001">
</div>
<div class="node-param">
<label>Mix: <span id="mix-${nodeId}">1.0</span></label>
<input type="range" data-node="${nodeId}" data-param="3" min="0.0" max="1.0" value="1.0" step="0.01">
</div>
</div>
`
}
};
/**
* Get all node types in a specific category
*/
export function getNodesByCategory(category) {
return Object.entries(nodeTypes)
.filter(([_, def]) => def.category === category)
.map(([type, def]) => ({ type, ...def }));
}
/**
* Get all categories that have nodes
*/
export function getCategories() {
const categories = new Set();
Object.values(nodeTypes).forEach(def => categories.add(def.category));
return Array.from(categories);
}