# How RPi Pico WAV Player Reads ID3v2 Tags and Displays Cover Art

> Learn how the RPi Pico WAV Player efficiently reads ID3v2 tags and displays cover art from WAV files using a lightweight parser and picojpeg decoder. No temp files needed.

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

---

**The RPi Pico WAV Player extracts ID3v2 metadata and embedded JPEG cover art directly from WAV files using a lightweight tag parser, then renders the image on an ST7735S LCD via the picojpeg decoder without creating temporary files.**

The RPi Pico WAV Player is an open-source audio firmware for the Raspberry Pi Pico that plays high-resolution WAV files from SD cards and displays track metadata on a small color LCD. Understanding how it parses ID3v2 tags and renders embedded cover art requires tracing the flow from the raw file bytes through the tag parser, UI logic, and JPEG decoding pipeline.

## Parsing the ID3v2 Header and Frame List

The tag parsing logic lives in [`src/TagRead.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/TagRead.cpp) and [`src/TagRead.h`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/TagRead.h). When a track is selected, `UIPlayMode::play()` (lines 66‑71 of [`src/UIMode.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/UIMode.cpp)) calls `readTag()`, which instantiates a `TagRead` object and invokes `GetID3HeadersFull()` (lines 91‑112).

### Detecting the ID3v2 Header

`GetID3HeadersFull()` first attempts to read an ID3v1 tag, then calls `ID32Detect()` to handle the ID3v2 header. `ID32Detect()` reads the 10‑byte header (lines 189‑225) and populates an `id32` structure containing the tag size, version (2.2, 2.3, or 2.4), and flags.

### Walking the Frame List

Once the header is validated, the parser walks the frame list using a `while` loop (lines 332‑408, 426‑459, 473‑511) that iterates until the read position exceeds `id32header->size`. For each frame, it reads the 4‑byte frame ID (e.g., **APIC** for pictures), size, flags, and allocates an `id32frame` node linked to the header. The raw frame payload remains in the file; only metadata and pointers are stored in RAM.

## Extracting Cover Art from APIC Frames

The UI does not need the entire payload in memory; it only needs the file offset and size of the JPEG data. `TagRead::getPicturePos()` (lines 578‑595 of [`src/TagRead.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/TagRead.cpp)) provides this interface:

1. It calls `getID32Picture(idx, mime, ptype, pos, size, isUnsynced)` which scans the `id32frame` list for **APIC** (v2.3/v2.4) or **PIC** (v2.2) frames.
2. It extracts:
   * `mime` – MIME type (e.g., `image/jpeg`)
   * `ptype` – picture type (front cover, back cover, etc.)
   * `pos` – absolute file offset of the picture data
   * `size` – length of the picture data in bytes
   * `isUnsynced` – flag indicating if the frame uses ID3 unsynchronisation (currently unsupported by the player)

The function returns `true` if a picture is found, allowing the UI to proceed with rendering.

## UI Layer: Selecting the Image Source

The display logic resides in `UIPlayMode::draw()` (lines 25‑40 of [`src/UIMode.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/UIMode.cpp)). After rendering text tags (title, artist), the code checks for embedded cover art:

```cpp
if (tag.getPicturePos(0, mime, ptype, pos, size, isUnsynced)) {
    if (!isUnsynced && mime == jpeg && size != tagImageSize) {
        file_menu_get_fname(vars->idx_play, str, sizeof(str));
        lcd->setImageJpeg(str, pos, size);   // pass filename + offset/size
        tagImageSize = size;
        loadImageFromDir = false;
    }
}

```

If no embedded picture exists, the UI falls back to scanning the same directory for a standalone `.jpg` or `.jpeg` file (lines 42‑52) and loads it using the same `setImageJpeg()` method, but without the offset and size parameters.

## Decoding and Rendering the JPEG to LCD

The final rendering pipeline involves `LcdCanvas`, `ImageFitter`, and the `picojpeg` library.

### Configuring the Image Pipeline

