Skip to content
andrewfaderPublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

LOAMP - Linux Open Audio Music Player

CI Coverage

A music player built with Ruby, GTK4 and libadwaita for Linux.

Screenshot

LOAMP main window playing Relationships by Haim

Features

  • Native libadwaita interface: adaptive split view, header bar, toasts, and automatic light/dark following the desktop preference
  • GStreamer playback with accurate seeking, real position reporting and gapless track transitions, ReplayGain, a ten-band preset EQ, and optional two-pipeline crossfade
  • A projectM-style visualization page: uses the projectm GStreamer element when installed and automatically falls back to Goom
  • MPRIS2 desktop integration: media keys, the GNOME Shell media widget and playerctl all drive the player, with cover art in the notification
  • Album art from embedded tags, from cover art beside the file, or fetched from the Cover Art Archive when the file has neither
  • Broad format support (MP3, FLAC, Ogg Vorbis, Opus, M4A, WAV and more) with full tag reading via TagLib
  • An indexed local library: folders are scanned on a background thread into a SQLite database with full-text search, so browsing and searching stay instant
  • Virtualised playlist that stays responsive at any size
  • Playlist management with shuffle and repeat modes; a track that will not play is reported and skipped rather than ending the queue
  • Standard M3U/M3U8 playlist import and export
  • Volume with a perceptual (cubic) curve, and mute; volume, shuffle and repeat are remembered between launches
  • Internet-radio discovery through Radio Browser, with live ICY song-title updates, favorites, history, and directory-server failover
  • Synced embedded/sidecar/LRCLIB lyrics, podcasts with resume and downloads, Subsonic/Navidrome/Jellyfin servers, and durable ListenBrainz/Last.fm scrobbling
  • Similar-artist discovery graph and an endless, steerable local-library radio

Requirements

  • Ruby 4.0+
  • GTK4 and libadwaita development libraries
  • GStreamer 1.0 with the good/bad/ugly plugin sets
  • TagLib
  • SQLite with FTS5, for the library index and the podcast and graph stores. The packaged sqlite3 gem ships one, so nothing extra is needed.

The visualizer needs gtk4paintablesink and at least one visualization plugin. LOAMP prefers projectM, then goom, then goom2k1; the standard GStreamer good/bad packages provide the fallback on most distributions.

Installation

Install system dependencies (Ubuntu/Debian)

Install Ruby 4.0+ and its development headers first. Ubuntu 24.04's default Ruby package is too old; use a Ruby 4.0+ installation on that release.

sudo apt-get update
sudo apt-get install libgtk-4-dev libadwaita-1-dev libtag1-dev \
  libgstreamer1.0-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
  gstreamer1.0-plugins-ugly build-essential pkg-config

make install runs install.sh, which does the same thing and adds the PipeWire GStreamer plugin.

Install Ruby gems

bundle install

make deps checks that everything the player needs is actually present.

Desktop entry (optional)

make desktop

Installs loamp.desktop into ~/.local/share/applications/, pointed at the current checkout, so LOAMP appears in the application launcher.

Usage

Run the music player:

ruby loamp.rb

The pages

A view switcher along the bottom of the window moves between pages. Now Playing, Lyrics and Visualizer are always there; the rest appear when the thing behind them is available.

Page What it is Appears when
Now Playing Album art, track details and the queue always
Lyrics Synced lyrics from tags, a sidecar .lrc, or LRCLIB always
Visualizer projectM or Goom, rendered into the window always
Library The indexed local collection, with search the library database opened
Radio Radio Browser stations, favorites and history a directory server answered
Podcasts Subscriptions, episodes, resume points and downloads the podcast store opened
Streaming Search across connected Subsonic/Jellyfin servers a provider is configured
Discover The similar-artist graph and steerable radio a similarity service is reachable

Building the library

The Library page starts empty. Add Folder indexes a music folder in the background — the UI stays live while it runs, and the page fills in as tracks land. Folders are remembered as auto-scan paths, so launching LOAMP again (or choosing Rescan Library in the main menu) picks up anything added or changed since. Manage those paths under Library Folders…: add entire trees, or remove a folder (and its tracks) from the library. Overlapping folders are deduplicated rather than indexed twice.

Double-clicking a track queues everything listed beside it and starts there; right-clicking one offers Play, Play Next and Add to Queue, so neither consequence has to be guessed at. A search that matches nothing says so in the list rather than leaving an empty pane behind.

Keyboard shortcuts

