Skip to content

Releases: shakfu/playr

0.9.1

Choose a tag to compare

@github-actions github-actions released this 21 Sep 18:04

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.rs reproduces 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_track failed 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 in docs/dev/device.md. PipeWire holds only the device it plays to, so another card's hw: device plays alongside it. The cpal lookup bug playr works around is drafted as an upstream report in docs/dev/issues/cpal-issue.md.

0.9.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 15:20

Added

  • Output device selection. --device ID on all three programs, or device in settings.toml, plays to a device other than the default; playr devices lists the IDs, which are cpal's <host>:<id>, and a bare hw:2,0 means 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 in docs/dev/device.md.

    cpal 0.18.2 cannot find some IDs it lists, such as alsa:sysdefault:CARD=X: its ALSA lookup appends ,DEV=0 to 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::new takes the device ID; output::default_device is gone.

  • A spectrogram display in the sampler view, :display spectrogram, between dB and Braille in the w cycle; 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%. :display in 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

Choose a tag to compare

@github-actions github-actions released this 18 Sep 14:11

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_done at once, but frames_out, track_start and position_offset still 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: :slice planned the region around it, b marked 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 :scan or playr scan, so new files are picked up without retyping paths. Each scan records its directory in a roots table, and a bare playr scan, a bare playr prune, :prune with 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 with auto_prune = true in settings.toml. It does not ask over a prompt being typed, and auto_prune steps 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::Prune and Confirm::Prune take Option<PathBuf>, None meaning every recorded root.

  • :roots lists the directories the library covers, :roots rm DIR forgets one, and :roots add DIR is another spelling of :scan DIR. playr roots, playr roots rm DIR and playr roots add DIR do 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 :prune it 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, through souvlaki. Linux uses its use_zbus feature, so nothing links libdbus. Both frontends call Model::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 resume table 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. :cursor moves it and h returns it to the playhead; :pick next|prev puts it on a mark, which is how one is picked up. :nudge-mark moves that mark a column, :move-mark TIME puts it at a time, :del-mark removes it wherever it sits in the chain B undoes, and :snap-mark moves 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, a in 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 :loop does. Playing on continues the track from there. Cmd::PlayOnce sets the same loop with once, and the engine leaves the decoder at the end rather than seeking back; Status::looping leaves a one-shot out, so :loop cannot switch off a range that was only auditioned.

Changed

  • playr-server drops --music DIR. The page's Rescan library re-scans the directories playr scan recorded, 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 --music must drop it; nothing replaces it.

0.8.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 15:18

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 DIR the 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-reply sends the state back; playr-server osc-schema lists the addresses, and make touchosc builds a TouchOSC layout from them. See docs/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 the Host must name the machine, so another web site open in the browser cannot reach it. It listens on 127.0.0.1 unless --listen says otherwise, and shares the lock of playr and playr-gui. playr and playr-gui still have no network code. Release archives include playr-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-server and links its guide, docs/server-guide.md, and the "no network code" claim now names playr and playr-gui only. docs/architecture.md and its crate diagram add the server, whose owner thread is half of the daemon the notes left open. docs/sampler.md records that OSC for marks would go in playr-server, the one program with network code. TODO.md lists the server's gaps, among them the Pi and TouchOSC checks, CI browser tests and a [server] settings table.

Fixed

  • :speed could not set a negative speed. A signed number is relative, so :speed -3 lowered the speed by three semitones from where it was, yet the error for :speed 13 gave 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

Choose a tag to compare

@github-actions github-actions released this 15 Sep 19:05

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, on S, 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 :step setting, 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, replacing MIN_FRAMES_PER_COLUMN; DETAIL_MARGIN; window and Layout::new take 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 marks cuts 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_slices and export take a range.

  • Looping the range in the sampler view. l or :loop plays 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. esc now 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 in settings.toml can 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 install installed the desktop entry, but its icon did not show when another app had left an icon-theme.cache under ~/.local/share/icons/hicolor. GTK trusts that cache while hicolor/ is no newer than it, and adding a file to 256x256/apps/ does not change hicolor/. make install now 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's MimeType= only through ~/.local/share/applications/mimeinfo.cache. make install now rebuilds that cache, creating it if needed.

0.6.2

Choose a tag to compare

@github-actions github-actions released this 15 Sep 11:26

