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,136 @@
|
||||
import QtQuick
|
||||
|
||||
// The stem faders as an Excel column chart. Drag a column to set its level; double-click
|
||||
// for 0 dB. Below each category: M(ute) and S(olo).
|
||||
Rectangle {
|
||||
id: ch
|
||||
color: "#FFFFFF"
|
||||
border.color: "#D9D9D9"
|
||||
readonly property var stems: player.stems
|
||||
readonly property int n: stems.length
|
||||
readonly property real unity: 0.8
|
||||
|
||||
T {
|
||||
id: title
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 6
|
||||
text: "Stem Mix"
|
||||
font.family: cellFont
|
||||
font.pixelSize: 17
|
||||
color: X.chartText
|
||||
}
|
||||
|
||||
Item {
|
||||
id: plot
|
||||
x: 34
|
||||
y: title.y + title.height + 18
|
||||
width: parent.width - x - 8
|
||||
height: parent.height - y - 52
|
||||
|
||||
// value axis: fader position, 0 dB marked
|
||||
Repeater {
|
||||
model: [0, 0.25, 0.5, 0.8, 1.0]
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
y: plot.height * (1 - modelData)
|
||||
width: plot.width
|
||||
Rectangle { width: parent.width; height: 1; color: modelData === 0.8 ? "#A6A6A6" : X.chartLine }
|
||||
T {
|
||||
x: -32
|
||||
width: 28
|
||||
y: -7
|
||||
height: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font.family: cellFont
|
||||
font.pixelSize: 11
|
||||
color: X.chartText
|
||||
text: modelData === 0 ? "-∞" : modelData === 0.8 ? "0 dB" : modelData === 1 ? "+4" : (40 * Math.log(modelData / 0.8) / Math.LN10).toFixed(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ch.stems
|
||||
delegate: Item {
|
||||
id: cat
|
||||
required property int index
|
||||
required property var modelData
|
||||
readonly property real cw: plot.width / Math.max(1, ch.n)
|
||||
x: index * cw
|
||||
width: cw
|
||||
height: plot.height
|
||||
|
||||
Rectangle {
|
||||
id: bar
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: cat.cw * 0.62
|
||||
height: Math.max(1, plot.height * cat.modelData.level)
|
||||
y: plot.height - height
|
||||
color: cat.modelData.color
|
||||
opacity: cat.modelData.audible ? 1 : 0.22
|
||||
border.color: dm.containsMouse || dm.pressed ? "#404040" : "transparent"
|
||||
Behavior on height { enabled: !dm.pressed; NumberAnimation { duration: X.easeMs; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
T { // data label
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: bar.y - 16
|
||||
font.family: cellFont
|
||||
font.pixelSize: 11
|
||||
color: X.chartText
|
||||
text: cat.modelData.muted ? "Muted" : !cat.modelData.audible ? "—" : cat.modelData.level <= 0.001 ? "-∞" : (cat.modelData.db >= 0 ? "+" : "") + cat.modelData.db.toFixed(1)
|
||||
}
|
||||
MouseArea {
|
||||
id: dm
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.SizeVerCursor
|
||||
function set(m) { player.setLevel(cat.index, Math.max(0, Math.min(1, 1 - m.y / plot.height))) }
|
||||
onPressed: (m) => set(m)
|
||||
onPositionChanged: (m) => { if (pressed) set(m) }
|
||||
onDoubleClicked: player.setLevel(cat.index, ch.unity)
|
||||
onWheel: (w) => player.setLevel(cat.index, Math.max(0, Math.min(1, cat.modelData.level + (w.angleDelta.y > 0 ? 0.02 : -0.02))))
|
||||
}
|
||||
Tip { target: dm; text: cat.modelData.label + ": drag to set, double-click for 0 dB" }
|
||||
|
||||
// category axis label and the M / S toggles
|
||||
Column {
|
||||
y: plot.height + 4
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 3
|
||||
T {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: cellFont
|
||||
font.pixelSize: 12
|
||||
color: X.chartText
|
||||
text: cat.modelData.label
|
||||
width: Math.min(implicitWidth, cat.cw)
|
||||
}
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
Toggle { label: "M"; on: cat.modelData.muted; onFill: X.bad; onText: X.badText; tip: "Mute " + cat.modelData.label; onClicked: player.toggleMute(cat.index) }
|
||||
Toggle { label: "S"; on: cat.modelData.solo; onFill: X.neutral; onText: X.neutralText; tip: "Solo " + cat.modelData.label; onClicked: player.toggleSolo(cat.index) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { y: plot.height; width: plot.width; height: 1; color: "#BFBFBF" }
|
||||
}
|
||||
|
||||
component Toggle: Rectangle {
|
||||
id: tg
|
||||
property string label
|
||||
property bool on: false
|
||||
property color onFill
|
||||
property color onText
|
||||
property string tip
|
||||
signal clicked()
|
||||
width: 18
|
||||
height: 18
|
||||
color: on ? onFill : tgm.containsMouse ? "#F2F2F2" : "#FFFFFF"
|
||||
border.color: on ? onText : "#BFBFBF"
|
||||
T { anchors.centerIn: parent; text: tg.label; font.pixelSize: 11; font.bold: tg.on; color: tg.on ? tg.onText : X.inkSoft }
|
||||
MouseArea { id: tgm; anchors.fill: parent; hoverEnabled: true; onClicked: tg.clicked() }
|
||||
Tip { target: tgm; text: tg.tip }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user