Key Action
Space Play / pause
← / → Seek back / forward 10 seconds
Ctrl+← / Ctrl+→ Previous / next track
↑ / ↓ Volume up / down
M Toggle mute
Ctrl+F Open and focus Library search
Ctrl+O Add audio files
Delete (in queue) Remove selected track
Menu / Shift+F10 (in queue or library) Open selected row's actions
Alt+↑ / Alt+↓ (in queue) Reorder selected track

Right-click a queue entry to play it next, move it, or remove it. Actions apply to the clicked entry, even when a different entry was selected.

Media keys work too, through MPRIS — see below.

Radio, Discover, Podcasts and the Visualizer

The Radio page opens with popular stations from the public Radio Browser directory. Search by station, place, or genre; double-click a result to add it to the queue and start its live stream.

The Similar Artists item in the main menu opens Discover, LOAMP's live artist graph and Pandora-style local-library radio. Enter an artist to expand related artists, click a node to explore it, or double-click one to start a station. ListenBrainz supplies the default graph. Set LOAMP_LASTFM_API_KEY to enable Last.fm as a fallback for artists that cannot be resolved to a MusicBrainz ID.

The Podcasts page opens with top charts from Apple’s podcast directory. Search by show name, double-click a result to subscribe, or paste an RSS/Atom URL. Episodes remember where you stopped; unsubscribe from My Shows.

The Visualizer page is always present. Start it while audio is playing; its button tooltip shows the selected backend. Playback continues normally when no supported visualization plugin is installed.

Optional services

LOAMP discovers optional accounts from environment variables:

LOAMP_SUBSONIC_URL=https://music.example \
LOAMP_SUBSONIC_USER=me LOAMP_SUBSONIC_PASSWORD=secret ruby loamp.rb

LOAMP_JELLYFIN_URL=https://media.example \
LOAMP_JELLYFIN_USER_ID=user-id LOAMP_JELLYFIN_TOKEN=token ruby loamp.rb

LOAMP_LISTENBRAINZ_TOKEN=token ruby loamp.rb
LOAMP_LASTFM_API_KEY=key LOAMP_LASTFM_SECRET=secret \
LOAMP_LASTFM_SESSION_KEY=session ruby loamp.rb

Credentials stay in the process environment and are not written into the library database. Playback processing choices are available from the main menu, where the current choice is marked, and persist between launches; changing crossfade takes effect on the next launch because it selects a different GStreamer engine.

Controlling it from the desktop

LOAMP registers itself on the session bus as org.mpris.MediaPlayer2.loamp, so the standard media keys and any MPRIS client work with it:

playerctl -p loamp play-pause
playerctl -p loamp metadata

Without a session bus the player runs exactly as before, just without desktop integration.

Cover art from the network

A track with no embedded artwork and no cover image beside it is looked up on MusicBrainz and the Cover Art Archive, on a background thread — playback never waits on it. Files tagged by Picard skip the search, since they already say which release they belong to.

Both services are free and need no account. MusicBrainz allows about one request a second and asks that callers identify themselves, so LOAMP sends a User-Agent naming the application and a contact address. Point it at your own if you are packaging or forking it:

LOAMP_CONTACT='https://example.org/my-fork' ruby loamp.rb

Covers are cached under $XDG_CACHE_HOME/loamp/art/, keyed by album, and a lookup that comes back empty is remembered for a month so the same album is not searched for on every play. Delete the cache directory to start over.

Where LOAMP keeps its files

Everything follows the XDG base directory spec, so XDG_DATA_HOME, XDG_CONFIG_HOME and XDG_CACHE_HOME all move it. Anything under the cache root is safe to delete; it is rebuilt on demand.

Path Contents
$XDG_DATA_HOME/loamp/library.db The indexed local collection
$XDG_DATA_HOME/loamp/podcasts.db Subscriptions, episodes, resume points
$XDG_DATA_HOME/loamp/radio.json Station favorites and history
$XDG_CONFIG_HOME/loamp/playback.json ReplayGain, EQ, crossfade, volume, shuffle and repeat
$XDG_CACHE_HOME/loamp/art/ Downloaded cover art, keyed by album
$XDG_CACHE_HOME/loamp/lyrics/ Lyrics fetched from LRCLIB
$XDG_CACHE_HOME/loamp/radio-graph.db The similar-artist graph
$XDG_CACHE_HOME/loamp/scrobbles.json Scrobbles waiting to be submitted

Scrobbles are queued on disk so a listen survives a network outage or a crash, and are replayed on the next successful submission.

Project Structure

