Files
stemify/stemroller/qml/StemRoller/Main.qml
T
ClericalErrorandClaude Opus 5.5 1845f03c41 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>
2026-10-14 21:17:17 -07:00

210 lines
7.7 KiB
QML

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 }
}