Load sample .nam amps
This commit is contained in:
parent
7e3f18c95b
commit
725faa4445
|
|
@ -224,6 +224,21 @@ pub enum PendingScriptSampleLoad {
|
||||||
FromFile { node_id: NodeId, backend_node_id: u32, slot_index: usize },
|
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
|
/// Pending sampler load request from bottom_ui(), handled by the node graph pane
|
||||||
pub enum PendingSamplerLoad {
|
pub enum PendingSamplerLoad {
|
||||||
/// Load a single clip from the audio pool into a SimpleSampler
|
/// 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)>,
|
pub pending_draw_param_changes: Vec<(NodeId, u32, f32)>,
|
||||||
/// Active sample import dialog (folder import with heuristic mapping)
|
/// Active sample import dialog (folder import with heuristic mapping)
|
||||||
pub sample_import_dialog: Option<crate::sample_import_dialog::SampleImportDialog>,
|
pub sample_import_dialog: Option<crate::sample_import_dialog::SampleImportDialog>,
|
||||||
/// Pending AmpSim model load (node_id, backend_node_id) — triggers file dialog for .nam
|
/// Pending AmpSim model load — triggers file dialog or direct load
|
||||||
pub pending_amp_sim_load: Option<(NodeId, u32)>,
|
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 {
|
impl Default for GraphState {
|
||||||
|
|
@ -303,6 +322,8 @@ impl Default for GraphState {
|
||||||
pending_draw_param_changes: Vec::new(),
|
pending_draw_param_changes: Vec::new(),
|
||||||
sample_import_dialog: None,
|
sample_import_dialog: None,
|
||||||
pending_amp_sim_load: 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 {
|
} else if self.template == NodeTemplate::AmpSim {
|
||||||
let backend_node_id = user_state.node_backend_ids.get(&node_id).copied().unwrap_or(0);
|
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...");
|
let button_text = self.nam_model_name.as_deref().unwrap_or("Select Model...");
|
||||||
if ui.button(button_text).clicked() {
|
|
||||||
user_state.pending_amp_sim_load = Some((node_id, backend_node_id));
|
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 {
|
} else {
|
||||||
ui.label("");
|
ui.label("");
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ pub mod backend;
|
||||||
pub mod graph_data;
|
pub mod graph_data;
|
||||||
|
|
||||||
use backend::{BackendNodeId, GraphBackend};
|
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 super::NodePath;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui_node_graph2::*;
|
use egui_node_graph2::*;
|
||||||
|
|
@ -403,6 +403,20 @@ impl NodeGraphPane {
|
||||||
);
|
);
|
||||||
self.node_id_map.insert(node_id, backend_id);
|
self.node_id_map.insert(node_id, backend_id);
|
||||||
self.backend_to_frontend_map.insert(backend_id, node_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.node_id_map.insert(frontend_id, backend_id);
|
||||||
self.backend_to_frontend_map.insert(backend_id, frontend_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();
|
.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
|
||||||
|
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
|
// Node backend ID map
|
||||||
self.user_state.node_backend_ids = self.node_id_map.iter()
|
self.user_state.node_backend_ids = self.node_id_map.iter()
|
||||||
.map(|(&node_id, backend_id)| {
|
.map(|(&node_id, backend_id)| {
|
||||||
|
|
@ -2518,8 +2586,19 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle pending AmpSim model load from bottom_ui()
|
// 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(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()
|
if let Some(path) = rfd::FileDialog::new()
|
||||||
.add_filter("NAM Model", &["nam"])
|
.add_filter("NAM Model", &["nam"])
|
||||||
.pick_file()
|
.pick_file()
|
||||||
|
|
@ -2527,16 +2606,25 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
let model_name = path.file_stem()
|
let model_name = path.file_stem()
|
||||||
.map(|s| s.to_string_lossy().to_string())
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| "Model".to_string());
|
.unwrap_or_else(|| "Model".to_string());
|
||||||
if let Some(controller_arc) = &shared.audio_controller {
|
controller_arc.lock().unwrap().amp_sim_load_model(
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
|
||||||
controller.amp_sim_load_model(
|
|
||||||
backend_track_id,
|
backend_track_id,
|
||||||
backend_node_id,
|
backend_node_id,
|
||||||
path.to_string_lossy().to_string(),
|
path.to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
|
if let Some(node) = self.state.graph.nodes.get_mut(node_id) {
|
||||||
node.user_data.nam_model_name = Some(model_name);
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue