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-character “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, Setup downloads the full build (gyan.dev, latest release, via its GitHub mirror). 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 disable \
-ss <startSec> -t <durSec> \
-i D: \
-map_chapters -1 \
-c: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. Fast (the default) passes -paranoia_mode disable: one pass, raw read — fast, but a scratched sector can become a click. Accurate passes -paranoia_mode full, which turns on every libcdio-paranoia correction flag (verification, overlap, re-read) — slower, and meant for worn or scratched discs. Since 1.1.0.0 the command also passes -map_chapters -1: the libcdio input exposes one chapter per disc track, and without it every file inherited the whole disc’s chapter table.
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’s Setup downloads the official libdiscid 0.6.5 Windows package from the MusicBrainz FTP server and copies the x64 discid.dll into the venv’s Scripts\ folder, next to python.exe, where the wrapper finds it on the DLL search path.
The DLL is about 40 KB. The setup step does:
- Detect
libdiscid.dllin the venv. If present, skip. - Otherwise, download
libdiscid-0.6.5-win.zipfrom the MusicBrainz FTP server, extract it, and copy the x64discid.dllintopython_env\Scripts\. - 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) the Python discid package is a ctypes wrapper that ships without the DLL on Windows, so the binary has to come from MusicBrainz anyway. The download is pinned to libdiscid 0.6.5; it does not update itself. 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 (discid,musicbrainzngs,mutagen,requests,Pillow). - No registry, no PATH mutation. The Python interpreter lives in
runtime\uv\python\inside the install folder. 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 — uv and Python download, venv creation, install of five 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 two Python scripts (discinfo.py and rip.py) report back to the C# UI through plain text lines, using three line-prefix conventions:
PROGRESS: <text> (human-readable progress line, on stderr, copied to the log)
EVENT: <json> (rip.py, stdout: "track_start", "track_done", "track_failed", "rip_complete", "fatal")
RESULT: <json> (discinfo.py, stdout: the disc identification, written once at the end)
The C# side reads stdout and stderr line by line (OutputDataReceived / ErrorDataReceived), 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: the C# side parses only the RESULT: payload of discinfo.py and the EVENT: objects of rip.py, reads the fields it knows and ignores the rest. 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 MP3 / OGG / M4A / FLAC / WAV grid
The UI offers five output formats. Three are lossy, two are lossless, and every encoder used is part of the FFmpeg build that Setup downloads. Since 1.1.0.0 each format has its own quality list, and a value that does not fit the format falls back to that format’s default with a log line instead of failing the track:
- MP3 via
libmp3lame— the universal compatibility target. Default 320 kbps CBR; 256 / 192 / 128 kbps and V0 / V2 VBR are also offered. - OGG (Vorbis) via
libvorbis— small files, good quality, fully open. Default quality q6 (q4 to q10, or a fixed bitrate). - M4A (AAC) via FFmpeg’s native
aacencoder — default 256 kbps. - FLAC via the native FFmpeg encoder — lossless, ~50% the size of WAV, universally supported. Compression level 5 by default (0, 8 and 12 offered) — the level changes size and speed, never the audio.
- WAV — uncompressed PCM 16/44.1, for the rare case of bit-exact archival. WAV files carry no tags.
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, with copies alongside
The artwork from Cover Art Archive is fetched as a single JPEG (the “front” image, or the closest available) and embedded into the file — the APIC ID3v2 frame for MP3, a FLAC picture block for FLAC, the covr atom for M4A (OGG and WAV files carry no embedded picture). A copy is also saved as cover.jpg in the album folder.
Lyrics from LRCLIB (the synced version when it exists, plain text otherwise) are saved as one .lrc file per track in a lyrics\ subfolder, and the same text is embedded: USLT frame for MP3, LYRICS Vorbis comment for FLAC and OGG, ©lyr atom for M4A. 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. With a uv-created venv it goes further: Scripts\python.exe is a small binary launcher with the old interpreter path built in, and uv’s cpython-3.12 version folder is a junction with an absolute target, so patching text alone leaves an app that cannot start Python.
Mp3 Extractor runs an AutoPatchVenvPaths repair before Check Prerequisites, Load Disc and Rip — not at launch. It compares the install root recorded in pyvenv.cfg with the current folder and rewrites the path-bearing text files. It then points home at the real cpython-3.12.x folder that contains python.exe (never the junction), replaces the launchers in Scripts\ with real copies of python.exe / pythonw.exe, and copies the base runtime DLLs next to them. Every step is idempotent: on an already-repaired install it changes nothing. Check Prerequisites then actually starts the environment’s Python and imports the packages, so a broken environment shows as a failed step, not a green one.
End result: the same install folder works on any Windows machine, from any drive letter, including a USB stick passed between machines. No reinstall — the only requirement on each PC is the .NET 8 Desktop Runtime.
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; libdiscid is LGPL and the musicbrainzngs client is BSD-licensed. 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 — audio/video framework (LGPL/GPL). Setup downloads the gyan.dev “full” build (a GPLv3 configuration that includes libcdio); Mp3 Extractor uses its libmp3lame and libvorbis encoders and FFmpeg’s native AAC and FLAC encoders.
- 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 relocatable Python 3.12 build that uv installs during Setup.
- mutagen — GPLv2+. ID3v2 / Vorbis Comment / MP4 tag manipulation. Used to write the ID3v2 frames (TPE1/TPE2, APIC, USLT), the FLAC picture block and the MP4 atoms.
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 larger download 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 where the format allows it, and keep a
cover.jpgand.lrcfiles next to the tracks for players that read them.