Self-hosted neural audio analysis for Emby — a privacy-first equivalent of Plexamp's Sonic Analysis. Maps a music library into a multi-dimensional "sonic space" using audio embeddings (not genre tags), enabling sonically intelligent discovery: similar tracks/artists/albums, track radio, sonic adventures, auto-curated mixes, and a Guest DJ.
Status: Phase 1 (Python analysis service) and Phase 2 (C# Emby plugin) complete. Phase 3 (Android app, liquidWave) is well advanced and running on real hardware — browse, Media3 playback, sonic mixes (per-mix refresh), crossfade with artwork cross-dissolve, an in-app equalizer, Track Radio, Sonic Adventure, the Artist Mix Creator, Stations, Recent Plays, offline playlist downloads, per-track volume normalisation, and search across music + audiobooks. See
docs/spec.mdfor the full architecture and milestone list, anddocs/faq.mdif something isn't behaving.
Emby Server (existing)
└── Emby Plugin (C#, config UI + scan trigger — Phase 2 ✅)
└── Emby Sonic Coordinator (FastAPI, :8765 — Phase 1 ✅)
├── SQLite — metadata, analysis state, playlists
└── FAISS — 128-dim cosine similarity index
Any LAN machine (e.g. a GPU box)
└── Analysis Workers — claim tracks, stream audio from Emby, embed, report back
Workers can run on the Emby host or on any networked machine. They stream audio directly from Emby's HTTP API — no file shares or special network config needed.
The coordinator also serves a browser app (PWA) at http://<host>:8765/app for
non-Android users (iPhone/iPad/desktop). It mirrors the liquidWave app layout —
a home screen with Recent Plays, Stations (Library Radio, Random Album, Decade
Radio, Genres, Sonic Adventure, Artist Mix Creator) and a Sonic Mixes shelf, a
Library tab (Artists A–Z with album/track drill-down, Playlists with
play/shuffle/delete/remove-track), Search (Tracks/Albums/Artists filter chips),
a Mixes tab, and Settings (account + live analysis status, with Log out moved
there rather than a stray top-bar icon). Playback runs through a mini player +
Now Playing overlay (seek, shuffle/repeat, stop) with browser/lock-screen media
controls, and any queue — an artist mix, radio, or a sonic mix — can be saved
back to a real Emby playlist. A "Build Sonic Mixes" trigger in the Mixes tab
handles the initial clustering too, so the web app is a complete standalone
client — no Android device needed to create mixes on a fresh coordinator.
Audiobooks remain Android-only for now; the web app is otherwise close to
feature parity for music.
New installs should start with the scenario guide:
docs/quickstart.md.
- Python 3.11+ (tested on 3.12)
- Emby Server with API key
On Windows, the repository wrapper creates a Python 3.12 virtual environment, installs the CPU development dependencies, and runs the same Python and web-app checks as CI:
.\dev.ps1 bootstrap # first run, or repair/recreate .venv
.\dev.ps1 test # subsequent full test runs
.\dev.ps1 check # environment/import diagnostics onlyThe wrapper uses .venv\Scripts\python.exe directly, so a global python
command does not need to be on PATH. Install Python 3.12 first if the wrapper
reports that py -3.12 is unavailable.
For Linux, macOS, containers, or a manual installation:
# CPU-only PyTorch (recommended for Emby hosts; workers can use GPU separately)
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
cp .env.example .env # set EMBY_URL and EMBY_API_KEY
python main.py # coordinator on http://0.0.0.0:8765GPU workers (bare metal, not Docker): install a CUDA build of torch instead
of the CPU wheel above, e.g. pip install torch --index-url https://download.pytorch.org/whl/cu126 (use cu126 for older/pre-Ampere GPUs,
or check pytorch.org for the current
stable cuXXX index). Verify with python -c "import torch; print(torch.cuda.is_available())"
before running worker.py — a plain pip install torch on Windows commonly
resolves to the CPU wheel.
PANNs CNN14 checkpoint + labels (~327 MB) — auto-downloaded by workers on
first use (stdlib urllib, cross-platform), including the AudioSet labels CSV
that panns_inference itself would otherwise try (and, on Windows/NAS, fail)
to fetch via wget. No manual step needed. To pre-place or relocate the
checkpoint, set PANNS_CHECKPOINT_PATH (default ~/panns_data/Cnn14_mAP=0.431.pth);
an existing file is reused.
The coordinator is lightweight (no torch/librosa/panns — those live on workers), so it runs in a small container on any NAS (Synology, QNAP, UGREEN, ...), including ARM. Audio analysis runs on separate workers on a machine with spare CPU/GPU.
EMBY_URL=http://<emby-host>:8096 EMBY_API_KEY=<key> docker compose up -d --buildThis builds Dockerfile.coordinator and starts the
coordinator on port 8765 with a persistent emby-sonic-data volume (SQLite +
FAISS index). Then point the Emby plugin's Python Service URL at
http://<nas-host>:8765 and run one or more workers on your GPU/CPU box — they
stream audio from Emby, so no file shares are needed.
Prebuilt images: coordinator and worker images are published to GHCR on every release. Set
COORDINATOR_IMAGE/WORKER_IMAGEand drop--buildto pull instead of build. The guided./install.shdoes this automatically.
python benchmark.py /path/to/a/track.flacReports per-stage timing + real-time factor. Expect ~10–15s/track with GPU workers, ~20–30s/track CPU-only.
# 1. Sync library from Emby (populates the queue; no audio work yet)
curl -X POST http://localhost:8765/sonic/library/scan \
-H "X-Emby-Token: <your-emby-token>"
# 2. Run a worker (on this machine or any other on the LAN)
COORDINATOR_URL=http://<coordinator-host>:8765 \
WORKER_SECRET=<worker-secret> \
WORKER_ID=my-worker \
python worker.pyWorkers authenticate to the coordinator with WORKER_SECRET when it is set, or
fall back to EMBY_API_KEY for older deployments. They auto-detect CUDA. Run
multiple workers in parallel for faster scanning.
Music libraries only. The scan is scoped to Emby libraries whose collection type is
music— audiobooks (a separateaudiobookslibrary) and other audio are never analysed, so spoken-word content can't pollute Track Radio / Similar / Adventure. If an earlier scan already embedded audiobooks, clean them out withpython tools/purge_audiobooks.py(dry-run by default;--applyto delete; rebuilds on the next coordinator restart).
Some playback features read a per-track measurement the worker now takes during analysis. Tracks embedded before that feature have nothing to read, so they fall back to the old behaviour. These scripts fill them in without re-embedding — CPU-only, the neural model never loads, and resumable (only rows still missing the value are touched, so stopping and re-running is safe):
python tools/backfill_loudness.py # volume normalisation (integrated LUFS)
python tools/backfill_edges.py # crossfade edge trimming (effective start/end)On Docker, run these as a one-off WORKER container on the coordinator's host, with the data volume attached — not the coordinator, and not a separate worker box (#40). They need librosa and the database, and no single container has both by default: the coordinator image deliberately ships without librosa (that split is what keeps it small and ARM-buildable), while the worker has librosa but doesn't normally mount the DB. And because a Docker named volume is local to its host, this must run wherever the volume lives — i.e. the coordinator's host, even if your long-running worker is elsewhere:
docker compose run --rm -v emby-sonic-data:/app/data \ worker python tools/backfill_edges.pyBare metal: run them on the coordinator's host, wherever you installed the full
requirements.txt(notrequirements-coordinator.txt), pointing--dbat the database.
Measured on an Intel N100 streaming from Emby over LAN, backfill_edges.py runs
at ~42 tracks/minute (~1,000 tracks ≈ 25 min, ~25,000 ≈ 10 hours) — it
decodes each file in full, since a track's edges are precisely what the analyser's
sampled windows skip. backfill_loudness.py is quicker (sampled windows only).
Both skip audiobooks where the feature doesn't apply, and both are safe to run
against a live coordinator. Newly-analysed tracks get these from the worker
automatically — these scripts are only for the back catalogue.
Tracks that fail analysis are left with analysis_status='error' so workers do
not retry the same stale/missing file forever. Export them for library cleanup,
or requeue them after fixing files in Emby:
# Export id,title,artist,album,file_path,error for all broken tracks.
python tools/broken_tracks.py --db data/sonic.db export --output broken_tracks.csv
# Requeue selected tracks from a prior export CSV.
python tools/broken_tracks.py --db data/sonic.db requeue --csv broken_tracks.csv
# Or requeue every error row.
python tools/broken_tracks.py --db data/sonic.db requeue --all
# Permanently delete broken tracks instead — for ones that will never
# succeed (a corrupt file, or a stale/orphaned Emby library entry with no
# real file behind it). Same --all/--id/--ids-file/--csv selection as requeue.
python tools/broken_tracks.py --db data/sonic.db purge --allRequeue changes analysis_status from error to pending, clears the claim and
error text, and lets the next running worker retry the track. Purge deletes the
row outright, so it stops showing up in the skipped-tracks list; if the same
Emby item still genuinely exists, the next library scan just recreates a fresh
pending row for it.
broken_tracks.py only sees rows that already failed. Replacing files leaves
a different mess: the old rows succeeded, so they sit there analysed and indexed,
and a mix can serve the same recording twice under two ids. Find those with:
# Report rows whose Emby item no longer exists. Dry run — writes a CSV,
# never touches the database.
python tools/find_orphans.pyIt pulls Emby's full audio id list in a few paged calls and does a set difference, rather than checking every id one at a time, and refuses to report anything if that fetch returns implausibly few ids — otherwise a failed connection would mark your whole library as orphaned.
Deleting is deliberate and separate. Back up data/sonic.db first, then remove
the ids from tracks and embeddings, and afterwards:
-
Restart the coordinator — it rebuilds the FAISS index from the database on startup, so the index needs no separate attention.
-
Rebuild your mixes. Existing mixes still reference the deleted tracks in
mix_tracks. Nothing breaks — the mix endpoint skips tracks it can't resolve — but those mixes come back short until rebuilt:curl -X POST http://<coordinator>:8765/sonic/library/build-mixes \ -H "X-Emby-Token: <token>"
A bare python main.py in a terminal has nothing supervising it — close the
window, let the box sleep, or hit an unhandled exception, and the coordinator's
listening socket is just gone until you restart it by hand. The Emby plugin
then shows "Service: offline" and workers get connection-refused errors
posting results. On Windows, install it as a supervised, auto-restarting
scheduled task instead:
# Run elevated, on the box that should host the coordinator:
./deploy/coordinator-install.ps1Runs as SYSTEM, starts at boot, restarts automatically if it dies, no console window. (Docker deployments don't need this — see "Deploy on a NAS" above, which already runs the coordinator as a supervised container.)
Inspect or safely restart either Windows task with the shared operations tool:
# Read-only task, process, and health status:
./deploy/service-control.ps1 -Service coordinator -Action status
./deploy/service-control.ps1 -Service worker -Action status
# Preview the exact stop/process-sweep/start operations:
./deploy/service-control.ps1 -Service coordinator -Action restart -WhatIf
# Perform the restart (run elevated on the service host):
./deploy/service-control.ps1 -Service coordinator -Action restartThe generated launcher supervises a child Python process that Task Scheduler
does not stop itself. The operations tool identifies that child using the exact
repository launcher, configured Python executable, and service entry point
before restarting, then waits for coordinator HTTP readiness or a live worker
process. Avoid restarting these tasks with Stop-ScheduledTask and
Start-ScheduledTask directly, which can leave the child behind.
For hands-off operation, install the worker as an OS service so newly added tracks are analysed without running anything manually. The Emby plugin already triggers a library scan on every add; the worker then drains the queue on its own.
Windows scheduled task:
# On the coordinator / server box — always-on worker (run elevated):
./deploy/worker-install.ps1 -Mode service
# On a separate GPU desktop you also use — only runs while the machine is idle:
./deploy/worker-install.ps1 -Mode idle -CoordinatorUrl http://<coordinator-host>:8765service mode runs as SYSTEM, starts at boot, and restarts if it dies. idle
mode runs only when the machine is idle and stops the instant you return, so it
never fights your foreground work. Both can run together on different machines.
The script auto-detects the repo + .venv, writes the run wrapper, and pins
USERPROFILE so panns_inference finds its model under SYSTEM. See
./deploy/worker-install.ps1 -? for all options.
Linux systemd service:
# On the coordinator / server box:
sudo ./deploy/worker-install.sh
# On a separate GPU/CPU worker box:
sudo ./deploy/worker-install.sh --coordinator-url http://<coordinator-host>:8765The Linux installer renders deploy/emby-sonic-worker.service
into /etc/systemd/system, runs from the repo venv, restarts on failure, starts
at boot, and reads EMBY_URL / EMBY_API_KEY from the repo .env.
Coordinator-only NAS deploy:
EMBY_URL=http://<emby-host>:8096 EMBY_API_KEY=<key> docker compose up -d --build coordinatorCoordinator plus worker in Compose:
EMBY_URL=http://<emby-host>:8096 EMBY_API_KEY=<key> docker compose up -d --buildThe Compose worker service builds the full Dockerfile, runs
python worker.py, and stores the CNN14 checkpoint in the named
emby-sonic-panns volume mounted at /root/panns_data, so the ~327 MB model is
not redownloaded on every container rebuild/restart. Workers stream audio from
Emby; no music library bind mount is needed.
Worker images are CPU-only by default. To build a GPU-capable worker, set
TORCH_VARIANT before building: cuda for CUDA 12.8+ (modern GPUs), cu126
for CUDA 12.4 (older / pre-Ampere GPUs). The guided ./install.sh detects your
CUDA version and picks the right image automatically.
# Keep the coordinator running normally.
EMBY_URL=http://<emby-host>:8096 EMBY_API_KEY=<key> docker compose up -d --build coordinator
# Build a CUDA-capable worker image and run it with the NVIDIA runtime.
TORCH_VARIANT=cuda docker compose build worker
EMBY_URL=http://<emby-host>:8096 EMBY_API_KEY=<key> \
docker compose run --rm --gpus all workerFor a detached GPU worker, uncomment gpus: all under the worker service in
docker-compose.yml, then run:
TORCH_VARIANT=cuda EMBY_URL=http://<emby-host>:8096 EMBY_API_KEY=<key> \
docker compose up -d --build workerThe startup log is the proof: docker compose run --rm --gpus all worker prints
[worker <id>] device=cuda to the terminal. For a detached worker, check
docker logs emby-sonic-worker. If it says device=cpu, confirm the image was
built with TORCH_VARIANT=cuda and that nvidia-smi works inside a test
container.
Prebuilt images: published to GHCR on every release —
…-worker:latest(CPU),:cu126(CUDA 12.6, older GPUs), or:cuda(CUDA 12.8+). SetWORKER_IMAGEand drop--buildto pull instead of build.
Standalone worker container:
docker build -t emby-sonic-worker .
docker run -d --name emby-sonic-worker \
-e COORDINATOR_URL=http://<coordinator-host>:8765 \
-e EMBY_URL=http://<emby-host>:8096 \
-e EMBY_API_KEY=<key> \
-v emby-sonic-panns:/root/panns_data \
emby-sonic-worker python worker.pyStandalone NVIDIA GPU worker (use cu126 instead of cuda for older / pre-Ampere hardware):
docker build --build-arg TORCH_VARIANT=cuda -t emby-sonic-worker:cuda .
docker run -d --name emby-sonic-worker-gpu --gpus all \
-e COORDINATOR_URL=http://<coordinator-host>:8765 \
-e EMBY_URL=http://<emby-host>:8096 \
-e EMBY_API_KEY=<key> \
-v emby-sonic-panns:/root/panns_data \
emby-sonic-worker:cuda python worker.py
docker logs emby-sonic-worker-gpu | grep 'device='Besides your own Emby server, the containers make outbound calls to a small, fixed set of domains — useful if you run a firewall/router that flags new outbound traffic:
| Domain | Called by | Why |
|---|---|---|
pypi.org, pythonhosted.org |
build time (pip install) |
Resolving/downloading the Python dependencies in requirements.txt / requirements-coordinator.txt. Not called at runtime once built. |
download.pytorch.org |
build time (pip install torch) |
The CPU/CUDA PyTorch wheel, per Dockerfile's TORCH_VARIANT install step. |
zenodo.org |
worker, first run only | One-time download of the ~327 MB PANNs Cnn14_mAP=0.431.pth checkpoint (pretrained AudioSet audio-tagging model) from its author's own hosting. Cached in the emby-sonic-panns volume — see above — so it should only be fetched once per volume, not on every restart. |
ghcr.io |
anyone using prebuilt images | Pulling the published coordinator/worker images instead of building locally. |
If zenodo.org is being called repeatedly rather than once, the checkpoint
isn't persisting — check that -v emby-sonic-panns:/root/panns_data (or the
Compose equivalent) is actually mounted and that the volume hasn't been
recreated (e.g. by docker compose down -v).
All user-facing routes are under /sonic and require an X-Emby-Token header
(validated against Emby's /System/Info). Worker routes use X-Worker-Token.
| Endpoint | Method | Description |
|---|---|---|
/sonic/auth/login |
POST | Browser login proxy to Emby's AuthenticateByName |
/sonic/search/tracks |
GET | Authenticated browser track-search proxy to Emby |
/sonic/status |
GET | Analysis progress + library stats |
/sonic/tracks/{id}/similar |
GET | Sonically similar tracks |
/sonic/tracks/{id}/radio |
GET | Track radio playlist |
/sonic/adventure |
POST | Mood-transitioning playlist A→B |
/sonic/mixes |
GET | Auto-curated mixes (named by sonic character + dominant artist) |
/sonic/mixes/{id} |
GET | One mix with its tracks |
/sonic/mixes/{id}/regenerate |
POST | Refresh one mix: full track turnover from its stored centroid, on-theme |
/sonic/queue/inject |
POST | Guest DJ queue injection |
/sonic/artists/{id}/similar |
GET | Similar artists |
/sonic/albums/{id}/similar |
GET | Similar albums |
/sonic/library/scan |
POST | Trigger library sync |
/sonic/library/build-mixes |
POST | Rebuild auto-curated mixes (k-means) |
/sonic/library/build-state |
GET | Report whether a mix rebuild is running |
Interactive docs at http://<host>:8765/docs once running.
Set via environment variables or a .env file:
| Variable | Default | Description |
|---|---|---|
EMBY_URL |
http://localhost:8096 |
Emby server URL. Use the LAN address (e.g. http://192.168.1.50:8096) even if Emby runs on the same host as the coordinator — not localhost/127.0.0.1. The web app hands this to the browser to fetch music directly, so a loopback address makes every browser look for Emby on itself: login works, library is empty |
EMBY_URL_EXTERNAL |
(blank = same as EMBY_URL) |
Emby's publicly-reachable address (FQDN/reverse proxy). Only needed for the web app: it streams audio browser→Emby directly, so a LAN-only EMBY_URL fails for anyone loading the page over WAN. Set this and the web app picks whichever address the browser loaded the page from |
EMBY_API_KEY |
(required) | Emby API key for coordinator admin calls and worker audio downloads |
WORKER_SECRET |
falls back to EMBY_API_KEY |
Shared secret required in X-Worker-Token for worker routes |
AUTH_CACHE_TTL_SECONDS |
30 |
Seconds to cache successful Emby user-token validation by SHA-256 digest; 0 disables caching |
AUTH_CACHE_MAX_ENTRIES |
1024 |
Maximum successful token digests retained in the in-process validation cache; 0 disables caching |
HOST |
0.0.0.0 |
Bind address |
PORT |
8765 |
Bind port |
NUM_WINDOWS |
3 |
Windows sampled per track (speed/quality knob) |
WINDOW_SECONDS |
30 |
Duration of each analysis window |
EMBEDDING_DIM |
128 |
PCA target dimensionality |
A .NET 8 plugin (plugin/) that adds an Emby dashboard config page (set the
coordinator URL, view live analysis status — including a "skipped tracks" list
with the reason each couldn't be analysed — and trigger scans / mix rebuilds) and
fires an incremental scan when tracks are added to the library.
Requires Emby Server 4.8 or newer. .NET assembly binding only resolves "upward", so a plugin must be built against an Emby SDK no newer than the target server or Emby silently skips it (it never appears on the Plugins page). The shipped builds target the 4.8 SDK, so they load on 4.8 / 4.9 stable and the 4.10 beta alike. If you build it yourself, use the oldest Emby you want to support — see the note in
plugin/EmbysonicPlugin.csproj.
Build (requires the .NET 8 SDK and Emby's SDK DLLs in plugin/lib/ —
MediaBrowser.Common.dll, MediaBrowser.Controller.dll, MediaBrowser.Model.dll,
copied from your Emby install's system/ folder; they are not redistributable):
cd plugin
./build.ps1 # Release build + dist/EmbysonicPlugin_<version>.zipInstall: Emby (unlike Jellyfin) has no plugin-zip upload in its dashboard, so
the plugin is installed by placing its DLL in Emby's programdata/plugins/ folder.
The release zip ships install scripts that do this for you — run on the Emby host:
./install.ps1 # Windows: auto-detects Emby, copies DLL, restarts./install.sh # Linux: auto-detects Emby plugins dir, copies DLLOr copy EmbysonicPlugin.dll into …/Emby-Server/programdata/plugins/ manually
and restart Emby. The plugin then appears under Dashboard → Plugins → Emby Sonic.
The plugin talks to the coordinator over HTTP; run the coordinator wherever it's convenient (same host or another LAN machine) and point the plugin's config page at its URL.
A Kotlin / Jetpack Compose app (android/, package guru.liquid.embysonic,
minSdk 26). Browse/stream/auth go to the Emby API directly; sonic features go
to the coordinator. Stack: Compose + Hilt + Retrofit/OkHttp (two clients) +
Media3 ExoPlayer + DataStore.
Features: library + audiobook browse with A–Z fast-scroll; a library picker
when your server has more than one music or audiobook library (switch via a
dropdown on the Library screen — the choice is remembered across restarts and
also scopes Home, Search, and Artist Mix); Now Playing with
queue, shuffle/repeat, mini player, and a system media notification; durable
audiobook resume, with back/forward skip buttons (15s/30s by default, configurable)
and a draggable progress bar — long-form plays the original file directly wherever
the phone can decode it, so seeking is instant and audiobooks never transcode;
music crossfade with a synced artwork cross-dissolve; an in-app
equalizer (presets + per-band, also broadcasts its session for external EQ apps);
auto-curated sonic mixes (per-mix refresh, save as playlist); Track Radio; Sonic
Adventure (a sonic journey from one track to another); the Artist Mix Creator
(pick a set of artists — the grid suggests sonically similar ones as you go —
then build a shuffled cross-artist mix, sized by the shared "tracks per mix"
setting); Stations (Library / Random Album / Decade radios); Recent Plays; offline
downloads (download a playlist or a whole audiobook's original source files for
browsing and playback with no network — audiobooks keep durable resume across the
offline→online boundary; Wi-Fi-only by default; managed under Settings → Downloads;
a foreground service keeps a download alive if the app is backgrounded, with a
progress notification and a download-complete notification when it finishes);
per-track volume normalisation (levels playback to a consistent loudness using
the coordinator's measured LUFS — a GainAudioProcessor in the audio sink, toggle
in Settings, on by default); a configurable offline prefetch buffer (Settings →
Offline prefetch, 3/5/10/15 tracks ahead) to ride through signal drops; a
responsive Now Playing that switches to compact chrome in short panes
(split-screen, AppPair) so transport controls stay visible; a Dynamic theme
that follows the system light/dark setting (in the app and the Now Playing
widget), alongside five fixed dark palettes; Android Auto with a glanceable
Home — Continue listening resumes the last session at the exact position, the
one-tap Stations sit beneath it, and Recent plays and Sonic Mixes open as artwork
grids — plus Library and Audiobooks tabs, and shuffle/repeat controls mirrored in
the notification shade; and search across music (tracks/albums/artists),
audiobooks (books/authors), or everything from Home.
![]() Home |
![]() Sonic Mixes |
![]() Track Radio & Similar |
![]() Sonic Adventure |
![]() Artist Mix Creator |
![]() Now Playing |
![]() Audiobooks |
![]() Offline downloads |
Requires the Android SDK (platform android-36) and JDK 17 (Android Studio's bundled JBR works).
cd android
JAVA_HOME="<path-to-jdk17>" ./gradlew :app:assembleDebug
# APK: android/app/build/outputs/apk/debug/app-debug.apkRelease builds are R8-minified and resource-shrunk. They are signed with the
release key only if the four properties below are present; if they are
absent, assembleRelease silently falls back to the debug signing config so
contributors can still produce an installable APK without the keystore. Check
what you actually got before distributing a build:
apksigner verify --print-certs android/app/build/outputs/apk/release/app-release.apk
# "CN=Android Debug" means the fallback was used — NOT a real release signature.Put these in ~/.gradle/gradle.properties or export them as environment
variables; never commit the keystore or passwords:
LIQUIDWAVE_RELEASE_STORE_FILE=/absolute/path/to/liquidwave-release.jks
LIQUIDWAVE_RELEASE_STORE_PASSWORD=...
LIQUIDWAVE_RELEASE_KEY_ALIAS=...
LIQUIDWAVE_RELEASE_KEY_PASSWORD=...cd android
JAVA_HOME="<path-to-jdk17>" ./gradlew :app:assembleRelease
# APK: android/app/build/outputs/apk/release/app-release.apkThe debug APK is fine for real use (debug vs release doesn't affect audio).
USB: enable Developer options → USB debugging, plug in, then:
adb install -r android/app/build/outputs/apk/debug/app-debug.apkWireless ADB (no cable; Android 11+): on the phone, Developer options → Wireless debugging. Pair once, then connect and install — after pairing, only the port changes between sessions:
# one-time pairing (use the IP:port + 6-digit code from "Pair device with pairing code")
adb pair <phone-ip>:<pair-port> <code>
# then each session (IP:port from the main Wireless debugging screen):
adb connect <phone-ip>:<connect-port>
adb -s <phone-ip>:<connect-port> install -r android/app/build/outputs/apk/debug/app-debug.apkOn first launch, enter your Emby server URL + credentials and the coordinator URL in the login screen. The phone must be on the same LAN as Emby and the coordinator.
Installed from Google Play: nothing to do — liquidWave appears in Android Auto's app list like any other media app.
Sideloaded APK: Android Auto hides apps that didn't come from the Play Store. Enable developer mode and allow unknown sources — once, on the phone:
- Android Auto settings → tap the Version repeatedly to unlock Developer settings.
- Developer settings → enable Unknown sources.
liquidWave then appears in Android Auto's app list.
MIT — see LICENSE. Free to use, modify, and distribute.
If you run Emby and want to try liquidWave, see the tester quickstart — it walks through standing up the coordinator, installing the plugin, running a worker to analyse your library, and installing the Android app.








