54 Commits
Author SHA1 Message Date
rootandClaude Opus 5.5 db55b5eea0 iOS: iPhone Duo split layout at the hinge, 1.0 RC 3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 07:57:58 -08:00
cdnutterandClaude Sonnet 5.5 cf5f992e60 stemserve: refresh index in a background thread
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 14:17:38 -08:00
rootandClaude Opus 5.5 f099682a88 ios: build-remote defaults to white-m1 host
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 09:33:45 -08:00
rootandClaude Opus 5.5 9415bfb50f iOS: source.json for 1.0 RC 2 rebuild
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:55:29 -08:00
rootandClaude Opus 5.5 ee6ca41fd3 iOS 1.0 RC 2: Update Download and Update All Downloads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:55:26 -08:00
rootandClaude Opus 5.5 abb157d140 iOS: source.json for 1.0 RC 2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:41:09 -08:00
rootandClaude Opus 5.5 2fb8040388 iOS 1.0 RC 2: Plex streams under upload caps, shuffle and album radio stations, Recently Added list, photo covers, crossfade
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:40:37 -08:00
rootandClaude Opus 5.5 c0e33b5e3e iOS 1.0 RC 1: battery and heat work, version label, web player redraw tweaks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:31:00 -08:00
cdnutterandClaude Sonnet 5.5 0fc35b13e8 Prune stems whose source song was removed or replaced
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 10:28:35 -07:00
rootandClaude Sonnet 5.5 5b7fadb7f7 iOS: section reorder and visibility, CarPlay downloads fix, stem playlist refresh fix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 16:30:30 -07:00
rootandClaude Sonnet 5.5 3e832e3125 iOS: Random Album Radio plays whole albums in random order, loading each next album lazily
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 15:56:02 -07:00
rootandClaude Sonnet 5.5 a4c870251c iOS: stem playlist refresh and covers, download queue page, reorderable Library, whole-library radio
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 15:49:09 -07:00
cdnutterandClaude Sonnet 5.5 7c8dfb54ca Add Kevin Viers Venmo link to About, build 20
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 20:22:03 -07:00
cdnutterandClaude Sonnet 5.5 df5d847169 Stemify iOS 1.1 and WebUI updates, repo restructure into desktop/ios/server/web
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:08:51 -07:00
cdnutterandClaude Sonnet 5.5 894dd5a695 ios 1.0.17: pooled libraries, random sort, stems-only filter
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 18:45:25 -07:00
rootandClaude Opus 5.5 7cdfbef160 Web player overhaul; iOS random sort, stems-only filter, pooled libraries
Web: live Stem Now status that tracks every queued song (albums too),
right-click menus with Stem Now, Songs and Genres pages, a Stems Only
filter in place of the sidebar section, Random sort, several pooled
Plex servers and libraries, themes plus custom CSS, a reorganised
settings page that loads without a refresh, saved mixes, karaoke and
synced lyrics, stem playlists and offline downloads.

iOS: Random in every sort menu, a Stems Only toggle (sort menu and
Settings), and several music libraries pooled from the signed-in server.
Settings now persists karaoke and speed across launches.

stemd writes a status file that stemserve serves at /api/queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:29:45 -07:00
rootandClaude Sonnet 5.5 48faa6682f web: prepare the next track only after the current one is playing and fully buffered
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:42:22 -07:00
rootandClaude Sonnet 5.5 a1cf0a00b3 ios: prepare the next track only after the current one is playing and fully cached
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:40:59 -07:00
rootandClaude Sonnet 5.5 4277688a38 stemserve: stay quiet on dropped client connections
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 16:48:44 -07:00
rootandClaude Sonnet 5.5 455d9a790c stemd: AUTO_SCAN off by default
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:46:32 -07:00
rootandClaude Sonnet 5.5 4ccb5051ca web: Stem Now button on album, artist and playlist pages
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:44:12 -07:00
cdnutterandClaude Sonnet 5.5 e35ebedb56 stemd: AUTO_SCAN=0 runs on request only (Stem Now)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:42:55 -07:00
cdnutterandClaude Sonnet 5.5 d69c44eff9 README: make it generic (no host paths, IPs or personal examples)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:09:12 -07:00
cdnutterandClaude Sonnet 5.5 1cf9341cda source.json: 1.0.16
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:39:00 -07:00
cdnutterandClaude Sonnet 5.5 75c6d89608 ios 1.0.16: more room between seek bar and time/codec row
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:38:54 -07:00
cdnutterandClaude Opus 5.5 f065c95422 ios: source.json for 1.0.15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:22:36 -07:00
cdnutterandClaude Opus 5.5 c17d6f174b ios: 1.0.15, mixer rows stretch to fill spare height
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:22:27 -07:00
cdnutterandClaude Sonnet 5.5 e7e4fcc9d6 ios: 1.0.14, source.json, README for tab bar, AirPlay and debug logs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:14:55 -07:00
cdnutterandClaude Sonnet 5.5 926ad6ee99 ios: reorderable tab bar with more tabs, debug log export, AirPlay picker, more room above seeker
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:11:58 -07:00
cdnutterandClaude Sonnet 5.5 a0fe35a8d8 iOS: source.json for 1.0.13
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:42:31 -07:00
cdnutterandClaude Sonnet 5.5 d773b3e5a5 iOS: bump to 1.0.13
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:40:13 -07:00
cdnutterandClaude Opus 5.5 9c1e70da4d iOS: one-line mixer faders so every stem fits; wide now-playing layout and landscape for iPhone Duo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:37:38 -07:00
cdnutterandClaude Sonnet 5.5 4e2816673d iOS: export/import stem playlists and sound settings
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:33:54 -07:00
cdnutterandClaude Sonnet 5.5 9646b4c010 source.json: add 1.0.12
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:29:08 -07:00
cdnutterandClaude Sonnet 5.5 99c4de91c4 iOS 1.0.12: more space between seek bar and transport buttons
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:27:44 -07:00
cdnutterandClaude Sonnet 5.5 1a5dba78f7 Document /plex/ proxy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:12:25 -07:00
cdnutterandClaude Sonnet 5.5 7ca9d0374d Proxy Plex through stemserve (/plex/) so HTTPS pages avoid mixed-content blocks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:11:18 -07:00
cdnutterandClaude Sonnet 5.5 4b53a720c4 Web UI: fix Plex status text, PLEX_URL/PLEX_TOKEN env, local stem server auto-detect, WEB=0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:00:34 -07:00
rootandClaude Sonnet 5.5 2a4e848788 stemify: build-ipa writes an unsigned release IPA plus a personal signed IPA in prov/
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 12:41:36 -07:00
cdnutterandClaude Sonnet 5.5 629901616b ios: optional signing step in build-ipa.sh (profile + p12 from ios/prov)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 12:36:05 -07:00
cdnutterandClaude Opus 5.5 4fe7f6d16a ios: source.json for 1.0.11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:24:42 -07:00
cdnutterandClaude Opus 5.5 555a79657c ios 1.0.11: Now Playing tap opens the app
UIApplicationSupportsMultipleScenes off; the app is iPhone-only and CarPlay doesn't need it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:24:40 -07:00
cdnutterandClaude Opus 5.5 9299fee9a0 Web player served by stemserve
Plex browsing, stem mixer with presets, shuffle, repeat all/one, playback
speed, queue, waveform seek. stemserve serves web/ at / without the token.
stemserve image now builds from the repo root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:30:02 -07:00
cdnutterandClaude Opus 5.5 605b88f1eb ios: source.json for 1.0.10
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:21:34 -07:00
cdnutterandClaude Opus 5.5 b0dba777f9 ios 1.0.10: shuffle/repeat on Now Playing, playback speed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:21:32 -07:00
cdnutterandClaude Opus 5.5 5b767cacc2 README: iOS has the EQ too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:50:22 -07:00
cdnutterandClaude Opus 5.5 bba92967ea README: iOS section, stem playlists, tighter layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:50:14 -07:00
cdnutterandClaude Opus 5.5 068f2309b4 iOS: source.json for 1.0.9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:28:27 -07:00
cdnutterandClaude Opus 5.5 eb541b9a16 iOS 1.0.9: Add to Playlist sheet
Replaces the chain of dialogs (each waited ~450 ms for the last to close)
with one sheet: Plex | Stem tabs, inline new-playlist name field, playlist
rows with art, and mix chips for stem playlists. One "Add to Playlist…"
menu entry now covers both kinds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:28:25 -07:00
cdnutterandClaude Opus 5.5 790f8a35a2 iOS: source.json for 1.0.8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:02:44 -07:00
cdnutterandClaude Opus 5.5 e2cf62cb07 iOS 1.0.8: stem playlists, karaoke softness slider in lyrics
- Playlists tab splits into Plex and Stem. Plex playlists are unchanged and
  now show their custom poster when one is set.
