{"slug": "running-nam-a2-full-natively-on-an-esp32-p4", "title": "Running Nam A2-Full Natively on an ESP32-P4", "summary": "The ESP32-P4 now runs a full Neural Amp Modeler A2-Full capture natively in real time on the same chip that hosts USB audio and the pedal's touch interface, using an integer engine with a 16-bit path and a 24-bit wide path for early layers. Across 193 open source A2-Full captures, the engine nulls against the original NeuralAmpModelerCore at a median of −62.4 dB, with the worst at −51.0 dB and no arithmetic overflows.", "body_md": "# Running NAM A2-Full natively on an ESP32-P4\n\nA 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.\n\nThe ESP32-P4 now runs it in real time, on the same chip that also hosts USB audio and draws the pedal's touch interface.\n\n## The chip has no floating point where it matters\n\nThe 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.\n\nThat 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.\n\nSo the model runs in integers, or it does not run.\n\n## Sixteen bits, and then twenty-four\n\nA 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.\n\nWhat closed the gap was to stop treating a lane as a whole number. Each 16-bit\nlane holds the *top half* of a 24-bit value, and a second pass recovers the low\neight bits. The accumulator's extract shifts in place, so both halves come out of\ntwo successive extracts — the vector unit ends up computing a wider number than\nit has any instruction for.\n\nDoing 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:\n\n| Layers on the wide path | Null against A2-Full |\n|---|---|\n| none | −27.2 dB |\n| the first eight | −53.9 dB |\n| the first twelve | −70.0 dB |\n| all twenty-three | −75.9 dB |\n\nThirteen 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.\n\n## What that costs, across 193 amps\n\nA 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.\n\nSo 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.\n\nThe reference deserves a sentence of its own. It is the **original\nNeuralAmpModelerCore** — the canonical A2 code — playing the original `.nam`\n\ncapture files at full width. Every board capture on this page is nulled\nagainst that code's own output; the sweep's in-repo float harness reproduces\nit to −122 dB, a thousandth of the engine's own error.\n\n| Result across 193 profiles | Null against the original A2-Full |\n|---|---|\n| Worst — SLO100 C1 V30 | −51.0 dB |\n| Median | −62.4 dB |\n| Best — THC C2 V30 | −70.1 dB |\n| Profiles with a true arithmetic overflow | 0 |\n\nNothing lands in the −40s, the −30s or the −20s.\n\nThe 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.\n\nThat 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.\n\nA relative null, though, is the engine's report card, not your ears'. It\ndivides each profile's error by that profile's own output, so a quiet capture\nwith a proportionally large error tops the table while producing a residual\nnobody can hear. What a listener experiences is the error's absolute level. So\nwe rendered the residual of all 193 profiles, sorted them by RMS, took the\nthree **loudest** — the worst offenders any listener could ever find in this\nlibrary — and captured those off the physical board. Each is subtracted from\nthe original NeuralAmpModelerCore's render; alongside its level, the column\nthat bounds the worst instant anywhere in fifteen seconds: the largest\nsingle-sample difference between the board and the original code, out of\n720,000 samples.\n\n| Profile, loudest residuals of the 193 | Null vs original NAMCore | Residual RMS | Largest single-sample error |\n|---|---|---|---|\n| Zrek / 2x12 — crunch | −59.5 dB | −75.3 dBFS | 6.1e-4 (−64.3 dBFS) |\n| SS100 Plus / 4x12 — high gain | −59.0 dB | −77.8 dBFS | 9.5e-3 (−40.4 dBFS) |\n| JEL50 / 4x12 — crunch | −62.2 dB | −78.1 dBFS | 1.7e-3 (−55.5 dBFS) |\n\nEvery 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.\n\n## Listen to it\n\nFirst the levels, because a null test means nothing if the two files were not the same loudness to begin with:\n\n| Render | Integrated LUFS | RMS | Peak |\n|---|---|---|---|\n| DI going in | −35.8 | −35.32 dBFS | −7.54 dBFS |\n| A2-Full, original NAMCore — the reference | −15.6 | −16.15 dBFS | −2.48 dBFS |\n| A2-Full on the ESP32-P4 | −15.6 | −16.16 dBFS | −2.49 dBFS |\n| A2-Lite, float | −15.7 | −16.16 dBFS | −2.63 dBFS |\n\nThe 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.\n\nEvery 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.\n\nThe 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.\n\nThen 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.", "url": "https://wpnews.pro/news/running-nam-a2-full-natively-on-an-esp32-p4", "canonical_source": "https://playtaurus.com/blog/running-nam-a2-full-natively-on-an-esp32-p4", "published_at": "2026-08-20 18:15:58+00:00", "updated_at": "2026-08-20 18:45:40.335908+00:00", "lang": "en", "topics": ["artificial-intelligence", "machine-learning", "ai-infrastructure", "ai-products"], "entities": ["ESP32-P4", "Neural Amp Modeler", "NeuralAmpModelerCore"], "alternates": {"html": "https://wpnews.pro/news/running-nam-a2-full-natively-on-an-esp32-p4", "markdown": "https://wpnews.pro/news/running-nam-a2-full-natively-on-an-esp32-p4.md", "text": "https://wpnews.pro/news/running-nam-a2-full-natively-on-an-esp32-p4.txt", "jsonld": "https://wpnews.pro/news/running-nam-a2-full-natively-on-an-esp32-p4.jsonld"}}