* feat(windows): leaner portable package and opt-in in-app updater (#421) Issue #421 asked for Python embedded in a single EXE so updating would not mean copying ~20k loose files over an existing install. A onefile EXE is not viable for this stack (onefile modes re-extract the whole multi-GB payload on every launch, and torch/onnxruntime fight frozen-import hooks), so this addresses the root cause instead: ship less, and stop making users hand-copy a full zip for a release that only changed app code. Leaner package (make-portable.ps1): - Stripping is now unconditional. The -StripVenv opt-in gate was a silent regression risk: nothing stopped a future workflow edit from shipping the unstripped venv with no error. - Also strips stdlib base/Lib/test and per-package test/tests dirs. - Deliberately does NOT strip .dist-info/RECORD. pip needs it to replace a package, and install_cuda_torch pip-installs into this venv on every NVIDIA machine at first run; removing it yields "Failed to uninstall ... missing RECORD file". - Adds a post-strip import check so an over-aggressive strip fails the build rather than a release. Updater (main.rs, catalog.js): - New commands installed_runtime_id, download_app_update, apply_app_update. - Opt-in: the check on launch is unchanged, but download and apply are each an explicit click. It never auto-applies and never interrupts a running job. - Replaces StemDeck.exe and backend/ only. python/ is never touched, because an NVIDIA install rewrites it with CUDA torch at first run and torchDeviceSettled skips ensure_torch_device once the device is cuda, so swapping the directory would silently drop that machine to CPU with no recovery. - The runtime id (uv.lock + interpreter major.minor) is a compatibility gate, not a download trigger: if a release changed the Python dependency set the updater stands down and points at the full download. Only 19 of the last 200 commits touch uv.lock, so the fast path covers most releases. - apply_app_update stages and validates everything before any destructive rename, stops the backend synchronously first (the existing stop_backend returns before the process dies, which would have made every update fail on Windows), and retries renames past transient AV/indexer handles. - Known gap, documented in code: the two exe renames are not atomic. A hard crash in that window leaves StemDeck.exe.old needing a manual rename. Closing it needs a bootstrap launcher that is never itself replaced. CI publishes -app.zip, its .sha256 and -runtime-version.json alongside the unchanged full zips. Fresh installs are unaffected. i18n: the 5 new strings are translated into all 7 language tables, not just English. t() falls back to English silently, so an English-only key looks correct in testing and ships untranslated to six locales. Verified: Windows and Linux (WSL) both compile clean with no new clippy warnings, 39 Rust tests pass on both, JS suites pass, ruff clean. Two new unit tests pin the JSON contract between the PowerShell writer and the Rust reader. Not yet verified: no end-to-end run against a real release. * fix(updater): make the in-app update actually work, verified end to end (#421) Built both packages on a real Windows box and drove the whole flow. Four bugs that only surfaced by running it, none of which static checks could see. 1. Stale version after updating. app_version() read installed dist metadata, which lives in python/ -- the directory the updater deliberately never replaces. A self-updated install kept reporting the old version and would re-offer an update it had already applied, forever. It now prefers the app layer's static/version.json, which moves with backend/. Gitignored, so Docker and source checkouts still fall through to the hatch-vcs metadata. Proven: after a real update, python/ dist-info says 0.13.0 while /api/health reports 0.13.1. 2. The page CSP blocked the whole feature. The UI is served over http by the Python backend, so its connect-src applies: api.github.com is allowed, github.com and objects.githubusercontent.com are not, and that is where release assets live. Fetching the checksum and runtime id from JS was refused, so the pill would simply never appear. Those two reads moved into Rust (check_app_update), whose HTTP client is not bound by the page CSP, so the policy from #171 stays exactly as tight as it was. 3. plugin:event|listen refused by the Tauri ACL. App-defined commands are not ACL-gated but plugin commands are, and the capability does not cover the remote http origin the UI is served from. The progress bar is now indeterminate instead of granting a remote origin event permissions to put a percentage on a 5 MB download. 4. The post-strip import check re-bloated the package. Running Python regenerated 1,912 files / 39 MB of __pycache__ that the strip had just removed, cancelling nearly all of it: the net saving was 180 files. Swept once after the last interpreter run, and backend/ no longer ships a developer's local __pycache__ either. Also: the *.old sweep now runs on every launch rather than only on a version change. apply_app_update relaunches then exits, so on the first launch of the new build Windows still holds StemDeck.exe.old open, the delete fails silently, and gated on a change that already happened it would never retry. Observed for real: 15.7 MB stranded. Verified swept on the next launch. UI: "Update now" is an accent pill BESIDE Download, not a replacement, so the zip stays one click away and is the escape hatch if an update fails. Measured against the published v0.13.0 package: 18,143 -> 16,056 files (-2,087, -11.5%) and 883 -> 850 MB. The real win for #421 is the update path itself: 5 MB and 123 files instead of 284 MB and 16,056. Verified on this machine: a real 6-stem Demucs separation through the stripped package; the full notify -> Update now -> download -> restart -> relaunch cycle, after which user data (job, 7 stems, 130 MB of models), portable.txt, cpu-only and python/ were all untouched; and the safety gate correctly declining, with no download attempted, when the release's runtime id differs. Not covered: the NVIDIA package was not built, though the risk that motivated the gate is structurally gone now that python/ is never swapped. * fix: address code-quality review on the version-source change (#421) Both findings from the automated review were fair. Narrow the bare `except Exception: pass` in app_version() to (OSError, ValueError, AttributeError). That is bandit B110, which this repo's own security conventions call out. The three cover every real failure here -- absent or unreadable file, invalid JSON or bad encoding, and valid JSON that is not an object so has no .get -- while letting an actual bug in the function surface instead of silently degrading the reported version. Bandit now reports no issues for the file. Use one import style in test_health_api.py so app.main is no longer imported both as `import app.main as main` and `from app.main import app` in the same module. Also added a "[]" case: JSON that parses but is not an object, which is the AttributeError branch the narrowed except now names explicitly. * feat(updater): extend the in-app update to Linux (#421) Linux ships the same shape as Windows -- executable, backend/ and python/ side by side -- so the updater generalises rather than needing a second design. The platform-specific parts are now three small seams: the archive format, the executable name, and one new gate. Rust: - widen the updater's cfg gates from `windows` to `any(windows, linux)`, and replace extract_zip_archive with extract_update_archive, which uses zip on Windows and the existing extract_tar_archive on Linux - APP_EXE_NAME so the swap and the leftover sweep stop hardcoding StemDeck.exe - stop_backend_and_wait now sends SIGTERM and waits before escalating on unix, matching what stop_backend already does on window close - new app_root_is_writable gate: packaging/linux/install.sh offers a global install into /opt/stemdeck, which is root-owned while the app runs as the user. check_app_update declines up front rather than failing part way through a swap. Windows portable installs are user-writable by construction, but the probe is cheap and honest on both. tar rather than zip on Linux is deliberate: it preserves the executable bit. A zip would land StemDeck without +x and the relaunch after an update would fail with a permission error. Packaging (scripts/linux/make-portable.sh): - write python/runtime-version.json using the same uv.lock + interpreter major.minor formula as the Windows script, so the compatibility gate behaves identically on both - bring the strip to parity: stdlib test/, per-package test/tests, a post-strip import check, and a final __pycache__ sweep after the last interpreter run - PUBLISH_UPDATER_ASSETS=1 emits the slim app-layer tarball, its checksum and the runtime marker; wired into the CPU build in linux-release.yml since StemDeck and backend/ are identical between both variants Frontend: updaterAssetNames() maps the platform to its asset names, and the wiring is gated on that rather than on os === "windows". macOS is deliberately still excluded, and the comments now say why rather than just that it is: backend_dir() resolves the backend inside the downloaded runtime pack rather than the .app, so its app layer is a different thing and the existing runtime-pack updater already covers most of it. Verified: both platforms compile clean with no new clippy warnings, 42 tests on Windows and 43 on Linux (the extra one is the read-only-root gate, which is meaningless on Windows). The app-layer archive was round-tripped on Linux to confirm it contains exactly StemDeck + backend/, that python/ does not leak into it, that the executable bit survives, and that replacing a running binary works. Not yet run end to end against a real Linux release. * fix(updater): see pre-releases, and compile the Rust in CI (#421) Two gaps that would each have undermined the update flow on release day. The update check polled /releases/latest, which GitHub defines as the most recent NON-PRERELEASE, non-draft release. Ship a version with the pre-release box ticked and it becomes invisible: no notification, no update button, on any platform, with nothing in the logs to explain it. StemDeck has always published even its alphas as normal releases (v0.8.0-alpha.17 has prerelease=false), which is the only reason this has not bitten yet -- it was a trap waiting on someone ticking a box. Now polls the releases list and takes the newest non-draft, so it is correct either way. Drafts stay excluded: they are already invisible unauthenticated, and a maintainer should not be offered a release whose assets do not exist yet. windows-check.yml and macos-check.yml now also run on pull requests that touch desktop/src-tauri/**, not workflow_dispatch only. This PR added roughly 600 lines of mostly cfg-gated Rust across two commits and every CI check passed without compiling a single line of it; the comment at the top of windows-check.yml notes that exact gap already shipped a broken Windows build in v0.11.1's first release attempt. Scoped by path so the self-hosted runners see no extra load from the majority of PRs, which never go near src-tauri. This also gets the macOS branch compiled for the first time. Local verification covered Windows and Linux, so the cfg(not(any(windows, linux))) arm of the three updater commands has never been near a compiler. * test(e2e): match the releases-list shape the app now polls (#421) The update-check stub returned a single release object, which was right for /releases/latest. The app now polls the releases list so a pre-release is still seen, so the fixture has to return an array or checkForUpdate bails and the release card never appears. Caught by frontend-e2e on the previous commit, which is the suite doing exactly its job: the only assertion that covers this path is report-failure.spec.mjs:98, and it went red immediately. * feat(i18n): add French, and make the runtime id line-ending independent French is a complete table, not a partial one: 435 keys, the same set German and Portuguese carry (English's 443 minus the ten Polish-only .few/.many forms and the bare upload.skippedFiles, plus singular forms for the three playlist.skip.* families). French takes the one/other buckets, so plural() needs no change. Verified with the checks from .claude/rules/i18n.md: the drift check reports clean, and separately there are zero {placeholder} mismatches and zero HTML tag mismatches against English. The 27 strings identical to English are genuinely identical in French (Piano, Solo, Transport, Position, LUFS, Standard, Port, the brand names, CUDA (NVIDIA), MPS (Apple Silicon)). Separately: the runtime id was being computed from the raw bytes of uv.lock, so a Windows checkout with core.autocrlf=true hashed CRLF and Linux hashed LF, and the same lockfile produced two different ids -- caught by building the Linux package and seeing py3.12-d74d6ef80c5e9d1f where Windows had produced py3.12-dbda45e38e1044cf. Each platform stayed self-consistent so the gate still worked, but the id would shift spuriously if a runner's autocrlf ever changed, silently declining app-only updates that were in fact compatible. Both scripts now hash the content with newlines normalised; PowerShell, bash and a reference Python implementation all agree on d74d6ef80c5e9d1f. * chore: pin Unraid template to 0.14.0 Per .claude/rules/unraid-template-version.md this is an explicit decision each time, not a default. Confirmed for this release. The 0.14.0 GHCR image is published by docker-publish.yml when the release is created, so the tag exists shortly after this lands. --------- Co-authored-by: Thales <>
Drop in an MP3, WAV, FLAC, OGG/Opus, MP4, or M4A file, or paste a YouTube URL, and StemDeck splits the audio into up to six stems (vocals, drums, bass, guitar, piano, other). Play them back in a DAW-style multitrack mixer: mute, solo, balance levels, zoom the waveform, loop a region, and export individual stems or a custom mix. Everything runs locally on your own machine.
What is this? StemDeck is a stem separation tool, not a downloader. Its main job is processing audio you already own: drag an MP3, WAV, FLAC, OGG, or M4A onto the import bar and go. YouTube support is a convenience for content you have the right to process. StemDeck does not store, cache, or redistribute any downloaded content. Everything happens locally and nothing leaves your machine.
StemDeck is a free, open alternative to cloud stem-splitters like Moises and LALAL.AI: no account, no quota, no uploads, no subscription. If you want stems for personal study and prefer to keep things local and free, StemDeck has you covered. If you need the polish, a mobile app, or deeper musician tooling, the commercial products are a better fit.
Star History
We Recommend
StemDeck is free and does not accept any money, sponsorship, or funding - not from users, not from anyone listed below. We share these makers and artists purely for the joy of pointing you toward wonderful people doing beautiful work. Go meet them ❤️
| Name | What they do | Link |
|---|---|---|
| Analog4Lyfe | All-analog music gear, no digital shortcuts | @analog4lyfe |
| Beltr | Turns the songs you already own into karaoke gold, right on your own machine, no subscription, no cloud, just you and the mic | beltr.app |
| Dlima Guitars | Custom guitars and basses, built one at a time | @dlimaguitars |
| Empress Effects | Boutique effects pedals for tone chasers who don't settle | empresseffects.com |
| Joao Gaspar | Producer and film scorer, also plays as a touring/session musician | @jay_glaspar |
| Kris Luthier | Hand-repairs and restores instruments in Lisbon, one careful fix at a time | @krisluthier |
| Lisbon Guitar Works | Guitars built by hand in Lisbon | dlimaguitars.com |
| More Notes Less Talk | Instruments and gear with personality, recorded raw to tape. No hype, no gatekeeping. | @morenoteslesstalk |
| Seratone | Turns any TV into a studio-grade karaoke stage | seratone.audio |
| Thomann | One of Europe's largest music gear retailers, practically everything a musician could need | @thomann.music |
Features
6-stem separation via Demucs htdemucs_6s, with auto-detection of the best Torch device (CUDA on NVIDIA, MPS on Apple Silicon, CPU fallback).
YouTube and local file import. Paste a YouTube URL or drop an MP3, WAV, FLAC, OGG/Opus, MP4, or M4A directly onto the import bar.
DAW-style waveform editor with min/max sample rendering across all stems, shared normalization, zoom in/out/Fit, loop drag on the ruler, gold playhead overlay, and stem-aligned lanes.
Stem subset extraction. Click stem chips to choose which stems to keep. Clicking from "all selected" snaps to "only this one"; subsequent clicks add or remove.
"Original" backing track. When you pick a subset, a 7th lane contains the complement (full song minus selected stems), perfect for A/B reference without doubling.
Downloadable selected mix. A single mix.wav of just your selected stems, summed via ffmpeg amix.
Per-stem mixer with volume fader, mute, solo, and "monitor" (solo-only) per stem. State syncs between the preview mixer and the stems sidebar.
Live VU meters per stem. Post-gain RMS via Web Audio analysers with peak hold and slow falloff.
Song analysis including BPM (librosa beat tracker), key, scale, and confidence (Albrecht-Shanahan profiles), integrated LUFS (BS.1770), and sample peak in dBFS.
Cancellable jobs. Cancel mid-pipeline and the runner terminates the active subprocess immediately, deletes the partial job dir, and returns to ready.
Library panel with folder-based track organisation, drag-and-drop, search, and trash.
Honest Comparison
StemDeck is not trying to compete with commercial stem-separation products. It covers the core use case well and stops there. This table exists so you can make an informed choice rather than discover the gaps after the fact.
| StemDeck | Moises / LALAL.AI / similar | |
|---|---|---|
| Price | Free, forever | Freemium; credits or subscription required for regular use |
| Hosting | Runs entirely on your machine | Cloud; audio must be uploaded to their servers |
| Account / login | None | Required |
| Internet required | Only for YouTube download and first model fetch (~170 MB, cached after) | Always; no offline use |
| Privacy | Audio never leaves your machine | Audio is uploaded and processed on third-party servers |
| Data retention | You control it; delete anytime | Governed by their privacy policy and retention period |
| Stem model | Demucs htdemucs_6s (open source, Meta AI) |
Proprietary models, regularly updated, generally higher quality |
| Stem count | 6 (vocals, drums, bass, guitar, piano, other) | Up to 10 depending on service and plan |
| Input formats | YouTube URL, MP3, WAV, FLAC, OGG/Opus, MP4, M4A | MP3, WAV, FLAC, M4A, and more depending on service |
| Processing speed | Depends on your hardware; fast with a GPU, slow on CPU only | Fast regardless of your hardware (runs on their servers) |
| Batch processing | One job at a time | Yes, on paid plans |
| Mobile app | No | iOS and Android |
| Extra features | No (no pitch shift, chord detection, lyrics, click track, BPM tap) | Yes, varies by product |
| Polish | Functional, hobby-grade UI | Polished, production-grade apps |
| Source code | Open source, forkable, self-hostable | Closed source |
If you need speed, quality, mobile access, or the extra musician tooling, the commercial products are worth the money. If you want stems for personal study, prefer to keep audio private, or just want something that runs locally with no strings attached, StemDeck is enough.
Download
Pre-built installers and zips are attached to each GitHub Release.
macOS
| DMG | GPU | Chip |
|---|---|---|
StemDeck-macOS-arm64.dmg |
Apple Silicon (MPS) | M1 and later |
StemDeck-macOS-x64.dmg |
CPU only | Intel |
Open the DMG, drag StemDeck to Applications, and launch it. On first launch the setup screen downloads the Python runtime (~500 MB), FFmpeg, and the Demucs model (~170 MB). Subsequent launches skip setup and start in seconds. No Python or system dependencies required.
macOS may show a Gatekeeper prompt on first open — right-click the app and choose Open to bypass it.
Windows
| Zip | GPU | Approx. size |
|---|---|---|
StemDeck-Windows-x64.zip |
CPU only | ~700 MB |
StemDeck-Windows-x64.NVIDIA.zip |
NVIDIA CUDA | ~1.6 GB |
Extract the zip anywhere, run StemDeck.exe. FFmpeg, the Demucs model, config, and logs live in a data/ folder next to StemDeck.exe, not in AppData; move or copy the whole extracted folder anywhere and it keeps working. On first launch the app verifies the bundled Python runtime and downloads FFmpeg and the Demucs model (~170 MB) into that folder. Subsequent launches skip this and start in seconds. Everything is self-contained; no Python or system dependencies required. Your job/library data stays in its usual location (~/Documents/StemDeck by default) and is relocatable anytime from Settings → StemData location.
Technologies
StemDeck is built on Python 3.12 managed via uv, with a FastAPI backend serving REST and Server-Sent Events. Stem separation uses Demucs (htdemucs_6s), Meta AI's open-source 6-stem neural network. The optional on-demand lead/backing vocal split runs the UVR-MDX-NET Karaoke 2 model via audio-separator, trained as part of the Ultimate Vocal Remover project by Anjok07. YouTube audio is fetched via yt-dlp; transcoding and mixing use FFmpeg. BPM detection and key analysis run on librosa; loudness measurement uses pyloudnorm (ITU-R BS.1770). The macOS and Windows desktop shells are Tauri v2 (Rust/WKWebView on macOS, Rust/WebView2 on Windows). The frontend is vanilla JS with the Web Audio API, no framework and no build step; waveforms are rendered on <canvas> using min/max sample rendering.
Thanks to the creators and maintainers of all the open-source libraries that make StemDeck possible.
Build from Source
macOS Native App
Requires Rust, Node.js, and Python 3.12. Builds a self-contained .app that downloads its own runtime on first launch.
# First time only — add the cross-compilation targets
rustup target add aarch64-apple-darwin # Apple Silicon
rustup target add x86_64-apple-darwin # Intel
# Build Apple Silicon
ARCH=arm64 scripts/macos/make-runtime-pack.sh
ARCH=arm64 scripts/macos/make-app.sh
ARCH=arm64 scripts/macos/make-dmg.sh
# Build Intel (requires Rosetta 2 and an x86_64 Python)
ARCH=x64 scripts/macos/make-runtime-pack.sh
ARCH=x64 scripts/macos/make-app.sh
ARCH=x64 scripts/macos/make-dmg.sh
The .app lands at desktop/src-tauri/target/<target>/release/bundle/macos/StemDeck.app. The DMG lands at .build/macos-dist/StemDeck-macOS-<arch>.dmg.
To run a fresh build directly without the DMG:
open desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/StemDeck.app
If macOS blocks the app with a Gatekeeper prompt, run:
xattr -dr com.apple.quarantine desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/StemDeck.app
Note: To test a clean first-launch during development, you can wipe previous app data first:
rm -rf ~/Library/Application\ Support/StemDeck. Don't do this on a real install.
Web Server (macOS / Linux / Windows with Python 3.12+)
Prerequisites
Python 3.12 or newer, ffmpeg on your PATH, and uv. Around 170 MB of free disk for the Demucs model, which downloads automatically on first run.
macOS / Linux (one-shot)
git clone https://github.com/stemdeckapp/stemdeck stemdeck && cd stemdeck
./run.sh setup # installs ffmpeg + uv, runs uv sync
./run.sh start
Open http://localhost:8000.
setup uses Homebrew on macOS and apt-get on Debian/Ubuntu. For other Linux distros, install ffmpeg and uv manually, then run uv sync followed by ./run.sh start.
Windows (PowerShell)
Install prerequisites:
- uv —
winget install astral-sh.uv - ffmpeg —
winget install Gyan.FFmpeg(or Chocolatey:choco install ffmpeg)
git clone https://github.com/stemdeckapp/stemdeck stemdeck; cd stemdeck
uv sync
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000 --timeout-graceful-shutdown 5
Open http://localhost:8000.
run.shis macOS/Linux only. On Windows use the PowerShell commands above, or run inside WSL.
NVIDIA GPU (CUDA): install the CUDA-enabled torch build before starting:
uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
$env:STEMDECK_DEMUCS_DEVICE = "cuda"
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000 --timeout-graceful-shutdown 5
Manual (any platform)
git clone https://github.com/stemdeckapp/stemdeck stemdeck && cd stemdeck
uv sync
uv run uvicorn app.main:app --reload --timeout-graceful-shutdown 5
--timeout-graceful-shutdownbounds how long uvicorn waits for open connections when you stop it. StemDeck keeps a long-lived SSE stream open for the import queue while a browser tab is on the app, so without it Ctrl-C waits for that stream instead of exiting.
Docker
docker compose -f build/docker-compose.yml up --build
Stems land in ./jobs/ on the host. Demucs weights are cached in a named volume so they don't re-download on rebuild. Note: no GPU passthrough on macOS Docker.
A prebuilt image is published to GHCR. Tags: edge (rolling, rebuilt on every merge to main), latest (newest stable release), and X.Y.Z (pinned to a release).
docker run -d --name stemdeck -p 8000:8000 \
-v /path/to/jobs:/app/jobs \
-v /path/to/cache:/cache \
-e STEMDECK_PERSIST_LIBRARY=1 \
ghcr.io/stemdeckapp/stemdeck:edge
On a Linux host with an NVIDIA GPU (driver + NVIDIA Container Toolkit installed), add --runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all and StemDeck auto-detects CUDA. The image already bundles CUDA-enabled torch, so no separate CUDA install is needed.
Unraid
StemDeck is available in Unraid Community Applications: open Apps, search "StemDeck", and install. Map the two volumes to persistent appdata paths:
/app/jobs->/mnt/user/appdata/stemdeck/jobs(library + stems)/cache->/mnt/user/appdata/stemdeck/cache(model weights)
The library is persistent by default (STEMDECK_PERSIST_LIBRARY=1), so tracks are never auto-deleted. For GPU acceleration, install the Nvidia Driver plugin, then set the container's Extra Parameters to --runtime=nvidia (the NVIDIA_VISIBLE_DEVICES and NVIDIA_DRIVER_CAPABILITIES variables are already in the template). CPU-only works with no extra configuration.
run.sh control script
./run.sh setup # one-shot: install ffmpeg + uv, then uv sync
./run.sh start # boots uvicorn in the background
./run.sh stop # graceful shutdown
./run.sh restart # stop + start
./run.sh status # is it running?
How to Use
- On the import bar, click stem chips to choose which stems to extract (defaults to all 6).
- Paste a YouTube URL or drop an audio file (MP3, WAV, FLAC, OGG, MP4, M4A), then click Process.
- Wait through
Uploading.../Downloading...→Analyzing...→Separating...→Mixing tracks.... - When done, the studio dashboard appears. If you picked a subset, the first lane is Original (full song minus your selection); the rest are your isolated stems.
- Mix: Play/Pause/Stop controls the master transport. M mutes a stem, S solos it (additive; multiple solos stay audible), Monitor solos only that stem and clears others. The volume fader moves 1:1 with drag; double-click resets to 0 dB;
Shift+wheelgives coarse adjustment and plain wheel gives fine. The Reset, Mute, and Solo toolbar buttons act on all stems at once. - Drag on the ruler to define a loop region; click
Loopto enable. Use+/-/FitorCtrl/Cmd+wheelto zoom. - Download Mix in the footer gives you a WAV of your selected stems summed together.
Keyboard shortcuts: Space play/pause · [ seek -5s · ] seek +5s · L loop · I loop in · O loop out
Configuration
| Variable | Default | Purpose |
|---|---|---|
STEMDECK_DEMUCS_DEVICE |
auto | Force Torch device: cuda, mps, or cpu. |
STEMDECK_DEMUCS_MODEL |
htdemucs_6s |
Demucs model name. |
STEMDECK_JOBS_DIR |
./jobs |
Where job directories land. |
STEMDECK_DATA_DIR |
(none) | Portable mode root; sets all sub-dirs below to live inside it. |
STEMDECK_CACHE_DIR |
<data>/cache |
Torch model cache directory. |
STEMDECK_DOWNLOADS_DIR |
<data>/downloads |
yt-dlp download scratch space. |
STEMDECK_MODELS_DIR |
<data>/models |
Demucs model weights directory. |
STEMDECK_LOGS_DIR |
<data>/logs |
Log file output directory. |
STEMDECK_FFMPEG_DIR |
(none) | Directory containing a bundled ffmpeg binary. |
STEMDECK_FFMPEG |
ffmpeg |
Path to the ffmpeg executable. |
STEMDECK_FFPROBE |
ffprobe |
Path to the ffprobe executable. |
STEMDECK_MAX_DURATION_SEC |
1200 |
Reject audio longer than this (seconds). |
STEMDECK_JOB_TTL_SECONDS |
86400 |
How long to keep job dirs on disk. |
STEMDECK_MAX_PENDING_JOBS |
3 |
Max queued jobs before returning 503. |
STEMDECK_TIMEOUT_FFMPEG |
300 |
ffmpeg subprocess timeout (seconds). |
STEMDECK_TIMEOUT_ANALYZE |
120 |
Audio analysis timeout (seconds). |
STEMDECK_TIMEOUT_DEMUCS_STALL |
1800 |
Kill Demucs if no output for this many seconds. |
run.sh also reads: HOST (default 127.0.0.1), PORT (default 8765), RELOAD=1 (enable uvicorn auto-reload for development), FOREGROUND=1 (run in foreground instead of backgrounding).
API
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
Server health and version info |
| POST | /api/jobs |
JSON {url, stems?} or multipart file + stems → {job_id} |
| GET | /api/jobs |
List completed (library) jobs |
| GET | /api/jobs/{id} |
Job state snapshot |
| GET | /api/jobs/{id}/events |
SSE stream of job state |
| POST | /api/jobs/{id}/cancel |
Terminate active subprocess and cancel job |
| PATCH | /api/jobs/{id}/sections |
Save waveform section markers for a job |
| GET | /api/jobs/{id}/stems/{name}.wav |
Stream a single stem WAV file |
| GET | /api/jobs/{id}/stems/{name}.mp3 |
Transcode and stream a stem as MP3 |
| GET | /api/jobs/{id}/video.mp4 |
Mux the current mix with the source video (MP4 upload or YouTube) into an MP4 |
| DELETE | /api/jobs/{id} |
Remove job dir from disk (terminal jobs only) |
Troubleshooting
ffmpeg: command not found: install ffmpeg and restart with ./run.sh restart.
WARNING: [youtube] No supported JavaScript runtime: install deno (brew install deno on macOS) and restart. Downloads still work without it but may pick suboptimal formats.
First separation is very slow: Demucs downloads htdemucs_6s weights (~170 MB) on first run; cached afterwards.
Demucs runs on CPU only: check the startup log for device=mps or device=cuda. If you see cpu, your torch install may be CPU-only.
Page reloaded mid-job: the job keeps running server-side. Wait for it to finish, then resubmit.
./run.sh: Permission denied: run chmod +x run.sh.
Layout on Disk
jobs/<job_id>/
└── stems/
├── vocals.wav # the 6 Demucs stems (always present)
├── drums.wav
├── bass.wav
├── guitar.wav
├── piano.wav
├── other.wav
├── original.wav # sum of un-selected stems (subset only)
└── mix.wav # ffmpeg amix of selected stems (subset only)
Job state is in-memory. Restart the server and the job list resets, but files persist on disk. Old dirs are swept automatically (TTL 24 h, configurable).
Disclaimer
StemDeck is a local audio stem separation tool intended for personal study, research, and experimentation. It is not a downloading service. It does not store, cache, or redistribute any audio content. All processing runs on the user's own machine and no audio is transmitted anywhere.
YouTube URL support is provided via yt-dlp as a convenience. Automated downloading may violate YouTube's Terms of Service. You, the user, are solely responsible for ensuring you have the right to process any audio you submit, complying with the terms of service of any site you download from, and respecting the copyright of the material you work with.
You are also responsible for following the licenses of the underlying tools this project depends on (yt-dlp, Demucs, FFmpeg, PyTorch, and others listed in pyproject.toml).
The author(s) of StemDeck provide this software "as is", without warranty of any kind, and accept no responsibility or liability for how it is used.
Community
| Platform | Link |
|---|---|
| GitHub | stemdeckapp/stemdeck |
| Discord | discord.gg/2MVsWqaPRe |
| r/StemDeckApp | |
| @stemdeck | |
| X | @StemDeckApp |
| Website | stemdeck.app (coming soon) |
Environment Variables
These are for development and testing. Release builds only recognize the variables marked "release".
| Variable | Platform | Scope | Description |
|---|---|---|---|
STEMDECK_DATA_DIR |
all | release | Override the user data directory (default: platform-standard location) |
STEMDECK_ROOT |
all | release | Override the app root directory (default: derived from executable path) |
STEMDECK_PYTHON |
all | debug builds only | Override the Python executable path |
STEMDECK_FFMPEG_URL |
Windows, macOS | release | Override the FFmpeg download URL |
STEMDECK_FFPROBE_URL |
macOS | release | Override the ffprobe download URL |
Contributing
Issues, feature suggestions, and pull requests are welcome. See open issues for what's planned.
