diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e627048 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/NeuralAudio"] + path = vendor/NeuralAudio + url = https://github.com/mikeoliphant/NeuralAudio.git diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index ee4ecaa..c2812bd 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -1158,62 +1158,16 @@ impl Engine { if let Some(graph) = graph { // Create the node based on type - let node: Box = match node_type.as_str() { - "Oscillator" => Box::new(OscillatorNode::new("Oscillator".to_string())), - "Gain" => Box::new(GainNode::new("Gain".to_string())), - "Mixer" => Box::new(MixerNode::new("Mixer".to_string())), - "Filter" => Box::new(FilterNode::new("Filter".to_string())), - "SVF" => Box::new(SVFNode::new("SVF".to_string())), - "ADSR" => Box::new(ADSRNode::new("ADSR".to_string())), - "LFO" => Box::new(LFONode::new("LFO".to_string())), - "NoiseGenerator" => Box::new(NoiseGeneratorNode::new("Noise".to_string())), - "Splitter" => Box::new(SplitterNode::new("Splitter".to_string())), - "Pan" => Box::new(PanNode::new("Pan".to_string())), - "Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())), - "Echo" | "Delay" => Box::new(EchoNode::new("Echo".to_string())), - "Distortion" => Box::new(DistortionNode::new("Distortion".to_string())), - "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), - "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), - "Compressor" => Box::new(CompressorNode::new("Compressor".to_string())), - "Constant" => Box::new(ConstantNode::new("Constant".to_string())), - "BpmDetector" => Box::new(BpmDetectorNode::new("BPM Detector".to_string())), - "Beat" => Box::new(BeatNode::new("Beat".to_string())), - "Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator".to_string())), - "Sequencer" => Box::new(SequencerNode::new("Sequencer".to_string())), - "Script" => Box::new(ScriptNode::new("Script".to_string())), - "EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())), - "Limiter" => Box::new(LimiterNode::new("Limiter".to_string())), - "Math" => Box::new(MathNode::new("Math".to_string())), - "EQ" => Box::new(EQNode::new("EQ".to_string())), - "Flanger" => Box::new(FlangerNode::new("Flanger".to_string())), - "FMSynth" => Box::new(FMSynthNode::new("FM Synth".to_string())), - "Phaser" => Box::new(PhaserNode::new("Phaser".to_string())), - "BitCrusher" => Box::new(BitCrusherNode::new("Bit Crusher".to_string())), - "Vocoder" => Box::new(VocoderNode::new("Vocoder".to_string())), - "RingModulator" => Box::new(RingModulatorNode::new("Ring Modulator".to_string())), - "SampleHold" => Box::new(SampleHoldNode::new("Sample & Hold".to_string())), - "WavetableOscillator" => Box::new(WavetableOscillatorNode::new("Wavetable".to_string())), - "SimpleSampler" => Box::new(SimpleSamplerNode::new("Sampler".to_string())), - "SlewLimiter" => Box::new(SlewLimiterNode::new("Slew Limiter".to_string())), - "MultiSampler" => Box::new(MultiSamplerNode::new("Multi Sampler".to_string())), - "MidiInput" => Box::new(MidiInputNode::new("MIDI Input".to_string())), - "MidiToCV" => Box::new(MidiToCVNode::new("MIDI→CV".to_string())), - "AudioToCV" => Box::new(AudioToCVNode::new("Audio→CV".to_string())), - "AudioInput" => Box::new(AudioInputNode::new("Audio Input".to_string())), - "AutomationInput" => Box::new(AutomationInputNode::new("Automation".to_string())), - "Oscilloscope" => Box::new(OscilloscopeNode::new("Oscilloscope".to_string())), - "TemplateInput" => Box::new(TemplateInputNode::new("Template Input".to_string())), - "TemplateOutput" => Box::new(TemplateOutputNode::new("Template Output".to_string())), - "VoiceAllocator" => Box::new(VoiceAllocatorNode::new("VoiceAllocator".to_string(), self.sample_rate, 8192)), - "AudioOutput" => Box::new(AudioOutputNode::new("Output".to_string())), - _ => { - let _ = self.event_tx.push(AudioEvent::GraphConnectionError( - track_id, - format!("Unknown node type: {}", node_type) - )); - return; - } - }; + let node = match crate::audio::node_graph::nodes::create_node(&node_type, self.sample_rate, 8192) { + Some(n) => n, + None => { + let _ = self.event_tx.push(AudioEvent::GraphConnectionError( + track_id, + format!("Unknown node type: {}", node_type) + )); + return; + } + }; // Add node to graph let node_idx = graph.add_node(node); @@ -1250,53 +1204,9 @@ impl Engine { let va_idx = NodeIndex::new(voice_allocator_id as usize); // Create the node - let node: Box = match node_type.as_str() { - "Oscillator" => Box::new(OscillatorNode::new("Oscillator".to_string())), - "Gain" => Box::new(GainNode::new("Gain".to_string())), - "Mixer" => Box::new(MixerNode::new("Mixer".to_string())), - "Filter" => Box::new(FilterNode::new("Filter".to_string())), - "SVF" => Box::new(SVFNode::new("SVF".to_string())), - "ADSR" => Box::new(ADSRNode::new("ADSR".to_string())), - "LFO" => Box::new(LFONode::new("LFO".to_string())), - "NoiseGenerator" => Box::new(NoiseGeneratorNode::new("Noise".to_string())), - "Splitter" => Box::new(SplitterNode::new("Splitter".to_string())), - "Pan" => Box::new(PanNode::new("Pan".to_string())), - "Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())), - "Echo" | "Delay" => Box::new(EchoNode::new("Echo".to_string())), - "Distortion" => Box::new(DistortionNode::new("Distortion".to_string())), - "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), - "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), - "Compressor" => Box::new(CompressorNode::new("Compressor".to_string())), - "Constant" => Box::new(ConstantNode::new("Constant".to_string())), - "BpmDetector" => Box::new(BpmDetectorNode::new("BPM Detector".to_string())), - "Beat" => Box::new(BeatNode::new("Beat".to_string())), - "Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator".to_string())), - "Sequencer" => Box::new(SequencerNode::new("Sequencer".to_string())), - "Script" => Box::new(ScriptNode::new("Script".to_string())), - "EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())), - "Limiter" => Box::new(LimiterNode::new("Limiter".to_string())), - "Math" => Box::new(MathNode::new("Math".to_string())), - "EQ" => Box::new(EQNode::new("EQ".to_string())), - "Flanger" => Box::new(FlangerNode::new("Flanger".to_string())), - "FMSynth" => Box::new(FMSynthNode::new("FM Synth".to_string())), - "Phaser" => Box::new(PhaserNode::new("Phaser".to_string())), - "BitCrusher" => Box::new(BitCrusherNode::new("Bit Crusher".to_string())), - "Vocoder" => Box::new(VocoderNode::new("Vocoder".to_string())), - "RingModulator" => Box::new(RingModulatorNode::new("Ring Modulator".to_string())), - "SampleHold" => Box::new(SampleHoldNode::new("Sample & Hold".to_string())), - "WavetableOscillator" => Box::new(WavetableOscillatorNode::new("Wavetable".to_string())), - "SimpleSampler" => Box::new(SimpleSamplerNode::new("Sampler".to_string())), - "SlewLimiter" => Box::new(SlewLimiterNode::new("Slew Limiter".to_string())), - "MultiSampler" => Box::new(MultiSamplerNode::new("Multi Sampler".to_string())), - "MidiInput" => Box::new(MidiInputNode::new("MIDI Input".to_string())), - "MidiToCV" => Box::new(MidiToCVNode::new("MIDI→CV".to_string())), - "AudioToCV" => Box::new(AudioToCVNode::new("Audio→CV".to_string())), - "AutomationInput" => Box::new(AutomationInputNode::new("Automation".to_string())), - "Oscilloscope" => Box::new(OscilloscopeNode::new("Oscilloscope".to_string())), - "TemplateInput" => Box::new(TemplateInputNode::new("Template Input".to_string())), - "TemplateOutput" => Box::new(TemplateOutputNode::new("Template Output".to_string())), - "AudioOutput" => Box::new(AudioOutputNode::new("Output".to_string())), - _ => { + let node = match crate::audio::node_graph::nodes::create_node(&node_type, self.sample_rate, 8192) { + Some(n) => n, + None => { let _ = self.event_tx.push(AudioEvent::GraphConnectionError( track_id, format!("Unknown node type: {}", node_type) @@ -1723,6 +1633,26 @@ impl Engine { } } + Command::AmpSimLoadModel(track_id, node_id, model_path) => { + use crate::audio::node_graph::nodes::AmpSimNode; + + let graph = match self.project.get_track_mut(track_id) { + Some(TrackNode::Midi(track)) => Some(&mut track.instrument_graph), + Some(TrackNode::Audio(track)) => Some(&mut track.effects_graph), + _ => None, + }; + if let Some(graph) = graph { + let node_idx = NodeIndex::new(node_id as usize); + if let Some(graph_node) = graph.get_graph_node_mut(node_idx) { + if let Some(amp_sim) = graph_node.node.as_any_mut().downcast_mut::() { + if let Err(e) = amp_sim.load_model(&model_path) { + eprintln!("Failed to load NAM model: {}", e); + } + } + } + } + } + Command::SamplerLoadSample(track_id, node_id, file_path) => { use crate::audio::node_graph::nodes::SimpleSamplerNode; @@ -3352,6 +3282,11 @@ impl EngineController { let _ = self.command_tx.push(Command::GraphSaveTemplatePreset(track_id, voice_allocator_id, preset_path, preset_name)); } + /// Load a NAM model into an AmpSim node + pub fn amp_sim_load_model(&mut self, track_id: TrackId, node_id: u32, model_path: String) { + let _ = self.command_tx.push(Command::AmpSimLoadModel(track_id, node_id, model_path)); + } + /// Load a sample into a SimpleSampler node pub fn sampler_load_sample(&mut self, track_id: TrackId, node_id: u32, file_path: String) { let _ = self.command_tx.push(Command::SamplerLoadSample(track_id, node_id, file_path)); diff --git a/daw-backend/src/audio/node_graph/graph.rs b/daw-backend/src/audio/node_graph/graph.rs index 865d256..0a77303 100644 --- a/daw-backend/src/audio/node_graph/graph.rs +++ b/daw-backend/src/audio/node_graph/graph.rs @@ -917,6 +917,14 @@ impl AudioGraph { } } + // For AmpSim nodes, serialize the model path + if node.node_type() == "AmpSim" { + use crate::audio::node_graph::nodes::AmpSimNode; + if let Some(amp_sim) = node.as_any().downcast_ref::() { + serialized.nam_model_path = amp_sim.model_path().map(|s| s.to_string()); + } + } + // Save position if available if let Some(pos) = self.get_node_position(node_idx) { serialized.set_position(pos.0, pos.1); @@ -983,66 +991,19 @@ impl AudioGraph { // Create all nodes for serialized_node in &preset.nodes { // Create the node based on type - let node: Box = match serialized_node.node_type.as_str() { - "Oscillator" => Box::new(OscillatorNode::new("Oscillator")), - "Gain" => Box::new(GainNode::new("Gain")), - "Mixer" => Box::new(MixerNode::new("Mixer")), - "Filter" => Box::new(FilterNode::new("Filter")), - "SVF" => Box::new(SVFNode::new("SVF")), - "ADSR" => Box::new(ADSRNode::new("ADSR")), - "LFO" => Box::new(LFONode::new("LFO")), - "NoiseGenerator" => Box::new(NoiseGeneratorNode::new("Noise")), - "Splitter" => Box::new(SplitterNode::new("Splitter")), - "Pan" => Box::new(PanNode::new("Pan")), - "Quantizer" => Box::new(QuantizerNode::new("Quantizer")), - "Echo" | "Delay" => Box::new(EchoNode::new("Echo")), - "Distortion" => Box::new(DistortionNode::new("Distortion")), - "Reverb" => Box::new(ReverbNode::new("Reverb")), - "Chorus" => Box::new(ChorusNode::new("Chorus")), - "Compressor" => Box::new(CompressorNode::new("Compressor")), - "Constant" => Box::new(ConstantNode::new("Constant")), - "Beat" => Box::new(BeatNode::new("Beat")), - "Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator")), - "Sequencer" => Box::new(SequencerNode::new("Sequencer")), - "Script" => Box::new(ScriptNode::new("Script")), - "EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower")), - "Limiter" => Box::new(LimiterNode::new("Limiter")), - "Math" => Box::new(MathNode::new("Math")), - "EQ" => Box::new(EQNode::new("EQ")), - "Flanger" => Box::new(FlangerNode::new("Flanger")), - "FMSynth" => Box::new(FMSynthNode::new("FM Synth")), - "Phaser" => Box::new(PhaserNode::new("Phaser")), - "BitCrusher" => Box::new(BitCrusherNode::new("Bit Crusher")), - "Vocoder" => Box::new(VocoderNode::new("Vocoder")), - "RingModulator" => Box::new(RingModulatorNode::new("Ring Modulator")), - "SampleHold" => Box::new(SampleHoldNode::new("Sample & Hold")), - "WavetableOscillator" => Box::new(WavetableOscillatorNode::new("Wavetable")), - "SimpleSampler" => Box::new(SimpleSamplerNode::new("Sampler")), - "SlewLimiter" => Box::new(SlewLimiterNode::new("Slew Limiter")), - "MultiSampler" => Box::new(MultiSamplerNode::new("Multi Sampler")), - "MidiInput" => Box::new(MidiInputNode::new("MIDI Input")), - "MidiToCV" => Box::new(MidiToCVNode::new("MIDI→CV")), - "AudioToCV" => Box::new(AudioToCVNode::new("Audio→CV")), - "AudioInput" => Box::new(AudioInputNode::new("Audio Input")), - "AutomationInput" => Box::new(AutomationInputNode::new("Automation")), - "Oscilloscope" => Box::new(OscilloscopeNode::new("Oscilloscope")), - "TemplateInput" => Box::new(TemplateInputNode::new("Template Input")), - "TemplateOutput" => Box::new(TemplateOutputNode::new("Template Output")), - "VoiceAllocator" => { - let mut va = VoiceAllocatorNode::new("VoiceAllocator", sample_rate, buffer_size); + let mut node = crate::audio::node_graph::nodes::create_node(&serialized_node.node_type, sample_rate, buffer_size) + .ok_or_else(|| format!("Unknown node type: {}", serialized_node.node_type))?; - // If there's a template graph, deserialize and set it - if let Some(ref template_preset) = serialized_node.template_graph { + // VoiceAllocator needs its template graph deserialized and set + if serialized_node.node_type == "VoiceAllocator" { + if let Some(ref template_preset) = serialized_node.template_graph { + if let Some(va) = node.as_any_mut().downcast_mut::() { let template_graph = Self::from_preset(template_preset, sample_rate, buffer_size, preset_base_path)?; *va.template_graph_mut() = template_graph; va.rebuild_voices(); } - - Box::new(va) } - "AudioOutput" => Box::new(AudioOutputNode::new("Output")), - _ => return Err(format!("Unknown node type: {}", serialized_node.node_type)), - }; + } let node_idx = graph.add_node(node); index_map.insert(serialized_node.id, node_idx); @@ -1161,6 +1122,21 @@ impl AudioGraph { } } + // Restore NAM model for AmpSim nodes + if let Some(ref model_path) = serialized_node.nam_model_path { + if serialized_node.node_type == "AmpSim" { + use crate::audio::node_graph::nodes::AmpSimNode; + let resolved_path = resolve_sample_path(model_path); + if let Some(graph_node) = graph.graph.node_weight_mut(node_idx) { + if let Some(amp_sim) = graph_node.node.as_any_mut().downcast_mut::() { + if let Err(e) = amp_sim.load_model(&resolved_path) { + eprintln!("Warning: failed to load NAM model {}: {}", resolved_path, e); + } + } + } + } + } + // Restore position graph.set_node_position(node_idx, serialized_node.position.0, serialized_node.position.1); } diff --git a/daw-backend/src/audio/node_graph/nodes/mod.rs b/daw-backend/src/audio/node_graph/nodes/mod.rs index 80599bb..cedd816 100644 --- a/daw-backend/src/audio/node_graph/nodes/mod.rs +++ b/daw-backend/src/audio/node_graph/nodes/mod.rs @@ -1,3 +1,4 @@ +mod amp_sim; mod adsr; mod arpeggiator; mod audio_input; @@ -45,6 +46,7 @@ mod vocoder; mod voice_allocator; mod wavetable_oscillator; +pub use amp_sim::AmpSimNode; pub use adsr::ADSRNode; pub use arpeggiator::ArpeggiatorNode; pub use audio_input::AudioInputNode; @@ -91,3 +93,61 @@ pub use template_io::{TemplateInputNode, TemplateOutputNode}; pub use vocoder::VocoderNode; pub use voice_allocator::VoiceAllocatorNode; pub use wavetable_oscillator::WavetableOscillatorNode; + +/// Create a node instance by type name string. +/// +/// Returns `None` for unknown type names. `sample_rate` and `buffer_size` +/// are only used by VoiceAllocator; other nodes ignore them. +pub fn create_node(node_type: &str, sample_rate: u32, buffer_size: usize) -> Option> { + Some(match node_type { + "Oscillator" => Box::new(OscillatorNode::new("Oscillator")), + "Gain" => Box::new(GainNode::new("Gain")), + "Mixer" => Box::new(MixerNode::new("Mixer")), + "Filter" => Box::new(FilterNode::new("Filter")), + "SVF" => Box::new(SVFNode::new("SVF")), + "ADSR" => Box::new(ADSRNode::new("ADSR")), + "LFO" => Box::new(LFONode::new("LFO")), + "NoiseGenerator" => Box::new(NoiseGeneratorNode::new("Noise")), + "Splitter" => Box::new(SplitterNode::new("Splitter")), + "Pan" => Box::new(PanNode::new("Pan")), + "Quantizer" => Box::new(QuantizerNode::new("Quantizer")), + "Echo" | "Delay" => Box::new(EchoNode::new("Echo")), + "Distortion" => Box::new(DistortionNode::new("Distortion")), + "Reverb" => Box::new(ReverbNode::new("Reverb")), + "Chorus" => Box::new(ChorusNode::new("Chorus")), + "Compressor" => Box::new(CompressorNode::new("Compressor")), + "Constant" => Box::new(ConstantNode::new("Constant")), + "BpmDetector" => Box::new(BpmDetectorNode::new("BPM Detector")), + "Beat" => Box::new(BeatNode::new("Beat")), + "Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator")), + "Sequencer" => Box::new(SequencerNode::new("Sequencer")), + "Script" => Box::new(ScriptNode::new("Script")), + "EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower")), + "Limiter" => Box::new(LimiterNode::new("Limiter")), + "Math" => Box::new(MathNode::new("Math")), + "EQ" => Box::new(EQNode::new("EQ")), + "Flanger" => Box::new(FlangerNode::new("Flanger")), + "FMSynth" => Box::new(FMSynthNode::new("FM Synth")), + "Phaser" => Box::new(PhaserNode::new("Phaser")), + "BitCrusher" => Box::new(BitCrusherNode::new("Bit Crusher")), + "Vocoder" => Box::new(VocoderNode::new("Vocoder")), + "RingModulator" => Box::new(RingModulatorNode::new("Ring Modulator")), + "SampleHold" => Box::new(SampleHoldNode::new("Sample & Hold")), + "WavetableOscillator" => Box::new(WavetableOscillatorNode::new("Wavetable")), + "SimpleSampler" => Box::new(SimpleSamplerNode::new("Sampler")), + "SlewLimiter" => Box::new(SlewLimiterNode::new("Slew Limiter")), + "MultiSampler" => Box::new(MultiSamplerNode::new("Multi Sampler")), + "MidiInput" => Box::new(MidiInputNode::new("MIDI Input")), + "MidiToCV" => Box::new(MidiToCVNode::new("MIDI→CV")), + "AudioToCV" => Box::new(AudioToCVNode::new("Audio→CV")), + "AudioInput" => Box::new(AudioInputNode::new("Audio Input")), + "AutomationInput" => Box::new(AutomationInputNode::new("Automation")), + "Oscilloscope" => Box::new(OscilloscopeNode::new("Oscilloscope")), + "TemplateInput" => Box::new(TemplateInputNode::new("Template Input")), + "TemplateOutput" => Box::new(TemplateOutputNode::new("Template Output")), + "VoiceAllocator" => Box::new(VoiceAllocatorNode::new("VoiceAllocator", sample_rate, buffer_size)), + "AmpSim" => Box::new(AmpSimNode::new("Amp Sim")), + "AudioOutput" => Box::new(AudioOutputNode::new("Output")), + _ => return None, + }) +} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs index f285d9f..b7ee6a5 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs @@ -16,132 +16,140 @@ pub enum DataType { CV, } -/// Node templates - types of nodes that can be created -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum NodeTemplate { - // Inputs - MidiInput, - AudioInput, - AutomationInput, - Beat, +/// Macro that defines `NodeTemplate` enum and generates metadata methods from a single table. +/// +/// Each row: `variant, backend_name, display_label, category, in_finder;` +/// +/// Generated methods: +/// - `backend_type_name() -> &'static str` +/// - `display_label() -> &'static str` (used by `node_finder_label`) +/// - `category() -> &'static str` (used by `node_finder_categories`) +/// - `in_finder() -> bool` +/// - `from_backend_name(s: &str) -> Option` +/// - `all_finder_kinds() -> Vec` (only variants with `in_finder = true`) +macro_rules! node_templates { + ( + $( $variant:ident, $backend:literal, $label:literal, $category:literal, $in_finder:literal );+ + $(;)? + ) => { + /// Node templates - types of nodes that can be created + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub enum NodeTemplate { + $($variant),+ + } - // Generators - Oscillator, - WavetableOscillator, - FmSynth, - Noise, - SimpleSampler, - MultiSampler, + impl NodeTemplate { + /// Returns the backend-compatible type name string (matches daw-backend match arms) + pub fn backend_type_name(&self) -> &'static str { + match self { + $(NodeTemplate::$variant => $backend),+ + } + } - // Effects - Filter, - Svf, - Gain, - Echo, - Reverb, - Chorus, - Flanger, - Phaser, - Distortion, - BitCrusher, - Compressor, - Limiter, - Eq, - Pan, - RingModulator, - Vocoder, + /// Display label for the node finder + fn display_label(&self) -> &'static str { + match self { + $(NodeTemplate::$variant => $label),+ + } + } - // Utilities - Adsr, - Lfo, - Mixer, - Splitter, - Constant, - MidiToCv, - AudioToCv, - Arpeggiator, - Sequencer, - Math, - SampleHold, - SlewLimiter, - Quantizer, - EnvelopeFollower, - BpmDetector, - Mod, + /// Category for the node finder + fn category(&self) -> &'static str { + match self { + $(NodeTemplate::$variant => $category),+ + } + } - // Scripting - Script, + /// Whether this node appears in the node finder + #[allow(dead_code)] + fn in_finder(&self) -> bool { + match self { + $(NodeTemplate::$variant => $in_finder),+ + } + } - // Analysis - Oscilloscope, + /// Map a backend type name string to a NodeTemplate variant. + /// + /// Handles canonical names from the table plus legacy aliases. + pub fn from_backend_name(s: &str) -> Option { + match s { + $($backend => Some(NodeTemplate::$variant),)+ + // Legacy / alternate aliases + "Delay" => Some(NodeTemplate::Echo), + "BPMDetector" => Some(NodeTemplate::BpmDetector), + _ => None, + } + } - // Advanced - VoiceAllocator, - Group, - - // Subgraph I/O (only visible when editing inside a container node) - TemplateInput, - TemplateOutput, - - // Outputs - AudioOutput, + /// All node templates that should appear in the default node finder + pub fn all_finder_kinds() -> Vec { + let mut v = Vec::new(); + $(if $in_finder { v.push(NodeTemplate::$variant); })+ + v + } + } + }; } -impl NodeTemplate { - /// Returns the backend-compatible type name string (matches daw-backend match arms) - pub fn backend_type_name(&self) -> &'static str { - match self { - NodeTemplate::MidiInput => "MidiInput", - NodeTemplate::AudioInput => "AudioInput", - NodeTemplate::AutomationInput => "AutomationInput", - NodeTemplate::Oscillator => "Oscillator", - NodeTemplate::WavetableOscillator => "WavetableOscillator", - NodeTemplate::FmSynth => "FMSynth", - NodeTemplate::Noise => "NoiseGenerator", - NodeTemplate::SimpleSampler => "SimpleSampler", - NodeTemplate::MultiSampler => "MultiSampler", - NodeTemplate::Filter => "Filter", - NodeTemplate::Svf => "SVF", - NodeTemplate::Gain => "Gain", - NodeTemplate::Echo => "Echo", - NodeTemplate::Reverb => "Reverb", - NodeTemplate::Chorus => "Chorus", - NodeTemplate::Flanger => "Flanger", - NodeTemplate::Phaser => "Phaser", - NodeTemplate::Distortion => "Distortion", - NodeTemplate::BitCrusher => "BitCrusher", - NodeTemplate::Compressor => "Compressor", - NodeTemplate::Limiter => "Limiter", - NodeTemplate::Eq => "EQ", - NodeTemplate::Pan => "Pan", - NodeTemplate::RingModulator => "RingModulator", - NodeTemplate::Vocoder => "Vocoder", - NodeTemplate::Adsr => "ADSR", - NodeTemplate::Lfo => "LFO", - NodeTemplate::Mixer => "Mixer", - NodeTemplate::Splitter => "Splitter", - NodeTemplate::Constant => "Constant", - NodeTemplate::MidiToCv => "MidiToCV", - NodeTemplate::AudioToCv => "AudioToCV", - NodeTemplate::Arpeggiator => "Arpeggiator", - NodeTemplate::Sequencer => "Sequencer", - NodeTemplate::Math => "Math", - NodeTemplate::SampleHold => "SampleHold", - NodeTemplate::SlewLimiter => "SlewLimiter", - NodeTemplate::Quantizer => "Quantizer", - NodeTemplate::EnvelopeFollower => "EnvelopeFollower", - NodeTemplate::BpmDetector => "BpmDetector", - NodeTemplate::Beat => "Beat", - NodeTemplate::Script => "Script", - NodeTemplate::Mod => "Mod", - NodeTemplate::Oscilloscope => "Oscilloscope", - NodeTemplate::VoiceAllocator => "VoiceAllocator", - NodeTemplate::Group => "Group", - NodeTemplate::TemplateInput => "TemplateInput", - NodeTemplate::TemplateOutput => "TemplateOutput", - NodeTemplate::AudioOutput => "AudioOutput", - } - } +node_templates! { + // Inputs + MidiInput, "MidiInput", "MIDI Input", "Inputs", true; + AudioInput, "AudioInput", "Audio Input", "Inputs", true; + AutomationInput, "AutomationInput", "Automation Input", "Inputs", true; + Beat, "Beat", "Beat", "Inputs", true; + // Generators + Oscillator, "Oscillator", "Oscillator", "Generators", true; + WavetableOscillator,"WavetableOscillator","Wavetable Oscillator","Generators", true; + FmSynth, "FMSynth", "FM Synth", "Generators", true; + Noise, "NoiseGenerator", "Noise Generator", "Generators", true; + SimpleSampler, "SimpleSampler", "Simple Sampler", "Generators", true; + MultiSampler, "MultiSampler", "Multi Sampler", "Generators", true; + // Effects + Filter, "Filter", "Filter", "Effects", true; + Svf, "SVF", "SVF", "Effects", true; + Gain, "Gain", "Gain", "Effects", true; + Echo, "Echo", "Echo", "Effects", true; + Reverb, "Reverb", "Reverb", "Effects", true; + Chorus, "Chorus", "Chorus", "Effects", true; + Flanger, "Flanger", "Flanger", "Effects", true; + Phaser, "Phaser", "Phaser", "Effects", true; + Distortion, "Distortion", "Distortion", "Effects", true; + AmpSim, "AmpSim", "Amp Sim", "Effects", true; + BitCrusher, "BitCrusher", "Bit Crusher", "Effects", true; + Compressor, "Compressor", "Compressor", "Effects", true; + Limiter, "Limiter", "Limiter", "Effects", true; + Eq, "EQ", "EQ", "Effects", true; + Pan, "Pan", "Pan", "Effects", true; + RingModulator, "RingModulator", "Ring Modulator", "Effects", true; + Vocoder, "Vocoder", "Vocoder", "Effects", true; + // Utilities + Adsr, "ADSR", "ADSR Envelope", "Utilities", true; + Lfo, "LFO", "LFO", "Utilities", true; + Mixer, "Mixer", "Mixer", "Utilities", true; + Splitter, "Splitter", "Splitter", "Utilities", true; + Constant, "Constant", "Constant", "Utilities", true; + MidiToCv, "MidiToCV", "MIDI to CV", "Utilities", true; + AudioToCv, "AudioToCV", "Audio to CV", "Utilities", true; + Arpeggiator, "Arpeggiator", "Arpeggiator", "Utilities", true; + Sequencer, "Sequencer", "Step Sequencer", "Utilities", true; + Math, "Math", "Math", "Utilities", true; + SampleHold, "SampleHold", "Sample & Hold", "Utilities", true; + SlewLimiter, "SlewLimiter", "Slew Limiter", "Utilities", true; + Quantizer, "Quantizer", "Quantizer", "Utilities", true; + EnvelopeFollower, "EnvelopeFollower", "Envelope Follower", "Utilities", true; + BpmDetector, "BpmDetector", "BPM Detector", "Utilities", true; + Mod, "Mod", "Modulator", "Utilities", true; + // Analysis + Oscilloscope, "Oscilloscope", "Oscilloscope", "Analysis", true; + // Advanced + VoiceAllocator, "VoiceAllocator", "Voice Allocator", "Advanced", true; + Script, "Script", "Script", "Advanced", true; + Group, "Group", "Group", "Advanced", false; + // Subgraph I/O + TemplateInput, "TemplateInput", "Template Input", "Subgraph I/O", false; + TemplateOutput, "TemplateOutput", "Template Output", "Subgraph I/O", false; + // Outputs + AudioOutput, "AudioOutput", "Audio Output", "Outputs", true; } /// Custom node data @@ -166,6 +174,9 @@ pub struct NodeData { /// Display names of loaded samples per slot (slot_index → display name) #[serde(skip)] pub script_sample_names: HashMap, + /// Display name of loaded NAM model (for AmpSim nodes) + #[serde(default)] + pub nam_model_name: Option, } fn default_root_note() -> u8 { 69 } @@ -180,6 +191,7 @@ impl NodeData { ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new(), + nam_model_name: None, } } } @@ -265,6 +277,8 @@ pub struct GraphState { pub pending_draw_param_changes: Vec<(NodeId, u32, f32)>, /// Active sample import dialog (folder import with heuristic mapping) pub sample_import_dialog: Option, + /// Pending AmpSim model load (node_id, backend_node_id) — triggers file dialog for .nam + pub pending_amp_sim_load: Option<(NodeId, u32)>, } impl Default for GraphState { @@ -288,6 +302,7 @@ impl Default for GraphState { draw_vms: HashMap::new(), pending_draw_param_changes: Vec::new(), sample_import_dialog: None, + pending_amp_sim_load: None, } } } @@ -393,87 +408,11 @@ impl NodeTemplateTrait for NodeTemplate { type CategoryType = &'static str; fn node_finder_label(&self, _user_state: &mut Self::UserState) -> std::borrow::Cow<'_, str> { - match self { - // Inputs - NodeTemplate::MidiInput => "MIDI Input".into(), - NodeTemplate::AudioInput => "Audio Input".into(), - NodeTemplate::AutomationInput => "Automation Input".into(), - // Generators - NodeTemplate::Oscillator => "Oscillator".into(), - NodeTemplate::WavetableOscillator => "Wavetable Oscillator".into(), - NodeTemplate::FmSynth => "FM Synth".into(), - NodeTemplate::Noise => "Noise Generator".into(), - NodeTemplate::SimpleSampler => "Simple Sampler".into(), - NodeTemplate::MultiSampler => "Multi Sampler".into(), - // Effects - NodeTemplate::Filter => "Filter".into(), - NodeTemplate::Svf => "SVF".into(), - NodeTemplate::Gain => "Gain".into(), - NodeTemplate::Echo => "Echo".into(), - NodeTemplate::Reverb => "Reverb".into(), - NodeTemplate::Chorus => "Chorus".into(), - NodeTemplate::Flanger => "Flanger".into(), - NodeTemplate::Phaser => "Phaser".into(), - NodeTemplate::Distortion => "Distortion".into(), - NodeTemplate::BitCrusher => "Bit Crusher".into(), - NodeTemplate::Compressor => "Compressor".into(), - NodeTemplate::Limiter => "Limiter".into(), - NodeTemplate::Eq => "EQ".into(), - NodeTemplate::Pan => "Pan".into(), - NodeTemplate::RingModulator => "Ring Modulator".into(), - NodeTemplate::Vocoder => "Vocoder".into(), - // Utilities - NodeTemplate::Adsr => "ADSR Envelope".into(), - NodeTemplate::Lfo => "LFO".into(), - NodeTemplate::Mixer => "Mixer".into(), - NodeTemplate::Splitter => "Splitter".into(), - NodeTemplate::Constant => "Constant".into(), - NodeTemplate::MidiToCv => "MIDI to CV".into(), - NodeTemplate::AudioToCv => "Audio to CV".into(), - NodeTemplate::Arpeggiator => "Arpeggiator".into(), - NodeTemplate::Sequencer => "Step Sequencer".into(), - NodeTemplate::Math => "Math".into(), - NodeTemplate::SampleHold => "Sample & Hold".into(), - NodeTemplate::SlewLimiter => "Slew Limiter".into(), - NodeTemplate::Quantizer => "Quantizer".into(), - NodeTemplate::EnvelopeFollower => "Envelope Follower".into(), - NodeTemplate::BpmDetector => "BPM Detector".into(), - NodeTemplate::Beat => "Beat".into(), - NodeTemplate::Mod => "Modulator".into(), - // Scripting - NodeTemplate::Script => "Script".into(), - // Analysis - NodeTemplate::Oscilloscope => "Oscilloscope".into(), - // Advanced - NodeTemplate::VoiceAllocator => "Voice Allocator".into(), - NodeTemplate::Group => "Group".into(), - // Subgraph I/O - NodeTemplate::TemplateInput => "Template Input".into(), - NodeTemplate::TemplateOutput => "Template Output".into(), - // Outputs - NodeTemplate::AudioOutput => "Audio Output".into(), - } + self.display_label().into() } fn node_finder_categories(&self, _user_state: &mut Self::UserState) -> Vec<&'static str> { - match self { - NodeTemplate::MidiInput | NodeTemplate::AudioInput | NodeTemplate::AutomationInput | NodeTemplate::Beat => vec!["Inputs"], - NodeTemplate::Oscillator | NodeTemplate::WavetableOscillator | NodeTemplate::FmSynth - | NodeTemplate::Noise | NodeTemplate::SimpleSampler | NodeTemplate::MultiSampler => vec!["Generators"], - NodeTemplate::Filter | NodeTemplate::Svf | NodeTemplate::Gain | NodeTemplate::Echo | NodeTemplate::Reverb - | NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser | NodeTemplate::Distortion - | NodeTemplate::BitCrusher | NodeTemplate::Compressor | NodeTemplate::Limiter | NodeTemplate::Eq - | NodeTemplate::Pan | NodeTemplate::RingModulator | NodeTemplate::Vocoder => vec!["Effects"], - NodeTemplate::Adsr | NodeTemplate::Lfo | NodeTemplate::Mixer | NodeTemplate::Splitter - | NodeTemplate::Constant | NodeTemplate::MidiToCv | NodeTemplate::AudioToCv | NodeTemplate::Arpeggiator | NodeTemplate::Sequencer | NodeTemplate::Math - | NodeTemplate::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer - | NodeTemplate::EnvelopeFollower | NodeTemplate::BpmDetector | NodeTemplate::Mod => vec!["Utilities"], - NodeTemplate::Script => vec!["Advanced"], - NodeTemplate::Oscilloscope => vec!["Analysis"], - NodeTemplate::VoiceAllocator | NodeTemplate::Group => vec!["Advanced"], - NodeTemplate::TemplateInput | NodeTemplate::TemplateOutput => vec!["Subgraph I/O"], - NodeTemplate::AudioOutput => vec!["Outputs"], - } + vec![self.category()] } fn node_graph_label(&self, user_state: &mut Self::UserState) -> String { @@ -735,6 +674,16 @@ impl NodeTemplateTrait for NodeTemplate { ValueType::float_param(1.0, 0.0, 1.0, "", 3, None), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } + NodeTemplate::AmpSim => { + graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); + graph.add_input_param(node_id, "Input Gain".into(), DataType::CV, + ValueType::float_param(1.0, 0.0, 4.0, "", 0, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Output Gain".into(), DataType::CV, + ValueType::float_param(1.0, 0.0, 4.0, "", 1, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Mix".into(), DataType::CV, + ValueType::float_param(1.0, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); + } NodeTemplate::BitCrusher => { graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "Bit Depth".into(), DataType::CV, @@ -1449,6 +1398,12 @@ impl NodeDataTrait for NodeData { &mut user_state.pending_draw_param_changes, ); } + } else if self.template == NodeTemplate::AmpSim { + let backend_node_id = user_state.node_backend_ids.get(&node_id).copied().unwrap_or(0); + let button_text = self.nam_model_name.as_deref().unwrap_or("Load Model..."); + if ui.button(button_text).clicked() { + user_state.pending_amp_sim_load = Some((node_id, backend_node_id)); + } } else { ui.label(""); } @@ -1677,7 +1632,7 @@ impl NodeTemplateIter for VoiceAllocatorNodeTemplates { type Item = NodeTemplate; fn all_kinds(&self) -> Vec { - let mut templates = AllNodeTemplates.all_kinds(); + let mut templates = NodeTemplate::all_finder_kinds(); // VA nodes can't be nested — signals inside a VA are monophonic templates.retain(|t| *t != NodeTemplate::VoiceAllocator); templates.push(NodeTemplate::TemplateInput); @@ -1690,7 +1645,7 @@ impl NodeTemplateIter for SubgraphNodeTemplates { type Item = NodeTemplate; fn all_kinds(&self) -> Vec { - let mut templates = AllNodeTemplates.all_kinds(); + let mut templates = NodeTemplate::all_finder_kinds(); templates.push(NodeTemplate::TemplateInput); templates.push(NodeTemplate::TemplateOutput); templates @@ -1701,63 +1656,6 @@ impl NodeTemplateIter for AllNodeTemplates { type Item = NodeTemplate; fn all_kinds(&self) -> Vec { - vec![ - // Inputs - NodeTemplate::MidiInput, - NodeTemplate::AudioInput, - NodeTemplate::AutomationInput, - // Generators - NodeTemplate::Oscillator, - NodeTemplate::WavetableOscillator, - NodeTemplate::FmSynth, - NodeTemplate::Noise, - NodeTemplate::SimpleSampler, - NodeTemplate::MultiSampler, - // Effects - NodeTemplate::Filter, - NodeTemplate::Svf, - NodeTemplate::Gain, - NodeTemplate::Echo, - NodeTemplate::Reverb, - NodeTemplate::Chorus, - NodeTemplate::Flanger, - NodeTemplate::Phaser, - NodeTemplate::Distortion, - NodeTemplate::BitCrusher, - NodeTemplate::Compressor, - NodeTemplate::Limiter, - NodeTemplate::Eq, - NodeTemplate::Pan, - NodeTemplate::RingModulator, - NodeTemplate::Vocoder, - // Utilities - NodeTemplate::Adsr, - NodeTemplate::Lfo, - NodeTemplate::Mixer, - NodeTemplate::Splitter, - NodeTemplate::Constant, - NodeTemplate::MidiToCv, - NodeTemplate::AudioToCv, - NodeTemplate::Arpeggiator, - NodeTemplate::Sequencer, - NodeTemplate::Math, - NodeTemplate::SampleHold, - NodeTemplate::SlewLimiter, - NodeTemplate::Quantizer, - NodeTemplate::EnvelopeFollower, - NodeTemplate::BpmDetector, - NodeTemplate::Beat, - NodeTemplate::Mod, - // Analysis - NodeTemplate::Oscilloscope, - // Advanced - NodeTemplate::VoiceAllocator, - NodeTemplate::Script, - // Note: Group is not in the node finder — groups are created via Ctrl+G selection. - // Note: TemplateInput/TemplateOutput are excluded from the default finder. - // They are added dynamically when editing inside a subgraph. - // Outputs - NodeTemplate::AudioOutput, - ] + NodeTemplate::all_finder_kinds() } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs index edb8310..ab03c73 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs @@ -1385,7 +1385,7 @@ impl NodeGraphPane { // Create nodes in frontend self.pending_script_resolutions.clear(); for node in &graph_state.nodes { - let node_template = match Self::backend_type_to_template(&node.node_type) { + let node_template = match NodeTemplate::from_backend_name(&node.node_type) { Some(t) => t, None => { eprintln!("Unknown node type: {}", node.node_type); @@ -1836,7 +1836,7 @@ impl NodeGraphPane { continue; } - let node_template = match Self::backend_type_to_template(&node.node_type) { + let node_template = match NodeTemplate::from_backend_name(&node.node_type) { Some(t) => t, None => { eprintln!("Unknown node type: {}", node.node_type); @@ -2101,62 +2101,6 @@ impl NodeGraphPane { } } - /// Helper: map backend node type string to frontend NodeTemplate - fn backend_type_to_template(node_type: &str) -> Option { - match node_type { - "MidiInput" => Some(NodeTemplate::MidiInput), - "AudioInput" => Some(NodeTemplate::AudioInput), - "AutomationInput" => Some(NodeTemplate::AutomationInput), - "Oscillator" => Some(NodeTemplate::Oscillator), - "WavetableOscillator" => Some(NodeTemplate::WavetableOscillator), - "FMSynth" => Some(NodeTemplate::FmSynth), - "NoiseGenerator" => Some(NodeTemplate::Noise), - "SimpleSampler" => Some(NodeTemplate::SimpleSampler), - "MultiSampler" => Some(NodeTemplate::MultiSampler), - "Filter" => Some(NodeTemplate::Filter), - "SVF" => Some(NodeTemplate::Svf), - "Gain" => Some(NodeTemplate::Gain), - "Echo" | "Delay" => Some(NodeTemplate::Echo), - "Reverb" => Some(NodeTemplate::Reverb), - "Chorus" => Some(NodeTemplate::Chorus), - "Flanger" => Some(NodeTemplate::Flanger), - "Phaser" => Some(NodeTemplate::Phaser), - "Distortion" => Some(NodeTemplate::Distortion), - "BitCrusher" => Some(NodeTemplate::BitCrusher), - "Compressor" => Some(NodeTemplate::Compressor), - "Limiter" => Some(NodeTemplate::Limiter), - "EQ" => Some(NodeTemplate::Eq), - "Pan" => Some(NodeTemplate::Pan), - "RingModulator" => Some(NodeTemplate::RingModulator), - "Vocoder" => Some(NodeTemplate::Vocoder), - "ADSR" => Some(NodeTemplate::Adsr), - "LFO" => Some(NodeTemplate::Lfo), - "Mixer" => Some(NodeTemplate::Mixer), - "Splitter" => Some(NodeTemplate::Splitter), - "Constant" => Some(NodeTemplate::Constant), - "MidiToCV" => Some(NodeTemplate::MidiToCv), - "AudioToCV" => Some(NodeTemplate::AudioToCv), - "Math" => Some(NodeTemplate::Math), - "SampleHold" => Some(NodeTemplate::SampleHold), - "SlewLimiter" => Some(NodeTemplate::SlewLimiter), - "Quantizer" => Some(NodeTemplate::Quantizer), - "EnvelopeFollower" => Some(NodeTemplate::EnvelopeFollower), - "BPMDetector" => Some(NodeTemplate::BpmDetector), - "Mod" => Some(NodeTemplate::Mod), - "Oscilloscope" => Some(NodeTemplate::Oscilloscope), - "Arpeggiator" => Some(NodeTemplate::Arpeggiator), - "Sequencer" => Some(NodeTemplate::Sequencer), - "Script" => Some(NodeTemplate::Script), - "Beat" => Some(NodeTemplate::Beat), - "VoiceAllocator" => Some(NodeTemplate::VoiceAllocator), - "Group" => Some(NodeTemplate::Group), - "TemplateInput" => Some(NodeTemplate::TemplateInput), - "TemplateOutput" => Some(NodeTemplate::TemplateOutput), - "AudioOutput" => Some(NodeTemplate::AudioOutput), - _ => None, - } - } - /// Helper: add a node to the editor state and return its frontend ID fn add_node_to_editor( &mut self, @@ -2573,6 +2517,31 @@ impl crate::panes::PaneRenderer for NodeGraphPane { self.handle_pending_sampler_load(load, shared); } + // Handle pending AmpSim model load from bottom_ui() + if let Some((node_id, backend_node_id)) = self.user_state.pending_amp_sim_load.take() { + if let Some(backend_track_id) = self.backend_track_id { + if let Some(path) = rfd::FileDialog::new() + .add_filter("NAM Model", &["nam"]) + .pick_file() + { + let model_name = path.file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "Model".to_string()); + if let Some(controller_arc) = &shared.audio_controller { + let mut controller = controller_arc.lock().unwrap(); + controller.amp_sim_load_model( + backend_track_id, + backend_node_id, + path.to_string_lossy().to_string(), + ); + } + if let Some(node) = self.state.graph.nodes.get_mut(node_id) { + node.user_data.nam_model_name = Some(model_name); + } + } + } + } + // Render sample import dialog if active if let Some(dialog) = &mut self.user_state.sample_import_dialog { let still_open = dialog.show(ui.ctx()); diff --git a/vendor/NeuralAudio b/vendor/NeuralAudio new file mode 160000 index 0000000..1eab464 --- /dev/null +++ b/vendor/NeuralAudio @@ -0,0 +1 @@ +Subproject commit 1eab4645f7073e752314b33946b69bfe3fbc01f9