{"slug": "coyopedal-full-size-neural-amp-modeler-captures-on-an-esp32-s3", "title": "CoyoPedal – Full-Size Neural Amp Modeler Captures on an ESP32-S3", "summary": "CoyoPedal is a standalone guitar amp and effects pedal that runs full-size Neural Amp Modeler A2 captures in real time on a Waveshare ESP32-S3-Touch-AMOLED-2.06 board, with the same firmware also compiling to WebAssembly for browser playback. The 23-layer, eight-channel WaveNet model runs at 48 kHz in block floating point with hand-written Xtensa kernels split across both cores in 64-frame blocks, and the firmware also builds for a bare ESP32-S3 module with no panel, measured at 91% and 94% of the 1,333 µs block budget across the two cores with no missed deadlines. The pedal requires an ESP32-S3 with 8 MB of PSRAM and drives a class-compliant USB Audio Class 2 interface as a USB host at 48 kHz, tested with the XTONE Pro, IK Multimedia iRig HD X, and iRig HD 2 via a dedicated UAC1 profile.", "body_md": "A standalone guitar amp and effects pedal built on the Waveshare\n**ESP32-S3-Touch-AMOLED-2.06**. It runs full-size\n[Neural Amp Modeler](https://www.neuralampmodeler.com/) A2 captures in real time\non the ESP32-S3, drives a class-compliant USB audio interface as a USB host, and\nhas a touchscreen UI written in TSX that is compiled to native C++ — there is no\nJavaScript engine on the device.\n\nThe screen is one board's worth of it rather than the pedal itself: the same firmware builds for a bare ESP32-S3 module with no panel at all, where the amp, the effects and the presets are the same and the BOOT button is the footswitch.\n\n**[Play it in your browser →](https://coyopedal.playtaurus.com/)** The same\nfirmware compiled to WebAssembly: the same UI, the same DSP, the same neural amp\nmodel, playing your guitar through an audio interface. No board required to try\nit, and nothing to install.\n\n- **Full-size NAM A2 models at 48 kHz.** The 23-layer, eight-channel WaveNet runs\nin block floating point with hand-written Xtensa kernels, split across both\ncores and processed in 64-frame blocks.\n- **Load your own captures.** Copy original`.nam` files to a microSD card, or\npoint the browser build at a folder. The pedal parses, validates and prepares\nthem on the device; there is no desktop converter.\n- **An effects chain around the amp:** gate, compressor, chorus and drive before\nit; digital delay and stereo spring reverb after it.\n- **Presets** for amp, controls and effects, stored on the pedal, with an\non-screen keyboard for naming them.\n- **A tuner** on the same screen, with the output muted while tuning.\n- **Maintenance mode** with authenticated Wi-Fi OTA updates, remote diagnostics\nand BLE discovery. The radios are completely off while playing.\n\n| Board | Alias | Flash | What it has | How you control it | \n|---|---|---|---|---|\n| Waveshare ESP32-S3-Touch-AMOLED-2.06 | `amoled` | 32 MB | ESP32-S3R8, 8 MB PSRAM, 410 × 502 AMOLED, touch, AXP2101 PMIC, microSD slot | Touchscreen; BOOT: tap to bypass, hold 1.5 s for maintenance | \n| ESP32-S3-DevKitC-1 N16R8, and bare S3R8 modules | `s3-devkit` | 16 MB | ESP32-S3R8, 8 MB PSRAM, no panel, no PMIC, no SD slot | BOOT: tap to engage or bypass, hold 1.5 s for maintenance mode | \n\nThe AMOLED board is the reference. It is what the factory presets, the browser build and the screenshots are made against, and the panel-specific parts of the firmware exist for it. A board with no screen runs the same amp through the same DSP at the same 48 kHz — measured on the devkit at 91% and 94% of the 1,333 µs block budget across the two cores, with no missed deadlines — and is configured over the maintenance API instead of by hand.\n\nWhat a board has to bring either way: an ESP32-S3 with **8 MB of PSRAM**, because\nthat is where the A2 model lives, and a USB port the chip can drive as a host. A\nbare module has no PMIC and no battery path, so unlike the AMOLED it does not\nfeed the host port itself; the interface needs power from your own supply.\n\n| Part | Notes | \n|---|---|\n| Audio | A USB Audio Class 2 interface on the board's USB port, which runs as a USB host at 48 kHz | \n| Storage | Optional microSD card, on a board that has a slot, for your own captures | \n| Tested with | XTONE Pro and IK Multimedia iRig HD X; iRig HD 2 through a dedicated UAC1 profile | \n\nThe interface is discovered from its USB descriptors, so any interface that\nexposes a 48 kHz UAC2 input and output should work. See\n[USB audio](https://github.com/dashersw/coyopedal/blob/main/docs/USB_AUDIO.md) for the formats and limits.\n\nThe home screen shows the chain in signal order. Tap a block to turn it on or\noff, or hold it to edit it: drag a slider to change a value, and use ‹ › to page\nthrough the controls. Hold the **Amp** block to open the capture browser, which\nshows the factory captures, the imported ones and the SD card's own folders. The\nswitch at the top right bypasses the whole pedal, and **Tuner** opens the tuner.\n\nTap the preset name to switch, save, rename, delete or create presets. A new preset starts from the current sound.\n\nOn every board, BOOT works as a footswitch: a short press engages or bypasses\nthe pedal, and holding it switches between audio and maintenance mode. The\nswitch starts the moment the hold reaches 1.5 seconds, so the screen tells you\nwhen you can let go. Short presses do nothing in maintenance mode.\nOn a board with no screen, presets, captures and the rest of the controls are\nreached from maintenance mode with `tools/esp32/amoled_remote.py`. The pedal\nboots engaged with the remembered preset, or the first preset, **Silver Lining**\n(clean), when no selection has been saved.\n\nSupported models are **48 kHz, eight-channel NAM A2 (\"A2-Full\") WaveNets**,\nincluding a matching member of a `SlimmableContainer`. Other architectures,\nsample rates and layer shapes are rejected with an error. Prepared `.namb` files\nare accepted as well.\n\nFormat a card as FAT and put `.nam` or `.namb` files anywhere under a `nam`\nfolder at its root, in whatever folders you like (up to six levels deep); the\nbrowser shows that tree as it is, and a capture is named after its file. File\nnames must fit in 127 bytes and files in 2 MiB. Insert the card before powering\non, then pick the capture from the **Amp** browser.\n\nThe first time a capture is selected, audio pauses while the pedal prepares it.\nThis can take tens of seconds. When the card is writable, the pedal stores a\nverified `.s3cache` file next to the original, so later loads are fast. The\noriginal file is never modified.\n\nThe full VoLum library is published in the\n[VoLum repository](https://github.com/guitarlum/VoLum/tree/main/rigs), and this\ncopies it to a mounted card as `/nam/VoLum/<amp>/`:\n\n```\npython3 tools/fetch_volum.py /Volumes/SDCARD\n```\n\n[coyopedal.playtaurus.com](https://coyopedal.playtaurus.com/) runs the firmware\nitself, and it has no card slot, so it asks for a folder instead and answers the\nfirmware's SD calls out of it. The captures show up under **SD card** in the\npedal's own amp browser with the same names and the same ids they would have on\nthe card, an original `.nam` is prepared with the same tuner and cached beside\nthe file as `.s3cache` exactly as the board caches it, and presets are mirrored\ninto the folder as `coyopedal-presets.json` — the same document\n`assets/presets.json` is, which you can open in a text editor, keep in a repo or\ndrop onto a card. Fill a folder in the browser, copy it to a card, and the board\nreads it without preparing anything again.\n\nChrome and Edge write back to the folder; Safari and Firefox will only let a page read one, so there the writes are kept in the browser's own storage.\n\n- Git and Node.js 22.13 or newer\n- Python 3 with Pillow and fontTools, used to rasterize the UI font\n- A C++20 compiler and CMake, for the host tests\n- The [Emscripten SDK](https://emscripten.org/) on`PATH` , for the web build only\n\nThe Gea CLI installs everything else, including ESP-IDF.\n\n1. \nInstall the Gea CLI: \n\n```\nnpm i -g @geastack/cli\n```\n\n2. \nInstall ESP-IDF 6.0.2 and its ESP32-S3 toolchain. It goes to `~/esp/esp-idf` ;\nan existing install under`~/esp` or`~/esp32` , or at`IDF_PATH` , is found\nautomatically.\n\n```\ngea setup --esp-idf\n```\n\n3. \nClone the repository and install its dependencies: \n\n```\ngit clone git@github.com:dashersw/coyopedal.git\ncd coyopedal\nnpm ci\n```\n\n4. \nConnect the board over USB and register it: \n\n```\ngea setup\n```\n\n Choose **Known supported board** , then**Waveshare ESP32-S3 Touch AMOLED\n2.06** , and keep the alias`amoled` : the npm scripts use it. Select the\ndetected USB device. The CLI identifies the board by its USB serial number,\nso it does not matter which port it shows up on. The OTA host is optional.For a board without a screen, see [Another board](#another-board) below; the\nrest of this section is the same.\n5. \nCheck the toolchain and the board: \n\n```\ngea doctor\ngea build --board amoled\n```\n\nThe firmware image is written to **`build/pedalboard.bin`**. The ESP-IDF build\ntree stays in `.gea/build/`; you never need to open it.\n\nThe first flash has to go over USB, because it writes the partition table and the factory data as well as the firmware:\n\n```\ngea flash --board amoled\n```\n\nAdd `--dry-run` to see what would be written without flashing.\n`npm run build:firmware` and `npm run flash:firmware` run the same two commands.\n`--board s3-devkit` builds the same firmware for the headless board; the npm\nscripts are the `amoled` shorthand.\n\nOn the AMOLED the flash is laid out as two 8 MB OTA slots, a factory-model partition, a factory-preset partition and a partition for imported models. Saved presets live in NVS, which flashing does not erase. A board with a different flash size gets its own layout — see below.\n\nThe firmware does not carry a list of boards. It asks the target definition what the hardware has and compiles out whatever is absent, and two questions decide almost everything.\n\n**Is there a screen?** A target definition with neither a `chips.display` nor a\n`canvas` means the board has no display at all, and the build gets\n`GEA_EMBEDDED_NO_DISPLAY=1`: no framebuffers, no app tree, no frame scheduler,\nno runtime task. That is about 1 MB of image and 1.25 MB of PSRAM a panel board\nspends and this one never allocates. Note that a `canvas` without a panel is a\ndifferent thing — an offscreen surface that still renders, for screenshots and\nOTA previews — so it is the absence of *both* that means \"no display\". Do not\ngive a headless board an empty canvas.\n\n**Is there a PMIC?** A definition that declares no power chip builds with\n`GEA_BOARD_HAS_POWER=0`, and `src/native/drivers/power.cpp` compiles to a no-op\nrather than failing to link against an AXP2101 that is not on the board.\n\nTo bring a new board up:\n\n1. Pick or write its target definition in `@geastack/targets` . The devkit's is`targets/esp32-s3-devkit-n16r8.json` .\n2. Register an alias for it with `gea setup` , or by hand in`.gea/boards.json` :\nthe target id, the adapter (`esp32-idf` ), the board's USB serial number, and`flashSize` when it is not 32 MB.\n3. If the flash size differs from the AMOLED's, add that board's partition\nlayout under `gea.targets.esp32.partitionsByTarget` in`package.json` , keyed\nby target id. The default`gea.targets.esp32.partitions` stays the 32 MB one,\nand a board without an entry of its own uses it.\n4. `gea flash --board <alias> --monitor` .\n\nOne rule worth knowing when a board misbehaves at compile time: the app's own\n`gea.defines` win over the board's. The pedal declares its display dimensions,\nso a board that also declares a canvas size does not get to redefine them.\n\n```\nnpm test      # USB descriptors, effects, presets and the prepared NAM model\nnpm run check # lint, TypeScript and formatting\n```\n\n[GitHub Actions](https://github.com/dashersw/coyopedal/blob/main/.github/workflows/ci.yml) runs both on every push and pull\nrequest, and builds the firmware image and the web module as well. A runner has\nno board, so the firmware job stops at the image — which is still the thing\nworth having, because the amp graph, the panel and the drivers only meet at the\nlink step.\n\n`npm run dev` is a DOM preview: the same TSX recompiled onto the web framework\nand laid out by the browser. It is quick to iterate on, and it drifts from the\nboard exactly where you would want to trust it.\n\nThe deployed page is the other one. geatsc lowers the same TSX to C++ and emcc links it with the same Gea layout and paint engine the firmware runs, so what the page shows is what the panel shows:\n\n```\nnpm run build:web-wasm\nnpm run serve:web\n```\n\nServing it locally matters: the audio graph runs in an AudioWorklet over shared\nmemory, and a browser only hands out `SharedArrayBuffer` to a\ncross-origin-isolated document, which is what `scripts/serve-web.mjs` and the\n`web/_headers` file arrange.\n\nA push to `main` publishes it. GitHub Actions builds the module and uploads the\nsite to the R2 bucket it is served from, so the page and the module it names are\nalways stamped and uploaded together. That workflow runs `scripts/deploy-web.sh`,\nwhich stages `build/site` and uploads it; run it yourself to publish without a\npush, and it wants `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` in the\nenvironment or in a `.env` the repository never tracks. `--stage` stops after\nstaging.\n\nThat token is an R2 API token with Object Read & Write on this one bucket and\nnothing else. The upload goes over R2's S3 API rather than through Wrangler,\nbecause Wrangler's `r2 object` commands use a REST endpoint that accepts only an\naccount-wide R2 Admin token and answers 403 to a token scoped this narrowly. The\ntoken is the only secret needed: `scripts/r2-credentials.sh` derives the S3 key\npair from it.\n\nThe bucket is served through a custom domain, and the two cross-origin isolation\nheaders in `web/_headers` are set by a response header transform rule on the\nzone: R2 serves only the headers an object carries as metadata, and those two\nare not among them. Cache-Control is per-object and the script sets it.\n\nMaintenance mode unloads the audio graph and starts Wi-Fi and BLE. Enter it by\ntapping the preset name, then **Setup**, then **Maintenance mode**, or with the\nboard's BOOT button — hold it until the screen says \"Please wait\", about 1.5\nseconds, on either board. Hold it again, or choose **Return to pedalboard**, to\nreboot into audio mode with the radios off. The pedal always starts in audio\nmode, unless the previous boot crashed.\n\nWi-Fi credentials and the authentication token are compiled into the firmware\nfrom `src/native/services/remote_config.h`, which is gitignored. Generate it\nbefore building:\n\n```\npython3 tools/esp32/configure_remote.py --mode ap\n```\n\n`--mode ap` makes the pedal host its own access point at `192.168.4.1`.\n`--mode station --ssid YOUR_WIFI` joins an existing network instead.\n[`remote_config.h.example`](https://github.com/dashersw/coyopedal/blob/main/src/native/services/remote_config.h.example) shows\nthe generated format.\n\nWith the pedal in maintenance mode, `tools/esp32/amoled_remote.py` finds it,\nreads its logs and updates it:\n\n```\npython3 tools/esp32/amoled_remote.py discover\npython3 tools/esp32/amoled_remote.py --host PEDAL_IP ota\n```\n\n`ota` uploads `build/pedalboard.bin` unless you pass another image. After an OTA\nupdate started from maintenance mode, the pedal boots the new image in audio\nmode once, then returns to maintenance mode so you can check the logs. Run the\nscript with `--help` to see the diagnostic commands.\n\n```\nsrc/\n  ui/          Touchscreen app (TSX, CSS, stores) for the device and the browser\n  preview/     Browser adapter with a simulated pedal\n  audio/       Effects, tuner and the per-block audio processor\n    nam/       NAM A2 engine and its ESP32-S3 assembly kernels\n  native/      Firmware\n    main/         Boot, mode selection and audio graph lifecycle\n    drivers/      USB audio host, flash storage, power\n    audio/        Board-side DSP controls and PCM packing\n    storage/      Model catalogue, SD import, .nam parsing, presets\n    services/     Maintenance Wi-Fi, OTA, BLE discovery\n    ui/           Bridge between the UI and the audio controls\n    nam_banks/    Fixed-address allocator for the model's SRAM banks\n    diagnostics/  Heap census\nweb/           The page that hosts the WASM build, and its bridge to the browser\nassets/        Factory captures, capture library index, presets, fonts\nscripts/       Build, deploy, test, format and preview helpers\ntools/         Asset packers and the maintenance client\ntests/         Host tests\nthird_party/   Vendored ESP-IDF USB host and cJSON\ndocs/          Architecture, memory layout and USB audio notes\n```\n\nStart with [docs/ARCHITECTURE.md](https://github.com/dashersw/coyopedal/blob/main/docs/ARCHITECTURE.md) for how the pieces fit\ntogether, and [docs/MEMORY.md](https://github.com/dashersw/coyopedal/blob/main/docs/MEMORY.md) before changing anything that\nallocates memory or places code.\n\nThe firmware ships with two captures from\n[VoLum](https://github.com/guitarlum/VoLum) by Lum: **Diezel Herbert, channel 1,\nV30 cabinet** and **Ampete One, channel 4, V30 cabinet**. They are distributed\nunder the MIT License; see [THIRD_PARTY_NOTICES.md](https://github.com/dashersw/coyopedal/blob/main/THIRD_PARTY_NOTICES.md).\n\nIssues and pull requests are welcome.\n[CONTRIBUTING.md](https://github.com/dashersw/coyopedal/blob/main/CONTRIBUTING.md) covers the checks, the formatting rules and\nthe handful of things about this codebase that are easy to get wrong — the\nfirmware is a Gea app, so everything the native build needs is declared in\n`package.json` rather than in a CMake file of its own.\n\nThis project is licensed under the [GNU General Public License v3.0](https://github.com/dashersw/coyopedal/blob/main/LICENSE).\nThird-party components keep their own licenses, and the firmware carries one\nadditional permission for the Espressif binary components it links; see\n[NOTICE.md](https://github.com/dashersw/coyopedal/blob/main/NOTICE.md) and [THIRD_PARTY_NOTICES.md](https://github.com/dashersw/coyopedal/blob/main/THIRD_PARTY_NOTICES.md).", "url": "https://wpnews.pro/news/coyopedal-full-size-neural-amp-modeler-captures-on-an-esp32-s3", "canonical_source": "https://github.com/dashersw/coyopedal", "published_at": "2026-09-20 09:37:39+00:00", "updated_at": "2026-09-20 09:52:49.570540+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "ai-infrastructure", "developer-tools"], "entities": ["CoyoPedal", "Neural Amp Modeler", "ESP32-S3", "Waveshare ESP32-S3-Touch-AMOLED-2.06", "ESP32-S3-DevKitC-1 N16R8", "XTONE Pro", "IK Multimedia iRig HD X", "IK Multimedia iRig HD 2"], "alternates": {"html": "https://wpnews.pro/news/coyopedal-full-size-neural-amp-modeler-captures-on-an-esp32-s3", "markdown": "https://wpnews.pro/news/coyopedal-full-size-neural-amp-modeler-captures-on-an-esp32-s3.md", "text": "https://wpnews.pro/news/coyopedal-full-size-neural-amp-modeler-captures-on-an-esp32-s3.txt", "jsonld": "https://wpnews.pro/news/coyopedal-full-size-neural-amp-modeler-captures-on-an-esp32-s3.jsonld"}}