Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

  • Four playback modes on one key: normal, shuffle, repeat, repeat one

  • Seek by 5 seconds in either direction, or 30 with shift, resuming on the exact sample

  • Marks: b marks a moment in a track, B undoes the last mark, , and . seek between marks, and marks are kept in the library

  • 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

  • Plays to float, 32-bit, 24-bit and 16-bit integer devices

  • 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 under the scanned directory are pruned

  • Scans commit every 500 files, so an interrupted scan keeps its progress

  • Full-text search over title, artist, album and album artist, or within one of them with artist:evans

  • Search results play directly, in library order

  • Playlists saved to and loaded from the database

Interface

  • Three views: library, selection, playlists

  • Search filters as you type

  • A selection to collect tracks into, edit and save as a playlist; it does not change what plays, and its tracks are marked + in the library

  • Now playing shows title, artist, source rate and channels

  • Progress bar with elapsed and total time

  • Skipped files and audio device errors shown in the status line

  • Vim and arrow key navigation

  • Vim-style : commands for every action, with Tab completion and a history; they also take arguments such as :seek 1:23 or :playlist late night

  • ? lists every key; the bottom line shows messages, speed, volume, and a level meter

  • The level meter reads momentary loudness in LUFS (ITU-R BS.1770, 400 ms) and holds the sample peak for 1.5 s. It measures the recording before the volume setting

  • The meter bar runs from -40 dB to full scale and fills green below -18 dB, yellow to -6 dB, and red above; the peak marker | takes the colour of where it sits. Red on the bar means near the top, which is normal for loud masters. The peak number turns red only at full scale, where the recording clips

Install

make build                    # debug
make release                  # release
make install                  # release, copied to ~/.local/bin

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. make install builds without Opus; to install with it, copy target/release/playr after the build above.

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 search album:blue     # match the album only
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>. Only playr scan creates it. Until then the other commands run without a library, and s cannot save a playlist. Paths are stored in full, so a scan run from any directory finds the same rows. A path that is not valid UTF-8 is skipped and counted as unreadable.

Rescanning only re-reads files whose size or modification time changed, and drops rows under the scanned directory whose files are gone. Rows elsewhere are kept, so a scan made while a drive is unmounted does not empty its playlists.

Keys

keys action
tab, 1 2 3 switch to library, selection or 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, play it
a select or unselect, then move down
/ search; esc clears
r rename the selected playlist
s save the selection; asks before overwriting
d remove from selection; delete a playlist
J K, shift up/down move a track within the selection
c clear the selection; asks y/n
space play or pause
n p next or previous track
x stop
m M next or previous playback mode
left/right seek back or forward 5 seconds
shift left/right seek back or forward 30 seconds
b mark the playing position
, . seek to the previous or next mark
B undo the last mark
C clear all marks in this track; asks y/n
[ ] varispeed down or up, one semitone a press
\ back to normal speed
+ - volume
: type a command; see Commands
? list every key
q quit

Searching filters as you type, across title, artist, album and album artist. Pressing enter on the results plays them.

Every word must match, as the start of a word. Prefix a word with a field to match it in that field alone: title:, artist:, album:, or albumartist:. Quote words to match them together in order, as in artist:"bill evans"; unquoted, a field applies only to the word it is attached to. A prefix that is not one of these fields is searched as text, so op:1 still finds a title with a colon in it.

Enter plays the list you are looking at, from the selected track: the library, search results, the selection, or a playlist. The selection is separate from what plays. It starts empty. In the library, a selects the track under the cursor, or unselects it if it is marked +, without interrupting playback. On a playlist, a adds its tracks, skipping any already selected but keeping the playlist's own repeats. s saves the selection as a playlist. To edit a playlist, add it to the selection with a, change it, and save it under the same name.

Playback modes

m steps through four modes and M steps back. The bottom line names the mode unless it is normal.

mode order after the last track
normal list order stop
shuffle every track once per pass, in random order reshuffle, go on
repeat list order start again
repeat one the current track only play it again

A mode applies to whatever list is playing: the library, search results, the selection, or a playlist. To shuffle across several playlists, add them to the selection with a and play the selection. Shuffle keeps the list on screen in its own order, and p goes back through the tracks it has played. Under repeat one, n moves on to the next track, which then repeats. Changing mode takes effect from the next track, even if it has already started loading.

Marks

b marks the playing position in the current track. Marks show as ^ under the progress bar. . seeks to the next mark and , to the previous one; within a second after a mark, , goes to the one before it, so pressing it twice steps back twice. A mark within half a second of an existing one is not added again.

Marks form a chain: B removes the mark added most recently, then the one before, whatever their positions in the track. C clears all of the track's marks; it asks first, and only y confirms.

Marks are stored in the library by file path and source frame, so they survive a rescan and stay exact at any playback speed. Without a library file they last until playr exits. A mark lands slightly after the moment you meant, by your reaction time.

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.

Commands

: opens a command line: :seek 1:23, :volume 60, :playlist late night. Every key's action has a command, and commands also take arguments no key can, such as a time or a name. Some commands work only in one view, as :remove in the selection. Tab completes, up recalls earlier lines, and :help lists every command. docs/cheatsheet.md has the full list.

Configuration

playr reads $XDG_CONFIG_HOME/playr/settings.toml, or ~/.config/playr/settings.toml, when it starts. --settings <path> reads another file instead. The file is optional, and it is read on top of the defaults in src/ui/settings.toml, so it only needs what it changes. Copying the defaults file whole is also valid.

volume = 60          # percent, 0 to 100
mode = "shuffle"     # normal, shuffle, repeat or repeat-one
speed = -3           # semitones, -12 to 12

[keys]               # every view
right = "seek +10"
shift-right = "seek +60"
ctrl-s = "save"
q = "nop"
"?" = "help"

[keys.selection]     # one view: library, selection or playlists
x = "remove"
  • Each key's value is a : command, as listed in docs/cheatsheet.md. "nop" makes a key do nothing, and "command" opens the : prompt.
  • A key under [keys.VIEW] wins in that view over the same key under [keys].
  • A key under [keys] needs a command that works in every view. d = "remove" there is refused, with the table to put it in.
  • Keys are named by their character (j, J), or as space, enter, esc, tab, backtab, backspace, delete, insert, up, down, left, right, home, end, pageup, pagedown, or f1 to f12. Prefix ctrl-, alt- or shift- for a chord; a chord only matches a binding that names it. TOML needs quotes around a key that is not a letter, digit, - or _, such as "?".
  • ctrl-c always quits, and the keys inside prompts and help lists cannot be changed.

Any error stops playr before it starts, and every bad setting is listed with its line number. ? lists the keys as bound. :map and :unmap change keys until playr exits.

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.

Tags are not read from CAF, MKV or WebM files. Those are indexed under their file names, and MKV and WebM files also show no duration.

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. The device format is chosen in the order f32, f64, 32-bit, 24-bit, then 16-bit integer.

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.

Roadmap

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. The engine, key-handling and device-failure tests play to a fake output device, so they need no audio device. One smoke test plays to the real default device at zero volume, and skips without one. Set PLAYR_REQUIRE_FFMPEG=1 or PLAYR_REQUIRE_DEVICE=1 to fail instead of skip, so a CI run cannot pass by testing nothing.

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.

About

a minimal tui music player in rust

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages