Case Study · Personal Project

Legendary MP3

A music player for people who still own their files, built around repairing the library instead of only playing it. My first native Android app, written in Kotlin and Jetpack Compose, and tested first on my own 3,000-song collection. Built solo, end to end.

376 to 28 Files Without Cover Art
0.296 to 0.581 Key Detection Score
205 Unit Tests
$4.99 One-Time Unlock

The Library

Every music player treats a broken library as the user’s chore.

3,061Songs in my own collection, which became the hostile test set
7Players compared; none performed automatic, batch metadata repair
183Artists shown for a library with one, in the player I used daily, because it split every “feat.” credit

A decade-old collection has tracks titled “Track 7,” albums called “Unknown album (4/3/2022 5:10 PM),” and missing covers. In every player I compared, tagging was a manual, one-file-at-a-time editor. That gap is the whole opening: the player is the price of admission, and the repair is the product.

The player is free, ad-free and needs no account. The repair engine is a one-time $4.99 unlock, with no backend to pay for: every lookup hits a free public database and everything else happens on the phone.

Before And After

Real problems from my own files, and what changed.

ProblemBeforeAfter
Placeholder tags“Track 7,” “Untitled” and “Unknown album (4/3/2022…)” read as complete, so repair skipped them and they stayed brokenJudged as missing; the file name and folder stand in, and the album is queued for repair
Cover art376 files with no embedded art28, and those are personal recordings no database can match
A regex bug“Daft Punk” indexed as artist “Da” featuring “Punk”A word boundary on “feat.”; 35 Daft Punk tracks verified on a phone
Split creditsA soundtrack credited “Jack Wall & Sam Hulick” spawned an artist per credit stringArtists are album first: 142 entries became 123, and two albums and 63 songs sit under one name
Musical key43% of songs came back in C, because kick drums were choosing the keyC is 7.3% and 90% of the library gets a key
Wrong or empty fieldsFixes meant editing files one at a timeOne reviewed batch, one consent prompt, three-deep undo

How A Repair Works

From a scan to a rewritten file, with nothing changing until you accept it.

  1. Scan Once, Render From The Index

    The app reads MediaStore once into a Room database and every screen draws from that index. It never crawls the filesystem at launch, which is reportedly what gives AIMP a startup measured in minutes. A rescan compares modified date and size and re-reads tags only for changed files.

  2. Judge Every Track

    Each track gets a state: complete, missing art, missing tags, untagged or conflicted. A filled-in tag can still say nothing, so placeholder titles and artists count as missing.

  3. Group By Album Before Asking

    MusicBrainz allows one request per second. One release lookup resolves a whole folder, which cuts request volume by an order of magnitude.

  4. Score The Candidates

    Track count, duration within three seconds, title, artist and year are weighed together. A title floor caps a different album by the same artist below review, a fault the match tests found.

  5. Review In Three Bands

    High scores are proposed automatically, middle scores wait for confirmation, low scores are rejected. Every proposal shows a confidence chip, and nothing touches a file yet.

  6. Write One Batch

    Android 11 and later requires consent for files the app did not create, which done naively is a dialog per file. One request covers the batch. Each file is copied to cache, edited, streamed back, and verified by length and by re-reading its header, with one retry. My own review found a kill between truncating and finishing could damage a file, which is why the check exists.

  7. Undo

    The original tag block is saved before each write, and the last three batches can be reversed. A field the engine cannot write is marked failed so it stops proposing it.

Measuring Key