loamp/
├── loamp.rb                  # Main application entry point
├── lib/
│   ├── loamp.rb              # Main module
│   └── loamp/
│       ├── adw.rb              # libadwaita bindings via GObject Introspection
│       ├── application.rb      # Main application class
│       ├── audio_engine.rb     # GStreamer playbin3 pipeline
│       ├── crossfade_engine.rb # Two-pipeline engine for overlapping tracks
│       ├── playback_settings.rb# ReplayGain/EQ/crossfade choices, persisted
│       ├── player.rb           # Play order, repeat/shuffle, gapless hand-off
│       ├── playlist.rb         # Playlist management
│       ├── track.rb            # Track model
│       ├── metadata.rb         # Tag values
│       ├── metadata/reader.rb  # TagLib reading
│       ├── artwork.rb          # Cover art resolution
│       ├── art_cache.rb        # Cover art as a file:// URL, cached on disk
│       ├── file_uri.rb         # Path <-> file:// URI conversion
│       ├── visualizer.rb       # Visualization backend selection
│       ├── musicbrainz_artist.rb # Artist name -> MusicBrainz ID
│       ├── listenbrainz.rb     # ListenBrainz similarity and scrobbling
│       ├── lastfm.rb           # Last.fm similarity and scrobbling
│       ├── scrobbler.rb        # Durable scrobble queue over both services
│       ├── cover_art/          # MusicBrainz + Cover Art Archive lookup
│       │   ├── fetcher.rb      #   Background resolution, miss-aware
│       │   ├── musicbrainz.rb  #   Release lookup
│       │   ├── archive.rb      #   Image download
│       │   └── miss_log.rb     #   Remembered empty results
│       ├── http/               # Shared HTTP client and rate limiting
│       ├── library.rb          # SQLite-backed local index
│       ├── library/
│       │   ├── scanner.rb      #   Background folder walk and indexing
│       │   ├── schema.rb       #   Tables, FTS5 index, migrations
│       │   └── search.rb       #   Query parsing and ranking
│       ├── lyrics/             # Embedded, sidecar and LRCLIB lyrics
│       │   ├── resolver.rb     #   Source preference and fallback
│       │   ├── lrclib.rb       #   LRCLIB client
│       │   ├── lrc_parser.rb   #   .lrc timing parser
│       │   ├── document.rb     #   Parsed lyrics with timings
│       │   └── cache.rb        #   On-disk cache
│       ├── podcast/            # Feeds, episodes, downloads
│       │   ├── client.rb       #   Feed fetching
│       │   ├── parser.rb       #   RSS/Atom parsing
│       │   ├── feed.rb         #   Feed and episode models
│       │   ├── opml.rb         #   OPML subscription lists
│       │   ├── store.rb        #   Subscriptions and resume points
│       │   └── downloader.rb   #   Offline episode downloads
│       ├── provider.rb         # Streaming provider interface
│       ├── provider/
│       │   ├── subsonic.rb     #   Subsonic/Navidrome
│       │   └── jellyfin.rb     #   Jellyfin
│       ├── radio/              # Internet radio and the similarity graph
│       │   ├── browser.rb      #   Radio Browser directory, with failover
│       │   ├── station.rb      #   Station model
│       │   ├── store.rb        #   Favorites and history
│       │   ├── station_queue.rb#   Live stream queueing and ICY titles
│       │   ├── similarity.rb   #   Similar artists from ListenBrainz/Last.fm
│       │   ├── graph_store.rb  #   Cached graph
│       │   └── graph_layout.rb #   Force-directed layout
│       ├── mpris.rb            # MPRIS2 interface definitions
│       ├── mpris/
│       │   ├── adapter.rb      #   MPRIS semantics over the player
│       │   ├── service.rb      #   D-Bus export via GIO
│       │   └── variant.rb      #   Ruby values as GVariants
│       └── ui/
│           ├── style.rb              # Colour scheme and CSS
│           ├── main_window.rb        # Main window and view stack
│           ├── secondary_views.rb    # Optional pages, built when available
│           ├── player_controls.rb    # Player controls widget
│           ├── playback_menu.rb      # ReplayGain, EQ and crossfade menu
│           ├── playback_observers.rb # Player state -> widget updates
│           ├── keyboard_shortcuts.rb # Key bindings
│           ├── playlist_view.rb      # Virtualised playlist (Gtk::ColumnView)
│           ├── track_info.rb         # Now playing pane with album art
│           ├── library_view.rb       # Library browsing and search
│           ├── library_name_factory.rb # Library list item rendering
│           ├── library_row_menu.rb    # Track/album/artist row menu
│           ├── lyrics_view.rb        # Synced lyrics page
│           ├── visualizer_view.rb    # Visualization page
│           ├── radio_view.rb         # Radio search, favorites, history
│           ├── podcast_view.rb       # Subscriptions and episodes
│           ├── provider_view.rb      # Streaming server search
│           └── graph_view.rb         # Similar-artist graph and radio
├── assets/                   # Icons, images and stylesheet
├── Gemfile                   # Ruby dependencies
├── PLAN.md                   # Running record of in-flight work
└── README.md                 # This file

