Load sample .nam amps

This commit is contained in:
Skyler Lehmkuhl 2026-02-21 10:25:55 -05:00
parent 7e3f18c95b
commit 725faa4445
2 changed files with 201 additions and 24 deletions

View File

@ -224,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
@ -277,8 +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 (node_id, backend_node_id) — triggers file dialog for .nam
pub pending_amp_sim_load: Option<(NodeId, u32)>,
/// 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 {
@ -303,6 +322,8 @@ impl Default for GraphState {
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(),
}
}
}
@ -1400,9 +1421,77 @@ impl NodeDataTrait for NodeData {
}
} 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("Load Model...");
if ui.button(button_text).clicked() {
user_state.pending_amp_sim_load = Some((node_id, backend_node_id));
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("");

View File

@ -8,7 +8,7 @@ pub mod backend;
pub mod graph_data;
use backend::{BackendNodeId, GraphBackend};
use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType};
use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NamModelInfo, NodeData, NodeTemplate, PendingAmpSimLoad, ValueType};
use super::NodePath;
use eframe::egui;
use egui_node_graph2::*;
@ -403,6 +403,20 @@ impl NodeGraphPane {
);
self.node_id_map.insert(node_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, node_id);
// Auto-load default NAM model for new AmpSim nodes
if node_type == "AmpSim" {
if let Some(model) = self.user_state.available_nam_models.iter().find(|m| m.is_bundled) {
controller.amp_sim_load_model(
backend_track_id,
backend_node.id,
model.path.clone(),
);
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
node.user_data.nam_model_name = Some(model.name.clone());
}
}
}
}
}
}
@ -665,6 +679,20 @@ impl NodeGraphPane {
);
self.node_id_map.insert(frontend_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, frontend_id);
// Auto-load default NAM model for new AmpSim nodes
if node_type == "AmpSim" {
if let Some(model) = self.user_state.available_nam_models.iter().find(|m| m.is_bundled) {
controller.amp_sim_load_model(
backend_track_id,
backend_node.id,
model.path.clone(),
);
if let Some(node) = self.state.graph.nodes.get_mut(frontend_id) {
node.user_data.nam_model_name = Some(model.name.clone());
}
}
}
}
}
}
@ -2469,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)| {
@ -2518,25 +2586,45 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
}
// Handle pending AmpSim model load from bottom_ui()
if let Some((node_id, backend_node_id)) = self.user_state.pending_amp_sim_load.take() {
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(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());
if let Some(controller_arc) = &shared.audio_controller {
let mut controller = controller_arc.lock().unwrap();
controller.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);
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,
});
}
}
}
}
}
}