• home

  • blogs

    æ
  • art

  • games

  • projects

● GTK/C++ music player.

Author(s): Renato Sanchez

Published on Thu Apr 10 2025

Summary: In order to explore more deeply GTK, a new music player has join the battle.

URL: https://github.com/renatosanz/riplay

#GTK

#linux


A fast, native, lyrics-aware music player for the GNOME desktop.

Home

riplay preview

Player

![[riplay_player.png]]

  • Version: 0.1
  • License: GPL-3.0-or-later
  • Language: C++ (gtkmm-4.0)
  • UI Toolkit: GTK4 + Libadwaita

What is it?

riplay is a GTK4 music player that reads a file’s tags and plays it, and the feature it is built around is lyrics — both kinds. A file with synchronized lyrics (SYLT, or the [mm:ss.xx] LRC convention) is played back line by line against the audio position; a file with only unsynchronized lyrics gets the whole text as a block. On top of that it exposes an extra metadata view for the fields players normally hide, and a recents list that survives a restart. Everything runs locally: no account, no network, no service.

It is not a web app in a webview: tags are parsed in-process by a statically linked TagLib, the lyrics parser is hand-written, and the whole interface is declared in Blueprint and compiled into the binary. Opening a file you have never seen touches nothing but that file.

Why?

dec2bin was the warm-up — a C program, one screen, mostly an excuse to learn what a GTK window actually is. This is the same exercise with real data. Lyrics live in the most inconsistent tags imaginable: three container formats, two incompatible lyric kinds, timestamps that are sometimes off by a hundred milliseconds and sometimes absent entirely. That mess is the point. I wanted something small enough to finish and ugly enough to teach me how a desktop app is put together, and openness is non-negotiable for me.

Some key points

1. Three Containers, Two Lyric Kinds, One Parser -> src/metadata/lyrics.cpp

LyricsManager::extractLyrics (lyrics.cpp:120) never asks the file what it is; it dynamic_casts the concrete TagLib type and reads whichever field that format uses — ID3v2 USLT first, falling back to SYLT (lyrics.cpp:132), FLAC Xiph LYRICS (lyrics.cpp:150), MP4 ©lyr (lyrics.cpp:161). Untimed text is returned as a plain string, so the caller never has to ask which kind it got: the return type is a std::variant<bool, std::string> and the string arrives with its timestamps already stripped.

parser_lyrics (lyrics.cpp:222) turns whatever came back into std::vector<LyricBar>, each entry a timestamp in microseconds plus the line. [by:...] and [offset:...] headers are recognised and collected as properties rather than being mistaken for lyrics, and a malformed line throws inside format_lyric (lyrics.cpp:176) and is skipped — one bad line never costs you the rest of the song.

2. Sync That Never Blocks The Main Loop -> src/metadata/lyrics.cpp:69

The synchronized mode runs off a plain Glib::signal_timeout at 100 ms (lyrics.cpp:28, lyrics.cpp:110) that reads the pipeline position and advances the label. The trick is that it walks the lyric list forward only, never rescanning from the top, so a track is O(1) per tick regardless of length:

for (size_t i = lyrics_index; i < sync_lyrics.size(); i++) {
  if (current_time < sync_lyrics[i].timestamp) {
    break;
  }
  if (i != lyrics_index) {
    show_lyric(i);
  }
}

LyricsManager also owns its timer, which is the part that bites: a sigc::connection does not disconnect itself, so a manager destroyed with the timer still armed will fire into freed memory. ~LyricsManager (metadata.h:34) disconnects it, which is the whole lifetime rule for this class.

3. One Long-Lived Player, Released On Purpose -> src/audio/audio_manager.cpp

AppState owns a single AudioManager for the whole process and swaps songs through it, so audio state has exactly one home. Each loadFile (audio_manager.cpp:28) tears the old playbin down first, converts the path with gst_filename_to_uri, attaches a bus watch for EOS, and pre-rolls to PAUSED so duration and stream metadata are queryable before the first frame plays.

The class is non-copyable and has a reset() (audio_manager.cpp:26) that releases the pipeline in place. Assigning a fresh manager instead would be shorter to write and quietly leak a still-playing pipeline, so the copy and move operations are deleted rather than left to do the wrong thing.

4. Declarative UI Via Blueprint -> data/ui/*.blp

Every window is authored in Blueprint and batch-compiled to .ui at build time (meson.build:48), then embedded as a GResource through gnome.compile_resources(). Views are Gtk::Builder lookups by name, so the C++ never mentions a widget’s position, styling or hierarchy — the running binary has no runtime file dependencies and nothing to install next to it.

5. Every Action Is A Real D-Bus Action -> src/models/state.cpp:82

Playback, opening files, recents, keymaps and quit are six Gio::SimpleActions registered on the application rather than callbacks hung off menu items. That single decision buys three things at once: the same code path serves the menus, the keyboard and anything else on the bus; GApplication gets single-instance behaviour for free, so a second riplay song.mp3 forwards the path to the running instance instead of starting a rival one; and the player becomes scriptable.

Architecture Map

  • main.cpp — 12 lines. Init GStreamer, init libadwaita, hand off to AppState.
  • models/models.h — AppState, SongInstance, PlaybackInfo, and the three view owners. The declaration of what exists.
  • models/state.cpp — the application: actions, window lifecycle, on_activate and on_open, and the one function that swaps songs.
  • views/home.cpp — landing window and the Gtk::FileDialog open flow.
  • views/recents.cpp — the recents sheet.
  • views/player.cpp — transport, seek bar, lyrics toggle, metadata side panel.
  • metadata/metadata.cpp — tags to FileMetadata.
  • metadata/albumart.cpp — cover art from ID3v2 attached pictures (albumart.cpp:30), FLAC picture blocks (albumart.cpp:44) or MP4 cover art (albumart.cpp:59).
  • metadata/lyrics.cpp — the container probe, the LRC parser, the sync timer.
  • audio/audio_manager.cpp — the playbin wrapper: load, play, seek, position, EOS.
  • logic/file_history.cpp — recents persistence as a GKeyFile under $XDG_CONFIG_HOME/riplay (file_history.cpp:12). No database, no daemon.
  • types.h — FileMetadata, which owns its AudioProps and cover-art buffer (types.h:19); the one place in the codebase that has to think about who frees what.

Build pipeline: Blueprint (.blp) → Greenfield XML → GResource → single native binary orchestrated by Meson + Ninja, with TagLib linked statically.

back to the top