A music player for GNOME and Android

Two native apps.One Rust core.

Your music library, at home and on the move. Built to feel at home on each platform.

An independent project by Marvin Baudach.
Product design, architecture and quality — with AI-assisted development.

See it in action

Reprise in 58 seconds.

The desktop, the phone, and the sync between them. Recorded in the running apps.

Browse music and podcasts, discover releases and concerts, then sync the library to Android — with the same visualizer on both platforms.

Architecture

One core, four frontends.

I separated the music library and application logic from platform code. GNOME and Android keep their native interfaces; the CLI and MCP server use the same core without a screen. Adding a platform builds on existing behaviour.

Core and edges

Four surfaces. Dependency only points inward.

machine-enforced ↗
  1. 01GNOMEGTK4 · libadwaitanative desktop
  2. 02AndroidKotlin · Composenative mobile
  3. 03CLIRust · terminalheadless surface
  4. 04MCPRust · JSON-RPCagent surface
The dependency arrows only point inward. GNOME and Android keep their native toolkit and interaction model; CLI and MCP reuse the same application layer without pretending to be screens. The build rejects GTK, libadwaita, GStreamer and D-Bus in the core.

The boundary is enforced in the build: the core cannot depend on a UI framework. Explore CLI and MCP.

An Android album with no cover of its own gets one from MusicBrainz and the Cover Art Archive, through the same download code the desktop uses. It happens only once online sources are switched on, and the cover stays in the app's cache.

Code breakdown
529'858
lines of Rust and Kotlin470'616 Rust · 59'242 Kotlin
49.7 %
of them are tests228'912 Rust · 34'583 Kotlin
1 → 4
one core, four frontendsGNOME · Android · CLI · MCP
29
gates before every mergeall of them, every time

Where the lines sit

  • Rust, product · 231'362the core and everything built on it
  • Rust, tests · 212'782inline `#[cfg(test)]` items and the files they pull in
  • Rust, Android bridge · 26'472crates/reprise-android-ffi
  • Kotlin · 59'242the whole Android frontend, product and tests

Performance

A faster library. A measured trade-off.

A slow title-list query led me to rebuild its database index. The measurements below show the before and after, alongside the extra storage it needs.

The ledgerWhat the title index cost, and what it bought
Measured over 100'000 tracks, before and after the index rebuild. Quoted from the record, which carries the commit, the date and the method behind every row.
WhatBeforeAfterDelta
Title window over 100'000 tracks53'605 µs1'333 µs−97.51 %
Playback ID projection8'125 µs298 µs−96.33 %
Main-thread CPU while idle110 ms/s64 ms/s−41.8 %
Tag reads on a warm start4190−100 %

The price sits next to it, not in the small print: the title index costs 2'379'776 extra database bytes, up 9.85 %. The track list stays pinned by test to eight cached SQL windows and 1'600 retained rows — unchanged between 10'000 and 100'000 tracks.

Quality

AI-assisted development. Verified changes.

I use AI agents to implement focused changes, with architecture rules and automated checks governing what can merge. I also test the tests: a useful check must detect the defect it claims to prevent.

Read the quality case study

One incident · 2026-08-14

A test was measuring an app that never ships.

A queue test failed on header heights. The headers were fine. The fixture never installs the app stylesheet, so it was measuring widgets the app never renders.

what the test measured

Fixture, no stylesheet

what ships

The app, with its stylesheet

Section header heights, drawn at 3×. The unstyled fixture measured 20 px for Now Playing as a bare label and 34 px for Play Next with a button. The app stylesheet makes both uniform at the 36 px floor.

“A geometry assertion against unstyled widgets passes while the shipped button is a different size.”

the doc comment on app_css_for_test() — written before the incident, naming the trap that produced it

Since then no pull request may claim Fixes #444 until three mutations turn the suite red. If one leaves it green, the claim does not go in.

Fail closed

There is no partial merge.

SourceAI agentone change
Gates · 29
Bounds
Install
Reachable
Traceable
Green
Toolchain
Mutations3 must turn red
Mergedmainall green

29 checks green · ready to merge

29 checks from check-merge-readiness.sh. Hover one to see what it is; click one to fail it. A red check does not stop the report. It stops the merge.

What the checks refuse

Six ways a change can stop short of the branch.

05Boundaries

The core cannot grow a UI framework.

05Distribution

It installs as a desktop app, not as a demo.

03Reachable

Every action works without a mouse.

03Traceable

A rule without a test fails the build.

08Green means green

Tests, lints, formatting, documented API.

05Toolchain hygiene

The branch, the shell scripts, the worktrees.

A rule ID leads to a test, the test to a commit, and the commit to the decision. This keeps the reasoning and verification behind each change available for review.

Native design

Two frameworks. One visual signature.

GNOME conventions on the desktop, Material on the phone. I designed a shared visual language that preserves each platform's navigation and interaction patterns. The spectral seek bar makes the structure of a track visible on both.

On the phone, the waveform and spectrogram behind that bar arrive with the desktop sync. A track without them is decoded and analysed on the device, and the rest of the library follows in the background, only while music plays and never in battery saver.

Each platform also keeps its own habits. On the desktop the mini-player is a window of its own: switching to it hides the library window instead of resizing it, so the library keeps its size and place. On Android a long press on an artist offers Delete from device…; after a confirmation that it cannot be undone, it deletes the files of that artist's tracks from the phone.

Explore the interactive seek bar

The spectral seek bar, live

Height is the level. Colour is the frequency.

0:00centroid 0.00level 0.00−—:—

Move across the measured track to inspect its values. The bars are shaped by the same functions the apps use — bars.rs, waveform.rs and spectral_colour.rs — with only the band values standing in for live PCM.

Height — the body

Every bar is the RMS of its slice, mapped through the track's own p10–p95 window and smoothed against flicker. A compressed master still shows verse against chorus instead of one loud wall.

Colour — the frequency

The tint is the spectral centroid: coral is low and weighty, teal high and airy.

#FF6F5E · lowhigh · #4FDBD4

Automation

The same library, ready for automation.

The CLI and MCP server bring the same library into scripts and agent workflows. Capability flags control access, and changes appear in the running desktop app.

CLI commands and MCP permissions
reprise-cli · frontend 03

reprise-cli library summary

reprise-cli search "portishead"

reprise-cli playlist create "Focus"

reprise-cli scan ~/Music

reprise-cli instrumental create 481

reprise-cli jobs status --batch b-2f9c

reprise-cli events tail --since 0

reprise-cli concerts list --all --json

Every command takes --json for machine consumption and --db for a scratch library, so automation never has to touch the real one. Deleting a playlist refuses to run without --yes.

reprise-mcp · frontend 04read is safe, writes are opt-in
  • library:readsearch tracks, artists, albums, playlistson
  • playback:controltransport, volume, seek, queueon
  • playlist:createcreate a manual playlistoff
  • playlist:managerename, append tracksoff
  • sources:managepodcasts, YouTube, radiooff
  • tags:writeapply Library Doctor tag fixesoff
  • device:syncconfigure and run phone syncoff

Tools over stdio, each behind one of seven capability flags read live from the library. A revocation takes effect on the next call. Responses never carry filesystem paths, cache locations or credentials — and the source resources omit stored URLs, because those can hold access tokens.