From 22651b9f2517b450e390228fcd5ef602f2f45a02 Mon Sep 17 00:00:00 2001 From: OpenSauce Date: Sun, 31 May 2026 14:59:51 +0100 Subject: [PATCH] feat(nam): support LSTM models via nam-rs 0.2 arch-agnostic Model Bump nam-rs 0.1 -> 0.2 and swap the hardcoded WaveNet for nam_rs::Model, which dispatches over the .nam architecture (WaveNet or LSTM). LSTM .nam files previously parsed into the registry but WaveNet::new rejected them, falling back to silent passthrough; they now run. Processing stays per-sample for now. --- Cargo.lock | 4 ++-- rustortion-core/Cargo.toml | 2 +- rustortion-core/src/amp/stages/nam.rs | 27 ++++++++++++++------------- rustortion-core/src/nam/mod.rs | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8edf3fc..95d402b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2616,9 +2616,9 @@ dependencies = [ [[package]] name = "nam-rs" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a2bedcceead65147c900244ce77cb1da4f5b33686a991999f1ab435090e0a2" +checksum = "f8740673c58dca29a4db6fc5cf41cb633039b43303ac45b2e423ea8ac0f178ce" dependencies = [ "serde", "serde_json", diff --git a/rustortion-core/Cargo.toml b/rustortion-core/Cargo.toml index 5e14e4e..e0f5932 100644 --- a/rustortion-core/Cargo.toml +++ b/rustortion-core/Cargo.toml @@ -17,7 +17,7 @@ rustfft = "6.4" realfft = "3.5" arc-swap = "1.8" assert_no_alloc = { version = "1.1", features = ["warn_debug"] } -nam-rs = "0.1.0" +nam-rs = "0.2.0" [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } diff --git a/rustortion-core/src/amp/stages/nam.rs b/rustortion-core/src/amp/stages/nam.rs index 8b7ee61..1f23b41 100644 --- a/rustortion-core/src/amp/stages/nam.rs +++ b/rustortion-core/src/amp/stages/nam.rs @@ -1,5 +1,5 @@ use log::warn; -use nam_rs::WaveNet; +use nam_rs::Model; use serde::{Deserialize, Serialize}; use crate::amp::stages::Stage; @@ -10,12 +10,13 @@ use crate::nam::registry; const GAIN_DB_MIN: f32 = -24.0; const GAIN_DB_MAX: f32 = 24.0; -/// A Neural Amp Modeler stage running a WaveNet `.nam` model. +/// A Neural Amp Modeler stage running a `.nam` model of any supported architecture +/// (WaveNet or LSTM), via the architecture-agnostic [`nam_rs::Model`]. /// /// With no model loaded the stage is a passthrough. Input/output gain are applied /// around the model and the wet output is blended with the dry signal via `mix`. pub struct NamStage { - wavenet: Option, + model: Option, input_gain: f32, output_gain: f32, mix: f32, @@ -28,7 +29,7 @@ pub struct NamStage { impl NamStage { const fn passthrough(input_gain: f32, output_gain: f32, mix: f32) -> Self { Self { - wavenet: None, + model: None, input_gain, output_gain, mix, @@ -46,7 +47,7 @@ impl NamStage { native_sample_rate: f32, ) -> Self { Self { - wavenet: None, + model: None, input_gain, output_gain, mix, @@ -58,10 +59,10 @@ impl NamStage { impl Stage for NamStage { fn process(&mut self, input: f32) -> f32 { - let Some(wavenet) = self.wavenet.as_mut() else { + let Some(model) = self.model.as_mut() else { return input; }; - let wet = wavenet.process_sample(input * self.input_gain) * self.output_gain; + let wet = model.process_sample(input * self.input_gain) * self.output_gain; self.mix.mul_add(wet - input, input) } @@ -136,7 +137,7 @@ impl Default for NamConfig { impl NamConfig { /// Build a runnable stage. Resolves the model from the global registry and - /// allocates the `WaveNet` here (off the real-time thread). On any failure the + /// allocates the model here (off the real-time thread). On any failure the /// stage falls back to passthrough with a warning. pub fn to_stage(&self, sample_rate: f32) -> NamStage { let input_gain = db_to_lin(self.input_gain_db.clamp(GAIN_DB_MIN, GAIN_DB_MAX)); @@ -168,9 +169,9 @@ impl NamConfig { ); } - match WaveNet::new(&model) { - Ok(wavenet) => NamStage { - wavenet: Some(wavenet), + match Model::from_nam(&model) { + Ok(runtime) => NamStage { + model: Some(runtime), input_gain, output_gain, mix, @@ -201,9 +202,9 @@ mod tests { #[test] fn mismatch_bypass_is_dry_passthrough() { - // A rate-mismatch stage is built without a WaveNet but records the real native + // A rate-mismatch stage is built without a model but records the real native // rate and the mismatch flag. We construct it directly here because building a - // real `WaveNet` requires loading a `.nam` model into the registry, which unit + // real model requires loading a `.nam` file into the registry, which unit // tests can't do; this still verifies the RT-path passthrough contract and the // params reported to the UI. let mut stage = diff --git a/rustortion-core/src/nam/mod.rs b/rustortion-core/src/nam/mod.rs index c5d3311..07bbe9a 100644 --- a/rustortion-core/src/nam/mod.rs +++ b/rustortion-core/src/nam/mod.rs @@ -1,7 +1,7 @@ //! NAM (Neural Amp Modeler) model loading and a process-global parsed-model //! registry. //! -//! `.nam` models are parsed (and the `WaveNet` allocated) off the real-time thread. +//! `.nam` models are parsed (and the runtime model allocated) off the real-time thread. //! The [`loader`] scans a directory and parses every `*.nam` file into memory at //! startup; the [`registry`] makes those parsed models reachable from //! `StageConfig::to_runtime`, which has no other handle to the loader.