All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
- Plex Media Server browsing via
plex://scheme — mDNS discovery (_plexmediasvr._tcp.local.), token-in-URL auth, full library / playlist / album / artist / track navigation - Jellyfin Media Server browsing via
jellyfin://scheme — manual server entry, API-key auth, full content hierarchy browsing - Navidrome Media Server browsing via
navidrome://scheme — manual server entry, MD5 token auth (Subsonic API),getIndexes+getMusicDirectorybrowsing - Kodi/XBMC Media Server browsing via
kodi://scheme — JSON-RPC API, library browsing for audio albums, artists, genres, and tracks - Expo mobile app: Plex, Jellyfin, Navidrome, and Kodi server browsing surfaced in the Files tab alongside the existing local filesystem view
- WASM browser build: settings API (persist EQ / DSP / volume / crossfade to in-memory config), playlist persistence across reloads,
rb_set_repeatexport (repeat off / all / one / shuffle) - Real-time DSP/EQ API exposed over HTTP, gRPC, and GraphQL —
setEqmutation withenabled,precut, and per-bandcutoff/Q/gainfields; backed bydsp_set_eq_coefs()called directly on the audio thread to avoid audible cuts
- Docker base images upgraded from Debian bookworm → trixie across all three Dockerfiles; Rust base image bumped from 1.94 → 1.95
- Nix flake now builds only
rockboxd(removed unused outputs) settings.tomlexample updated to document the new media-serveraudio_outputentries
- WASM:
seek, crossfade, bass/treble DSP controls now apply correctly; real-time events (position, track change) fire reliably; crossfade mode change postsQ_AUDIO_REMAKE_AUDIO_BUFFERonly when audio is playing to avoid an audible cut when stopped - WASM: EQ real-time application and persistence — coefficient updates call
dsp_set_eq_coefs()in thewasm_cmdhandler without postingREMAKE; band gain multiplied by 10 (tenths of dB) before passing torb_set_eq_band - WASM: EQ cutoff and Q values now match the preset layout (Q 7.0, 10-band display) after correcting the unit conversion in
web/rockbox.js - Dithering, Auditory Frequency Resolution (AFR), and Perceptual Bass Enhancement (PBE) controls in the web UI now reflect changes immediately —
GlobalSettingsmutations now call the corresponding DSP setters and triggertracing-level log output
- DSP compressor divide-by-zero crash on x86_64 (
SIGFPEinget_att_rls_coeff) — addedrelease > 0guard incompressor_update()mirroring the existingattack > 0guard; ARM64 silently returned 0 on integer divide-by-zero while x86_64 faulted; also added function-level guards inget_att_rls_coeffandget_lpf_coefffor zerorc/fs/rc_unitsparameters, and an earlyfs <= 0return incompressor_updatefor uninitialised output frequency - Startup hang on second+ launch — FTS5 backfill
WHERE NOT EXISTS (SELECT 1 FROM fts_table f WHERE f.id = t.id)forced an O(N) full scan per row (O(N²) total) becauseidisUNINDEXEDin FTS5; replaced all four backfill INSERTs with an uncorrelatedWHERE NOT EXISTS (SELECT 1 FROM fts_table)which SQLite short-circuits at the first row (O(1) for non-empty tables) - Library startup blocked indefinitely on repeated runs — SQLx hangs when re-executing
CREATE VIRTUAL TABLE IF NOT EXISTSon an existing FTS5 virtual table; fixed by checkingsqlite_masterbefore the migration and skipping it entirely iftrack_ftsalready exists; same guard added fordedupe_genres(checksUNIQUEconstraint ongenretable) - FTS5 and
dedupe_genresmigrations ran in slow DELETE journal mode —PRAGMA journal_mode=WALwas set only after all migrations; moved toSqliteConnectOptions::journal_mode(Wal)so WAL is active from the first connection - FTS5 index migration moved to a background
tokio::spawntask so startup is non-blocking;dedupe_genres(schema DDL) remains synchronous with an O(1) skip guard - cpal PCM sink: audible silence gap at the start of every track on Linux —
sink_dma_start()previously stored the first chunk inpcm_data/pcm_sizeand then calledpthread_create, leaving the ring empty for the 1–5 ms thread-creation window; fixed by pushing the first chunk synchronously viapcm_cpal_push()before spawning the writer thread so the ring is pre-filled whenrunning=trueis set; the writer thread now picks up from chunk 2 onwards; also added!r.runningearly-exit to the f32 cpal callback (mirrors the existing i16 guard) and reset resampler state (cur_valid = false,phase = 0) inpcm_cpal_start()to prevent interpolation artefacts from the tail of the previous track
- Headless host target and
cpalPCM sink (audio_output = "cpal") — runs Rockbox without SDL on any OS audio backend (ALSA, CoreAudio, WASAPI, JACK) via CPAL; build withscripts/build-headless.sh; documented inHEADLESS.md - Genres API — gRPC, GraphQL, REST, and CLI endpoints to list genres, fetch tracks by genre, and add genre-based smart playlist rules; genre deduplication SQL migration bundled
- Disc/track number support in the Expo mobile album view —
TrackListcomponent sorts by (disc, track) and renders disc-section headers for multi-disc releases;proto track_number/disc_numberfields mapped through to the UI - Pull-to-refresh / rescan in the Expo library tab
- CI workflows, macOS build scripts, Dockerfile, and
install.shstreamlined — significant reduction in duplication and overall build time - Android
cdyliboption now available in thetools/configureinteractive menu
- M4A/AAC files decode silently in
CODECS_STATICbuilds — dead-write elimination inlibm4a/demux.cwas optimizing away box-parsing reads; replaced with live-return readers (stream_read_uint*+stream_skip) - macOS linker:
Security.frameworkexplicitly linked inzig/build.zigto resolve missing symbol errors when using macOS Security APIs - Expo mobile app re-establishes gRPC subscriptions when the app returns to the foreground (
reconnectEpochbump +reapplyServerUrl())
- Mintlify documentation site under
mintlify/with the Linden theme; OpenAPI spec regenerated and ASCII architecture diagrams replaced withCardGroupcomponents - Linux-specific window controls (minimize / maximize / close) in the GPUI titlebar — macOS/Windows continue to use native traffic-light controls
- GPUI titlebar drag areas now call
window.start_window_move()from anon_mouse_downhandler instead of relying onWindowControlArea::Drag, fixing window dragging on Linux/X11 - Debian and RPM packages now declare XKB/XCB build dependencies (
libxkbcommon-dev,libxkbcommon-x11-dev,libxcb1-dev,libxcb-render0-dev,libxcb-shape0-dev,libxcb-xfixes0-dev); README updated with the matching install instructions - Debian package version bumped to
2026.05.03
- GPUI app no longer fails to build on Linux:
souvlakiis now a non-Linux-only dependency andNowPlayingManagerships a no-op Linux stub, since the OS media-control APIs souvlaki targets are not available there
- New SDKs for controlling rockboxd from Python, Ruby, Elixir, Gleam, and Clojure (
sdk/python/,sdk/ruby/,sdk/elixir/,sdk/gleam/,sdk/clojure/) — each ships with examples covering playback, queue, library search, saved/smart playlists, volume/EQ, browse, devices, Bluetooth, and plugins - TypeScript SDK gains 15 runnable examples (
sdk/typescript/examples/) plus a Bluetooth API (api/bluetooth.ts) and agetVolume/VolumeInfoendpoint onapi/sound.ts - TS SDK types extended with
browse.displayNameandalbum.copyrightMessage
- HTTP/remote tracks now hydrate
Mp3Entrymetadata (title, artist, album, duration, etc.) from the DBTrackrecord in the playlist handlers when Rockbox cannot read tags locally - GPUI Library page: text truncation and unexpected overflow on likes and track rows resolved by adding
min_w_0/flex_shrink_0to the flex containers - Regenerated tonic/prost UPnP bindings under
crates/upnp/src/api/
- Bluetooth button in the GPUI mini-player — shown when Bluetooth is available; opens the device picker and fetches paired devices on toggle
- Cover URLs in GPUI now follow the active server via
get_covers_base()instead of the hardcodedhttp://localhost:6062/covers/base
- HTTP server (
crates/server) migrated from a custom request/response layer to Actix-web — handlers now acceptweb::Data,web::Path, andweb::Queryand returnactix_web::Result<HttpResponse>; blocking C FFI work is offloaded toweb::block - Tokio runtimes for the controls and MPD servers are now shared via
OnceLockinstead of being created per-thread, reducing overhead and avoiding nested-runtime panics RLIMIT_NOFILEis raised to 4 096 at startup on Unix to accommodate large music libraries
- Audio
stopandpauseare now non-blocking — they useaudio_queue_postso they can safely be called from any OS thread;audio_hard_stoppostsQ_AUDIO_STOPwithdata=2and the audio thread freesaudiobuf_handleitself, preventing cross-thread frees - Blocking C FFI calls in playlist handlers run on
web::blockthreads to avoid starving Actix worker threads and prevent nested tokio/reqwest blocking contexts - Live metadata lookups are skipped for HTTP tracks; Rockbox's own UPnP renderers are excluded from the UPnP device list
- Bluetooth availability check uses
fetchGlobalStatus()(gRPCGetGlobalStatus) instead ofgetDevices()to avoid spuriousUNIMPLEMENTEDerrors on probe - Bluetooth availability is now polled in a background task and updated via
std::sync::mpscto avoid cross-runtime waker issues when bridging Tokio → GPUI observe_globalregistrations in GPUI now call.detach()instead of silently dropping the subscription handle- RFC3339 datetime migration — a SQL migration normalises
NULL/blank andYYYY-MM-DD HH:MM:SStimestamps in the library database to RFC3339 so SQLxDateTime<Utc>decoding no longer fails - Favourites queries now use
INNER JOINand filter out empty-string IDs, excluding bogus entries from results - mDNS scanning now prefers IPv4 addresses (192.168 → 10 → others) and selects the best non-loopback/link-local address so multiple records for the same host coalesce correctly
println!/eprintln!diagnostics incrates/controlsandcrates/mpdreplaced withtracing::error!- macOS app listens for server-change notifications and restarts streaming, re-fetches settings, device state, and Bluetooth state on server switch
- mDNS device ID is now persisted across restarts — a 64-bit hex ID is generated once and cached in
~/.config/rockbox.org/device-id, so the registered mDNS service name remains stable between daemon restarts instead of changing on every launch
- Bluetooth device support in the GPUI and web UIs — list paired/discovered devices, connect and disconnect directly from the device picker
- mDNS-based server discovery and runtime server switching —
scan_mdns()in the daemon registers itself via mDNS; the GPUI app and macOS app gain a Server Picker UI that enumerates nearbyrockboxdinstances and switches without restart; a notification triggers one-shot syncs to re-run on server change - UPnP album art saved for remote tracks —
album_art_uriis returned from UPnP directory listings;save_audio_metadatadownloads and caches the cover when no embedded art is present; remote metadata is persisted concurrently (semaphore-limited) without blocking C/FFI copyright_messagefield on theAlbumGraphQL type, displayed inAlbumDetailsalongside a formatted release date- Typesense bundled in the Docker image — the Dockerfile now pulls the typesense image and copies
typesense-serverinto the final image
- Bluetooth speaker management commands in the
rockboxCLI (bluetooth scan,bluetooth devices,bluetooth connect <address>,bluetooth disconnect <address>) — Linux only, talks to a runningrockboxdvia gRPC - Bluetooth GraphQL resolvers (
bluetoothDevicesquery,bluetoothScan/bluetoothConnect/bluetoothDisconnectmutations) now callrockbox-bluetoothdirectly instead of going through the HTTP server — eliminates an extra round-trip on Linux
BluetoothServicegRPC RPC renamed fromConnecttoConnectDeviceto avoid a name collision with tonic's auto-generated transportconnectconstructor, which caused a compile error (duplicate definitions with name connect)
- macOS app Files view: navigating from the root into Music no longer yields an empty list —
.taskID now encodes both mode and path so a mode change with a nil path correctly triggers a reload - macOS app device picker: now lists all output devices (including the current one, marked with a checkmark) instead of only non-current devices; added
snapcasticon/colour entry - macOS app device picker: no longer shows a loading spinner on open when devices were already preloaded at startup —
refresh()only setsisLoadingwhen the device list is empty
- UPnP device browsing in the Files view — queue and play tracks directly from any UPnP/DLNA media server on the local network
- HTTP stream (
netstream) no longer permanently breaks after a failed seek:seek_to()now only replaces the active response on success, so a failed Range request leaves the stream readable at the current position - Small forward seeks (≤ 128 KB) in HTTP streams are now satisfied by skipping bytes in the existing response body instead of issuing a new Range request, avoiding unnecessary round-trips during codec metadata parsing
- Buffering:
TYPE_ID3handles for remote tracks that fail to open now sendBUFFER_EVENT_FINISHEDwith an emptymp3entryinstead of silently never postingQ_AUDIO_FINISH_LOAD_TRACK, which caused the track-loading chain to stall on playlist restore with many queued UPnP tracks - Web UI Files view: Music and UPnP Devices row icons no longer disappear on hover — CSS selector changed from descendant (
) to direct-child (>) combinator so the.no-playguard is respected
- Real-time PCM loudness normalizer (
normalize_volume = trueinsettings.toml) — RMS-based AGC with asymmetric attack/release, similar to Spotify's "Normalize Volume"; applied across all PCM sinks (SDL, FIFO, AirPlay, Squeezelite, UPnP, Chromecast, Snapcast TCP) GET /player/volumeREST endpoint returning{ volume, min, max }volumeGraphQL query returning live current volume with min/max rangeuseGetVolumeQueryGraphQL hook in the web UIget_current_volume()gRPC client helper in the GPUI app
- Volume slider in GPUI mini-player now responds to mouse clicks (replaced plain
divwithSeekBarcomponent) - Volume slider in web UI now uses correct 0–100 range with explicit
min/maxon the MUI Slider globalSettings.volumein GraphQL now returns the live current volume viarb::sound::current(0)instead of a hardcoded0VOLUME_MIN_DBconstant in GPUI corrected from-74to-80(SDL target range)- Volume in GPUI loads the live value at startup via
SoundCurrentgRPC instead of the stale saved setting adjust_volumenow has audible effect on all non-SDL PCM sinks (FIFO, AirPlay, Squeezelite, UPnP, Chromecast, Snapcast TCP) — SW volume scaling (pcm_copy_buffer) was not being applied in any of these sinks
- Snapcast TCP PCM sink (
audio_output = "snapcast_tcp") — streams S16LE PCM directly to a Snapcasttcp://source; compatible with snapserver v0.35+ - Stream metadata forwarding for Snapcast TCP sink
- MPD
getvol/setvolhandlers now correctly map the Rockbox dB range (−80..0) to the MPD 0–100 scale
- TypeScript SDK (
@rockbox/sdk) for controlling rockboxd from Node.js / browser applications - Playlists UI in the web interface — create, edit, and manage saved and smart playlists
- Album art footer overlay shown on album cover hover
- Web UI data layer migrated from Apollo Client to TanStack React Query
- Playlist modals rendered into document body via React portal (fixes z-index stacking issues)
- Chromecast PCM sink (
audio_output = "chromecast") — streams WAV over HTTP and controls playback via the Cast Media protocol - UPnP/DLNA support: ContentDirectory media server (
upnp_server_enabled), MediaRenderer:1 (upnp_renderer_enabled), and UPnP PCM sink (audio_output = "upnp") with auto-renderer discovery - Device picker UI in the GPUI and web mini-player — switch audio output (Rockbox built-in, AirPlay, Squeezelite, Chromecast) without restarting
- Multi-room AirPlay:
airplay_receiverslist insettings.tomlsupports sending to multiple RAOP receivers simultaneously - Squeezelite multi-room PCM sink (
audio_output = "squeezelite") — Slim Protocol TCP server + HTTP PCM broadcast; supports unlimited concurrent squeezelite clients with independent reader cursors
- Duplicate Chromecast devices skipped during discovery
- Typesense search index initialised before the HTTP server accepts requests (avoids empty results on cold start)
- Saved playlists: create, rename, delete, and reorder tracks via gRPC, GraphQL, and REST APIs
- Smart playlists: rule-based auto-generated playlists with play-count and skip-count tracking
- Playlist search integration with Typesense
StreamLibrarygRPC server-streaming RPC — pushes library updates to clients when a scan completes- GPUI file browser: navigate the local filesystem and enqueue directories directly
- Now Playing widget in GPUI shows correctly when the app opens with a paused track (initial status fetched once at startup)
- Rocksky registration failures logged at
debuglevel instead ofwarnto reduce noise - Global Play/Pause keybind no longer fires when a text input field has focus