# Challenges When Playing 192KHz WAV Files on Raspberry Pi Pico

> Explore challenges of playing 192KHz WAV files on Raspberry Pi Pico. Overcome high throughput demands, RAM limits & real-time I²S re-initialization for seamless audio.

- Repository: [Elehobica/rpi_pico_wav_player](https://github.com/elehobica/rpi_pico_wav_player)
- Tags: deep-dive
- Published: 2026-03-01

---

**Playing very high sample rate WAV files such as 192KHz on the RP2040 requires sustaining approximately 8.8 MiB/s throughput over a single-bit SPI bus while managing severe RAM constraints and real-time I²S re-initialization to prevent buffer underruns.**

The `elehobica/rpi_pico_wav_player` project demonstrates that the Raspberry Pi Pico can decode Hi-Res PCM audio, but pushing the architecture to 24-bit 192KHz playback exposes critical bottlenecks in storage I/O, memory allocation, and CPU scheduling. Understanding these limitations as implemented in the dual-core playback engine is essential for developers building high-resolution audio applications on resource-constrained microcontrollers.

## SD Card Read Speed Bottlenecks

The player streams audio data through a **single-bit SPI** interface, which creates a hard ceiling on throughput. A 24-bit 192KHz stereo WAV file requires approximately **8.8 MiB/s** (192,000 samples · 3 bytes · 2 channels), a rate that approaches the practical limit of many SD cards when accessed via 1-bit SPI. When the bus cannot sustain this data rate, the `ReadBuffer` empties faster than it refills, causing audible glitches or silence.

According to the project README, this constraint is the primary failure mode for high-resolution playback, as the SPI bus lacks the bandwidth margin to tolerate slower card response times or file system overhead.

## Buffer Architecture and Memory Pressure

The `ReadBuffer` class in [`lib/PlayAudio/ReadBuffer.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/ReadBuffer.cpp) implements a double-buffered scheme to decouple SD reads from I²S output. The primary buffer size is calculated as `RDBUF_SIZE = SAMPLES_PER_BUFFER * 8` (bytes per sample), and the system maintains **8 secondary buffers** (`SECONDARY_BUFFER_SIZE` each). When playing 192KHz streams, the fill threshold (`RDBUF_THRESHOLD = RDBUF_SIZE/4`) triggers aggressive refilling; if the core-1 read task cannot execute `ReadBuffer::fill` (line 67) before the I²S DMA exhausts the current block, underruns occur.

Memory pressure compounds this issue. The RP2040 provides only ~200KB of usable RAM, and `RDBUF_SIZE` is already sized for the maximum supported format. As defined in [`lib/PlayAudio/PlayAudio.h`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayAudio.h) (line 26), increasing buffer sizes to add headroom would exceed available RAM, leaving minimal margin for stack allocations or UI operations.

## I²S Re-initialization Requirements

Sample rate changes force a complete I²S peripheral reconfiguration. When `PlayWav::skipToDataChunk` (line 55) parses a new WAV header, it compares the file's sample frequency (`sf`) against the current `PlayAudio::sampFreq`. If they differ, it sets `reinitI2s = true`. The subsequent call to `PlayAudio::play` (line 92) detects this flag, disables the DAC, invokes `i2s_setup(sampFreq, ap)` to reconfigure the clock, then re-enables output.

Failure to re-initialize produces pitch-shifted audio or complete silence, as the I²S bit clock remains synchronized to the previous file's rate. This logic is critical when switching between standard 44.1KHz and high-resolution 192KHz tracks.

## CPU Workload in the Decode Loop

The sample processing loop in `PlayWav::decode` (line 100) executes per-sample scaling and level-meter accumulation. At 192KHz stereo, this loop processes **384,000 samples per second**, each requiring 32-bit integer scaling operations (`samples[i*2+j] = …`) and accumulator updates. On the 133MHz Cortex-M0+, this workload consumes significant CPU cycles, especially when concurrent tasks (UI rendering in [`LcdCanvas.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/LcdCanvas.cpp), file system operations) contend for core-0 time.

If the decode loop cannot complete before the next DMA interrupt, the system exhibits jitter or drop-outs, as there is no hardware mixer to smooth buffer transitions.

## Volume Scaling and Dynamic Range

For 24-bit audio, the project's volume lookup table preserves full resolution only above a setting of approximately **34/100**. When users select lower volumes during 192KHz playback, the effective bit-depth reduction introduces quantization noise that is particularly audible on high-resolution material with wide dynamic range. This limitation is documented in the README (line 205) as a trade-off between computational efficiency (table lookup vs. runtime division) and audio fidelity at low listening levels.

## Hardware Mitigations: SD Card Selection

