#aA+ Qobee
A local audio player for Windows, focused on fidelity. Indexes one or more music folders, plays FLAC and other lossless formats through the OS mixer, and is built so that a future bit-perfect output path can drop in without rewriting the rest of the app.
The current build delivers a complete listening experience: scanning, albums / artists / genres / playlists / favorites, search, queue management, 10-band equalizer, ReplayGain, gapless playback, sample-accurate seek, lyrics (synced + plain), a mini player, themes, accent following the cover, OS media keys, and Discord Rich Presence. The audio engine ships with two output backends — Shared (default, OS mixer) and WASAPI Exclusive (opt-in, strictly bit-perfect when the device cooperates). See "Bit-perfect honesty" below for the truth about each chain.
Qobee will tell you the truth about its current playback chain rather than ship a misleading badge.
Bit-perfect: false on
this backend even when the in-app DSP is at unity.is_bit_perfect back to false. While Exclusive is active
no other application can play to the same device.crates/engine — Symphonia (decoding), CPAL (Shared output),
WASAPI Exclusive backend, biquad EQ, and a fidelity-first DSP
chain shared by both backends (see "Audio chain" below).crates/library — walkdir + lofty for tag extraction,
rusqlite (bundled) for storage, embedded covers cached on
disk, FTS-style search, settings table.crates/core — playback queue (cursor + history),
repeat / shuffle / endless logic, orchestration between engine
and library.src-tauri/, including a Discord
Rich Presence worker and a cover-hosting worker (background
uploads to a public host so Discord can render local album art).ui/.The engine runs a single, deterministic DSP pipeline on top of both
the Shared and Exclusive backends. Every stage is bypassable and
honest about what it does — the truthful is_bit_perfect badge
reflects the actual chain, not a marketing flag.
REPLAYGAIN_TRACK_PEAK /
_ALBUM_PEAK tags) so the output never exceeds the chosen
ceiling. Missing peak tags fall back to a conservative 1.0,
and the attenuation actually applied is reported in the player
state.tpdf (legacy white triangular PDF),
shaped_hp (HF-emphasis shaping), and shaped_f_weighted (the
default — F-weighted noise shaping that pushes the noise floor
away from the ear's peak sensitivity). The stage is in exact
bypass when the output is wider than 16 bits.off (passthrough), soft_clip
(smooth tanh-style clipper, legacy behaviour), and
lookahead_limiter (the default — true-peak look-ahead limiter
with configurable ceiling, look-ahead and release).logarithmic curve, configurable floor) so a small movement at
low levels behaves like the volume control on a real amplifier.
The legacy quadratic curve remains available.bauer, bauer_strong, custom) and tunable
inter-aural delay and low-pass cutoff. Off by default.BitPerfect / Modified / Inconclusive verdict.Pinned versions:
| Component | Version |
|---|---|
tauri |
2.11 |
tauri-plugin-dialog |
2.2 |
symphonia |
0.5.5 |
cpal |
0.17 |
wasapi (Windows-only) |
0.23 |
windows (Windows-only) |
0.62 |
lofty |
0.24 |
rusqlite (bundled) |
0.39 |
notify |
8.2 |
crossbeam-channel |
0.5 |
rtrb |
0.3 |
rubato |
0.16 |
blake3 |
1.5 |
discord-rich-presence |
1.1 |
reqwest (rustls, blocking) |
0.12 |
svelte |
5.20 |
vite |
6.1 |
typescript |
5.7 |
@tauri-apps/api |
2.4 |
Vec<f32>. The engine exposes
F32Interleaved, I16Interleaved, I24In32Interleaved and
I32Interleaved. Only F32Interleaved is produced today, but the
integer variants are part of the public API so a future
bit-perfect path can plug in without breaking call sites.rtrb. The
current ring carries f32; an integer ring will be added
alongside the Exclusive backend.%APPDATA%\Qobee\covers\<aa>\<hash>.<ext>) and exposed to the
webview via the custom qobee-cover:// URI scheme. The frontend
loads them with plain <img> tags.is_bit_perfect: false.Qobee/
├── Cargo.toml # workspace
├── crates/
│ ├── engine/ # AudioEngine + backends + EQ
│ ├── library/ # scan + DB + cover cache + settings
│ └── core/ # queue + player FSM + history
├── src-tauri/ # Tauri shell
│ ├── tauri.conf.json
│ ├── capabilities/default.json
│ ├── icons/
│ └── src/
│ ├── main.rs
│ ├── lib.rs # builder + qobee-cover:// protocol
│ ├── commands.rs # Tauri command surface
│ ├── state.rs # AppState (library + player + discord)
│ ├── discord.rs # Discord Rich Presence worker
│ └── cover_host.rs # async cover uploader for Discord
└── ui/ # Svelte 5 + TS strict
├── package.json
├── vite.config.ts
├── svelte.config.js
├── tsconfig.json
├── index.html
└── src/
├── main.ts
├── App.svelte
├── components/ # Sidebar, AlbumGrid, PlayerBar, …
├── lib/
│ ├── api.ts # typed wrappers around invoke()
│ ├── stores.svelte.ts # reactive app state
│ ├── settings.svelte.ts # persisted preferences
│ ├── accent.svelte.ts # cover-driven accent color
│ ├── discordPresence.ts # Discord singleton
│ ├── mediaSession.ts # OS media keys
│ └── … # context menu, palette, format, etc.
└── styles/global.css
All commands live in src-tauri/src/commands.rs and are mirrored 1:1
in ui/src/lib/api.ts (typed wrappers).
Library / browsing:
| Command | Description |
|---|---|
scan_library |
Index a folder, emit progress events |
scan_all_roots |
Rescan every saved library root in sequence |
list_albums / list_artists |
Album grid / artist list data |
get_album |
One album with all its tracks |
get_track |
Resolve a single track by ID |
get_tracks |
Resolve many ids → full Track records |
get_artist_detail |
Artist page (singles, EPs, albums, kind, length) |
list_genres / list_albums_by_genre |
Genre browsing |
search |
Title / artist / album search |
recently_played_tracks / ..._albums / ..._artists |
Home page data |
library_stats |
Counts + DB / cover cache size |
list_library_roots / add_library_root / remove_library_root |
Multi-root config |
track_ids_for_album |
Track ids in album order |
track_ids_by_artist |
Track ids of an artist's full catalog |
album_id_for_track |
Reverse lookup for the player bar |
album_size_bytes |
On-disk size of an album |
Playback:
| Command | Description |
|---|---|
play_track |
Load and play a single track |
play_album_from_track |
Start an album at a given track |
play_playlist_from_track |
Start a playlist at a given track |
play_tracks |
Play an arbitrary id list, optional start index |
play_random_album |
Pick and play a random album |
pause / resume |
Engine pause / resume |
seek |
Seek to position_seconds (Coarse seek) |
set_volume |
Software volume (Shared mode) |
next / prev |
Queue navigation |
get_player_state |
Snapshot of PlayerState |
get_output_mode / set_output_mode |
Auto / Shared (Exclusive reserved) |
list_output_devices / get_selected_output_device / set_output_device |
Output device picker |
get_repeat_mode / set_repeat_mode |
Off / track / queue |
get_shuffle / set_shuffle |
Shuffle toggle |
get_endless / set_endless |
Endless playback |
Queue:
| Command | Description |
|---|---|
get_queue |
Current items + cursor |
play_next / add_to_queue |
Insert after current / append |
queue_remove_at / queue_move / queue_jump_to |
Mutation |
Playlists / favorites:
| Command | Description |
|---|---|
list_playlists / get_playlist |
Playlist index / detail |
create_playlist / rename_playlist / delete_playlist |
Lifecycle |
add_to_playlist / remove_from_playlist |
Track membership |
add_favorite / remove_favorite / is_favorite |
Per-track favorite |
list_favorite_track_ids / list_favorites |
Favorites screen data |
Equalizer / settings / maintenance / Discord:
| Command | Description |
|---|---|
get_eq_gains / set_eq_gains |
10-band peaking EQ (±12 dB) |
get_setting / set_setting / list_settings / clear_settings |
Key/value preferences |
clear_history / clear_cover_cache / reset_library |
Maintenance |
discord_init |
Boot the Rich Presence worker |
discord_set_client_id |
Configure the Discord Application ID |
discord_status |
Connection state for the UI badge |
discord_update_track / discord_set_paused / discord_clear_presence |
Push state |
Events emitted to the UI:
| Topic | Payload |
|---|---|
player:state |
PlayerEvent::StateChanged { state } |
player:position |
PlayerEvent::Position { position_seconds } |
player:end-of-track |
PlayerEvent::EndOfTrack |
player:error |
PlayerEvent::Error { message } |
library:scan-progress |
ScanProgress { files_visited, files_indexed, current } |
library:scan-finished |
ScanResult { files_visited, files_indexed, errors } |
Prerequisites:
rust-toolchain.toml (currently 1.96.0); rustup installs and
selects it automatically inside the workspace, so your local
rustfmt / clippy match CI. CI pins the same version, so the
"passes locally, fails in CI" drift can't happen.One-time setup — enable the git pre-push hook so cargo fmt --check
and cargo clippy -D warnings run before every push (the two gates
most prone to toolchain drift):
./scripts/setup-hooks.ps1 # macOS/Linux: ./scripts/setup-hooks.sh
Bypass a single push with git push --no-verify if you really need
to.
Steps:
# 1. install JS dependencies (only needed once, and after package.json changes)
cd ui
npm install
cd ..
# 2. start the dev app — Tauri spins up vite for the frontend automatically
cargo run -p qobee-app
If you prefer the official Tauri CLI workflow, run it from the
workspace root (Tauri 2's CLI looks for tauri.conf.json in
subfolders only, so it must see both ui/ and src-tauri/ as
siblings beneath its cwd):
cd ui
npm install
cd ..
.\ui\node_modules\.bin\tauri.cmd dev
(The tauri.conf.json points frontendDist at ../ui/dist and devUrl
at http://localhost:1420, which is what npm run dev serves.)
cd ui
npm install
npm run build
cd ..
cargo build -p qobee-app --release
The bundled installer / .exe lands under target/release/.
For a fully bundled installer (MSI, NSIS, etc.) run the Tauri CLI
from the workspace root (not from ui/) so it can locate the
tauri.conf.json in src-tauri/:
cd ui
npm install
cd ..
.\ui\node_modules\.bin\tauri.cmd build
The MSI / NSIS installers land under src-tauri/target/release/bundle/.
Play in Qobee, Add to Qobee queue, Play next, Import to library),
folder context menu (Play folder, Add folder to queue, Scan folder, Import folder), qobee:// protocol handler. Every
registry write goes to HKCU only; Qobee never claims the system
default and never modifies machine-wide associations.qobee.exe --play foo.flac while the app is already running sends the
command to the existing window instead of spawning a duplicate.
Same path for qobee:// deep links.lofty.%APPDATA%\Qobee\library.sqlite3.
Covers are cached at %APPDATA%\Qobee\covers\ and served via
qobee-cover://. Logs land in %APPDATA%\Qobee\logs\ with
daily rotation.BitPerfect / Modified / Inconclusive verdict.F) to open an immersive page with large artwork,
scrubbable progress, transport controls, and a heart toggle.Space (play/pause), ←/→ (-5s / +5s), n / p /
Alt+← / Alt+→ (prev / next track), F (toggle Now Playing),
Esc (close Now Playing or clear search), Ctrl/Cmd+F
(focus search).tauri-plugin-window-state.Every audio preference is persisted as a JSON value in the SQLite
settings table under an audio.* key. Booleans, numbers and
arrays are stored as JSON; enums are stored as snake_case
strings. Out-of-range values read at boot are clamped and
rewritten; missing keys are created with their default. The full
list is below; bounds and defaults are kept in sync with
crates/engine/src/audio_settings.rs and the design document.
| Key | Type | Default | Bounds | Description |
|---|---|---|---|---|
audio.rg_peak_protection |
bool | true |
— | Cap pre-gain so RG-boosted peaks never exceed the limiter ceiling. |
audio.rg_safety_headroom_db |
f32 | 1.0 |
[0.0, 3.0] |
Extra headroom subtracted from the ceiling when peak protection is on. |
audio.dither_profile |
enum | shaped_f_weighted |
tpdf | shaped_hp | shaped_f_weighted |
Noise profile used when output is ≤ 16 bits. |
audio.peak_limiter_mode |
enum | lookahead_limiter |
off | soft_clip | lookahead_limiter |
Final stage protecting the DAC from inter-sample / peak overshoot. |
audio.peak_limiter_ceiling_dbfs |
f32 | -1.0 |
[-3.0, 0.0] |
True-peak ceiling enforced by the limiter. |
audio.peak_limiter_lookahead_ms |
f32 | 5.0 |
[2.0, 10.0] |
Look-ahead window for the limiter. |
audio.peak_limiter_release_ms |
f32 | 100.0 |
[20.0, 500.0] |
Release time of the limiter envelope. |
audio.volume_curve |
enum | logarithmic |
quadratic | logarithmic |
Slider-to-gain mapping for software volume. |
audio.volume_floor_db |
f32 | -60.0 |
[-80.0, -30.0] |
Minimum gain (in dB) the slider can reach above 0 % (mute is exact). |
audio.crossfeed_enabled |
bool | false |
— | Enables Bauer-style headphones crossfeed. |
audio.crossfeed_preset |
enum | bauer |
bauer | bauer_strong | custom |
Crossfeed preset; custom honours the delay/cutoff fields below. |
audio.crossfeed_delay_us |
f32 | 300.0 |
[200.0, 400.0] |
Inter-aural delay in microseconds (used by custom). |
audio.crossfeed_lp_cutoff_hz |
f32 | 700.0 |
[500.0, 1500.0] |
Crossfeed low-pass cutoff (used by custom). |
audio.resampler_quality |
enum | best |
standard | best |
Sinc length / oversampling factor of the rubato resampler. |
audio.convolver_enabled |
bool | false |
— | Enables the FFT-partitioned IR convolver. |
audio.convolver_ir_path |
string | null | null |
readable file path | Impulse response file picked from disk. |
audio.convolver_gain_db |
f32 | -6.0 |
[-24.0, 0.0] |
Trim applied to the convolver output to stay below the ceiling. |
audio.balance |
f32 | 0.0 |
[-1.0, 1.0] |
Stereo balance (-1 = full left, +1 = full right). |
audio.trim_db_per_channel |
array of f32 | [] |
length ≤ 8, each ∈ [-12.0, 0.0] |
Per-channel trim in dB for asymmetric setups. |
Qobee ships a real Windows desktop integration, opt-in per item from
Settings → Windows Integration so the user keeps control over
every shell hook. None of the hooks ever require admin privileges:
every registry write goes to HKCU only, and Qobee never claims to
be the system-wide default audio player.
app.qobee.player, set on the running
process via SetCurrentProcessExplicitAppUserModelID. Windows
uses it to group the taskbar icon, notifications, and (static)
Jump List under a single identity that survives updates.tauri-plugin-single-instance. A second qobee.exe invocation
forwards its argv to the running window and exits; there is
never a duplicate process or window.Show / focus, Play / Pause,
Previous, Next, Open library, Settings, Quit.
Toggleable from Settings.HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Qobee,
optionally with --minimized.qobee:// protocol handler — registered as a soft handler
under HKCU\Software\Classes\qobee. Examples:
qobee://play?path=C%3A%2Fmusic%2Fa.flac,
qobee://enqueue?path=..., qobee://play-next?path=...,
qobee://play-folder?path=..., qobee://import-folder?path=...,
qobee://library, qobee://settings.OpenWithProgids
hint on the 10 supported extensions
(.mp3 .flac .wav .ogg .m4a .aac .opus .alac .aiff .wv) and
attaches verbs Open in Qobee, Play in Qobee,
Add to Qobee queue, Play next in Qobee,
Import to Qobee library. Never replaces the default audio app.Play folder, Add folder to queue, Scan folder, Import folder on
Directory\shell, Directory\Background\shell (right-click empty
space inside an open folder), and Drive\shell (right-click a
drive letter).--play <paths>, --enqueue <paths>, --play-next <paths>, --play-folder <path>,
--enqueue-folder <path>, --scan-folder <path>,
--import-folder <path>, --open-library, --open-settings,
--minimized. Bare paths are treated as --play <path>.| Key | Purpose |
|---|---|
Software\Microsoft\Windows\CurrentVersion\Run\Qobee |
Autostart entry. |
Software\Classes\qobee\… |
qobee:// protocol handler. |
Software\Classes\Qobee.Music.AudioFile\… |
ProgID with the audio file verbs. |
Software\Classes\.<ext>\OpenWithProgids\Qobee.Music.AudioFile |
Soft "Open with" hint per audio extension. |
Software\Classes\Directory\shell\QobeeFolder.<verb> |
Folder verbs (right-click a folder). |
Software\Classes\Directory\Background\shell\QobeeFolder.<verb> |
Folder verbs (empty space inside a folder). |
Software\Classes\Drive\shell\QobeeFolder.<verb> |
Folder verbs (right-click a drive). |
Software\Qobee\WindowsIntegration |
Cleanup anchor written by the installer. |
The NSIS installer (src-tauri/installer/qobee.nsh) registers the
protocol + AUMID at install time and reverses every key listed above
on uninstall. Toggling shell hooks at runtime from the Settings
panel performs the equivalent registry writes through the
set_windows_integration Tauri command.
engine-bass Cargo feature exists as a
seam; the module is a stub with no external dependency.engine-bass Cargo feature.Dual-licensed under MIT or Apache-2.0. See the workspace Cargo.toml.