NAM Player started as a simple question: what would it feel like if a guitar amp capture behaved like a proper VCV Rack module, not like a plugin awkwardly wedged into a modular environment?
That distinction matters. A typical amp sim assumes a linear studio signal chain: guitar in, amp model, cabinet, maybe a few effects before and after. VCV Rack is different. Everything is voltage. Everything can be modulated. A guitar tone is not just a tone, it can become part of a patch: envelope followers pushing filters, sequencers changing gain staging, gates responding to clocks, cabinets being blended like oscillators.
NAM Player is my attempt to make Neural Amp Modeler feel native to that world. It loads .nam captures for amps, pedals, and preamps, but the module around that core is very much a Rack instrument: CV-addressable gain, gate, and EQ controls; a real-time waveform display; context-menu model loading; automatic sample-rate handling; and enough real-time paranoia to keep a neural network from making the audio thread miserable.
The Rack Shape
Every VCV Rack module has two lives. The Module owns the engine state and runs DSP. The ModuleWidget owns the panel, knobs, ports, lights, displays, and context menu.
For NAM Player, that split is direct:
struct NamPlayer : Module {
std::unique_ptr<NamDSP> namDsp;
std::unique_ptr<NamDSP> pendingDsp;
std::atomic<bool> hasPendingDsp{false};
std::vector<float> inputBuffer;
std::vector<float> outputBuffer;
int bufferPos = 0;
void process(const ProcessArgs& args) override;
void loadModel(const std::string& path);
};
The plugin itself registers both NAM Player and Cabinet Simulator in Rack's normal init() callback:
void init(Plugin* p) {
pluginInstance = p;
p->addModel(modelNamPlayer);
p->addModel(modelCabSim);
}
That sounds mundane, but it is the foundation for the whole thing. Rack calls process() continuously as part of the audio engine, while the widget can safely deal with human-scale actions like opening menus, choosing bundled models, selecting custom .nam files, or changing waveform colors.
The module is mono because the source material is guitar amp modeling. If you want stereo width, you can patch it the modular way: duplicate chains, different captures, different IRs, micro-delays, modulation, or whatever other questionable decision turns into a beautiful one at 1:00 AM.
Volts In, Neural Network Out
Rack audio commonly moves around as roughly +/-5V. NAM models expect normalized floating-point audio. So the module treats the audio port like a translation boundary:
float input = inputs[AUDIO_INPUT].getVoltage() / 5.f * inputGain;
After inference and output gain, the signal gets scaled back into Rack voltage:
outputs[AUDIO_OUTPUT].setVoltage(finalOutput * 5.f);
That conversion is small, but it makes the rest of the DSP easier to reason about. Inside NamDSP, samples are float, not double, and the in-tree NAM engine is compiled with:
#define NAM_SAMPLE float
That was not just an optimization preference. Keeping the sample type consistent across all translation units avoids the kind of ABI mismatch that can turn a perfectly good WaveNet model into mystery noise. Neural DSP bugs can be wonderfully impolite: one wrong assumption at the buffer boundary and the output stops sounding like an amp and starts sounding like a haunted spreadsheet.
Why Block Processing Exists
VCV Rack calls process() sample by sample. NAM inference wants blocks. NAM Player reconciles those two worlds with small pre-allocated buffers:
static constexpr int BLOCK_SIZE = 128;
inputBuffer[bufferPos] = input;
float output = outputBuffer[bufferPos];
bufferPos++;
if (bufferPos >= BLOCK_SIZE) {
namDsp->process(inputBuffer.data(), outputBuffer.data(), BLOCK_SIZE);
bufferPos = 0;
}
This adds a tiny amount of buffering latency, but it avoids asking a neural model to do heavy work one sample at a time. That tradeoff is worth it. A 128-sample block is short enough to feel immediate in a guitar patch and large enough to keep inference from thrashing the CPU.
The other important part is what does not happen in the hot path. NAM Player pre-allocates the core buffers for input, output, display data, resampling, gated audio, and Eco Mode. The audio thread should not be casually allocating memory, blocking on file I/O, or waiting for a model loader to finish reading JSON and weights from disk.
Loading Models Without Stopping Audio
NAM files are not tiny impulse responses. Depending on the architecture, a model can include a lot of weights, metadata, and layer configuration. Loading one directly inside process() would be rude to the audio engine.
So model loading happens on a background thread. The loader creates a new NamDSP, parses the file, initializes the model at the current sample rate, and then hands it to the audio thread through an atomic pending flag:
pendingDsp = std::move(newDsp);
hasPendingDsp.store(true, std::memory_order_release);
The audio thread performs the actual swap at the top of process():
if (hasPendingDsp.exchange(false, std::memory_order_acq_rel)) {
namDsp = std::move(pendingDsp);
namDsp->setEcoModeLevel(ecoModeLevel);
}
This is one of those details that makes a module feel calm even when it is doing something heavy. The UI can say, "load this amp," while the audio path keeps moving. Once the new model is ready, the engine swaps to it at a predictable point.
Patch persistence uses the same philosophy. The module saves the model path, waveform color, Eco Mode state, and fast-tanh setting into Rack patch JSON, then reloads the model when the patch opens.
The NAM Engine Underneath
NAM Player uses an in-tree nam_rack implementation rather than treating NAM as a black box. The loader supports the architectures people actually run into with NAM captures:
LinearConvNetLSTMWaveNet
The loader parses the .nam JSON, validates supported config versions, reads weights into float buffers, extracts expected sample rate metadata, and constructs the right DSP class through a small factory registry.
WaveNet support is especially fun because the model is not just "run this array through a transfer function." It is a stack of dilated convolutions, bottleneck channels, gated activations, skip/residual behavior, and head scaling. A guitar amp capture might feel immediate under the fingers, but under the panel it is temporal modeling: the current output depends on a short history of what came before it.
That is the interesting part of NAM in a modular environment. You are not just distorting a waveform. You are running a learned dynamical system at audio rate and then letting Rack users patch voltage around it.
Fast Tanh, Eco Mode, and Other CPU Bargains
Activation functions are everywhere in neural amp models. If you are processing a WaveNet with many layers, tanh and sigmoid paths can become expensive quickly.
NAM Player exposes a Use Fast Tanh context-menu toggle. When it is on, activation paths use a fast approximation rather than exact std::tanh. For most guitar use, the difference is less important than the CPU headroom it buys. When you want conservative numerical behavior for a particular model, you can turn it off and compare.
Eco Mode is a more obvious bargain. In the current implementation, Eco Mode reduces the number of frames sent through the model by combining neighboring samples, processing the smaller block, then interpolating back:
ecoInputBuffer[i] = 0.5f * (gatedInputBuffer[src0] + gatedInputBuffer[src1]);
model->process(ecoInputBuffer.data(), ecoOutputBuffer.data(), ecoFrames);
It is not meant to be magic. It is a practical patching mode for dense sessions, live sets, higher sample rates, or places where one more neural amp is musically useful but the CPU meter is giving you a look.
There is another small optimization hiding in the gate. If the noise gate has been closed and the gated signal stays near silent for several blocks, the wrapper can skip model inference and output silence, or keep feeding silence through the tone stack if the EQ is active. Processing nothing should cost close to nothing.
Noise Gate Before the Amp
High-gain amp captures do exactly what real high-gain rigs do: they make small noises very large. So the noise gate lives before the NAM model.
The gate uses an RMS-style envelope, hysteresis, attack/release smoothing, and a hold timer. The open and close thresholds are precomputed in the linear power domain so the hot path is mostly comparisons and smoothing:
openThresholdLinear = std::pow(10.f, threshold / 10.f);
closeThresholdLinear = std::pow(10.f, (threshold - hysteresis) / 10.f);
That hysteresis matters. Without it, decaying notes can make a gate chatter around the threshold. With it, the module can clamp idle noise while still letting pick attack and sustain behave naturally.
The gate parameters are also CV-controllable. That means you can patch the gate like a modular tool, not a static guitar utility. Tighten it during a sequenced riff, open it during a drone, or make it respond to something entirely unrelated to the guitar.
The Tone Stack Is Post-NAM
The 5-band EQ sits after neural amp processing:
- Depth at 80 Hz for low-end resonance
- Bass at 100 Hz for body
- Middle at 650 Hz for punch
- Treble around 3.2 kHz for clarity
- Presence around 3.5 kHz for bite
Internally this uses Rack's TBiquadFilter<float> with shelf and peak filters. The controls are centered, so the default position is flat, and each band maps to roughly +/-12 dB.
Post-NAM placement is intentional. It makes the EQ behave like the tone-shaping stage you might reach for after choosing the amp capture: not changing the model itself, but adapting it to the patch, cabinet, register, or mix. In Rack terms, it also means all five bands become modulation destinations. A slow LFO on Presence after a captured preamp is a very different thing from a static guitar plugin preset.
Sample Rates Are a First-Class Problem
Most NAM models are trained at 48 kHz. Rack users may run at 44.1, 48, 88.2, 96, or whatever their interface and patch demand. NAM Player treats model sample rate as metadata that must be respected.
If the engine and model rates differ, the wrapper uses Rack's Speex-based SampleRateConverter:
srcIn.setRates(engineSampleRate, modelSampleRate);
srcOut.setRates(modelSampleRate, engineSampleRate);
The signal is converted into the model's expected rate, processed there, then converted back to Rack's engine rate. A yellow light tells you when that path is active. The best case is still simple: run Rack at the model's native rate, usually 48 kHz, and skip the conversion. But the module should not punish you for a different session rate.
UI as a DSP Instrument
The panel is not decoration. NAM Player's display keeps a 512-sample ring buffer of normalized output and draws a compact waveform from per-bar peaks. The color is patch-persistent because small visual preferences matter when a patch becomes part of your working environment.
The context menu carries the heavier interactions:
- Browse bundled models from
res/models - Load custom
.namfiles - Unload the current model
- Toggle Eco Mode
- Toggle Fast Tanh
- Choose waveform color
- See the loaded model name and sample-rate warning
That split keeps the front panel focused on performance controls while still making model management available where Rack users expect it.
Why Cabinet Simulator Ships With It
NAM Player is an amp/pedal/preamp stage. Guitar tone still usually needs a cabinet. That is why Guitar Tools also includes Cabinet Simulator: a companion module for loading and blending impulse responses after NAM Player.
The two-module design is deliberate. Keeping the amp model and cabinet convolution separate makes the patch more modular. You can swap cabinets without reloading amp captures, blend IRs, insert Rack modules between amp and cab, or skip the cabinet entirely if the capture already includes one.
In a normal plugin, that separation might feel inconvenient. In Rack, it is the point.
The Part I Like Most
The satisfying thing about NAM Player is not just that it runs amp captures in Rack. It is that the implementation has to respect both sides of the project.
From the NAM side, it needs model parsing, architecture dispatch, sample-rate awareness, activation behavior, buffering, and neural inference that is numerically boring in the best possible way.
From the Rack side, it needs voltage conventions, real-time discipline, patch persistence, widget ergonomics, context menus, lights, CV inputs, and a process() method that never forgets it is being called constantly.
The result is a guitar amp capture that can live inside a modular patch as a first-class signal processor. You can use it like a straightforward amp module. You can also abuse it with sequencers, envelopes, random voltage, clocked modulation, parallel cabinets, feedback networks, and all the other things that make Rack feel alive.
NAM Player is free, open source, and part of Guitar Tools for VCV Rack 2. The source is on GitHub, and the module is available through the VCV Library.
