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) => {
|
Command::AmpSimLoadModel(track_id, node_id, model_path) => {
|
||||||
use crate::audio::node_graph::nodes::AmpSimNode;
|
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) {
|
let graph = match self.project.get_track_mut(track_id) {
|
||||||
Some(TrackNode::Midi(track)) => Some(&mut track.instrument_graph),
|
Some(TrackNode::Midi(track)) => Some(&mut track.instrument_graph),
|
||||||
Some(TrackNode::Audio(track)) => Some(&mut track.effects_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);
|
let node_idx = NodeIndex::new(node_id as usize);
|
||||||
if let Some(graph_node) = graph.get_graph_node_mut(node_idx) {
|
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 Some(amp_sim) = graph_node.node.as_any_mut().downcast_mut::<AmpSimNode>() {
|
||||||
if let Err(e) = amp_sim.load_model(&model_path) {
|
let result = if let Some(bundled_name) = model_path.strip_prefix("bundled:") {
|
||||||
eprintln!("Failed to load NAM model: {}", e);
|
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 let Some(ref model_path) = serialized_node.nam_model_path {
|
||||||
if serialized_node.node_type == "AmpSim" {
|
if serialized_node.node_type == "AmpSim" {
|
||||||
use crate::audio::node_graph::nodes::AmpSimNode;
|
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(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 Some(amp_sim) = graph_node.node.as_any_mut().downcast_mut::<AmpSimNode>() {
|
||||||
if let Err(e) = amp_sim.load_model(&resolved_path) {
|
let result = if let Some(bundled_name) = model_path.strip_prefix("bundled:") {
|
||||||
eprintln!("Warning: failed to load NAM model {}: {}", resolved_path, e);
|
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_DECAY: u32 = 1;
|
||||||
const PARAM_SUSTAIN: u32 = 2;
|
const PARAM_SUSTAIN: u32 = 2;
|
||||||
const PARAM_RELEASE: u32 = 3;
|
const PARAM_RELEASE: u32 = 3;
|
||||||
|
const PARAM_CURVE: u32 = 4;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum EnvelopeStage {
|
enum EnvelopeStage {
|
||||||
|
|
@ -15,6 +16,19 @@ enum EnvelopeStage {
|
||||||
Release,
|
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
|
/// ADSR Envelope Generator
|
||||||
/// Outputs a CV signal (0.0-1.0) based on gate input and ADSR parameters
|
/// Outputs a CV signal (0.0-1.0) based on gate input and ADSR parameters
|
||||||
pub struct ADSRNode {
|
pub struct ADSRNode {
|
||||||
|
|
@ -23,8 +37,15 @@ pub struct ADSRNode {
|
||||||
decay: f32, // seconds
|
decay: f32, // seconds
|
||||||
sustain: f32, // level (0.0-1.0)
|
sustain: f32, // level (0.0-1.0)
|
||||||
release: f32, // seconds
|
release: f32, // seconds
|
||||||
|
curve: CurveType,
|
||||||
stage: EnvelopeStage,
|
stage: EnvelopeStage,
|
||||||
level: f32, // current envelope level
|
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,
|
gate_was_high: bool,
|
||||||
inputs: Vec<NodePort>,
|
inputs: Vec<NodePort>,
|
||||||
outputs: 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_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_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_RELEASE, "Release", 0.001, 5.0, 0.2, ParameterUnit::Time),
|
||||||
|
Parameter::new(PARAM_CURVE, "Curve", 0.0, 1.0, 0.0, ParameterUnit::Generic),
|
||||||
];
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -56,8 +78,12 @@ impl ADSRNode {
|
||||||
decay: 0.1,
|
decay: 0.1,
|
||||||
sustain: 0.7,
|
sustain: 0.7,
|
||||||
release: 0.2,
|
release: 0.2,
|
||||||
|
curve: CurveType::Linear,
|
||||||
stage: EnvelopeStage::Idle,
|
stage: EnvelopeStage::Idle,
|
||||||
level: 0.0,
|
level: 0.0,
|
||||||
|
exp_coeff: 0.0,
|
||||||
|
exp_base: 0.0,
|
||||||
|
exp_target: 0.0,
|
||||||
gate_was_high: false,
|
gate_was_high: false,
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
|
|
@ -89,6 +115,7 @@ impl AudioNode for ADSRNode {
|
||||||
PARAM_DECAY => self.decay = value.clamp(0.001, 5.0),
|
PARAM_DECAY => self.decay = value.clamp(0.001, 5.0),
|
||||||
PARAM_SUSTAIN => self.sustain = value.clamp(0.0, 1.0),
|
PARAM_SUSTAIN => self.sustain = value.clamp(0.0, 1.0),
|
||||||
PARAM_RELEASE => self.release = value.clamp(0.001, 5.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_DECAY => self.decay,
|
||||||
PARAM_SUSTAIN => self.sustain,
|
PARAM_SUSTAIN => self.sustain,
|
||||||
PARAM_RELEASE => self.release,
|
PARAM_RELEASE => self.release,
|
||||||
|
PARAM_CURVE => match self.curve { CurveType::Linear => 0.0, CurveType::Exponential => 1.0 },
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,9 +158,23 @@ impl AudioNode for ADSRNode {
|
||||||
if gate_high && !self.gate_was_high {
|
if gate_high && !self.gate_was_high {
|
||||||
// Note on: Start attack
|
// Note on: Start attack
|
||||||
self.stage = EnvelopeStage::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 {
|
} else if !gate_high && self.gate_was_high {
|
||||||
// Note off: Start release
|
// Note off: Start release
|
||||||
self.stage = EnvelopeStage::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;
|
self.gate_was_high = gate_high;
|
||||||
|
|
||||||
|
|
@ -142,7 +184,8 @@ impl AudioNode for ADSRNode {
|
||||||
self.level = 0.0;
|
self.level = 0.0;
|
||||||
}
|
}
|
||||||
EnvelopeStage::Attack => {
|
EnvelopeStage::Attack => {
|
||||||
// Rise from current level to 1.0
|
match self.curve {
|
||||||
|
CurveType::Linear => {
|
||||||
let increment = 1.0 / (self.attack * sample_rate_f32);
|
let increment = 1.0 / (self.attack * sample_rate_f32);
|
||||||
self.level += increment;
|
self.level += increment;
|
||||||
if self.level >= 1.0 {
|
if self.level >= 1.0 {
|
||||||
|
|
@ -150,9 +193,27 @@ impl AudioNode for ADSRNode {
|
||||||
self.stage = EnvelopeStage::Decay;
|
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 => {
|
EnvelopeStage::Decay => {
|
||||||
// Fall from 1.0 to sustain level
|
|
||||||
let target = self.sustain;
|
let target = self.sustain;
|
||||||
|
match self.curve {
|
||||||
|
CurveType::Linear => {
|
||||||
let decrement = (1.0 - target) / (self.decay * sample_rate_f32);
|
let decrement = (1.0 - target) / (self.decay * sample_rate_f32);
|
||||||
self.level -= decrement;
|
self.level -= decrement;
|
||||||
if self.level <= target {
|
if self.level <= target {
|
||||||
|
|
@ -160,12 +221,23 @@ impl AudioNode for ADSRNode {
|
||||||
self.stage = EnvelopeStage::Sustain;
|
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 => {
|
EnvelopeStage::Sustain => {
|
||||||
// Hold at sustain level
|
// Hold at sustain level
|
||||||
self.level = self.sustain;
|
self.level = self.sustain;
|
||||||
}
|
}
|
||||||
EnvelopeStage::Release => {
|
EnvelopeStage::Release => {
|
||||||
// Fall from current level to 0.0
|
match self.curve {
|
||||||
|
CurveType::Linear => {
|
||||||
let decrement = self.level / (self.release * sample_rate_f32);
|
let decrement = self.level / (self.release * sample_rate_f32);
|
||||||
self.level -= decrement;
|
self.level -= decrement;
|
||||||
if self.level <= 0.001 {
|
if self.level <= 0.001 {
|
||||||
|
|
@ -173,6 +245,16 @@ impl AudioNode for ADSRNode {
|
||||||
self.stage = EnvelopeStage::Idle;
|
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)
|
// Write envelope value (CV is mono)
|
||||||
|
|
@ -183,6 +265,9 @@ impl AudioNode for ADSRNode {
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
self.stage = EnvelopeStage::Idle;
|
self.stage = EnvelopeStage::Idle;
|
||||||
self.level = 0.0;
|
self.level = 0.0;
|
||||||
|
self.exp_coeff = 0.0;
|
||||||
|
self.exp_base = 0.0;
|
||||||
|
self.exp_target = 0.0;
|
||||||
self.gate_was_high = false;
|
self.gate_was_high = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,9 +286,13 @@ impl AudioNode for ADSRNode {
|
||||||
decay: self.decay,
|
decay: self.decay,
|
||||||
sustain: self.sustain,
|
sustain: self.sustain,
|
||||||
release: self.release,
|
release: self.release,
|
||||||
stage: EnvelopeStage::Idle, // Reset state
|
curve: self.curve,
|
||||||
level: 0.0, // Reset level
|
stage: EnvelopeStage::Idle,
|
||||||
gate_was_high: false, // Reset gate
|
level: 0.0,
|
||||||
|
exp_coeff: 0.0,
|
||||||
|
exp_base: 0.0,
|
||||||
|
exp_target: 0.0,
|
||||||
|
gate_was_high: false,
|
||||||
inputs: self.inputs.clone(),
|
inputs: self.inputs.clone(),
|
||||||
outputs: self.outputs.clone(),
|
outputs: self.outputs.clone(),
|
||||||
parameters: self.parameters.clone(),
|
parameters: self.parameters.clone(),
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,16 @@ impl AmpSimNode {
|
||||||
Ok(())
|
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).
|
/// Get the loaded model path (for preset serialization).
|
||||||
pub fn model_path(&self) -> Option<&str> {
|
pub fn model_path(&self) -> Option<&str> {
|
||||||
self.model_path.as_deref()
|
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;
|
mod amp_sim;
|
||||||
|
pub mod bundled_models;
|
||||||
mod adsr;
|
mod adsr;
|
||||||
mod arpeggiator;
|
mod arpeggiator;
|
||||||
mod audio_input;
|
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);
|
ValueType::float_param(0.7, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true);
|
||||||
graph.add_input_param(node_id, "Release".into(), DataType::CV,
|
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);
|
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);
|
graph.add_output_param(node_id, "Envelope Out".into(), DataType::CV);
|
||||||
}
|
}
|
||||||
NodeTemplate::Lfo => {
|
NodeTemplate::Lfo => {
|
||||||
|
|
|
||||||
|
|
@ -2499,43 +2499,15 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
.collect();
|
.collect();
|
||||||
self.user_state.available_scripts.sort_by(|a, b| a.1.to_lowercase().cmp(&b.1.to_lowercase()));
|
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() {
|
if self.user_state.available_nam_models.is_empty() {
|
||||||
let bundled_dirs = [
|
for name in daw_backend::audio::node_graph::nodes::bundled_models::bundled_model_names() {
|
||||||
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 {
|
self.user_state.available_nam_models.push(NamModelInfo {
|
||||||
name,
|
name: name.to_string(),
|
||||||
path: path.to_string_lossy().to_string(),
|
path: format!("bundled:{}", name),
|
||||||
is_bundled: true,
|
is_bundled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
break; // use first directory found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.user_state.available_nam_models.sort_by(|a, b| a.name.cmp(&b.name));
|
self.user_state.available_nam_models.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
type WChar = u16;
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
type WChar = u32;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod ffi {
|
mod ffi {
|
||||||
use super::WChar;
|
use std::os::raw::{c_char, c_float, c_int};
|
||||||
use std::os::raw::{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)]
|
#[repr(C)]
|
||||||
pub struct NeuralModel {
|
pub struct NeuralModel {
|
||||||
|
|
@ -16,7 +15,7 @@ mod ffi {
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe extern "C" {
|
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 DeleteModel(model: *mut NeuralModel);
|
||||||
|
|
||||||
pub fn SetLSTMLoadMode(load_mode: c_int);
|
pub fn SetLSTMLoadMode(load_mode: c_int);
|
||||||
|
|
@ -58,24 +57,36 @@ pub struct NamModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
pub fn from_file(path: &Path) -> Result<Self, NamError> {
|
||||||
let wide: Vec<WChar> = {
|
let ptr = unsafe {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
use std::os::windows::ffi::OsStrExt;
|
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))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
path.to_string_lossy()
|
use std::ffi::CString;
|
||||||
.chars()
|
let c_path = CString::new(path.to_string_lossy().as_bytes())
|
||||||
.map(|c| c as WChar)
|
.map_err(|_| NamError::ModelLoadFailed(path.display().to_string()))?;
|
||||||
.chain(std::iter::once(0))
|
ffi::CreateModelFromFile(c_path.as_ptr())
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let ptr = unsafe { ffi::CreateModelFromFile(wide.as_ptr()) };
|
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
return Err(NamError::ModelLoadFailed(path.display().to_string()));
|
return Err(NamError::ModelLoadFailed(path.display().to_string()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue