Local music player for desktop β built with Tauri 2, React 19 & Rust
WaveFlow is a local music player desktop app with a Spotify-inspired 3-panel UI. It scans your local audio folders, organizes tracks by album/artist/genre, and plays them with a real-time audio engine β no streaming, no cloud, your music stays on your machine.
- Audio playback β symphonia decoder + cpal output, supports MP3, FLAC, WAV, OGG Vorbis, AAC, ALAC (M4A)
- Real-time engine β lock-free 3-thread architecture (decoder, ring buffer, cpal callback), zero allocations in the hot path
- Crossfade DSP β real dual-decoder mix with equal-power gains (cos/sin) over the user-set window
- Audio settings β volume normalization (-3 dB), mono downmix, configurable crossfade
- Resume β remembers last track + position across app restarts
- Queue β persistent queue with shuffle (Fisher-Yates), repeat (off/all/one), auto-advance, drag-and-drop reorder
- Scanning β point to any folder, metadata extraction via lofty, embedded artwork extraction
- Watch folders β
notify-driven filesystem watcher, debounced rescans so dropped-in files appear automatically; deleted files are flagged unavailable rather than purged so play history survives - Track analysis β on-demand or auto-after-scan: peak, loudness (dB), ReplayGain, BPM via autocorrelation; tagged musical key (
TKEY/INITIALKEY) read at scan time - Audio quality surfacing β sample rate / bitrate / size / codec / bit depth strip under the player; Hi-Res badges (β₯ 24-bit, β₯ 44.1 kHz) on covers and rows
- Track Properties dialog β foobar2000-style modal with metadata, audio specs, analysis results, file path, and Show in Explorer
- POPM ratings β 5-star ratings (with half-steps) extracted from tags + editable inline
- Multi-select β ctrl/shift selection across views with floating action bar (Play / Add to queue / Add to playlist / Remove)
- Multi-artist β automatic split of
"Artist A, Artist B"into individual, independently-linkable artists - Album & artist detail pages β clickable cards open dedicated views with tracklist, discography, biography, and stats
- Cover picker β manual Deezer search, local file upload (magic-byte validation), batch fetch for albums missing artwork
- A-Z navigator β letter rail on the artists tab, NFD-normalized for diacritics
- Lightbox β double-click any cover or artist photo to view full-size
- Playlists β create, edit, delete; add tracks from folders/albums/artists in bulk; drag-and-drop reorder (virtualized for large playlists)
- Likes β heart any track, dedicated "Liked tracks" view
- Recent β automatic 50-track recency list driven by
play_event - Search β instant full-text search (FTS5 contentless) across titles, artists, albums with prefix matching
- Right-click context menu β Spotify-style: Play next, Add to queue, Add to playlist (submenu), Like, Go to album, Go to artist (submenu when multi-artist), Properties, Show in explorer
- Metadata enrichment β Deezer public API (artist images, album covers, labels) + Last.fm (artist biographies) cached 30 days locally; artwork downloaded into a hash-addressed on-disk cache so it renders offline on re-visits
- Last.fm scrobbling β signed
auth.getMobileSessionlogin, retry queue with exponential backoff, livetrack.updateNowPlaying, automatic re-auth prompt on session expiry - Synchronized lyrics β LRCLIB lookup with embedded-tag fallback and
.lrcfile import
- System tray β quick playback controls + close-to-tray
- Now Playing / Lyrics panels β Spotify-style right-edge panels (large artwork, clickable artists, artist biography, synchronized lyrics)
- Statistics view β KPIs, listening-by-day / listening-by-hour charts, top tracks / artists / albums
- Thumbnails β SIMD-accelerated 1x / 2x covers via
fast_image_resize - Virtual scroll β handles 6000+ tracks without UI freeze (
@tanstack/react-virtual) - Single-click play β optional toggle, sort memory persisted per context
- Dark mode β animated radial transition via View Transitions API
- i18n β French, English, Spanish, German; auto-detected, switchable in settings
- Accessibility β keyboard navigation, ARIA roles, focus rings,
prefers-reduced-motion - Profiles β isolated per-profile database (libraries, playlists, settings, play history); shared metadata cache across profiles
| Layer | Technologies |
|---|---|
| Desktop shell | Tauri 2.10 (tray icon, opener, dialog plugins) |
| 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.15 (output), rubato 0.15 (resample), rtrb 0.3 (SPSC ring) |
| Metadata extraction | lofty 0.22 (tags, embedded art, POPM, INITIALKEY) |
| Imaging | image 0.25 + fast_image_resize 6 (SIMD thumbnails) |
| Filesystem watcher | notify 8 (debounced rescans of watched folders) |
| External APIs | Deezer public API (no auth) + Last.fm (read + signed methods via md-5 + reqwest 0.12 with rustls) + LRCLIB (synchronized lyrics) |
| Package manager | Bun |
# Install dependencies
bun install
# Run the desktop app in development mode
bun run tauri dev
# Build for production
bun run tauri buildbun run dev # Vite dev server only (no Tauri shell)
bun run typecheck # TypeScript check
bun run lint # ESLint
bun run lint:fix # ESLint with auto-fix
bun run format # Prettierwaveflow/
βββ src/ # React frontend
β βββ components/
β β βββ common/ # Reusable UI (Artwork, ArtistLink, ContextMenu, TrackContextMenu, TrackPropertiesModal, HiResBadge, Lightbox, CoverPickerModal, StarRating, SelectionActionBar, modals, EmptyStateβ¦)
β β βββ layout/ # Sidebar, TopBar, AppLayout, QueuePanel, NowPlayingPanel, LyricsPanel, DeviceMenu
β β βββ player/ # PlayerBar, PlaybackControls, VolumeControl, ProgressBar, AudioQualityFooter
β β βββ views/ # Home, Library, Playlist, AlbumDetail, ArtistDetail, Liked, Recent, Settings, Statisticsβ¦
β βββ contexts/ # ThemeContext, PlayerContext, LibraryContext, PlaylistContext, ProfileContext
β βββ hooks/ # useTheme, usePlayer, useLibrary, usePlaylist, useProfile, useTrackContextMenu, useMultiSelect, useSortMemory
β βββ lib/
β β βββ tauri/ # Typed invoke() wrappers (track, browse, player, playlist, detail, integration, analysis, lyrics, stats, profile, dialog, deezer, library, artwork)
β β βββ hiRes.ts # `isHiRes` helper (β₯ 24-bit, β₯ 44.1 kHz threshold)
β β βββ imageCache.ts # In-memory LRU for resolved artwork URLs
β β βββ playlistVisuals.ts # Shared color/icon constants for playlists
β β βββ PlaylistIcon.tsx # Icon dispatcher component
β βββ i18n/locales/ # fr.json, en.json, es.json, de.json
β βββ types/ # ViewId, LibraryTab, NavItemProps, etc.
β βββ App.tsx # Provider tree
β βββ main.tsx # Entry point
βββ src-tauri/ # Rust backend
β βββ src/
β β βββ audio/ # Audio engine (engine, decoder, output, resampler, state, analytics, crossfade)
β β βββ commands/ # Tauri commands (library, playlist, track, browse, player, scan, profile, deezer, integration, lyrics, stats, analysis, maintenance, app_info)
β β βββ db/ # Database open/migrate helpers (app.db + per-profile data.db)
β β βββ analysis.rs # Per-track audio analysis (peak, loudness dB, ReplayGain, BPM)
β β βββ deezer.rs # Deezer public API client (search/get artist & album)
β β βββ lastfm.rs # Last.fm API client (artist.getInfo + signed mobile-session / scrobble / now-playing)
β β βββ lrclib.rs # LRCLIB API client (synchronized lyrics)
β β βββ metadata_artwork.rs # Shared on-disk cache for remote artwork (blake3-hashed)
β β βββ queue.rs # Persistent queue operations (fill, advance, shuffle, reorder, restore)
β β βββ scrobbler.rs # Last.fm scrobble worker (queue drain, retry/backoff, re-auth prompt)
β β βββ thumbnails.rs # SIMD-accelerated 1x/2x cover thumbnails
β β βββ watcher.rs # Filesystem watcher manager (per-folder notify watchers, debounced rescans)
β β βββ state.rs # AppState (profile pool, paths, global app_db)
β β βββ paths.rs # Filesystem layout
β β βββ error.rs # AppError + AppResult
β β βββ lib.rs # Tauri setup, command registration, system tray, shutdown hook
β βββ migrations/
β β βββ app/ # Global app.db schema (profile list, app_setting, shared metadata cache: metadata_artist, metadata_album, lyrics)
β β βββ profile/ # Per-profile SQLite schema (FTS5 contentless, triggers, indexes, scrobble_queue, track_analysis, audio quality columns)
β βββ Cargo.toml
β βββ tauri.conf.json
βββ package.json
ββ Tauri commands (tokio) ββ Decoder thread (std) ββ cpal callback (real-time)
β player_play, pause, seek β symphonia FormatReader + β pop f32 from SPSC ring
β β crossbeam::Sender βββββββΊβ Decoder + rubato Resampler β Γ volume Γ normalization
β β push f32 β rtrb::Producer βββΊβ mono downmix (if enabled)
β β emit position/state events β β device native format
ββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββ
Rules: the cpal callback never allocates, never locks, never logs. It only touches rtrb::Consumer and Atomic* fields in SharedPlayback.
Currently shipping French, English, Spanish and German β auto-detected at first launch from the OS locale, switchable from Settings.
Strings are externalized in src/i18n/locales/. To add a language:
- Create
src/i18n/locales/xx.json(same structure asfr.jsonβ keep all keys translated, the loader doesn't fall back per-key) - Import it in
src/i18n/index.tsand add toSUPPORTED_LANGUAGES - It will appear in the Settings language selector automatically
GPL-3.0 β see LICENSE