# How WAV Playback Is Handled Internally by the PlayWav Library on Raspberry Pi Pico

> Discover how the PlayWav library handles WAV playback internally on Raspberry Pi Pico. Learn about its DMA-driven decoding and I2S streaming for RMS level meters.

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

---

**The PlayWav library handles WAV playback by parsing RIFF headers in `skipToDataChunk()`, converting raw PCM or IEEE-float samples to 32-bit integers in a DMA-driven decode loop, and streaming buffers to the I²S driver while maintaining RMS level meters.**

The `elehobica/rpi_pico_wav_player` repository provides a dedicated `PlayWav` class for WAV file playback on the Raspberry Pi Pico. Understanding how WAV playback is handled internally by the PlayWav library reveals a layered architecture that separates file parsing, sample conversion, and hardware abstraction into distinct, testable components.

## Architecture of the PlayWav Class

### Inheritance and Static Instance Management

`PlayWav` is declared in [`lib/PlayAudio/PlayWav.h`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.h) and inherits from the abstract `PlayAudio` base class. This design allows the WAV-specific decoder to leverage generic buffering and I²S output logic while focusing exclusively on RIFF container parsing and sample format conversion.

The implementation uses a static singleton pattern to bridge C-style callback interfaces with the C++ class structure. The static member `g_inst` holds a pointer to the active `PlayWav` instance, and the static method `decode_func()` forwards interrupt-driven decode requests to the instance method `decode()`【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L21-L25】.

### Integration with the Generic Audio Stack

`PlayWav` interacts with three core subsystems provided by the base class and associated libraries:

- **ReadBuffer** – Supplies buffered file I/O and tracks the current position within the WAV data chunk. Accessed via the protected `rdbuf` member inherited from `PlayAudio`.
- **I²S Driver** – The base class owns an `audio_t *ap` handle. `PlayWav` obtains DMA buffers via `take_audio_buffer()` and releases them with `give_audio_buffer()`, abstracting hardware-specific DMA handling.
- **Volume Control** – Per-sample volume scaling uses a pre-computed `vol_table` lookup, applying the current volume level and DAC offset during the conversion stage.

## WAV Playback Lifecycle

### Initialization and Header Parsing

When playback begins via `play()`, the method first resets RMS level-meter accumulators to zero, then delegates file opening and buffer setup to `PlayAudio::play()`【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L36-L42】.

The critical parsing step occurs in `skipToDataChunk()`, which scans the RIFF container structure to locate the `fmt` and `data` chunks. This method extracts essential parameters including sample rate, channel count, bits-per-sample, and audio format (PCM or IEEE float). It also records the size of the data chunk and positions the `ReadBuffer` at the start of the raw PCM data【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L44-L70】.

### The Decode Loop and Buffer Management

Actual audio output is driven by the `decode()` method, invoked periodically from the I²S DMA interrupt context via the static `decode_func()` trampoline. The loop follows this sequence:

1. **Acquire Buffer** – Obtain an empty audio buffer from the DMA pool using `take_audio_buffer()`.
2. **Fill and Convert** – Read raw bytes from the `ReadBuffer`, assemble them into samples according to the detected format (PCM 16/24/32-bit or IEEE float), convert to signed 32-bit integers, apply volume scaling via `vol_table`, and add the DAC offset.
3. **Release Buffer** – Return the filled buffer to the I²S driver with `give_audio_buffer()`.
4. **Advance and Check** – Increment sample counters, update the read buffer position, and terminate playback if the remaining data size reaches zero【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L83-L129】.

## Sample Conversion and Format Handling

### Supported Audio Formats

`PlayWav` supports multiple WAV sub-formats through conditional conversion logic in the decode loop:

- **PCM 16-bit** – Sign-extended to 32-bit.
- **PCM 24-bit** – Assembled from three bytes and sign-extended.
- **PCM 32-bit** – Used directly.
- **IEEE Float** – Converted from float to signed 32-bit integer representation.

All formats ultimately normalize to a signed 32-bit integer sample before volume scaling and DAC offset application【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L100-L115】.

### Volume Control and Level Metering

