Compare commits
3 Commits
728b88365d
...
725faa4445
| Author | SHA1 | Date |
|---|---|---|
|
|
725faa4445 | |
|
|
7e3f18c95b | |
|
|
3eba231447 |
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "vendor/NeuralAudio"]
|
||||
path = vendor/NeuralAudio
|
||||
url = https://github.com/mikeoliphant/NeuralAudio.git
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 1eab4645f7073e752314b33946b69bfe3fbc01f9
|
||||
Loading…
Reference in New Issue