nam-rs
Pure-Rust, real-time-safe inference for Neural Amp Modeler (NAM) .nam models.
nam-rs loads a .nam model file and runs its neural-network forward pass — a whole
buffer at a time (WaveNet uses a cache-friendly block kernel) or one sample at a time —
with no heap allocation on the audio thread, suitable for use inside a JACK
callback, a VST3/CLAP process(), or any real-time audio graph.
WaveNet, LSTM, and SlimmableContainer (NAM "A2") models all load through
one entry point, Model::from_nam, which dispatches on the file's declared
architecture.
Design contract
- Parity with the reference. Output must match the canonical Python/C++ NAM
implementations within
1e-5per sample for the same model and input. Enforced bytests/parity.rsagainst reference-generated fixtures. - Real-time safety.
process_buffer(every architecture, reached viaModel) performs zero heap allocation, locks, or syscalls; all scratch buffers are pre-allocated at construction. Enforced bytests/rt_safety.rsviaassert_no_alloc.
Install
MSRV: Rust 1.74. No C/C++ toolchain or native dependencies needed.
Usage
use ;
// Off the audio thread: load + allocate. `Model::from_nam` dispatches on the
// model's architecture, so the same code runs WaveNet and LSTM `.nam` files.
let model = from_file?;
let mut amp = from_nam?;
// On the audio thread: in-place, allocation-free. Call once per audio block;
// state carries across calls, so block-wise output matches one whole-buffer call.
let mut audio_buffer = vec!; // your host's block, filled with input
amp.process_buffer;
To smoke-test a model file without writing any code:
(examples/streaming.rs shows the block-wise hot-path loop in full.)
For WaveNet models, the first Model::receptive_field() output samples are a startup
transient (the dilated stack filling against zero-history) — the model's inherent
latency, the same convention NAM Core / NeuralAudio use. LSTM models have no such
warmup. Call Model::reset to return to silence.
Sample rate. A .nam expects audio at the rate it was captured
(NamModel::expected_sample_rate(), 48 kHz if the file omits it). nam-rs does not
resample: feed the model audio at that rate, or resample in your host first. A
mismatched rate produces silently wrong output, since the model's dilations and
recurrence are defined in samples, not seconds.
Processing boundary. nam-rs runs only the model's forward pass. The reference
NAM plugin additionally applies a DC blocker (high-pass) and, optionally, loudness
normalization on the output — those belong to the host's audio graph, not the model.
The calibration accessors (NamModel::loudness() etc.) give you the numbers for that
gain-staging.
Metadata
NamModel::metadata_typed() parses the file's metadata block into a Metadata
struct — everything a model browser needs to show, none of it used by the forward
pass. Every field is Option, since any of them may be absent.
| Field | Type | Meaning |
|---|---|---|
name |
Option<String> |
The author's name for the model, often better than the filename |
modeled_by |
Option<String> |
Who captured it |
gear_make |
Option<String> |
e.g. "Marshall" |
gear_model |
Option<String> |
e.g. "JMP-50" |
gear_type |
Option<String> |
What was captured, and how much of the chain: amp, pedal, amp_cab, full-rig, … |
tone_type |
Option<String> |
clean, overdrive, crunch, hi_gain, fuzz, … |
trainer |
Option<String> |
Which trainer produced the file, e.g. "TONE3000" |
date |
Option<Date> |
Export timestamp (year…second; ordered chronologically) |
loudness |
Option<f32> |
How loud the model is against NAM's standardized input, in dBFS RMS — not LUFS, and not perceptual |
input_level_dbu, output_level_dbu |
Option<f32> |
Analog dBu at 0 dBFS — the calibration numbers for gain-staging |
gain |
Option<f32> |
The trainer's 0.0..=1.0 estimate of how much gain/compression the model has |
training |
Option<serde_json::Value> |
The trainer's raw training record, left untyped |
Call metadata_typed() once and keep the struct: the single-field shortcuts on
NamModel (loudness(), input_level_dbu(), output_level_dbu()) each re-parse the
raw JSON. All of it is load-time, never the audio thread. The unparsed block stays
available as NamModel::metadata if you need a key we don't type, and parsing is
per-field lenient — one malformed entry yields None for itself instead of discarding
the whole block.
gear_type and tone_type are String, not enums, because there is no single
vocabulary: NAM's trainer and TONE3000 use overlapping-but-different sets, TONE3000
has already deprecated values that exist in files on disk, and real captures write
values in neither ("vintage", "T3K-Null"). An enum would reject those files.
For the one question that actually changes a signal chain — is a speaker cab already
baked into this capture, so that adding an IR would be a second one? —
Metadata::includes_cab() (also on NamModel) classifies gear_type across both
vocabularies, ignoring case, surrounding whitespace, and - vs _:
let model = from_file?;
match model.includes_cab
It returns None rather than guessing at a value it doesn't recognize, and it only
ever reports what the file claims: gear_type is author-supplied and captures do
mislabel themselves, so treat the answer as a default to offer the user, not a fact to
reroute audio on silently. The two answers are not equally strong — Some(true)
required someone to actively pick a cab-ish value, while Some(false) is mostly
"amp", which is what you get by leaving the field alone. One of the three real
captures in this repo's test corpus is a full rig tagged gear_type: "amp".
Supported architectures
- WaveNet (A1 and A2 single models) — dilated-conv forward pass, parity-tested.
- LSTM — recurrent forward pass, parity-tested.
- SlimmableContainer (NAM "A2") — a set of complete standalone submodels (any mix
of WaveNet/LSTM) with a runtime width dial as a CPU/quality trade-off. Select via
as_slimmable_mut()→set_slim_sizeorselect; switching is real-time-safe. See the crate docs for the selection semantics.
The A2 feature set is covered: FiLM, gating, bottleneck, grouped convs, multi-tap conv
heads, the optional post-stack head, and condition_dsp (a nested model that generates
the conditioning signal, multi-channel included). A few restrictions remain —
multi-channel input, a post-stack head with more than one output channel, mixed
gating modes within one array, and unrecognized activations — and these are rejected
with a descriptive error at load time rather than silently mis-run.
Performance
Rough numbers from the included Criterion bench (cargo bench), standard-size
fixture models, one x86-64 desktop core, release + LTO:
- Standard WaveNet capture: ≈1.9 µs/sample via
process_buffer(≈11× real-time at 48 kHz). The block path is ~3.5× faster than per-sample, so prefer whole blocks. - Standard LSTM capture: ≈1.2 µs/sample (≈17× real-time).
Numbers vary with CPU and model size — run cargo bench on your own target.
Development
Parity fixtures are committed under tests/fixtures/; regenerate them from Python NAM
with tests/fixtures/gen_fixtures.py (see tests/fixtures/README.md).
Attribution & license
nam-rs is MIT-licensed (see LICENSE). It is a derivative work: the
algorithm and .nam weight layout are ported from the projects below. Their license
texts are reproduced in NOTICE.
| Project | Role | License |
|---|---|---|
| neural-amp-modeler | Reference trainer + .nam exporter (source of truth for weight/config layout) |
MIT |
| NeuralAmpModelerCore | Canonical C++ inference library | MIT |
| NeuralAudio | High-performance C++ NAM runtime; primary porting reference | MIT |
.nam model files are licensed separately by whoever captured them; nam-rs ships
no model files.