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,78 @@
|
||||
"""Persistent settings exposed to QML as plain properties."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import Property, QObject, Signal
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.expanduser("~/.config/stemroller"), "settings.json")
|
||||
|
||||
DEFAULTS = {
|
||||
"modelId": "htdemucs",
|
||||
"twoStems": False,
|
||||
"deviceId": "auto",
|
||||
"outputMode": "beside", # "beside" the source file, or "folder" (stemsDir)
|
||||
"stemsDir": os.path.expanduser("~/Music/Stems"),
|
||||
"exportFormat": "wav", # wav | flac | mp3
|
||||
"segment": 0.0, # seconds; 0 = model default
|
||||
"shifts": 0,
|
||||
"overlap": 0.25,
|
||||
"fp16": True,
|
||||
"autoExport": False,
|
||||
"frameOverlay": False,
|
||||
"cacheLimitGb": 8.0,
|
||||
"masterVolume": 1.0, # slider position 0..1 (cubic taper in the audio engine)
|
||||
}
|
||||
|
||||
|
||||
class _SettingsBase(QObject):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._values = dict(DEFAULTS)
|
||||
try:
|
||||
with open(CONFIG_PATH) as f:
|
||||
stored = json.load(f)
|
||||
for k, v in stored.items():
|
||||
if k not in DEFAULTS:
|
||||
continue
|
||||
want = type(DEFAULTS[k])
|
||||
if type(v) is want or (want is float and type(v) is int):
|
||||
self._values[k] = want(v)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
tmp = CONFIG_PATH + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(self._values, f, indent=2)
|
||||
os.replace(tmp, CONFIG_PATH)
|
||||
|
||||
def get(self, name):
|
||||
return self._values[name]
|
||||
|
||||
|
||||
def _build():
|
||||
# Qt only sees properties and signals present when the class is created,
|
||||
# so generate one Property + <name>Changed signal per default up front.
|
||||
ns = {}
|
||||
for name, default in DEFAULTS.items():
|
||||
type_ = type(default)
|
||||
sig = Signal()
|
||||
ns[f"{name}Changed"] = sig
|
||||
|
||||
def getter(self, _n=name):
|
||||
return self._values[_n]
|
||||
|
||||
def setter(self, value, _n=name, _t=type_):
|
||||
value = _t(value)
|
||||
if self._values[_n] != value:
|
||||
self._values[_n] = value
|
||||
getattr(self, f"{_n}Changed").emit()
|
||||
self.save()
|
||||
|
||||
ns[name] = Property(type_, getter, setter, notify=sig)
|
||||
return type("Settings", (_SettingsBase,), ns)
|
||||
|
||||
|
||||
Settings = _build()
|
||||
Reference in new issue
Block a user