The source code cannot overcome physical SPI limitations, but hardware selection mitigates the risk. The README's compatibility table (line 221) identifies **UHS-I high-speed cards** (e.g., Samsung PRO Plus, Kioxia Exceria G2) as capable of sustaining the necessary throughput for 24-bit 192KHz streams, while cheaper cards produce intermittent glitches even at identical bitrate settings. No code changes are required, but the `PIN_SD_*` wiring must interface with a card rated for sustained sequential reads above the calculated data rate.

## Code Examples

### Forcing I²S Re-initialization for New Sample Rates

When opening a high-resolution file, ensure the I²S clock updates automatically:

```cpp
// Initialize the playback subsystem
PlayAudio::initialize();

// Create WAV player instance
PlayWav *player = new PlayWav();

// This call detects the 192KHz sample rate in the header,
// sets reinitI2s = true in PlayWav::skipToDataChunk (line 62),
// and PlayAudio::play (line 92) re-configures the peripheral.
player->play("high_res_192k.wav");

```

### Monitoring Read Buffer Fill Levels

Add diagnostic logging to `ReadBuffer::shift` to detect underrun conditions during 192KHz playback:

```cpp
bool ReadBuffer::shift(size_t bytes)
{
    if (_left < bytes) { return false; }
    _ptr += bytes;
    _left -= bytes;
    
    // Triggered when data drops below RDBUF_THRESHOLD (RDBUF_SIZE/4)
    if (_left < _fillThreshold) {
        printf("Buffer low (%zu bytes); refilling...\n", _left);
        fill();  // Line 67 in ReadBuffer.cpp
    }
    return true;
}

```

Frequent "Buffer low" messages indicate the SD card cannot sustain the 192KHz data rate through the SPI interface.

### Configuring Pinout for High-Speed Cards

While no code modification is necessary, ensure the board definition uses proper `PIN_SD_*` assignments for UHS-I cards. The secondary buffer queue in [`ReadBuffer.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/ReadBuffer.cpp) relies on fast SPI transfers to keep the I²S pipeline fed; slower cards will trigger the fill threshold continuously, causing audible artifacts on Hi-Res files.

## Summary

- **SPI throughput** is the primary constraint: 24-bit 192KHz stereo requires ~8.8 MiB/s, pushing the single-bit SPI bus to its practical limit and demanding high-quality UHS-I SD cards.
- **Buffer sizing** is fixed to `RDBUF_SIZE = SAMPLES_PER_BUFFER * 8` with 8 secondary buffers; the 200KB RAM ceiling prevents increasing these values for additional headroom.
- **I²S re-initialization** via `reinitI2s` detection in `PlayWav::skipToDataChunk` is mandatory when switching sample rates to maintain correct pitch.
- **CPU load** from the `PlayWav::decode` loop scales linearly with sample rate, leaving minimal cycles for concurrent UI or file operations at 192KHz.
- **Volume scaling** below 34/100 reduces effective bit depth on 24-bit files, introducing quantization noise during low-volume high-resolution playback.

## Frequently Asked Questions

### What SD card speed is required for 192KHz WAV playback?

You need a **UHS-I high-speed card** (such as Samsung PRO Plus or Kioxia Exceria G2) capable of sustained sequential reads through a single-bit SPI interface. Cheaper cards often fail to deliver the ~8.8 MiB/s required for 24-bit 192KHz stereo, causing buffer underruns and audible glitches as the `ReadBuffer` cannot refill in time.

### Why must the I²S peripheral re-initialize when changing files?

The I²S bit clock must match the WAV file's sample frequency exactly. When `PlayWav::skipToDataChunk` detects a different `sf` value in the new file's header, it sets `reinitI2s = true`, causing `PlayAudio::play` to call `i2s_setup` with the new rate. Without this re-initialization, the DAC would play at the wrong speed or produce no sound.

### How does RAM limitation affect high-resolution audio?

The RP2040's ~200KB RAM fixes the maximum buffer size. `RDBUF_SIZE` is calculated as `SAMPLES_PER_BUFFER * 8` bytes, and the system allocates 8 secondary buffers. Increasing these values to add margin for 192KHz streams would exceed available memory, causing allocation failures in `PlayAudio::initialize`.

### What causes drop-outs during 192KHz playback?

Drop-outs occur when the **SD read speed** cannot keep pace with the **I²S consumption rate**. At 192KHz, the decode loop in `PlayWav::decode` (line 100) and the DMA engine consume data faster than the SPI bus can refill the `RDBUF_THRESHOLD` (set to `RDBUF_SIZE/4`). Using a high-speed UHS-I card minimizes this gap, but the architecture remains bandwidth-constrained.