- Kotlin 45.8%
- Rust 26.6%
- C 14.8%
- Svelte 6.4%
- TypeScript 3.9%
- Other 2.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .cargo | ||
| .forgejo/workflows | ||
| .gitea/workflows | ||
| .github/workflows | ||
| android | ||
| desktop | ||
| examples | ||
| exobench | ||
| native/opus | ||
| scripts | ||
| src | ||
| .env.example | ||
| .gitignore | ||
| BENCH.md | ||
| build.sh | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| TODO.md | ||
RS Music Player
Cross-platform music player: high-performance Rust core (decoding, DSP, library, networking) behind a UniFFI FFI boundary, Jetpack Compose UI on Android, and a modern Tauri + SvelteKit desktop application for Linux & Windows.
Prerequisites
General & Rust
- Rust toolchain (stable 1.80+)
cargo testfor running core tests
Desktop (Linux & Windows)
- Node.js (20+) and pnpm (
corepack enable pnpmornpm install -g pnpm) - Linux system dependencies (WebKit2GTK, ALSA, AppIndicator):
- Ubuntu / Debian:
sudo apt update && sudo apt install -y \ libwebkit2gtk-4.1-dev build-essential curl wget file \ libxdo-dev libssl-dev libayatana-appindicator3-dev \ librsvg2-dev libasound2-dev pkg-config - Arch Linux / Manjaro:
sudo pacman -S webkit2gtk-4.1 base-devel curl wget file \ xdotool openssl libayatana-appindicator librsvg alsa-lib pkgconf - Fedora:
sudo dnf install webkit2gtk4.1-devel @development-tools curl wget \ libxdo-devel openssl-devel libayatana-appindicator3-devel \ librsvg2-devel alsa-lib-devel pkgconf-pkg-config
- Ubuntu / Debian:
- Windows Cross-compilation from Linux (optional):
cargo install cargo-xwinrustup target add x86_64-pc-windows-msvc- Note: Prebuilt
opus.libfor MSVC is already bundled undernative/opus/x86_64-pc-windows-msvc/.
Android
cargo-ndk(cargo install cargo-ndk)- Android NDK (e.g.
/opt/android-ndkor set viaANDROID_NDK_HOME) - Android SDK / JDK (OpenJDK 17+)
- Prebuilt Opus in
native/opus/(included in repository)
Building
Desktop Application (Linux & Windows)
1. Linux Desktop (Native)
To compile the release binary and bundle packages (.deb, .rpm, .AppImage):
./scripts/build-linux.sh
Or manually using pnpm:
cd desktop
pnpm install --frozen-lockfile
NO_STRIP=1 pnpm tauri build
- Binary output:
dist/rs-player-linux(ordesktop/src-tauri/target/release/desktop) - Package bundles:
desktop/src-tauri/target/release/bundle/(deb / rpm / appimage)
To launch the desktop application in development mode (with live hot-reload):
cd desktop
pnpm install
pnpm tauri dev
2. Windows Desktop (Cross-compile from Linux)
You can cross-compile a standalone Windows executable directly from Linux without a Windows VM using cargo-xwin:
./scripts/build-windows.sh
- First run: automatically downloads the Windows SDK and CRT into
~/.cache/cargo-xwin(~1 GB). - Binary output:
dist/rs-player-windows.exe(statically linked, self-contained single.exe).
3. Windows Desktop (Native on Windows)
On a Windows PC with Node.js and Visual Studio C++ Build Tools installed:
cd desktop
pnpm install
pnpm tauri build
Android Application (APK)
Standard Build (All Default Features)
./build.sh # host lib → Kotlin UniFFI bindings → Android APK (arm64-v8a) → install
cargo test # Rust integration tests
Feature Flags
The Rust core is modular and supports compile-time feature flags to reduce binary size and memory footprint:
| Feature | Description | Heavy Dependencies |
|---|---|---|
webdav |
WebDAV cloud streaming & metadata sync | reqwest, quick-xml, base64, percent-encoding |
share |
LAN / P2P library sharing server & client | tiny_http, serde, serde_json (requires webdav) |
replaygain |
ReplayGain EBU R128 loudness analysis | ebur128 |
speedpitch |
WSOLA time-stretching & pitch shifting | wsola |
sleeptimer |
Sleep timer (planned — see below) | none |
transcode |
On-the-fly Opus transcode of shared streams (future idea — see below) | rubato, ogg-rs (optional) |
Default features: ["webdav", "share", "replaygain", "speedpitch"]
When a feature is excluded at compile time, lightweight fallback stubs are provided so the Kotlin UI compiles seamlessly against the full UniFFI contract, while the compiled .so drops the heavy dependencies.
Minimal Build (No Default Features)
To build a lightweight APK containing only the local audio player engine:
./build.sh --no-default-features
Or assemble a release APK without installing to a device:
# 1. Build host library and generate UniFFI bindings
cargo build --lib --release
cargo run --release --bin uniffi-bindgen generate \
--library target/release/librs_music_player.so \
--language kotlin \
--no-format \
--out-dir android/app/src/main/java
# 2. Cross-compile Rust core for Android without default features
export ANDROID_NDK_HOME=/opt/android-ndk
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_PATH=$(pwd)/native/opus/aarch64-linux-android/lib/pkgconfig
export ANDROID_ABI=arm64-v8a
cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --release --no-default-features
# 3. Assemble Release APK
cd android
./gradlew assembleRelease
# Output: android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
Custom Feature Build
Select only the features you need (e.g. only ReplayGain + Speed/Pitch):
./build.sh --no-default-features --features "replaygain,speedpitch"
Planned: Sleep Timer (sleeptimer)
Optional end-of-evening timer, shipped as a compile-time module so it can be excluded from minimal builds like the other features (UI hides itself when the module is absent).
- Countdown trigger — either after N tracks or after N minutes, N ∈ 1…180 (mode + value chosen in Settings).
- End action (independently configurable in Settings):
- Pause — pause the current track, keeping its position and the queue intact;
- Close app — stop playback and shut the application down.
Future idea: On-the-fly transcoding (transcode)
Server-side transcode of shared-library streams (share://): decode → resample → re-encode, so weak links stream lossy instead of pulling full FLAC.
- Scope — exactly two transports:
share.rs/shared://streams (both ends are this app, we control the protocol) and JellyFin (transcoding happens server-side via its API). WebDAV hosts (SFTPGo & co.) serve raw bytes and cannot transcode; other media servers (Plex, Subsonic/Navidrome, …) are not planned. - Why it's nearly free — the decode side (symphonia) and the encode side (libopus doubles as an encoder via audiopus) are already linked into the
.so. Missing pieces: resampling 44.1k→48k (rubato, ~0.2 MB), optionally an Ogg muxer (ogg-rs, ~0.1 MB) for standard-player compatibility; ~200–400 lines of glue. Total: well under 1 MB, no new heavy dependencies. - Pipeline — decode → 48 kHz → Opus VBR 96–128 kbps → stream; encode runs magnitudes faster than realtime, so the server never becomes the bottleneck.
- Rejected alternative — bundling ffmpeg: +25–50 MB per ABI for codecs we don't need; only worth revisiting if AAC/HLS is ever required.
- Client-side transcoding is intentionally out of scope: the client decodes everything locally anyway, so re-encoding before decoding wastes battery and saves no bandwidth.
Repository layout
src/ Rust core (compiled as cdylib for Android, rlib for desktop)
├── lib.rs Module wiring + UniFFI re-exports (the FFI surface)
├── player/
│ ├── mod.rs Player API: queue, playback state, DB access
│ └── output.rs Audio engine: StreamControl, producer/consumer threads
├── stream.rs Decoder abstraction (symphonia + audiopus/Opus), seek, duration
├── library/
│ ├── mod.rs SQLite schema, scanner, queries, playlists, thumbnails
│ ├── metadata.rs Tag reading (lofty) + fast duration probing
│ ├── webdav_scan.rs WebDAV library scan (range-based metadata)
│ ├── loudness.rs ReplayGain analysis (EBU R128)
│ └── lrc.rs Synchronized & plain LRC parsing, formatting, and saving
├── webdav.rs WebDAV client (PROPFIND, streaming, range fetch)
├── share.rs P2P shared-library server + client sync
├── android_audio.rs AudioTrack JNI bridge (Android only)
└── bin/uniffi-bindgen.rs Kotlin binding generator (used by build.sh)
desktop/ Desktop application (Tauri 2 + SvelteKit + Tailwind v4)
├── src/ Frontend UI (Svelte 5 runes, TypeScript, Lucide icons)
│ ├── lib/ Components (FullscreenPlayer, LrcEditor, PlayerBar, Sidebar, etc.)
│ └── routes/ Main views (Tracks, Share, Settings, FolderExplorer)
└── src-tauri/ Tauri 2 backend (Rust), integrates rs-music-player core
android/ Android application (Kotlin, Jetpack Compose)
└── app/src/main/java/com/olyx/rsmusicplayer/
├── MainActivity.kt / SettingsScreen.kt / SharedLibraryScreen.kt
├── PlayerScreen.kt / FullScreenPlayer.kt / MiniPlayer.kt / Components.kt
├── PlayerViewModel.kt / PlayerRepository.kt / PlayerHolder.kt
├── PlaybackService.kt / AudioFocusHelper.kt / NsdHelper.kt
└── ../uniffi/…/rs_music_player.kt generated bindings — do not edit
scripts/ Build, packaging and benchmark scripts
├── build-linux.sh Build Linux desktop binary and package bundles
├── build-windows.sh Cross-compile Windows desktop .exe via cargo-xwin
└── profile_on_device.sh Profiling tool for Android devices
tests/ Rust integration tests (real asserts, no local-media dependencies)
native/opus/ Prebuilt libopus for Android (ARM64, ARMv7) and Windows (MSVC)
.cargo/config.toml Linker configurations
build.sh Android build pipeline script
TODO.md Live roadmap checklist
Architecture notes
- Heavy logic (decode, SQLite, DSP, HTTP, LRC parsing) lives in Rust; UI layers are thin.
- Audio path: decoder thread → lock-free ring buffer → output (
cpalon desktop, AudioTrack/JNI on Android). Volume, speed/pitch (WSOLA), ReplayGain are applied in Rust before the ring buffer. - Gapless advance (mpv-style): EOF track switching is decided by the audio thread at buffer drain — not by UI timers — so suspend can't stall it and no boundary wake lock is needed. The same audio output is kept open across same-format tracks; a format change swaps in a fresh one.