How the Wrong iRig Forced Me to Rewrite USB Audio
I had built my travel pedal around an iRig HD 2 and accidentally packed an HD X.
USB setup stopped before the first audio packet because a UAC1 driver on the ESP32-S3's 12 Mbit/s Full-Speed controller could not run an HD X designed around UAC2 and 480 Mbit/s High Speed.
Waiting for the HD 2 meant losing a week of work, so I downloaded the HD X firmware updater and started pulling both USB implementations apart.

The S3 needed a UAC2 driver first
UAC stands for USB Audio Class. Espressif's host component supported the UAC1 device I had planned to use. Its lower-level USB API exposed enough of the S3 controller to exchange UAC2 packets, so I wrote the missing driver for the HD X.
The driver reads the iRig's USB description to find its clock, mono guitar input, stereo output and playback timing signal. It sets the clock to 48 kHz, opens the input and output, and keeps several transfers queued so the USB controller always has more work ready.
The timing signal tells the S3 how quickly the iRig consumes playback samples. The driver follows it when deciding how many samples to send in the next USB packet. Incoming guitar samples go through the amp and effects; the processed stereo samples are queued for the trip back to the iRig.
The HD X also puts its headphone mute and digital volume under UAC2 host control. The driver unmutes the headphone path and sets it to 0 dB, leaving the physical knob to control listening level. This was a separate issue behind the silent early playback tests.
During connection, the HD X reported the largest packet it could handle in each direction. Both values were too small to carry 48 kHz audio over the S3's USB connection.
What the S3 saw
The ESP32-S3 USB controller tops out at Full Speed: 12 Mbit/s. At that speed each direction of a USB audio connection gets one packet every millisecond.
The iRig runs at 48,000 samples per second. Divide that by the 1,000 USB packets sent each second and every packet has to carry 48 samples. The guitar input is mono and stores each sample in four bytes, so capture needs 48 × 4 = 192 bytes. Playback has two channels, so it needs 48 × 2 × 4 = 384 bytes.
| Direction | Audio due every millisecond | Required packet size |
|---|---|---|
| Guitar into the S3 | 48 mono samples | 192 bytes |
| Stereo back to the iRig | 48 samples × 2 channels | 384 bytes |
Together they account for 576 bytes per millisecond. Multiply by eight bits per byte and 1,000 packets per second and the audio uses 4.608 Mbit/s, well within the S3's 12 Mbit/s USB limit.
The HD X told the host that its Full-Speed packets could hold at most 156 bytes of capture and 104 bytes of playback. A host following those values has room for 39 mono input samples and 13 stereo output samples during a millisecond in which 48 of each are due. A continuous 48 kHz stream cannot fit.
Those values came from the HD X's usual High-Speed schedule. High Speed gives the interface a chance to move audio every 125 microseconds—eight times per millisecond—so each transfer can be smaller. The firmware reused that schedule when the iRig fell back to Full Speed.
I changed the advertised Full-Speed limits to 192 and 384. The S3 could now reserve large enough packets and open both audio directions. These advertised limits are part of the USB descriptor, the information a device gives the host when it connects. Separate functions inside the iRig fill and empty the actual packets, and those functions still moved the old, smaller chunks.
Seven samples arrived
Once connected, the iRig returned about 28 bytes of capture data every millisecond. At four bytes per mono sample, that is seven guitar samples per packet: roughly 7,000 samples per second from a 48 kHz interface. That fed the pedal at less than one-sixth of real time, with large gaps between pieces of the guitar signal.
The S3's playback counter reached 48,000 samples per second because it measured what the S3 tried to send. Capture was starving the effects pipeline, so most of those outgoing samples were silence. Playback also had a separate problem inside the iRig. A small function runs whenever a USB playback packet arrives; it still requested 104-byte chunks, and the factory playback queue was too small to accept a complete 384-byte Full-Speed packet.
The next patch had to change both per-packet functions and the memory behind them.
Finding the speed bit
The HD X is built around an NXP i.MX RT1011 microcontroller. Its firmware keeps some code in external flash and copies its fastest routines into a small block of instruction RAM when it boots. The updater gave me a raw ARM image with no source or symbols. I kept the iRig closed; every backup, memory capture, recovery and test went through USB.
I backed up the complete 2 MiB flash, captured the live instruction RAM, disassembled both, and traced the functions that receive and transmit USB audio.
The wrapper reads the negotiated bus speed before each call, forwarding High-Speed traffic to the factory scheduler and applying the corrected packet sizes at Full Speed.
I first used a controller field named PSPD, short for port speed. That gave me
the wrong result because NXP defines the field for hardware acting as a USB
host. Here the S3 is the host and the iRig is the device. In device mode the
iRig's negotiated speed lives in the HSP bit of another controller register:
clear for Full Speed and set for High Speed.
I inserted a small wrapper in front of the factory audio functions. It reads that bit and chooses one of two paths:
Mac / High Speed → factory scheduling
ESP32-S3 / Full Speed → 192-byte capture, 384-byte playback
At Full Speed my wrapper asks the original capture function for 192 bytes. The playback side gets a 384-byte request, a larger queue, and memory buffers aligned so the RT1011's USB hardware and CPU cache agree on their contents. At High Speed the wrapper calls the factory functions unchanged.
The permanent wrapper is 360 bytes of ARM code.
The last 2.7 KiB of RAM
The first wrapper used addresses that looked empty in the firmware image. The iRig stopped booting.
Its NXP ROM bootloader still appeared over USB. I loaded a temporary flash programmer into RAM and wrote a known-good image back from the 2 MiB backup. The recovery tool then read all 2 MiB back and compared it byte for byte before resetting the iRig.
The RT1011 has 128 KiB of FlexRAM, divided at boot between code, data and general-purpose memory. The HD X gives 64 KiB to code, 32 KiB to data and 32 KiB to general use. Its startup code claims almost all of that last block, including areas that look empty in the firmware file.
Only 2,792 bytes at the end were genuinely free. Into that space I fitted a 192-byte capture buffer, a 384-byte playback buffer, a small diagnostic record and a 2 KiB playback queue. The buffers also needed 32-byte alignment so the USB hardware and CPU cache saw the same data.
Moving everything into that verified 2.7 KiB tail brought the iRig back to life.
Sending diagnostics through the guitar input
For one packet, the guitar input became my debugging cable. A diagnostic build placed the iRig's playback state where guitar samples normally go, and the S3 decoded it. The values showed a successful 384-byte playback request, 384 bytes available, 768 bytes waiting in the new 2 KiB queue, and the Full-Speed bit set correctly.
The probe switched itself off as soon as playback worked. The next packet carried guitar audio again.
The 56-second acceptance run
The final S3 run lasted just over 56 seconds, moving more than 2.7 million samples in each direction at 48 kHz. It was long enough to catch the failures I had been chasing. A proper soak test still needs doing.
| Result on the ESP32-S3 | Measured |
|---|---|
| Capture rate | ≈48,000 samples/s |
| Playback rate | ≈48,000 stereo pairs/s |
| USB transfer errors | 0 |
| Input/output drops | 0 / 0 |
| Core 1 deadline misses | 0 |
Each DSP stage has 667 microseconds to finish its part of an audio block. A separate timing run put Core 0 at 94.7% and Core 1 at 95.9% of that window. The live firmware counts late blocks on Core 1, the busier stage, and recorded zero.
Five of the seven effect slots were enabled for the live test. The S3 captured the guitar, ran it through the NAM amp and those five effects, and sent it back to the iRig's headphone output.
I heard it. It sounded excellent.

Back on the Mac
Back on the Mac, the patched iRig negotiated at 480 Mbit/s and followed its original High-Speed path. A five-second CoreAudio test completed cleanly. Mac operation stayed exactly as before.
I changed the reported firmware version from 1.02 to 1.03 so macOS makes the installed image obvious.
Development moved to Wi-Fi
The working iRig occupies the S3's only USB-C port. Firmware updates and logs had used that port too, which became inconvenient immediately.
The pedal now has authenticated Wi-Fi maintenance: dual-slot OTA, buffered boot logs, device status and a small set of maintenance commands. USB stays connected to the iRig while builds and status travel over the network.
The HD X stays in the bag
The HD X stays in my travel setup now. Its firmware selects the factory High-Speed schedule on a Mac and one complete 48-sample packet per millisecond on the ESP32-S3. The speed choice happens during USB enumeration, so I can move the cable without reflashing it.
The interface I packed by mistake became part of the pedal.