Compare commits
35
Commits
ios-v1.0.16
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47514127d8 | ||
|
|
5a7a36b898 | ||
|
|
f9933a4883 | ||
|
|
ec1cbdfe75 | ||
|
|
fabca5d1aa | ||
|
|
e8bc920454 | ||
|
|
08f7af9421 | ||
|
|
f4af7b6cf8 | ||
|
|
d996d3e236 | ||
|
|
4f2dcb94f5 | ||
|
|
dfdd9a8ae5 | ||
|
|
db55b5eea0 | ||
|
|
cf5f992e60 | ||
|
|
f099682a88 | ||
|
|
9415bfb50f | ||
|
|
ee6ca41fd3 | ||
|
|
abb157d140 | ||
|
|
2fb8040388 | ||
|
|
c0e33b5e3e | ||
|
|
0fc35b13e8 | ||
|
|
5b7fadb7f7 | ||
|
|
3e832e3125 | ||
|
|
a4c870251c | ||
|
|
7c8dfb54ca | ||
|
|
df5d847169 | ||
|
|
894dd5a695 | ||
|
|
7cdfbef160 | ||
|
|
48faa6682f | ||
|
|
a1cf0a00b3 | ||
|
|
4277688a38 | ||
|
|
455d9a790c | ||
|
|
4ccb5051ca | ||
|
|
e35ebedb56 | ||
|
|
d69c44eff9 | ||
|
|
1cf9341cda |
No files matched your search
+2
-2
@@ -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.
@@ -0,0 +1,35 @@
|
|||||||
|
# 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).
|
||||||
|
2026-11-09 iOS: Duo split now engages with the phone lying flat (hinge region reports inactive then), with a screen-size fallback if no region is reported. 1.0 RC 4 (build 24).
|
||||||
|
2026-11-09 iOS: dev builds (`ios/scripts/build-remote.sh dev`): bundle id org.cdnutter.stemify-dev, "Stemify Dev", own App Group, into ios/build/dev. Internal tests only, never on Gitea or Sideload.
|
||||||
|
2026-11-09 iOS: Duo unfolded redesign. Tabs move to a rail on the right edge by the status bar, Now Playing fills the screen with stems and queue on the left and the player on the right, picking a tab swaps the left pane, Swap flips the sides (remembered). Folded screen unchanged.
|
||||||
|
2026-11-09 iOS: build-remote.sh now builds dev + prov by default; public only with `public`.
|
||||||
@@ -20,7 +20,7 @@ It runs on iPhone (with CarPlay), Android (with Android Auto) and Linux desktop.
|
|||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
The stems folder mirrors the music folder. If a song lives at `Kansas/Leftoverture/01 Carry On.flac`, its stems live in `Stems/Kansas/Leftoverture/01 Carry On/`.
|
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/`.
|
||||||
|
|
||||||
```
|
```
|
||||||
your server
|
your server
|
||||||
@@ -63,48 +63,48 @@ Mix presets are Full, Karaoke, A Cappella, No Drums, Rhythm, No Guitar and No Ba
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Set up the server first, then the apps. The examples use Unraid paths and `192.168.86.2` for the server; use your own.
|
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.
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
You need Docker, and an NVIDIA GPU for stemd (on Unraid, install the Nvidia-Driver plugin). A CPU build exists for testing, but it's far too slow for a whole library.
|
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.
|
||||||
|
|
||||||
Make the folders, owned by `99:100` (Unraid's nobody:users):
|
Make the folders, owned by whatever user the containers run as (`1000:1000` here):
|
||||||
|
|
||||||
```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 uses the `stemify` package. Copy the repo (at least `stemify/`, `server/` and `.dockerignore`) to `/mnt/user/appdata/stemd/src`, then:
|
**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:
|
||||||
|
|
||||||
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
**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 `/mnt/user/appdata/stemserve` and run `docker compose up -d --build` in `server/`, or by hand:
|
**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:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build -f /mnt/user/appdata/stemserve/server/Dockerfile -t stemserve /mnt/user/appdata/stemserve
|
docker build -f /srv/stemify/stemserve/server/Dockerfile -t stemserve /srv/stemify/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
|
||||||
```
|
```
|
||||||
|
|
||||||
`curl http://192.168.86.2:8765/api/info` should return JSON with a track count.
|
`curl http://SERVER_IP:8765/api/info` should return JSON with a track count.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -123,6 +123,10 @@ To update, copy the new files, rebuild, `docker rm -f` the container and run it
|
|||||||
| `MODEL` | `roformer_bs_sw` | Any id from [the model table](#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 caught up |
|
| `RESCAN_MIN` | `30` | Minutes between library scans once caught up |
|
||||||
|
| `PRUNE` | `1` | Remove stems whose song is gone from the library, at start and every `RESCAN_MIN` |
|
||||||
|
| `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) |
|
| `FP16` | `1` | Half precision on the GPU (`0` for fp32) |
|
||||||
| `OVERLAP`, `SEGMENT`, `SHIFTS` | `0.25`, `0`, `0` | Separation quality, same as the desktop app |
|
| `OVERLAP`, `SEGMENT`, `SHIFTS` | `0.25`, `0`, `0` | Separation quality, same as the desktop app |
|
||||||
|
|
||||||
@@ -155,14 +159,14 @@ The apps check stemserve for newly separated songs every minute, so the library
|
|||||||
|
|
||||||
## iOS
|
## iOS
|
||||||
|
|
||||||
A SwiftUI app for iOS 17 or newer, with CarPlay and Siri ("play Paranoid in Stemify").
|
A SwiftUI app for iOS 17 or newer, with CarPlay and Siri ("play <song> in Stemify").
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
Grab `stemify-<version>.ipa` from [Releases](https://git.iamchrisama.com/cdnutter/stemify/releases) and sideload it with AltStore, SideStore, Sideloadly or Feather. To get updates in AltStore, SideStore or Feather, add this source:
|
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:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://git.iamchrisama.com/cdnutter/stemify/raw/branch/main/ios/source.json
|
https://<your-git-host>/<owner>/stemify/raw/branch/main/ios/source.json
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
@@ -186,7 +190,7 @@ CarPlay and Siri need the `carplay-audio` and `siri` entitlements, which a free
|
|||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
Kotlin and Jetpack Compose, built and tested on a OnePlus 15. Grab `Stemify.apk` from Releases (or [build it](#building-and-testing)) and `adb install -r Stemify.apk`.
|
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`.
|
||||||
|
|
||||||
### Using it
|
### Using it
|
||||||
|
|
||||||
@@ -194,7 +198,7 @@ Kotlin and Jetpack Compose, built and tested on a OnePlus 15. Grab `Stemify.apk`
|
|||||||
- **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.
|
- **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.
|
||||||
- **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.
|
- **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).
|
- **Stem Quality:** lossless FLAC, or Opus at a bitrate you choose (about an eighth of the size, transcoded and cached on the server).
|
||||||
- **Streaming** needs your home network, since Plex and stemserve are plain HTTP on the LAN. Downloads and already-streamed songs play anywhere.
|
- **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.
|
||||||
|
|
||||||
### Equalizer
|
### Equalizer
|
||||||
|
|
||||||
@@ -209,12 +213,12 @@ Profiles are tied to outputs (speaker, wired, each Bluetooth device, USB DACs) a
|
|||||||
|
|
||||||
### Android Auto
|
### Android Auto
|
||||||
|
|
||||||
Built for a 2016 Mazda MX-5 with Mazda Connect: 800×480, no touch, knob and wheel buttons only.
|
Designed for small non-touch head units (800×480, knob and wheel buttons only), and it works the same on larger screens.
|
||||||
|
|
||||||
- Tabs for Playlists, Albums, Artists and Downloads (Downloads first when you're away from home).
|
- Tabs for Playlists, Albums, Artists and Downloads (Downloads first when you're away from home).
|
||||||
- Every song list starts with Shuffle all, and lists over 40 items split into A to Z folders.
|
- Every song list starts with Shuffle all, and lists over 40 items split into A to Z folders.
|
||||||
- Now Playing has a Vocals on/off button and a Mix button that cycles presets. The mix shows on the artist line, like "Black Sabbath · Karaoke".
|
- 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".
|
||||||
- Voice: "play Paranoid on Stemify" works for playlists, artists, albums and songs.
|
- Voice: "play <song> on Stemify" works for playlists, artists, albums and songs.
|
||||||
- Away from home it notices the server is gone and only lists songs that will play.
|
- Away from home it notices the server is gone and only lists songs that will play.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
@@ -240,9 +244,10 @@ It's plain HTML and JavaScript in `web/` with no build step. To run it somewhere
|
|||||||
|
|
||||||
## Desktop
|
## Desktop
|
||||||
|
|
||||||
Linux, Python 3.12 and [uv](https://docs.astral.sh/uv/). PyTorch defaults to ROCm (AMD); point the torch index in `pyproject.toml` elsewhere for NVIDIA or CPU.
|
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.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
cd desktop
|
||||||
uv sync
|
uv sync
|
||||||
uv run stemify # the drag-and-drop separator
|
uv run stemify # the drag-and-drop separator
|
||||||
uv run stemify-player # the player
|
uv run stemify-player # the player
|
||||||
@@ -251,7 +256,7 @@ uv run stemify-player # the player
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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 [`stemify/player/README.md`](stemify/player/README.md).
|
**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
|
## Stem Now
|
||||||
|
|
||||||
@@ -279,11 +284,11 @@ Requests run oldest first and survive restarts. stemd matches Plex's paths to it
|
|||||||
### Folder layout
|
### Folder layout
|
||||||
|
|
||||||
```
|
```
|
||||||
Music/Kansas/Leftoverture/01 Carry On Wayward Son.flac
|
Music/Artist/Album/01 Song.flac
|
||||||
Stems/Kansas/Leftoverture/01 Carry On Wayward Son/
|
Stems/Artist/Album/01 Song/
|
||||||
.stemify.json done marker
|
.stemify.json done marker
|
||||||
01 Carry On Wayward Son - vocals.flac
|
01 Song - vocals.flac
|
||||||
01 Carry On Wayward Son - drums.flac
|
01 Song - drums.flac
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -303,7 +308,7 @@ Stems/Kansas/Leftoverture/01 Carry On Wayward Son/
|
|||||||
| `GET /file/K/<file>.flac[?fmt=opus&br=256]` | The stem, with range support |
|
| `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 |
|
| `GET /` | The web player from `web/` (or `WEB_DIR`), no token needed |
|
||||||
|
|
||||||
`K` is the song's folder under the stems root, like `Kansas/Leftoverture/01 Carry On Wayward Son`. Outside Docker:
|
`K` is the song's folder under the stems root, like `Artist/Album/01 Song`. Outside Docker:
|
||||||
|
|
||||||
```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
|
||||||
@@ -318,12 +323,13 @@ ios/ iOS and CarPlay app (SwiftUI, xcodegen project)
|
|||||||
android/ Android and Android Auto app (Kotlin, Compose, Media3)
|
android/ Android and Android Auto app (Kotlin, Compose, Media3)
|
||||||
server/ stemd.py, stemserve.py, Dockerfiles, compose file
|
server/ stemd.py, stemserve.py, Dockerfiles, compose file
|
||||||
web/ web player (served by stemserve)
|
web/ web player (served by stemserve)
|
||||||
stemify/ desktop separator (PySide6 + QML) and shared separation code
|
desktop/ desktop apps (uv project)
|
||||||
|
stemify/ separator (PySide6 + QML) and shared separation code
|
||||||
worker/ Demucs and RoFormer backends, run in a separate process
|
worker/ Demucs and RoFormer backends, run in a separate process
|
||||||
player/ Stemify Player
|
player/ Stemify Player
|
||||||
library_core.py folder layout rules shared with stemd
|
library_core.py folder layout rules shared with stemd
|
||||||
scripts/ install-desktop.sh
|
scripts/ install-desktop.sh
|
||||||
specs.txt original design doc for the desktop separator
|
docs/specs.txt original design doc for the desktop separator
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building and testing
|
## Building and testing
|
||||||
@@ -332,11 +338,20 @@ specs.txt original design doc for the desktop separator
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd ios
|
cd ios
|
||||||
scripts/build-ipa.sh # unsigned build/stemify-<version>.ipa
|
scripts/build-remote.sh # from DevOps: dev build on WHITE-M1, pulled back to build/dev
|
||||||
|
scripts/build-remote.sh release # release build, build/release
|
||||||
|
scripts/build-remote.sh prov # personal signed build, build/prov
|
||||||
|
scripts/build-ipa.sh release|dev|prov # same thing directly on the Mac
|
||||||
xcodegen generate # or open Stemify.xcodeproj in Xcode
|
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`.
|
There are three builds, each with its own bundle ID so they install side by side:
|
||||||
|
|
||||||
|
- `org.cdnutter.stemify-pub` (build/release): releases from `main`. `scripts/publish.sh release` tags `ios-v<version>`, uploads the IPA to Gitea and Sideload, and regenerates `ios/source.json`.
|
||||||
|
- `org.cdnutter.stemify-dev` (build/dev): ribbon icon, built from the `dev` branch. `scripts/publish.sh dev` makes a Gitea pre-release `ios-v<version>-dev.<build>` and uploads it to Sideload Developer. Bump `CURRENT_PROJECT_VERSION` for each one.
|
||||||
|
- `org.cdnutter.stemify` (build/prov): signed with the personal profile in `ios/prov/` so CarPlay and Siri work. Never published.
|
||||||
|
|
||||||
|
The version lives in `ios/project.yml` (`VERSION_LABEL`, `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`).
|
||||||
|
|
||||||
**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):
|
||||||
|
|
||||||
@@ -351,6 +366,7 @@ The cache and index tests use a stemserve on `localhost:8765` and skip themselve
|
|||||||
**Desktop** can be checked without 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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -358,7 +374,7 @@ QT_QPA_PLATFORM=offscreen uv run stemify-player --no-mpris --screenshot /tmp/p.p
|
|||||||
|
|
||||||
**A song says NO STEMS.** Hold it and choose Stem Now.
|
**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 owned by `99:100` before the container first starts, or Docker creates it as root.
|
**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.
|
||||||
|
|
||||||
**stemd is separating on the CPU.** The container can't see the GPU. Run it with `--runtime=nvidia` and the NVIDIA variables above.
|
**stemd is separating on the CPU.** The container can't see the GPU. Run it with `--runtime=nvidia` and the NVIDIA variables above.
|
||||||
|
|
||||||
@@ -381,7 +397,7 @@ QT_QPA_PLATFORM=offscreen uv run stemify-player --no-mpris --screenshot /tmp/p.p
|
|||||||
- [LRCLIB](https://lrclib.net) for synced lyrics
|
- [LRCLIB](https://lrclib.net) for synced lyrics
|
||||||
- Inter, Selawik and Carlito fonts (licenses included alongside them)
|
- Inter, Selawik and Carlito fonts (licenses included alongside them)
|
||||||
|
|
||||||
Stemify isn't affiliated with Plex, Apple, Google, Microsoft or Mazda.
|
Stemify isn't affiliated with Plex, Apple, Google or Microsoft.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
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.
@@ -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.
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.
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 182 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user