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,209 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
import QtQuick.Dialogs
|
||||
import StemRoller
|
||||
|
||||
Window {
|
||||
id: win
|
||||
width: 1440
|
||||
height: 900
|
||||
minimumWidth: 960
|
||||
minimumHeight: 600
|
||||
visible: true
|
||||
title: "StemRoller"
|
||||
color: "transparent"
|
||||
flags: Qt.Window | Qt.FramelessWindowHint
|
||||
|
||||
readonly property bool maximized: visibility === Window.Maximized || visibility === Window.FullScreen
|
||||
readonly property int radius: maximized ? 0 : 10
|
||||
function toggleMaximized() { maximized ? showNormal() : showMaximized() }
|
||||
|
||||
// Glass snapshot size: refreshed only after a resize settles.
|
||||
property int fxW: width
|
||||
property int fxH: height
|
||||
Timer { id: fxTimer; interval: 150; onTriggered: { win.fxW = win.width; win.fxH = win.height } }
|
||||
onWidthChanged: fxTimer.restart()
|
||||
onHeightChanged: fxTimer.restart()
|
||||
|
||||
// Per-frame playback state, read from the audio ring buffer.
|
||||
property real playPos: 0
|
||||
property real levelL: 0
|
||||
property real levelR: 0
|
||||
FrameAnimation {
|
||||
running: audio.playing || win.levelL > 0.0005 || win.levelR > 0.0005
|
||||
onTriggered: {
|
||||
var s = audio.snapshot()
|
||||
if (s[3]) win.playPos = s[0]
|
||||
var k = Math.exp(-frameTime * 10)
|
||||
win.levelL = Math.max(s[1], win.levelL * k)
|
||||
win.levelR = Math.max(s[2], win.levelR * k)
|
||||
if (win.levelL < 0.0005) win.levelL = 0
|
||||
if (win.levelR < 0.0005) win.levelR = 0
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: audio
|
||||
function onLoadedChanged() { win.playPos = 0 }
|
||||
}
|
||||
Connections {
|
||||
target: app
|
||||
function onToast(msg, kind, folder) { toast.show(msg, kind, folder) }
|
||||
}
|
||||
Component.onCompleted: Theme.fontFamily = uiFont
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: "Add audio files"
|
||||
fileMode: FileDialog.OpenFiles
|
||||
nameFilters: ["Audio (*.mp3 *.wav *.flac *.ogg *.oga *.opus *.m4a *.aac *.aif *.aiff *.wma *.mp4 *.mkv *.webm *.mka *.wv *.ape)", "All files (*)"]
|
||||
onAccepted: app.addUrls(selectedFiles)
|
||||
}
|
||||
|
||||
Shortcut { sequences: ["Space"]; enabled: audio.loaded && !settings.opened; onActivated: audio.toggle() }
|
||||
Shortcut { sequences: ["Home"]; enabled: audio.loaded; onActivated: { audio.seek(0); win.playPos = 0 } }
|
||||
Shortcut { sequences: [StandardKey.Open]; onActivated: fileDialog.open() }
|
||||
Shortcut { sequences: ["Ctrl+,"]; onActivated: settings.open() }
|
||||
Shortcut { sequences: ["F12"]; onActivated: appSettings.frameOverlay = !appSettings.frameOverlay }
|
||||
Shortcut { sequences: ["F11"]; onActivated: win.toggleMaximized() }
|
||||
Shortcut { sequences: [StandardKey.Quit]; onActivated: win.close() }
|
||||
|
||||
Item {
|
||||
id: frame
|
||||
anchors.fill: parent
|
||||
|
||||
// window background: placeholder until the aurora texture arrives
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: win.radius
|
||||
color: Theme.bg
|
||||
}
|
||||
Image {
|
||||
id: backdrop
|
||||
anchors.fill: parent
|
||||
source: "image://fx/backdrop/" + win.fxW + "x" + win.fxH + "/" + win.radius
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
cache: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.slow } }
|
||||
}
|
||||
|
||||
TitleBar {
|
||||
id: titleBar
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
offsetX: 0
|
||||
offsetY: 0
|
||||
onOpenSettings: settings.open()
|
||||
}
|
||||
|
||||
Sidebar {
|
||||
id: sidebar
|
||||
anchors { left: parent.left; top: titleBar.bottom; bottom: transport.top }
|
||||
width: 220
|
||||
offsetX: 0
|
||||
offsetY: titleBar.height
|
||||
onBrowse: fileDialog.open()
|
||||
Rectangle { anchors { right: parent.right; top: parent.top; bottom: parent.bottom } width: 1; color: Theme.hairline }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mainPane
|
||||
anchors { left: sidebar.right; right: parent.right; top: titleBar.bottom; bottom: transport.top }
|
||||
clip: true
|
||||
|
||||
Image { // dither the dark gradients behind content
|
||||
anchors.fill: parent
|
||||
source: "image://fx/noise"
|
||||
fillMode: Image.Tile
|
||||
cache: true
|
||||
}
|
||||
|
||||
DropZone {
|
||||
anchors.fill: parent
|
||||
visible: !app.selected.valid
|
||||
dragging: drop.containsDrag
|
||||
onBrowse: fileDialog.open()
|
||||
}
|
||||
TrackView {
|
||||
anchors.fill: parent
|
||||
visible: app.selected.valid
|
||||
}
|
||||
|
||||
// drag-over affordance when a track is already showing
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: opacity > 0
|
||||
opacity: drop.containsDrag && app.selected.valid ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.med } }
|
||||
Rectangle { anchors.fill: parent; color: Theme.alpha(Theme.bg, 0.82) }
|
||||
DashedFrame { anchors.fill: parent; anchors.margins: 24; radius: 16 }
|
||||
Glow { anchors.margins: 24 - m; radius: 16; sigma: 8; opacity: 0.4 }
|
||||
Txt {
|
||||
anchors.centerIn: parent
|
||||
text: "Drop to add to the queue"
|
||||
font.pixelSize: Theme.fs28
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
FrameOverlay {
|
||||
visible: appSettings.frameOverlay || devOverlay
|
||||
anchors { right: parent.right; bottom: parent.bottom; margins: 12 }
|
||||
z: 100
|
||||
}
|
||||
}
|
||||
|
||||
Transport {
|
||||
id: transport
|
||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||
height: 64
|
||||
offsetX: 0
|
||||
offsetY: win.height - 64
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors { horizontalCenter: mainPane.horizontalCenter; bottom: transport.top; bottomMargin: 16 }
|
||||
width: toast.width
|
||||
height: 48
|
||||
Toast { id: toast; maxWidth: mainPane.width - 48 }
|
||||
}
|
||||
|
||||
// window edge highlight
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: win.radius
|
||||
color: "transparent"
|
||||
border.width: win.maximized ? 0 : 1
|
||||
border.color: Qt.rgba(1, 1, 1, 0.12)
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: drop
|
||||
anchors.fill: parent
|
||||
keys: ["text/uri-list"]
|
||||
onDropped: (e) => {
|
||||
if (e.hasUrls) {
|
||||
app.addUrls(e.urls)
|
||||
e.acceptProposedAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resize grips (compositor-driven)
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: !win.maximized
|
||||
readonly property int g: 6
|
||||
ResizeEdge { edges: Qt.LeftEdge; x: 0; y: 12; width: parent.g; height: parent.height - 24 }
|
||||
ResizeEdge { edges: Qt.RightEdge; x: parent.width - parent.g; y: 12; width: parent.g; height: parent.height - 24 }
|
||||
ResizeEdge { edges: Qt.TopEdge; x: 12; y: 0; width: parent.width - 24; height: parent.g - 2 }
|
||||
ResizeEdge { edges: Qt.BottomEdge; x: 12; y: parent.height - parent.g; width: parent.width - 24; height: parent.g }
|
||||
ResizeEdge { edges: Qt.LeftEdge | Qt.TopEdge; x: 0; y: 0; width: 12; height: 12 }
|
||||
ResizeEdge { edges: Qt.RightEdge | Qt.TopEdge; x: parent.width - 12; y: 0; width: 12; height: 12 }
|
||||
ResizeEdge { edges: Qt.LeftEdge | Qt.BottomEdge; x: 0; y: parent.height - 12; width: 12; height: 12 }
|
||||
ResizeEdge { edges: Qt.RightEdge | Qt.BottomEdge; x: parent.width - 12; y: parent.height - 12; width: 12; height: 12 }
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDialog { id: settings; parent: Overlay.overlay; win: win }
|
||||
}
|
||||
Reference in new issue
Block a user