Files
stemify/stemroller/qml/StemRoller/SettingsDialog.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

275 lines
12 KiB
QML

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Dialogs
// Glass dialog (elevation 3, stronger 24px blur, 50% scrim).
Popup {
id: dlg
property var win: null
modal: true
focus: true
width: Math.min(parent.width - 64, 640)
height: Math.min(parent.height - 64, content.implicitHeight + 120)
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: app.refreshCacheSize()
Overlay.modal: Rectangle {
color: Qt.rgba(0, 0, 0, 0.5)
Behavior on opacity { NumberAnimation { duration: Theme.med } }
}
enter: Transition {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: Theme.med; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.ease }
NumberAnimation { property: "scale"; from: 0.95; to: 1; duration: Theme.slow; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.ease }
}
exit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: Theme.fast } }
background: Item {
Elevation { level: 3; radius: 12 }
Image { // stronger-blur glass sheet, pre-rounded, rendered once per open/resize
anchors.fill: parent
asynchronous: true
retainWhileLoading: true
source: dlg.win && dlg.width > 0
? "image://fx/glasscrop/" + dlg.win.fxW + "x" + dlg.win.fxH + "/0/24/"
+ Math.round(dlg.x) + "," + Math.round(dlg.y) + "," + Math.round(dlg.width) + "," + Math.round(dlg.height) + "/12"
: ""
}
Rectangle { // Aero gloss
anchors { left: parent.left; right: parent.right; top: parent.top; margins: 1 }
height: 48
topLeftRadius: 11
topRightRadius: 11
gradient: Gradient {
GradientStop { position: 0; color: Qt.rgba(1, 1, 1, 0.08) }
GradientStop { position: 1; color: Qt.rgba(1, 1, 1, 0.0) }
}
}
Rectangle {
anchors.fill: parent
radius: 12
color: "transparent"
border.color: Qt.rgba(1, 1, 1, 0.12)
}
}
FolderDialog {
id: folderDialog
title: "Choose the stems folder"
currentFolder: "file://" + appSettings.stemsDir
onAccepted: appSettings.stemsDir = decodeURIComponent(selectedFolder.toString().replace("file://", ""))
}
component SectionTitle: Txt {
font.pixelSize: 11
font.weight: Font.DemiBold
font.letterSpacing: 0.8
color: Theme.accentGlow
topPadding: 8
}
component Hint: Txt {
font.pixelSize: Theme.fs12
color: Theme.textLo
wrapMode: Text.WordWrap
elide: Text.ElideNone
}
component Segmented: Row {
id: seg
property var options: []
property string value: ""
signal picked(string v)
spacing: 0
Repeater {
model: seg.options
AbstractButton {
id: sb
required property var modelData
required property int index
readonly property bool on: seg.value === modelData.v
implicitWidth: 84
implicitHeight: 34
hoverEnabled: true
onClicked: seg.picked(modelData.v)
background: Rectangle {
topLeftRadius: sb.index === 0 ? 17 : 0
bottomLeftRadius: sb.index === 0 ? 17 : 0
topRightRadius: sb.index === seg.options.length - 1 ? 17 : 0
bottomRightRadius: sb.index === seg.options.length - 1 ? 17 : 0
color: sb.on ? Theme.alpha(Theme.primary, 0.28) : (sb.hovered ? Qt.rgba(1, 1, 1, 0.07) : Qt.rgba(1, 1, 1, 0.03))
border.color: sb.on ? Theme.alpha(Theme.primary, 0.7) : Qt.rgba(1, 1, 1, 0.12)
Behavior on color { ColorAnimation { duration: Theme.fast } }
}
contentItem: Txt {
text: sb.modelData.t
horizontalAlignment: Text.AlignHCenter
font.weight: sb.on ? Font.DemiBold : Font.Normal
color: sb.on ? Theme.textHi : Theme.textLo
}
}
}
}
contentItem: Item {
Item {
id: head
anchors { left: parent.left; right: parent.right; top: parent.top }
height: 60
Txt {
anchors { left: parent.left; leftMargin: 24; verticalCenter: parent.verticalCenter }
text: "Settings"
font.pixelSize: Theme.fs20
font.weight: Font.DemiBold
}
IconButton {
anchors { right: parent.right; rightMargin: 14; verticalCenter: parent.verticalCenter }
iconName: "close"
onClicked: dlg.close()
}
Rectangle { anchors { left: parent.left; right: parent.right; bottom: parent.bottom } height: 1; color: Theme.hairline }
}
Flickable {
anchors { left: parent.left; right: parent.right; top: head.bottom; bottom: parent.bottom }
contentHeight: content.implicitHeight + 32
clip: true
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar { contentItem: Rectangle { implicitWidth: 4; radius: 2; color: Qt.rgba(1, 1, 1, 0.18); visible: parent.size < 1 }
background: Item {} }
Column {
id: content
x: 24
y: 12
width: parent.width - 48
spacing: 12
SectionTitle { text: "OUTPUT" }
Segmented {
options: [{ v: "beside", t: "Beside" }, { v: "folder", t: "Folder" }]
value: appSettings.outputMode
onPicked: (v) => appSettings.outputMode = v
}
Hint {
width: parent.width
text: appSettings.outputMode === "beside"
? "Stems go next to the original, in “<name> - stems”."
: "Stems go to a folder per track inside the stems folder."
}
Row {
visible: appSettings.outputMode === "folder"
width: parent.width
spacing: 8
Rectangle {
width: parent.width - browseBtn.width - 8
height: 36
radius: 18
color: Qt.rgba(1, 1, 1, 0.05)
border.color: Qt.rgba(1, 1, 1, 0.12)
Txt {
anchors { fill: parent; leftMargin: 16; rightMargin: 16 }
text: appSettings.stemsDir
elide: Text.ElideMiddle
}
}
AeroButton { id: browseBtn; variant: "tonal"; pillHeight: 36; iconName: "folder"; text: "Browse"; onClicked: folderDialog.open() }
}
Row {
spacing: 16
Txt { text: "Format"; width: 80; height: 34 }
Segmented {
options: [{ v: "wav", t: "WAV" }, { v: "flac", t: "FLAC" }, { v: "mp3", t: "MP3" }]
value: appSettings.exportFormat
onPicked: (v) => appSettings.exportFormat = v
}
}
Hint { width: parent.width; text: "WAV and FLAC are 24-bit (WAV switches to 32-bit float if a stem would clip). MP3 is 320 kbps." }
AeroSwitch {
width: parent.width
text: "Export all stems automatically when separation finishes"
checked: appSettings.autoExport
onToggled: appSettings.autoExport = checked
}
SectionTitle { text: "SEPARATION" }
AeroSwitch {
width: parent.width
text: "Half precision on GPU (about 2x faster on the RX 9070 XT, same quality)"
checked: appSettings.fp16
onToggled: appSettings.fp16 = checked
}
Row {
width: parent.width
spacing: 12
Txt { text: "Segment"; width: 80; height: 24 }
AeroSlider {
id: segSlider
width: parent.width - 200
from: 0; to: 7.8; stepSize: 0.1
value: appSettings.segment
onMoved: appSettings.segment = value
}
Txt { text: appSettings.segment <= 0.05 ? "Model default" : appSettings.segment.toFixed(1) + " s"; width: 96; height: 24; color: Theme.textLo }
}
Hint { width: parent.width; text: "Demucs chunk length. Lower it only if you run out of VRAM; 16 GB fits every model at the default." }
Row {
width: parent.width
spacing: 12
Txt { text: "Shifts"; width: 80; height: 24 }
AeroSlider {
width: parent.width - 200
from: 0; to: 5; stepSize: 1; snapMode: Slider.SnapAlways
value: appSettings.shifts
onMoved: appSettings.shifts = Math.round(value)
}
Txt { text: appSettings.shifts === 0 ? "Off" : appSettings.shifts + " passes"; width: 96; height: 24; color: Theme.textLo }
}
Hint { width: parent.width; text: "Demucs random-shift averaging: small quality gain, each pass adds a full run." }
Row {
width: parent.width
spacing: 12
Txt { text: "Overlap"; width: 80; height: 24 }
AeroSlider {
width: parent.width - 200
from: 0.1; to: 0.9; stepSize: 0.05
value: appSettings.overlap
onMoved: appSettings.overlap = value
}
Txt { text: Math.round(appSettings.overlap * 100) + "%"; width: 96; height: 24; color: Theme.textLo }
}
Hint { width: parent.width; text: "Segment overlap. RoFormer models always use at least 75%." }
SectionTitle { text: "STORAGE" }
Row {
width: parent.width
spacing: 12
Txt { text: "Stem cache uses " + app.cacheText; height: 36 }
AeroButton { variant: "tonal"; pillHeight: 36; iconName: "delete"; text: "Clear cache"; onClicked: app.clearCache() }
}
Row {
width: parent.width
spacing: 12
Txt { text: "Keep up to"; width: 80; height: 24 }
AeroSlider {
width: parent.width - 200
from: 1; to: 40; stepSize: 1; snapMode: Slider.SnapAlways
value: appSettings.cacheLimitGb
onMoved: appSettings.cacheLimitGb = Math.round(value)
}
Txt { text: appSettings.cacheLimitGb.toFixed(0) + " GB"; width: 96; height: 24; color: Theme.textLo }
}
Hint { width: parent.width; text: "Cached stems make re-opening a track instant. Oldest entries are pruned past the limit." }
SectionTitle { text: "DEVELOPER" }
AeroSwitch {
width: parent.width
text: "Frame-time overlay (p50 / p99) · F12"
checked: appSettings.frameOverlay
onToggled: appSettings.frameOverlay = checked
}
}
}
}
}