Testing

LOAMP includes a comprehensive test suite with unit tests, integration tests, and code quality checks.

Running Tests

Quick Test Run

# Run all tests
make test

# Or using the test runner script
./test_runner.sh

Individual Test Suites

# Unit tests only
bundle exec rspec

# Integration tests only  
ruby spec/integration_test.rb

# Code quality check
bundle exec rubocop

# Tests with coverage
make coverage

Test Structure

spec/
├── spec_helper.rb              # SimpleCov (≥80%) + RSpec config
├── support/                    # Helpers (GTK, audio fixtures, BDD aliases, async wait)
├── features/                   # BDD-style feature scenarios (user journeys)
├── factories/                  # Test data factories
├── loamp/                      # Unit tests, mirroring lib/loamp/
│   └── ui/                     #   UI component + e2e visualizer specs
├── integration_test.rb         # Smoke integration (playback, library, podcasts)
├── real_audio_integration_spec.rb
└── loamp_spec.rb

Feature specs under spec/features/ use scenario/feature aliases and wait_until for idle-driven GTK flows (podcast browse, auto-scan folders, queue polish, radio Recent, Discover).

Specs that reach the network talk to a local stub server, so the suite runs offline. GTK must be able to connect to a Wayland or X11 display when the suite loads; MPRIS specs also need a session bus. xvfb-run -a dbus-run-session -- bundle exec rspec runs everything on a headless machine. End-to-end UI specs save window captures under a temporary directory; LOAMP_SCREENSHOT_DIR puts them somewhere you can keep. Tests default to Cairo and software OpenGL for reproducible captures; the application keeps the desktop's rendering defaults.

Test Coverage

The test suite covers:

  • Core Logic: Track metadata, playlist management, player state
  • Playback: Engine state transitions, and real decoding of generated files
  • Library: Scanning, schema migrations, search ranking, auto-scan folders
  • Services: Radio, podcasts (directory browse + subscribe), lyrics, streaming providers and scrobbling, each against a local stub HTTP server
  • Desktop Integration: MPRIS adapter, D-Bus export and GVariant conversion
  • UI Components: GTK widgets and user interactions
  • BDD features: User journeys under spec/features/ (scenario aliases)
  • Integration: Smoke script plus e2e visualizer / screenshot specs
  • Error Handling: Graceful failure scenarios
  • Edge Cases: Boundary conditions and invalid inputs

Coverage (SimpleCov, minimum 80%) is opt-in via COVERAGE=true so a green suite is not poisoned by GTK teardown. CI sets that flag when uploading coverage/coverage.json to Codecov.

Continuous Integration

GitHub Actions runs RuboCop, tests, integration checks and the source build on every push and pull request, against Ruby 4.0. Native libraries are installed before Bundler compiles the Ruby bindings. GTK tests run under Xvfb and a private D-Bus session; optional GStreamer plugin tests skip only when their plugin is absent.

Ruby 4.0 enforces the 80% coverage minimum and uploads the report to Codecov. Set the repository's CODECOV_TOKEN secret if Codecov requires authentication. CI saves test screenshots and coverage reports as workflow artifacts, along with loamp-source, the application archive. Dependabot checks gems and Actions monthly.

Build locally with bundle exec rake build. It checks all Ruby syntax and creates pkg/loamp-1.0.0.tar.gz, containing the application, assets, locked dependencies and installation instructions. Building does not need a display; running the extracted application still requires the system libraries and bundle install described above.

Test Commands Reference

Command Description
make test Run all tests
make coverage Run tests with coverage report
make quality Run quality checks (rubocop + tests)
make ci Run full CI suite
bundle exec rake build Check syntax and build the source archive
bundle exec rspec Unit tests only
ruby spec/integration_test.rb Integration tests only
bundle exec rubocop Code style check
./test_runner.sh Full test suite with colored output

License

MIT License

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages