- Rust 48.4%
- JavaScript 20.9%
- TypeScript 16.5%
- Swift 10.9%
- CSS 1.9%
- Other 1.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .vscode | ||
| benchmarks/results | ||
| docs | ||
| plugins/tauri-plugin-nmrav-player | ||
| public | ||
| scripts | ||
| src | ||
| src-tauri | ||
| tests | ||
| .gitignore | ||
| .prettierrc.json | ||
| .swiftformat | ||
| AGENTS.md | ||
| BENCHMARKS.md | ||
| index.html | ||
| missing-cover.png | ||
| missing-cover.svg | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| TODO.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
nmrav
A small Tauri 2 music-library indexer. The first pass deliberately supports only M4A/MP4 metadata and focuses on bounded work, minimal reads, and a small UI.
What is implemented
- Recursive, case-insensitive
.m4adiscovery. - A focused ISO-BMFF parser for artist, album artist, album, title, release date, disc/track number, duration, and MusicBrainz Track Id.
- No heap allocations in the parser hot path. Tag values have fixed inline bounds; media payloads, cover art, and unknown atoms are skipped by size.
- A small
ReadAteffect boundary backed by the winning fixed 4 KiB page reader. - A bounded 128-file pipeline with eight desktop I/O workers and four mobile workers.
- A transactional SQLite catalog with FTS5 prefix search.
- Release-date and date-added album-group sorting in either direction; disc and track order always remain ascending within each album.
- Absolute times are UTC Unix milliseconds. Release tags retain year/month/day precision as a small integer; raw date strings are never persisted.
- A virtualized HTML list backed by a resident packed struct of arrays. Search and sorting transfer only row indexes; scrolling performs no IPC. Only visible and overscan rows and their cover images are rendered.
- Compact, tappable track cards modeled on the earlier Swift app.
- Case-insensitive
folder/coverJPEG or PNG discovery that selects the highest-resolution image, with lazy native thumbnails. - One shared 2.28 KiB SVG placeholder asset; it is cached once rather than copied into every virtualized track card.
- A native desktop folder picker and import progress.
- A persisted Rust playback queue with deterministic shuffle, fixed native lookahead, seek IDs, and stale native-event rejection.
- Monotonic native playback checkpoints for elapsed listening time; UTC epoch milliseconds are used only for absolute persisted event dates.
- An iOS
AVQueuePlayerTauri plugin scaffold with a direct Swift-to-Rust event boundary that does not depend on the WebView being awake. - Linux playback through GStreamer
playbin3, with MPRIS metadata and desktop play/pause, next, previous, and seek controls routed through the same Rust state machine.
Directory walking, paths, SQLite storage, Tauri IPC, and DOM rendering necessarily allocate. The zero-allocation claim is intentionally limited to parsing one already opened file.
Desktop development on Arch Linux
Install Tauri's Linux prerequisites, Rust 1.98+, GStreamer 1.24+ development files, and an AAC/M4A GStreamer decoder, then:
sudo pacman -S --needed gstreamer gst-plugins-base gst-plugins-good gst-libav
pnpm install
pnpm tauri dev
The development command opens the app and reloads the frontend as it changes.
Choose Import folder, select a library such as ~/Music/m4a, then scroll and
search the result.
Application state is stored in app.sqlite3 below Tauri's app-data directory.
This unreleased build starts with an empty library and disconnected scrobbling
services; refresh/import the music and reconnect the services. Music files remain
in place. Subsequent launches retain the new state. See storage.
For an intentional development reset, close the app and remove its state files. This clears imported metadata, playlists, playback, preferences, and credentials:
nmrav_data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/com.nmrav.client"
rm -f -- "$nmrav_data_dir/app.sqlite3" \
"$nmrav_data_dir/app.sqlite3-wal" \
"$nmrav_data_dir/app.sqlite3-shm"
Build and run a release executable without creating distro bundles:
pnpm tauri build --no-bundle
./src-tauri/target/release/nmrav
Tauri can emit Linux bundle formats with pnpm tauri build, but it does not emit a
native Arch pkg.tar.zst; use a PKGBUILD when distribution packaging becomes useful.
Tests and storage benchmark
Cross-platform operation traces are persisted in the device SQLite alongside the playback journal. See structured diagnostics for coverage, correlation, clock semantics, retention limits and capture queries.
pnpm lint includes frontend, Rust, and Swift formatting checks. Install the pinned
SwiftFormat version and see code style and scalar semantics
for formatter setup, type boundaries, and the complete verification commands.
pnpm lint
cargo test --all-targets --manifest-path src-tauri/Cargo.toml
cargo run --release --manifest-path src-tauri/Cargo.toml \
--bin nmrav-import-bench -- "$HOME/Music/m4a" 3 9705 warm 8
cargo run --release --manifest-path src-tauri/Cargo.toml \
--bin nmrav-import-bench -- "$HOME/Music/m4a" 1 1000 cold 8 path
cargo run --release --manifest-path src-tauri/Cargo.toml \
--bin nmrav-catalog-bench -- "$HOME/Music/m4a" 8 cold
The parser benchmark takes passes, maximum file count, warm/cold, worker count,
and optional native/path ordering. On Linux, cold uses
POSIX_FADV_DONTNEED on only the selected, clean music-file cache pages; it does not
use global drop_caches. It reports logical parser bytes and kernel-accounted
physical reads. The catalog benchmark additionally includes the directory walk,
SQLite inserts, FTS triggers, commit, browse, and search queries.
On macOS, pnpm bench:webkit-memory opens an isolated WKWebView with the
production frontend, 10,000 synthetic tracks, and simulated playing/meter events.
It measures WebContent's physical footprint (including compressed pages) every
10 seconds, checks that the library stays virtualized with one full snapshot,
and allows at most 64 MiB growth after warmup. Use BENCH_SECONDS=300 for a
five-minute run. Keep the profiling window visible. No user library, audio, or
network services are used; the server binds to loopback. The standalone Swift
profiling adapter uses diagnostic WebKit SPI to identify its own child process.
This catches a WebKit allocation problem that JavaScript heap checks miss: updating an inherited progress CSS variable on the player every frame caused rapid WebCore memory growth. Progress now changes the fill element's transform directly. With the unchanged 60-second regression, the old frontend grew 220.8 MiB between the first and last samples; the corrected frontend's maximum growth was 3.6 MiB while animation and meter polling continued. The five-minute run finished at 59.3 MiB with a 76.2 MiB highest sampled footprint.
Playback command replies and native notifications use separate delivery paths. Every UI playback snapshot carries its queue revision and native event sequence; the UI rejects older versions before changing transport availability, track metadata, or the elapsed-time anchor. A command remains awaited until completion, but its late reply cannot undo a newer native observation. Discriminated unions describe valid states; version comparisons separately enforce their ordering. The transport browser regression deliberately delivers the ready event before the older transitioning reply. Pure ordering tests cover all 24 permutations of old-queue, transitioning, playing, and paused snapshots, plus delayed updates after Stop. These tests need no audio hardware or timing-dependent sleeps.
Library scrolling
Library navigation includes All songs, Artists, Albums, Decades, Years and named playlists. Browsing stays temporary until a track is selected for playback; Cancel restores the previous committed view. See navigation and playlist behavior.
library_snapshot reads the whole library in one SQLite transaction and returns
typed numeric columns, references into a deduplicated UTF-8 string pool, and the
initial display order. The UI keeps that snapshot resident and materializes only
visible rows. Music-file paths stay native; the UI receives a format label and
deduplicated cover paths. library_order returns four bytes per matching row when
search or sorting changes. Large scroll jumps reuse the existing DOM slots.
Run pnpm exec playwright install webkit once, then pnpm test:webkit for
WebKit touch-feedback, native scrolling, row-reuse, and title-paint regressions.
These desktop engine tests do not certify iPhone frame timing or UIKit scroll
indicator behavior. A sub-millisecond catalog lookup excludes DOM updates,
layout, image decoding, painting, and compositor presentation.
A persisted revision changes in the same transaction as library metadata. Imports notify the UI after finishing, including partial failures with committed batches. An order request with a stale revision triggers a new snapshot. Resuming the app also checks through an order request, recovering missed update notifications. Search changes during loading retain the snapshot and coalesce to the latest selection; imports discard responses from the old library revision.
Date added continues to use the earlier of filesystem creation/birth time and
modification time. If only one is available, it supplies the date; if neither is
available, the first import time is retained across refreshes. Albums use the
latest of their tracks' dates, while disc/track order remains ascending. Creation
time here means birth time, not Unix inode-change ctime.
The metadata snapshot needs one rowid-ordered table scan, with no new metadata index. Existing date-added/release-date covering indexes and FTS still supply display orders. The current schema includes facet, queue, playback, scrobble, and diagnostic tables. Tests and benchmarks use the same binary snapshots, orders, and section queries as the application; there is no JSON page/catalog API.
For the 9,705-track Linux library, the binary snapshot is 908,128 bytes and takes 16.7 ms to build (warm p95). An unfiltered order response is 38,852 bytes and takes about 0.8 ms. The Chromium probe measured 8.6 ms p95 for distant-jump JavaScript and synchronous layout work after DOM reuse, down from 16.6 ms. Its maximum was 21.2 ms; it uses placeholder covers and does not measure presentation. These are desktop measurements, not an iPhone frame-rate guarantee. Full results and limitations are in the recorded benchmark.
To reproduce on a scratch copy of a library database:
pnpm bench:queries --database /path/to/app.sqlite3 --samples 25 \
--snapshot-output /tmp/nmrav-snapshot.bin
pnpm build
node tests/snapshot-render-bench.mjs /tmp/nmrav-snapshot.bin
The native benchmark opens the source read-only and uses VACUUM INTO to create
its scratch database. The binary export contains actual library metadata and
cover paths; only aggregate results belong in the repository. The wire layout
and shared Rust/TypeScript fixture are described in
the snapshot format.
Mobile boundary
The Rust parser/database/import architecture is shared by desktop, Android, and iOS.
Mobile document pickers can yield scoped URLs, Android content:// descriptors,
provider streams, or cloud-backed documents. The parser therefore depends only on
ReadAt; the ordinary-file implementation is a fixed 4 KiB positional-read cache,
and a future mobile provider needs to implement only that small interface.
The official Tauri dialog plugin does not support directory picking on Android or
iOS. On iOS, the app instead scans its own Documents directory, stores paths relative
to the stable documents root, and exposes that directory through Finder/Files using
UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace. Desktop folder import
continues to use the native picker. Android library acquisition still needs its own
document-tree/media adapter and is deliberately not hidden behind a fake path.
Android can be initialized/run from Linux:
pnpm tauri android init
pnpm tauri android dev
pnpm tauri android build --debug --apk --target aarch64
iOS builds require macOS and Xcode:
pnpm tauri ios init
pnpm tauri ios dev --open
pnpm tauri ios dev --open --host # physical device
pnpm tauri ios build
The repository already contains the local Swift player package and the iOS plist
settings. On the Mac, launch Xcode through ios dev --open and keep that command
running while using the IDE. The generated build phase loads the user's zsh environment
so Xcode can find shell-managed pnpm, Node, and Cargo installations.
Use Xcode or xcodebuild for verification while that IDE session is open. A
separate pnpm tauri ios build replaces Tauri's temporary server-address file
for this app, leaving the IDE pointed at an exited helper when the standalone
build finishes. If switching to standalone builds, start a fresh
pnpm tauri ios dev --open session before returning to Xcode.
Run the package's
NmravPlayerTests target first; it decodes the
same command fixture serialized by the Rust test suite, encodes the native snapshot
fixture parsed by Rust, and locks the callback event numbers. Every iOS app startup
also sends every native command variant through the real Tauri plugin bridge for Swift
to decode and compare. Swift then sends a sentinel packet through the real scalar
callback, and Rust checks every received field before starting the event worker. A
bridge, symbol, JSON shape, integer-width, signedness, or field-order mismatch therefore
fails startup. Then select a development team in Xcode, run on a
simulator/device, copy M4A files into the app's Documents directory, press Refresh
Documents, and tap a track. Xcode execution is the remaining gate for Swift
type-checking, signing, and actual AVFoundation behavior.
On every iOS or Linux process launch, Rust restores the persisted queue and asks the native player to seek to the last checkpoint while paused. Launch never resumes playback by itself; playback resumes only after an explicit user or system play request.
Queue revision identifies the ordered queue, not the audio object currently producing sound. Changing shuffle/repeat/end policy intentionally advances the revision and can give the current track a different ordinal. Every platform adapter must compare the incoming current track ID with the active native item and, when they match, retain that item and replace only the upcoming queue. It must not pause, seek, reload, or rebuild the current item merely because revision or ordinal changed. The iOS and Linux adapters implement this rule; the future Android adapter must do the same. Violating it produces an audible discontinuity on every mode change.
The database is created from one current schema. There are no schema upgrades or historical data conversions. Development schema changes require an explicit fresh store; normal launches reopen existing state.
iOS uses MPNowPlayingSession automatic publication, with descriptive metadata on
each AVPlayerItem. AVFoundation supplies the system playback timeline. Diagnostics
in diagnostic_events with source ios_now_playing record automatic_observation:
the reason, sampled audio position/rate, queue identity, callback thread and output route.
Historical manual-publication records also include published position/rate and pending
seek. cleared entries record withdrawal of the player from its Now Playing session.
None of these entries acknowledges what iOS rendered; the held scrub gesture is not recorded.
Correlate event_monotonic_ms, revision, and native sequence with the native player events.
The sequence is sampled without incrementing it. A separate bounded background queue writes
the traces, so row insertion order and the attached Rust state can lag the native callback;
use native timestamps for ordering. droppedBefore is a cumulative loss count for the process,
including queue overflow and storage failures. Diagnostic failure never changes playback.
Periodic audio callbacks continue to checkpoint native playback in Rust. The application
does not manually write system elapsed time or playback rate. Native protocol version 5
reports interrupted seeks as seek_cancelled, with the matching request ID and actual
position, and desktop Stop as stop_requested, distinct from Pause. See the
playback contract and Simulator test commands
for the bounded event-order tests, production-controller host and remaining system-display
verification requirements.
Playback intents, native observations and effect completions enter one Rust coordinator. It prepares and admits commands in one order; a separate worker awaits native effects while observations continue to be reduced. Seek effects target the captured AVPlayerItem, and requested positions remain distinct from observed audio positions. The execution protocol records the invariants, exhaustive finite models, regression evidence and assumptions behind this ordering.
Audio-session category/activation/deactivation calls run on one background serial worker;
AVPlayer and Now Playing updates remain on main. Apple's AVAudioSession.h documents
setActive as blocking and warns against using a thread that cannot tolerate a long
block. The controller has one in-flight operation and one replaceable latest request,
including its continuation, rather than queuing setup/teardown for every tap. Requests
before the worker starts also coalesce. Completions resolve the latest intent on main,
so an obsolete activation cannot start playback after Stop. An Apple call already in
progress cannot be cancelled; a subsequent Play may wait for that call and activation,
but does not replay the intervening taps. Stop's rewind is cancelled/retired by newer
transport or queue work, and its callback cannot mark newer playback stopped.
ios_audio_session diagnostics record set_category, activate, and deactivate
individually, with monotonic start time, elapsedMs, callback thread, and a tagged
success/error-code outcome. These use the existing bounded diagnostic worker and loss
counter without changing playback state. The elapsed time excludes tracing and main
queue delivery; database insertion time is not the duration of the platform call.
Deterministic regressions cover 2,001 rapid intents, in-flight and delayed-completion
races, failure handling, and obsolete Stop rewinds. The synchronous baseline failed
the caller-responsiveness regression before the scheduler change; the unchanged test
passes with background dispatch and latest-intent coalescing.
Current scope
Metadata import is M4A-only. FLAC, MP3, the Android document provider, and native Android audio remain future work. Scrobbling storage/HTTP and the cross-platform playback state machine are implemented. Linux uses GStreamer/MPRIS; the Swift adapter still requires its macOS Xcode verification pass.