

A Neural Amp Modeler A2-Full capture is a twenty-three-layer network that has to run once for every output sample, forty-eight thousand times a second. It is the full eight-channel architecture the captures are made with, not the three-channel A2-Lite that embedded hardware normally settles for — about seven times the arithmetic.
The ESP32-P4 now runs it in real time, on the same chip that also hosts USB audio and draws the pedal's touch interface.
The P4's vector unit is fourteen times faster than its floating-point unit at what a convolution actually does, and all 410 of its instructions are integer.
That is not a preference we could design around. A float multiply-accumulate on this core is two instructions — one to load the weight, one to multiply-add it — and the weights differ for all 1,248 tap-and-input pairs in a frame, so none of them can stay in a register. At 11,584 multiply-accumulates a frame that is 23,168 instructions against the 15,000 cycles both cores have. Even written perfectly by hand, float A2-Full needs 154% of the entire chip.
So the model runs in integers, or it does not run.
A straight int16 engine works. It sounds fine, and against the original A2-Full render it nulls at −27 dB — close enough to beat A2-Lite, and not close enough to call it the same amp.
What closed the gap was to stop treating a lane as a whole number. Each 16-bit lane holds the top half of a 24-bit value, and a second pass recovers the low eight bits. The accumulator's extract shifts in place, so both halves come out of two successive extracts — the vector unit ends up computing a wider number than it has any instruction for.
Doing that on every layer would miss the deadline, and it turns out not to be necessary. Precision compounds through the early layers and stops mattering near the output, so the wide path goes where the compounding is:
Thirteen early layers and two later hot layers ship on it. The rest stay 16-bit, and the common case stays cheap. It is one engine and one set of rules for every profile — no per-profile scales, exceptions or hand-tuned patches.
A hand-picked list of wide layers invites an obvious suspicion: that it was quietly fitted to whichever capture happened to be on the bench. The only way to answer that is to stop choosing the test.
So we downloaded 193 open source A2-Full captures — clean, crunch and high gain alike — and ran the same fifteen-second guitar DI through every one of them, one ordinary riff at 48 kHz. Each output was then subtracted sample for sample from that profile's own A2-Full render, with no gain matching and no alignment shift.
The reference deserves a sentence of its own. It is the original NeuralAmpModelerCore — the canonical A2 code — playing the original .nam capture files at full width. Every board capture on this page is nulled against that code's own output; the sweep's in-repo float harness reproduces it to −122 dB, a thousandth of the engine's own error.
Nothing lands in the −40s, the −30s or the −20s.
The last row carries more weight than it looks. This vector unit's accumulator extract wraps rather than saturating, so an overflow does not soften into distortion the way clipping does — it flips the sign and fires a full-scale click. A single one anywhere in the sweep would be impossible to miss.
That is 193 amps and one input signal, so it is a measured floor for these profiles and this DI rather than a guarantee for every guitar signal.
A relative null, though, is the engine's report card, not your ears'. It divides each profile's error by that profile's own output, so a quiet capture with a proportionally large error tops the table while producing a residual nobody can hear. What a listener experiences is the error's absolute level. So we rendered the residual of all 193 profiles, sorted them by RMS, took the three loudest — the worst offenders any listener could ever find in this library — and captured those off the physical board. Each is subtracted from the original NeuralAmpModelerCore's render; alongside its level, the column that bounds the worst instant anywhere in fifteen seconds: the largest single-sample difference between the board and the original code, out of 720,000 samples.
Every other profile in the library leaves a quieter error than these three. That is the entire cost of running the eight-channel model on an integer vector unit, and you can listen to all of it below.
First the levels, because a null test means nothing if the two files were not the same loudness to begin with:
The board matches the original code to 0.0 LU and a hundredth of a decibel RMS, and all three amp renders land inside 0.1 LU of each other. None of this is gain-matched after the fact — they come out that way. So anything you hear between them is timbre.
Every file here is lossless float32, end to end — the differences are the point, and a lossy codec would put its own floor on top of them.
The first four rows below are those same fifteen seconds through each engine, including the one captured off the board rather than rendered on a computer. Play the original NeuralAmpModelerCore render and the P4 back to back.
Then open the differences, which is where the actual answer is. Subtract the board's output from the original render and what is left is everything the integer engine got wrong — at its true level, against the model gap it replaced.
The same 15 seconds, the same DI, the same profile. The reference is the original NeuralAmpModelerCore playing the original capture file at full width; the third row is what the P4's vector unit produced, captured off the board rather than rendered on a host. The fourth is the three-channel model the pedal ran before it.
A2-Full, NAMCore −15.6 LUFS 8 channels, the original code — the reference
A2-Full on the P4 −15.6 LUFS 8 channels, selective q24 on the vector unit — captured from the board
A2-Lite, NAMCore −15.7 LUFS 3 channels, the original code at lite width — what the pedal ran before
Both differences subtracted from the same original-NAMCore reference, played at their true level — no gain is added. The first is what the pedal used to be wrong by; the second is what it is wrong by now.
A2-Full − A2-Lite −31.7 LUFS The old model gap — 3 channels standing in for 8. −17.6 dB.
A2-Full − P4 −76.7 dBFS rms What the P4 gets wrong on this profile: −60.5 dB against the original NeuralAmpModelerCore, no gain matching and no alignment; no single sample is further than 1.4e-3 of full scale from the original. Its integrated loudness is −inf, because every block falls under BS.1770's absolute gate. This is the loudest profile of the four, so its residual is the one you can still faintly hear. The capture is byte-identical to the host engine's render — what you hear is exactly what the arithmetic defines.
Both waveforms are drawn to the same absolute scale as the renders above. The model gap averages 0.18 of the loudest render; the P4's residual averages 0.0007 — about 240× smaller. That is the whole of what quantising the eight-channel model cost on this profile, and the next section is what it costs on three others.
That is one profile. Below are the three loudest error tracks the entire 193-profile library produces on this board — this time with the original NeuralAmpModelerCore render and the physical-board capture alongside each difference, so you can A/B the amp itself before listening to what separates the two. Everything plays at its true level: the differences sit at −75.3, −77.8 and −78.1 dBFS, and every other profile in the library errs more quietly than these. Every board capture on this page is also byte-identical to the host build of the same engine — all 720,000 samples of each — so the 193-profile host sweep above is not a proxy for the silicon; it is the silicon.
Mattfig's Zrek crunch capture. The original NeuralAmpModelerCore render, the physical ESP32-P4, and the original minus the board at unity gain — the loudest error track the whole library produces, at −75.3 dBFS RMS.
Original NAMCore −15.4 LUFS The original code playing the original capture file at full width.
ESP32-P4 −15.4 LUFS The same fifteen seconds captured off the physical board, byte-identical to the host engine. −59.5 dB against the original NAMCore.
Original − P4 −75.3 dBFS rms Everything the board got wrong, as a signal. Worst single sample: 6.1e-4 of full scale.
Mattfig's SS100 Plus high-gain capture — the second-loudest residual of the 193. The engine fix this build carries (full q28 range through the exact residual) pushed the old second-place Diezel Herbert down to −80.7 dBFS and out of this lab.
Original NAMCore −18.0 LUFS The original code playing the original capture file at full width.
ESP32-P4 −18.0 LUFS Captured off the physical board, byte-identical to the host engine. −59.0 dB against the original NAMCore.
Original − P4 −77.8 dBFS rms The difference at unity gain. Worst single sample: 9.5e-3 of full scale, the largest anywhere in the library.
Mattfig's JEL50 crunch capture — the third-loudest residual of the 193.
Original NAMCore −14.1 LUFS The original code playing the original capture file at full width.
ESP32-P4 −14.1 LUFS Captured off the physical board, byte-identical to the host engine. −62.2 dB against the original NAMCore.
Original − P4 −78.1 dBFS rms The difference at unity gain. Worst single sample: 1.7e-3 of full scale.
Every waveform in this lab shares one absolute scale — its loudest render — and every track plays at unity gain, lossless float32, with no alignment shift and no gain matching. These three difference tracks are the loudest the 193-profile library produces: every other profile's error is quieter than what you hear here. The full sweep spans −51.0 to −70.1 dB relative with a −62.4 dB median and zero true arithmetic overflows, and every physical capture on this page is byte-identical to the host build of the same engine.
The production build is ESP-IDF 6.0.2 at the P4 rev 1.3's qualified 400 MHz. A 128-frame pipeline gives each core 2,666.7 µs to finish a block, and no whole-layer split could balance the two halves — so the model is cut inside a layer: eight layers plus the ninth layer's convolution on one core, that layer's activation and the remaining fourteen layers and the head on the other.
Zero deadline misses in steady state, and the input drop counters stay flat after startup — every block, every frame, with USB audio, the display and touch all live. A kernel pass that removed a quarter of the epilogue kernels' address arithmetic paid for the second wide layer; bit-identical output, verified by capture CRC on the physical board.
The block size is also the pedal's latency. A 128-frame block has to be captured before the model can start on it, and two stages that each fill ~95% of a block period cannot share one, so the model spends three block periods end to end: 8 ms . High-speed USB adds roughly 2 ms on top — four-microframe transfers with a few queued either side — for about 10 ms in total. An earlier build of this engine needed 512-frame blocks and 32 ms; getting the same arithmetic to fit in a quarter of the block was mostly integer kernel work on the head readout and that mid-layer split.
The ESP32-C6 on the board does none of the inference — twenty-three layers, 193 profiles, live USB audio and the touch interface all run on the one P4.
The ESP32-P4 target will be open-sourced along with the Zero 2 W and ESP32-S3 work.
Hacker News
news.ycombinator.com