fix NAM model loading
This commit is contained in:
parent
6b3a286caf
commit
b4c7a45990
|
|
@ -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::<AmpSimNode>() {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<AmpSimNode>() {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
|
|
@ -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,7 +184,8 @@ impl AudioNode for ADSRNode {
|
|||
self.level = 0.0;
|
||||
}
|
||||
EnvelopeStage::Attack => {
|
||||
// Rise from current level to 1.0
|
||||
match self.curve {
|
||||
CurveType::Linear => {
|
||||
let increment = 1.0 / (self.attack * sample_rate_f32);
|
||||
self.level += increment;
|
||||
if self.level >= 1.0 {
|
||||
|
|
@ -150,9 +193,27 @@ impl AudioNode for ADSRNode {
|
|||
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;
|
||||
match self.curve {
|
||||
CurveType::Linear => {
|
||||
let decrement = (1.0 - target) / (self.decay * sample_rate_f32);
|
||||
self.level -= decrement;
|
||||
if self.level <= target {
|
||||
|
|
@ -160,12 +221,23 @@ impl AudioNode for ADSRNode {
|
|||
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 => {
|
||||
// Hold at sustain level
|
||||
self.level = self.sustain;
|
||||
}
|
||||
EnvelopeStage::Release => {
|
||||
// Fall from current level to 0.0
|
||||
match self.curve {
|
||||
CurveType::Linear => {
|
||||
let decrement = self.level / (self.release * sample_rate_f32);
|
||||
self.level -= decrement;
|
||||
if self.level <= 0.001 {
|
||||
|
|
@ -173,6 +245,16 @@ impl AudioNode for ADSRNode {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write envelope value (CV is mono)
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<Result<NamModel, String>> {
|
||||
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()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
mod amp_sim;
|
||||
pub mod bundled_models;
|
||||
mod adsr;
|
||||
mod arpeggiator;
|
||||
mod audio_input;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -2499,43 +2499,15 @@ 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();
|
||||
for name in daw_backend::audio::node_graph::nodes::bundled_models::bundled_model_names() {
|
||||
self.user_state.available_nam_models.push(NamModelInfo {
|
||||
name,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
name: name.to_string(),
|
||||
path: format!("bundled:{}", name),
|
||||
is_bundled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
break; // use first directory found
|
||||
}
|
||||
}
|
||||
}
|
||||
self.user_state.available_nam_models.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Self, NamError> {
|
||||
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<Self, NamError> {
|
||||
let wide: Vec<WChar> = {
|
||||
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<u16> = 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()));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue