diff --git a/CMakeLists.txt b/CMakeLists.txt
index 962604dddf76..3e9d6268deb7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2605,6 +2605,7 @@ set(
set(QT_EXTRA_COMPONENTS "")
if(QT6)
find_package(QT 6.2 NAMES Qt6 COMPONENTS Core REQUIRED)
+ list(APPEND QT_EXTRA_COMPONENTS "ShaderTools")
list(APPEND QT_EXTRA_COMPONENTS "SvgWidgets")
list(APPEND QT_EXTRA_COMPONENTS "Core5Compat")
list(APPEND QT_EXTRA_COMPONENTS "Quick")
@@ -2630,6 +2631,8 @@ if(QT_EXTRA_COMPONENTS)
endif()
if(QML)
+ add_subdirectory(res/shaders)
+
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)
qt_add_library(mixxx-qml-lib STATIC)
foreach(COMPONENT ${QT_COMPONENTS})
@@ -2689,6 +2692,8 @@ if(QML)
src/qml/asyncimageprovider.cpp
src/qml/qmlapplication.cpp
src/qml/qmlautoreload.cpp
+ src/qml/qmlbeatsmodel.cpp
+ src/qml/qmlcuesmodel.cpp
src/qml/qmlcontrolproxy.cpp
src/qml/qmlconfigproxy.cpp
src/qml/qmldlgpreferencesproxy.cpp
diff --git a/res/qml/Theme/Theme.qml b/res/qml/Theme/Theme.qml
index 23301140a0b5..ca1b4d498e51 100644
--- a/res/qml/Theme/Theme.qml
+++ b/res/qml/Theme/Theme.qml
@@ -36,6 +36,15 @@ QtObject {
property color buttonNormalColor: midGray
property color textColor: lightGray2
property color toolbarActiveColor: white
+ property color waveformPrerollColor: midGray
+ property color waveformPostrollColor: midGray
+ property color waveformBeatColor: lightGray
+ property color waveformCursorColor: white
+ property color waveformMarkerDefault: '#ff7a01'
+ property color waveformMarkerLabel: Qt.rgba(255, 255, 255, 0.8)
+ property color waveformMarkerIntroOutroColor: '#2c5c9a'
+ property color waveformMarkerLoopColor: '#00b400'
+ property color waveformMarkerLoopColorDisabled: '#FFFFFF'
property string fontFamily: "Open Sans"
property int textFontPixelSize: 14
property int buttonFontPixelSize: 10
diff --git a/res/qml/WaveformCue.qml b/res/qml/WaveformCue.qml
new file mode 100644
index 000000000000..74ba5dff2469
--- /dev/null
+++ b/res/qml/WaveformCue.qml
@@ -0,0 +1,71 @@
+import Mixxx 1.0 as Mixxx
+import QtQuick 2.15
+import QtQuick.Shapes 1.4
+import QtQuick.Window 2.15
+
+import QtQuick.Controls 2.15
+
+import "Theme"
+
+Item {
+ id: root
+
+ property color color: Theme.waveformMarkerDefault
+
+ property real markerHeight: root.height
+ property color labelColor: Theme.waveformMarkerLabel
+ property real radiusSize: 4
+ property string cueLabel: qsTr("CUE")
+
+ FontMetrics {
+ id: fontMetrics
+ font.family: Theme.fontFamily
+ }
+
+ property rect contentRect: fontMetrics.tightBoundingRect(cueLabel)
+
+ Shape {
+ ShapePath {
+ strokeWidth: 0
+ strokeColor: 'transparent'
+ fillColor: color
+ strokeStyle: ShapePath.SolidLine
+ startX: -1; startY: 0
+
+ PathLine { x: 8; y: 0 }
+ PathLine { x: 8 - radiusSize + contentRect.width; y: 0 }
+ PathArc {
+ x: 8 + contentRect.width
+ y: radiusSize
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Clockwise
+ }
+ PathLine { x: 8 + contentRect.width; y: 16 - radiusSize }
+ PathArc {
+ x: 8 - radiusSize + contentRect.width
+ y: 16
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Clockwise
+ }
+ PathLine { x: 8; y: 16 }
+ PathLine { x: 2; y: 16 }
+ PathLine { x: 2; y: markerHeight }
+ PathLine { x: -1; y: markerHeight }
+ PathLine { x: -1; y: 0 }
+ }
+ }
+ Shape {
+ ShapePath {
+ fillColor: labelColor
+ strokeColor: labelColor
+ PathText {
+ x: 3
+ y: 3
+ font.family: Theme.fontFamily
+ font.pixelSize: 13
+ font.weight: Font.Bold
+ text: cueLabel
+ }
+ }
+ }
+}
diff --git a/res/qml/WaveformHotcue.qml b/res/qml/WaveformHotcue.qml
new file mode 100644
index 000000000000..0c010e42cb03
--- /dev/null
+++ b/res/qml/WaveformHotcue.qml
@@ -0,0 +1,100 @@
+import "." as Skin
+import Mixxx 1.0 as Mixxx
+import QtQuick 2.15
+import QtQuick.Shapes 1.4
+import QtQuick.Window 2.15
+
+import QtQuick.Controls 2.15
+
+import "Theme"
+
+Item {
+ id: root
+
+ required property int hotcueNumber
+ required property string group
+ required property string label
+ property bool isLoop: false
+
+ Skin.Hotcue {
+ id: hotcue
+
+ group: root.group
+ hotcueNumber: root.hotcueNumber
+ }
+
+ property real markerHeight: root.height
+ property color labelColor: Theme.waveformMarkerLabel
+ property real radiusSize: 4
+ property string hotcueLabel: label != "" ? `${hotcueNumber}: ${label}` : `${hotcueNumber}`
+
+ FontMetrics {
+ id: fontMetrics
+ font.family: Theme.fontFamily
+ }
+
+ property rect contentRect: fontMetrics.tightBoundingRect(hotcueLabel)
+
+ Rectangle {
+ visible: root.isLoop
+ anchors.fill: parent
+ color: Qt.alpha(hotcue.color, 0.3)
+ }
+
+ Shape {
+ ShapePath {
+ strokeWidth: 0
+ strokeColor: 'transparent'
+ fillColor: hotcue.color
+ strokeStyle: ShapePath.SolidLine
+ startX: -1; startY: 0
+
+ PathLine { x: 1; y: 0 }
+ PathLine { x: 1; y: markerHeight - 16 }
+ PathLine { x: 8 - radiusSize + contentRect.width; y: markerHeight - 16 }
+ PathArc {
+ x: 8 + contentRect.width
+ y: markerHeight - 16 + radiusSize
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Clockwise
+ }
+ PathLine { x: 8 + contentRect.width; y: markerHeight - radiusSize }
+ PathArc {
+ x: 8 - radiusSize + contentRect.width
+ y: markerHeight
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Clockwise
+ }
+ PathLine { x: -1; y: markerHeight }
+ PathLine { x: -1; y: 0 }
+ }
+ }
+ Shape {
+ visible: root.isLoop
+ ShapePath {
+ strokeWidth: 0
+ strokeColor: 'transparent'
+ fillColor: hotcue.color
+ strokeStyle: ShapePath.SolidLine
+ startX: root.width - 1; startY: 0
+
+ PathLine { x: root.width - 1; y: markerHeight }
+ PathLine { x: root.width + 1; y: markerHeight }
+ PathLine { x: root.width + 1; y: 0 }
+ }
+ }
+ Shape {
+ ShapePath {
+ fillColor: labelColor
+ strokeColor: labelColor
+ PathText {
+ x: 3
+ y: markerHeight - 13
+ font.family: Theme.fontFamily
+ font.pixelSize: 13
+ font.weight: Font.Medium
+ text: hotcueLabel
+ }
+ }
+ }
+}
diff --git a/res/qml/WaveformIntroOutro.qml b/res/qml/WaveformIntroOutro.qml
new file mode 100644
index 000000000000..ba6779cd6925
--- /dev/null
+++ b/res/qml/WaveformIntroOutro.qml
@@ -0,0 +1,95 @@
+import Mixxx 1.0 as Mixxx
+import QtQuick 2.15
+import QtQuick.Shapes 1.4
+import QtQuick.Window 2.15
+
+import QtQuick.Controls 2.15
+
+import "Theme"
+
+Item {
+ id: root
+
+ property color mainColor: Theme.waveformMarkerIntroOutroColor
+ property bool isIntro: true
+
+ property real markerHeight: root.height
+ property real radiusSize: 4
+
+ Rectangle {
+ anchors.fill: parent
+ color: Qt.alpha(root.mainColor, 0.1)
+ }
+
+ Shape {
+ ShapePath {
+ strokeWidth: 0
+ strokeColor: 'transparent'
+ fillColor: root.mainColor
+ strokeStyle: ShapePath.SolidLine
+ startX: -1; startY: 0
+
+ PathLine { x: 16 - radiusSize; y: 0 }
+ PathArc {
+ x: 16
+ y: radiusSize
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Clockwise
+ }
+ PathLine { x: 16; y: 16 - radiusSize }
+ PathArc {
+ x: 16 - radiusSize
+ y: 16
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Clockwise
+ }
+ PathLine { x: 1; y: 16 }
+ PathLine { x: 1; y: markerHeight }
+ PathLine { x: -1; y: markerHeight }
+ }
+ }
+ Shape {
+ visible: root.width != 0
+ ShapePath {
+ strokeWidth: 0
+ strokeColor: 'transparent'
+ fillColor: root.mainColor
+ strokeStyle: ShapePath.SolidLine
+ startX: root.width - 1; startY: 0
+
+ PathLine { x: root.width - 16 + radiusSize; y: 0 }
+ PathArc {
+ x: root.width - 16
+ y: radiusSize
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Counterclockwise
+ }
+ PathLine { x: root.width - 16; y: 16 - radiusSize }
+ PathArc {
+ x: root.width - 16 + radiusSize
+ y: 16
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Counterclockwise
+ }
+ PathLine { x: root.width -1; y: 16 }
+ PathLine { x: root.width -1; y: markerHeight }
+ PathLine { x: root.width + 1; y: markerHeight }
+ PathLine { x: root.width + 1; y: 0 }
+ }
+ }
+ Image {
+ x: 2
+ y: 2
+ width: 12
+ height: 12
+ source: `images/mark_${(root.isIntro ? 'intro' : 'outro')}.svg`
+ }
+ Image {
+ visible: root.width != 0
+ x: root.width - 14
+ y: 2
+ width: 12
+ height: 12
+ source: `images/mark_${(root.isIntro ? 'intro' : 'outro')}.svg`
+ }
+}
diff --git a/res/qml/WaveformLoop.qml b/res/qml/WaveformLoop.qml
new file mode 100644
index 000000000000..d05e1bbe26c7
--- /dev/null
+++ b/res/qml/WaveformLoop.qml
@@ -0,0 +1,76 @@
+import Mixxx 1.0 as Mixxx
+import QtQuick 2.15
+import QtQuick.Shapes 1.4
+import QtQuick.Window 2.15
+
+import QtQuick.Controls 2.15
+
+import "Theme"
+
+Item {
+ id: root
+
+ property color mainColor: Theme.waveformMarkerLoopColor
+ property color disabledColor: Theme.waveformMarkerLoopColorDisabled
+ property real enabledOpacity: 0.8
+ property real disabledOpacity: 0.5
+
+ property real markerHeight: root.height
+ property real radiusSize: 4
+ property bool enabled: true
+
+ Rectangle {
+ anchors.fill: parent
+ color: Qt.alpha(root.enabled ? root.mainColor : root.disabledColor, root.enabled ? root.enabledOpacity : root.disabledOpacity)
+ }
+
+ Shape {
+ ShapePath {
+ strokeWidth: 0
+ strokeColor: 'transparent'
+ fillColor: root.mainColor
+ strokeStyle: ShapePath.SolidLine
+ startX: -1; startY: 0
+
+ PathLine { x: -16 + radiusSize; y: 0 }
+ PathArc {
+ x: -16
+ y: radiusSize
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Counterclockwise
+ }
+ PathLine { x: -16; y: 16 - radiusSize }
+ PathArc {
+ x: -16 + radiusSize
+ y: 16
+ radiusX: radiusSize; radiusY: radiusSize
+ direction: PathArc.Counterclockwise
+ }
+ PathLine { x: -1; y: 16 }
+ PathLine { x: -1; y: markerHeight }
+ PathLine { x: 1; y: markerHeight }
+ PathLine { x: 1; y: 0 }
+ }
+ }
+ Shape {
+
+ ShapePath {
+ strokeWidth: 0
+ strokeColor: 'transparent'
+ fillColor: root.mainColor
+ strokeStyle: ShapePath.SolidLine
+ startX: root.width - 1; startY: 0
+
+ PathLine { x: root.width - 1; y: markerHeight }
+ PathLine { x: root.width + 1; y: markerHeight }
+ PathLine { x: root.width + 1; y: 0 }
+ }
+ }
+ Image {
+ x: -14
+ y: 2
+ width: 12
+ height: 12
+ source: "images/mark_loop.svg"
+ }
+}
diff --git a/res/qml/WaveformRow.qml b/res/qml/WaveformRow.qml
new file mode 100644
index 000000000000..be129fc42bfa
--- /dev/null
+++ b/res/qml/WaveformRow.qml
@@ -0,0 +1,380 @@
+import "." as Skin
+import Mixxx 1.0 as Mixxx
+import QtQuick 2.14
+import QtQuick.Shapes 1.12
+import "Theme"
+
+Item {
+ id: root
+
+ enum MouseStatus {
+ Normal,
+ Bending,
+ Scratching
+ }
+
+ property string group // required
+ property var deckPlayer: Mixxx.PlayerManager.getPlayer(group)
+
+ Item {
+ id: waveformContainer
+
+ property real duration: samplesControl.value / sampleRateControl.value
+
+ anchors.fill: parent
+ clip: true
+
+ Mixxx.ControlProxy {
+ id: samplesControl
+
+ group: root.group
+ key: "track_samples"
+ }
+
+ Mixxx.ControlProxy {
+ id: sampleRateControl
+
+ group: root.group
+ key: "track_samplerate"
+ }
+
+ Mixxx.ControlProxy {
+ id: playPositionControl
+
+ group: root.group
+ key: "playposition"
+ }
+
+ Mixxx.ControlProxy {
+ id: rateRatioControl
+
+ group: root.group
+ key: "rate_ratio"
+ }
+
+ Mixxx.ControlProxy {
+ id: zoomControl
+
+ group: root.group
+ key: "waveform_zoom"
+ }
+
+ Mixxx.ControlProxy {
+ id: introStartPosition
+
+ group: root.group
+ key: "intro_start_position"
+ }
+
+ Mixxx.ControlProxy {
+ id: introEndPosition
+
+ group: root.group
+ key: "intro_end_position"
+ }
+
+ Mixxx.ControlProxy {
+ id: outroStartPosition
+
+ group: root.group
+ key: "outro_start_position"
+ }
+
+ Mixxx.ControlProxy {
+ id: outroEndPosition
+
+ group: root.group
+ key: "outro_end_position"
+ }
+
+ Mixxx.ControlProxy {
+ id: loopStartPosition
+
+ group: root.group
+ key: "loop_start_position"
+ }
+
+ Mixxx.ControlProxy {
+ id: loopEndPosition
+
+ group: root.group
+ key: "loop_end_position"
+ }
+
+ Mixxx.ControlProxy {
+ id: loopEnabled
+
+ group: root.group
+ key: "loop_enabled"
+ }
+
+ Mixxx.ControlProxy {
+ id: mainCuePosition
+
+ group: root.group
+ key: "cue_point"
+ }
+
+ Item {
+ id: waveform
+
+ property real effectiveZoomFactor: (1 / rateRatioControl.value) * (100 / zoomControl.value)
+
+ width: waveformContainer.duration * effectiveZoomFactor
+ height: parent.height
+ x: playMarker.screenPosition * waveformContainer.width - playPositionControl.value * width
+ visible: root.deckPlayer.isLoaded
+
+ WaveformShader {
+ group: root.group
+ anchors.fill: parent
+ }
+
+ Shape {
+ id: preroll
+
+ property real triangleHeight: waveform.height
+ property real triangleWidth: 0.25 * waveform.effectiveZoomFactor
+ property int numTriangles: Math.ceil(width / triangleWidth)
+
+ anchors.top: waveform.top
+ anchors.right: waveform.left
+ width: Math.max(0, waveform.x)
+ height: waveform.height
+
+ ShapePath {
+ strokeColor: Theme.waveformPrerollColor
+ strokeWidth: 1
+ fillColor: "transparent"
+
+ PathMultiline {
+ paths: {
+ let p = [];
+ for (let i = 0; i < preroll.numTriangles; i++) {
+ p.push([
+ Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2),
+ Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, 0),
+ Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, preroll.triangleHeight),
+ Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2),
+ ]);
+ }
+ return p;
+ }
+ }
+ }
+ }
+
+ Shape {
+ id: postroll
+
+ property real triangleHeight: waveform.height
+ property real triangleWidth: 0.25 * waveform.effectiveZoomFactor
+ property int numTriangles: Math.ceil(width / triangleWidth)
+
+ anchors.top: waveform.top
+ anchors.left: waveform.right
+ width: waveformContainer.width / 2
+ height: waveform.height
+
+ ShapePath {
+ strokeColor: Theme.waveformPostrollColor
+ strokeWidth: 1
+ fillColor: "transparent"
+
+ PathMultiline {
+ paths: {
+ let p = [];
+ for (let i = 0; i < postroll.numTriangles; i++) {
+ p.push([
+ Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2),
+ Qt.point((i + 1) * postroll.triangleWidth, 0),
+ Qt.point((i + 1) * postroll.triangleWidth, postroll.triangleHeight),
+ Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2),
+ ]);
+ }
+ return p;
+ }
+ }
+ }
+ }
+
+ Repeater {
+ model: root.deckPlayer.beatsModel
+
+ Rectangle {
+ property real alpha: 0.9 // TODO: Make this configurable (i.e., "[Waveform],beatGridAlpha" config option)
+
+ width: 1
+ height: waveform.height
+ x: (framePosition * 2 / samplesControl.value) * waveform.width
+ color: Theme.waveformBeatColor
+ }
+ }
+
+ Skin.WaveformIntroOutro {
+ id: intro
+
+ visible: introStartPosition.value != -1 || introEndPosition.value != -1
+
+ height: waveform.height
+ x: ((introStartPosition.value != -1 ? introStartPosition.value : introEndPosition.value) / samplesControl.value) * waveform.width
+ width: introEndPosition.value == -1 ? 0 : ((introEndPosition.value - introStartPosition.value) / samplesControl.value) * waveform.width
+ }
+
+ Skin.WaveformIntroOutro {
+ id: outro
+
+ visible: outroStartPosition.value != -1 || outroEndPosition.value != -1
+ isIntro: false
+
+ height: waveform.height
+ x: ((outroStartPosition.value != -1 ? outroStartPosition.value : outroEndPosition.value) / samplesControl.value) * waveform.width
+ width: outroEndPosition.value == -1 || outroStartPosition.value == -1 ? 0 : ((outroEndPosition.value - outroStartPosition.value) / samplesControl.value) * waveform.width
+ }
+
+ Skin.WaveformLoop {
+ id: loop
+
+ visible: loopStartPosition.value != -1 && loopEndPosition.value != -1
+
+ height: waveform.height
+ x: (loopStartPosition.value / samplesControl.value) * waveform.width
+ width: ((loopEndPosition.value - loopStartPosition.value) / samplesControl.value) * waveform.width
+ enabled: loopEnabled.value
+ }
+
+ Repeater {
+ model: root.deckPlayer.hotcuesModel
+
+ Item {
+ id: cue
+
+ required property int startPosition
+ required property int endPosition
+ required property string label
+ required property bool isLoop
+ required property int hotcueNumber
+
+ Skin.WaveformHotcue {
+ group: root.group
+ hotcueNumber: cue.hotcueNumber + 1
+ label: cue.label
+ isLoop: cue.isLoop
+
+ x: (startPosition * 2 / samplesControl.value) * waveform.width
+ width: cue.isLoop ? ((endPosition - startPosition) * 2 / samplesControl.value) * waveform.width : null
+ height: waveform.height
+ }
+ }
+ }
+
+ Skin.WaveformCue {
+ id: maincue
+
+ height: waveform.height
+ x: (mainCuePosition.value / samplesControl.value) * waveform.width
+ }
+ }
+ }
+
+ Shape {
+ id: playMarkerShape
+
+ anchors.fill: parent
+
+ ShapePath {
+ id: playMarker
+
+ property real screenPosition: 0.5
+
+ startX: playMarkerShape.width * playMarker.screenPosition
+ startY: 0
+ strokeColor: Theme.waveformCursorColor
+ strokeWidth: 1
+
+ PathLine {
+ id: marker
+
+ x: playMarkerShape.width * playMarker.screenPosition
+ y: playMarkerShape.height
+ }
+ }
+ }
+
+ Mixxx.ControlProxy {
+ id: scratchPositionEnableControl
+
+ group: root.group
+ key: "scratch_position_enable"
+ }
+
+ Mixxx.ControlProxy {
+ id: scratchPositionControl
+
+ group: root.group
+ key: "scratch_position"
+ }
+
+ Mixxx.ControlProxy {
+ id: wheelControl
+
+ group: root.group
+ key: "wheel"
+ }
+
+ MouseArea {
+ property int mouseStatus: WaveformRow.MouseStatus.Normal
+ property point mouseAnchor: Qt.point(0, 0)
+
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onPressed: {
+ mouseAnchor = Qt.point(mouse.x, mouse.y);
+ if (mouse.button == Qt.LeftButton) {
+ if (mouseStatus == WaveformRow.MouseStatus.Bending)
+ wheelControl.parameter = 0.5;
+
+ mouseStatus = WaveformRow.MouseStatus.Scratching;
+ scratchPositionEnableControl.value = 1;
+ // TODO: Calculate position properly
+ scratchPositionControl.value = -mouse.x * waveform.effectiveZoomFactor * 2;
+ console.log(mouse.x);
+ } else {
+ if (mouseStatus == WaveformRow.MouseStatus.Scratching)
+ scratchPositionEnableControl.value = 0;
+
+ wheelControl.parameter = 0.5;
+ mouseStatus = WaveformRow.MouseStatus.Bending;
+ }
+ }
+ onPositionChanged: {
+ switch (mouseStatus) {
+ case WaveformRow.MouseStatus.Bending: {
+ const diff = mouse.x - mouseAnchor.x;
+ // Start at the middle of [0.0, 1.0], and emit values based on how far
+ // the mouse has traveled horizontally. Note, for legacy (MIDI) reasons,
+ // this is tuned to 127.
+ const v = 0.5 + (diff / 1270);
+ // clamp to [0.0, 1.0]
+ wheelControl.parameter = Mixxx.MathUtils.clamp(v, 0, 1);
+ break;
+ };
+ case WaveformRow.MouseStatus.Scratching:
+ // TODO: Calculate position properly
+ scratchPositionControl.value = -mouse.x * waveform.effectiveZoomFactor * 2;
+ break;
+ }
+ }
+ onReleased: {
+ switch (mouseStatus) {
+ case WaveformRow.MouseStatus.Bending:
+ wheelControl.parameter = 0.5;
+ break;
+ case WaveformRow.MouseStatus.Scratching:
+ scratchPositionEnableControl.value = 0;
+ break;
+ }
+ mouseStatus = WaveformRow.MouseStatus.Normal;
+ }
+ }
+}
diff --git a/res/qml/WaveformShader.qml b/res/qml/WaveformShader.qml
new file mode 100644
index 000000000000..33de4acde0cf
--- /dev/null
+++ b/res/qml/WaveformShader.qml
@@ -0,0 +1,88 @@
+import Mixxx 1.0 as Mixxx
+import QtQuick 2.12
+
+ShaderEffect {
+ id: root
+
+ property string group // required
+ property var deckPlayer: Mixxx.PlayerManager.getPlayer(group)
+ property size framebufferSize: Qt.size(width, height)
+ property int waveformLength: root.deckPlayer.waveformLength
+ property int textureSize: root.deckPlayer.waveformTextureSize
+ property int textureStride: root.deckPlayer.waveformTextureStride
+ property real firstVisualIndex: 1
+ property real lastVisualIndex: root.deckPlayer.waveformLength / 2
+ property color axesColor: "#FFFFFF"
+ property color highColor: "#0000FF"
+ property color midColor: "#00FF00"
+ property color lowColor: "#FF0000"
+ property real highGain: filterWaveformEnableControl.value ? (filterHighKillControl.value ? 0 : filterHighControl.value) : 1
+ property real midGain: filterWaveformEnableControl.value ? (filterMidKillControl.value ? 0 : filterMidControl.value) : 1
+ property real lowGain: filterWaveformEnableControl.value ? (filterLowKillControl.value ? 0 : filterLowControl.value) : 1
+ property real allGain: pregainControl.value
+ property Image waveformTexture
+
+ fragmentShader: "qrc:/shaders/rgbsignal_qml.frag.qsb"
+
+ Mixxx.ControlProxy {
+ id: pregainControl
+
+ group: root.group
+ key: "pregain"
+ }
+
+ Mixxx.ControlProxy {
+ id: filterWaveformEnableControl
+
+ group: root.group
+ key: "filterWaveformEnable"
+ }
+
+ Mixxx.ControlProxy {
+ id: filterHighControl
+
+ group: "[EqualizerRack1_" + root.group + "_Effect1]"
+ key: "parameter3"
+ }
+
+ Mixxx.ControlProxy {
+ id: filterHighKillControl
+
+ group: "[EqualizerRack1_" + root.group + "_Effect1]"
+ key: "button_parameter3"
+ }
+
+ Mixxx.ControlProxy {
+ id: filterMidControl
+
+ group: "[EqualizerRack1_" + root.group + "_Effect1]"
+ key: "parameter2"
+ }
+
+ Mixxx.ControlProxy {
+ id: filterMidKillControl
+
+ group: "[EqualizerRack1_" + root.group + "_Effect1]"
+ key: "button_parameter2"
+ }
+
+ Mixxx.ControlProxy {
+ id: filterLowControl
+
+ group: "[EqualizerRack1_" + root.group + "_Effect1]"
+ key: "parameter1"
+ }
+
+ Mixxx.ControlProxy {
+ id: filterLowKillControl
+
+ group: "[EqualizerRack1_" + root.group + "_Effect1]"
+ key: "button_parameter1"
+ }
+
+ waveformTexture: Image {
+ visible: false
+ layer.enabled: false
+ source: root.deckPlayer.waveformTexture
+ }
+}
diff --git a/res/qml/images/mark_intro.svg b/res/qml/images/mark_intro.svg
new file mode 100644
index 000000000000..96dd7bcd7025
--- /dev/null
+++ b/res/qml/images/mark_intro.svg
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/res/qml/images/mark_loop.svg b/res/qml/images/mark_loop.svg
new file mode 100644
index 000000000000..3bcb6feff1c4
--- /dev/null
+++ b/res/qml/images/mark_loop.svg
@@ -0,0 +1,32 @@
+
+
+
+
diff --git a/res/qml/images/mark_outro.svg b/res/qml/images/mark_outro.svg
new file mode 100644
index 000000000000..ad9c98f174ba
--- /dev/null
+++ b/res/qml/images/mark_outro.svg
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/res/qml/main.qml b/res/qml/main.qml
index 73804bfd66bd..15578d303cb2 100644
--- a/res/qml/main.qml
+++ b/res/qml/main.qml
@@ -98,6 +98,58 @@ ApplicationWindow {
}
}
+ WaveformRow {
+ id: deck3waveform
+
+ group: "[Channel3]"
+ width: root.width
+ height: 60
+ visible: root.show4decks && !root.maximizeLibrary
+
+ FadeBehavior on visible {
+ fadeTarget: deck3waveform
+ }
+ }
+
+ WaveformRow {
+ id: deck1waveform
+
+ group: "[Channel1]"
+ width: root.width
+ height: 60
+ visible: !root.maximizeLibrary
+
+ FadeBehavior on visible {
+ fadeTarget: deck1waveform
+ }
+ }
+
+ WaveformRow {
+ id: deck2waveform
+
+ group: "[Channel2]"
+ width: root.width
+ height: 60
+ visible: !root.maximizeLibrary
+
+ FadeBehavior on visible {
+ fadeTarget: deck2waveform
+ }
+ }
+
+ WaveformRow {
+ id: deck4waveform
+
+ group: "[Channel4]"
+ width: root.width
+ height: 60
+ visible: root.show4decks && !root.maximizeLibrary
+
+ FadeBehavior on visible {
+ fadeTarget: deck4waveform
+ }
+ }
+
Skin.DeckRow {
id: decks12
diff --git a/res/shaders/CMakeLists.txt b/res/shaders/CMakeLists.txt
new file mode 100644
index 000000000000..c54470accf9a
--- /dev/null
+++ b/res/shaders/CMakeLists.txt
@@ -0,0 +1,5 @@
+qt_add_shaders(mixxx-lib "waveform_shaders"
+ PREFIX "/shaders"
+ FILES
+ "rgbsignal_qml.frag"
+)
diff --git a/res/shaders/rgbsignal_qml.frag b/res/shaders/rgbsignal_qml.frag
new file mode 100644
index 000000000000..602f858ba71a
--- /dev/null
+++ b/res/shaders/rgbsignal_qml.frag
@@ -0,0 +1,144 @@
+#version 450
+
+layout(location = 0) in vec2 qt_TexCoord0;
+layout(location = 0) out vec4 fragColor;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 qt_Matrix;
+ float qt_Opacity;
+
+ vec2 framebufferSize;
+ vec4 axesColor;
+ vec4 lowColor;
+ vec4 midColor;
+ vec4 highColor;
+
+ int waveformLength;
+ int textureSize;
+ int textureStride;
+
+ float allGain;
+ float lowGain;
+ float midGain;
+ float highGain;
+ float firstVisualIndex;
+ float lastVisualIndex;
+};
+
+layout(binding = 1) uniform sampler2D waveformTexture;
+
+vec4 getWaveformData(float index) {
+ vec2 uv_data;
+ uv_data.y = floor(index / float(textureStride));
+ uv_data.x = floor(index - uv_data.y * float(textureStride));
+ // Divide again to convert to normalized UV coordinates.
+ return texture(waveformTexture, uv_data / float(textureStride));
+}
+
+void main(void) {
+ vec2 uv = qt_TexCoord0.st;
+ vec4 pixel = gl_FragCoord;
+ fragColor = vec4(1, 0, 0, 1);
+
+ float new_currentIndex =
+ floor(firstVisualIndex +
+ uv.x * (lastVisualIndex - firstVisualIndex)) *
+ 2;
+
+ // Texture coordinates put (0,0) at the bottom left, so show the right
+ // channel if we are in the bottom half.
+ if (uv.y < 0.5) {
+ new_currentIndex += 1;
+ }
+
+ vec4 outputColor = vec4(0.0, 0.0, 0.0, 0.0);
+ bool showing = false;
+ bool showingUnscaled = false;
+ vec4 showingColor = vec4(0.0, 0.0, 0.0, 0.0);
+ vec4 showingUnscaledColor = vec4(0.0, 0.0, 0.0, 0.0);
+
+ // We don't exit early if the waveform data is not valid because we may want
+ // to show other things (e.g. the axes lines) even when we are on a pixel
+ // that does not have valid waveform data.
+ if (new_currentIndex >= 0 && new_currentIndex <= waveformLength - 1) {
+ vec4 new_currentDataUnscaled = getWaveformData(new_currentIndex) * allGain;
+
+ vec4 new_currentData = new_currentDataUnscaled;
+ new_currentData.x *= lowGain;
+ new_currentData.y *= midGain;
+ new_currentData.z *= highGain;
+
+ //(vrince) debug see pre-computed signal
+ // gl_FragColor = new_currentData;
+ // return;
+
+ // Represents the [-1, 1] distance of this pixel. Subtracting this from
+ // the signal data in new_currentData, we can tell if a signal band should
+ // show in this pixel if the component is > 0.
+ float ourDistance = abs((uv.y - 0.5) * 2.0);
+
+ // Since the magnitude of the (low, mid, high) vector is used as the
+ // waveform height, re-scale the maximum height to 1.
+ const float scaleFactor = 1.0 / sqrt(3.0);
+
+ float signalDistance = sqrt(new_currentData.x * new_currentData.x +
+ new_currentData.y * new_currentData.y +
+ new_currentData.z * new_currentData.z) *
+ scaleFactor;
+ showing = (signalDistance - ourDistance) >= 0.0;
+
+ // Linearly combine the low, mid, and high colors according to the low,
+ // mid, and high components.
+ showingColor = lowColor * new_currentData.x +
+ midColor * new_currentData.y +
+ highColor * new_currentData.z;
+
+ // Re-scale the color by the maximum component.
+ float showingMax = max(showingColor.x, max(showingColor.y, showingColor.z));
+ showingColor = showingColor / showingMax;
+ showingColor.w = 1.0;
+
+ // Now do it all over again for the unscaled version of the waveform,
+ // which we will draw at very low opacity.
+ float signalDistanceUnscaled =
+ sqrt(new_currentDataUnscaled.x * new_currentDataUnscaled.x +
+ new_currentDataUnscaled.y * new_currentDataUnscaled.y +
+ new_currentDataUnscaled.z * new_currentDataUnscaled.z) *
+ scaleFactor;
+ showingUnscaled = (signalDistanceUnscaled - ourDistance) >= 0.0;
+
+ // Linearly combine the low, mid, and high colors according to the
+ // original low, mid, and high components.
+ showingUnscaledColor = lowColor * new_currentDataUnscaled.x +
+ midColor * new_currentDataUnscaled.y +
+ highColor * new_currentDataUnscaled.z;
+
+ // Re-scale the color by the maximum component.
+ float showingUnscaledMax = max(showingUnscaledColor.x,
+ max(showingUnscaledColor.y, showingUnscaledColor.z));
+ showingUnscaledColor = showingUnscaledColor / showingUnscaledMax;
+ showingUnscaledColor.w = 1.0;
+ }
+
+ // Draw the axes color as the lowest item on the screen.
+ // TODO(owilliams): The 4 in this line makes sure the axis gets
+ // rendered even when the waveform is fairly short. Really this
+ // value should be based on the size of the widget.
+ if (abs(framebufferSize.y / 2 - pixel.y) <= 4) {
+ outputColor.xyz = mix(outputColor.xyz, axesColor.xyz, axesColor.w);
+ outputColor.w = 1.0;
+ }
+
+ if (showingUnscaled) {
+ float alpha = 0.4;
+ outputColor.xyz = mix(outputColor.xyz, showingUnscaledColor.xyz, alpha);
+ outputColor.w = 1.0;
+ }
+
+ if (showing) {
+ float alpha = 0.8;
+ outputColor.xyz = mix(outputColor.xyz, showingColor.xyz, alpha);
+ outputColor.w = 1.0;
+ }
+ fragColor = outputColor;
+}
diff --git a/src/qml/qmlbeatsmodel.cpp b/src/qml/qmlbeatsmodel.cpp
new file mode 100644
index 000000000000..09b84f907123
--- /dev/null
+++ b/src/qml/qmlbeatsmodel.cpp
@@ -0,0 +1,73 @@
+#include "qml/qmlbeatsmodel.h"
+
+#include
+
+namespace mixxx {
+namespace qml {
+namespace {
+const QHash kRoleNames = {
+ {QmlBeatsModel::FramePositionRole, "framePosition"},
+};
+}
+
+QmlBeatsModel::QmlBeatsModel(
+ QObject* parent)
+ : QAbstractListModel(parent), m_pBeats(nullptr), m_numBeats(0) {
+}
+
+void QmlBeatsModel::setBeats(const BeatsPointer pBeats, audio::FramePos trackEndPosition) {
+ beginResetModel();
+ m_numBeats = 0;
+ m_pBeats = pBeats;
+ if (pBeats != nullptr) {
+ m_numBeats = pBeats->numBeatsInRange(audio::kStartFramePos, trackEndPosition);
+ }
+ endResetModel();
+}
+
+QVariant QmlBeatsModel::data(const QModelIndex& index, int role) const {
+ if (index.row() < 0 || index.row() >= m_numBeats) {
+ return QVariant();
+ }
+
+ const BeatsPointer pBeats = m_pBeats;
+ if (pBeats == nullptr) {
+ return QVariant();
+ }
+
+ auto it = pBeats->iteratorFrom(audio::kStartFramePos) + index.row();
+ VERIFY_OR_DEBUG_ASSERT(it != pBeats->cend()) {
+ return QVariant();
+ }
+
+ switch (role) {
+ case QmlBeatsModel::FramePositionRole:
+ return (*it).value();
+ default:
+ return QVariant();
+ }
+}
+
+int QmlBeatsModel::rowCount(const QModelIndex& parent) const {
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return m_numBeats;
+}
+
+QHash QmlBeatsModel::roleNames() const {
+ return kRoleNames;
+}
+
+QVariant QmlBeatsModel::get(int row) const {
+ QModelIndex idx = index(row, 0);
+ QVariantMap dataMap;
+ for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) {
+ dataMap.insert(it.value(), data(idx, it.key()));
+ }
+ return dataMap;
+}
+
+} // namespace qml
+} // namespace mixxx
diff --git a/src/qml/qmlbeatsmodel.h b/src/qml/qmlbeatsmodel.h
new file mode 100644
index 000000000000..0ce8590db55e
--- /dev/null
+++ b/src/qml/qmlbeatsmodel.h
@@ -0,0 +1,33 @@
+#pragma once
+#include
+#include
+
+#include "track/beats.h"
+
+namespace mixxx {
+namespace qml {
+
+class QmlBeatsModel : public QAbstractListModel {
+ Q_OBJECT
+ public:
+ enum Roles {
+ FramePositionRole = Qt::UserRole + 1,
+ };
+ Q_ENUM(Roles)
+
+ explicit QmlBeatsModel(QObject* parent = nullptr);
+
+ void setBeats(const BeatsPointer pBeats, audio::FramePos trackEndPosition);
+
+ QVariant data(const QModelIndex& index, int role) const override;
+ int rowCount(const QModelIndex& parent) const override;
+ QHash roleNames() const override;
+ Q_INVOKABLE QVariant get(int row) const;
+
+ private:
+ BeatsPointer m_pBeats;
+ int m_numBeats;
+};
+
+} // namespace qml
+} // namespace mixxx
diff --git a/src/qml/qmlcuesmodel.cpp b/src/qml/qmlcuesmodel.cpp
new file mode 100644
index 000000000000..353a176132eb
--- /dev/null
+++ b/src/qml/qmlcuesmodel.cpp
@@ -0,0 +1,83 @@
+#include "qml/qmlcuesmodel.h"
+
+#include
+
+#include "moc_qmlcuesmodel.cpp"
+#include "track/cue.h"
+
+namespace mixxx {
+namespace qml {
+namespace {
+const QHash kRoleNames = {
+ {QmlCuesModel::StartPositionRole, "startPosition"},
+ {QmlCuesModel::EndPositionRole, "endPosition"},
+ {QmlCuesModel::LabelRole, "label"},
+ {QmlCuesModel::IsLoopRole, "isLoop"},
+ {QmlCuesModel::HotcueNumberRole, "hotcueNumber"},
+};
+}
+
+QmlCuesModel::QmlCuesModel(
+ QObject* parent)
+ : QAbstractListModel(parent), m_pCues() {
+}
+
+void QmlCuesModel::setCues(const QList pCues) {
+ beginResetModel();
+ m_pCues = QList(pCues);
+ endResetModel();
+}
+
+QVariant QmlCuesModel::data(const QModelIndex& index, int role) const {
+ if (index.row() < 0 || index.row() >= m_pCues.size()) {
+ return QVariant();
+ }
+
+ const CuePointer& pCue = m_pCues.at(index.row());
+ VERIFY_OR_DEBUG_ASSERT(pCue.get()) {
+ return QVariant();
+ }
+
+ switch (role) {
+ case QmlCuesModel::StartPositionRole: {
+ const auto position = pCue->getPosition();
+ return position.isValid() ? position.value() : QVariant();
+ }
+ case QmlCuesModel::EndPositionRole: {
+ const auto position = pCue->getEndPosition();
+ return position.isValid() ? position.value() : QVariant();
+ }
+ case QmlCuesModel::LabelRole:
+ return pCue->getLabel();
+ case QmlCuesModel::IsLoopRole:
+ return pCue->getType() == CueType::Loop;
+ case QmlCuesModel::HotcueNumberRole:
+ return pCue->getHotCue();
+ default:
+ return QVariant();
+ }
+}
+
+int QmlCuesModel::rowCount(const QModelIndex& parent) const {
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return m_pCues.size();
+}
+
+QHash QmlCuesModel::roleNames() const {
+ return kRoleNames;
+}
+
+QVariant QmlCuesModel::get(int row) const {
+ QModelIndex idx = index(row, 0);
+ QVariantMap dataMap;
+ for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) {
+ dataMap.insert(it.value(), data(idx, it.key()));
+ }
+ return dataMap;
+}
+
+} // namespace qml
+} // namespace mixxx
diff --git a/src/qml/qmlcuesmodel.h b/src/qml/qmlcuesmodel.h
new file mode 100644
index 000000000000..25abdfe6b2bc
--- /dev/null
+++ b/src/qml/qmlcuesmodel.h
@@ -0,0 +1,36 @@
+#pragma once
+#include
+#include
+
+class CuePointer;
+
+namespace mixxx {
+namespace qml {
+
+class QmlCuesModel : public QAbstractListModel {
+ Q_OBJECT
+ public:
+ enum Roles {
+ StartPositionRole = Qt::UserRole + 1,
+ EndPositionRole,
+ LabelRole,
+ IsLoopRole,
+ HotcueNumberRole,
+ };
+ Q_ENUM(Roles)
+
+ explicit QmlCuesModel(QObject* parent = nullptr);
+
+ void setCues(const QList pCues);
+
+ QVariant data(const QModelIndex& index, int role) const override;
+ int rowCount(const QModelIndex& parent) const override;
+ QHash roleNames() const override;
+ Q_INVOKABLE QVariant get(int row) const;
+
+ private:
+ QList m_pCues;
+};
+
+} // namespace qml
+} // namespace mixxx
diff --git a/src/qml/qmlplayerproxy.cpp b/src/qml/qmlplayerproxy.cpp
index e35a9fc99465..12b1b6f7ff0e 100644
--- a/src/qml/qmlplayerproxy.cpp
+++ b/src/qml/qmlplayerproxy.cpp
@@ -1,5 +1,7 @@
#include "qml/qmlplayerproxy.h"
+#include
+
#include "mixer/basetrackplayer.h"
#include "moc_qmlplayerproxy.cpp"
#include "qml/asyncimageprovider.h"
@@ -26,7 +28,10 @@ namespace mixxx {
namespace qml {
QmlPlayerProxy::QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent)
- : QObject(parent), m_pTrackPlayer(pTrackPlayer) {
+ : QObject(parent),
+ m_pTrackPlayer(pTrackPlayer),
+ m_pBeatsModel(new QmlBeatsModel(this)),
+ m_pHotcuesModel(new QmlCuesModel(this)) {
connect(m_pTrackPlayer,
&BaseTrackPlayer::loadingTrack,
this,
@@ -112,6 +117,20 @@ void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) {
&Track::colorUpdated,
this,
&QmlPlayerProxy::colorChanged);
+ connect(pTrack.get(),
+ &Track::waveformUpdated,
+ this,
+ &QmlPlayerProxy::slotWaveformChanged);
+ connect(pTrack.get(),
+ &Track::beatsUpdated,
+ this,
+ &QmlPlayerProxy::slotBeatsChanged);
+ connect(pTrack.get(),
+ &Track::cuesUpdated,
+ this,
+ &QmlPlayerProxy::slotHotcuesChanged);
+ slotBeatsChanged();
+ slotHotcuesChanged();
}
emit trackChanged();
emit trackLoaded();
@@ -135,6 +154,7 @@ void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldT
}
m_pCurrentTrack.reset();
m_pCurrentTrack = pNewTrack;
+ m_waveformTexture = QImage();
emit trackChanged();
emit trackLoading();
}
@@ -155,6 +175,118 @@ void QmlPlayerProxy::slotTrackChanged() {
emit colorChanged();
emit coverArtUrlChanged();
emit trackLocationUrlChanged();
+
+ emit waveformLengthChanged();
+ emit waveformTextureChanged();
+ emit waveformTextureSizeChanged();
+ emit waveformTextureStrideChanged();
+}
+
+void QmlPlayerProxy::slotWaveformChanged() {
+ emit waveformLengthChanged();
+ emit waveformTextureSizeChanged();
+ emit waveformTextureStrideChanged();
+
+ const TrackPointer pTrack = m_pCurrentTrack;
+ if (pTrack) {
+ const ConstWaveformPointer pWaveform = pTrack->getWaveform();
+ const int textureWidth = pWaveform->getTextureStride();
+ const int textureHeight = pWaveform->getTextureSize() / pWaveform->getTextureStride();
+ if (pWaveform) {
+ const uchar* data = reinterpret_cast(pWaveform->data());
+ m_waveformTexture = QImage(data, textureWidth, textureHeight, QImage::Format_RGBA8888);
+ emit waveformTextureChanged();
+ }
+ }
+}
+
+void QmlPlayerProxy::slotBeatsChanged() {
+ VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel != nullptr) {
+ return;
+ }
+
+ const TrackPointer pTrack = m_pCurrentTrack;
+ if (pTrack) {
+ const auto trackEndPosition = mixxx::audio::FramePos{
+ pTrack->getDuration() * pTrack->getSampleRate()};
+ const auto pBeats = pTrack->getBeats();
+ m_pBeatsModel->setBeats(pBeats, trackEndPosition);
+ } else {
+ m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos);
+ }
+}
+
+void QmlPlayerProxy::slotHotcuesChanged() {
+ VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel != nullptr) {
+ return;
+ }
+
+ QList hotcues;
+
+ const TrackPointer pTrack = m_pCurrentTrack;
+ if (pTrack) {
+ for (const auto& cuePoint : pTrack->getCuePoints()) {
+ if (cuePoint->getHotCue() == Cue::kNoHotCue)
+ continue;
+ hotcues.append(cuePoint);
+ }
+ }
+ m_pHotcuesModel->setCues(hotcues);
+ emit cuesChanged();
+}
+
+int QmlPlayerProxy::getWaveformLength() const {
+ const TrackPointer pTrack = m_pCurrentTrack;
+ if (pTrack) {
+ const ConstWaveformPointer pWaveform = pTrack->getWaveform();
+ if (pWaveform) {
+ return pWaveform->getDataSize();
+ }
+ }
+ return 0;
+}
+
+QString QmlPlayerProxy::getWaveformTexture() const {
+ if (m_waveformTexture.isNull()) {
+ return QString();
+ }
+ QByteArray byteArray;
+ QBuffer buffer(&byteArray);
+ buffer.open(QIODevice::WriteOnly);
+ m_waveformTexture.save(&buffer, "png");
+
+ QString imageData = QString::fromLatin1(byteArray.toBase64().data());
+ if (imageData.isEmpty()) {
+ return QString();
+ }
+
+ return QStringLiteral("data:image/png;base64,") + imageData;
+}
+
+int QmlPlayerProxy::getWaveformTextureSize() const {
+ const TrackPointer pTrack = m_pCurrentTrack;
+ if (pTrack) {
+ const ConstWaveformPointer pWaveform = pTrack->getWaveform();
+ if (pWaveform) {
+ return pWaveform->getTextureSize();
+ }
+ }
+ return 0;
+}
+
+int QmlPlayerProxy::getWaveformTextureStride() const {
+ const TrackPointer pTrack = m_pCurrentTrack;
+ if (pTrack) {
+ const ConstWaveformPointer pWaveform = pTrack->getWaveform();
+ if (pWaveform) {
+ return pWaveform->getTextureStride();
+ }
+ }
+ return 0;
+}
+
+bool QmlPlayerProxy::isLoaded() const {
+ return m_pCurrentTrack != nullptr;
}
PROPERTY_IMPL(QString, artist, getArtist, setArtist)
diff --git a/src/qml/qmlplayerproxy.h b/src/qml/qmlplayerproxy.h
index 3ca284ab6ec5..7bcc20fa5583 100644
--- a/src/qml/qmlplayerproxy.h
+++ b/src/qml/qmlplayerproxy.h
@@ -7,6 +7,9 @@
#include
#include "mixer/basetrackplayer.h"
+#include "qml/qmlbeatsmodel.h"
+#include "qml/qmlcuesmodel.h"
+#include "track/cueinfo.h"
#include "track/track.h"
namespace mixxx {
@@ -14,6 +17,7 @@ namespace qml {
class QmlPlayerProxy : public QObject {
Q_OBJECT
+ Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged)
Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
@@ -34,9 +38,20 @@ class QmlPlayerProxy : public QObject {
QML_NAMED_ELEMENT(Player)
QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)")
+ Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged)
+ Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged)
+ Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY
+ waveformTextureSizeChanged)
+ Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY
+ waveformTextureStrideChanged)
+
+ Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
+ Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
+
public:
explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr);
+ bool isLoaded() const;
QString getTrack() const;
QString getTitle() const;
QString getArtist() const;
@@ -54,6 +69,11 @@ class QmlPlayerProxy : public QObject {
QUrl getCoverArtUrl() const;
QUrl getTrackLocationUrl() const;
+ int getWaveformLength() const;
+ QString getWaveformTexture() const;
+ int getWaveformTextureSize() const;
+ int getWaveformTextureStride() const;
+
/// Needed for interacting with the raw track player object.
BaseTrackPlayer* internalTrackPlayer() const {
return m_pTrackPlayer;
@@ -66,6 +86,9 @@ class QmlPlayerProxy : public QObject {
void slotTrackLoaded(TrackPointer pTrack);
void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack);
void slotTrackChanged();
+ void slotWaveformChanged();
+ void slotBeatsChanged();
+ void slotHotcuesChanged();
void setArtist(const QString& artist);
void setTitle(const QString& title);
@@ -102,12 +125,21 @@ class QmlPlayerProxy : public QObject {
void colorChanged();
void coverArtUrlChanged();
void trackLocationUrlChanged();
+ void cuesChanged();
void loadTrackFromLocationRequested(const QString& trackLocation, bool play);
+ void waveformLengthChanged();
+ void waveformTextureChanged();
+ void waveformTextureSizeChanged();
+ void waveformTextureStrideChanged();
+
private:
+ QImage m_waveformTexture;
QPointer m_pTrackPlayer;
TrackPointer m_pCurrentTrack;
+ QmlBeatsModel* m_pBeatsModel;
+ QmlCuesModel* m_pHotcuesModel;
};
} // namespace qml
diff --git a/tools/debian_buildenv.sh b/tools/debian_buildenv.sh
index 2c1c79750eb3..1c002fb5b2dc 100755
--- a/tools/debian_buildenv.sh
+++ b/tools/debian_buildenv.sh
@@ -80,6 +80,7 @@ case "$1" in
libportmidi-dev \
libprotobuf-dev \
libqt6core5compat6-dev\
+ libqt6shadertools6-dev \
libqt6opengl6-dev \
libqt6sql6-sqlite \
libqt6svg6-dev \