Four tries, and the first three were the same mistake: I tuned the scoring when the problem was the evidence.

  1. Kick Drums Chose The Key

    The first full run over 3,047 songs came back 43% in C, 18% in F and 16% in D minor: 65, 87 and 73 Hz, where bass and kick live. The analysis started at 55 Hz, so on a hip-hop library the drum was picking the key. The fix was a longer window, a 185 Hz floor and per-frame normalization.

  2. Ambiguous Tracks Fell To C

    The library was still 38% C, because a winner only needed to beat the runner-up by 0.01 and a tie went to whichever key was tested first. A strict gate cut false C to 12.4% but answered for only 46% of songs. Nothing was measured, so I could not tell which change helped.

  3. Build The Benchmark First

    Twenty-six records with published keys, scored with the MIREX weighting: exact is 1.0, a perfect fifth 0.5, relative major or minor 0.3, parallel 0.2. A fifth away is a different kind of wrong from a random key, and scoring them the same hides whether a change helped. Writing it took a couple of hours.

  4. Rebuild The Front End

    Spectral peaks only, with parabolic interpolation, harmonic voting and a tuning correction. Coverage went from 50% to 96%, exact matches from 23% to 50%, and the weighted score from 0.296 to 0.581. On my library, 2,784 of 3,061 songs now get a key.

Which Key Profile Won

ProfileExact MatchesWeighted Score
Sha’ath (chosen)50%0.581
EDMA50%0.550
bgate46%0.542
Krumhansl42%0.531
Temperley30%0.385

The Audio Chain

Media3 does the decoding. The rest is custom processors, built to run on the audio thread without blocking.

Decode

Media3 and ExoPlayer inside a media-library service, so the notification, lock screen and Bluetooth controls come with it.

ReplayGain

Read from tags at index time and applied as volume, with the pause fade kept from fighting it.

Parametric EQ

Ten cascaded biquad filters in float, with a pre-amp, gain smoothing so a slider does not click, and a profile per output device.

Visualizer Tap

PCM copied into a lock-free ring buffer, with the FFT on another coroutine. No microphone permission.

Output

The platform audio path, or a USB DAC with the file’s own sample rate requested.

What I Cut

The decisions that kept the product small, each with its reason.

CutWhy
Audio fingerprintingThe lookup service is free only for non-commercial use, and a paid app is commercial: 50 euros a month, about $650 a year, or roughly 155 sales at $4.24 net before it pays for itself. Of 3,063 tracks, 2 (0.1%) had neither an artist nor an album, the only case it solves.
ChromecastA minority feature in this category, untestable without a Cast device, and it disables the equalizer, visualizer and crossfade while active.
Genre browsingSix library tabs did not fit at large text. Genre survives as a tag field and a smart-playlist rule.
A skin marketplaceThird-party skins lock a layout in place forever, which is why Poweramp and AIMP are stuck with aging interfaces. Three first-party styles instead, all free.
Prompt-to-playlistThere is no catalogue to draw from. A text box that only searches your own tags while looking like AI would be a lie, so smart playlists use real values from the library with a live match count.
A “bit-perfect” claimThe phone I tested does not offer it. Rate matching is what exists, and it is named as such.

Selling It Honestly

With no server, the price only has to cover a store’s cut.

AreaFreeLegendary ($4.99, Once)
PlaybackThe whole player: gapless, crossfade, ReplayGain, a queue, widget, no adsIncluded
EqualizerSimple preset equalizer10-band parametric with per-device profiles
VisualizersAll five, deliberately free: they are the screenshot that sells the appIncluded
RepairA 25-track trial, so the value shows on your own libraryUnlimited batch repair and automatic artwork
OrganizationPlaylists and M3U import and exportSmart playlists and multiple saved queues

Android First, Then iOS

My first plan was a second codebase in Swift. After Android shipped I reversed it, and put the numbers on the page.

94Android builds before launch, because local builds were free
$1Per iOS build on a rented macOS runner, about 20 minutes each
70%Of the app shared with iOS through a Kotlin Multiplatform module

Under The Hood

Local first: a native app, a playback service that outlives the screen, and an index on the phone.

Kotlin Jetpack Compose Media3 Room WorkManager MusicBrainz RevenueCat Sentry Maestro Kotlin Multiplatform

You cannot optimize what you have not measured, and a number is only as good as the reference it was checked against.

The through-line with my professional work is measurement. It is the same rule that makes a cloud cost report worth reading. The key detector went from argument to improvement the day it had a benchmark, and the cost of a feature became a decision the day it had a price per sale.
 Visit the Legendary MP3 Site
 Back to Projects