diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index b07f4ac..b50a31c 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -1710,6 +1710,7 @@ impl Engine { Command::AmpSimLoadModel(track_id, node_id, model_path) => { use crate::audio::node_graph::nodes::AmpSimNode; + eprintln!("[AmpSim] Loading model: {:?} for track {:?} node {}", model_path, track_id, node_id); 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), @@ -1719,8 +1720,16 @@ impl Engine { let node_idx = NodeIndex::new(node_id as usize); if let Some(graph_node) = graph.get_graph_node_mut(node_idx) { if let Some(amp_sim) = graph_node.node.as_any_mut().downcast_mut::() { - if let Err(e) = amp_sim.load_model(&model_path) { - eprintln!("Failed to load NAM model: {}", e); + let result = if let Some(bundled_name) = model_path.strip_prefix("bundled:") { + eprintln!("[AmpSim] Loading bundled model: {}", bundled_name); + amp_sim.load_bundled_model(bundled_name) + } else { + eprintln!("[AmpSim] Loading model from file: {}", model_path); + amp_sim.load_model(&model_path) + }; + match &result { + Ok(()) => eprintln!("[AmpSim] Model loaded successfully"), + Err(e) => eprintln!("[AmpSim] Failed to load NAM model: {}", e), } } } diff --git a/daw-backend/src/audio/node_graph/graph.rs b/daw-backend/src/audio/node_graph/graph.rs index 51df5d8..c5bb6b2 100644 --- a/daw-backend/src/audio/node_graph/graph.rs +++ b/daw-backend/src/audio/node_graph/graph.rs @@ -1140,11 +1140,20 @@ impl AudioGraph { 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); + eprintln!("[AmpSim] Preset restore: nam_model_path={:?}", model_path); if let Some(graph_node) = graph.graph.node_weight_mut(node_idx) { if let Some(amp_sim) = graph_node.node.as_any_mut().downcast_mut::() { - if let Err(e) = amp_sim.load_model(&resolved_path) { - eprintln!("Warning: failed to load NAM model {}: {}", resolved_path, e); + let result = if let Some(bundled_name) = model_path.strip_prefix("bundled:") { + eprintln!("[AmpSim] Preset: loading bundled model {:?}", bundled_name); + amp_sim.load_bundled_model(bundled_name) + } else { + let resolved_path = resolve_sample_path(model_path); + eprintln!("[AmpSim] Preset: loading from file {:?}", resolved_path); + amp_sim.load_model(&resolved_path) + }; + match &result { + Ok(()) => eprintln!("[AmpSim] Preset: model loaded successfully"), + Err(e) => eprintln!("[AmpSim] Preset: failed to load NAM model: {}", e), } } } diff --git a/daw-backend/src/audio/node_graph/nodes/adsr.rs b/daw-backend/src/audio/node_graph/nodes/adsr.rs index b594c1a..b4bdbe9 100644 --- a/daw-backend/src/audio/node_graph/nodes/adsr.rs +++ b/daw-backend/src/audio/node_graph/nodes/adsr.rs @@ -5,6 +5,7 @@ const PARAM_ATTACK: u32 = 0; const PARAM_DECAY: u32 = 1; const PARAM_SUSTAIN: u32 = 2; const PARAM_RELEASE: u32 = 3; +const PARAM_CURVE: u32 = 4; #[derive(Debug, Clone, Copy, PartialEq)] enum EnvelopeStage { @@ -15,6 +16,19 @@ enum EnvelopeStage { Release, } +/// Curve shape for envelope segments +#[derive(Debug, Clone, Copy, PartialEq)] +enum CurveType { + Linear, + Exponential, +} + +impl CurveType { + fn from_f32(v: f32) -> Self { + if v >= 0.5 { CurveType::Exponential } else { CurveType::Linear } + } +} + /// ADSR Envelope Generator /// Outputs a CV signal (0.0-1.0) based on gate input and ADSR parameters pub struct ADSRNode { @@ -23,8 +37,15 @@ pub struct ADSRNode { decay: f32, // seconds sustain: f32, // level (0.0-1.0) release: f32, // seconds + curve: CurveType, stage: EnvelopeStage, level: f32, // current envelope level + /// For exponential curves: the coefficient per sample (computed on stage entry) + exp_coeff: f32, + /// For exponential curves: the base level when the stage started + exp_base: f32, + /// For exponential curves: the target level + exp_target: f32, gate_was_high: bool, inputs: Vec, outputs: Vec, @@ -48,6 +69,7 @@ impl ADSRNode { Parameter::new(PARAM_DECAY, "Decay", 0.001, 5.0, 0.1, ParameterUnit::Time), Parameter::new(PARAM_SUSTAIN, "Sustain", 0.0, 1.0, 0.7, ParameterUnit::Generic), Parameter::new(PARAM_RELEASE, "Release", 0.001, 5.0, 0.2, ParameterUnit::Time), + Parameter::new(PARAM_CURVE, "Curve", 0.0, 1.0, 0.0, ParameterUnit::Generic), ]; Self { @@ -56,8 +78,12 @@ impl ADSRNode { decay: 0.1, sustain: 0.7, release: 0.2, + curve: CurveType::Linear, stage: EnvelopeStage::Idle, level: 0.0, + exp_coeff: 0.0, + exp_base: 0.0, + exp_target: 0.0, gate_was_high: false, inputs, outputs, @@ -89,6 +115,7 @@ impl AudioNode for ADSRNode { PARAM_DECAY => self.decay = value.clamp(0.001, 5.0), PARAM_SUSTAIN => self.sustain = value.clamp(0.0, 1.0), PARAM_RELEASE => self.release = value.clamp(0.001, 5.0), + PARAM_CURVE => self.curve = CurveType::from_f32(value), _ => {} } } @@ -99,6 +126,7 @@ impl AudioNode for ADSRNode { PARAM_DECAY => self.decay, PARAM_SUSTAIN => self.sustain, PARAM_RELEASE => self.release, + PARAM_CURVE => match self.curve { CurveType::Linear => 0.0, CurveType::Exponential => 1.0 }, _ => 0.0, } } @@ -130,9 +158,23 @@ impl AudioNode for ADSRNode { if gate_high && !self.gate_was_high { // Note on: Start attack self.stage = EnvelopeStage::Attack; + if self.curve == CurveType::Exponential { + // For exponential attack, compute coefficient for ~5 time constants + // We overshoot the target slightly so the curve reaches 1.0 naturally + let samples = self.attack * sample_rate_f32; + self.exp_coeff = (-5.0 / samples).exp(); + self.exp_base = self.level; + self.exp_target = 1.0; + } } else if !gate_high && self.gate_was_high { // Note off: Start release self.stage = EnvelopeStage::Release; + if self.curve == CurveType::Exponential { + let samples = self.release * sample_rate_f32; + self.exp_coeff = (-5.0 / samples).exp(); + self.exp_base = self.level; + self.exp_target = 0.0; + } } self.gate_was_high = gate_high; @@ -142,22 +184,51 @@ impl AudioNode for ADSRNode { self.level = 0.0; } EnvelopeStage::Attack => { - // Rise from current level to 1.0 - let increment = 1.0 / (self.attack * sample_rate_f32); - self.level += increment; - if self.level >= 1.0 { - self.level = 1.0; - self.stage = EnvelopeStage::Decay; + match self.curve { + CurveType::Linear => { + let increment = 1.0 / (self.attack * sample_rate_f32); + self.level += increment; + if self.level >= 1.0 { + self.level = 1.0; + self.stage = EnvelopeStage::Decay; + } + } + CurveType::Exponential => { + // Asymptotic approach: level moves toward overshoot target + // Using target of 1.0 + small overshoot so we actually reach 1.0 + let overshoot_target = 1.0 + (1.0 - self.exp_base) * 0.01; + self.level = overshoot_target - (overshoot_target - self.level) * self.exp_coeff; + if self.level >= 1.0 { + self.level = 1.0; + self.stage = EnvelopeStage::Decay; + // Set up decay exponential + let samples = self.decay * sample_rate_f32; + self.exp_coeff = (-5.0 / samples).exp(); + self.exp_base = 1.0; + self.exp_target = self.sustain; + } + } } } EnvelopeStage::Decay => { - // Fall from 1.0 to sustain level let target = self.sustain; - let decrement = (1.0 - target) / (self.decay * sample_rate_f32); - self.level -= decrement; - if self.level <= target { - self.level = target; - self.stage = EnvelopeStage::Sustain; + match self.curve { + CurveType::Linear => { + let decrement = (1.0 - target) / (self.decay * sample_rate_f32); + self.level -= decrement; + if self.level <= target { + self.level = target; + self.stage = EnvelopeStage::Sustain; + } + } + CurveType::Exponential => { + // Exponential decay toward sustain level + self.level = target + (self.level - target) * self.exp_coeff; + if (self.level - target).abs() < 0.001 { + self.level = target; + self.stage = EnvelopeStage::Sustain; + } + } } } EnvelopeStage::Sustain => { @@ -165,12 +236,23 @@ impl AudioNode for ADSRNode { self.level = self.sustain; } EnvelopeStage::Release => { - // Fall from current level to 0.0 - let decrement = self.level / (self.release * sample_rate_f32); - self.level -= decrement; - if self.level <= 0.001 { - self.level = 0.0; - self.stage = EnvelopeStage::Idle; + match self.curve { + CurveType::Linear => { + let decrement = self.level / (self.release * sample_rate_f32); + self.level -= decrement; + if self.level <= 0.001 { + self.level = 0.0; + self.stage = EnvelopeStage::Idle; + } + } + CurveType::Exponential => { + // Exponential decay toward 0 + self.level *= self.exp_coeff; + if self.level <= 0.001 { + self.level = 0.0; + self.stage = EnvelopeStage::Idle; + } + } } } } @@ -183,6 +265,9 @@ impl AudioNode for ADSRNode { fn reset(&mut self) { self.stage = EnvelopeStage::Idle; self.level = 0.0; + self.exp_coeff = 0.0; + self.exp_base = 0.0; + self.exp_target = 0.0; self.gate_was_high = false; } @@ -201,9 +286,13 @@ impl AudioNode for ADSRNode { decay: self.decay, sustain: self.sustain, release: self.release, - stage: EnvelopeStage::Idle, // Reset state - level: 0.0, // Reset level - gate_was_high: false, // Reset gate + curve: self.curve, + stage: EnvelopeStage::Idle, + level: 0.0, + exp_coeff: 0.0, + exp_base: 0.0, + exp_target: 0.0, + gate_was_high: false, inputs: self.inputs.clone(), outputs: self.outputs.clone(), parameters: self.parameters.clone(), diff --git a/daw-backend/src/audio/node_graph/nodes/amp_sim.rs b/daw-backend/src/audio/node_graph/nodes/amp_sim.rs index fddb720..142a556 100644 --- a/daw-backend/src/audio/node_graph/nodes/amp_sim.rs +++ b/daw-backend/src/audio/node_graph/nodes/amp_sim.rs @@ -65,6 +65,16 @@ impl AmpSimNode { Ok(()) } + /// Load a bundled NAM model by name (e.g. "BossSD1"). + pub fn load_bundled_model(&mut self, name: &str) -> Result<(), String> { + let mut model = super::bundled_models::load_bundled_model(name) + .ok_or_else(|| format!("Unknown bundled model: {}", name))??; + model.set_max_buffer_size(1024); + self.model = Some(model); + self.model_path = Some(format!("bundled:{}", name)); + Ok(()) + } + /// Get the loaded model path (for preset serialization). pub fn model_path(&self) -> Option<&str> { self.model_path.as_deref() diff --git a/daw-backend/src/audio/node_graph/nodes/bundled_models.rs b/daw-backend/src/audio/node_graph/nodes/bundled_models.rs new file mode 100644 index 0000000..bb65107 --- /dev/null +++ b/daw-backend/src/audio/node_graph/nodes/bundled_models.rs @@ -0,0 +1,50 @@ +use nam_ffi::NamModel; + +struct BundledModel { + name: &'static str, + filename: &'static str, + data: &'static [u8], +} + +const BUNDLED_MODELS: &[BundledModel] = &[ + BundledModel { + name: "BossSD1", + filename: "BossSD1-WaveNet.nam", + data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/BossSD1-WaveNet.nam"), + }, + BundledModel { + name: "DeluxeReverb", + filename: "DeluxeReverb.nam", + data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/DeluxeReverb.nam"), + }, + BundledModel { + name: "DingwallBass", + filename: "DingwallBass.nam", + data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/DingwallBass.nam"), + }, + BundledModel { + name: "Rhythm", + filename: "Rhythm.nam", + data: include_bytes!("../../../../../vendor/NeuralAudio/Utils/Models/Rhythm.nam"), + }, +]; + +/// Return display names of all bundled NAM models. +pub fn bundled_model_names() -> Vec<&'static str> { + BUNDLED_MODELS.iter().map(|m| m.name).collect() +} + +/// Load a bundled NAM model by display name. +/// Returns `None` if the name isn't found, `Some(Err(...))` on load failure. +pub fn load_bundled_model(name: &str) -> Option> { + eprintln!("[NAM] load_bundled_model: looking up {:?}", name); + let model = BUNDLED_MODELS.iter().find(|m| m.name == name)?; + eprintln!("[NAM] Found bundled model: name={}, filename={}, data_len={}", model.name, model.filename, model.data.len()); + Some( + NamModel::from_bytes(model.filename, model.data) + .map_err(|e| { + eprintln!("[NAM] from_bytes failed for {}: {}", model.filename, e); + e.to_string() + }), + ) +} diff --git a/daw-backend/src/audio/node_graph/nodes/mod.rs b/daw-backend/src/audio/node_graph/nodes/mod.rs index 2e75c6a..1b0825d 100644 --- a/daw-backend/src/audio/node_graph/nodes/mod.rs +++ b/daw-backend/src/audio/node_graph/nodes/mod.rs @@ -1,4 +1,5 @@ mod amp_sim; +pub mod bundled_models; mod adsr; mod arpeggiator; mod audio_input; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs index bca106e..8f80465 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs @@ -515,6 +515,8 @@ impl NodeTemplateTrait for NodeTemplate { ValueType::float_param(0.7, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true); graph.add_input_param(node_id, "Release".into(), DataType::CV, ValueType::float_param(0.2, 0.001, 5.0, " s", 3, None), InputParamKind::ConstantOnly, true); + graph.add_input_param(node_id, "Curve".into(), DataType::CV, + ValueType::float_param(0.0, 0.0, 1.0, "", 4, Some(&["Linear", "Exponential"])), InputParamKind::ConstantOnly, true); graph.add_output_param(node_id, "Envelope Out".into(), DataType::CV); } NodeTemplate::Lfo => { diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs index c3cd4bc..32a33ef 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs @@ -2499,42 +2499,14 @@ 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 + // Bundled NAM models — populate from embedded registry 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 - } - } + for name in daw_backend::audio::node_graph::nodes::bundled_models::bundled_model_names() { + self.user_state.available_nam_models.push(NamModelInfo { + name: name.to_string(), + path: format!("bundled:{}", name), + is_bundled: true, + }); } self.user_state.available_nam_models.sort_by(|a, b| a.name.cmp(&b.name)); } diff --git a/nam-ffi/src/lib.rs b/nam-ffi/src/lib.rs index d6d8366..afc0858 100644 --- a/nam-ffi/src/lib.rs +++ b/nam-ffi/src/lib.rs @@ -1,14 +1,13 @@ use std::path::Path; -#[cfg(windows)] -type WChar = u16; -#[cfg(not(windows))] -type WChar = u32; - #[allow(dead_code)] mod ffi { - use super::WChar; - use std::os::raw::{c_float, c_int}; + use std::os::raw::{c_char, c_float, c_int}; + + #[cfg(windows)] + type PathChar = u16; // wchar_t on Windows + #[cfg(not(windows))] + type PathChar = c_char; // char on Linux/macOS #[repr(C)] pub struct NeuralModel { @@ -16,7 +15,7 @@ mod ffi { } unsafe extern "C" { - pub fn CreateModelFromFile(model_path: *const WChar) -> *mut NeuralModel; + pub fn CreateModelFromFile(model_path: *const PathChar) -> *mut NeuralModel; pub fn DeleteModel(model: *mut NeuralModel); pub fn SetLSTMLoadMode(load_mode: c_int); @@ -58,24 +57,36 @@ pub struct NamModel { } impl NamModel { + /// Load a model from in-memory bytes by writing to a temp file first. + /// The NAM C API only supports file-based loading. + pub fn from_bytes(name: &str, data: &[u8]) -> Result { + let dir = std::env::temp_dir().join("lightningbeam-nam"); + eprintln!("[NAM] from_bytes: name={}, data_len={}, temp_dir={}", name, data.len(), dir.display()); + std::fs::create_dir_all(&dir) + .map_err(|e| NamError::ModelLoadFailed(format!("create_dir_all failed: {}", e)))?; + let file_path = dir.join(name); + std::fs::write(&file_path, data) + .map_err(|e| NamError::ModelLoadFailed(format!("write failed: {}", e)))?; + eprintln!("[NAM] Wrote {} bytes to {}", data.len(), file_path.display()); + Self::from_file(&file_path) + } + pub fn from_file(path: &Path) -> Result { - let wide: Vec = { + let ptr = unsafe { #[cfg(windows)] { use std::os::windows::ffi::OsStrExt; - path.as_os_str().encode_wide().chain(std::iter::once(0)).collect() + let wide: Vec = path.as_os_str().encode_wide().chain(std::iter::once(0)).collect(); + ffi::CreateModelFromFile(wide.as_ptr()) } #[cfg(not(windows))] { - path.to_string_lossy() - .chars() - .map(|c| c as WChar) - .chain(std::iter::once(0)) - .collect() + use std::ffi::CString; + let c_path = CString::new(path.to_string_lossy().as_bytes()) + .map_err(|_| NamError::ModelLoadFailed(path.display().to_string()))?; + ffi::CreateModelFromFile(c_path.as_ptr()) } }; - - let ptr = unsafe { ffi::CreateModelFromFile(wide.as_ptr()) }; if ptr.is_null() { return Err(NamError::ModelLoadFailed(path.display().to_string())); }