playr 0.1.0

A minimal TUI music player that plays local files and contacts nothing
Documentation

playr

A minimal TUI music player. Plays a directory, a saved playlist, or the results of a search. Keeps a SQLite index of your library.

It contacts no server, fetches no metadata, scrobbles nothing, and has no network code in it at all.

Varispeed on a Boards of Canada record is a good use of an afternoon.

Features

Playback

  • Plays a file, a directory recursively, a saved playlist, or a search result
  • Gapless within a run of tracks that share a sample rate
  • Play, pause, next, previous, stop
  • Seek by 5 seconds in either direction
  • Varispeed in semitone steps, 0.5x to 2.0x, pitch moving with tempo
  • Volume as a float gain applied before quantisation
  • A file it cannot decode is reported and skipped, never fatal

Audio quality

  • Opens the output at the file's own sample rate whenever the device allows it, so nothing is resampled in the common case
  • Sinc resampling when a rate is refused, not linear interpolation
  • f32 throughout: decode, mix and gain, quantised once at the device
  • Lock-free ring between the decoder and the realtime callback
  • Underruns emit silence rather than repeating stale samples

Library

  • Recursive scan with tag and stream-property reading
  • Rescan skips files whose size and modification time are unchanged
  • Rows for deleted files are pruned
  • Full-text search over title, artist, album and album artist
  • Search results play directly as an ad-hoc queue
  • Playlists saved to and loaded from the database

Interface

  • Three views: library, queue, playlists
  • Search filters as you type
  • Queue cursor follows playback, so the playing track stays on screen
  • Now playing shows title, artist, source rate, channels, volume and speed
  • Progress bar with elapsed and total time
  • Vim and arrow key navigation

Install

make build                    # debug
cargo build --release         # release

Needs Rust 1.89+, ALSA headers (libasound2-dev on Debian and Ubuntu), and a C compiler. SQLite is vendored and compiled from source, which is what the C compiler is for; no SQLite package has to be installed.

Opus

Opus is off by default. It needs libopus, which is vendored and built with cmake -- the only part of playr that needs it. To include it:

cargo build --release --features opus

Without it, Opus files are reported as undecodable and skipped, the same as WMA or DSD. playr formats says which build you have.

Use

playr scan ~/music          # index a directory
playr                       # browse the library
playr ~/music/some/album    # play a directory, recursively, without indexing
playr search bill evans     # play everything that matches
playr playlist "late night" # play a saved playlist
playr playlists             # list saved playlists
playr formats               # show what this build can decode

The library lives at $XDG_DATA_HOME/playr/library.db, or ~/.local/share/playr/library.db. Override it with --db <path>.

Rescanning only re-reads files whose size or modification time changed, and drops rows whose files are gone.

Keys

keys action
tab, 1 2 3 switch between library, queue and playlists
j k, up/down move
g G, home/end jump to first or last
page up/down move by ten
enter play from here; in playlists, load it
a add the selection to the queue
/ search; esc clears
s save the queue as a playlist
d delete the selected playlist
space play or pause
n p next or previous track
x stop
left/right seek back or forward 5 seconds
[ ] varispeed down or up, one semitone a press
\ back to normal speed
+ - volume
q quit

Searching filters as you type, across title, artist, album and album artist. Pressing enter on the results plays them as an ad-hoc queue.

In the queue, the cursor follows playback, so the playing track stays on screen through a long album. It moves only when the track changes, so scrolling with j/k is not fought while a track plays.

Varispeed

[ and ] change playback speed in semitone steps, and pitch moves with it, as on a tape machine or a turntable. Twelve presses is exactly an octave, so the range is 0.5x to 2.0x. The speed shows in the status bar as 1.19x (+3 st) and \ returns to normal.

This is not the pitch-preserving speed change of a podcast app. That is time-stretching, which needs a phase vocoder; this is a change of resampling ratio, which is what varispeed means.

Formats

Decoded: FLAC, ALAC, MP3, MP1, MP2, AAC-LC, Vorbis, PCM and ADPCM, in WAV, AIFF, CAF, MP4/M4A, MKV/WebM, OGG and raw FLAC containers. Opus as well, when built with --features opus.

Opus is decoded by playr itself, in both OGG and WebM. Symphonia 0.6 demuxes Opus but ships no decoder, so src/audio/opus.rs supplies one on top of libopus via the opus crate and registers it in a custom codec registry. Mono and stereo only; multistream surround is not handled.

Not decoded: WavPack, WMA, Musepack, APE, DSD, TTA, TAK, and Opus unless the feature is enabled. playr reports such a file and moves to the next track rather than stopping. Run playr formats for the current list.

Audio quality

The output stream is opened at the file's own sample rate whenever the device accepts it, so nothing is resampled in the common case. When a rate is refused, a sinc resampler converts it rather than linear interpolation. Decoding, mixing and volume are all f32, quantised once at the device.

This is not bit-perfect output. On a PipeWire system the ALSA default device accepts every rate and may convert internally. Bit-perfect playback would need a hw: device, which playr does not yet select.

Volume is a float gain applied before quantisation.

Design

docs/dev/PLAN.md covers the architecture, the trade-off behind choosing Symphonia over mpv or ffmpeg, and the assumptions that implementation disproved.

TODO.md lists what is missing and what is blocked upstream.

Tests

make test

make test runs the suite twice, with and without the opus feature, so neither build can rot unnoticed. The format and scanner tests generate real audio with ffmpeg when it is present and skip themselves when it is not. The rendering tests draw into a headless terminal, so they need no audio device.

Opus output was checked against ffmpeg by decoding the same file both ways: identical frame counts and 138.7 dB SNR, with no alignment offset.

License

MIT. See LICENSE.