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
- Library scanning β point to any folder, metadata extraction via lofty, embedded artwork extraction
- Multi-artist β automatic split of
"Artist A, Artist B"into individual, independently-linkable artists - Album & artist detail pages β clickable album/artist cards open a dedicated view with tracklist, discography, and stats
- Now Playing panel β Spotify-style right-edge panel with large artwork, clickable artists, and artist biography
- Metadata enrichment β Deezer public API (artist images, album covers, labels) + Last.fm (artist biographies) cached 30 days locally; artwork is downloaded into a hash-addressed on-disk cache so it renders offline on re-visits
- Playlists β create, edit, delete, add tracks from folders/albums/artists in bulk
- Likes β heart any track, dedicated "Liked tracks" view
- Search β instant full-text search (FTS5 contentless) across titles, artists, albums with prefix matching
- Queue β persistent queue with shuffle (Fisher-Yates), repeat (off/all/one), auto-advance
- Resume β remembers last track + position across app restarts
- Audio settings β volume normalization (-3 dB), mono downmix, crossfade slider (UI ready)
- Virtual scroll β handles 6000+ tracks without UI freeze (@tanstack/react-virtual)
- Dark mode β animated radial transition via View Transitions API
- i18n β French & English, 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)
| Layer | Technologies |
|---|---|
| Desktop shell | Tauri 2.10 |
| Frontend | React 19, TypeScript, Vite 8, Tailwind CSS 4, Lucide icons |
| 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) |
| External metadata | Deezer public API (no auth) + Last.fm (user-provided API key) via reqwest 0.12 with rustls |
| 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 (NavItem, Artwork, ArtistLink, modals, EmptyState)
β β βββ layout/ # Sidebar, TopBar, AppLayout, QueuePanel, NowPlayingPanel
β β βββ player/ # PlayerBar, PlaybackControls, VolumeControl, ProgressBar
β β βββ views/ # Home, Library, Playlist, AlbumDetail, ArtistDetail, Liked, Recent, Settings, etc.
β βββ contexts/ # ThemeContext, PlayerContext, LibraryContext, PlaylistContext, ProfileContext
β βββ hooks/ # useTheme, usePlayer, useLibrary, usePlaylist, useProfile
β βββ lib/
β β βββ tauri/ # Typed invoke() wrappers (track, browse, player, playlist, detail, integration)
β β βββ playlistVisuals.ts # Shared color/icon constants for playlists
β β βββ PlaylistIcon.tsx # Icon dispatcher component
β βββ i18n/locales/ # fr.json, en.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)
β β βββ commands/ # Tauri commands (library, playlist, track, browse, player, scan, profile, deezer, integration)
β β βββ db/ # Database open/migrate helpers (app.db + per-profile data.db)
β β βββ deezer.rs # Deezer public API client (search/get artist & album)
β β βββ lastfm.rs # Last.fm API client (artist.getInfo with HTML strip)
β β βββ 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, restore)
β β βββ state.rs # AppState (profile pool, paths, global app_db)
β β βββ paths.rs # Filesystem layout
β β βββ error.rs # AppError + AppResult
β β βββ lib.rs # Tauri setup, command registration, shutdown hook
β βββ migrations/
β β βββ app/ # Global app.db schema (profile list, app_setting, deezer cache tables)
β β βββ profile/ # Per-profile SQLite schema (FTS5 contentless, triggers, indexes, lyrics)
β βββ 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.
Strings are externalized in src/i18n/locales/. To add a language:
- Create
src/i18n/locales/xx.json(same structure asfr.json) - Import it in
src/i18n/index.tsand add toSUPPORTED_LANGUAGES - It will appear in the Settings language selector automatically
GPL-3.0 β see LICENSE