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:
ClericalErrorandClaude Opus 5.5 committed 2026-10-14 21:17:17 -07:00
1 parent 716c5499ff
commit 1845f03c41
184 files changed
+24648

No files matched your search

@@ -0,0 +1,96 @@
import QtQuick
// Ready / Calculating on the left; Average, Count, Sum; view buttons; the zoom slider is the volume.
Rectangle {
id: sb
property var win: Window.window
height: 24
color: X.green
T {
x: 10
height: parent.height
color: "#FFFFFF"
text: library.status !== "" ? library.status + (library.progress >= 0 ? ": " + Math.round(library.progress * 100) + "%" : "")
: player.status === "loading" ? "Calculating..."
: player.status === "buffering" ? "Buffering..."
: player.status === "playing" ? "Playing" + (player.presetLabel && player.presetLabel !== "Full Mix" ? " · " + player.presetLabel : "")
: "Ready"
}
Rectangle { // progress for Calculating, like Excel's
visible: library.progress >= 0
x: 190
anchors.verticalCenter: parent.verticalCenter
width: 120
height: 8
color: "#FFFFFF"
opacity: 0.9
Rectangle { width: parent.width * Math.max(0, library.progress); height: parent.height; color: "#9FD2B4" }
}
Row {
anchors.right: parent.right
anchors.rightMargin: 10
height: parent.height
spacing: 18
Row {
height: parent.height
spacing: 18
visible: !!sb.win.selStats.count
T { color: "#FFFFFF"; height: parent.height; visible: !!sb.win.selStats.average; text: "Average: " + (sb.win.selStats.average || "") }
T { color: "#FFFFFF"; height: parent.height; text: "Count: " + (sb.win.selStats.count || "") }
T { color: "#FFFFFF"; height: parent.height; visible: !!sb.win.selStats.sum; text: "Sum: " + (sb.win.selStats.sum || "") }
}
Row {
height: parent.height
spacing: 2
Repeater {
model: ["normal_view", "page_view", "break_view"]
delegate: Item {
required property int index
required property string modelData
width: 24
height: sb.height
Rectangle { anchors.fill: parent; anchors.margins: 2; color: index === 0 ? X.greenDark : "transparent" }
Ico { anchors.centerIn: parent; name: parent.modelData; ink: "#FFFFFF"; size: 16 }
}
}
}
// the zoom slider, wired to the master volume
Row {
height: parent.height
spacing: 6
T { color: "#FFFFFF"; height: parent.height; text: "−"; font.pixelSize: 16
MouseArea { anchors.fill: parent; anchors.margins: -4; onClicked: engine.volume = Math.max(0, engine.volume - 0.05) } }
Item {
id: track
width: 110
height: parent.height
Rectangle { anchors.verticalCenter: parent.verticalCenter; width: parent.width; height: 2; color: "#FFFFFF"; opacity: 0.8 }
Rectangle { x: parent.width / 2 - 1; anchors.verticalCenter: parent.verticalCenter; width: 2; height: 8; color: "#FFFFFF"; opacity: 0.8 }
Rectangle {
x: engine.volume * (track.width - width)
anchors.verticalCenter: parent.verticalCenter
width: 6
height: 13
color: vm.pressed ? "#E0E0E0" : "#FFFFFF"
}
MouseArea {
id: vm
anchors.fill: parent
anchors.margins: -3
hoverEnabled: true
function set(m) { engine.volume = Math.max(0, Math.min(1, (m.x - 3) / (track.width))) }
onPressed: (m) => set(m)
onPositionChanged: (m) => { if (pressed) set(m) }
onWheel: (w) => engine.volume = Math.max(0, Math.min(1, engine.volume + (w.angleDelta.y > 0 ? 0.03 : -0.03)))
}
Tip { target: vm; text: "Volume" }
}
T { color: "#FFFFFF"; height: parent.height; text: "+"; font.pixelSize: 15
MouseArea { anchors.fill: parent; anchors.margins: -4; onClicked: engine.volume = Math.min(1, engine.volume + 0.05) } }
T { color: "#FFFFFF"; height: parent.height; width: 40; horizontalAlignment: Text.AlignRight; text: Math.round(engine.volume * 100) + "%" }
}
}
}