Compare commits

...

3 Commits

Author SHA1 Message Date
Skyler Lehmkuhl 725faa4445 Load sample .nam amps 2026-02-21 10:25:55 -05:00
Skyler Lehmkuhl 7e3f18c95b Add amp sim 2026-02-21 09:43:03 -05:00
Skyler Lehmkuhl 3eba231447 deduplicate node list 2026-02-21 09:42:05 -05:00
13 changed files with 722 additions and 472 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

@ -39,6 +39,9 @@ serde_json = "1.0"
# BeamDSP scripting engine
beamdsp = { path = "../lightningbeam-ui/beamdsp" }
# Neural Amp Modeler FFI
nam-ffi = { path = "../nam-ffi" }
[dev-dependencies]
[profile.release]

View File

@ -1158,62 +1158,16 @@ impl Engine {
if let Some(graph) = graph {
// Create the node based on type
let node: Box<dyn crate::audio::node_graph::AudioNode> = 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<dyn crate::audio::node_graph::AudioNode> = 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::<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) => {
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));

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
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<dyn crate::audio::node_graph::AudioNode> = 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::<VoiceAllocatorNode>() {
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::<AmpSimNode>() {
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);
}

View File

@ -0,0 +1,200 @@
use crate::audio::midi::MidiEvent;
use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType};
use nam_ffi::NamModel;
use std::path::Path;
const PARAM_INPUT_GAIN: u32 = 0;
const PARAM_OUTPUT_GAIN: u32 = 1;
const PARAM_MIX: u32 = 2;
/// Guitar amp simulator node using Neural Amp Modeler (.nam) models.
pub struct AmpSimNode {
name: String,
input_gain: f32,
output_gain: f32,
mix: f32,
model: Option<NamModel>,
model_path: Option<String>,
// Mono scratch buffers for NAM processing (NAM is mono-only)
mono_in: Vec<f32>,
mono_out: Vec<f32>,
inputs: Vec<NodePort>,
outputs: Vec<NodePort>,
parameters: Vec<Parameter>,
}
impl AmpSimNode {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
let inputs = vec![NodePort::new("Audio In", SignalType::Audio, 0)];
let outputs = vec![NodePort::new("Audio Out", SignalType::Audio, 0)];
let parameters = vec![
Parameter::new(PARAM_INPUT_GAIN, "Input Gain", 0.0, 4.0, 1.0, ParameterUnit::Generic),
Parameter::new(PARAM_OUTPUT_GAIN, "Output Gain", 0.0, 4.0, 1.0, ParameterUnit::Generic),
Parameter::new(PARAM_MIX, "Mix", 0.0, 1.0, 1.0, ParameterUnit::Generic),
];
Self {
name,
input_gain: 1.0,
output_gain: 1.0,
mix: 1.0,
model: None,
model_path: None,
mono_in: Vec::new(),
mono_out: Vec::new(),
inputs,
outputs,
parameters,
}
}
/// Load a .nam model file. Call from the audio thread via command dispatch.
pub fn load_model(&mut self, path: &str) -> Result<(), String> {
let model_path = Path::new(path);
let mut model =
NamModel::from_file(model_path).map_err(|e| format!("{}", e))?;
model.set_max_buffer_size(1024);
self.model = Some(model);
self.model_path = Some(path.to_string());
Ok(())
}
/// Get the loaded model path (for preset serialization).
pub fn model_path(&self) -> Option<&str> {
self.model_path.as_deref()
}
}
impl AudioNode for AmpSimNode {
fn category(&self) -> NodeCategory {
NodeCategory::Effect
}
fn inputs(&self) -> &[NodePort] {
&self.inputs
}
fn outputs(&self) -> &[NodePort] {
&self.outputs
}
fn parameters(&self) -> &[Parameter] {
&self.parameters
}
fn set_parameter(&mut self, id: u32, value: f32) {
match id {
PARAM_INPUT_GAIN => self.input_gain = value.clamp(0.0, 4.0),
PARAM_OUTPUT_GAIN => self.output_gain = value.clamp(0.0, 4.0),
PARAM_MIX => self.mix = value.clamp(0.0, 1.0),
_ => {}
}
}
fn get_parameter(&self, id: u32) -> f32 {
match id {
PARAM_INPUT_GAIN => self.input_gain,
PARAM_OUTPUT_GAIN => self.output_gain,
PARAM_MIX => self.mix,
_ => 0.0,
}
}
fn process(
&mut self,
inputs: &[&[f32]],
outputs: &mut [&mut [f32]],
_midi_inputs: &[&[MidiEvent]],
_midi_outputs: &mut [&mut Vec<MidiEvent>],
_sample_rate: u32,
) {
if inputs.is_empty() || outputs.is_empty() {
return;
}
let input = inputs[0];
let output = &mut outputs[0];
let frames = input.len() / 2;
let output_frames = output.len() / 2;
let frames_to_process = frames.min(output_frames);
if let Some(ref mut model) = self.model {
// Ensure scratch buffers are large enough
if self.mono_in.len() < frames_to_process {
self.mono_in.resize(frames_to_process, 0.0);
self.mono_out.resize(frames_to_process, 0.0);
}
// Deinterleave stereo to mono (average L+R) and apply input gain
for frame in 0..frames_to_process {
let left = input[frame * 2];
let right = input[frame * 2 + 1];
self.mono_in[frame] = (left + right) * 0.5 * self.input_gain;
}
// Process through NAM model
model.process(
&self.mono_in[..frames_to_process],
&mut self.mono_out[..frames_to_process],
);
// Apply output gain, mix wet/dry, copy mono back to stereo
for frame in 0..frames_to_process {
let dry = (input[frame * 2] + input[frame * 2 + 1]) * 0.5;
let wet = self.mono_out[frame] * self.output_gain;
let mixed = dry * (1.0 - self.mix) + wet * self.mix;
output[frame * 2] = mixed;
output[frame * 2 + 1] = mixed;
}
} else {
// No model loaded — pass through unchanged
let samples = frames_to_process * 2;
output[..samples].copy_from_slice(&input[..samples]);
}
}
fn reset(&mut self) {
// No persistent filter state to reset
}
fn node_type(&self) -> &str {
"AmpSim"
}
fn name(&self) -> &str {
&self.name
}
fn clone_node(&self) -> Box<dyn AudioNode> {
// Cannot clone the NAM model (C++ pointer), so clone without model.
// The model will need to be reloaded via command if needed.
Box::new(Self {
name: self.name.clone(),
input_gain: self.input_gain,
output_gain: self.output_gain,
mix: self.mix,
model: None,
model_path: self.model_path.clone(),
mono_in: Vec::new(),
mono_out: Vec::new(),
inputs: self.inputs.clone(),
outputs: self.outputs.clone(),
parameters: self.parameters.clone(),
})
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@ -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<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

@ -127,6 +127,10 @@ pub struct SerializedNode {
/// For Script nodes: BeamDSP source code
#[serde(skip_serializing_if = "Option::is_none")]
pub script_source: Option<String>,
/// For AmpSim nodes: path to the .nam model file
#[serde(skip_serializing_if = "Option::is_none")]
pub nam_model_path: Option<String>,
}
/// Serialized group definition (frontend-only visual grouping, stored opaquely by backend)
@ -222,6 +226,7 @@ impl SerializedNode {
template_graph: None,
sample_data: None,
script_source: None,
nam_model_path: None,
}
}

View File

@ -186,6 +186,9 @@ pub enum Command {
/// Load audio sample data into a Script node's sample slot (track_id, node_id, slot_index, audio_data, sample_rate, name)
GraphSetScriptSample(TrackId, u32, usize, Vec<f32>, u32, String),
/// Load a NAM model into an AmpSim node (track_id, node_id, model_path)
AmpSimLoadModel(TrackId, u32, String),
/// Load a sample into a SimpleSampler node (track_id, node_id, file_path)
SamplerLoadSample(TrackId, u32, String),
/// Load a sample from the audio pool into a SimpleSampler node (track_id, node_id, pool_index)

View File

@ -1142,6 +1142,15 @@ dependencies = [
"error-code",
]
[[package]]
name = "cmake"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
dependencies = [
"cc",
]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
@ -1678,6 +1687,7 @@ dependencies = [
"memmap2",
"midir",
"midly",
"nam-ffi",
"pathdiff",
"petgraph 0.6.5",
"rand 0.8.5",
@ -3823,6 +3833,13 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "nam-ffi"
version = "0.1.0"
dependencies = [
"cmake",
]
[[package]]
name = "ndk"
version = "0.9.0"

View File

@ -50,6 +50,9 @@ notify-rust = "4.11"
[profile.dev.package.daw-backend]
opt-level = 2
[profile.dev.package.nam-ffi]
opt-level = 2
[profile.dev.package.beamdsp]
opt-level = 2

View File

@ -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<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
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<NodeTemplate> {
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<NodeTemplate> {
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<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 }
@ -180,6 +191,7 @@ impl NodeData {
ui_declaration: None,
sample_slot_names: Vec::new(),
script_sample_names: HashMap::new(),
nam_model_name: None,
}
}
}
@ -212,6 +224,21 @@ pub enum PendingScriptSampleLoad {
FromFile { node_id: NodeId, backend_node_id: u32, slot_index: usize },
}
/// Info about an available NAM model for amp sim selection
pub struct NamModelInfo {
pub name: String,
pub path: String,
pub is_bundled: bool,
}
/// Pending AmpSim model load request from bottom_ui(), handled by the node graph pane
pub enum PendingAmpSimLoad {
/// Load a known model by path (from bundled list or previously loaded)
FromPath { node_id: NodeId, backend_node_id: u32, path: String, name: String },
/// Open file dialog to browse for a .nam file
FromFile { node_id: NodeId, backend_node_id: u32 },
}
/// Pending sampler load request from bottom_ui(), handled by the node graph pane
pub enum PendingSamplerLoad {
/// Load a single clip from the audio pool into a SimpleSampler
@ -265,6 +292,12 @@ 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<crate::sample_import_dialog::SampleImportDialog>,
/// Pending AmpSim model load — triggers file dialog or direct load
pub pending_amp_sim_load: Option<PendingAmpSimLoad>,
/// Available NAM models for amp sim selection, populated before draw
pub available_nam_models: Vec<NamModelInfo>,
/// Search text for the NAM model picker popup
pub nam_search_text: String,
}
impl Default for GraphState {
@ -288,6 +321,9 @@ impl Default for GraphState {
draw_vms: HashMap::new(),
pending_draw_param_changes: Vec::new(),
sample_import_dialog: None,
pending_amp_sim_load: None,
available_nam_models: Vec::new(),
nam_search_text: String::new(),
}
}
}
@ -393,87 +429,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 +695,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 +1419,80 @@ 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("Select Model...");
let button = ui.button(button_text);
if button.clicked() {
user_state.nam_search_text.clear();
}
let popup_id = egui::Popup::default_response_id(&button);
let mut close_popup = false;
egui::Popup::from_toggle_button_response(&button)
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.width(200.0)
.show(|ui| {
let search_width = ui.available_width();
ui.add_sized([search_width, 0.0], egui::TextEdit::singleline(&mut user_state.nam_search_text).hint_text("Search..."));
ui.separator();
let search = user_state.nam_search_text.to_lowercase();
let bundled: Vec<&NamModelInfo> = user_state.available_nam_models.iter()
.filter(|m| m.is_bundled && (search.is_empty() || m.name.to_lowercase().contains(&search)))
.collect();
let user_models: Vec<&NamModelInfo> = user_state.available_nam_models.iter()
.filter(|m| !m.is_bundled && (search.is_empty() || m.name.to_lowercase().contains(&search)))
.collect();
if !bundled.is_empty() {
ui.label(egui::RichText::new("Bundled").small().weak());
let items = bundled.iter().map(|m| {
let selected = self.nam_model_name.as_deref() == Some(m.name.as_str());
(selected, m.name.as_str())
});
if let Some(idx) = widgets::scrollable_list(ui, 200.0, items) {
let model = bundled[idx];
user_state.pending_amp_sim_load = Some(PendingAmpSimLoad::FromPath {
node_id, backend_node_id,
path: model.path.clone(),
name: model.name.clone(),
});
close_popup = true;
}
}
if !user_models.is_empty() {
ui.separator();
ui.label(egui::RichText::new("User").small().weak());
let items = user_models.iter().map(|m| {
let selected = self.nam_model_name.as_deref() == Some(m.name.as_str());
(selected, m.name.as_str())
});
if let Some(idx) = widgets::scrollable_list(ui, 200.0, items) {
let model = user_models[idx];
user_state.pending_amp_sim_load = Some(PendingAmpSimLoad::FromPath {
node_id, backend_node_id,
path: model.path.clone(),
name: model.name.clone(),
});
close_popup = true;
}
}
ui.separator();
if ui.button("Open...").clicked() {
user_state.pending_amp_sim_load = Some(PendingAmpSimLoad::FromFile {
node_id, backend_node_id,
});
close_popup = true;
}
});
if close_popup {
egui::Popup::close_id(ui.ctx(), popup_id);
}
} else {
ui.label("");
}
@ -1677,7 +1721,7 @@ impl NodeTemplateIter for VoiceAllocatorNodeTemplates {
type Item = NodeTemplate;
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
templates.retain(|t| *t != NodeTemplate::VoiceAllocator);
templates.push(NodeTemplate::TemplateInput);
@ -1690,7 +1734,7 @@ impl NodeTemplateIter for SubgraphNodeTemplates {
type Item = NodeTemplate;
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::TemplateOutput);
templates
@ -1701,63 +1745,6 @@ impl NodeTemplateIter for AllNodeTemplates {
type Item = NodeTemplate;
fn all_kinds(&self) -> Vec<Self::Item> {
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()
}
}

View File

@ -8,7 +8,7 @@ pub mod backend;
pub mod graph_data;
use backend::{BackendNodeId, GraphBackend};
use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType};
use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NamModelInfo, NodeData, NodeTemplate, PendingAmpSimLoad, ValueType};
use super::NodePath;
use eframe::egui;
use egui_node_graph2::*;
@ -403,6 +403,20 @@ impl NodeGraphPane {
);
self.node_id_map.insert(node_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, node_id);
// Auto-load default NAM model for new AmpSim nodes
if node_type == "AmpSim" {
if let Some(model) = self.user_state.available_nam_models.iter().find(|m| m.is_bundled) {
controller.amp_sim_load_model(
backend_track_id,
backend_node.id,
model.path.clone(),
);
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
node.user_data.nam_model_name = Some(model.name.clone());
}
}
}
}
}
}
@ -665,6 +679,20 @@ impl NodeGraphPane {
);
self.node_id_map.insert(frontend_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, frontend_id);
// Auto-load default NAM model for new AmpSim nodes
if node_type == "AmpSim" {
if let Some(model) = self.user_state.available_nam_models.iter().find(|m| m.is_bundled) {
controller.amp_sim_load_model(
backend_track_id,
backend_node.id,
model.path.clone(),
);
if let Some(node) = self.state.graph.nodes.get_mut(frontend_id) {
node.user_data.nam_model_name = Some(model.name.clone());
}
}
}
}
}
}
@ -1385,7 +1413,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 +1864,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 +2129,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
fn add_node_to_editor(
&mut self,
@ -2525,6 +2497,46 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
.collect();
self.user_state.available_scripts.sort_by(|a, b| a.1.to_lowercase().cmp(&b.1.to_lowercase()));
// Bundled NAM models — discover once and cache
if self.user_state.available_nam_models.is_empty() {
let bundled_dirs = [
std::env::current_exe().ok()
.and_then(|p| p.parent().map(|d| d.join("models")))
.unwrap_or_default(),
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../vendor/NeuralAudio/Utils/Models"),
];
for dir in &bundled_dirs {
if let Ok(canon) = dir.canonicalize() {
if canon.is_dir() {
for entry in std::fs::read_dir(&canon).into_iter().flatten().flatten() {
let path = entry.path();
if path.extension().map_or(false, |e| e == "nam") {
let stem = path.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
// Skip LSTM variants (performance alternates, not separate amps)
if stem.ends_with("-LSTM") {
continue;
}
// Clean up display name: remove "-WaveNet" suffix
let name = stem.strip_suffix("-WaveNet")
.unwrap_or(&stem)
.to_string();
self.user_state.available_nam_models.push(NamModelInfo {
name,
path: path.to_string_lossy().to_string(),
is_bundled: true,
});
}
}
break; // use first directory found
}
}
}
self.user_state.available_nam_models.sort_by(|a, b| a.name.cmp(&b.name));
}
// Node backend ID map
self.user_state.node_backend_ids = self.node_id_map.iter()
.map(|(&node_id, backend_id)| {
@ -2573,6 +2585,51 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
self.handle_pending_sampler_load(load, shared);
}
// Handle pending AmpSim model load from bottom_ui()
if let Some(load) = self.user_state.pending_amp_sim_load.take() {
if let Some(backend_track_id) = self.backend_track_id {
if let Some(controller_arc) = &shared.audio_controller {
match load {
PendingAmpSimLoad::FromPath { node_id, backend_node_id, path, name } => {
controller_arc.lock().unwrap().amp_sim_load_model(
backend_track_id, backend_node_id, path,
);
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
node.user_data.nam_model_name = Some(name);
}
}
PendingAmpSimLoad::FromFile { node_id, backend_node_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());
controller_arc.lock().unwrap().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.clone());
}
// Add user-loaded model to the available list if not already present
let path_str = path.to_string_lossy().to_string();
if !self.user_state.available_nam_models.iter().any(|m| m.path == path_str) {
self.user_state.available_nam_models.push(NamModelInfo {
name: model_name,
path: path_str,
is_bundled: false,
});
}
}
}
}
}
}
}
// Render sample import dialog if active
if let Some(dialog) = &mut self.user_state.sample_import_dialog {
let still_open = dialog.show(ui.ctx());

1
vendor/NeuralAudio vendored Submodule

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