Rename everything to Stemify, and add a README
- Python package stemroller/ -> stemify/, Android package com.stemroller.mobile -> com.stemify.mobile, app names, QML module, icons, desktop entries, MPRIS and Plex product names - Done-markers are now .stemify.json; stemd renames the old ones once at startup so songs separated before the rename aren't separated again - A top-level README covering the server, the Android and Android Auto app, the EQ, Stem Now and the desktop apps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1845f03c41
commit
45bc0eefe0
159 files changed
+1136
-542
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
|
||||||
!stemroller/*.py
|
!stemify/*.py
|
||||||
!stemroller/worker/*.py
|
!stemify/worker/*.py
|
||||||
@@ -0,0 +1,562 @@
|
|||||||
|
# Stemify
|
||||||
|
|
||||||
|
**Your whole music library, taken apart and put back together however you like.**
|
||||||
|
|
||||||
|
Ever wanted to hear "Carry On Wayward Son" without the vocals so you can belt it out yourself? Or
|
||||||
|
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
|
||||||
|
pressing the "mute vocals" button at exactly the wrong moment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [The big picture](#the-big-picture)
|
||||||
|
- [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)
|
||||||
|
- [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)
|
||||||
|
- [Repo tour](#repo-tour)
|
||||||
|
- [Troubleshooting and FAQ](#troubleshooting-and-faq)
|
||||||
|
- [Credits](#credits)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The big picture
|
||||||
|
|
||||||
|
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) ───────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ Music/ ──read──▶ stemd (GPU) ──writes──▶ Stems/ ──read──▶ stemserve (:8765) ─────┼──▶ phone, car, desktop
|
||||||
|
│ ▲ ▲ │ │
|
||||||
|
│ │ └──────── "Stem Now" requests ◀──────────────┘ │
|
||||||
|
│ 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,
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
**On your phone (Android)**
|
||||||
|
- 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)**
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
**On your desktop (Linux)**
|
||||||
|
- **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**
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setting it up
|
||||||
|
|
||||||
|
The recommended order is server first, then phone, then (if you like) desktop. The examples below
|
||||||
|
use unraid paths and the addresses from the original setup (`192.168.86.2`). Swap in your own.
|
||||||
|
|
||||||
|
### 1. The server: stemd and stemserve
|
||||||
|
|
||||||
|
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
|
||||||
|
mkdir -p /mnt/user/appdata/stemserve/cache \
|
||||||
|
/mnt/user/appdata/stemd/models \
|
||||||
|
/mnt/user/appdata/stemd/state/queue
|
||||||
|
chown -R 99:100 /mnt/user/appdata/stemserve /mnt/user/appdata/stemd
|
||||||
|
```
|
||||||
|
|
||||||
|
**stemd** builds from the repo root because it borrows the `stemify` package. Copy the repo (or
|
||||||
|
at least `stemify/`, `server/` and `.dockerignore`) to `/mnt/user/appdata/stemd/src`, then:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -f /mnt/user/appdata/stemd/src/server/stemd.Dockerfile -t stemd /mnt/user/appdata/stemd/src
|
||||||
|
docker run -d --name stemd --restart unless-stopped --stop-timeout 60 \
|
||||||
|
--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
|
||||||
|
-v "/mnt/user/Music/Actual music/Flacs":/music:ro \
|
||||||
|
-v /mnt/user/Music/Stems:/stems \
|
||||||
|
-v /mnt/user/appdata/stemd/models:/models \
|
||||||
|
-v /mnt/user/appdata/stemd/state:/state \
|
||||||
|
stemd
|
||||||
|
docker logs -f stemd
|
||||||
|
```
|
||||||
|
|
||||||
|
The first song downloads the model (about 700 MB). After that the log reads like a very calm
|
||||||
|
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
|
||||||
|
the included compose file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /mnt/user/appdata/stemserve && docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
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 \
|
||||||
|
-v /mnt/user/Music/Stems:/stems:ro \
|
||||||
|
-v /mnt/user/appdata/stemserve/cache:/cache \
|
||||||
|
-v /mnt/user/appdata/stemd/state/queue:/queue \
|
||||||
|
stemserve
|
||||||
|
```
|
||||||
|
|
||||||
|
Check it from any machine on your network:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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>
|
||||||
|
<summary><b>Server settings (environment variables)</b></summary>
|
||||||
|
|
||||||
|
**stemd**
|
||||||
|
|
||||||
|
| Variable | Default | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| `MUSIC_DIR` | `/music` | Your music library (read-only is fine) |
|
||||||
|
| `STEMS_DIR` | `/stems` | Where stems go |
|
||||||
|
| `STATE_DIR` | `/state` | The failed-track list lives here |
|
||||||
|
| `QUEUE_DIR` | `STATE_DIR/queue` | Where "Stem Now" requests arrive |
|
||||||
|
| `MODELS_DIR` | `/models` | Model weights, downloaded on first use |
|
||||||
|
| `MODEL` | `roformer_bs_sw` | Any model id from [the table below](#separation-models) |
|
||||||
|
| `DEVICE` | `cuda` | `cuda`, `cuda:N` or `cpu` |
|
||||||
|
| `RESCAN_MIN` | `30` | Minutes between library scans once it's caught up |
|
||||||
|
| `FP16` | `1` | Half precision on the GPU (`0` for full fp32) |
|
||||||
|
| `OVERLAP`, `SEGMENT`, `SHIFTS` | `0.25`, `0`, `0` | Separation quality knobs, same as the desktop app |
|
||||||
|
|
||||||
|
`docker build --build-arg TORCH_INDEX=cpu ...` makes a small GPU-less image for testing.
|
||||||
|
|
||||||
|
**stemserve**
|
||||||
|
|
||||||
|
| Variable | Default | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| `STEMS_DIR` | `/stems` | The stems folder |
|
||||||
|
| `CACHE_DIR` | `/cache` | Opus transcodes and waveforms (safe to delete any time) |
|
||||||
|
| `QUEUE_DIR` | `/queue` | stemd's "Stem Now" folder, shared with it |
|
||||||
|
| `PORT` | `8765` | Listen port |
|
||||||
|
| `TOKEN` | empty | If set, every request needs `X-Stem-Token` or `?token=` |
|
||||||
|
| `INDEX_TTL` | `30` | Seconds between rescans of the stems folder |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
**Updating later:** copy the new files over, rebuild the image, `docker rm -f` the container, and
|
||||||
|
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
|
||||||
|
|
||||||
|
Built for (and lovingly tested on) a OnePlus 15, but any recent Android phone should be happy.
|
||||||
|
|
||||||
|
1. Grab `Stemify.apk` from the Releases page, or [build it yourself](#building-and-testing),
|
||||||
|
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
|
||||||
|
up" as stemd works through it.
|
||||||
|
|
||||||
|
### 3. The desktop apps (optional)
|
||||||
|
|
||||||
|
Linux, Python 3.12 and [uv](https://docs.astral.sh/uv/). The default setup uses PyTorch for ROCm
|
||||||
|
(AMD GPUs, developed on an RX 9070 XT). NVIDIA and CPU work too if you point the torch index
|
||||||
|
elsewhere in `pyproject.toml`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv sync
|
||||||
|
uv run stemify # the drag-and-drop separator
|
||||||
|
uv run stemify-player # the Excel-shaped music player
|
||||||
|
./scripts/install-desktop.sh # adds both to your app grid ("--uninstall" removes them)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using the Android app
|
||||||
|
|
||||||
|
**Tabs:** Library, Playlists, Search, Downloads and Settings.
|
||||||
|
|
||||||
|
**Stems are the norm, so the exceptions are what's marked.** Songs without stems yet say
|
||||||
|
**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:
|
||||||
|
|
||||||
|
- **Add to Front of Queue** / **Add to End of Queue**
|
||||||
|
- **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,
|
||||||
|
meters, master volume) and the queue. Your mix can carry over from song to song or reset each
|
||||||
|
time, your choice.
|
||||||
|
|
||||||
|
**Downloads** are proper offline copies: every stem fully fetched (or the original file for songs
|
||||||
|
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
|
||||||
|
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
|
||||||
|
you've downloaded or already streamed plays anywhere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stem Now: skipping the line
|
||||||
|
|
||||||
|
stemd works through your library on its own, but a big library takes days. If you're desperate to
|
||||||
|
karaoke your way through an album tonight, hold it and choose **Stem Now**.
|
||||||
|
|
||||||
|
Here's what happens:
|
||||||
|
|
||||||
|
1. The app sends the songs that still lack stems to stemserve.
|
||||||
|
2. stemserve drops a little request file into stemd's queue folder.
|
||||||
|
3. stemd finishes the song it's on, then does yours next, ahead of everything else. If it was
|
||||||
|
idle, it starts within about 5 seconds.
|
||||||
|
4. About a minute after each song finishes, the app notices and plays it with stems from then on.
|
||||||
|
|
||||||
|
Requests are handled oldest first and survive restarts. stemd matches the paths Plex reports to
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
adb forward tcp:5277 tcp:5277
|
||||||
|
~/Android/Sdk/extras/google/auto/desktop-head-unit -c ~/Android/Sdk/extras/google/auto/config/rotary.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
last, so a separation interrupted by a reboot is simply redone next time. No half-baked songs.
|
||||||
|
- When a folder holds the same song in several formats, the best one wins (FLAC/WAV, then M4A,
|
||||||
|
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
|
||||||
|
|
||||||
|
| Request | Returns |
|
||||||
|
|---|---|
|
||||||
|
| `GET /api/info` | Track count, whether ffmpeg is available, whether Stem Now is set up |
|
||||||
|
| `GET /api/index` | Every finished song (with ETag, so unchanged indexes cost one tiny 304) |
|
||||||
|
| `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/stem` with `{"paths": [...]}` | Stem Now: stemd separates these Plex files next |
|
||||||
|
| `GET /file/K/<file>.flac[?fmt=opus&br=256]` | The stem itself, with HTTP range support for seeking |
|
||||||
|
|
||||||
|
`K` is the song's folder under the stems root, e.g. `Kansas/Leftoverture/01 Carry On Wayward Son`.
|
||||||
|
Want to run it outside Docker? It's one file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
STEMS_DIR=/path/to/Stems CACHE_DIR=~/.cache/stemserve python3 server/stemserve.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building and testing
|
||||||
|
|
||||||
|
**Android** needs JDK 21 and the Android SDK (`android/local.properties` points at it):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd android
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
The cache and index tests stream real stems from a stemserve on `localhost:8765` and skip
|
||||||
|
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:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
QT_QPA_PLATFORM=offscreen uv run stemify-player --no-mpris --screenshot /tmp/p.png@5 --quit-after 6
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repo tour
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting and FAQ
|
||||||
|
|
||||||
|
**A song says NO STEMS and I'm impatient.**
|
||||||
|
Hold it and choose **Stem Now**. It'll be next in line.
|
||||||
|
|
||||||
|
**Stem Now says "stemserve needs updating" or "queue folder isn't mounted".**
|
||||||
|
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.**
|
||||||
|
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".**
|
||||||
|
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
|
||||||
|
|
||||||
|
Standing on the shoulders of some very clever people:
|
||||||
|
|
||||||
|
- [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,
|
||||||
|
and Kim's vocal model fine-tuned by unwa, via
|
||||||
|
[python-audio-separator](https://github.com/nomadkaraoke/python-audio-separator)
|
||||||
|
- [PyTorch](https://pytorch.org/), [PySide6 / Qt](https://www.qt.io/qt-for-python),
|
||||||
|
[Jetpack Compose](https://developer.android.com/compose) and
|
||||||
|
[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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[GPL-3.0](LICENSE). Take it apart and put it back together however you like, just like the songs.
|
||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
# StemRoller for Android
|
# Stemify for Android
|
||||||
|
|
||||||
A Plex music player for the OnePlus 15 that plays the stems stemd separates on your
|
A Plex music player for the OnePlus 15 that plays the stems stemd separates on your
|
||||||
unraid server: per-stem faders, mute/solo, karaoke-style presets, a master volume drawn
|
unraid server: per-stem faders, mute/solo, karaoke-style presets, a master volume drawn
|
||||||
@@ -8,10 +8,10 @@ iOS look in Black, Dark or White (flat colours, no gradients).
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
`../StemRoller.apk` is a release build signed with the debug key, ready to sideload:
|
`../Stemify.apk` is a release build signed with the debug key, ready to sideload:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
adb install -r ../StemRoller.apk
|
adb install -r ../Stemify.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
## First run
|
## First run
|
||||||
@@ -87,7 +87,7 @@ that's what is shaped for the knob (`auto/`, `playback/PlaybackService.kt`):
|
|||||||
shows on the artist line ("Black Sabbath · Karaoke"), since a button press gets no other
|
shows on the artist line ("Black Sabbath · Karaoke"), since a button press gets no other
|
||||||
feedback. Songs without stems just get shuffle and repeat, and say "No stems" in the lists. The same buttons appear on the
|
feedback. Songs without stems just get shuffle and repeat, and say "No stems" in the lists. The same buttons appear on the
|
||||||
phone's lock-screen controls.
|
phone's lock-screen controls.
|
||||||
- **Voice:** hold the wheel's talk button: "play Paranoid on StemRoller" (a playlist,
|
- **Voice:** hold the wheel's talk button: "play Paranoid on Stemify" (a playlist,
|
||||||
artist, album or song; artists shuffle). There's no keyboard while driving, so Auto's
|
artist, album or song; artists shuffle). There's no keyboard while driving, so Auto's
|
||||||
search button is voice too.
|
search button is voice too.
|
||||||
- **Offline:** in the car the phone is on mobile data and Plex/stemserve are LAN-only. The
|
- **Offline:** in the car the phone is on mobile data and Plex/stemserve are LAN-only. The
|
||||||
@@ -103,7 +103,7 @@ Auto hides sideloaded apps until you allow them:
|
|||||||
1. Phone Settings → search "Android Auto" → scroll to the bottom and tap **Version** ten
|
1. Phone Settings → search "Android Auto" → scroll to the bottom and tap **Version** ten
|
||||||
times → OK.
|
times → OK.
|
||||||
2. ⋮ menu → **Developer settings** → tick **Unknown sources**.
|
2. ⋮ menu → **Developer settings** → tick **Unknown sources**.
|
||||||
3. Plug into the Miata. StemRoller is in Auto's app launcher (the grid of icons).
|
3. Plug into the Miata. Stemify is in Auto's app launcher (the grid of icons).
|
||||||
|
|
||||||
Download what you want for the drive while on home Wi-Fi.
|
Download what you want for the drive while on home Wi-Fi.
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.stemroller.mobile"
|
namespace = "com.stemify.mobile"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.stemroller.mobile"
|
applicationId = "com.stemify.mobile"
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
|
|||||||
Vendored
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
# kotlinx.serialization keeps its generated serializers via its own consumer rules.
|
# kotlinx.serialization keeps its generated serializers via its own consumer rules.
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.**
|
-dontnote kotlinx.serialization.**
|
||||||
-keep,includedescriptorclasses class com.stemroller.mobile.**$$serializer { *; }
|
-keep,includedescriptorclasses class com.stemify.mobile.**$$serializer { *; }
|
||||||
-keepclassmembers class com.stemroller.mobile.** {
|
-keepclassmembers class com.stemify.mobile.** {
|
||||||
*** Companion;
|
*** Companion;
|
||||||
}
|
}
|
||||||
-keepclasseswithmembers class com.stemroller.mobile.** {
|
-keepclasseswithmembers class com.stemify.mobile.** {
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile
|
package com.stemify.mobile
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
@@ -17,10 +17,10 @@ import java.io.File
|
|||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `adb shell am broadcast -a com.stemroller.mobile.CAPTURE -p com.stemroller.mobile`
|
* `adb shell am broadcast -a com.stemify.mobile.CAPTURE -p com.stemify.mobile`
|
||||||
* writes files/capture.png: the current activity drawn in software.
|
* writes files/capture.png: the current activity drawn in software.
|
||||||
*
|
*
|
||||||
* `... -a com.stemroller.mobile.BACK --es phase start|progress|commit|cancel --ef p 0.4`
|
* `... -a com.stemify.mobile.BACK --es phase start|progress|commit|cancel --ef p 0.4`
|
||||||
* feeds the predictive back callbacks the way a system swipe would.
|
* feeds the predictive back callbacks the way a system swipe would.
|
||||||
*/
|
*/
|
||||||
class DebugCapture : ContentProvider() {
|
class DebugCapture : ContentProvider() {
|
||||||
@@ -45,7 +45,7 @@ class DebugCapture : ContentProvider() {
|
|||||||
v.draw(Canvas(bmp))
|
v.draw(Canvas(bmp))
|
||||||
File(a.filesDir, "capture.png").outputStream().use { bmp.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
File(a.filesDir, "capture.png").outputStream().use { bmp.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
||||||
}
|
}
|
||||||
}, IntentFilter("com.stemroller.mobile.CAPTURE"), Context.RECEIVER_EXPORTED)
|
}, IntentFilter("com.stemify.mobile.CAPTURE"), Context.RECEIVER_EXPORTED)
|
||||||
app.registerReceiver(object : BroadcastReceiver() {
|
app.registerReceiver(object : BroadcastReceiver() {
|
||||||
override fun onReceive(c: Context, i: Intent) {
|
override fun onReceive(c: Context, i: Intent) {
|
||||||
val a = current.get() as? ComponentActivity ?: return
|
val a = current.get() as? ComponentActivity ?: return
|
||||||
@@ -59,7 +59,7 @@ class DebugCapture : ContentProvider() {
|
|||||||
"cancel" -> d.dispatchOnBackCancelled()
|
"cancel" -> d.dispatchOnBackCancelled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, IntentFilter("com.stemroller.mobile.BACK"), Context.RECEIVER_EXPORTED)
|
}, IntentFilter("com.stemify.mobile.BACK"), Context.RECEIVER_EXPORTED)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.StemRoller"
|
android:theme="@style/Theme.Stemify"
|
||||||
tools:targetApi="36">
|
tools:targetApi="36">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<!-- Album art for Android Auto, which only loads content:// artwork. Read-only. -->
|
<!-- Album art for Android Auto, which only loads content:// artwork. Read-only. -->
|
||||||
<provider
|
<provider
|
||||||
android:name=".auto.CarArtProvider"
|
android:name=".auto.CarArtProvider"
|
||||||
android:authorities="com.stemroller.mobile.carart"
|
android:authorities="com.stemify.mobile.carart"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
tools:ignore="ExportedContentProvider" />
|
tools:ignore="ExportedContentProvider" />
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile
|
package com.stemify.mobile
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
@@ -11,16 +11,16 @@ import coil3.memory.MemoryCache
|
|||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import coil3.request.allowHardware
|
import coil3.request.allowHardware
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import com.stemroller.mobile.auto.HomeReach
|
import com.stemify.mobile.auto.HomeReach
|
||||||
import com.stemroller.mobile.cache.Downloads
|
import com.stemify.mobile.cache.Downloads
|
||||||
import com.stemroller.mobile.cache.MediaCache
|
import com.stemify.mobile.cache.MediaCache
|
||||||
import com.stemroller.mobile.core.Connectivity
|
import com.stemify.mobile.core.Connectivity
|
||||||
import com.stemroller.mobile.core.Settings
|
import com.stemify.mobile.core.Settings
|
||||||
import com.stemroller.mobile.eq.EqController
|
import com.stemify.mobile.eq.EqController
|
||||||
import com.stemroller.mobile.playback.PlaybackManager
|
import com.stemify.mobile.playback.PlaybackManager
|
||||||
import com.stemroller.mobile.plex.PlexClient
|
import com.stemify.mobile.plex.PlexClient
|
||||||
import com.stemroller.mobile.stems.StemCoverage
|
import com.stemify.mobile.stems.StemCoverage
|
||||||
import com.stemroller.mobile.stems.StemServer
|
import com.stemify.mobile.stems.StemServer
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile
|
package com.stemify.mobile
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
@@ -19,9 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.stemroller.mobile.core.ThemeMode
|
import com.stemify.mobile.core.ThemeMode
|
||||||
import com.stemroller.mobile.playback.PlaybackService
|
import com.stemify.mobile.playback.PlaybackService
|
||||||
import com.stemroller.mobile.ui.StemApp
|
import com.stemify.mobile.ui.StemApp
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.auto
|
package com.stemify.mobile.auto
|
||||||
|
|
||||||
import android.content.ContentProvider
|
import android.content.ContentProvider
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
@@ -10,7 +10,7 @@ import android.graphics.Typeface
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@@ -102,7 +102,7 @@ class CarArtProvider : ContentProvider() {
|
|||||||
override fun update(uri: Uri, v: ContentValues?, s: String?, a: Array<out String>?) = 0
|
override fun update(uri: Uri, v: ContentValues?, s: String?, a: Array<out String>?) = 0
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val AUTHORITY = "com.stemroller.mobile.carart"
|
const val AUTHORITY = "com.stemify.mobile.carart"
|
||||||
private const val THUMB = "thumb"
|
private const val THUMB = "thumb"
|
||||||
private const val LETTER = "letter"
|
private const val LETTER = "letter"
|
||||||
private const val LETTER_PX = 256
|
private const val LETTER_PX = 256
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.auto
|
package com.stemify.mobile.auto
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -7,11 +7,11 @@ import androidx.media3.common.MediaItem
|
|||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.session.MediaConstants
|
import androidx.media3.session.MediaConstants
|
||||||
import com.stemroller.mobile.Graph
|
import com.stemify.mobile.Graph
|
||||||
import com.stemroller.mobile.plex.Album
|
import com.stemify.mobile.plex.Album
|
||||||
import com.stemroller.mobile.plex.Artist
|
import com.stemify.mobile.plex.Artist
|
||||||
import com.stemroller.mobile.plex.Playlist
|
import com.stemify.mobile.plex.Playlist
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ class CarLibrary(private val g: Graph) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Hey Google, play … on StemRoller". Prefers a playlist, then an artist (shuffled), then an
|
* "Hey Google, play … on Stemify". Prefers a playlist, then an artist (shuffled), then an
|
||||||
* album whose name matches; otherwise plays the song hits. Away from home it searches downloads.
|
* album whose name matches; otherwise plays the song hits. Away from home it searches downloads.
|
||||||
*/
|
*/
|
||||||
suspend fun resolveSearch(query: String): Resolved? = withContext(Dispatchers.IO) {
|
suspend fun resolveSearch(query: String): Resolved? = withContext(Dispatchers.IO) {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.auto
|
package com.stemify.mobile.auto
|
||||||
|
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
|
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
package com.stemroller.mobile.auto
|
package com.stemify.mobile.auto
|
||||||
|
|
||||||
import com.stemroller.mobile.core.Connectivity
|
import com.stemify.mobile.core.Connectivity
|
||||||
import com.stemroller.mobile.core.Settings
|
import com.stemify.mobile.core.Settings
|
||||||
import com.stemroller.mobile.core.await
|
import com.stemify.mobile.core.await
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.cache
|
package com.stemify.mobile.cache
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
@@ -11,16 +11,16 @@ import android.content.pm.ServiceInfo
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.MainActivity
|
import com.stemify.mobile.MainActivity
|
||||||
import com.stemroller.mobile.R
|
import com.stemify.mobile.R
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import com.stemroller.mobile.core.Connectivity
|
import com.stemify.mobile.core.Connectivity
|
||||||
import com.stemroller.mobile.core.Settings
|
import com.stemify.mobile.core.Settings
|
||||||
import com.stemroller.mobile.playback.PlaybackManager
|
import com.stemify.mobile.playback.PlaybackManager
|
||||||
import com.stemroller.mobile.plex.PlexClient
|
import com.stemify.mobile.plex.PlexClient
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import com.stemroller.mobile.stems.StemServer
|
import com.stemify.mobile.stems.StemServer
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package com.stemroller.mobile.cache
|
package com.stemify.mobile.cache
|
||||||
|
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.core
|
package com.stemify.mobile.core
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.core
|
package com.stemify.mobile.core
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.engine
|
package com.stemify.mobile.engine
|
||||||
|
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.engine
|
package com.stemify.mobile.engine
|
||||||
|
|
||||||
import android.media.AudioFormat
|
import android.media.AudioFormat
|
||||||
import android.media.MediaCodec
|
import android.media.MediaCodec
|
||||||
@@ -6,7 +6,7 @@ import android.media.MediaDataSource
|
|||||||
import android.media.MediaExtractor
|
import android.media.MediaExtractor
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stemroller.mobile.cache.MediaCache
|
import com.stemify.mobile.cache.MediaCache
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.engine
|
package com.stemify.mobile.engine
|
||||||
|
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioDeviceInfo
|
import android.media.AudioDeviceInfo
|
||||||
+6
-6
@@ -1,9 +1,9 @@
|
|||||||
package com.stemroller.mobile.eq
|
package com.stemify.mobile.eq
|
||||||
|
|
||||||
import com.stemroller.mobile.engine.EQ_Q
|
import com.stemify.mobile.engine.EQ_Q
|
||||||
import com.stemroller.mobile.engine.EqChannel
|
import com.stemify.mobile.engine.EqChannel
|
||||||
import com.stemroller.mobile.engine.EqFilter
|
import com.stemify.mobile.engine.EqFilter
|
||||||
import com.stemroller.mobile.engine.FilterType
|
import com.stemify.mobile.engine.FilterType
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
@@ -107,7 +107,7 @@ object ApoConfig {
|
|||||||
/** The profile as an Equalizer APO config: preamp, graphic bands as peaks, then the filters. */
|
/** The profile as an Equalizer APO config: preamp, graphic bands as peaks, then the filters. */
|
||||||
fun format(p: EqProfile): String = buildString {
|
fun format(p: EqProfile): String = buildString {
|
||||||
fun n(v: Float) = String.format(Locale.ROOT, "%.2f", v).trimEnd('0').trimEnd('.')
|
fun n(v: Float) = String.format(Locale.ROOT, "%.2f", v).trimEnd('0').trimEnd('.')
|
||||||
appendLine("# ${p.name}, from StemRoller")
|
appendLine("# ${p.name}, from Stemify")
|
||||||
appendLine("Preamp: ${n(p.preamp)} dB")
|
appendLine("Preamp: ${n(p.preamp)} dB")
|
||||||
var channel = EqChannel.Both
|
var channel = EqChannel.Both
|
||||||
var k = 1
|
var k = 1
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.eq
|
package com.stemify.mobile.eq
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
@@ -7,12 +7,12 @@ import android.media.AudioDeviceInfo
|
|||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import com.stemroller.mobile.engine.EQ_BANDS
|
import com.stemify.mobile.engine.EQ_BANDS
|
||||||
import com.stemroller.mobile.engine.EQ_MAX_DB
|
import com.stemify.mobile.engine.EQ_MAX_DB
|
||||||
import com.stemroller.mobile.engine.EqFilter
|
import com.stemify.mobile.engine.EqFilter
|
||||||
import com.stemroller.mobile.engine.EqMath
|
import com.stemify.mobile.engine.EqMath
|
||||||
import com.stemroller.mobile.engine.EqParams
|
import com.stemify.mobile.engine.EqParams
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.playback
|
package com.stemify.mobile.playback
|
||||||
|
|
||||||
/** A one-tap stem mix. [build] returns null when the track lacks the stems it needs. */
|
/** A one-tap stem mix. [build] returns null when the track lacks the stems it needs. */
|
||||||
class MixPreset(val label: String, val build: (List<String>) -> Map<String, StemSetting>?)
|
class MixPreset(val label: String, val build: (List<String>) -> Map<String, StemSetting>?)
|
||||||
+19
-19
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.playback
|
package com.stemify.mobile.playback
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -12,22 +12,22 @@ import android.net.wifi.WifiManager
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import com.stemroller.mobile.cache.MediaCache
|
import com.stemify.mobile.cache.MediaCache
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import com.stemroller.mobile.core.Settings
|
import com.stemify.mobile.core.Settings
|
||||||
import com.stemroller.mobile.core.StemQuality
|
import com.stemify.mobile.core.StemQuality
|
||||||
import com.stemroller.mobile.engine.EqParams
|
import com.stemify.mobile.engine.EqParams
|
||||||
import com.stemroller.mobile.engine.ResourceDataSource
|
import com.stemify.mobile.engine.ResourceDataSource
|
||||||
import com.stemroller.mobile.engine.StemDecoder
|
import com.stemify.mobile.engine.StemDecoder
|
||||||
import com.stemroller.mobile.engine.StemEngine
|
import com.stemify.mobile.engine.StemEngine
|
||||||
import com.stemroller.mobile.engine.TrackSession
|
import com.stemify.mobile.engine.TrackSession
|
||||||
import com.stemroller.mobile.engine.masterTaper
|
import com.stemify.mobile.engine.masterTaper
|
||||||
import com.stemroller.mobile.engine.stemTaper
|
import com.stemify.mobile.engine.stemTaper
|
||||||
import com.stemroller.mobile.eq.EqController
|
import com.stemify.mobile.eq.EqController
|
||||||
import com.stemroller.mobile.plex.PlexClient
|
import com.stemify.mobile.plex.PlexClient
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import com.stemroller.mobile.stems.StemEntry
|
import com.stemify.mobile.stems.StemEntry
|
||||||
import com.stemroller.mobile.stems.StemServer
|
import com.stemify.mobile.stems.StemServer
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -697,10 +697,10 @@ class PlaybackManager(
|
|||||||
}
|
}
|
||||||
private val noisyRegistered = AtomicBoolean(false)
|
private val noisyRegistered = AtomicBoolean(false)
|
||||||
private val wakeLock = context.getSystemService(PowerManager::class.java)
|
private val wakeLock = context.getSystemService(PowerManager::class.java)
|
||||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "StemRoller:playback").apply { setReferenceCounted(false) }
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Stemify:playback").apply { setReferenceCounted(false) }
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private val wifiLock = context.applicationContext.getSystemService(WifiManager::class.java)
|
private val wifiLock = context.applicationContext.getSystemService(WifiManager::class.java)
|
||||||
.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "StemRoller:stream").apply { setReferenceCounted(false) }
|
.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Stemify:stream").apply { setReferenceCounted(false) }
|
||||||
|
|
||||||
private fun holdLocks(on: Boolean) {
|
private fun holdLocks(on: Boolean) {
|
||||||
if (on) {
|
if (on) {
|
||||||
+17
-17
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.playback
|
package com.stemify.mobile.playback
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -25,15 +25,15 @@ import androidx.media3.session.SessionResult
|
|||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.MainActivity
|
import com.stemify.mobile.MainActivity
|
||||||
import com.stemroller.mobile.R
|
import com.stemify.mobile.R
|
||||||
import com.stemroller.mobile.auto.CarArtProvider
|
import com.stemify.mobile.auto.CarArtProvider
|
||||||
import com.stemroller.mobile.auto.CarIds
|
import com.stemify.mobile.auto.CarIds
|
||||||
import com.stemroller.mobile.auto.CarLibrary
|
import com.stemify.mobile.auto.CarLibrary
|
||||||
import com.stemroller.mobile.auto.Resolved
|
import com.stemify.mobile.auto.Resolved
|
||||||
import com.stemroller.mobile.plex.NotSignedInException
|
import com.stemify.mobile.plex.NotSignedInException
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@@ -399,7 +399,7 @@ class PlaybackService : MediaLibraryService() {
|
|||||||
// Auto says how many tabs it can show (4 on the Miata's screen).
|
// Auto says how many tabs it can show (4 on the Miata's screen).
|
||||||
params?.extras?.getInt(MediaConstants.EXTRAS_KEY_ROOT_CHILDREN_LIMIT, 0)?.takeIf { it > 0 }?.let { rootLimit = it }
|
params?.extras?.getInt(MediaConstants.EXTRAS_KEY_ROOT_CHILDREN_LIMIT, 0)?.takeIf { it > 0 }?.let { rootLimit = it }
|
||||||
val root = MediaItem.Builder().setMediaId(CarIds.ROOT).setMediaMetadata(
|
val root = MediaItem.Builder().setMediaId(CarIds.ROOT).setMediaMetadata(
|
||||||
MediaMetadata.Builder().setTitle("StemRoller").setIsBrowsable(true).setIsPlayable(false)
|
MediaMetadata.Builder().setTitle("Stemify").setIsBrowsable(true).setIsPlayable(false)
|
||||||
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED).build(),
|
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED).build(),
|
||||||
).build()
|
).build()
|
||||||
val extras = Bundle().apply {
|
val extras = Bundle().apply {
|
||||||
@@ -477,14 +477,14 @@ class PlaybackService : MediaLibraryService() {
|
|||||||
override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
||||||
val q = pm.queue.value
|
val q = pm.queue.value
|
||||||
if (q.isEmpty()) return Futures.immediateFailedFuture(UnsupportedOperationException("no saved queue"))
|
if (q.isEmpty()) return Futures.immediateFailedFuture(UnsupportedOperationException("no saved queue"))
|
||||||
val items = q.map { MediaItem.Builder().setMediaId("${it.uid}:${it.track.ratingKey}").setUri("stemroller://resume").setTag(ResumeTag).build() }
|
val items = q.map { MediaItem.Builder().setMediaId("${it.uid}:${it.track.ratingKey}").setUri("stemify://resume").setTag(ResumeTag).build() }
|
||||||
return Futures.immediateFuture(MediaSession.MediaItemsWithStartPosition(items, pm.index.value.coerceIn(0, q.lastIndex), pm.positionMs()))
|
return Futures.immediateFuture(MediaSession.MediaItemsWithStartPosition(items, pm.index.value.coerceIn(0, q.lastIndex), pm.positionMs()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tagged(t: Track, shuffle: Boolean) = MediaItem.Builder()
|
private fun tagged(t: Track, shuffle: Boolean) = MediaItem.Builder()
|
||||||
.setMediaId("track:${t.ratingKey}")
|
.setMediaId("track:${t.ratingKey}")
|
||||||
.setUri("stemroller://track/${t.ratingKey}")
|
.setUri("stemify://track/${t.ratingKey}")
|
||||||
.setTag(PlayTag(t, shuffle))
|
.setTag(PlayTag(t, shuffle))
|
||||||
.setMediaMetadata(MediaMetadata.Builder().setTitle(t.title).setArtist(t.artist).setArtworkUri(CarArtProvider.uri(t.thumb)).build())
|
.setMediaMetadata(MediaMetadata.Builder().setTitle(t.title).setArtist(t.artist).setArtworkUri(CarArtProvider.uri(t.thumb)).build())
|
||||||
.build()
|
.build()
|
||||||
@@ -506,10 +506,10 @@ class PlaybackService : MediaLibraryService() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "StemCar"
|
private const val TAG = "StemCar"
|
||||||
private const val ACTION_VOCALS = "stemroller.VOCALS"
|
private const val ACTION_VOCALS = "stemify.VOCALS"
|
||||||
private const val ACTION_MIX = "stemroller.MIX"
|
private const val ACTION_MIX = "stemify.MIX"
|
||||||
private const val ACTION_SHUFFLE = "stemroller.SHUFFLE"
|
private const val ACTION_SHUFFLE = "stemify.SHUFFLE"
|
||||||
private const val ACTION_REPEAT = "stemroller.REPEAT"
|
private const val ACTION_REPEAT = "stemify.REPEAT"
|
||||||
private val ACTIONS = listOf(ACTION_VOCALS, ACTION_MIX, ACTION_SHUFFLE, ACTION_REPEAT)
|
private val ACTIONS = listOf(ACTION_VOCALS, ACTION_MIX, ACTION_SHUFFLE, ACTION_REPEAT)
|
||||||
|
|
||||||
/** Tells Auto to show its (voice) search button. */
|
/** Tells Auto to show its (voice) search button. */
|
||||||
+7
-7
@@ -1,12 +1,12 @@
|
|||||||
package com.stemroller.mobile.plex
|
package com.stemify.mobile.plex
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import com.stemroller.mobile.core.HttpException
|
import com.stemify.mobile.core.HttpException
|
||||||
import com.stemroller.mobile.core.Settings
|
import com.stemify.mobile.core.Settings
|
||||||
import com.stemroller.mobile.core.await
|
import com.stemify.mobile.core.await
|
||||||
import com.stemroller.mobile.core.awaitBody
|
import com.stemify.mobile.core.awaitBody
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
@@ -247,7 +247,7 @@ class PlexClient(private val http: OkHttpClient, private val settings: Settings)
|
|||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PRODUCT = "StemRoller"
|
const val PRODUCT = "Stemify"
|
||||||
private const val PAGE = 1000
|
private const val PAGE = 1000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.plex
|
package com.stemify.mobile.plex
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
package com.stemroller.mobile.stems
|
package com.stemify.mobile.stems
|
||||||
|
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import com.stemroller.mobile.core.Settings
|
import com.stemify.mobile.core.Settings
|
||||||
import com.stemroller.mobile.plex.PlexClient
|
import com.stemify.mobile.plex.PlexClient
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.stems
|
package com.stemify.mobile.stems
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
+8
-8
@@ -1,13 +1,13 @@
|
|||||||
package com.stemroller.mobile.stems
|
package com.stemify.mobile.stems
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import com.stemroller.mobile.core.HttpException
|
import com.stemify.mobile.core.HttpException
|
||||||
import com.stemroller.mobile.core.Settings
|
import com.stemify.mobile.core.Settings
|
||||||
import com.stemroller.mobile.core.StemQuality
|
import com.stemify.mobile.core.StemQuality
|
||||||
import com.stemroller.mobile.core.await
|
import com.stemify.mobile.core.await
|
||||||
import com.stemroller.mobile.core.awaitBody
|
import com.stemify.mobile.core.awaitBody
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
+41
-41
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui
|
package com.stemify.mobile.ui
|
||||||
|
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
@@ -46,46 +46,46 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.ui.components.Hairline
|
import com.stemify.mobile.ui.components.Hairline
|
||||||
import com.stemroller.mobile.ui.components.LocalBottomInset
|
import com.stemify.mobile.ui.components.LocalBottomInset
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.OverlayHost
|
import com.stemify.mobile.ui.components.OverlayHost
|
||||||
import com.stemroller.mobile.ui.components.Overlays
|
import com.stemify.mobile.ui.components.Overlays
|
||||||
import com.stemroller.mobile.ui.components.tappable
|
import com.stemify.mobile.ui.components.tappable
|
||||||
import com.stemroller.mobile.ui.nav.NavStack
|
import com.stemify.mobile.ui.nav.NavStack
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.ui.nav.StackHost
|
import com.stemify.mobile.ui.nav.StackHost
|
||||||
import com.stemroller.mobile.ui.screens.AlbumScreen
|
import com.stemify.mobile.ui.screens.AlbumScreen
|
||||||
import com.stemroller.mobile.ui.screens.EqFilterScreen
|
import com.stemify.mobile.ui.screens.EqFilterScreen
|
||||||
import com.stemroller.mobile.ui.screens.EqFiltersScreen
|
import com.stemify.mobile.ui.screens.EqFiltersScreen
|
||||||
import com.stemroller.mobile.ui.screens.AlbumsScreen
|
import com.stemify.mobile.ui.screens.AlbumsScreen
|
||||||
import com.stemroller.mobile.ui.screens.AppActions
|
import com.stemify.mobile.ui.screens.AppActions
|
||||||
import com.stemroller.mobile.ui.screens.AppearanceScreen
|
import com.stemify.mobile.ui.screens.AppearanceScreen
|
||||||
import com.stemroller.mobile.ui.screens.ArtistScreen
|
import com.stemify.mobile.ui.screens.ArtistScreen
|
||||||
import com.stemroller.mobile.ui.screens.ArtistsScreen
|
import com.stemify.mobile.ui.screens.ArtistsScreen
|
||||||
import com.stemroller.mobile.ui.screens.DownloadsScreen
|
import com.stemify.mobile.ui.screens.DownloadsScreen
|
||||||
import com.stemroller.mobile.ui.screens.EqScreen
|
import com.stemify.mobile.ui.screens.EqScreen
|
||||||
import com.stemroller.mobile.ui.screens.LibraryHome
|
import com.stemify.mobile.ui.screens.LibraryHome
|
||||||
import com.stemroller.mobile.ui.screens.LocalApp
|
import com.stemify.mobile.ui.screens.LocalApp
|
||||||
import com.stemroller.mobile.ui.screens.MiniPlayer
|
import com.stemify.mobile.ui.screens.MiniPlayer
|
||||||
import com.stemroller.mobile.ui.screens.NowPlayingScreen
|
import com.stemify.mobile.ui.screens.NowPlayingScreen
|
||||||
import com.stemroller.mobile.ui.screens.PlaylistScreen
|
import com.stemify.mobile.ui.screens.PlaylistScreen
|
||||||
import com.stemroller.mobile.ui.screens.PlaylistsScreen
|
import com.stemify.mobile.ui.screens.PlaylistsScreen
|
||||||
import com.stemroller.mobile.ui.screens.PlexAccountScreen
|
import com.stemify.mobile.ui.screens.PlexAccountScreen
|
||||||
import com.stemroller.mobile.ui.screens.QualityScreen
|
import com.stemify.mobile.ui.screens.QualityScreen
|
||||||
import com.stemroller.mobile.ui.screens.SearchScreen
|
import com.stemify.mobile.ui.screens.SearchScreen
|
||||||
import com.stemroller.mobile.ui.screens.SettingsScreen
|
import com.stemify.mobile.ui.screens.SettingsScreen
|
||||||
import com.stemroller.mobile.ui.screens.SongsScreen
|
import com.stemify.mobile.ui.screens.SongsScreen
|
||||||
import com.stemroller.mobile.ui.screens.StemServerScreen
|
import com.stemify.mobile.ui.screens.StemServerScreen
|
||||||
import com.stemroller.mobile.ui.screens.StorageScreen
|
import com.stemify.mobile.ui.screens.StorageScreen
|
||||||
import com.stemroller.mobile.ui.screens.Tab
|
import com.stemify.mobile.ui.screens.Tab
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.IosTheme
|
import com.stemify.mobile.ui.theme.IosTheme
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
+10
-10
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.components
|
package com.stemify.mobile.ui.components
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -58,14 +58,14 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.ui.nav.LocalEntry
|
import com.stemify.mobile.ui.nav.LocalEntry
|
||||||
import com.stemroller.mobile.ui.nav.LocalNav
|
import com.stemify.mobile.ui.nav.LocalNav
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
/** Height of the tab bar plus mini player, so lists can scroll clear of them. */
|
/** Height of the tab bar plus mini player, so lists can scroll clear of them. */
|
||||||
@@ -385,7 +385,7 @@ fun ErrorRow(message: String, onRetry: () -> Unit) {
|
|||||||
if (!App.graph.plex.signedIn) {
|
if (!App.graph.plex.signedIn) {
|
||||||
val nav = LocalNav.current
|
val nav = LocalNav.current
|
||||||
EmptyState(Sym.server, "Not Connected", "Sign in to your Plex server to see your music here.", "Connect") {
|
EmptyState(Sym.server, "Not Connected", "Sign in to your Plex server to see your music here.", "Connect") {
|
||||||
nav.push(com.stemroller.mobile.ui.nav.Route.PlexAccount)
|
nav.push(com.stemify.mobile.ui.nav.Route.PlexAccount)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
EmptyState(Sym.warning, "Couldn't Load", message, "Try Again", onRetry)
|
EmptyState(Sym.warning, "Couldn't Load", message, "Try Again", onRetry)
|
||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.components
|
package com.stemify.mobile.ui.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
@@ -56,11 +56,11 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.components
|
package com.stemify.mobile.ui.components
|
||||||
|
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
@@ -50,11 +50,11 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.nav
|
package com.stemify.mobile.ui.nav
|
||||||
|
|
||||||
import androidx.activity.BackEventCompat
|
import androidx.activity.BackEventCompat
|
||||||
import androidx.activity.compose.PredictiveBackHandler
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
@@ -29,9 +29,9 @@ import androidx.compose.ui.draw.clipToBounds
|
|||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import com.stemroller.mobile.plex.Album
|
import com.stemify.mobile.plex.Album
|
||||||
import com.stemroller.mobile.plex.Artist
|
import com.stemify.mobile.plex.Artist
|
||||||
import com.stemroller.mobile.plex.Playlist
|
import com.stemify.mobile.plex.Playlist
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
+26
-26
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -27,31 +27,31 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.stems.Coverage
|
import com.stemify.mobile.stems.Coverage
|
||||||
import com.stemroller.mobile.plex.Album
|
import com.stemify.mobile.plex.Album
|
||||||
import com.stemroller.mobile.plex.Artist
|
import com.stemify.mobile.plex.Artist
|
||||||
import com.stemroller.mobile.plex.Playlist
|
import com.stemify.mobile.plex.Playlist
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import com.stemroller.mobile.ui.components.Artwork
|
import com.stemify.mobile.ui.components.Artwork
|
||||||
import com.stemroller.mobile.ui.components.Chevron
|
import com.stemify.mobile.ui.components.Chevron
|
||||||
import com.stemroller.mobile.ui.components.DownloadedMark
|
import com.stemify.mobile.ui.components.DownloadedMark
|
||||||
import com.stemroller.mobile.ui.components.Hairline
|
import com.stemify.mobile.ui.components.Hairline
|
||||||
import com.stemroller.mobile.ui.components.IconButton
|
import com.stemify.mobile.ui.components.IconButton
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.MARGIN
|
import com.stemify.mobile.ui.components.MARGIN
|
||||||
import com.stemroller.mobile.ui.components.Overlays
|
import com.stemify.mobile.ui.components.Overlays
|
||||||
import com.stemroller.mobile.ui.components.PillButton
|
import com.stemify.mobile.ui.components.PillButton
|
||||||
import com.stemroller.mobile.ui.components.PlayingBars
|
import com.stemify.mobile.ui.components.PlayingBars
|
||||||
import com.stemroller.mobile.ui.components.ProgressRing
|
import com.stemify.mobile.ui.components.ProgressRing
|
||||||
import com.stemroller.mobile.ui.components.SheetAction
|
import com.stemify.mobile.ui.components.SheetAction
|
||||||
import com.stemroller.mobile.ui.components.Tag
|
import com.stemify.mobile.ui.components.Tag
|
||||||
import com.stemroller.mobile.ui.components.tappable
|
import com.stemify.mobile.ui.components.tappable
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
+25
-25
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
@@ -35,30 +35,30 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
|||||||
import androidx.compose.ui.text.drawText
|
import androidx.compose.ui.text.drawText
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.engine.EQ_BANDS
|
import com.stemify.mobile.engine.EQ_BANDS
|
||||||
import com.stemroller.mobile.engine.EQ_SLOPES
|
import com.stemify.mobile.engine.EQ_SLOPES
|
||||||
import com.stemroller.mobile.engine.EqChannel
|
import com.stemify.mobile.engine.EqChannel
|
||||||
import com.stemroller.mobile.engine.EqFilter
|
import com.stemify.mobile.engine.EqFilter
|
||||||
import com.stemroller.mobile.engine.EqMath
|
import com.stemify.mobile.engine.EqMath
|
||||||
import com.stemroller.mobile.engine.FilterType
|
import com.stemify.mobile.engine.FilterType
|
||||||
import com.stemroller.mobile.eq.ApoConfig
|
import com.stemify.mobile.eq.ApoConfig
|
||||||
import com.stemroller.mobile.eq.EqController
|
import com.stemify.mobile.eq.EqController
|
||||||
import com.stemroller.mobile.ui.components.Cell
|
import com.stemify.mobile.ui.components.Cell
|
||||||
import com.stemroller.mobile.ui.components.IconButton
|
import com.stemify.mobile.ui.components.IconButton
|
||||||
import com.stemroller.mobile.ui.components.IosSlider
|
import com.stemify.mobile.ui.components.IosSlider
|
||||||
import com.stemroller.mobile.ui.components.IosSwitch
|
import com.stemify.mobile.ui.components.IosSwitch
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.Page
|
import com.stemify.mobile.ui.components.Page
|
||||||
import com.stemroller.mobile.ui.components.SegmentedControl
|
import com.stemify.mobile.ui.components.SegmentedControl
|
||||||
import com.stemroller.mobile.ui.components.SheetAction
|
import com.stemify.mobile.ui.components.SheetAction
|
||||||
import com.stemroller.mobile.ui.components.section
|
import com.stemify.mobile.ui.components.section
|
||||||
import com.stemroller.mobile.ui.nav.LocalNav
|
import com.stemify.mobile.ui.nav.LocalNav
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
+25
-25
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -24,30 +24,30 @@ import androidx.compose.ui.graphics.drawscope.Stroke
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.engine.EQ_BANDS
|
import com.stemify.mobile.engine.EQ_BANDS
|
||||||
import com.stemroller.mobile.engine.EQ_LABELS
|
import com.stemify.mobile.engine.EQ_LABELS
|
||||||
import com.stemroller.mobile.engine.EQ_MAX_DB
|
import com.stemify.mobile.engine.EQ_MAX_DB
|
||||||
import com.stemroller.mobile.engine.EqMath
|
import com.stemify.mobile.engine.EqMath
|
||||||
import com.stemroller.mobile.eq.DeviceKind
|
import com.stemify.mobile.eq.DeviceKind
|
||||||
import com.stemroller.mobile.eq.EQ_PRESETS
|
import com.stemify.mobile.eq.EQ_PRESETS
|
||||||
import com.stemroller.mobile.eq.EqProfile
|
import com.stemify.mobile.eq.EqProfile
|
||||||
import com.stemroller.mobile.eq.OutputDevice
|
import com.stemify.mobile.eq.OutputDevice
|
||||||
import com.stemroller.mobile.ui.components.Cell
|
import com.stemify.mobile.ui.components.Cell
|
||||||
import com.stemroller.mobile.ui.components.IosSwitch
|
import com.stemify.mobile.ui.components.IosSwitch
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.Page
|
import com.stemify.mobile.ui.components.Page
|
||||||
import com.stemroller.mobile.ui.components.SegmentedControl
|
import com.stemify.mobile.ui.components.SegmentedControl
|
||||||
import com.stemroller.mobile.ui.components.SettingsIcon
|
import com.stemify.mobile.ui.components.SettingsIcon
|
||||||
import com.stemroller.mobile.ui.components.SheetAction
|
import com.stemify.mobile.ui.components.SheetAction
|
||||||
import com.stemroller.mobile.ui.components.VerticalBandSlider
|
import com.stemify.mobile.ui.components.VerticalBandSlider
|
||||||
import com.stemroller.mobile.ui.components.section
|
import com.stemify.mobile.ui.components.section
|
||||||
import com.stemroller.mobile.ui.nav.LocalNav
|
import com.stemify.mobile.ui.nav.LocalNav
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
fun deviceIcon(d: OutputDevice): Pair<ImageVector, androidx.compose.ui.graphics.Color> = when (d.kind) {
|
fun deviceIcon(d: OutputDevice): Pair<ImageVector, androidx.compose.ui.graphics.Color> = when (d.kind) {
|
||||||
+31
-31
@@ -1,7 +1,7 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import com.stemroller.mobile.ui.components.ProgressRing
|
import com.stemify.mobile.ui.components.ProgressRing
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -44,35 +44,35 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.plex.Album
|
import com.stemify.mobile.plex.Album
|
||||||
import com.stemroller.mobile.plex.Artist
|
import com.stemify.mobile.plex.Artist
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import com.stemroller.mobile.cache.DownloadRecord
|
import com.stemify.mobile.cache.DownloadRecord
|
||||||
import com.stemroller.mobile.ui.components.Artwork
|
import com.stemify.mobile.ui.components.Artwork
|
||||||
import com.stemroller.mobile.ui.components.Chevron
|
import com.stemify.mobile.ui.components.Chevron
|
||||||
import com.stemroller.mobile.ui.components.EmptyState
|
import com.stemify.mobile.ui.components.EmptyState
|
||||||
import com.stemroller.mobile.ui.components.ErrorRow
|
import com.stemify.mobile.ui.components.ErrorRow
|
||||||
import com.stemroller.mobile.ui.components.Hairline
|
import com.stemify.mobile.ui.components.Hairline
|
||||||
import com.stemroller.mobile.ui.components.IconButton
|
import com.stemify.mobile.ui.components.IconButton
|
||||||
import com.stemroller.mobile.ui.components.LoadingRow
|
import com.stemify.mobile.ui.components.LoadingRow
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.MARGIN
|
import com.stemify.mobile.ui.components.MARGIN
|
||||||
import com.stemroller.mobile.ui.components.Page
|
import com.stemify.mobile.ui.components.Page
|
||||||
import com.stemroller.mobile.ui.components.SectionTitle
|
import com.stemify.mobile.ui.components.SectionTitle
|
||||||
import com.stemroller.mobile.ui.components.SheetAction
|
import com.stemify.mobile.ui.components.SheetAction
|
||||||
import com.stemroller.mobile.ui.components.TextButton
|
import com.stemify.mobile.ui.components.TextButton
|
||||||
import com.stemroller.mobile.ui.components.tappable
|
import com.stemify.mobile.ui.components.tappable
|
||||||
import com.stemroller.mobile.ui.nav.Load
|
import com.stemify.mobile.ui.nav.Load
|
||||||
import com.stemroller.mobile.ui.nav.LocalNav
|
import com.stemify.mobile.ui.nav.LocalNav
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.ui.nav.rememberInEntry
|
import com.stemify.mobile.ui.nav.rememberInEntry
|
||||||
import com.stemroller.mobile.ui.nav.rememberLoader
|
import com.stemify.mobile.ui.nav.rememberLoader
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
+30
-30
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
@@ -66,33 +66,33 @@ import coil3.request.ImageRequest
|
|||||||
import coil3.request.SuccessResult
|
import coil3.request.SuccessResult
|
||||||
import coil3.request.allowHardware
|
import coil3.request.allowHardware
|
||||||
import coil3.toBitmap
|
import coil3.toBitmap
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.engine.MeterHistory
|
import com.stemify.mobile.engine.MeterHistory
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.core.StemQuality
|
import com.stemify.mobile.core.StemQuality
|
||||||
import com.stemroller.mobile.playback.MixPresets
|
import com.stemify.mobile.playback.MixPresets
|
||||||
import com.stemroller.mobile.playback.QueueItem
|
import com.stemify.mobile.playback.QueueItem
|
||||||
import com.stemroller.mobile.playback.RepeatMode
|
import com.stemify.mobile.playback.RepeatMode
|
||||||
import com.stemroller.mobile.playback.SourceKind
|
import com.stemify.mobile.playback.SourceKind
|
||||||
import com.stemroller.mobile.playback.StemSetting
|
import com.stemify.mobile.playback.StemSetting
|
||||||
import com.stemroller.mobile.playback.Stems
|
import com.stemify.mobile.playback.Stems
|
||||||
import com.stemroller.mobile.ui.components.Artwork
|
import com.stemify.mobile.ui.components.Artwork
|
||||||
import com.stemroller.mobile.ui.components.Chip
|
import com.stemify.mobile.ui.components.Chip
|
||||||
import com.stemroller.mobile.ui.components.Hairline
|
import com.stemify.mobile.ui.components.Hairline
|
||||||
import com.stemroller.mobile.ui.components.IconButton
|
import com.stemify.mobile.ui.components.IconButton
|
||||||
import com.stemroller.mobile.ui.components.IosSlider
|
import com.stemify.mobile.ui.components.IosSlider
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.SheetAction
|
import com.stemify.mobile.ui.components.SheetAction
|
||||||
import com.stemroller.mobile.ui.components.Spinner
|
import com.stemify.mobile.ui.components.Spinner
|
||||||
import com.stemroller.mobile.ui.components.TextButton
|
import com.stemify.mobile.ui.components.TextButton
|
||||||
import com.stemroller.mobile.ui.components.pressFade
|
import com.stemify.mobile.ui.components.pressFade
|
||||||
import com.stemroller.mobile.ui.components.tappable
|
import com.stemify.mobile.ui.components.tappable
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.LocalPalette
|
import com.stemify.mobile.ui.theme.LocalPalette
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.exp
|
import kotlin.math.exp
|
||||||
@@ -200,7 +200,7 @@ fun NowPlayingScreen(onClose: () -> Unit, dragModifier: Modifier) {
|
|||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Txt(track.title, Type.title3)
|
Txt(track.title, Type.title3)
|
||||||
Txt(track.artist, Type.title3, color = c.label2, weight = FontWeight.Normal, modifier = Modifier.tappable {
|
Txt(track.artist, Type.title3, color = c.label2, weight = FontWeight.Normal, modifier = Modifier.tappable {
|
||||||
track.artistKey?.let { k -> app.showInLibrary(com.stemroller.mobile.ui.nav.Route.ArtistPage(com.stemroller.mobile.plex.Artist(k, track.artist, track.artist))) }
|
track.artistKey?.let { k -> app.showInLibrary(com.stemify.mobile.ui.nav.Route.ArtistPage(com.stemify.mobile.plex.Artist(k, track.artist, track.artist))) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Box(Modifier.size(32.dp).clip(CircleShape).background(c.fill).pressFade { menu.show(track) }, contentAlignment = Alignment.Center) {
|
Box(Modifier.size(32.dp).clip(CircleShape).background(c.fill).pressFade { menu.show(track) }, contentAlignment = Alignment.Center) {
|
||||||
@@ -356,7 +356,7 @@ private fun StemFader(name: String, s: StemSetting, level: Float, enabled: Boole
|
|||||||
Box(Modifier.size(10.dp).clip(CircleShape).background(if (s.muted) c.label3 else color))
|
Box(Modifier.size(10.dp).clip(CircleShape).background(if (s.muted) c.label3 else color))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Txt(Stems.label(name), Type.headline, Modifier.weight(1f), color = if (s.muted) c.label2 else c.label)
|
Txt(Stems.label(name), Type.headline, Modifier.weight(1f), color = if (s.muted) c.label2 else c.label)
|
||||||
val pct = (com.stemroller.mobile.engine.stemTaper(s.level) * 100).toInt()
|
val pct = (com.stemify.mobile.engine.stemTaper(s.level) * 100).toInt()
|
||||||
Txt("$pct%", Type.footnote.copy(fontFeatureSettings = "tnum"), color = c.label2, modifier = Modifier.padding(end = 10.dp))
|
Txt("$pct%", Type.footnote.copy(fontFeatureSettings = "tnum"), color = c.label2, modifier = Modifier.padding(end = 10.dp))
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
ToggleBox("M", s.muted, c.red, onMute)
|
ToggleBox("M", s.muted, c.red, onMute)
|
||||||
+33
-33
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -35,37 +35,37 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.plex.Playlist
|
import com.stemify.mobile.plex.Playlist
|
||||||
import com.stemroller.mobile.plex.SearchResults
|
import com.stemify.mobile.plex.SearchResults
|
||||||
import com.stemroller.mobile.plex.Track
|
import com.stemify.mobile.plex.Track
|
||||||
import com.stemroller.mobile.ui.components.Artwork
|
import com.stemify.mobile.ui.components.Artwork
|
||||||
import com.stemroller.mobile.ui.components.Chevron
|
import com.stemify.mobile.ui.components.Chevron
|
||||||
import com.stemroller.mobile.ui.components.EmptyState
|
import com.stemify.mobile.ui.components.EmptyState
|
||||||
import com.stemroller.mobile.ui.components.ErrorRow
|
import com.stemify.mobile.ui.components.ErrorRow
|
||||||
import com.stemroller.mobile.ui.components.Hairline
|
import com.stemify.mobile.ui.components.Hairline
|
||||||
import com.stemroller.mobile.ui.components.IconButton
|
import com.stemify.mobile.ui.components.IconButton
|
||||||
import com.stemroller.mobile.ui.components.LoadingRow
|
import com.stemify.mobile.ui.components.LoadingRow
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.MARGIN
|
import com.stemify.mobile.ui.components.MARGIN
|
||||||
import com.stemroller.mobile.ui.components.Page
|
import com.stemify.mobile.ui.components.Page
|
||||||
import com.stemroller.mobile.ui.components.SearchField
|
import com.stemify.mobile.ui.components.SearchField
|
||||||
import com.stemroller.mobile.ui.components.SectionTitle
|
import com.stemify.mobile.ui.components.SectionTitle
|
||||||
import com.stemroller.mobile.ui.components.SheetAction
|
import com.stemify.mobile.ui.components.SheetAction
|
||||||
import com.stemroller.mobile.ui.components.Spinner
|
import com.stemify.mobile.ui.components.Spinner
|
||||||
import com.stemroller.mobile.ui.components.Tag
|
import com.stemify.mobile.ui.components.Tag
|
||||||
import com.stemroller.mobile.ui.components.TextButton
|
import com.stemify.mobile.ui.components.TextButton
|
||||||
import com.stemroller.mobile.ui.components.tappable
|
import com.stemify.mobile.ui.components.tappable
|
||||||
import com.stemroller.mobile.ui.nav.Load
|
import com.stemify.mobile.ui.nav.Load
|
||||||
import com.stemroller.mobile.ui.nav.LocalNav
|
import com.stemify.mobile.ui.nav.LocalNav
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.ui.nav.rememberInEntry
|
import com.stemify.mobile.ui.nav.rememberInEntry
|
||||||
import com.stemroller.mobile.ui.nav.rememberLoader
|
import com.stemify.mobile.ui.nav.rememberLoader
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ fun PlaylistScreen(playlist: Playlist) {
|
|||||||
sim.add(i, t)
|
sim.add(i, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { overlays.hud("Couldn't save: ${it.message}", com.stemroller.mobile.ui.theme.Sym.warning) }
|
}.onFailure { overlays.hud("Couldn't save: ${it.message}", com.stemify.mobile.ui.theme.Sym.warning) }
|
||||||
tracks.refresh()
|
tracks.refresh()
|
||||||
edit.saving = false
|
edit.saving = false
|
||||||
edit.editing = false
|
edit.editing = false
|
||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -39,16 +39,16 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
|||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.engine.masterTaper
|
import com.stemify.mobile.engine.masterTaper
|
||||||
import com.stemroller.mobile.engine.stemTaper
|
import com.stemify.mobile.engine.stemTaper
|
||||||
import com.stemroller.mobile.playback.StemSetting
|
import com.stemify.mobile.playback.StemSetting
|
||||||
import com.stemroller.mobile.playback.Waveform
|
import com.stemify.mobile.playback.Waveform
|
||||||
import com.stemroller.mobile.ui.theme.Icon
|
import com.stemify.mobile.ui.theme.Icon
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.log10
|
import kotlin.math.log10
|
||||||
+25
-25
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.screens
|
package com.stemify.mobile.ui.screens
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -40,29 +40,29 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.stemroller.mobile.App
|
import com.stemify.mobile.App
|
||||||
import com.stemroller.mobile.core.Accent
|
import com.stemify.mobile.core.Accent
|
||||||
import com.stemroller.mobile.core.StemQuality
|
import com.stemify.mobile.core.StemQuality
|
||||||
import com.stemroller.mobile.core.ThemeMode
|
import com.stemify.mobile.core.ThemeMode
|
||||||
import com.stemroller.mobile.plex.MusicSection
|
import com.stemify.mobile.plex.MusicSection
|
||||||
import com.stemroller.mobile.stems.StemServerStatus
|
import com.stemify.mobile.stems.StemServerStatus
|
||||||
import com.stemroller.mobile.ui.components.Cell
|
import com.stemify.mobile.ui.components.Cell
|
||||||
import com.stemroller.mobile.ui.components.IosSwitch
|
import com.stemify.mobile.ui.components.IosSwitch
|
||||||
import com.stemroller.mobile.ui.components.LocalOverlays
|
import com.stemify.mobile.ui.components.LocalOverlays
|
||||||
import com.stemroller.mobile.ui.components.Page
|
import com.stemify.mobile.ui.components.Page
|
||||||
import com.stemroller.mobile.ui.components.SegmentedControl
|
import com.stemify.mobile.ui.components.SegmentedControl
|
||||||
import com.stemroller.mobile.ui.components.SettingsIcon
|
import com.stemify.mobile.ui.components.SettingsIcon
|
||||||
import com.stemroller.mobile.ui.components.Spinner
|
import com.stemify.mobile.ui.components.Spinner
|
||||||
import com.stemroller.mobile.ui.components.section
|
import com.stemify.mobile.ui.components.section
|
||||||
import com.stemroller.mobile.ui.components.tappable
|
import com.stemify.mobile.ui.components.tappable
|
||||||
import com.stemroller.mobile.ui.nav.LocalNav
|
import com.stemify.mobile.ui.nav.LocalNav
|
||||||
import com.stemroller.mobile.ui.nav.Route
|
import com.stemify.mobile.ui.nav.Route
|
||||||
import com.stemroller.mobile.ui.nav.rememberInEntry
|
import com.stemify.mobile.ui.nav.rememberInEntry
|
||||||
import com.stemroller.mobile.ui.theme.Ios
|
import com.stemify.mobile.ui.theme.Ios
|
||||||
import com.stemroller.mobile.ui.theme.Sym
|
import com.stemify.mobile.ui.theme.Sym
|
||||||
import com.stemroller.mobile.ui.theme.Txt
|
import com.stemify.mobile.ui.theme.Txt
|
||||||
import com.stemroller.mobile.ui.theme.Type
|
import com.stemify.mobile.ui.theme.Type
|
||||||
import com.stemroller.mobile.ui.theme.palette
|
import com.stemify.mobile.ui.theme.palette
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ fun SettingsScreen() {
|
|||||||
{ Cell("Download on Wi-Fi Only", icon = { SettingsIcon(Sym.download, Color(0xFF30B0C7)) }, trailing = { IosSwitch(wifi, { s.downloadsWifiOnly.value = it }) }) },
|
{ Cell("Download on Wi-Fi Only", icon = { SettingsIcon(Sym.download, Color(0xFF30B0C7)) }, trailing = { IosSwitch(wifi, { s.downloadsWifiOnly.value = it }) }) },
|
||||||
))
|
))
|
||||||
section("display", header = "Display", separatorInset = 60.dp,
|
section("display", header = "Display", separatorInset = 60.dp,
|
||||||
footer = "StemRoller 1.1 · stems by stemd on your server" + (mode?.let { " · ${"%.0f".format(it.refreshRate)} Hz" } ?: ""),
|
footer = "Stemify 1.1 · stems by stemd on your server" + (mode?.let { " · ${"%.0f".format(it.refreshRate)} Hz" } ?: ""),
|
||||||
rows = listOf(
|
rows = listOf(
|
||||||
{ Cell("Appearance", icon = { SettingsIcon(Sym.paintbrush, Color(0xFF007AFF)) }, value = theme.label, chevron = true) { nav.push(Route.Appearance) } },
|
{ Cell("Appearance", icon = { SettingsIcon(Sym.paintbrush, Color(0xFF007AFF)) }, value = theme.label, chevron = true) { nav.push(Route.Appearance) } },
|
||||||
{ Cell("High Refresh Rate", icon = { SettingsIcon(Sym.phone, Color(0xFFFF9500)) }, trailing = { IosSwitch(hz, { s.highRefreshRate.value = it }) }) },
|
{ Cell("High Refresh Rate", icon = { SettingsIcon(Sym.phone, Color(0xFFFF9500)) }, trailing = { IosSwitch(hz, { s.highRefreshRate.value = it }) }) },
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.theme
|
package com.stemify.mobile.ui.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package com.stemroller.mobile.ui.theme
|
package com.stemify.mobile.ui.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -19,9 +19,9 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.stemroller.mobile.R
|
import com.stemify.mobile.R
|
||||||
import com.stemroller.mobile.core.Accent
|
import com.stemify.mobile.core.Accent
|
||||||
import com.stemroller.mobile.core.ThemeMode
|
import com.stemify.mobile.core.ThemeMode
|
||||||
import kotlin.math.exp
|
import kotlin.math.exp
|
||||||
|
|
||||||
/** iOS semantic colours, flattened to solid values for each scheme. */
|
/** iOS semantic colours, flattened to solid values for each scheme. */
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">StemRoller</string>
|
<string name="app_name">Stemify</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<style name="Theme.StemRoller" parent="android:Theme.Material.NoActionBar">
|
<style name="Theme.Stemify" parent="android:Theme.Material.NoActionBar">
|
||||||
<item name="android:windowBackground">@android:color/black</item>
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
|||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
package com.stemroller.mobile
|
package com.stemify.mobile
|
||||||
|
|
||||||
import com.stemroller.mobile.auto.CarIds
|
import com.stemify.mobile.auto.CarIds
|
||||||
import com.stemroller.mobile.auto.CarTree
|
import com.stemify.mobile.auto.CarTree
|
||||||
import com.stemroller.mobile.playback.MixPresets
|
import com.stemify.mobile.playback.MixPresets
|
||||||
import com.stemroller.mobile.playback.StemSetting
|
import com.stemify.mobile.playback.StemSetting
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
+15
-15
@@ -1,18 +1,18 @@
|
|||||||
package com.stemroller.mobile
|
package com.stemify.mobile
|
||||||
|
|
||||||
import com.stemroller.mobile.engine.EQ_BANDS
|
import com.stemify.mobile.engine.EQ_BANDS
|
||||||
import com.stemroller.mobile.engine.EqChannel
|
import com.stemify.mobile.engine.EqChannel
|
||||||
import com.stemroller.mobile.engine.EqFilter
|
import com.stemify.mobile.engine.EqFilter
|
||||||
import com.stemroller.mobile.engine.FilterType
|
import com.stemify.mobile.engine.FilterType
|
||||||
import com.stemroller.mobile.eq.ApoConfig
|
import com.stemify.mobile.eq.ApoConfig
|
||||||
import com.stemroller.mobile.eq.EqProfile
|
import com.stemify.mobile.eq.EqProfile
|
||||||
import com.stemroller.mobile.engine.EqMath
|
import com.stemify.mobile.engine.EqMath
|
||||||
import com.stemroller.mobile.engine.EqParams
|
import com.stemify.mobile.engine.EqParams
|
||||||
import com.stemroller.mobile.engine.Equalizer
|
import com.stemify.mobile.engine.Equalizer
|
||||||
import com.stemroller.mobile.engine.FloatRing
|
import com.stemify.mobile.engine.FloatRing
|
||||||
import com.stemroller.mobile.engine.Limiter
|
import com.stemify.mobile.engine.Limiter
|
||||||
import com.stemroller.mobile.engine.stemTaper
|
import com.stemify.mobile.engine.stemTaper
|
||||||
import com.stemroller.mobile.engine.stemUntaper
|
import com.stemify.mobile.engine.stemUntaper
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -143,7 +143,7 @@ class DspTest {
|
|||||||
@Test fun oldEqSettingsStillLoad() {
|
@Test fun oldEqSettingsStillLoad() {
|
||||||
val old = """{"enabled":true,"automatic":false,"manualId":"abc","profiles":[{"id":"abc","name":"XM5",
|
val old = """{"enabled":true,"automatic":false,"manualId":"abc","profiles":[{"id":"abc","name":"XM5",
|
||||||
"gains":[1,2,3,0,0,0,0,0,-1,-2],"devices":[{"type":8,"name":"WH-1000XM5"}]}]}"""
|
"gains":[1,2,3,0,0,0,0,0,-1,-2],"devices":[{"type":8,"name":"WH-1000XM5"}]}]}"""
|
||||||
val s = com.stemroller.mobile.core.AppJson.decodeFromString(com.stemroller.mobile.eq.EqState.serializer(), old)
|
val s = com.stemify.mobile.core.AppJson.decodeFromString(com.stemify.mobile.eq.EqState.serializer(), old)
|
||||||
val p = s.profiles.single()
|
val p = s.profiles.single()
|
||||||
assertEquals("XM5", p.name)
|
assertEquals("XM5", p.name)
|
||||||
assertEquals(0, p.filters.size)
|
assertEquals(0, p.filters.size)
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package com.stemroller.mobile
|
package com.stemify.mobile
|
||||||
|
|
||||||
import com.stemroller.mobile.cache.MediaCache
|
import com.stemify.mobile.cache.MediaCache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
+6
-6
@@ -1,10 +1,10 @@
|
|||||||
package com.stemroller.mobile
|
package com.stemify.mobile
|
||||||
|
|
||||||
import com.stemroller.mobile.core.AppJson
|
import com.stemify.mobile.core.AppJson
|
||||||
import com.stemroller.mobile.stems.StemEntry
|
import com.stemify.mobile.stems.StemEntry
|
||||||
import com.stemroller.mobile.stems.StemFile
|
import com.stemify.mobile.stems.StemFile
|
||||||
import com.stemroller.mobile.stems.StemIndex
|
import com.stemify.mobile.stems.StemIndex
|
||||||
import com.stemroller.mobile.stems.StemIndexFile
|
import com.stemify.mobile.stems.StemIndexFile
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assume.assumeTrue
|
import org.junit.Assume.assumeTrue
|
||||||
@@ -19,5 +19,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "StemRoller"
|
rootProject.name = "Stemify"
|
||||||
include(":app")
|
include(":app")
|
||||||
+5
-5
@@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "stemroller"
|
name = "stemify"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Drag-drop stem separation (Demucs + RoFormer) with a 240Hz Material x Aero UI"
|
description = "Drag-drop stem separation (Demucs + RoFormer) with a 240Hz Material x Aero UI"
|
||||||
requires-python = ">=3.12,<3.13"
|
requires-python = ">=3.12,<3.13"
|
||||||
@@ -17,9 +17,9 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
stemroller = "stemroller.app:main"
|
stemify = "stemify.app:main"
|
||||||
stemroller-worker-cli = "stemroller.worker.cli:main"
|
stemify-worker-cli = "stemify.worker.cli:main"
|
||||||
stemroller-player = "stemroller.player.app:main"
|
stemify-player = "stemify.player.app:main"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
environments = ["sys_platform == 'linux' and platform_machine == 'x86_64'"]
|
environments = ["sys_platform == 'linux' and platform_machine == 'x86_64'"]
|
||||||
@@ -44,4 +44,4 @@ build-backend = "hatchling.build"
|
|||||||
allow-direct-references = true
|
allow-direct-references = true
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["stemroller"]
|
packages = ["stemify"]
|
||||||
+18
-18
@@ -1,15 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Add StemRoller and StemRoller Player to the app grid (and "Open with" for audio files).
|
# Add Stemify and Stemify Player to the app grid (and "Open with" for audio files).
|
||||||
# Re-run after moving the project; `--uninstall` removes all of it.
|
# Re-run after moving the project; `--uninstall` removes all of it.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
||||||
ICONS="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps"
|
ICONS="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps"
|
||||||
DESKTOP="$APPS/stemroller.desktop"
|
DESKTOP="$APPS/stemify.desktop"
|
||||||
ICON="$ICONS/stemroller.svg"
|
ICON="$ICONS/stemify.svg"
|
||||||
PLAYER_DESKTOP="$APPS/stemroller-player.desktop"
|
PLAYER_DESKTOP="$APPS/stemify-player.desktop"
|
||||||
PLAYER_ICON="$ICONS/stemroller-player.svg"
|
PLAYER_ICON="$ICONS/stemify-player.svg"
|
||||||
# stem-on / stem-off from older installs; library separation now runs on the server (server/stemd.py).
|
# stem-on / stem-off from older installs; library separation now runs on the server (server/stemd.py).
|
||||||
OLD_BIN="$HOME/.local/bin"
|
OLD_BIN="$HOME/.local/bin"
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ if [[ "${1:-}" == "--uninstall" ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for bin in stemroller stemroller-player; do
|
for bin in stemify stemify-player; do
|
||||||
if [[ ! -x "$ROOT/.venv/bin/$bin" ]]; then
|
if [[ ! -x "$ROOT/.venv/bin/$bin" ]]; then
|
||||||
echo "missing $ROOT/.venv/bin/$bin; run 'uv sync' in $ROOT first" >&2
|
echo "missing $ROOT/.venv/bin/$bin; run 'uv sync' in $ROOT first" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -27,44 +27,44 @@ for bin in stemroller stemroller-player; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
mkdir -p "$APPS" "$ICONS"
|
mkdir -p "$APPS" "$ICONS"
|
||||||
install -m 644 "$ROOT/stemroller/assets/stemroller.svg" "$ICON"
|
install -m 644 "$ROOT/stemify/assets/stemify.svg" "$ICON"
|
||||||
install -m 644 "$ROOT/stemroller/player/assets/stemroller-player.svg" "$PLAYER_ICON"
|
install -m 644 "$ROOT/stemify/player/assets/stemify-player.svg" "$PLAYER_ICON"
|
||||||
|
|
||||||
# StartupWMClass / file name match QGuiApplication.setDesktopFileName("stemroller"),
|
# StartupWMClass / file name match QGuiApplication.setDesktopFileName("stemify"),
|
||||||
# so Wayland groups the window under this entry and shows its icon.
|
# so Wayland groups the window under this entry and shows its icon.
|
||||||
cat > "$DESKTOP" <<EOF
|
cat > "$DESKTOP" <<EOF
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=StemRoller
|
Name=Stemify
|
||||||
GenericName=Stem Separator
|
GenericName=Stem Separator
|
||||||
Comment=Split songs into vocals, drums, bass and more on the GPU
|
Comment=Split songs into vocals, drums, bass and more on the GPU
|
||||||
Exec="$ROOT/.venv/bin/stemroller" %F
|
Exec="$ROOT/.venv/bin/stemify" %F
|
||||||
Path=$ROOT
|
Path=$ROOT
|
||||||
Icon=stemroller
|
Icon=stemify
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=AudioVideo;Audio;
|
Categories=AudioVideo;Audio;
|
||||||
MimeType=audio/mpeg;audio/flac;audio/x-flac;audio/wav;audio/x-wav;audio/ogg;audio/opus;audio/mp4;audio/aac;audio/x-aiff;audio/x-m4a;
|
MimeType=audio/mpeg;audio/flac;audio/x-flac;audio/wav;audio/x-wav;audio/ogg;audio/opus;audio/mp4;audio/aac;audio/x-aiff;audio/x-m4a;
|
||||||
Keywords=stems;demucs;roformer;vocals;karaoke;separate;
|
Keywords=stems;demucs;roformer;vocals;karaoke;separate;
|
||||||
StartupWMClass=stemroller
|
StartupWMClass=stemify
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
EOF
|
EOF
|
||||||
chmod 644 "$DESKTOP"
|
chmod 644 "$DESKTOP"
|
||||||
|
|
||||||
# StartupWMClass matches QGuiApplication.setDesktopFileName("stemroller-player"); the
|
# StartupWMClass matches QGuiApplication.setDesktopFileName("stemify-player"); the
|
||||||
# MPRIS DesktopEntry points here too, so media controls show this icon.
|
# MPRIS DesktopEntry points here too, so media controls show this icon.
|
||||||
cat > "$PLAYER_DESKTOP" <<EOF
|
cat > "$PLAYER_DESKTOP" <<EOF
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=StemRoller Player
|
Name=Stemify Player
|
||||||
GenericName=Music Player
|
GenericName=Music Player
|
||||||
Comment=Your Plex library and its stems, in a spreadsheet
|
Comment=Your Plex library and its stems, in a spreadsheet
|
||||||
Exec="$ROOT/.venv/bin/stemroller-player"
|
Exec="$ROOT/.venv/bin/stemify-player"
|
||||||
Path=$ROOT
|
Path=$ROOT
|
||||||
Icon=stemroller-player
|
Icon=stemify-player
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=AudioVideo;Audio;Player;
|
Categories=AudioVideo;Audio;Player;
|
||||||
Keywords=music;plex;stems;karaoke;player;excel;
|
Keywords=music;plex;stems;karaoke;player;excel;
|
||||||
StartupWMClass=stemroller-player
|
StartupWMClass=stemify-player
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
EOF
|
EOF
|
||||||
chmod 644 "$PLAYER_DESKTOP"
|
chmod 644 "$PLAYER_DESKTOP"
|
||||||
|
|||||||
+12
-7
@@ -1,6 +1,6 @@
|
|||||||
# stemserve
|
# stemserve
|
||||||
|
|
||||||
Serves the StemRoller stems folder to the Android app: an index of every separated
|
Serves the Stemify stems folder to the Android app: an index of every separated
|
||||||
song, the stem files with HTTP range support (so the phone can seek and stream), per-stem
|
song, the stem files with HTTP range support (so the phone can seek and stream), per-stem
|
||||||
waveforms, and optional Opus transcodes so cached songs take ~1/8 the space.
|
waveforms, and optional Opus transcodes so cached songs take ~1/8 the space.
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ from the desktop). From an unraid terminal:
|
|||||||
```sh
|
```sh
|
||||||
mkdir -p /mnt/user/appdata/stemserve/cache /mnt/user/appdata/stemd/state/queue
|
mkdir -p /mnt/user/appdata/stemserve/cache /mnt/user/appdata/stemd/state/queue
|
||||||
chown -R 99:100 /mnt/user/appdata/stemserve /mnt/user/appdata/stemd
|
chown -R 99:100 /mnt/user/appdata/stemserve /mnt/user/appdata/stemd
|
||||||
cp -r /path/to/stemRoller2/server /mnt/user/appdata/stemserve/src # or copy over SMB
|
cp -r /path/to/stemify/server /mnt/user/appdata/stemserve/src # or copy over SMB
|
||||||
docker build -t stemserve /mnt/user/appdata/stemserve/src
|
docker build -t stemserve /mnt/user/appdata/stemserve/src
|
||||||
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 /mnt/user/Music/Stems:/stems:ro \
|
||||||
@@ -36,7 +36,7 @@ curl http://192.168.86.2:8765/api/info
|
|||||||
|
|
||||||
| Variable | Default | Meaning |
|
| Variable | Default | Meaning |
|
||||||
|-------------|----------|------------------------------------------------------------|
|
|-------------|----------|------------------------------------------------------------|
|
||||||
| `STEMS_DIR` | `/stems` | The StemRoller library output folder |
|
| `STEMS_DIR` | `/stems` | The Stemify library output folder |
|
||||||
| `CACHE_DIR` | `/cache` | Opus transcodes and waveforms (safe to delete) |
|
| `CACHE_DIR` | `/cache` | Opus transcodes and waveforms (safe to delete) |
|
||||||
| `PORT` | `8765` | Listen port |
|
| `PORT` | `8765` | Listen port |
|
||||||
| `TOKEN` | empty | If set, requests need `X-Stem-Token` or `?token=` |
|
| `TOKEN` | empty | If set, requests need `X-Stem-Token` or `?token=` |
|
||||||
@@ -66,19 +66,19 @@ STEMS_DIR=/var/mnt/unraid/Music/Stems CACHE_DIR=~/.cache/stemserve python3 stems
|
|||||||
# stemd: separate new music on the server's GPU
|
# stemd: separate new music on the server's GPU
|
||||||
|
|
||||||
`stemd.py` keeps the stems library up to date. It walks the music folder, separates every
|
`stemd.py` keeps the stems library up to date. It walks the music folder, separates every
|
||||||
track whose stems folder has no `.stemroller.json` yet (BS-RoFormer SW 6-stem by default,
|
track whose stems folder has no `.stemify.json` yet (BS-RoFormer SW 6-stem by default,
|
||||||
like the desktop app), and writes them in the layout above, so stemserve and the phone pick
|
like the desktop app), and writes them in the layout above, so stemserve and the phone pick
|
||||||
them up within `INDEX_TTL`. Once it has caught up it checks for new music every 30 minutes.
|
them up within `INDEX_TTL`. Once it has caught up it checks for new music every 30 minutes.
|
||||||
Nothing runs on the desktop.
|
Nothing runs on the desktop.
|
||||||
|
|
||||||
It writes the same layout the desktop app's old library mode did (`stemroller/library_core.py`),
|
It writes the same layout the desktop app's old library mode did (`stemify/library_core.py`),
|
||||||
so the ~300 tracks already done on the desktop are skipped, and a track interrupted
|
so the ~300 tracks already done on the desktop are skipped, and a track interrupted
|
||||||
mid-way (`docker stop`, a reboot) is redone from scratch next time.
|
mid-way (`docker stop`, a reboot) is redone from scratch next time.
|
||||||
|
|
||||||
## Run it on unraid (NVIDIA)
|
## Run it on unraid (NVIDIA)
|
||||||
|
|
||||||
Needs the **Nvidia-Driver** plugin (Apps tab). The image builds from the repo root
|
Needs the **Nvidia-Driver** plugin (Apps tab). The image builds from the repo root
|
||||||
because it uses the `stemroller` package. Copy the repo, or at least `stemroller/`,
|
because it uses the `stemify` package. Copy the repo, or at least `stemify/`,
|
||||||
`server/` and `.dockerignore`, to `/mnt/user/appdata/stemd/src`, then from an unraid terminal:
|
`server/` and `.dockerignore`, to `/mnt/user/appdata/stemd/src`, then from an unraid terminal:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -115,9 +115,14 @@ skipped here too.
|
|||||||
To update after changing the code: copy the files over again, rebuild, then
|
To update after changing the code: copy the files over again, rebuild, then
|
||||||
`docker rm -f stemd` and repeat the `docker run`.
|
`docker rm -f stemd` and repeat the `docker run`.
|
||||||
|
|
||||||
|
Coming from before the rename to Stemify: update stemd first. On its first start it renames
|
||||||
|
the old done-markers in the stems folder to `.stemify.json` (it logs how many), so nothing is
|
||||||
|
separated twice. The new stemserve only looks for the new name, so until stemd has done that
|
||||||
|
the app would briefly see no stems.
|
||||||
|
|
||||||
| Variable | Default | Meaning |
|
| Variable | Default | Meaning |
|
||||||
|--------------|------------------|------------------------------------------------------|
|
|--------------|------------------|------------------------------------------------------|
|
||||||
| `MODEL` | `roformer_bs_sw` | any id from `stemroller/catalog.py` |
|
| `MODEL` | `roformer_bs_sw` | any id from `stemify/catalog.py` |
|
||||||
| `DEVICE` | `cuda` | `cuda`, `cuda:N` or `cpu` |
|
| `DEVICE` | `cuda` | `cuda`, `cuda:N` or `cpu` |
|
||||||
| `RESCAN_MIN` | `30` | minutes between scans once caught up |
|
| `RESCAN_MIN` | `30` | minutes between scans once caught up |
|
||||||
| `FP16` | `1` | half precision on the GPU (`0` for fp32) |
|
| `FP16` | `1` | half precision on the GPU (`0` for fp32) |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# stemd: separates new music into stems on the server's NVIDIA GPU (see stemd.py).
|
# stemd: separates new music into stems on the server's NVIDIA GPU (see stemd.py).
|
||||||
# Build from the repo root, since it needs the stemroller package:
|
# Build from the repo root, since it needs the stemify package:
|
||||||
# docker build -f server/stemd.Dockerfile -t stemd .
|
# docker build -f server/stemd.Dockerfile -t stemd .
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||||
@@ -12,9 +12,9 @@ COPY server/stemd-requirements.txt /tmp/
|
|||||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev \
|
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev \
|
||||||
&& pip install --no-cache-dir --no-deps -r /tmp/stemd-requirements.txt && pip check \
|
&& pip install --no-cache-dir --no-deps -r /tmp/stemd-requirements.txt && pip check \
|
||||||
&& apt-get purge -y gcc libc6-dev && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
|
&& apt-get purge -y gcc libc6-dev && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
|
||||||
COPY stemroller/__init__.py stemroller/catalog.py stemroller/library_core.py /app/stemroller/
|
COPY stemify/__init__.py stemify/catalog.py stemify/library_core.py /app/stemify/
|
||||||
COPY stemroller/worker/__init__.py stemroller/worker/__main__.py stemroller/worker/audio_io.py \
|
COPY stemify/worker/__init__.py stemify/worker/__main__.py stemify/worker/audio_io.py \
|
||||||
stemroller/worker/backends.py /app/stemroller/worker/
|
stemify/worker/backends.py /app/stemify/worker/
|
||||||
COPY server/stemd.py /app/server/
|
COPY server/stemd.py /app/server/
|
||||||
ENV MUSIC_DIR=/music STEMS_DIR=/stems STATE_DIR=/state MODELS_DIR=/models \
|
ENV MUSIC_DIR=/music STEMS_DIR=/stems STATE_DIR=/state MODELS_DIR=/models \
|
||||||
TORCH_HOME=/models/torch HF_HOME=/models/hf NUMBA_CACHE_DIR=/tmp/numba HOME=/tmp \
|
TORCH_HOME=/models/torch HF_HOME=/models/hf NUMBA_CACHE_DIR=/tmp/numba HOME=/tmp \
|
||||||
|
|||||||
+33
-6
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Runs on the server next to stemserve. Walks MUSIC_DIR, and for each track whose folder
|
Runs on the server next to stemserve. Walks MUSIC_DIR, and for each track whose folder
|
||||||
under STEMS_DIR has no marker it runs the separation on the GPU and writes the stems with
|
under STEMS_DIR has no marker it runs the separation on the GPU and writes the stems with
|
||||||
the same layout and marker as the desktop app (stemroller/library_core.py), so stemserve
|
the same layout and marker as the desktop app (stemify/library_core.py), so stemserve
|
||||||
and the phone pick them up as they land. Once caught up it looks again every RESCAN_MIN.
|
and the phone pick them up as they land. Once caught up it looks again every RESCAN_MIN.
|
||||||
|
|
||||||
"Stem Now" in the app goes through stemserve, which drops a request (a JSON list of Plex
|
"Stem Now" in the app goes through stemserve, which drops a request (a JSON list of Plex
|
||||||
@@ -17,7 +17,7 @@ Settings (environment):
|
|||||||
STATE_DIR /state failed-track list
|
STATE_DIR /state failed-track list
|
||||||
QUEUE_DIR STATE_DIR/queue "Stem Now" requests from stemserve
|
QUEUE_DIR STATE_DIR/queue "Stem Now" requests from stemserve
|
||||||
MODELS_DIR /models model weights, downloaded on first use
|
MODELS_DIR /models model weights, downloaded on first use
|
||||||
MODEL roformer_bs_sw any id from stemroller/catalog.py
|
MODEL roformer_bs_sw any id from stemify/catalog.py
|
||||||
DEVICE cuda cuda, cuda:N or cpu
|
DEVICE cuda cuda, cuda:N or cpu
|
||||||
RESCAN_MIN 30 minutes between scans once caught up
|
RESCAN_MIN 30 minutes between scans once caught up
|
||||||
FP16 1 half precision on the GPU
|
FP16 1 half precision on the GPU
|
||||||
@@ -36,8 +36,8 @@ import time
|
|||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from stemroller.catalog import MODELS_BY_ID # noqa: E402
|
from stemify.catalog import MODELS_BY_ID # noqa: E402
|
||||||
from stemroller.library_core import (MAX_SECONDS, SOURCE_RANK, duration, is_done, is_share_error, # noqa: E402
|
from stemify.library_core import (MARKER, MAX_SECONDS, SOURCE_RANK, duration, is_done, is_share_error, # noqa: E402
|
||||||
iter_tracks, signature, write_track)
|
iter_tracks, signature, write_track)
|
||||||
|
|
||||||
MUSIC_DIR = os.environ.get("MUSIC_DIR", "/music")
|
MUSIC_DIR = os.environ.get("MUSIC_DIR", "/music")
|
||||||
@@ -101,7 +101,7 @@ class _NoProgress:
|
|||||||
class Separator:
|
class Separator:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
import torch
|
import torch
|
||||||
from stemroller.worker.backends import DemucsBackend, RoformerBackend
|
from stemify.worker.backends import DemucsBackend, RoformerBackend
|
||||||
|
|
||||||
self.torch = torch
|
self.torch = torch
|
||||||
self.model = MODELS_BY_ID[MODEL]
|
self.model = MODELS_BY_ID[MODEL]
|
||||||
@@ -116,7 +116,7 @@ class Separator:
|
|||||||
def run(self, path, tmp):
|
def run(self, path, tmp):
|
||||||
# Imported late: worker/__main__ sets ROCm env defaults, harmless here, and owns Job's
|
# Imported late: worker/__main__ sets ROCm env defaults, harmless here, and owns Job's
|
||||||
# parameter rules (overlap, fp16, ...), which must match the desktop's.
|
# parameter rules (overlap, fp16, ...), which must match the desktop's.
|
||||||
from stemroller.worker.__main__ import Job
|
from stemify.worker.__main__ import Job
|
||||||
job = Job({"id": "stemd", "file": path, "model": MODEL, "out_dir": tmp, "segment": SEGMENT,
|
job = Job({"id": "stemd", "file": path, "model": MODEL, "out_dir": tmp, "segment": SEGMENT,
|
||||||
"shifts": SHIFTS, "overlap": OVERLAP, "fp16": FP16}, _NoProgress())
|
"shifts": SHIFTS, "overlap": OVERLAP, "fp16": FP16}, _NoProgress())
|
||||||
if "file" in self.model and not os.path.exists(os.path.join(MODELS_DIR, self.model["file"])):
|
if "file" in self.model and not os.path.exists(os.path.join(MODELS_DIR, self.model["file"])):
|
||||||
@@ -183,6 +183,32 @@ def separate(sep, failed, path, dest, sig, label):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# The project was renamed to Stemify; songs separated before that carry the old marker name.
|
||||||
|
# migrate_markers() renames them once so they aren't separated again. Once stemd has run it
|
||||||
|
# against your library, this constant, the function and its call in main() can be deleted.
|
||||||
|
LEGACY_MARKER = ".stemroller.json"
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_markers():
|
||||||
|
"""Rename every legacy done-marker under STEMS_DIR to MARKER (a few seconds per 10k songs)."""
|
||||||
|
n = 0
|
||||||
|
for root, dirs, files in os.walk(STEMS_DIR):
|
||||||
|
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||||
|
if LEGACY_MARKER not in files:
|
||||||
|
continue
|
||||||
|
old, new = os.path.join(root, LEGACY_MARKER), os.path.join(root, MARKER)
|
||||||
|
try:
|
||||||
|
if MARKER in files:
|
||||||
|
os.remove(old)
|
||||||
|
else:
|
||||||
|
os.replace(old, new)
|
||||||
|
n += 1
|
||||||
|
except OSError as e:
|
||||||
|
log(f"can't rename {old}: {e}")
|
||||||
|
if n:
|
||||||
|
log(f"renamed {n:,} {LEGACY_MARKER} markers to {MARKER}")
|
||||||
|
|
||||||
|
|
||||||
def resolve(plex_path):
|
def resolve(plex_path):
|
||||||
"""The music file a Plex path names, or None. Plex sees the library under its own mount
|
"""The music file a Plex path names, or None. Plex sees the library under its own mount
|
||||||
point, so this takes the longest tail of the path (folder and file at least) that exists
|
point, so this takes the longest tail of the path (folder and file at least) that exists
|
||||||
@@ -320,6 +346,7 @@ def main():
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
log(f"WARNING: can't create {QUEUE_DIR} ({e}); Stem Now won't work")
|
log(f"WARNING: can't create {QUEUE_DIR} ({e}); Stem Now won't work")
|
||||||
log(f"stemd: {MUSIC_DIR} -> {STEMS_DIR}")
|
log(f"stemd: {MUSIC_DIR} -> {STEMS_DIR}")
|
||||||
|
migrate_markers()
|
||||||
sep = Separator()
|
sep = Separator()
|
||||||
failed = Failed()
|
failed = Failed()
|
||||||
while not stop.is_set():
|
while not stop.is_set():
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""stemserve: serves StemRoller library stems to the Android app over HTTP.
|
"""stemserve: serves Stemify library stems to the Android app over HTTP.
|
||||||
|
|
||||||
Stdlib only; ffmpeg is optional and enables Opus transcodes and waveforms.
|
Stdlib only; ffmpeg is optional and enables Opus transcodes and waveforms.
|
||||||
|
|
||||||
The stems folder is what stemroller.library writes:
|
The stems folder is what stemify.library writes:
|
||||||
|
|
||||||
<STEMS_DIR>/Artist/Album/01 Song/.stemroller.json
|
<STEMS_DIR>/Artist/Album/01 Song/.stemify.json
|
||||||
<STEMS_DIR>/Artist/Album/01 Song/01 Song - vocals.flac, ...
|
<STEMS_DIR>/Artist/Album/01 Song/01 Song - vocals.flac, ...
|
||||||
|
|
||||||
Endpoints (all GET unless noted; add ?token=... or X-Stem-Token when TOKEN is set):
|
Endpoints (all GET unless noted; add ?token=... or X-Stem-Token when TOKEN is set):
|
||||||
@@ -44,7 +44,7 @@ TOKEN = os.environ.get("TOKEN", "")
|
|||||||
INDEX_TTL = float(os.environ.get("INDEX_TTL", "30"))
|
INDEX_TTL = float(os.environ.get("INDEX_TTL", "30"))
|
||||||
QUEUE_DIR = os.environ.get("QUEUE_DIR", "/queue") # stemd's request folder, shared with it
|
QUEUE_DIR = os.environ.get("QUEUE_DIR", "/queue") # stemd's request folder, shared with it
|
||||||
MAX_BODY = 8 << 20
|
MAX_BODY = 8 << 20
|
||||||
MARKER = ".stemroller.json"
|
MARKER = ".stemify.json"
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
FFMPEG = shutil.which("ffmpeg")
|
FFMPEG = shutil.which("ffmpeg")
|
||||||
OPUS_RATES = (64, 96, 128, 160, 192, 256)
|
OPUS_RATES = (64, 96, 128, 160, 192, 256)
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
@@ -94,7 +94,7 @@ class FrameStats:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
_env_defaults()
|
_env_defaults()
|
||||||
ap = argparse.ArgumentParser(prog="stemroller")
|
ap = argparse.ArgumentParser(prog="stemify")
|
||||||
ap.add_argument("files", nargs="*", help="audio files to queue on launch")
|
ap.add_argument("files", nargs="*", help="audio files to queue on launch")
|
||||||
ap.add_argument("--frame-overlay", action="store_true", help="show p50/p99 frame-time overlay (F12 toggles)")
|
ap.add_argument("--frame-overlay", action="store_true", help="show p50/p99 frame-time overlay (F12 toggles)")
|
||||||
ap.add_argument("--bench", type=float, default=0, metavar="SECONDS",
|
ap.add_argument("--bench", type=float, default=0, metavar="SECONDS",
|
||||||
@@ -119,10 +119,10 @@ def main():
|
|||||||
QQuickWindow.setDefaultAlphaBuffer(True)
|
QQuickWindow.setDefaultAlphaBuffer(True)
|
||||||
|
|
||||||
app = QGuiApplication(sys.argv)
|
app = QGuiApplication(sys.argv)
|
||||||
app.setApplicationName("StemRoller")
|
app.setApplicationName("Stemify")
|
||||||
app.setOrganizationName("stemroller")
|
app.setOrganizationName("stemify")
|
||||||
app.setDesktopFileName("stemroller")
|
app.setDesktopFileName("stemify")
|
||||||
app.setWindowIcon(QIcon(os.path.join(HERE, "assets", "stemroller.svg")))
|
app.setWindowIcon(QIcon(os.path.join(HERE, "assets", "stemify.svg")))
|
||||||
|
|
||||||
font_dir = os.path.join(HERE, "assets", "fonts")
|
font_dir = os.path.join(HERE, "assets", "fonts")
|
||||||
if os.path.isdir(font_dir):
|
if os.path.isdir(font_dir):
|
||||||
@@ -188,7 +188,7 @@ def main():
|
|||||||
|
|
||||||
bridge = StatsBridge()
|
bridge = StatsBridge()
|
||||||
ctx.setContextProperty("frameStats", bridge)
|
ctx.setContextProperty("frameStats", bridge)
|
||||||
engine.load(QUrl.fromLocalFile(os.path.join(HERE, "qml", "StemRoller", "Main.qml")))
|
engine.load(QUrl.fromLocalFile(os.path.join(HERE, "qml", "Stemify", "Main.qml")))
|
||||||
if not engine.rootObjects():
|
if not engine.rootObjects():
|
||||||
print("failed to load QML", file=sys.stderr)
|
print("failed to load QML", file=sys.stderr)
|
||||||
controller.shutdown()
|
controller.shutdown()
|
||||||
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.
@@ -18,7 +18,7 @@ from .jobs_model import Job, JobsModel
|
|||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
from .worker_client import WorkerClient
|
from .worker_client import WorkerClient
|
||||||
|
|
||||||
CACHE_ROOT = os.path.join(os.path.expanduser("~/.cache/stemroller"), "stems")
|
CACHE_ROOT = os.path.join(os.path.expanduser("~/.cache/stemify"), "stems")
|
||||||
AUDIO_EXTS = {".mp3", ".wav", ".flac", ".ogg", ".oga", ".opus", ".m4a", ".aac", ".aif", ".aiff",
|
AUDIO_EXTS = {".mp3", ".wav", ".flac", ".ogg", ".oga", ".opus", ".m4a", ".aac", ".aif", ".aiff",
|
||||||
".wma", ".mp4", ".mkv", ".webm", ".mka", ".alac", ".wv", ".ape"}
|
".wma", ".mp4", ".mkv", ".webm", ".mka", ".alac", ".wv", ".ape"}
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -18,7 +18,7 @@ import soundfile as sf
|
|||||||
|
|
||||||
from .catalog import sort_stems
|
from .catalog import sort_stems
|
||||||
|
|
||||||
MARKER = ".stemroller.json"
|
MARKER = ".stemify.json"
|
||||||
BEST_MODEL = "roformer_bs_sw"
|
BEST_MODEL = "roformer_bs_sw"
|
||||||
MAX_SECONDS = 40 * 60 # longer files are mixes and radio rips
|
MAX_SECONDS = 40 * 60 # longer files are mixes and radio rips
|
||||||
# Lower wins when one folder holds the same track in several formats.
|
# Lower wins when one folder holds the same track in several formats.
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# StemRoller Player
|
# Stemify Player
|
||||||
|
|
||||||
The desktop counterpart to the phone app: your Plex library and its stems, with per-stem
|
The desktop counterpart to the phone app: your Plex library and its stems, with per-stem
|
||||||
faders, mute/solo, mix presets and a master volume. It is dressed as Microsoft Excel 2016.
|
faders, mute/solo, mix presets and a master volume. It is dressed as Microsoft Excel 2016.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
stemroller-player # or "StemRoller Player" in the app grid (scripts/install-desktop.sh)
|
stemify-player # or "Stemify Player" in the app grid (scripts/install-desktop.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
## First run
|
## First run
|
||||||
@@ -40,7 +40,7 @@ KDE Connect on the phone work through MPRIS.
|
|||||||
## How it plays
|
## How it plays
|
||||||
|
|
||||||
- **Stems** come straight off the `Stems` share (`/var/mnt/unraid/Music/Stems`) when it's
|
- **Stems** come straight off the `Stems` share (`/var/mnt/unraid/Music/Stems`) when it's
|
||||||
mounted, else they're downloaded from stemserve once into `~/.cache/stemroller/player/files`
|
mounted, else they're downloaded from stemserve once into `~/.cache/stemify/player/files`
|
||||||
(4 GB, oldest first out). Songs without stems play the original from the music share, or from
|
(4 GB, oldest first out). Songs without stems play the original from the music share, or from
|
||||||
Plex if the share isn't there.
|
Plex if the share isn't there.
|
||||||
- A decoder thread reads every stem about 1.5 s ahead of the playhead, so songs start at once
|
- A decoder thread reads every stem about 1.5 s ahead of the playhead, so songs start at once
|
||||||
@@ -48,12 +48,12 @@ KDE Connect on the phone work through MPRIS.
|
|||||||
taper matches the phone (0.8 = 0 dB, top = +3.9 dB).
|
taper matches the phone (0.8 = 0 dB, top = +3.9 dB).
|
||||||
- Plays are reported to Plex (now playing, and the play count at 90%).
|
- Plays are reported to Plex (now playing, and the play count at 90%).
|
||||||
- Album art: Plex's, or the album folder's `cover.*`, or the picture embedded in the file.
|
- Album art: Plex's, or the album folder's `cover.*`, or the picture embedded in the file.
|
||||||
- The queue, position, mix and volume come back on the next launch (`~/.config/stemroller/`).
|
- The queue, position, mix and volume come back on the next launch (`~/.config/stemify/`).
|
||||||
|
|
||||||
## Checking it without a window
|
## Checking it without a window
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
QT_QPA_PLATFORM=offscreen stemroller-player --no-mpris --screenshot /tmp/p.png@5 --quit-after 6
|
QT_QPA_PLATFORM=offscreen stemify-player --no-mpris --screenshot /tmp/p.png@5 --quit-after 6
|
||||||
```
|
```
|
||||||
|
|
||||||
`--eval 'JS@seconds'` runs JavaScript against the window (e.g. `library.activate(0, 0)@2`).
|
`--eval 'JS@seconds'` runs JavaScript against the window (e.g. `library.activate(0, 0)@2`).
|
||||||
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
|||||||
"""StemRoller Player: the Plex library and its stems, in a workbook."""
|
"""Stemify Player: the Plex library and its stems, in a workbook."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
@@ -12,7 +12,7 @@ def main():
|
|||||||
os.environ.setdefault("QT_QUICK_CONTROLS_STYLE", "Basic")
|
os.environ.setdefault("QT_QUICK_CONTROLS_STYLE", "Basic")
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "wayland;xcb")
|
os.environ.setdefault("QT_QPA_PLATFORM", "wayland;xcb")
|
||||||
os.environ.setdefault("QSG_RENDER_LOOP", "threaded")
|
os.environ.setdefault("QSG_RENDER_LOOP", "threaded")
|
||||||
ap = argparse.ArgumentParser(prog="stemroller-player")
|
ap = argparse.ArgumentParser(prog="stemify-player")
|
||||||
ap.add_argument("--screenshot", action="append", default=[], metavar="PNG@SECONDS",
|
ap.add_argument("--screenshot", action="append", default=[], metavar="PNG@SECONDS",
|
||||||
help="grab the window to PNG after N seconds (repeatable; for headless checks)")
|
help="grab the window to PNG after N seconds (repeatable; for headless checks)")
|
||||||
ap.add_argument("--quit-after", type=float, default=0, metavar="SECONDS")
|
ap.add_argument("--quit-after", type=float, default=0, metavar="SECONDS")
|
||||||
@@ -31,10 +31,10 @@ def main():
|
|||||||
QSurfaceFormat.setDefaultFormat(fmt)
|
QSurfaceFormat.setDefaultFormat(fmt)
|
||||||
|
|
||||||
app = QGuiApplication(sys.argv)
|
app = QGuiApplication(sys.argv)
|
||||||
app.setApplicationName("StemRoller Player")
|
app.setApplicationName("Stemify Player")
|
||||||
app.setOrganizationName("stemroller")
|
app.setOrganizationName("stemify")
|
||||||
app.setDesktopFileName("stemroller-player")
|
app.setDesktopFileName("stemify-player")
|
||||||
app.setWindowIcon(QIcon(os.path.join(HERE, "assets", "stemroller-player.svg")))
|
app.setWindowIcon(QIcon(os.path.join(HERE, "assets", "stemify-player.svg")))
|
||||||
for d in (os.path.join(HERE, "assets", "fonts"), os.path.join(HERE, "..", "assets", "fonts")):
|
for d in (os.path.join(HERE, "assets", "fonts"), os.path.join(HERE, "..", "assets", "fonts")):
|
||||||
if os.path.isdir(d):
|
if os.path.isdir(d):
|
||||||
for f in sorted(os.listdir(d)):
|
for f in sorted(os.listdir(d)):
|
||||||
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.
@@ -1,11 +1,11 @@
|
|||||||
"""Player settings and saved state: ~/.config/stemroller/player.json."""
|
"""Player settings and saved state: ~/.config/stemify/player.json."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
CONFIG_DIR = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "stemroller")
|
CONFIG_DIR = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "stemify")
|
||||||
CACHE_DIR = os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")), "stemroller", "player")
|
CACHE_DIR = os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")), "stemify", "player")
|
||||||
PATH = os.path.join(CONFIG_DIR, "player.json")
|
PATH = os.path.join(CONFIG_DIR, "player.json")
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -27,8 +27,8 @@ class _Root(QDBusAbstractAdaptor):
|
|||||||
CanQuit = Property(bool, lambda self: True)
|
CanQuit = Property(bool, lambda self: True)
|
||||||
CanRaise = Property(bool, lambda self: True)
|
CanRaise = Property(bool, lambda self: True)
|
||||||
HasTrackList = Property(bool, lambda self: False)
|
HasTrackList = Property(bool, lambda self: False)
|
||||||
Identity = Property(str, lambda self: "StemRoller Player")
|
Identity = Property(str, lambda self: "Stemify Player")
|
||||||
DesktopEntry = Property(str, lambda self: "stemroller-player")
|
DesktopEntry = Property(str, lambda self: "stemify-player")
|
||||||
SupportedUriSchemes = Property("QStringList", lambda self: [])
|
SupportedUriSchemes = Property("QStringList", lambda self: [])
|
||||||
SupportedMimeTypes = Property("QStringList", lambda self: [])
|
SupportedMimeTypes = Property("QStringList", lambda self: [])
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class Mpris(QObject):
|
|||||||
self._adaptor = _Player(self, self)
|
self._adaptor = _Player(self, self)
|
||||||
self.bus = QDBusConnection.sessionBus()
|
self.bus = QDBusConnection.sessionBus()
|
||||||
self.ok = self.bus.isConnected() and self.bus.registerObject(PATH, self) and \
|
self.ok = self.bus.isConnected() and self.bus.registerObject(PATH, self) and \
|
||||||
self.bus.registerService("org.mpris.MediaPlayer2.stemroller_player")
|
self.bus.registerService("org.mpris.MediaPlayer2.stemify_player")
|
||||||
self._last = {}
|
self._last = {}
|
||||||
player.changed.connect(self._changed)
|
player.changed.connect(self._changed)
|
||||||
player.mixChanged.connect(self._changed)
|
player.mixChanged.connect(self._changed)
|
||||||
@@ -128,7 +128,7 @@ class Mpris(QObject):
|
|||||||
if label and label != "Full Mix":
|
if label and label != "Full Mix":
|
||||||
artist = f"{artist} · {label}" # the mix rides on the artist line, as on the phone
|
artist = f"{artist} · {label}" # the mix rides on the artist line, as on the phone
|
||||||
md = {
|
md = {
|
||||||
"mpris:trackid": QDBusObjectPath("/stemroller/track/t" + hashlib.sha1(t["key"].encode()).hexdigest()[:16]),
|
"mpris:trackid": QDBusObjectPath("/stemify/track/t" + hashlib.sha1(t["key"].encode()).hexdigest()[:16]),
|
||||||
"mpris:length": int((self.engine.duration or t.get("durationMs", 0) / 1000) * 1e6),
|
"mpris:length": int((self.engine.duration or t.get("durationMs", 0) / 1000) * 1e6),
|
||||||
"xesam:title": t["title"],
|
"xesam:title": t["title"],
|
||||||
"xesam:artist": [artist],
|
"xesam:artist": [artist],
|
||||||
File renamed without changes.
@@ -6,7 +6,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
PRODUCT = "StemRoller Player"
|
PRODUCT = "Stemify Player"
|
||||||
PAGE = 1000
|
PAGE = 1000
|
||||||
|
|
||||||
|
|
||||||
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 159 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user