Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Rust / Tauri 2. Entry: `src-tauri/src/main.rs` → `lib.rs`.

- **Commands** (`src-tauri/src/commands/`): organized by domain — `library.rs`, `playlist.rs`, `smart_playlists.rs`, `track.rs`, `browse.rs`, `player.rs`, `scan.rs`, `edit.rs`, `profile.rs`, `analysis.rs`, `deezer.rs`, `similar.rs`, `lyrics.rs`, `stats.rs`, `wrapped.rs`, `integration.rs`, `maintenance.rs`, `app_info.rs`, `radio.rs`, `mood_radio.rs`, `duplicates.rs`, `preferences.rs`, `share.rs`, `changelog.rs`, etc. All registered in `lib.rs` via `generate_handler![]`.
- **External API clients** (crate-root modules): `deezer.rs` (public Deezer, no auth), `lastfm.rs` (`artist.getInfo`, user API key required). Both use `reqwest` with `rustls-tls`.
- **Audio engine** (`src-tauri/src/audio/`): 3-thread lock-free architecture — `decoder.rs` (symphonia + rubato), `output.rs` (cpal callback on a dedicated thread, SPSC `rtrb` ring buffer), `state.rs` (`SharedPlayback` with atomics, no locks in hot path), `analytics.rs` (tokio task for `play_event` writes + auto-advance), `crossfade.rs`, `eq.rs`, `spectrum.rs`, `wasapi_exclusive.rs` (Windows-only opt-in), and `dsd/` (in-house DSF/DFF parser + DSD→PCM converter, Symphonia 0.5 doesn't decode DSD). Deep dive: [`docs/features/playback.md`](docs/features/playback.md).
- **Audio engine** (`src-tauri/src/audio/`): 3-thread lock-free architecture — `decoder.rs` (symphonia + rubato), `output.rs` (cpal callback on a dedicated thread, SPSC `rtrb` ring buffer), `state.rs` (`SharedPlayback` with atomics, no locks in hot path), `analytics.rs` (tokio task for `play_event` writes + auto-advance), `crossfade.rs`, `eq.rs`, `spectrum.rs`, `wasapi_exclusive.rs` (Windows-only opt-in), and `dsd/` (in-house DSF/DFF parser + DSD→PCM converter, Symphonia doesn't decode DSD). Deep dive: [`docs/features/playback.md`](docs/features/playback.md).
- **DLNA / UPnP MediaServer** (`src-tauri/src/dlna/`): worker thread → axum HTTP server + SSDP announcer. Opt-in (`app_setting['dlna.enabled']`, default OFF). See [`docs/features/dlna.md`](docs/features/dlna.md).
- **OS media controls** (`media_controls.rs`): souvlaki bridge → SMTC / MPRIS / MediaRemote. Initialized post-window (needs HWND on Windows).
- **Discord Rich Presence** (`discord_presence.rs`): named-pipe IPC, opt-in `app_setting['integrations.discord_rpc']` (default ON). See [`docs/features/integrations.md`](docs/features/integrations.md#discord-rich-presence).
Expand All @@ -68,7 +68,7 @@ These bite you if you ignore them — they're the contract the rest of the codeb
- **Persistence**: per-profile settings live in `profile_setting` (key-value, typed). Pattern: `INSERT ... ON CONFLICT DO UPDATE`. App-wide settings live in `app_setting` with the same shape.
- **Events**: backend emits Tauri events (`player:state`, `player:position`, `player:track-changed`, `player:queue-changed`, `player:error`, `player:ab-loop`, `player:spectrum`, `track:updated`, `library:rescanned`, `scan:progress`, `lyrics:updated`, …). Frontend listens via `listen()` from `@tauri-apps/api/event`.
- **Audio callback is hot**: the cpal callback (and the WASAPI exclusive thread) MUST NOT allocate, lock, or log. Only `rtrb::Consumer` reads + `Atomic*` loads. All heavy work (EQ, ReplayGain, resampling, FFT, BLAKE3) runs on the decoder thread before samples reach the SPSC ring.
- **Migrations are immutable once merged**: sqlx records a SHA-384 checksum in `_sqlx_migrations.checksum` at apply time, so editing a merged migration crashes every existing install at boot with `"migration <id> was previously applied but has been modified"` (no auto-recovery — user has to wipe their data dir). For any schema evolution, **create a new dated migration** `YYYYMMDDhhmmss_<slug>.sql`. Same rule for `migrations/app/`.
- **Migrations are immutable once merged**: sqlx records a SHA-384 checksum in `_sqlx_migrations.checksum` at apply time, so editing a merged migration crashes every existing install at boot with `"migration <id> was previously applied but has been modified"`. For any schema evolution, **create a new dated migration** `YYYYMMDDhhmmss_<slug>.sql`. Same rule for `migrations/app/`. **Line-ending drift is a non-event** — [`db::migration_heal`](src-tauri/src/db/migration_heal.rs) reconciles stored checksums against the compiled-in migrator before each `Migrator::run`: when the stored hash matches the LF or CRLF variant of the same SQL (Windows `core.autocrlf=true` regression), it silently rewrites the row to the canonical hash and logs a warning. A real SQL change still panics, because neither LF nor CRLF normalization will rescue it.
- **Virtual scroll everywhere**: TrackTable uses `@tanstack/react-virtual` for 6000+ track performance. Virtualized tables consume `usePageScroll()` for the scroll element instead of nesting their own `overflow-y-auto` — drives a single Spotify-style scrollbar.
- **Multi-artist queries**: the scanner splits `"A, B"` on `", " / "; "` into individual `artist` rows linked via `track_artist`. Queries rebuild the display string via `GROUP_CONCAT` over `track_artist` ordered by `position`. `ArtistLink` accepts parallel `artist_name` + `artist_ids` strings so every contributor is individually clickable. New track queries must follow the same join pattern.
- **Album grouping = `(canonical_title, album_artist_id)`**: [`scan.rs::upsert_album`](src-tauri/src/commands/scan.rs) keys on the album artist (Album Artist tag → `is_compilation` → primary artist fallback). `album.is_compilation` is sticky and `merge_implicit_compilations` collapses ≥ 3 distinct-artist same-title rows into "Various Artists" after every scan. `edit.rs` re-runs `upsert_album` with the OLD album's Album Artist / compilation flags so renames don't re-split. Deep dive: [`docs/features/library.md`](docs/features/library.md#album-grouping).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Once installed (any of the above), the in-app updater fetches future versions au
| **Discord Rich Presence** | discord-rich-presence 1.1 (local IPC named pipe, no auth) |
| **Frontend** | React 19, TypeScript, Vite 8, Tailwind CSS 4, Lucide icons, `@dnd-kit` (drag-and-drop), `@tanstack/react-virtual` (virtualization) |
| **Backend** | Rust, SQLite (sqlx), FTS5 contentless full-text search |
| **Audio** | symphonia 0.5 (decode), cpal 0.17 (output), rubato 2.0 (resample), rtrb 0.3 (SPSC ring) |
| **Audio** | symphonia 0.6 (decode), cpal 0.17 (output), rubato 2.0 (resample), rtrb 0.3 (SPSC ring) |
| **Metadata extraction** | lofty 0.24 (tags, embedded art, POPM, INITIALKEY) |
| **Imaging** | image 0.25 + fast_image_resize 6 (SIMD thumbnails) |
| **Filesystem watcher** | notify 8 (debounced rescans of watched folders) |
Expand Down
4 changes: 2 additions & 2 deletions docs/features/playback.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ The audio path lives in [`src-tauri/src/audio/`](../../src-tauri/src/audio). It

## Decoding & output

- **Decoder** — [`symphonia 0.5`](https://crates.io/crates/symphonia) over MP3, FLAC, WAV, OGG Vorbis, AAC, ALAC (M4A). Source samples are converted to interleaved `f32`, channel-mapped (mono ↔ stereo, 5.1 → stereo Lo/Ro per ITU BS.775), then resampled to the device rate by [`rubato 2.0`](https://crates.io/crates/rubato) (`Fft<f32>` + `FixedSync::Input`, with a fast `Passthrough` variant when source rate already matches the device).
- **DSD pipeline** — symphonia 0.5 doesn't decode 1-bit DSD, so DSF (Sony) and DFF (Philips) containers route through [`audio/dsd/`](../../src-tauri/src/audio/dsd/): a custom container parser reads the layout (DSD64 → DSD1024, mono / stereo / multichannel), and a 256-tap windowed-sinc FIR with a Blackman-Harris envelope decimates the bitstream by 64 to land DSD64 at 44.1 kHz, DSD128 at 88.2 kHz, etc. The resulting PCM joins the same channel-convert + resample + ring-buffer pipeline as symphonia output. `ActiveStream` carries a `StreamBackend` enum (Symphonia / Dsd) so seeking and decoder reset stay uniform from the engine's perspective. **Limitation**: real audiophile players use multi-stage halfband cascades for lower CPU at the same SNR; ours prioritises code clarity. DoP (DSD-over-PCM) is not yet wired — the converter always produces PCM.
- **Decoder** — [`symphonia 0.6`](https://crates.io/crates/symphonia) over MP3, FLAC, WAV, OGG Vorbis, AAC, ALAC (M4A). Source samples are converted to interleaved `f32`, channel-mapped (mono ↔ stereo, 5.1 → stereo Lo/Ro per ITU BS.775), then resampled to the device rate by [`rubato 2.0`](https://crates.io/crates/rubato) (`Fft<f32>` + `FixedSync::Input`, with a fast `Passthrough` variant when source rate already matches the device).
- **DSD pipeline** — symphonia doesn't decode 1-bit DSD, so DSF (Sony) and DFF (Philips) containers route through [`audio/dsd/`](../../src-tauri/src/audio/dsd/): a custom container parser reads the layout (DSD64 → DSD1024, mono / stereo / multichannel), and a 256-tap windowed-sinc FIR with a Blackman-Harris envelope decimates the bitstream by 64 to land DSD64 at 44.1 kHz, DSD128 at 88.2 kHz, etc. The resulting PCM joins the same channel-convert + resample + ring-buffer pipeline as symphonia output. `ActiveStream` carries a `StreamBackend` enum (Symphonia / Dsd) so seeking and decoder reset stay uniform from the engine's perspective. **Limitation**: real audiophile players use multi-stage halfband cascades for lower CPU at the same SNR; ours prioritises code clarity. DoP (DSD-over-PCM) is not yet wired — the converter always produces PCM.
- **Output** — [`cpal 0.17`](https://crates.io/crates/cpal) on a dedicated thread because `cpal::Stream` is `!Send` on Windows. Samples cross the thread via an [`rtrb 0.3`](https://crates.io/crates/rtrb) SPSC ring (`RING_CAPACITY = 96 000` `f32`s ≈ 1 s @ 48 kHz stereo).
- **Hot-path rules** — the cpal callback never allocates, locks or logs. It only reads the `rtrb::Consumer` and `Atomic*` fields in `SharedPlayback`.

Expand Down
108 changes: 54 additions & 54 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ blake3 = "1"
# Audio playback: symphonia decodes, cpal outputs, rubato resamples,
# rtrb is the SPSC ring between decoder thread and audio callback,
# crossbeam-channel carries AudioCmd from tokio commands to the decoder.
symphonia = { version = "0.5", default-features = false, features = [
symphonia = { version = "0.6", default-features = false, features = [
"mp3",
"flac",
"wav",
Expand All @@ -104,6 +104,10 @@ symphonia = { version = "0.5", default-features = false, features = [
"alac",
"isomp4",
"pcm",
# Symphonia 0.6 split metadata behind feature flags; `all-meta` keeps
# ID3/APE/Vorbis tag detection during probe scoring (the scanner uses
# lofty for the actual tag read, so this is purely for probe accuracy).
"all-meta",
] }
cpal = "0.17"
rubato = "2.0"
Expand Down
Loading
Loading