The fragmented CD metadata landscape
Imagine you’ve just slipped a CD into your laptop and you’d like — on one Windows machine, with no cloud account — to end up with a folder of tagged audio files, embedded cover art, and synchronised lyrics. The good news is the data is all there, in the open, under permissive licences. The less good news is that it lives in three different places, talking to three different APIs, with three different identifiers.
- MusicBrainz hosts the canonical metadata for the disc — release title, artist, track titles, durations, release year, label. The data is CC0. The identifier used to fingerprint a physical CD is a 28-byte “disc ID” computed from the table-of-contents (TOC) plus the lead-out. The reference library is libdiscid, also under LGPL.
- Cover Art Archive hosts the artwork, keyed by the MusicBrainz release ID. Each image is licensed individually by its contributor — typically CC0 or CC-BY. The art is served as plain JPEG/PNG over HTTP, no API key, no rate-limit theatre, just GET the URL.
- LRCLIB serves lyrics — both plain and synchronised (LRC format) — under CC0, indexed by track title + artist + duration. No account, no API key.
Each of these is a wonderful service on its own. The friction is that nothing connects them out of the box. The user is left to compute the disc ID, query MusicBrainz to get the release MBID, then GET the Cover Art Archive URL, then query LRCLIB by track for each song, then somehow run FFmpeg with the right paranoia mode against the right CD drive — all while keeping per-track metadata in sync. Mp3 Extractor’s job is to make the seven steps look like one click.
Why an FFmpeg full build (and not the essentials)
The audio decode itself runs through FFmpeg. Almost every FFmpeg distribution for Windows lists two flavours: essentials and full. Essentials is smaller; full is bigger. Everywhere else in the ExpSoft catalogue (DatasetVideoChunker, the AI projects that produce intermediate video) we use the essentials build because the LGPL-clean subset of decoders/encoders is enough.
For Mp3 Extractor we ship the full build. The reason is a single dependency: libcdio. libcdio is the library FFmpeg uses to address the physical CD drive directly — to read raw audio sectors with paranoia (error correction by re-read and majority vote) instead of going through the Windows MMC driver as a generic file. libcdio is in the full build but not in essentials.
Concretely, the rip command looks like this:
ffmpeg \
-f libcdio -paranoia_mode 3 \
-ss <startSec> -t <durSec> \
-i D: \
-codec:a libmp3lame -b:a 320k \
output.mp3
Note the input is the drive letter D: with -f libcdio. Note also that -ss and -t are given in seconds — but the underlying physical position is in sectors, and one second of CD audio is exactly 75 sectors. libdiscid returns sector offsets when it reads the TOC; the conversion is one division and the inverse multiplication when we hand the value back to FFmpeg. FFmpeg deals with the actual sector-accurate seeking internally; it just exposes seconds as the user-facing interface.
The paranoia_mode value is what the user controls via the Fast / Accurate toggle. Mode 0 disables paranoia entirely (one pass, raw read — fast, but any scratched sector becomes a click). Mode 3 enables overlap and re-read with comparison. Modes go up to 255 (every cdparanoia trick, very slow). The two presets in the app map to Fast (mode 0) and Accurate (mode 3) — anything beyond that is academic for personal use.
libdiscid — fetched separately, kept tiny
libdiscid is the canonical library used to compute the MusicBrainz disc ID from the TOC. It’s LGPL, redistributable, but for some reason it isn’t bundled in any of the common FFmpeg Windows builds. So Mp3 Extractor downloads libdiscid.dll separately at first-run, from the MusicBrainz mirrors, into the venv’s site-packages directory where the Python wrapper expects it.
It’s a 75 KB file. The first-run setup step does:
- Detect
libdiscid.dllin the venv. If present, skip. - Otherwise, download from the MusicBrainz file mirror, verify by size (we know the expected size of the official build), and place in
venv\Lib\site-packages\. - Hand off to the Python wrapper (
python-discid, LGPL), which expects to find the DLL on the standard search path.
The reason we don’t bundle it in the installer zip is twofold: (a) it would mix a strict-LGPL DLL into the install package and force more cautious redistribution paperwork; (b) keeping it as a runtime fetch lets us pick up the latest version automatically if MusicBrainz ever ships a security update. Single setup step, no friction visible to the user.
uv + python-build-standalone — why not miniconda
The Python runtime for the project is fetched via Astral’s uv with a python-build-standalone distribution (Python 3.12). Three reasons over the miniconda baseline used by most CD-ripping Python ecosystems:
- Size. A python-build-standalone install is about 50 MB; a miniconda base environment is 400 MB minimum. On a USB-stick deployment, that matters.
- Speed.
uv pip installis roughly an order of magnitude faster thanpip, and several orders of magnitude faster than conda’s solver for the kind of small dependency graph used here (mutagen,musicbrainzngs,requests,python-discid). - No registry, no PATH mutation. The Python interpreter lives in
runtime\python\next to the exe. Nothing on the user’s machine is touched outside the install folder. Pull the USB stick out and there’s no trace left behind.
The whole setup phase — Python download, venv creation, pip install of four packages, FFmpeg full build extraction, libdiscid DLL fetch — completes in about 90 seconds on a residential broadband link.
IPC via stdout prefixes — small, debuggable, no protocol
The Python rip script communicates progress and results back to the C# UI through plain stdout, using three line-prefix conventions:
PROGRESS:<0..100> (overall ripping progress percentage)
EVENT:<json> (status updates: "drive_open", "track_start", "track_done", ...)
RESULT:<json> (final summary, written exactly once on success)
The C# side reads stdout line-by-line on the OutputDataReceived event, dispatches by prefix, and updates the UI. Anything that doesn’t match a prefix is treated as informational log output and tucked into the verbose log pane.
Why this and not a JSON-RPC framing or a pipe with length-prefixed messages: simplicity. Debugging is trivial — just run the Python script standalone in a terminal and read the output as-is. There’s no schema versioning to keep in sync because the only place the C# side parses the JSON is in the RESULT: line, and that JSON is forward-compatible (unknown fields are ignored). The two pieces stay independent.
Per-track artist — the compilation edge case
The trickier corner of CD metadata is the “Various Artists” compilation: a CD where each track has a different artist, but the release itself has a single album-artist (the compiler, like a label or a soundtrack project). The standard ID3v2 frames for this are TPE1 (track artist) and TPE2 (album artist / band). Many naive rippers fill TPE1 with the album artist for every track on a compilation, which then makes every player display “Various Artists” next to every song instead of the actual performer.
MusicBrainz returns this distinction cleanly in its release JSON — each track has its own artist-credit array. Mp3 Extractor uses it: TPE1 is filled per-track from artist-credit, TPE2 is filled from the release-level artist. Players display the real performer for each track, and the album-artist field still groups the album cleanly in the library view. Small detail, but the difference between “correctly tagged compilation” and “every-song-is-Various-Artists” for the user.
Five output formats — the LAME / Vorbis / Opus / FLAC / WAV grid
The UI offers five output formats. Three are lossy, two are lossless, all are LGPL-clean encoders shipped inside the FFmpeg full build:
- MP3 via
libmp3lame— the universal compatibility target. Default bitrate 320 kbps CBR. - Vorbis via
libvorbis— small files, good quality, fully open. - Opus via
libopus— best lossy quality per bit, recent decoder support. - FLAC via the native FFmpeg encoder — lossless, ~50% the size of WAV, universally supported.
- WAV — uncompressed PCM 16/44.1, for the rare case of bit-exact archival.
The output format is chosen once per rip session. Mp3 Extractor doesn’t try to be clever and offer a “best of both worlds” mode — a single format per disc is what users actually want, and offering two simultaneous encodes would double rip time without enough benefit to justify the complexity.
Cover art and lyrics — embedded in the file, no sidecars
The artwork from Cover Art Archive is fetched as a single JPEG (the “front” image, or the closest available) and embedded directly into the file via the APIC ID3v2 frame for MP3/Vorbis/Opus, or the METADATA_BLOCK_PICTURE field for FLAC. No sidecar folder.jpg is produced — the goal is a single self-contained file that travels well between devices.
Lyrics from LRCLIB are tagged similarly: USLT (unsynchronised) and SYLT (synchronised) frames in ID3v2, or LYRICS + SYNCEDLYRICS in Vorbis comment for FLAC/Vorbis. Where the player supports it (and most modern desktop and mobile players do, sometimes with the right plugin) lyrics scroll in time with the song. Where it doesn’t, the lyrics are still visible as a static text tag.
Portable USB deployment — the AutoPatchVenvPaths step
One subtle issue with shipping a Python venv on a removable drive: the venv records absolute paths to its interpreter and site-packages at creation time. Move the venv to a different drive letter (USB stick mounted as E: on one machine, F: on another) and the venv’s activate.bat, pyvenv.cfg, and the .pth files in site-packages still reference the original drive.
Mp3 Extractor’s setup includes an AutoPatchVenvPaths step that re-writes those paths at launch if it detects a mismatch between the recorded path and the current location of runtime\python\python.exe. The patch is idempotent (re-running it on an already-correct venv is a no-op) and it’s fast enough that we just run it on every launch as a check rather than caching “last patched location” somewhere.
End result: the same install folder works on any Windows machine, from any drive letter, including a USB stick passed between machines. No reinstall.
Credits and license
Mp3 Extractor stands on the work of several open-source communities that are explicit about their licences — none of which we paid for, all of which we credit:
- MusicBrainz — the canonical CD metadata catalogue. Data is CC0; the libraries (libdiscid, musicbrainzngs) are LGPL. Maintained by the MetaBrainz Foundation.
- Cover Art Archive — operated jointly by MusicBrainz and the Internet Archive. Each image is licensed individually by its contributor (typically CC0 or CC-BY).
- LRCLIB — CC0 lyrics, both plain and synchronised. Maintained as a community-curated database.
- FFmpeg — LGPL audio/video framework. We use the “full” distribution with libcdio, libmp3lame, libvorbis, libopus, native FLAC encoder.
- libdiscid — LGPL, MetaBrainz. The reference C library for computing MusicBrainz disc IDs.
- uv — Astral, MIT. Modern Python package and runtime installer.
- python-build-standalone — Astral, MIT. The redistributable Python distribution we vendor.
- mutagen — GPLv2+. ID3v2 / Vorbis Comment / MP4 tag manipulation. Used to write APIC, USLT/SYLT, TPE1/TPE2 cleanly.
The C# host is .NET 8 WPF. The shell is original ExpSoft work; the audio pipeline is FFmpeg; the metadata pipeline is the three CC0 catalogues above. The user retains ownership of any rips they produce from their own physical media.
Take-aways
- The data needed to tag a CD properly is all there in the open — MusicBrainz, Cover Art Archive, LRCLIB. The friction is the integration, not the access.
- FFmpeg full build is worth the extra ~30 MB if you need
-f libcdio— paranoia-aware CD reading is a different code path from generic file decode. - uv + python-build-standalone is a sane replacement for miniconda on small dependency graphs: ~10× smaller, ~10× faster, no PATH or registry touch.
- Per-track artist on compilations is the small detail most rippers get wrong. MusicBrainz exposes it cleanly — use it.
- Embed cover art and lyrics in the file. Single-file portability beats sidecar files for personal libraries.