`LcdCanvas::setImageJpeg()` (lines 75‑85 of [`src/LcdCanvas.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/LcdCanvas.cpp)) forwards the request to the singleton `ImageFitter`:

```cpp
image.getImagePtr(&img_ptr, &w, &h);
imgFit.config(img_ptr, w, h);
imgFit.loadJpegFile(filename, pos, size);
imgFit.getSizeAfterFit(&w, &h);
image.setImageSize(w, h);
image.update();

```

### Decoding from a File Region

`ImageFitter::loadJpegFile()` (lines 301‑324 of [`src/ImageFitter.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/ImageFitter.cpp)) uses the `JPEGDecoder` wrapper to decode the specific byte range:

```cpp
bool ImageFitter::loadJpegFile(const char *filename,
                              const uint64_t pos,
                              const size_t size)
{
    int decoded = JpegDec.decodeSdFile(filename, pos, size, 0);
    if (decoded <= 0) return false;
    // ... optional reduction and mapping to LCD buffer
}

```

`JPEGDecoder::decodeSdFile()` (line 211 of [`lib/picojpeg/JPEGDecoder.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/lib/picojpeg/JPEGDecoder.cpp)) opens the SD file via FatFS, seeks to `pos`, and reads exactly `size` bytes, feeding them to the `picojpeg` C decoder through the `pjpeg_need_bytes_callback`.

### Scaling and Display

After decoding MCU blocks, `ImageFitter::loadJpeg()` (lines 65‑165) optionally shrinks the image using `jpegMcu2sAccum` and maps the decoded RGB565 pixels into the LCD frame buffer, respecting `resizeFit`, `keepAspectRatio`, and `packHBlank` options. The cover art is rendered directly from the byte stream stored inside the WAV file’s ID3v2 tag without temporary file creation.

## Summary

- **TagRead** ([`src/TagRead.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/TagRead.cpp)) parses the ID3v2 header and walks the frame list to locate APIC/PIC frames containing cover art, storing only file offsets and sizes to conserve RAM.
- **UIMode** ([`src/UIMode.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/UIMode.cpp)) decides whether to use the embedded picture or a standalone JPEG from the same directory, then passes the filename and byte range to the LCD canvas.
- **ImageFitter** ([`src/ImageFitter.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/ImageFitter.cpp)) and **LcdCanvas** ([`src/LcdCanvas.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/LcdCanvas.cpp)) use the `picojpeg` library (`lib/picojpeg/`) to decode the specific file region, scale the image, and render it to the ST7735S LCD buffer.
- The entire pipeline operates on the SD card file directly using offsets, eliminating the need to extract temporary files on the resource-constrained Raspberry Pi Pico.

## Frequently Asked Questions

### What ID3v2 versions does the RPi Pico WAV Player support?

The parser supports ID3v2.2, ID3v2.3, and ID3v2.4. The `ID32Detect()` function reads the version bytes from the header (lines 189‑225 of [`src/TagRead.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/TagRead.cpp)), and separate frame-walking loops handle the slightly different frame header sizes for each version (lines 332‑511).

### Does the player support PNG cover art or only JPEG?

The player only supports JPEG images. The `getPicturePos()` function checks the MIME type extracted from the APIC frame (e.g., `image/jpeg`), and the downstream `ImageFitter` and `picojpeg` decoder are specifically designed for JPEG decompression. PNG is not implemented in the current codebase.

### How does the player handle ID3v2 unsynchronisation?

The parser detects the unsynchronisation flag via the `isUnsynced` parameter returned by `getPicturePos()` (line 578‑595 of [`src/TagRead.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/TagRead.cpp)). However, the UI layer explicitly checks `if (!isUnsynced)` before attempting to render the image (lines 25‑40 of [`src/UIMode.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/UIMode.cpp)), meaning the current firmware does not support decoding unsynchronised APIC frames and will skip them.

### Can the player display cover art from external JPEG files instead of embedded tags?

Yes. If no valid APIC frame is found in the ID3v2 tag, the UI falls back to scanning the same directory for standalone `.jpg` or `.jpeg` files (lines 42‑52 of [`src/UIMode.cpp`](https://github.com/elehobica/rpi_pico_wav_player/blob/main/src/UIMode.cpp)). It loads these using the same `LcdCanvas::setImageJpeg()` method but without passing offset and size parameters, indicating the entire file should be decoded.