Added

  • Light and dark themes, set by theme = "system", "light" or "dark" in settings.toml, by :theme until playr exits, and in the window by View, Theme. dark is the default in both, as before. system follows the system's appearance in the window. A terminal cannot reliably report its background, so in the terminal system and dark both 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_COLOR set, the terminal did not highlight the cursor row. crossterm honours NO_COLOR by 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

Choose a tag to compare

@github-actions github-actions released this 15 Sep 06:31

Changed

  • A scan no longer removes tracks whose files are gone. It counts them, and playr prune DIR, or :prune DIR after a confirmation, removes them with their places in playlists and the marks of every missing file under DIR. 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::missing replaces removed; db::missing_under; db::prune_missing returns Pruned and removes marks; Session::check_prune, prune and pruned; Event::Pruned; Outcome::PruneStarted and Pruned; Task::Prune; Confirm::Prune; Action::Prune.

  • Only one of playr and playr-gui runs at a time. While either is open, the other refuses to start with "playr is already running", and so does playr scan; playr playlists, playr search --json and playr formats still 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.lock beside the default library, whichever --db is 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_clear returns the path with the count, and Session::clear_marks takes 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 enter then 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::planning is the planning job's id, and Frontend::planning takes it.

  • A panic while scanning, such as one in a tag reader, ended the scan with no report, and every later :scan was 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-mark presses 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-gui declared 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.

  • playr started the interface with stdout redirected, and drew it into the file or pipe. crossterm opens /dev/tty when stdin is not a terminal, so the terminal check passed. playr now exits with an error when stdout is not a terminal. This also stopped tests/cli.rs from hanging when run from a terminal.

0.6.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 23:32

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 beside playr: as playr.app on macOS, unsigned, with playr.desktop and an icon on Linux, and with its icon embedded and no console window on Windows. make install installs it beside playr, with playr.app in ~/Applications on macOS and a desktop entry on Linux; cargo install --git builds it from GitHub. The release workflow, run by hand with no tag, builds and packages every platform without publishing. docs/dev/gui.md records the design and what is still open. It is built with egui's own dark style and wraps the same Model as the terminal, so a click does what the key does. Library API: View::ALL and View::title, Confirm::question, command::key_rows and command_rows, playr_app::meter, model::now_playing, Model::waking, CommandLine::replace, and sampler::Layout, peaks_of, plan_text and edges, which the terminal's sampler view now draws from too.

  • :scan DIR adds a directory to the library without leaving playr, and :open PATH plays 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. :open adds the tracks to the end of the selection and plays them, as playr <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, open and set_library_path; Event::ScanProgress, Scanned and Opened; scan::scan_into and scan::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::Model holds what the terminal's App held 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 implements Frontend, and App wraps it, so a GUI built on it does what a key does in the terminal. Nothing a user sees changes.

    Library API: ui::notice::text is playr_app::message::text, with fmt_time and home_as_tilde; ui::Snapshot, ui::Input, hold_peak and PEAK_HOLD are in playr_app::model, and ui re-exports Input and Snapshot; ui::sampler's Sampler, Wave, window, fmt_frames, db_height, DB_FLOOR and MIN_FRAMES_PER_COLUMN are in playr_app::sampler, and ui::sampler keeps the glyph code; App no longer implements Frontend.

  • The terminal's sampler view starts in the Braille display, which shows a waveform's shape most clearly in text; w then 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 for music found every track under C:\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 +N and :move -N move the track N places, as documented. They swapped it with the track N places away, so :move +2 on A, B, C gave C, B, A rather than B, C, A. J and K move by one place, where the two agree.

0.5.1

Choose a tag to compare

@github-actions github-actions released this 14 Sep 16:04

Added

  • Release binaries. Pushing a version tag such as 0.6.0 runs .github/workflows/release.yml: it checks that the tag matches Cargo.toml and that CHANGELOG.md has a section for it, runs make test on Linux, builds with Opus for Linux (x86_64, arm64), macOS (arm64, x86_64) and Windows (x86_64), and publishes the archives, SHA256SUMS and 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 HOME was set. The default samples directory expands ~, and without HOME the defaults failed to parse, which panicked at startup. Windows does not set HOME, so every Windows build would have stopped there. The home directory now comes from the platform, USERPROFILE on Windows.