Volume adjustment uses a pre-calculated `vol_table` that maps volume levels to scaling factors. During decoding, each sample is multiplied by the current volume factor and then offset for the DAC hardware.

Simultaneously, the decoder maintains RMS-style level-meter accumulators. Every approximately 576 milliseconds (adjusted for 44.1 kHz sample rate), the accumulated values are converted to a level indicator and the accumulators reset, providing real-time audio level feedback without impacting the decode loop performance【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L115-L126】.

## Seeking and Duration Calculation

For sample-accurate seeking, `parseSetPos()` first ensures the file pointer is positioned at the start of the data chunk by calling `skipToDataChunk()`, then delegates the byte-offset calculation to `PlayAudio::parseSetPos()`【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L76-L81】.

Total track duration is computed by `totalMillis()`, which calculates the full playback time from the data chunk size, sample rate, channel count, and bits per sample. The method includes a safeguard to never return a duration smaller than the already-elapsed playback time【https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/PlayAudio/PlayWav.cpp#L138-L144】.

## Practical Implementation Examples

### Basic WAV Playback

```cpp
#include "PlayAudio/PlayWav.h"

PlayWav player;

void setup() {
    // Initialize SD card and I2S hardware via PlayAudio base class
    player.init();
}

void loop() {
    // Play entire file from beginning
    player.play("/sdcard/music.wav");
    
    // Playback runs asynchronously via DMA interrupt
    while (player.isPlaying()) {
        delay(10);
    }
}

```

### Seeking to a Specific Time Offset

```cpp
// Seek to 30 seconds into the track
size_t samplePos = 30 * player.getSampleRate();
player.play("/sdcard/track.wav", samplePos);

```

The `play()` method invokes `parseSetPos()` internally, which ensures the `ReadBuffer` is positioned at the data chunk boundary before calculating the byte offset for the requested sample position.

## Summary

- **PlayWav** extends **PlayAudio** to provide WAV-specific decoding while inheriting generic I²S and buffering infrastructure.
- **Header parsing** occurs in `skipToDataChunk()`, which validates RIFF structure, extracts fmt parameters (sample rate, bit depth, channels), and locates the data chunk.
- **Decode loop** runs in interrupt context via `decode()`, filling DMA buffers by converting PCM 16/24/32-bit or IEEE float samples to 32-bit integers with volume scaling and DAC offset.
- **Buffer management** uses `take_audio_buffer()` and `give_audio_buffer()` to interface with the RP2040 I²S DMA driver without blocking the audio callback.
- **Seeking** is supported through `parseSetPos()`, which repositions the `ReadBuffer` and calculates byte offsets for sample-accurate jumps.
- **Level metering** accumulates RMS values during decoding, updating visual indicators every ~576 ms without impacting real-time performance.

## Frequently Asked Questions

### How does PlayWav handle different WAV bit depths?

**PlayWav** supports PCM 16-bit, 24-bit, 32-bit, and IEEE float formats through conditional assembly logic in the decode loop. Raw bytes are read from the `ReadBuffer` and assembled into signed 32-bit integers regardless of source format, then normalized with volume scaling and DAC offset before output to the I²S driver.

### What is the role of the static decode_func() in PlayWav?

The static method `decode_func()` serves as a C-compatible trampoline that forwards interrupt-driven decode requests to the singleton instance stored in `g_inst`. This design allows the RP2040 I²S DMA callback to invoke the decoder using a function pointer while maintaining full access to the `PlayWav` class context and state.

### How does seeking work within a WAV file?

Seeking is implemented in `parseSetPos()`, which first ensures the file pointer is positioned at the start of the data chunk via `skipToDataChunk()`, then delegates byte-offset calculations to `PlayAudio::parseSetPos()`. This approach enables sample-accurate seeking by converting the requested sample position into byte offsets relative to the data chunk boundary.

### Why does the decode loop use take_audio_buffer and give_audio_buffer?

These methods abstract the RP2040's I²S DMA hardware interface, allowing `decode()` to acquire empty DMA buffers, fill them with converted PCM samples, and return them to the driver without managing hardware registers directly. This zero-copy approach minimizes latency and prevents buffer underruns during real-time audio playback.