- Stem playlists live on the device; each song keeps its own mix. Add from
  any song or collection menu, or copy a whole Plex playlist.
- Queue items can carry a mix, which wins over the song's saved mix.
- Softness slider under the Vocals picker in lyrics when set to Soft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:02:42 -07:00
cdnutterandClaude Opus 5.5 6f634522e5 ios: source.json for 1.0.7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:39:04 -07:00
cdnutterandClaude Opus 5.5 d52a3e257f ios: Liquid Glass mini player, adjustable karaoke softness (1.0.7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:39:02 -07:00
cdnutterandClaude Opus 5.5 e98d595365 ios: source.json for 1.0.6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:31:01 -07:00
180 changed files with 9357 additions and 1199 deletions

No files matched your search

+2 -2
View File
@@ -2,5 +2,5 @@
* *
!server/stemd.py !server/stemd.py
!server/stemd-requirements.txt !server/stemd-requirements.txt
!stemify/*.py !desktop/stemify/*.py
!stemify/worker/*.py !desktop/stemify/worker/*.py
File renamed without changes.
+1
View File
@@ -22,3 +22,4 @@ ios/build/
ios/Stemify.xcodeproj/ ios/Stemify.xcodeproj/
ios/Stemify/Info.plist ios/Stemify/Info.plist
ios/Stemify/Stemify.entitlements ios/Stemify/Stemify.entitlements
ios/prov
+31
View File
@@ -0,0 +1,31 @@
# Changelog
## 2026-10-26
- Project created.
2026-10-28 iOS: stemd queue state (STEMMING/QUEUED/FAILED tags, Stem Now state, CarPlay, stemd line in Stem Server settings). Plex sign-in via plex.tv adds every server and all music libraries.
2026-10-28 iOS: media fetch errors (HTTP 500/404) now include the Plex host, port and path.
2026-10-28 iOS: Plex refresh keeps each server's library selection (new servers still get all libraries). Music Servers list moved to its own page under Plex Account.
2026-10-28 iOS: CarPlay Downloads opens onto Playlists/Albums/Artists/Songs, limited to downloaded music.
2026-10-28 iOS: stem playlists refresh their track metadata from Plex on each library load. Playlist covers can be changed from the playlist menu.
2026-10-28 iOS: Downloads 3-dots menu gets View Queue (waiting, downloading, failed, done).
2026-10-28 iOS: Library screen can be reordered (Reorder button). Library Radio and Random Album Radio shuffle the whole library.
2026-10-28 iOS: Random Album Radio plays whole albums in track order, albums in random order across the library, loading the next album as one finishes.
2026-10-28 iOS: Library screen Reorder now covers whole sections (Recent Plays, Recently Added, On This Day, Stations, Browse) and each can be shown or hidden
2026-10-28 iOS: Removed drag reorder from the Tab Bar settings page
2026-10-28 iOS: CarPlay Downloads Playlists, Albums and Artists load from local downloads, work offline
2026-10-28 iOS: Stem playlist metadata refresh matches by key, file, then title/artist with a search fallback, and also runs when the app returns to the foreground
2026-10-29 server: stemd prunes stems whose source song is gone (orphans). PRUNE, PRUNE_CHANGED, PRUNE_MAX env. server/prune.py is a dry-run CLI.
2026-10-31 iOS: version is now 1.0 RC 1 (build 21). Bundle version 1.0, label in StemifyVersionLabel, IPA stemify-1.0-rc1.ipa, tag ios-v1.0-rc1.
2026-10-31 iOS: battery and heat. Waveform redraws 4/s instead of 120, meters and playing bars capped at 30 fps. Engine sleeps until its buffer is half drained (120 ms foreground, 600 ms with no screen), 2 s idle while paused. Decoders wake on a low-water mark and run at userInitiated. stemd polling stops in the background unless CarPlay is connected. Widget reloads settle for 0.6 s.
2026-10-31 web: player redraws the waveform only when the playhead moves a bar (200 ms tick instead of rAF), caches the envelope, and stops polling while the tab is hidden.
2026-11-06 iOS: version is now 1.0 RC 2 (build 22). IPA stemify-1.0-rc2.ipa, tag ios-v1.0-rc2.
2026-11-06 iOS: plain Plex songs play when Remote Access has an upload cap. Streams now ask Plex for a playback decision first and fetch the part with that session, like Plexamp, instead of a bare part URL that Plex rejected with a 500.
2026-11-06 iOS: shuffle buttons (Songs, Library Radio) play server-side random batches of 100 and top up as the queue runs low, instead of loading the whole library and timing out.
2026-11-06 iOS: Random Album Radio works like Plexamp. It shuffles the album list once, keeps about 8 albums queued ahead in that order, plays each album in track order, and tops up when 3 are left. The station survives a restart.
2026-11-06 iOS: Recently Added has See All, a full scrolling list sorted by date added. Tapping a song on the Library page plays from it through the last 500 added songs.
2026-11-06 iOS: stem playlist covers can come from Photos (Change Cover > Choose from Photos). Stored on the phone, 1200 px JPEG.
2026-11-06 iOS: Update Download on downloaded playlists, albums, artists and stem playlists (their menus and the Downloads tab), plus Update All Downloads. Pulls the current song list, downloads new songs, drops removed ones, retries failed ones. Downloads still sync on their own when the app opens.
2026-11-06 iOS: crossfade, Off or 1-12 s in Settings > Listening. Equal-power fade, Sweet Fades skips it between songs of the same album. Seeking during a fade finishes it.
2026-11-09 iOS: iPhone Duo unfolded uses both sides of the hinge: library on one side, Now Playing docked on the other (no mini player while split). Folded and other iPhones unchanged. 1.0 RC 3 (build 23).
+244 -438
View File
@@ -2,589 +2,395 @@
# Stemify # Stemify
**Your whole music library, taken apart and put back together however you like.** Stemify splits every song in your Plex music library into stems (vocals, drums, bass, guitar, piano and other) and gives you players that mix them live. Mute the vocals and sing along, solo the bass to learn a part, or save an instrumental of a song and have it play that way every time.
Ever wanted to hear "Carry On Wayward Son" without the vocals so you can belt it out yourself? Or It runs on iPhone (with CarPlay), Android (with Android Auto) and Linux desktop. A GPU on your server does the separating in the background.
mute the drums on a song to figure out what the bass player is actually doing back there? Stemify
splits every song in your library into its separate parts (vocals, drums, bass, guitar, piano and
"other"), keeps them organised next to your music, and gives you players that let you mix them
live, on your phone, in your car, and on your desktop.
It's a home-grown, slightly over-engineered love letter to karaoke, music nerdery and the joy of - [How it works](#how-it-works)
pressing the "mute vocals" button at exactly the wrong moment. - [Apps](#apps)
- [Setup](#setup)
--- - [iOS](#ios)
- [Android](#android)
## Contents - [Desktop](#desktop)
- [Stem Now](#stem-now)
- [The big picture](#the-big-picture) - [Reference](#reference)
- [What you get](#what-you-get)
- [Setting it up](#setting-it-up)
- [1. The server: stemd and stemserve](#1-the-server-stemd-and-stemserve)
- [2. The Android app](#2-the-android-app)
- [3. The desktop apps (optional)](#3-the-desktop-apps-optional)
- [Using the Android app](#using-the-android-app)
- [Stem Now: skipping the line](#stem-now-skipping-the-line)
- [The Equalizer](#the-equalizer)
- [Android Auto](#android-auto)
- [iOS and CarPlay](#ios-and-carplay)
- [The desktop apps](#the-desktop-apps)
- [Separation models](#separation-models)
- [How the pieces find each other](#how-the-pieces-find-each-other)
- [stemserve API](#stemserve-api)
- [Building and testing](#building-and-testing) - [Building and testing](#building-and-testing)
- [Repo tour](#repo-tour) - [Troubleshooting](#troubleshooting)
- [Troubleshooting and FAQ](#troubleshooting-and-faq) - [Credits and license](#credits)
- [Credits](#credits)
- [License](#license)
--- ## How it works
## The big picture The stems folder mirrors the music folder. If a song lives at `Artist/Album/01 Song.flac`, its stems live in `Stems/Artist/Album/01 Song/`.
There are three moving parts, and they all agree on one simple idea: **the stems folder mirrors
the music folder.** If your song lives at `Kansas/Leftoverture/01 Carry On.flac`, its stems live at
`Stems/Kansas/Leftoverture/01 Carry On/`. That's it. That's the whole trick.
``` ```
┌──────────────────────────── your server (unraid, in our case) ───────────────────────────┐ your server
│ │ Music/ ──▶ stemd (GPU) ──▶ Stems/ ──▶ stemserve :8765 ──▶ phone, car, desktop
│ Music/ ──read──▶ stemd (GPU) ──writes──▶ Stems/ ──read──▶ stemserve (:8765) ─────┼──▶ phone, car, desktop ▲ │
│ ▲ ▲ │ │ └──── Stem Now requests ◀───┘
│ │ └──────── "Stem Now" requests ◀──────────────┘ │ Plex Media Server :32400 ───────────────────────────────▶ library, artwork, playlists
│ Plex Media Server (:32400) ──────────────────────────────────────────────────────────────┼──▶ library, artwork, playlists
└───────────────────────────────────────────────────────────────────────────────────────────┘
``` ```
- **stemd** sits on the server and chews through your library on the GPU, one song at a time, Three parts, and only stemd is required:
forever. New album? It notices within half an hour and gets to work.
- **stemserve** hands the finished stems to your devices over HTTP, with seeking, waveforms and
optional Opus transcodes so your phone storage doesn't cry.
- **Plex** stays in charge of what it's good at: your library, metadata, artwork and playlists.
- **The apps** (Android, Android Auto, and two desktop apps) play everything, and quietly fall back
to the original file for any song that hasn't been separated yet. Nothing is ever unplayable.
--- - **stemd** is the daemon. It does the actual separating and nothing else.
- **Stemify web UI** is optional, for desktop users. stemserve serves it at `/`. It plays the stems streamed from the server, and picks up a local stem server on `localhost:8765` by itself.
- **iOS and Android apps** are optional and built from this repo too.
## What you get - **stemd** works through the library on the GPU, one song at a time, and rescans every 30 minutes for new music.
- **stemserve** serves the finished stems over HTTP with range requests, waveforms and optional Opus transcodes.
- **Plex** handles the library, metadata, artwork and playlists.
- **The apps** play stems when a song has them and fall back to the original file when it doesn't, so everything plays.
**On your phone (Android)** ## Apps
- A Plex music player with per-stem faders, mute and solo, and one-tap mixes: Full, Karaoke,
A Cappella, No Drums, Rhythm, No Guitar, No Bass.
- A Plexamp-style waveform seek bar that doubles as a volume control (drag sideways to seek, up and
down for volume). The waveform is scaled to each song, so you can actually see the quiet verse
and the big chorus.
- Live level meters for every stem, updated every frame and in sync with what you hear.
- A 10-band graphic EQ **plus** Equalizer APO-style parametric filters, with per-device profiles
that switch automatically when you plug in headphones or connect to Bluetooth.
- Paste in an AutoEQ correction for your headphones and you're done.
- Downloads for offline listening, stems and all.
- Gapless playback, a proper queue, playlists (create, reorder, rename), search, and "On This
Day" album birthdays on the home screen.
- "Stem Now": jump any song, album, artist or playlist to the front of the separation queue.
- An iOS-flavoured look in Black, Dark or White, at whatever refresh rate your panel can manage.
**In your car (Android Auto)** | | iOS | Android | Desktop player | Web |
- A browse tree built for a rotary knob and no touchscreen, with voice search, a Vocals on/off |---|---|---|---|---|
button and a Mix button right next to play/pause. | Per-stem faders, mute, solo, mix presets | yes | yes | yes | yes |
| Waveform seek bar, live stem meters | yes | yes | | waveform |
| Shuffle, repeat all/one | yes | yes | | yes |
| Playback speed (keeps pitch) | yes | | | yes |
| Saved mix per song | yes | | | |
| Karaoke mode with synced lyrics | yes | | | |
| Stem playlists (a mix per song) | yes | | | |
| Plex playlists | yes | yes | yes | yes |
| Downloads for offline | yes | yes | | |
| 10-band and parametric EQ, AutoEQ import | yes | yes | | |
| Car | CarPlay | Android Auto | | |
| Voice | Siri | Google Assistant | | |
| Stem Now | yes | yes | | |
**On your desktop (Linux)** Mix presets are Full, Karaoke, A Cappella, No Drums, Rhythm, No Guitar and No Bass.
- **Stemify (the separator)**: drag a song onto the window, get stems out the other side, preview them in sync,
export them. Material layout meets Windows 7 Aero glass, running at 240 Hz because why not.
- **Stemify Player**: your Plex library and its stems, with full mixing... dressed up as
Microsoft Excel 2016. Yes, really. Your boss will think you're working on the quarterly numbers.
**On the server** ## Setup
- **stemd** separates everything automatically with BS-RoFormer SW (6 stems) on an NVIDIA GPU,
politely, at low priority, sharing the card with Plex transcodes.
- **stemserve** is one Python file using only the standard library. ffmpeg is optional.
--- Set up the server first, then the apps. The examples use `/srv/stemify` for app data, `/srv/music/library` for your music, `/srv/music/stems` for the output and `SERVER_IP` for the machine running the containers. Change them to match your setup.
## Setting it up ### Server
The recommended order is server first, then phone, then (if you like) desktop. The examples below You need Docker, and an NVIDIA GPU for stemd (with the NVIDIA container toolkit installed). A CPU build exists for testing, but it's far too slow for a whole library.
use unraid paths and the addresses from the original setup (`192.168.86.2`). Swap in your own.
### 1. The server: stemd and stemserve Make the folders, owned by whatever user the containers run as (`1000:1000` here):
You need Docker, and for stemd an NVIDIA GPU (on unraid, install the **Nvidia-Driver** plugin from
the Apps tab). A CPU-only build exists for testing, but separating a whole library on a CPU is a
good way to celebrate several birthdays while you wait.
**Folders first** (owned by `99:100`, unraid's nobody:users):
```sh ```sh
mkdir -p /mnt/user/appdata/stemserve/cache \ mkdir -p /srv/stemify/stemserve/cache \
/mnt/user/appdata/stemd/models \ /srv/stemify/stemd/models \
/mnt/user/appdata/stemd/state/queue /srv/stemify/stemd/state/queue
chown -R 99:100 /mnt/user/appdata/stemserve /mnt/user/appdata/stemd chown -R 1000:1000 /srv/stemify/stemserve /srv/stemify/stemd
``` ```
**stemd** builds from the repo root because it borrows the `stemify` package. Copy the repo (or **stemd** builds from the repo root because it uses the `stemify` package. Copy the repo (at least `desktop/stemify/`, `server/` and `.dockerignore`) to `/srv/stemify/stemd/src`, then:
at least `stemify/`, `server/` and `.dockerignore`) to `/mnt/user/appdata/stemd/src`, then:
```sh ```sh
docker build -f /mnt/user/appdata/stemd/src/server/stemd.Dockerfile -t stemd /mnt/user/appdata/stemd/src docker build -f /srv/stemify/stemd/src/server/stemd.Dockerfile -t stemd /srv/stemify/stemd/src
docker run -d --name stemd --restart unless-stopped --stop-timeout 60 \ docker run -d --name stemd --restart unless-stopped --stop-timeout 60 \
--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility \ --runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
-v "/mnt/user/Music/Actual music/Flacs":/music:ro \ -v /srv/music/library:/music:ro \
-v /mnt/user/Music/Stems:/stems \ -v /srv/music/stems:/stems \
-v /mnt/user/appdata/stemd/models:/models \ -v /srv/stemify/stemd/models:/models \
-v /mnt/user/appdata/stemd/state:/state \ -v /srv/stemify/stemd/state:/state \
stemd stemd
docker logs -f stemd
``` ```
The first song downloads the model (about 700 MB). After that the log reads like a very calm The first song downloads the model (about 700 MB). After that the log shows what's left, what it's on, and how fast it's going.
sports commentary: how many tracks are left, which one it's on, and how many times faster than
realtime it went.
**stemserve** is tiny. Copy the `server/` folder to `/mnt/user/appdata/stemserve` and either use **stemserve** is one Python file using only the standard library. It also serves the web player from `web/`. Copy the repo's `server/` and `web/` folders to `/srv/stemify/stemserve` and run `docker compose up -d --build` in `server/`, or by hand:
the included compose file:
```sh ```sh
cd /mnt/user/appdata/stemserve && docker compose up -d --build docker build -f /srv/stemify/stemserve/server/Dockerfile -t stemserve /srv/stemify/stemserve
```
or run it by hand:
```sh
docker build -t stemserve /mnt/user/appdata/stemserve
docker run -d --name stemserve --restart unless-stopped -p 8765:8765 \ docker run -d --name stemserve --restart unless-stopped -p 8765:8765 \
-v /mnt/user/Music/Stems:/stems:ro \ -v /srv/music/stems:/stems:ro \
-v /mnt/user/appdata/stemserve/cache:/cache \ -v /srv/stemify/stemserve/cache:/cache \
-v /mnt/user/appdata/stemd/state/queue:/queue \ -v /srv/stemify/stemd/state/queue:/queue \
stemserve stemserve
``` ```
Check it from any machine on your network: `curl http://SERVER_IP:8765/api/info` should return JSON with a track count.
```sh To update, copy the new files, rebuild, `docker rm -f` the container and run it again. Everything that matters lives in the mounted folders. Update stemd before stemserve.
curl http://192.168.86.2:8765/api/info
```
If you see a happy little JSON blob with a track count, you're in business.
<details> <details>
<summary><b>Server settings (environment variables)</b></summary> <summary>Server environment variables</summary>
**stemd** **stemd**
| Variable | Default | What it does | | Variable | Default | What it does |
|---|---|---| |---|---|---|
| `MUSIC_DIR` | `/music` | Your music library (read-only is fine) | | `MUSIC_DIR` | `/music` | Music library (read-only is fine) |
| `STEMS_DIR` | `/stems` | Where stems go | | `STEMS_DIR` | `/stems` | Where stems go |
| `STATE_DIR` | `/state` | The failed-track list lives here | | `STATE_DIR` | `/state` | Holds the failed-track list |
| `QUEUE_DIR` | `STATE_DIR/queue` | Where "Stem Now" requests arrive | | `QUEUE_DIR` | `STATE_DIR/queue` | Where Stem Now requests arrive |
| `MODELS_DIR` | `/models` | Model weights, downloaded on first use | | `MODELS_DIR` | `/models` | Model weights, downloaded on first use |
| `MODEL` | `roformer_bs_sw` | Any model id from [the table below](#separation-models) | | `MODEL` | `roformer_bs_sw` | Any id from [the model table](#separation-models) |
| `DEVICE` | `cuda` | `cuda`, `cuda:N` or `cpu` | | `DEVICE` | `cuda` | `cuda`, `cuda:N` or `cpu` |
| `RESCAN_MIN` | `30` | Minutes between library scans once it's caught up | | `RESCAN_MIN` | `30` | Minutes between library scans once caught up |
| `FP16` | `1` | Half precision on the GPU (`0` for full fp32) | | `PRUNE` | `1` | Remove stems whose song is gone from the library, at start and every `RESCAN_MIN` |
| `OVERLAP`, `SEGMENT`, `SHIFTS` | `0.25`, `0`, `0` | Separation quality knobs, same as the desktop app | | `PRUNE_CHANGED` | `0` | Also remove stems of songs replaced in place (size or mtime changed) |
| `PRUNE_MAX` | `0.2` | Skip a prune that would remove more than this share of the stems. `server/prune.py` is the manual dry run, `--apply` to remove |
| `AUTO_SCAN` | `0` | `1` to scan the library on a timer. Off by default: only Stem Now requests are separated |
| `FP16` | `1` | Half precision on the GPU (`0` for fp32) |
| `OVERLAP`, `SEGMENT`, `SHIFTS` | `0.25`, `0`, `0` | Separation quality, same as the desktop app |
`docker build --build-arg TORCH_INDEX=cpu ...` makes a small GPU-less image for testing. `docker build --build-arg TORCH_INDEX=cpu ...` makes a small CPU-only image.
**stemserve** **stemserve**
| Variable | Default | What it does | | Variable | Default | What it does |
|---|---|---| |---|---|---|
| `STEMS_DIR` | `/stems` | The stems folder | | `STEMS_DIR` | `/stems` | The stems folder |
| `CACHE_DIR` | `/cache` | Opus transcodes and waveforms (safe to delete any time) | | `CACHE_DIR` | `/cache` | Opus transcodes and waveforms (safe to delete) |
| `QUEUE_DIR` | `/queue` | stemd's "Stem Now" folder, shared with it | | `QUEUE_DIR` | `/queue` | stemd's Stem Now folder, shared with it |
| `PORT` | `8765` | Listen port | | `PORT` | `8765` | Listen port |
| `TOKEN` | empty | If set, every request needs `X-Stem-Token` or `?token=` | | `TOKEN` | empty | If set, requests need `X-Stem-Token` or `?token=` |
| `INDEX_TTL` | `30` | Seconds between rescans of the stems folder | | `INDEX_TTL` | `30` | Seconds between rescans of the stems folder |
| `PLEX_URL` | empty | Plex address. With `PLEX_TOKEN`, the web UI connects to Plex without signing in |
| `PLEX_TOKEN` | empty | Plex token. Only handed out to clients that pass `TOKEN`, so set `TOKEN` too |
| `WEB` | `1` | Set to `0` to turn off the web UI and serve the API only |
</details> </details>
**Updating later:** copy the new files over, rebuild the image, `docker rm -f` the container, and ### Connecting an app
run it again. Your stems, models and settings live in the mounted folders, so nothing is lost.
Update stemd before stemserve: if you separated songs before the project was called Stemify,
stemd renames their old done-markers on its first start so nothing gets separated twice.
### 2. The Android app Every app asks for the same two things:
Built for (and lovingly tested on) a OnePlus 15, but any recent Android phone should be happy. 1. **Plex:** sign in with the 4-character code at [plex.tv/link](https://plex.tv/link) and pick your music library.
2. **Stem server:** in Settings, enter your stemserve address. It should say Online with a song count.
1. Grab `Stemify.apk` from the Releases page, or [build it yourself](#building-and-testing), The apps check stemserve for newly separated songs every minute, so the library fills in as stemd works.
then sideload it:
```sh
adb install -r Stemify.apk
```
2. Open it and go to **Library → Connect** (or Settings → Plex). Enter the 4-character code at
[plex.tv/link](https://plex.tv/link) and pick your music library.
3. Visit **Settings → Stem Server** and point it at your stemserve. It should say "Online" with a
song count. (The default address is `http://192.168.86.2:8765`, which is almost certainly not
yours.)
The app checks stemserve for freshly separated songs every minute, so your library slowly "lights ## iOS
up" as stemd works through it.
### 3. The desktop apps (optional) A SwiftUI app for iOS 17 or newer, with CarPlay and Siri ("play <song> in Stemify").
Linux, Python 3.12 and [uv](https://docs.astral.sh/uv/). The default setup uses PyTorch for ROCm ### Install
(AMD GPUs, developed on an RX 9070 XT). NVIDIA and CPU work too if you point the torch index
elsewhere in `pyproject.toml`.
```sh Grab `stemify-<version>.ipa` from the repo's Releases page and sideload it with AltStore, SideStore, Sideloadly or Feather. `ios/source.json` is an AltStore-style source. Add its raw URL to AltStore, SideStore or Feather for updates:
uv sync
uv run stemify # the drag-and-drop separator ```
uv run stemify-player # the Excel-shaped music player https://<your-git-host>/<owner>/stemify/raw/branch/main/ios/source.json
./scripts/install-desktop.sh # adds both to your app grid ("--uninstall" removes them)
``` ```
--- CarPlay and Siri need the `carplay-audio` and `siri` entitlements, which a free Apple ID can't sign. On a free sideload the app works on the phone but not in CarPlay or Siri. A paid developer account covers Siri; CarPlay audio also needs Apple to approve the entitlement for your team.
## Using the Android app ### Using it
**Tabs:** Library, Playlists, Search, Downloads and Settings. - **Now Playing:** shuffle and repeat sit either side of the play controls; tap repeat again for repeat one. The speed button (next to the mixer) goes from 0.5× to 2× without changing pitch.
- **Mixer:** on Now Playing, under the waveform. Tap **Save** to keep a mix for that song; it comes back every time the song plays.
- **AirPlay:** the AirPlay button on Now Playing picks the speaker or TV. Stems are mixed on the phone, so mixer changes apply live while it plays over AirPlay.
- **Tab bar:** Settings → Tab Bar. Pick and reorder up to five of Library, Songs, Albums, Artists, Genres, Playlists, Search, Downloads and Settings. Settings always stays.
- **Debug logs:** Settings → Debug. Turn on Verbose Logging, then View, Export or Clear the log. Export opens the share sheet with the app version, device, iOS version and audio output at the top. Tokens are stripped from the log.
- **Karaoke:** the mic button mutes vocals on every song and shows synced lyrics from [LRCLIB](https://lrclib.net). Tap a line to jump to it. Vocals can be On, Soft or Off, and Soft has a slider for how quiet.
- **Playlists:** the Playlists tab switches between Plex and Stem.
- Plex playlists sync with Plex and Plexamp.
- Stem playlists stay on the phone, and each song in them has its own mix, so one playlist can hold an instrumental of one song and an a cappella of the next.
- Copy any Plex playlist to a stem playlist from its menu.
- **Add to Playlist:** hold any song, album, artist or playlist. One sheet covers both kinds: pick Plex or Stem, pick the mix for stem playlists, then tap a playlist or make a new one right there.
- **Sorting:** artists, albums, songs and playlists sort like Plexamp. Songs has a shuffle-everything button.
- **Hold anything** for Add to Front or End of Queue, Download, Add to Playlist and Stem Now.
- **Equalizer:** same graphic EQ, parametric filters and APO/AutoEQ import as [Android](#equalizer).
**Stems are the norm, so the exceptions are what's marked.** Songs without stems yet say ## Android
**NO STEMS**, and albums and artists say **NO STEMS** or something like **7/10 STEMS** until
they're done. Those songs still play, just from the original file.
**Hold anything** (a song, album, artist or playlist) for: Kotlin and Jetpack Compose, [build it](#building-and-testing) or grab `Stemify.apk` from Releases if one is attached, and `adb install -r Stemify.apk`.
- **Add to Front of Queue** / **Add to End of Queue** ### Using it
- **Download** (or **Remove Download**)
- **Add to Playlist** (playlists can even go into other playlists)
- **Stem Now**, while any of it lacks stems
**The Now Playing screen** has the waveform seek bar, the mixer (faders, mute, solo, mix presets, - **Tabs:** Library, Playlists, Search, Downloads and Settings.
meters, master volume) and the queue. Your mix can carry over from song to song or reset each - **Badges:** songs without stems yet say NO STEMS, and albums and artists show NO STEMS or a count like 7/10 STEMS. They still play from the original file.
time, your choice. - **Now Playing:** waveform seek bar (drag sideways to seek, up and down for volume), mixer, meters and queue. Your mix can carry over between songs or reset.
- **Stem Quality:** lossless FLAC, or Opus at a bitrate you choose (about an eighth of the size, transcoded and cached on the server).
- **Streaming** works anywhere you can reach Plex and stemserve. Both are plain HTTP by default, so use HTTPS or a VPN away from home. Downloads and already-streamed songs play anywhere.
**Downloads** are proper offline copies: every stem fully fetched (or the original file for songs ### Equalizer
without stems). Sort by download order, artist or release date.
**Stem Quality** decides how stems travel from the server: lossless FLAC, or Opus at a bitrate of Settings → Equalizer has two parts.
your choosing, which takes roughly an eighth of the space and is transcoded and cached on the
server.
**Streaming** needs your home network (Plex and stemserve are plain HTTP on the LAN). Anything - **Graphic EQ:** ten bands from 31 Hz to 16 kHz, with presets like Bass Booster, Late Night and Vocal Booster.
you've downloaded or already streamed plays anywhere. - **Parametric filters**, modeled on [Equalizer APO](https://sourceforge.net/projects/equalizerapo/): Peak, Low/High Shelf, Low/High Pass (6 to 48 dB/oct), Band Pass, Notch and All Pass. Each has frequency, gain and Q, works on both channels or one, and can be toggled on its own. Drag the handle on the curve or tap a value to type it.
--- To correct headphones, copy a `ParametricEQ.txt` from [AutoEQ](https://github.com/jaakkopasanen/AutoEq) and choose **••• → Paste APO Config** on the Filters page. **Copy as APO Config** goes the other way.
## Stem Now: skipping the line Profiles are tied to outputs (speaker, wired, each Bluetooth device, USB DACs) and switch automatically. Each has its own preamp, and anti-clipping adds just enough cut to keep the loudest point of the curve at 0 dBFS. A limiter on the master bus catches the rest.
stemd works through your library on its own, but a big library takes days. If you're desperate to ### Android Auto
karaoke your way through an album tonight, hold it and choose **Stem Now**.
Here's what happens: Designed for small non-touch head units (800×480, knob and wheel buttons only), and it works the same on larger screens.
1. The app sends the songs that still lack stems to stemserve. - Tabs for Playlists, Albums, Artists and Downloads (Downloads first when you're away from home).
2. stemserve drops a little request file into stemd's queue folder. - Every song list starts with Shuffle all, and lists over 40 items split into A to Z folders.
3. stemd finishes the song it's on, then does yours next, ahead of everything else. If it was - Now Playing has a Vocals on/off button and a Mix button that cycles presets. The mix shows on the artist line, like "Artist · Karaoke".
idle, it starts within about 5 seconds. - Voice: "play <song> on Stemify" works for playlists, artists, albums and songs.
4. About a minute after each song finishes, the app notices and plays it with stems from then on. - Away from home it notices the server is gone and only lists songs that will play.
Requests are handled oldest first and survive restarts. stemd matches the paths Plex reports to Android Auto hides sideloaded apps until you allow them: in the phone's Android Auto settings, tap Version ten times, then ⋮ → Developer settings → Unknown sources.
files in its own music folder by their artist/album/file tail, so it doesn't matter that Plex and
stemd see the library under different mount points. Songs that failed before (corrupt files, or
anything over 40 minutes) are skipped; delete `state/failed.json` to give them all another shot.
--- To test without a car, start the head unit server from Auto's developer settings, then:
## The Equalizer
Find it in **Settings → Equalizer**. It's two equalizers wearing one trench coat.
**The graphic EQ** has ten octave bands from 31 Hz to 16 kHz, a response curve, and a pile of
presets borrowed in spirit from iOS (Bass Booster, Late Night, Vocal Booster and friends).
**Parametric Filters** are modelled on [Equalizer APO](https://sourceforge.net/projects/equalizerapo/):
| Type | What it's for |
|---|---|
| Peak | Boost or cut around one frequency |
| Low Shelf / High Shelf | Tilt everything below or above a frequency |
| Low Pass / High Pass | Roll off the top or bottom, at 6, 12, 24, 36 or 48 dB per octave |
| Band Pass | Keep only a band |
| Notch | Surgically remove one frequency (hello, 60 Hz hum) |
| All Pass | Shift phase without changing level, for the true connoisseur |
Every filter has frequency, gain and Q, can apply to **both channels, or just left or right**,
and can be switched off on its own to compare. Drag a filter's handle on its curve (sideways for
frequency, up and down for gain), use the sliders, or tap any value to type it in.
**Headphone corrections in two taps:** copy a `ParametricEQ.txt` from
[AutoEQ](https://github.com/jaakkopasanen/AutoEq) for your headphones, then on the Filters page
choose **••• → Paste APO Config**. You can also **Copy as APO Config** to take your settings to a
PC.
**Profiles** link to output devices: the phone speaker, wired headphones, each Bluetooth headset,
USB DACs. Plug something in and its profile switches on automatically, or pick one by hand.
**Preamp and anti-clipping:** each profile has its own manual preamp, and the anti-clipping switch
adds just enough extra cut that a full-scale signal at the curve's loudest point still peaks at
0 dBFS. A limiter on the master bus catches anything else, so cranking the bass won't make things
crackle.
---
## Android Auto
Designed for a 2016 Mazda MX-5 with the Mazda Connect retrofit: an 800×480 screen with touch
disabled, driven entirely by the commander knob and the steering wheel buttons. If it works there,
it'll work in your car.
- **Four tabs:** Playlists, Albums, Artists, Downloads (Downloads comes first when you're away
from home).
- **Shallow and short:** every song list starts with **Shuffle all**, and anything longer than 40
items splits into A to Z folders, so you never have to twiddle the knob through 400 songs at a
red light.
- **Now playing:** next to the transport controls you get **Vocals** on/off (instant karaoke),
**Mix** (cycles through the presets), shuffle and repeat. The current mix shows on the artist
line, e.g. "Black Sabbath · Karaoke".
- **Voice:** hold the talk button and say "play Paranoid on Stemify". Playlists, artists,
albums and songs all work.
- **Offline:** away from home, the app quickly notices the server is unreachable and lists only
songs that will actually play (downloaded or fully cached).
**Setting up the car (once):** Android Auto hides sideloaded apps until you allow them. On the
phone, search Settings for "Android Auto", tap **Version** ten times, then in the ⋮ menu go to
**Developer settings** and tick **Unknown sources**. Stemify then shows up in Auto's app
launcher.
**Testing without a car:** Google's Desktop Head Unit can impersonate the Miata. On the phone,
start the head unit server from Auto's developer settings, then:
```sh ```sh
adb forward tcp:5277 tcp:5277 adb forward tcp:5277 tcp:5277
~/Android/Sdk/extras/google/auto/desktop-head-unit -c ~/Android/Sdk/extras/google/auto/config/rotary.ini ~/Android/Sdk/extras/google/auto/desktop-head-unit -c ~/Android/Sdk/extras/google/auto/config/rotary.ini
``` ```
--- ## Web
## iOS and CarPlay stemserve also serves a web player at its root, so `http://<server>:8765/` opens it. Sign in with Plex in Settings (or paste a server URL and token), then browse albums, artists, playlists and search like the apps. **Stems only** lists everything in the stems folder and works without Plex.
`ios/` is a SwiftUI port of the Android app with the same features, plus CarPlay (browse tabs, Now Playing with Vocals and Mix buttons) and Siri ("play X in Stemify"). iOS 17 or newer. - Mixer, presets, shuffle, repeat, speed, queue and the waveform seek bar work like the phone apps. The mix carries over to the next song.
- Songs without stems play straight from Plex.
- Keys: Space play/pause, arrows seek, S shuffle, R repeat, `[` `]` speed, M mixer, Q queue.
- Settings, queue and mix are kept in the browser. Nothing syncs with the apps.
- If stemserve has `TOKEN` set, enter it in Settings.
On top of the Android features: the Save chip in the mixer remembers a mix for that song and puts it back every time it plays, and the mic button turns on karaoke mode (vocals muted on every song, synced lyrics from [LRCLIB](https://lrclib.net) on Now Playing; tap a line to jump to it). It's plain HTML and JavaScript in `web/` with no build step. To run it somewhere else, serve `web/` and proxy `/api/` and `/file/` to stemserve on the same origin; with a different origin the stem faders fall back to element volume, which iOS Safari ignores.
Build an unsigned IPA on a Mac with Xcode and xcodegen (`brew install xcodegen`): ## Desktop
``` Linux, Python 3.12 and [uv](https://docs.astral.sh/uv/). PyTorch defaults to ROCm (AMD); point the torch index in `desktop/pyproject.toml` elsewhere for NVIDIA or CPU.
cd ios
scripts/build-ipa.sh # writes build/stemify-<version>.ipa ```sh
cd desktop
uv sync
uv run stemify # the drag-and-drop separator
uv run stemify-player # the player
./scripts/install-desktop.sh # add both to your app launcher (--uninstall removes them)
``` ```
Sideload the IPA with Sideloadly or AltStore, which sign it with your Apple ID. **Stemify** is the separator. Drop files on the window and each one is separated with your last-used model and device. You get a lane per stem with a waveform, solo, mute, gain and export (everything, one stem, or the current mix, as WAV, FLAC or MP3). Separation runs in its own process, results are cached, and it works on AMD, NVIDIA and CPU.
To get updates in AltStore, SideStore or Feather, add this source: **Stemify Player** is your Plex library and its stems, dressed up as Microsoft Excel 2016. Sheets are your library, the zoom slider is master volume, Cell Styles are mix presets, and Undo/Redo skip songs. It reports plays to Plex, supports media keys and MPRIS, and remembers your queue and mix. More in [`desktop/stemify/player/README.md`](desktop/stemify/player/README.md).
## Stem Now
A big library takes stemd days. Hold a song, album, artist or playlist and choose **Stem Now** to move its unseparated songs to the front.
1. The app sends the songs to stemserve, which drops a request file in stemd's queue folder.
2. stemd finishes the song it's on and does yours next. If it was idle, it starts within about 5 seconds.
3. About a minute after each song finishes, the app picks up its stems.
Requests run oldest first and survive restarts. stemd matches Plex's paths to its own files by the artist/album/file tail, so different mount points don't matter. Songs that failed before are skipped; delete `state/failed.json` to retry them.
## Reference
### Separation models
| Model id | Stems | Notes |
|---|---|---|
| `htdemucs` | 4 | HT Demucs, fast |
| `htdemucs_ft` | 4 | Fine-tuned, about 4x slower |
| `htdemucs_6s` | 6 | Adds guitar and piano |
| `roformer_bs_vocals` | 2 | BS-RoFormer vocals/instrumental (Viperx 1297) |
| `roformer_mel_kim_ft` | 2 | Mel-RoFormer, cleanest vocals (Kim FT by unwa) |
| `roformer_bs_sw` | 6 | BS-RoFormer SW by jarredou, stemd's default |
### Folder layout
``` ```
https://git.iamchrisama.com/cdnutter/stemify/raw/branch/main/ios/source.json Music/Artist/Album/01 Song.flac
``` Stems/Artist/Album/01 Song/
.stemify.json done marker
After uploading a new release (tag `ios-v<version>`, asset `stemify-<version>.ipa`), run `ios/scripts/make-source.py` to regenerate `ios/source.json` and commit it. To work in Xcode instead, run `xcodegen generate` and open `Stemify.xcodeproj`. 01 Song - vocals.flac
01 Song - drums.flac
CarPlay and Siri need the `carplay-audio` and `siri` entitlements. A free Apple ID can't sign those, so on a free sideload the app works on the phone but won't show up in CarPlay or Siri. A paid developer account covers Siri; CarPlay audio also needs Apple to approve the entitlement for your team.
## The desktop apps
### Stemify (the separator)
Drop files onto the window. Each becomes a queue item, separation starts right away with your
last-used model and device, and when it's done you get a lane per stem with a waveform, solo, mute,
gain and export. Export everything, one stem, or your current mix, as WAV, FLAC or MP3.
Other niceties:
- Separation runs in its own worker process, so the UI never stutters, even mid-separation.
- Works on AMD (ROCm), NVIDIA and CPU, and shows free VRAM.
- Results are cached, so flipping back to an old song is instant (with a size limit you can set).
- A dev frame-time overlay, for anyone who wants proof of the 240 fps claims.
### Stemify Player (the spreadsheet)
The desktop sibling of the phone app, disguised as Microsoft Excel 2016. Some highlights from the
translation guide:
| In Excel | In Stemify Player |
|---|---|
| Sheets (Songs, Artists, Albums, Playlists, Stems, Queue) | Your library |
| "Tell me what you want to do" | Search |
| Zoom slider | Master volume |
| Cell Styles | Mix presets (Karaoke is filed under "Bad", A Cappella under "Good") |
| AutoSum | AutoShuffle |
| Undo / Redo | Previous / next song |
| Format task pane | Now playing, with the stems as a column chart you can drag |
| `#GETTING_DATA` | A playlist loading from Plex |
It reports plays to Plex, supports media keys and desktop media controls (MPRIS), and remembers
your queue, position and mix between launches. The full tour is in
[`stemify/player/README.md`](stemify/player/README.md).
---
## Separation models
| Model id | Name | Stems | Notes |
|---|---|---|---|
| `htdemucs` | HT Demucs | 4 | Fast |
| `htdemucs_ft` | HT Demucs FT | 4 | Fine-tuned, about 4x slower |
| `htdemucs_6s` | HT Demucs 6-stem | 6 | Adds guitar and piano |
| `roformer_bs_vocals` | BS-RoFormer Vocals | 2 | Vocals / instrumental (Viperx 1297) |
| `roformer_mel_kim_ft` | Mel-RoFormer Vocals FT | 2 | Cleanest vocals (Kim FT by unwa) |
| `roformer_bs_sw` | BS-RoFormer SW 6-stem | 6 | stemd's default (by jarredou) |
The 4-stem models give vocals, drums, bass and other. The 6-stem ones add guitar and piano.
---
## How the pieces find each other
The stems folder mirrors the music folder, with one folder per song:
```
Music/Kansas/Leftoverture/01 Carry On Wayward Son.flac
Stems/Kansas/Leftoverture/01 Carry On Wayward Son/
.stemify.json ← the "this one's done" marker
01 Carry On Wayward Son - vocals.flac
01 Carry On Wayward Son - drums.flac
01 Carry On Wayward Son - bass.flac
... ...
``` ```
- Stems are written as 24-bit FLAC into a hidden `.partial` folder first, and the marker goes in - Stems are written as 24-bit FLAC to a hidden `.partial` folder and the marker goes in last, so an interrupted separation is just redone.
last, so a separation interrupted by a reboot is simply redone next time. No half-baked songs. - If a folder has the same song in several formats, the best one wins (FLAC/WAV, then M4A, Opus/Ogg, MP3, WMA).
- When a folder holds the same song in several formats, the best one wins (FLAC/WAV, then M4A, - Plex reports paths as its container sees them, so the apps match songs to stems by path suffix, check durations, and learn the prefix from the first confident match.
Opus/Ogg, MP3, WMA).
- Plex reports file paths as *its* container sees them, so the apps match them to stems by suffix,
checking durations along the way, and learn the path prefix from the first confident match. A
song called "Intro.flac" on one album will never steal another album's intro.
--- ### stemserve API
## stemserve API
| Request | Returns | | Request | Returns |
|---|---| |---|---|
| `GET /api/info` | Track count, whether ffmpeg is available, whether Stem Now is set up | | `GET /api/info` | Track count, ffmpeg availability, whether Stem Now is set up |
| `GET /api/index` | Every finished song (with ETag, so unchanged indexes cost one tiny 304) | | `GET /api/index` | Every finished song (with ETag) |
| `GET /api/peaks?key=K&n=256` | Per-stem loudness envelopes for waveforms, 0 to 1000 | | `GET /api/peaks?key=K&n=256` | Per-stem loudness envelopes for waveforms, 0 to 1000 |
| `POST /api/prepare?key=K&fmt=opus&br=256` | Starts Opus transcodes in the background | | `POST /api/prepare?key=K&fmt=opus&br=256` | Starts Opus transcodes in the background |
| `POST /api/stem` with `{"paths": [...]}` | Stem Now: stemd separates these Plex files next | | `POST /api/stem` with `{"paths": [...]}` | Stem Now for these Plex files |
| `GET /file/K/<file>.flac[?fmt=opus&br=256]` | The stem itself, with HTTP range support for seeking | | `GET /file/K/<file>.flac[?fmt=opus&br=256]` | The stem, with range support |
| `GET /` | The web player from `web/` (or `WEB_DIR`), no token needed |
`K` is the song's folder under the stems root, e.g. `Kansas/Leftoverture/01 Carry On Wayward Son`. `K` is the song's folder under the stems root, like `Artist/Album/01 Song`. Outside Docker:
Want to run it outside Docker? It's one file:
```sh ```sh
STEMS_DIR=/path/to/Stems CACHE_DIR=~/.cache/stemserve python3 server/stemserve.py STEMS_DIR=/path/to/Stems CACHE_DIR=~/.cache/stemserve python3 server/stemserve.py
``` ```
--- ### Repo layout
```
ios/ iOS and CarPlay app (SwiftUI, xcodegen project)
Stemify/ App, Plex, Playback, Stems, Engine, EQ, CarPlay, UI
scripts/ build-ipa.sh, make-source.py
android/ Android and Android Auto app (Kotlin, Compose, Media3)
server/ stemd.py, stemserve.py, Dockerfiles, compose file
web/ web player (served by stemserve)
desktop/ desktop apps (uv project)
stemify/ separator (PySide6 + QML) and shared separation code
worker/ Demucs and RoFormer backends, run in a separate process
player/ Stemify Player
library_core.py folder layout rules shared with stemd
scripts/ install-desktop.sh
docs/specs.txt original design doc for the desktop separator
```
## Building and testing ## Building and testing
**iOS** needs a Mac with Xcode and xcodegen (`brew install xcodegen`):
```sh
cd ios
scripts/build-ipa.sh # unsigned build/public/stemify-<version>.ipa
scripts/build-remote.sh # from DevOps: build on White-M1 and pull IPAs back to build/{public,prov}
xcodegen generate # or open Stemify.xcodeproj in Xcode
```
The version lives in `ios/project.yml` (`MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`). To release, tag `ios-v<version>`, attach `stemify-<version>.ipa`, then run `GITEA_TOKEN=... ios/scripts/make-source.py` and commit the new `ios/source.json`.
**Android** needs JDK 21 and the Android SDK (`android/local.properties` points at it): **Android** needs JDK 21 and the Android SDK (`android/local.properties` points at it):
```sh ```sh
cd android cd android
JAVA_HOME=/path/to/jdk-21 ./gradlew assembleRelease # app/build/outputs/apk/release/ JAVA_HOME=/path/to/jdk-21 ./gradlew assembleRelease # app/build/outputs/apk/release/
JAVA_HOME=/path/to/jdk-21 ./gradlew testDebugUnitTest # stem matching, EQ and filter maths, limiter, cache, car tree JAVA_HOME=/path/to/jdk-21 ./gradlew testDebugUnitTest # matching, EQ maths, limiter, cache, car tree
``` ```
The cache and index tests stream real stems from a stemserve on `localhost:8765` and skip The cache and index tests use a stemserve on `localhost:8765` and skip themselves without one.
themselves when there isn't one. Debug builds include a hook for screenshots and back-gesture
testing (`app/src/debug`); release builds don't.
**Desktop** apps can be checked without opening a window: **Desktop** can be checked without a window:
```sh ```sh
cd desktop
QT_QPA_PLATFORM=offscreen uv run stemify-player --no-mpris --screenshot /tmp/p.png@5 --quit-after 6 QT_QPA_PLATFORM=offscreen uv run stemify-player --no-mpris --screenshot /tmp/p.png@5 --quit-after 6
``` ```
--- ## Troubleshooting
## Repo tour **A song says NO STEMS.** Hold it and choose Stem Now.
``` **Stem Now says "stemserve needs updating" or "queue folder isn't mounted".** stemserve is older than the app or is missing the `/queue` mount. The queue folder has to be writable by the container user before the container first starts, or Docker creates it as root.
android/ the Android and Android Auto app (Kotlin, Jetpack Compose, Media3)
app/src/main/java/com/stemify/mobile/
plex/ Plex library, search, playlists, artwork, play reporting
stems/ stemserve client, path matching, which albums have stems
cache/ sparse block cache for streaming, and downloads
engine/ decoders, the sample-locked mixer, EQ, limiter, meters
eq/ EQ profiles, device switching, APO config import/export
playback/ queue, audio focus, mix presets, the Media3 session
auto/ Android Auto browse tree, voice search, offline mode
ui/ every screen, hand-built iOS-style components
server/
stemd.py the GPU separation daemon
stemserve.py the stem file server (standard library only)
*.Dockerfile, docker-compose.yml
stemify/ the desktop separator (PySide6 + QML) and shared separation code
worker/ separation backends (Demucs, RoFormer) in a separate process
player/ Stemify Player, the Excel-shaped one
library_core.py the folder layout rules shared with stemd
scripts/install-desktop.sh
specs.txt the original design doc for the desktop separator
```
--- **stemd is separating on the CPU.** The container can't see the GPU. Run it with `--runtime=nvidia` and the NVIDIA variables above.
## Troubleshooting and FAQ **stemd says "out of GPU memory".** Something else is using VRAM. stemd retries that song next pass without marking it failed.
**A song says NO STEMS and I'm impatient.** **The stem server shows Offline.** Check the address in Settings, try `curl http://<server>:8765/api/info` from another machine, and if stemserve has a `TOKEN`, set the same one in the app.
Hold it and choose **Stem Now**. It'll be next in line.
**Stem Now says "stemserve needs updating" or "queue folder isn't mounted".** **Stemify isn't in CarPlay or Siri.** The IPA was signed without those entitlements. See [iOS install](#install).
stemserve is older than the app, or it's missing the `/queue` mount. See
[the server setup](#1-the-server-stemd-and-stemserve). The queue folder needs to be owned by
`99:100` *before* the container first starts, or Docker creates it as root and nobody can write
to it.
**stemd says it's separating on the CPU.** **Stemify isn't in Android Auto.** Turn on Unknown sources in Auto's developer settings. See [Android Auto](#android-auto).
The container can't see the GPU. Make sure it's running with `--runtime=nvidia` and the NVIDIA
environment variables shown above.
**stemd keeps saying "out of GPU memory".** **Songs listed in `state/failed.json`.** Usually corrupt files or "songs" over 40 minutes (DJ mixes, radio rips). They retry when the file changes; delete the file to retry everything.
Something else (Plex transcoding, a game server, your ambitions) is hogging VRAM. stemd shrugs and
retries that song on the next pass. Nothing is marked failed.
**The Stem Server page says Offline.**
Check the address in Settings → Stem Server, check `curl http://<server>:8765/api/info` from
another machine, and if you set a `TOKEN` on the server, set the same one in the app.
**The app doesn't show up in Android Auto.**
Sideloaded apps need **Unknown sources** turned on in Auto's developer settings. See
[setting up the car](#android-auto).
**Some songs are marked failed in `state/failed.json`.**
Usually corrupt files, or "songs" over 40 minutes (those are almost always DJ mixes or radio rips).
Failed tracks retry automatically when the file changes; delete the file to retry everything.
**Why is the desktop player an Excel spreadsheet?**
We don't talk about it.
---
## Credits ## Credits
Standing on the shoulders of some very clever people:
- [Demucs](https://github.com/adefossez/demucs) by Alexandre Défossez - [Demucs](https://github.com/adefossez/demucs) by Alexandre Défossez
- BS-RoFormer and Mel-RoFormer models, including the SW 6-stem model by jarredou, Viperx's 1297, - BS-RoFormer and Mel-RoFormer models (SW 6-stem by jarredou, Viperx 1297, Kim FT by unwa) via [python-audio-separator](https://github.com/nomadkaraoke/python-audio-separator)
and Kim's vocal model fine-tuned by unwa, via - [PyTorch](https://pytorch.org/), [PySide6](https://www.qt.io/qt-for-python), [Jetpack Compose](https://developer.android.com/compose), [Media3](https://developer.android.com/media/media3)
[python-audio-separator](https://github.com/nomadkaraoke/python-audio-separator) - [Equalizer APO](https://sourceforge.net/projects/equalizerapo/) and [AutoEQ](https://github.com/jaakkopasanen/AutoEq), whose formats the EQ reads and writes
- [PyTorch](https://pytorch.org/), [PySide6 / Qt](https://www.qt.io/qt-for-python), - [LRCLIB](https://lrclib.net) for synced lyrics
[Jetpack Compose](https://developer.android.com/compose) and - Inter, Selawik and Carlito fonts (licenses included alongside them)
[Media3](https://developer.android.com/media/media3)
- [Equalizer APO](https://sourceforge.net/projects/equalizerapo/) and
[AutoEQ](https://github.com/jaakkopasanen/AutoEq), whose formats the EQ speaks
- The Inter, Selawik and Carlito fonts (licences included alongside them)
- [Plex](https://www.plex.tv/), for keeping the library tidy so we didn't have to
Stemify isn't affiliated with Plex, Google, Microsoft or Mazda. It just likes them. Stemify isn't affiliated with Plex, Apple, Google or Microsoft.
---
## License ## License
[GPL-3.0](LICENSE). Take it apart and put it back together however you like, just like the songs. [GPL-3.0](LICENSE)
+2
View File
@@ -0,0 +1,2 @@
# TODO
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -12,6 +12,7 @@ import os
import shutil import shutil
import subprocess import subprocess
import time import time
import unicodedata
import numpy as np import numpy as np
import soundfile as sf import soundfile as sf
@@ -105,3 +106,49 @@ def write_track(dest, stems, meta):
for name in [s["file"] for s in written] + [MARKER]: for name in [s["file"] for s in written] + [MARKER]:
os.replace(os.path.join(part, name), os.path.join(dest, name)) os.replace(os.path.join(part, name), os.path.join(dest, name))
os.rmdir(part) os.rmdir(part)
def _norm(path):
return unicodedata.normalize("NFC", os.path.normpath(path)).casefold()
def find_stale(src, dst):
"""Stems folders that no longer match the library.
Returns (orphans, changed): orphans have no source track any more (removed, renamed or
moved); changed still have one, but its size or mtime differs from the marker's, so the
stems are of an older version of the song.
"""
expected = {_norm(d): p for p, d in iter_tracks(src, dst)}
orphans, changed = [], []
for root, dirs, files in os.walk(dst):
dirs[:] = [d for d in dirs if not d.startswith(".")]
if MARKER not in files:
continue
dirs[:] = []
source = expected.get(_norm(root))
if source is None:
orphans.append(root)
continue
try:
with open(os.path.join(root, MARKER)) as f:
m = json.load(f)
if [m["size"], m["mtime_ns"]] != signature(source):
changed.append(root)
except (OSError, ValueError, KeyError):
pass # unreadable marker or source: leave it alone
return sorted(orphans), sorted(changed)
def remove_stems(dst, folders):
"""Delete stems folders, then any parent folders left empty (never dst itself)."""
dst = os.path.realpath(dst)
for folder in folders:
shutil.rmtree(folder)
parent = os.path.dirname(os.path.realpath(folder))
while parent != dst and parent.startswith(dst + os.sep):
try:
os.rmdir(parent)
except OSError:
break
parent = os.path.dirname(parent)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff. Show more