StemRoller: desktop stem separator, Android stems player, and server daemons
- stemroller/: PySide6/QML desktop app and player for separating music into stems - android/: Plex music player that plays per-stem mixes, with Android Auto support, a graphic + parametric (Equalizer APO-style) EQ, and offline downloads - server/: stemserve (serves stems to the app) and stemd (separates the library on the server's GPU, with "Stem Now" priority requests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
716c5499ff
commit
1845f03c41
184 files changed
+24648
No files matched your search
@@ -0,0 +1,112 @@
|
||||
"""UI-side owner of the separation worker process and its socket."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, Signal
|
||||
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
||||
|
||||
|
||||
class WorkerClient(QObject):
|
||||
stateChanged = Signal(str) # starting | ready | crashed
|
||||
hello = Signal(dict)
|
||||
vram = Signal(str, int, int) # device, free_mb, total_mb
|
||||
progress = Signal(str, float, str) # id, pct, stage
|
||||
done = Signal(str, dict)
|
||||
failed = Signal(str, str) # id, message
|
||||
cancelled = Signal(str)
|
||||
crashed = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
runtime = os.environ.get("XDG_RUNTIME_DIR") or "/tmp"
|
||||
self._sock_path = os.path.join(runtime, f"stemroller-{os.getpid()}.sock")
|
||||
QLocalServer.removeServer(self._sock_path)
|
||||
self._server = QLocalServer(self)
|
||||
self._server.newConnection.connect(self._on_connection)
|
||||
if not self._server.listen(self._sock_path):
|
||||
raise RuntimeError(f"cannot listen on {self._sock_path}: {self._server.errorString()}")
|
||||
self._sock: QLocalSocket | None = None
|
||||
self._buf = b""
|
||||
self._proc: QProcess | None = None
|
||||
self._quitting = False
|
||||
self.state = "starting"
|
||||
self._restart_delay = 500
|
||||
|
||||
def start(self):
|
||||
self._set_state("starting")
|
||||
proc = QProcess(self)
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
env.insert("PYTHONPATH", pkg_root + os.pathsep + env.value("PYTHONPATH", ""))
|
||||
env.insert("PYTHONUNBUFFERED", "1")
|
||||
proc.setProcessEnvironment(env)
|
||||
proc.setProcessChannelMode(QProcess.ProcessChannelMode.ForwardedChannels)
|
||||
proc.finished.connect(self._on_finished)
|
||||
proc.start(sys.executable, ["-m", "stemroller.worker", "--socket", self._sock_path])
|
||||
self._proc = proc
|
||||
|
||||
def send(self, type_, **payload):
|
||||
if self._sock and self._sock.state() == QLocalSocket.LocalSocketState.ConnectedState:
|
||||
self._sock.write((json.dumps({"type": type_, **payload}) + "\n").encode())
|
||||
self._sock.flush()
|
||||
return True
|
||||
return False
|
||||
|
||||
def pid(self):
|
||||
return int(self._proc.processId()) if self._proc else 0
|
||||
|
||||
def shutdown(self):
|
||||
self._quitting = True
|
||||
self.send("quit")
|
||||
if self._proc and self._proc.state() != QProcess.ProcessState.NotRunning:
|
||||
if not self._proc.waitForFinished(1500):
|
||||
self._proc.kill()
|
||||
self._proc.waitForFinished(1000)
|
||||
self._server.close()
|
||||
|
||||
def _set_state(self, s):
|
||||
self.state = s
|
||||
self.stateChanged.emit(s)
|
||||
|
||||
def _on_connection(self):
|
||||
sock = self._server.nextPendingConnection()
|
||||
if self._sock:
|
||||
self._sock.deleteLater()
|
||||
self._sock = sock
|
||||
self._buf = b""
|
||||
sock.readyRead.connect(self._on_ready_read)
|
||||
|
||||
def _on_ready_read(self):
|
||||
self._buf += bytes(self._sock.readAll())
|
||||
while b"\n" in self._buf:
|
||||
line, self._buf = self._buf.split(b"\n", 1)
|
||||
if line.strip():
|
||||
self._dispatch(json.loads(line))
|
||||
|
||||
def _dispatch(self, msg):
|
||||
t = msg.pop("type")
|
||||
if t == "hello":
|
||||
self._restart_delay = 500
|
||||
self._set_state("ready")
|
||||
self.hello.emit(msg)
|
||||
elif t == "vram":
|
||||
self.vram.emit(msg["device"], msg["free_mb"], msg["total_mb"])
|
||||
elif t == "progress":
|
||||
self.progress.emit(msg["id"], msg["pct"], msg["stage"])
|
||||
elif t == "done":
|
||||
self.done.emit(msg.pop("id"), msg)
|
||||
elif t == "error":
|
||||
self.failed.emit(msg.get("id") or "", msg.get("msg", "unknown error"))
|
||||
elif t == "cancelled":
|
||||
self.cancelled.emit(msg["id"])
|
||||
|
||||
def _on_finished(self, code, status):
|
||||
self._sock = None
|
||||
if self._quitting:
|
||||
return
|
||||
self._set_state("crashed")
|
||||
self.crashed.emit()
|
||||
QTimer.singleShot(self._restart_delay, self.start)
|
||||
self._restart_delay = min(self._restart_delay * 2, 10000)
|
||||
Reference in new issue
Block a user