deduplicate node list

This commit is contained in:
Skyler Lehmkuhl 2026-02-21 09:42:05 -05:00
parent 728b88365d
commit 3eba231447
7 changed files with 313 additions and 471 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "vendor/NeuralAudio"]
path = vendor/NeuralAudio
url = https://github.com/mikeoliphant/NeuralAudio.git

View File

@ -1158,62 +1158,16 @@ impl Engine {
if let Some(graph) = graph { if let Some(graph) = graph {
// Create the node based on type // Create the node based on type
let node: Box<dyn crate::audio::node_graph::AudioNode> = match node_type.as_str() { let node = match crate::audio::node_graph::nodes::create_node(&node_type, self.sample_rate, 8192) {
"Oscillator" => Box::new(OscillatorNode::new("Oscillator".to_string())), Some(n) => n,
"Gain" => Box::new(GainNode::new("Gain".to_string())), None => {
"Mixer" => Box::new(MixerNode::new("Mixer".to_string())), let _ = self.event_tx.push(AudioEvent::GraphConnectionError(
"Filter" => Box::new(FilterNode::new("Filter".to_string())), track_id,
"SVF" => Box::new(SVFNode::new("SVF".to_string())), format!("Unknown node type: {}", node_type)
"ADSR" => Box::new(ADSRNode::new("ADSR".to_string())), ));
"LFO" => Box::new(LFONode::new("LFO".to_string())), return;
"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;
}
};
// Add node to graph // Add node to graph
let node_idx = graph.add_node(node); let node_idx = graph.add_node(node);
@ -1250,53 +1204,9 @@ impl Engine {
let va_idx = NodeIndex::new(voice_allocator_id as usize); let va_idx = NodeIndex::new(voice_allocator_id as usize);
// Create the node // Create the node
let node: Box<dyn crate::audio::node_graph::AudioNode> = match node_type.as_str() { let node = match crate::audio::node_graph::nodes::create_node(&node_type, self.sample_rate, 8192) {
"Oscillator" => Box::new(OscillatorNode::new("Oscillator".to_string())), Some(n) => n,
"Gain" => Box::new(GainNode::new("Gain".to_string())), None => {
"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 _ = self.event_tx.push(AudioEvent::GraphConnectionError( let _ = self.event_tx.push(AudioEvent::GraphConnectionError(
track_id, track_id,
format!("Unknown node type: {}", node_type) 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::<AmpSimNode>() {
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) => { Command::SamplerLoadSample(track_id, node_id, file_path) => {
use crate::audio::node_graph::nodes::SimpleSamplerNode; 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)); 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 /// Load a sample into a SimpleSampler node
pub fn sampler_load_sample(&mut self, track_id: TrackId, node_id: u32, file_path: String) { 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)); let _ = self.command_tx.push(Command::SamplerLoadSample(track_id, node_id, file_path));

View File

@ -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::<AmpSimNode>() {
serialized.nam_model_path = amp_sim.model_path().map(|s| s.to_string());
}
}
// Save position if available // Save position if available
if let Some(pos) = self.get_node_position(node_idx) { if let Some(pos) = self.get_node_position(node_idx) {
serialized.set_position(pos.0, pos.1); serialized.set_position(pos.0, pos.1);
@ -983,66 +991,19 @@ impl AudioGraph {
// Create all nodes // Create all nodes
for serialized_node in &preset.nodes { for serialized_node in &preset.nodes {
// Create the node based on type // Create the node based on type
let node: Box<dyn crate::audio::node_graph::AudioNode> = match serialized_node.node_type.as_str() { let mut node = crate::audio::node_graph::nodes::create_node(&serialized_node.node_type, sample_rate, buffer_size)
"Oscillator" => Box::new(OscillatorNode::new("Oscillator")), .ok_or_else(|| format!("Unknown node type: {}", serialized_node.node_type))?;
"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);
// If there's a template graph, deserialize and set it // VoiceAllocator needs its template graph deserialized and set
if let Some(ref template_preset) = serialized_node.template_graph { 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::<VoiceAllocatorNode>() {
let template_graph = Self::from_preset(template_preset, sample_rate, buffer_size, preset_base_path)?; let template_graph = Self::from_preset(template_preset, sample_rate, buffer_size, preset_base_path)?;
*va.template_graph_mut() = template_graph; *va.template_graph_mut() = template_graph;
va.rebuild_voices(); 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); let node_idx = graph.add_node(node);
index_map.insert(serialized_node.id, node_idx); 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::<AmpSimNode>() {
if let Err(e) = amp_sim.load_model(&resolved_path) {
eprintln!("Warning: failed to load NAM model {}: {}", resolved_path, e);
}
}
}
}
}
// Restore position // Restore position
graph.set_node_position(node_idx, serialized_node.position.0, serialized_node.position.1); graph.set_node_position(node_idx, serialized_node.position.0, serialized_node.position.1);
} }

View File

@ -1,3 +1,4 @@
mod amp_sim;
mod adsr; mod adsr;
mod arpeggiator; mod arpeggiator;
mod audio_input; mod audio_input;
@ -45,6 +46,7 @@ mod vocoder;
mod voice_allocator; mod voice_allocator;
mod wavetable_oscillator; mod wavetable_oscillator;
pub use amp_sim::AmpSimNode;
pub use adsr::ADSRNode; pub use adsr::ADSRNode;
pub use arpeggiator::ArpeggiatorNode; pub use arpeggiator::ArpeggiatorNode;
pub use audio_input::AudioInputNode; pub use audio_input::AudioInputNode;
@ -91,3 +93,61 @@ pub use template_io::{TemplateInputNode, TemplateOutputNode};
pub use vocoder::VocoderNode; pub use vocoder::VocoderNode;
pub use voice_allocator::VoiceAllocatorNode; pub use voice_allocator::VoiceAllocatorNode;
pub use wavetable_oscillator::WavetableOscillatorNode; 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<Box<dyn super::AudioNode>> {
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,
})
}

View File

@ -16,132 +16,140 @@ pub enum DataType {
CV, CV,
} }
/// Node templates - types of nodes that can be created /// Macro that defines `NodeTemplate` enum and generates metadata methods from a single table.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] ///
pub enum NodeTemplate { /// Each row: `variant, backend_name, display_label, category, in_finder;`
// Inputs ///
MidiInput, /// Generated methods:
AudioInput, /// - `backend_type_name() -> &'static str`
AutomationInput, /// - `display_label() -> &'static str` (used by `node_finder_label`)
Beat, /// - `category() -> &'static str` (used by `node_finder_categories`)
/// - `in_finder() -> bool`
/// - `from_backend_name(s: &str) -> Option<NodeTemplate>`
/// - `all_finder_kinds() -> Vec<NodeTemplate>` (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 impl NodeTemplate {
Oscillator, /// Returns the backend-compatible type name string (matches daw-backend match arms)
WavetableOscillator, pub fn backend_type_name(&self) -> &'static str {
FmSynth, match self {
Noise, $(NodeTemplate::$variant => $backend),+
SimpleSampler, }
MultiSampler, }
// Effects /// Display label for the node finder
Filter, fn display_label(&self) -> &'static str {
Svf, match self {
Gain, $(NodeTemplate::$variant => $label),+
Echo, }
Reverb, }
Chorus,
Flanger,
Phaser,
Distortion,
BitCrusher,
Compressor,
Limiter,
Eq,
Pan,
RingModulator,
Vocoder,
// Utilities /// Category for the node finder
Adsr, fn category(&self) -> &'static str {
Lfo, match self {
Mixer, $(NodeTemplate::$variant => $category),+
Splitter, }
Constant, }
MidiToCv,
AudioToCv,
Arpeggiator,
Sequencer,
Math,
SampleHold,
SlewLimiter,
Quantizer,
EnvelopeFollower,
BpmDetector,
Mod,
// Scripting /// Whether this node appears in the node finder
Script, #[allow(dead_code)]
fn in_finder(&self) -> bool {
match self {
$(NodeTemplate::$variant => $in_finder),+
}
}
// Analysis /// Map a backend type name string to a NodeTemplate variant.
Oscilloscope, ///
/// Handles canonical names from the table plus legacy aliases.
pub fn from_backend_name(s: &str) -> Option<NodeTemplate> {
match s {
$($backend => Some(NodeTemplate::$variant),)+
// Legacy / alternate aliases
"Delay" => Some(NodeTemplate::Echo),
"BPMDetector" => Some(NodeTemplate::BpmDetector),
_ => None,
}
}
// Advanced /// All node templates that should appear in the default node finder
VoiceAllocator, pub fn all_finder_kinds() -> Vec<NodeTemplate> {
Group, let mut v = Vec::new();
$(if $in_finder { v.push(NodeTemplate::$variant); })+
// Subgraph I/O (only visible when editing inside a container node) v
TemplateInput, }
TemplateOutput, }
};
// Outputs
AudioOutput,
} }
impl NodeTemplate { node_templates! {
/// Returns the backend-compatible type name string (matches daw-backend match arms) // Inputs
pub fn backend_type_name(&self) -> &'static str { MidiInput, "MidiInput", "MIDI Input", "Inputs", true;
match self { AudioInput, "AudioInput", "Audio Input", "Inputs", true;
NodeTemplate::MidiInput => "MidiInput", AutomationInput, "AutomationInput", "Automation Input", "Inputs", true;
NodeTemplate::AudioInput => "AudioInput", Beat, "Beat", "Beat", "Inputs", true;
NodeTemplate::AutomationInput => "AutomationInput", // Generators
NodeTemplate::Oscillator => "Oscillator", Oscillator, "Oscillator", "Oscillator", "Generators", true;
NodeTemplate::WavetableOscillator => "WavetableOscillator", WavetableOscillator,"WavetableOscillator","Wavetable Oscillator","Generators", true;
NodeTemplate::FmSynth => "FMSynth", FmSynth, "FMSynth", "FM Synth", "Generators", true;
NodeTemplate::Noise => "NoiseGenerator", Noise, "NoiseGenerator", "Noise Generator", "Generators", true;
NodeTemplate::SimpleSampler => "SimpleSampler", SimpleSampler, "SimpleSampler", "Simple Sampler", "Generators", true;
NodeTemplate::MultiSampler => "MultiSampler", MultiSampler, "MultiSampler", "Multi Sampler", "Generators", true;
NodeTemplate::Filter => "Filter", // Effects
NodeTemplate::Svf => "SVF", Filter, "Filter", "Filter", "Effects", true;
NodeTemplate::Gain => "Gain", Svf, "SVF", "SVF", "Effects", true;
NodeTemplate::Echo => "Echo", Gain, "Gain", "Gain", "Effects", true;
NodeTemplate::Reverb => "Reverb", Echo, "Echo", "Echo", "Effects", true;
NodeTemplate::Chorus => "Chorus", Reverb, "Reverb", "Reverb", "Effects", true;
NodeTemplate::Flanger => "Flanger", Chorus, "Chorus", "Chorus", "Effects", true;
NodeTemplate::Phaser => "Phaser", Flanger, "Flanger", "Flanger", "Effects", true;
NodeTemplate::Distortion => "Distortion", Phaser, "Phaser", "Phaser", "Effects", true;
NodeTemplate::BitCrusher => "BitCrusher", Distortion, "Distortion", "Distortion", "Effects", true;
NodeTemplate::Compressor => "Compressor", AmpSim, "AmpSim", "Amp Sim", "Effects", true;
NodeTemplate::Limiter => "Limiter", BitCrusher, "BitCrusher", "Bit Crusher", "Effects", true;
NodeTemplate::Eq => "EQ", Compressor, "Compressor", "Compressor", "Effects", true;
NodeTemplate::Pan => "Pan", Limiter, "Limiter", "Limiter", "Effects", true;
NodeTemplate::RingModulator => "RingModulator", Eq, "EQ", "EQ", "Effects", true;
NodeTemplate::Vocoder => "Vocoder", Pan, "Pan", "Pan", "Effects", true;
NodeTemplate::Adsr => "ADSR", RingModulator, "RingModulator", "Ring Modulator", "Effects", true;
NodeTemplate::Lfo => "LFO", Vocoder, "Vocoder", "Vocoder", "Effects", true;
NodeTemplate::Mixer => "Mixer", // Utilities
NodeTemplate::Splitter => "Splitter", Adsr, "ADSR", "ADSR Envelope", "Utilities", true;
NodeTemplate::Constant => "Constant", Lfo, "LFO", "LFO", "Utilities", true;
NodeTemplate::MidiToCv => "MidiToCV", Mixer, "Mixer", "Mixer", "Utilities", true;
NodeTemplate::AudioToCv => "AudioToCV", Splitter, "Splitter", "Splitter", "Utilities", true;
NodeTemplate::Arpeggiator => "Arpeggiator", Constant, "Constant", "Constant", "Utilities", true;
NodeTemplate::Sequencer => "Sequencer", MidiToCv, "MidiToCV", "MIDI to CV", "Utilities", true;
NodeTemplate::Math => "Math", AudioToCv, "AudioToCV", "Audio to CV", "Utilities", true;
NodeTemplate::SampleHold => "SampleHold", Arpeggiator, "Arpeggiator", "Arpeggiator", "Utilities", true;
NodeTemplate::SlewLimiter => "SlewLimiter", Sequencer, "Sequencer", "Step Sequencer", "Utilities", true;
NodeTemplate::Quantizer => "Quantizer", Math, "Math", "Math", "Utilities", true;
NodeTemplate::EnvelopeFollower => "EnvelopeFollower", SampleHold, "SampleHold", "Sample & Hold", "Utilities", true;
NodeTemplate::BpmDetector => "BpmDetector", SlewLimiter, "SlewLimiter", "Slew Limiter", "Utilities", true;
NodeTemplate::Beat => "Beat", Quantizer, "Quantizer", "Quantizer", "Utilities", true;
NodeTemplate::Script => "Script", EnvelopeFollower, "EnvelopeFollower", "Envelope Follower", "Utilities", true;
NodeTemplate::Mod => "Mod", BpmDetector, "BpmDetector", "BPM Detector", "Utilities", true;
NodeTemplate::Oscilloscope => "Oscilloscope", Mod, "Mod", "Modulator", "Utilities", true;
NodeTemplate::VoiceAllocator => "VoiceAllocator", // Analysis
NodeTemplate::Group => "Group", Oscilloscope, "Oscilloscope", "Oscilloscope", "Analysis", true;
NodeTemplate::TemplateInput => "TemplateInput", // Advanced
NodeTemplate::TemplateOutput => "TemplateOutput", VoiceAllocator, "VoiceAllocator", "Voice Allocator", "Advanced", true;
NodeTemplate::AudioOutput => "AudioOutput", 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 /// Custom node data
@ -166,6 +174,9 @@ pub struct NodeData {
/// Display names of loaded samples per slot (slot_index → display name) /// Display names of loaded samples per slot (slot_index → display name)
#[serde(skip)] #[serde(skip)]
pub script_sample_names: HashMap<usize, String>, pub script_sample_names: HashMap<usize, String>,
/// Display name of loaded NAM model (for AmpSim nodes)
#[serde(default)]
pub nam_model_name: Option<String>,
} }
fn default_root_note() -> u8 { 69 } fn default_root_note() -> u8 { 69 }
@ -180,6 +191,7 @@ impl NodeData {
ui_declaration: None, ui_declaration: None,
sample_slot_names: Vec::new(), sample_slot_names: Vec::new(),
script_sample_names: HashMap::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)>, pub pending_draw_param_changes: Vec<(NodeId, u32, f32)>,
/// Active sample import dialog (folder import with heuristic mapping) /// Active sample import dialog (folder import with heuristic mapping)
pub sample_import_dialog: Option<crate::sample_import_dialog::SampleImportDialog>, pub sample_import_dialog: Option<crate::sample_import_dialog::SampleImportDialog>,
/// 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 { impl Default for GraphState {
@ -288,6 +302,7 @@ impl Default for GraphState {
draw_vms: HashMap::new(), draw_vms: HashMap::new(),
pending_draw_param_changes: Vec::new(), pending_draw_param_changes: Vec::new(),
sample_import_dialog: None, sample_import_dialog: None,
pending_amp_sim_load: None,
} }
} }
} }
@ -393,87 +408,11 @@ impl NodeTemplateTrait for NodeTemplate {
type CategoryType = &'static str; type CategoryType = &'static str;
fn node_finder_label(&self, _user_state: &mut Self::UserState) -> std::borrow::Cow<'_, str> { fn node_finder_label(&self, _user_state: &mut Self::UserState) -> std::borrow::Cow<'_, str> {
match self { self.display_label().into()
// 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(),
}
} }
fn node_finder_categories(&self, _user_state: &mut Self::UserState) -> Vec<&'static str> { fn node_finder_categories(&self, _user_state: &mut Self::UserState) -> Vec<&'static str> {
match self { vec![self.category()]
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"],
}
} }
fn node_graph_label(&self, user_state: &mut Self::UserState) -> String { 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); 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); 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 => { 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, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_input_param(node_id, "Bit Depth".into(), DataType::CV, 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, &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 { } else {
ui.label(""); ui.label("");
} }
@ -1677,7 +1632,7 @@ impl NodeTemplateIter for VoiceAllocatorNodeTemplates {
type Item = NodeTemplate; type Item = NodeTemplate;
fn all_kinds(&self) -> Vec<Self::Item> { fn all_kinds(&self) -> Vec<Self::Item> {
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 // VA nodes can't be nested — signals inside a VA are monophonic
templates.retain(|t| *t != NodeTemplate::VoiceAllocator); templates.retain(|t| *t != NodeTemplate::VoiceAllocator);
templates.push(NodeTemplate::TemplateInput); templates.push(NodeTemplate::TemplateInput);
@ -1690,7 +1645,7 @@ impl NodeTemplateIter for SubgraphNodeTemplates {
type Item = NodeTemplate; type Item = NodeTemplate;
fn all_kinds(&self) -> Vec<Self::Item> { fn all_kinds(&self) -> Vec<Self::Item> {
let mut templates = AllNodeTemplates.all_kinds(); let mut templates = NodeTemplate::all_finder_kinds();
templates.push(NodeTemplate::TemplateInput); templates.push(NodeTemplate::TemplateInput);
templates.push(NodeTemplate::TemplateOutput); templates.push(NodeTemplate::TemplateOutput);
templates templates
@ -1701,63 +1656,6 @@ impl NodeTemplateIter for AllNodeTemplates {
type Item = NodeTemplate; type Item = NodeTemplate;
fn all_kinds(&self) -> Vec<Self::Item> { fn all_kinds(&self) -> Vec<Self::Item> {
vec![ NodeTemplate::all_finder_kinds()
// 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,
]
} }
} }

View File

@ -1385,7 +1385,7 @@ impl NodeGraphPane {
// Create nodes in frontend // Create nodes in frontend
self.pending_script_resolutions.clear(); self.pending_script_resolutions.clear();
for node in &graph_state.nodes { 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, Some(t) => t,
None => { None => {
eprintln!("Unknown node type: {}", node.node_type); eprintln!("Unknown node type: {}", node.node_type);
@ -1836,7 +1836,7 @@ impl NodeGraphPane {
continue; 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, Some(t) => t,
None => { None => {
eprintln!("Unknown node type: {}", node.node_type); 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<NodeTemplate> {
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 /// Helper: add a node to the editor state and return its frontend ID
fn add_node_to_editor( fn add_node_to_editor(
&mut self, &mut self,
@ -2573,6 +2517,31 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
self.handle_pending_sampler_load(load, shared); 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 // Render sample import dialog if active
if let Some(dialog) = &mut self.user_state.sample_import_dialog { if let Some(dialog) = &mut self.user_state.sample_import_dialog {
let still_open = dialog.show(ui.ctx()); let still_open = dialog.show(ui.ctx());

1
vendor/NeuralAudio vendored Submodule

@ -0,0 +1 @@
Subproject commit 1eab4645f7073e752314b33946b69bfe3fbc01f9