Releases: shakfu/playr
Release list
0.9.1
Fixed
-
Playback could start with the channels swapped, and stay swapped. The audio callback popped samples one at a time, filling silence while the ring was empty; when the engine pushed during the callback, the next pop succeeded, so the track began wherever the callback had reached, which could be partway through a frame. Every frame after was then one sample off, left in the right channel. This can happen whenever the ring runs empty: at the start of a track, after a seek, or on an underrun. The callback now takes only the whole frames the ring holds when it starts, and leaves anything pushed later for the next callback. Found by
enqueueing_after_the_queue_ends_starts_playback, which failed about 1 run in 200 under load on macOS CI;crates/playr-core/tests/render.rsreproduces the race directly. The swap was not heard, only reproduced against the fake device. -
a_seek_near_the_end_of_a_track_stays_in_that_trackfailed now and then on the Windows runner. It allowed the engine 100 ms to take a seek, and until the engine takes it the position is the one before it. It now waits for the seek, as the other engine tests do.
Changed
- A
hw:device that PipeWire holds is now checked to fail as busy, and is recorded so indocs/dev/device.md. PipeWire holds only the device it plays to, so another card'shw:device plays alongside it. The cpal lookup bug playr works around is drafted as an upstream report indocs/dev/issues/cpal-issue.md.
0.9.0
Added
-
Output device selection.
--device IDon all three programs, ordeviceinsettings.toml, plays to a device other than the default;playr deviceslists the IDs, which are cpal's<host>:<id>, and a barehw:2,0means the default host. Matching is by ID only: on ALSA one card's name labels up to 15 PCMs. A device that is not found stops playr at startup with the list, rather than falling back to the default, which would play through the wrong speakers and hide why output is not bit-perfect. A device another program holds is reported as busy rather than as a failed stream. Design indocs/dev/device.md.cpal 0.18.2 cannot find some IDs it lists, such as
alsa:sysdefault:CARD=X: its ALSA lookup appends,DEV=0to an ID with a card and no device. playr matches the exact ID first.Library API:
output::device,output::devices,output::by_index,output::DeviceInfo;OutputError::NotFound,OutputError::Busy;Settings::device.Player::newtakes the device ID;output::default_deviceis gone. -
A spectrogram display in the sampler view,
:display spectrogram, between dB and Braille in thewcycle; the window has a Spectrogram button. It shows level by frequency on a log scale, to separate hits the waveform merges and to show a lossy source's cutoff. It is read in the same pass as the peaks, rather than on demand in a second decode: 2048-point transforms every 512 frames into 128 bands, about 90 ms more per 4-minute track. Both draw in magma: the terminal two rows a cell in half blocks, from the 256-colour entries nearest it, or in shades without colour; the window as one texture, its frequency labels on panels. Magma over a ramp in the waveform's blue: one hue from the ground colour spans too little lightness, and most of a 90 dB range drew as the same mid-blue.Below about 340 Hz at 44.1 kHz a band is narrower than a transform bin. Such a band reads the level interpolated between the bins either side of its centre, rather than the nearest bin, which drew runs of identical rows as stepped bands that looked like content. Widening those bands to a bin each was the alternative; it left 20 to 600 Hz 21% of the height rather than 48%.
:displayin the command help now reads[DISPLAY], since the four names widened the usage column and cut other commands' help at 80 columns.Library API:
spectrum::Spectrogram,Peaks::spectrum,Display::Spectrogram,Layout::spectrum,sampler::SPECTRUM_RANGE_DB.
0.8.1
Fixed
-
Just after a seek, the position could report where playback was before it, for one pass of the engine. The device discards the pre-seek audio and raises
flush_doneat once, butframes_out,track_startandposition_offsetstill describe that audio until the engine resets them, and a reader took the device's word for it. Anything reading the position in that window acted on the old playhead::sliceplanned the region around it,bmarked it, and,and.stepped from it. The position now holds the seek target until the engine has reset the counters. -
While looping, the reported position ran past the loop's end before returning to its start, by up to one pass of the engine. The audio callback advances the device's frame count; the engine advances the matching track start and offset on its next pass, so a position read between the two was measured from the loop's previous return. The player now applies the returns the device has reached when it is asked for a position. Applying them in the callback would be as exact but would put a queue on the realtime thread.
Added
-
:rescan, or:sync, re-scans every directory previously given to:scanorplayr scan, so new files are picked up without retyping paths. Each scan records its directory in arootstable, and a bareplayr scan, a bareplayr prune,:prunewith no argument and the window's File, Rescan library all cover the recorded ones. A directory holding no audio file is not recorded; nor is one inside a directory already recorded, since the wider one covers its files, and recording a wider one drops the narrower. A scan that finds missing tracks asks before pruning them, or prunes at once withauto_prune = trueinsettings.toml. It does not ask over a prompt being typed, andauto_prunesteps aside when a directory could not be read: an unmounted drive whose mount point survives counts every track under it as missing.Library API:
db::add_root,db::roots;scan::scan_roots;Session::rescan,Session::roots;Action::Rescan;Refusal::NoRoots;Settings::auto_prune;ScanReport::unavailable.Event::Scanned,Event::Pruned, their outcomes,Action::PruneandConfirm::PrunetakeOption<PathBuf>,Nonemeaning every recorded root. -
:rootslists the directories the library covers,:roots rm DIRforgets one, and:roots add DIRis another spelling of:scan DIR.playr roots,playr roots rm DIRandplayr roots add DIRdo the same; the window has File, Library directories, with a Forget button per row. Forgetting removes the tracks under the directory, their places in playlists and their marks. Unlike:pruneit asks the filesystem nothing, so a directory that is already gone can still be forgotten, matched by the path as stored when it no longer resolves. The web page may list the directories but not change them, since the two that change them name a path.Library API:
db::forget_root;Session::check_forget,Session::forget_root;Action::ShowRoots,Action::ForgetRoot;Confirm::ForgetRoot;Presentation::RootList;Input::Roots;Outcome::Forgot;Refusal::NotARoot. -
Media keys and the now-playing panel: the keyboard's play, pause, next and previous keys, MPRIS on Linux as
org.mpris.MediaPlayer2.playr, and the macOS and Windows panels, throughsouvlaki. Linux uses itsuse_zbusfeature, so nothing links libdbus. Both frontends callModel::attach_media, which is a no-op where there is no bus or panel, and playr runs as before; on Windows the panel needs a window, so the terminal has none. MPRIS is a local bus, not a network. -
Taking up again where playr left off. The playing track and position go into a one-row
resumetable when playr closes, and again at each track change, so a playr that is killed still leaves the track behind. The next start offers it, unless the command line named tracks to play or the file has gone. Library API:db::set_resume,db::resume,db::clear_resume;Session::remember,Session::resumable,Session::forget_resume,Session::resume;Confirm::Resume. -
A cursor in the sampler view, apart from the playhead, and mark editing through it.
:cursormoves it andhreturns it to the playhead;:pick next|prevputs it on a mark, which is how one is picked up.:nudge-markmoves that mark a column,:move-mark TIMEputs it at a time,:del-markremoves it wherever it sits in the chainBundoes, and:snap-markmoves it to the nearest rise in the two seconds either side, read in the background. The window drags a mark along the waveform. A move onto another mark is refused rather than merging the two. Library API:query::move_mark,query::remove_mark;samples::nearest_onset,samples::SNAP_WINDOW;Session::mark_near,move_mark,remove_mark,snap_mark;Event::Snapped;Sampler::cursor. -
:audition,ain the sampler, plays the range, the planned slice the playhead is in, or the region around it, once, and pauses at its end rather than returning to its start as:loopdoes. Playing on continues the track from there.Cmd::PlayOncesets the same loop withonce, and the engine leaves the decoder at the end rather than seeking back;Status::loopingleaves a one-shot out, so:loopcannot switch off a range that was only auditioned.
Changed
playr-serverdrops--music DIR. The page's Rescan library re-scans the directoriesplayr scanrecorded, so the one place a root is named is the library. The button appears once the library has one. A unit file or script passing--musicmust drop it; nothing replaces it.
0.8.0
Added
-
playr-server, playr with no screen, for a machine such as a Raspberry Pi with a DAC. Its web page has the window's views, key bindings,:command line, dialogs, marks and themes, without the sampler, and adapts to a phone, a tablet or a desktop browser. With--music DIRthe page can rescan that directory, and no other; it cannot open, scan or prune a path, or quit. With--osc, OSC controls playback and plays playlists by index, and--osc-replysends the state back;playr-server osc-schemalists the addresses, andmake touchoscbuilds a TouchOSC layout from them. Seedocs/dev/server.md. Every web request needs a token, printed at startup, or none with--open, for a trusted network or behind a proxy that authenticates. Either way theHostmust name the machine, so another web site open in the browser cannot reach it. It listens on 127.0.0.1 unless--listensays otherwise, and shares the lock ofplayrandplayr-gui.playrandplayr-guistill have no network code. Release archives includeplayr-server, Linux ones with a systemd user unit, and each release a TouchOSC layout; docs/server-guide.md covers setting it up on a Raspberry Pi.playr-server --listen 0.0.0.0:8080 --open --host pi.lan --music /mnt/music \ --osc 0.0.0.0:9000 --osc-reply 192.168.1.30:9001
Changed
- The documentation covers three programs. The README introduces
playr-serverand links its guide,docs/server-guide.md, and the "no network code" claim now namesplayrandplayr-guionly.docs/architecture.mdand its crate diagram add the server, whose owner thread is half of the daemon the notes left open.docs/sampler.mdrecords that OSC for marks would go inplayr-server, the one program with network code.TODO.mdlists the server's gaps, among them the Pi and TouchOSC checks, CI browser tests and a[server]settings table.
Fixed
-
:speedcould not set a negative speed. A signed number is relative, so:speed -3lowered the speed by three semitones from where it was, yet the error for:speed 13gave the range as -12 to 12.=now makes a signed speed absolute. It was chosen over reading a bare signed number as absolute, which would change the(and)bindings and every[keys]table that copies them.:speed =-3
0.7.0
Added
-
Fine movement in the sampler view. There the arrows move the playhead a column, and with shift a tenth of the view, so a step follows zoom; elsewhere they still seek 5 and 30 s.
:snap, onS, moves nudges, marks, seeks and range ends made in the view to the nearest zero crossing within 10 ms, where the channels' mean changes sign. Marks made in the view may be a frame apart, where 500 ms elsewhere blocked close hits. A step that follows zoom was chosen over a fixed:stepsetting, so zooming in is the one way to go finer. Sign bits are kept with the peaks, 1.3 MB for a 4-minute track, so a snap does not decode. The window has a Snap to zero tick box, and its arrows nudge too. Library API:Action::Nudge,Snap;Nudge;sampler::Scale,snap,nudge,frame_of,time_of,SNAP_WITHIN;Layout::columns;Model::set_scale;Drawn::scale;Message::NoWaveform,Snap;Peaks::crossing;Session::add_mark_within.:nudge +1 :nudge -10% :snap on -
Deeper zoom in the sampler view: to one frame a cell in the terminal, and to 16 points a frame in the window, where the line display draws each frame's channels' mean around a zero line, with a dot per frame. Past the 64 frames a column the peaks hold, the view decodes the frames it shows, and 2 s either side, in the background; the window says "reading frames" until they arrive. Decoding on demand was chosen over finer peaks, which would take 4 to 32 times their memory for every track. A nudge moves at least a frame. Library API:
sampler::DetailRead;DETAIL_BELOW, replacingMIN_FRAMES_PER_COLUMN;DETAIL_MARGIN;windowandLayout::newtake the most columns a frame;Layout::per_frame,detail,with_detail;Scale::start,per_frame,shown,needs_detail;Sampler::detail;wave::Detail;Session::read_detail;Event::Detail. -
A range to slice in the sampler view, set with
<and>at the playhead,:range START END, or a drag across the window's waveform, and cleared with backspace. With both ends set, every cut uses it in place of the region between marks, and:slice markscuts at the marks inside it. It lasts until the track changes and is not saved. Under the window's waveform, buttons set and clear it, and slice the region or range whole, at marks, into a chosen count of equal parts, or at onsets with a sensitivity slider. Library API:Action::RangeIn,RangeOut,SetRange;sampler::Range;Sampler::range,range_ends,set_range_start,set_range_end;Layout::with_range;Frontend::sampler,sampler_mut;Message::Range,EmptyRange;Job::range;Session::slice_job,plan_slicesandexporttake a range. -
Looping the range in the sampler view.
lor:loopplays it over and over, returning from its end to its start sample-exactly and without a gap, and starts a paused track. Moving either end moves the loop at once; in the window, a drag from a range's edge moves that edge, and a Loop range tick box sets it.escnow clears the range when no slices are planned, which ends a loop. The engine loops rather than the frontend seeking at the end, which would leave a gap and miss the end. Library API:Cmd::Loop;Status::looping;Action::Loop;Message::Loop,NoRangeToLoop.:range 1:02 1:04.5 :loop on -
Moving one end of the range a column at a time, for setting a loop's ends while it plays, when the playhead will not stay still for
<and>.[or]picks the start or end, drawn reversed in the terminal and thicker in the window, and{}move it earlier or later, snapping when snap is on; an end stops a frame short of the other. Picking an end, then moving it, was chosen over a pair of keys for each end, so one pair moves either end. The window has Move start, Move end, Earlier and Later. Library API:Action::PickEdge,MoveEdge;sampler::Edge;Sampler::edge;Message::Edge,NoEdge.:edge end :edge -4 :edge +10%
Changed
- Varispeed moved from
[and]to(and), in every view, so the sampler view can use the brackets for the range's ends.\still returns to normal speed. A[keys]table insettings.tomlcan bind the brackets back outside the sampler view.
Fixed
-
The time on the terminal's progress bar was hard to read. Over the filled part it was the default text colour on the bar, light on cyan in the dark theme; past it, it was the bar's colour, 3.8:1 on white in the light theme. It is now black over the filled part and the default colour past it, since black over the whole label vanishes past the fill on a dark background. It is not bold: some terminals draw bold black as grey. Library API:
Palette::progress_text. -
On Linux,
make installinstalled the desktop entry, but its icon did not show when another app had left anicon-theme.cacheunder~/.local/share/icons/hicolor. GTK trusts that cache whilehicolor/is no newer than it, and adding a file to256x256/apps/does not changehicolor/.make installnow rebuilds an existing cache. It does not create one, since a cache that nothing refreshes causes the same failure for the next installer. -
On Linux, playr was not offered to open audio files after
make install. GIO reads a desktop entry'sMimeType=only through~/.local/share/applications/mimeinfo.cache.make installnow rebuilds that cache, creating it if needed.
0.6.2
Added
- Light and dark themes, set by
theme = "system","light"or"dark"insettings.toml, by:themeuntil playr exits, and in the window by View, Theme.darkis the default in both, as before.systemfollows the system's appearance in the window. A terminal cannot reliably report its background, so in the terminalsystemanddarkboth use the terminal's own ANSI colours. The terminal's light set uses the 256-colour table over remapped ANSI colours: terminal themes rarely change that table, and ANSI yellow and cyan are unreadable on xterm-style light palettes. Tests hold both light sets and the window's dark set to WCAG contrast: 4.5:1 for text, 3:1 for lines and bars. Library API:Theme,Action::Theme,Presentation::Theme,Message::Theme,Config::theme,Model::theme,Screen::theme,playr::ui::palette,playr_gui::palette.
Fixed
- With
NO_COLORset, the terminal did not highlight the cursor row. crossterm honoursNO_COLORby sending an SGR reset in place of each colour, which also clears bold and reverse video. playr now removes colour itself and reverses the cursor row. Library API:Screen::colour,App::set_colour.
0.6.1
Changed
-
A scan no longer removes tracks whose files are gone. It counts them, and
playr prune DIR, or:prune DIRafter a confirmation, removes them with their places in playlists and the marks of every missing file underDIR. A scan pruned on its own, so a subfolder on an unplugged drive lost its tracks from every playlist at the next scan of its parent. The window has File, Remove missing files. Library API:ScanReport::missingreplacesremoved;db::missing_under;db::prune_missingreturnsPrunedand removes marks;Session::check_prune,pruneandpruned;Event::Pruned;Outcome::PruneStartedandPruned;Task::Prune;Confirm::Prune;Action::Prune. -
Only one of
playrandplayr-guiruns at a time. While either is open, the other refuses to start with "playr is already running", and so doesplayr scan;playr playlists,playr search --jsonandplayr formatsstill run. Each keeps playlists and marks in memory and checks changes against that copy, so two at once could replace each other's playlists without asking, and undo each other's marks. The claim is a per-user lock file,instance.lockbeside the default library, whichever--dbis open; the system releases it when playr exits, however it exits. Library API:playr_app::instance.
Fixed
-
Answering yes to "clear all marks" cleared the marks of the track playing at the answer, not the track asked about. A gapless track change while the question was open deleted the next track's marks. The question now names the track and carries its path. Library API:
Confirm::ClearMarks { path, count };Session::marks_to_clearreturns the path with the count, andSession::clear_markstakes the path. -
Saving a mark or a playlist during a scan could fail with "database is locked". The scan held the write lock while it read tags for up to 500 files, which can take longer than the 5 s busy timeout. Tags are now read before each batch's transaction opens.
-
In the sampler view, a slicing started before another finished could replace the newer one's planned slices, and
enterthen wrote the older cut. Its result also cleared "planning slices" while the newer one still ran. A plan is now shown only if it belongs to the latest slicing. Library API:Sampler::planningis the planning job's id, andFrontend::planningtakes it. -
A panic while scanning, such as one in a tag reader, ended the scan with no report, and every later
:scanwas refused as already running. The scan now ends with its error, and another can start. -
Reindexing an older library on open was not atomic. Interrupted between dropping the old search index and refilling the new one, it left an index that later opens took as current, so searches missed every existing track. The migration now runs in one transaction and is redone on the next open.
-
A seek to or past the end of a track did nothing, with no message, since the decoder refuses it. It now moves on as the track's end would: to the next track in play order, still paused if playback was, or stops after the last.
-
A seek reported the position from before it until the device discarded its buffer, so two quick
:next-markpresses could both choose the same mark. The position is now the seek's target until then. Changing track in that window also left the discard pending, and it then dropped up to 2 s from the start of the next track. -
playr-guideclared Rust 1.89, the workspace's version, but egui 0.36 needs 1.95. It now declares 1.95. CI checks the other crates on 1.89. -
playrstarted the interface with stdout redirected, and drew it into the file or pipe. crossterm opens/dev/ttywhen stdin is not a terminal, so the terminal check passed. playr now exits with an error when stdout is not a terminal. This also stoppedtests/cli.rsfrom hanging when run from a terminal.
0.6.0
Added
-
A desktop window,
playr-gui. It opens with the terminal's options, key bindings and:commands, and draws the library, selection and playlists as tables, with search, the transport, marks, volume, speed, mode and the level meter. Menus and a right-click menu on each row reach every action a key does; selection rows drag to a new place; File opens files and folders or adds a folder to the library, and files dropped on the window play; the command bar completes with Tab and recalls with the arrows. The sampler view paints the waveform in the terminal's three displays, with the region, marks, playhead and planned slice edges; a click seeks, a shift-click marks, the mouse wheel zooms, and buttons write or discard planned slices. Release archives carry it besideplayr: asplayr.appon macOS, unsigned, withplayr.desktopand an icon on Linux, and with its icon embedded and no console window on Windows.make installinstalls it besideplayr, withplayr.appin~/Applicationson macOS and a desktop entry on Linux;cargo install --gitbuilds it from GitHub. The release workflow, run by hand with no tag, builds and packages every platform without publishing.docs/dev/gui.mdrecords the design and what is still open. It is built with egui's own dark style and wraps the sameModelas the terminal, so a click does what the key does. Library API:View::ALLandView::title,Confirm::question,command::key_rowsandcommand_rows,playr_app::meter,model::now_playing,Model::waking,CommandLine::replace, andsampler::Layout,peaks_of,plan_textandedges, which the terminal's sampler view now draws from too. -
:scan DIRadds a directory to the library without leaving playr, and:open PATHplays a file or directory. A scan runs in the background, counts files on the bottom line, and creates the library if playr started without one; the library view shows the new tracks when it finishes.:openadds the tracks to the end of the selection and plays them, asplayr <path>does, so tracks already collected for a playlist are kept. Both exist so a GUI user who never opens a terminal can build a library and play files, and the terminal has them too so the two frontends do the same things.Saving a mark or a playlist during a scan waits while the scan commits its current batch of 500 files, and fails after 5 s. Library API:
Session::scan,scanned,openandset_library_path;Event::ScanProgress,ScannedandOpened;scan::scan_intoandscan::playable;App::set_library_path.
Changed
-
The interface's state is shared, the first step of a GUI frontend (
docs/dev/gui.md).playr_app::model::Modelholds what the terminal'sAppheld apart from drawing: the session, views and cursors, search results, the list playing, prompts and questions with their typed text, the message, the sampler's state and the per-frame snapshot. It implementsFrontend, andAppwraps it, so a GUI built on it does what a key does in the terminal. Nothing a user sees changes.Library API:
ui::notice::textisplayr_app::message::text, withfmt_timeandhome_as_tilde;ui::Snapshot,ui::Input,hold_peakandPEAK_HOLDare inplayr_app::model, anduire-exportsInputandSnapshot;ui::sampler'sSampler,Wave,window,fmt_frames,db_height,DB_FLOORandMIN_FRAMES_PER_COLUMNare inplayr_app::sampler, andui::samplerkeeps the glyph code;Appno longer implementsFrontend. -
The terminal's sampler view starts in the Braille display, which shows a waveform's shape most clearly in text;
wthen steps to the envelope and dB displays. The desktop window still starts on the envelope. Library API:Model::set_display.
Fixed
-
On Windows, searching matched the folders a track sits in. The search index takes a file's name from its path, and read only
/as a separator, so a Windows path went in whole and a search formusicfound every track underC:\Users\...\Music. A Windows path, one starting with a drive or\\, now has its backslashes read as separators, and a library indexed by 0.5.0 or 0.5.1 is reindexed when opened. A backslash in a Unix file name stays part of the name. -
:move +Nand:move -Nmove the track N places, as documented. They swapped it with the track N places away, so:move +2on A, B, C gave C, B, A rather than B, C, A.JandKmove by one place, where the two agree.
0.5.1
Added
- Release binaries. Pushing a version tag such as
0.6.0runs.github/workflows/release.yml: it checks that the tag matchesCargo.tomland thatCHANGELOG.mdhas a section for it, runsmake teston Linux, builds with Opus for Linux (x86_64, arm64), macOS (arm64, x86_64) and Windows (x86_64), and publishes the archives,SHA256SUMSand that changelog section as the GitHub release. The workflow can also be run by hand for a tag pushed earlier.
Fixed
- playr started only when
HOMEwas set. The defaultsamplesdirectory expands~, and withoutHOMEthe defaults failed to parse, which panicked at startup. Windows does not setHOME, so every Windows build would have stopped there. The home directory now comes from the platform,USERPROFILEon Windows.