From 1e7a61af639192cca7ddcf58ddb457bff2e1e6aa Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sun, 19 Jan 2025 02:22:15 +0000 Subject: [PATCH 01/22] using geometrynode for end of track --- .../allshader/waveformrendererendoftrack.cpp | 97 ++++++++++--------- .../allshader/waveformrendererendoftrack.h | 22 +++-- 2 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp index b0c15678701..23756e74f0d 100644 --- a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp +++ b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp @@ -1,9 +1,14 @@ #include "waveform/renderers/allshader/waveformrendererendoftrack.h" #include +#include #include #include "control/controlproxy.h" +#include "rendergraph/geometry.h" +#include "rendergraph/material/rgbamaterial.h" +#include "rendergraph/vertexupdaters/rgbavertexupdater.h" +#include "util/colorcomponents.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveformwidgetfactory.h" #include "widget/wskincolor.h" @@ -11,19 +16,26 @@ namespace { constexpr int kBlinkingPeriodMillis = 1000; -constexpr float positionArray[] = {-1.f, -1.f, 1.f, -1.f, -1.f, 1.f, 1.f, 1.f}; -constexpr float verticalGradientArray[] = {1.f, 1.f, -1.f, -1.f}; -constexpr float horizontalGradientArray[] = {-1.f, 1.f, -1.f, 1.f}; } // anonymous namespace +using namespace rendergraph; + namespace allshader { WaveformRendererEndOfTrack::WaveformRendererEndOfTrack( WaveformWidgetRenderer* waveformWidget) - : WaveformRenderer(waveformWidget), + : ::WaveformRendererAbstract(waveformWidget), m_pEndOfTrackControl(nullptr), m_pTimeRemainingControl(nullptr) { + initForRectangles(0); + setUsePreprocess(true); +} + +void WaveformRendererEndOfTrack::draw(QPainter* painter, QPaintEvent* event) { + Q_UNUSED(painter); + Q_UNUSED(event); + DEBUG_ASSERT(false); } bool WaveformRendererEndOfTrack::init() { @@ -37,54 +49,32 @@ bool WaveformRendererEndOfTrack::init() { return true; } -void WaveformRendererEndOfTrack::setup(const QDomNode& node, const SkinContext& context) { +void WaveformRendererEndOfTrack::setup(const QDomNode& node, const SkinContext& skinContext) { m_color = QColor(200, 25, 20); - const QString endOfTrackColorName = context.selectString(node, "EndOfTrackColor"); + const QString endOfTrackColorName = skinContext.selectString(node, "EndOfTrackColor"); if (!endOfTrackColorName.isNull()) { m_color = QColor(endOfTrackColorName); m_color = WSkinColor::getCorrectColor(m_color); } } -void WaveformRendererEndOfTrack::initializeGL() { - m_shader.init(); -} - -void WaveformRendererEndOfTrack::fillWithGradient(QColor color) { - const int colorLocation = m_shader.colorLocation(); - const int positionLocation = m_shader.positionLocation(); - const int gradientLocation = m_shader.gradientLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); - m_shader.enableAttributeArray(gradientLocation); - - m_shader.setUniformValue(colorLocation, color); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, positionArray, 2); - m_shader.setAttributeArray(gradientLocation, - GL_FLOAT, - m_waveformRenderer->getOrientation() == Qt::Vertical - ? verticalGradientArray - : horizontalGradientArray, - 1); - - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - m_shader.disableAttributeArray(positionLocation); - m_shader.disableAttributeArray(gradientLocation); - m_shader.release(); +void WaveformRendererEndOfTrack::preprocess() { + if (!preprocessInner()) { + geometry().allocate(0); + markDirtyGeometry(); + } } -void WaveformRendererEndOfTrack::paintGL() { +bool WaveformRendererEndOfTrack::preprocessInner() { if (!m_pEndOfTrackControl->toBool()) { - return; + return false; } const int elapsed = m_timer.elapsed().toIntegerMillis() % kBlinkingPeriodMillis; - const double blinkIntensity = (double)(2 * abs(elapsed - kBlinkingPeriodMillis / 2)) / + const double blinkIntensity = + static_cast( + 2 * std::abs(elapsed - kBlinkingPeriodMillis / 2)) / kBlinkingPeriodMillis; const double remainingTime = m_pTimeRemainingControl->get(); @@ -93,17 +83,32 @@ void WaveformRendererEndOfTrack::paintGL() { const double criticalIntensity = (remainingTimeTriggerSeconds - remainingTime) / remainingTimeTriggerSeconds; - const double alpha = criticalIntensity * blinkIntensity; + const double alpha = std::clamp(criticalIntensity * blinkIntensity, 0.0, 1.0); - if (alpha != 0.0) { - QColor color = m_color; - color.setAlphaF(static_cast(alpha)); + QSizeF size(m_waveformRenderer->getWidth(), m_waveformRenderer->getHeight()); + float r, g, b, a; + getRgbF(m_color, &r, &g, &b, &a); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + const float posx0 = 0.f; + const float posx1 = size.width() / 2.f; + const float posx2 = size.width(); + const float posy1 = 0.f; + const float posy2 = size.height(); - fillWithGradient(color); - } + float minAlpha = 0.5f * static_cast(alpha); + float maxAlpha = 0.83f * static_cast(alpha); + + geometry().allocate(6 * 2); + RGBAVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + vertexUpdater.addRectangleHGradient( + {posx0, posy1}, {posx1, posy2}, {r, g, b, minAlpha}, {r, g, b, minAlpha}); + vertexUpdater.addRectangleHGradient( + {posx1, posy1}, {posx2, posy2}, {r, g, b, minAlpha}, {r, g, b, maxAlpha}); + + markDirtyGeometry(); + markDirtyMaterial(); + + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendererendoftrack.h b/src/waveform/renderers/allshader/waveformrendererendoftrack.h index 46b8a2a677b..d43d901f677 100644 --- a/src/waveform/renderers/allshader/waveformrendererendoftrack.h +++ b/src/waveform/renderers/allshader/waveformrendererendoftrack.h @@ -3,11 +3,11 @@ #include #include -#include "rendergraph/openglnode.h" -#include "shaders/endoftrackshader.h" +#include "rendergraph/geometrynode.h" +#include "rendergraph/opacitynode.h" #include "util/class.h" #include "util/performancetimer.h" -#include "waveform/renderers/allshader/waveformrenderer.h" +#include "waveform/renderers/waveformrendererabstract.h" class ControlProxy; class QDomNode; @@ -18,28 +18,30 @@ class WaveformRendererEndOfTrack; } class allshader::WaveformRendererEndOfTrack final - : public allshader::WaveformRenderer, - public rendergraph::OpenGLNode { + : public ::WaveformRendererAbstract, + public rendergraph::GeometryNode { public: explicit WaveformRendererEndOfTrack( WaveformWidgetRenderer* waveformWidget); + // Pure virtual from WaveformRendererAbstract, not used + void draw(QPainter* painter, QPaintEvent* event) override final; + void setup(const QDomNode& node, const SkinContext& skinContext) override; bool init() override; - void initializeGL() override; - void paintGL() override; + // Virtual for rendergraph::Node + void preprocess() override; private: - void fillWithGradient(QColor color); - - mixxx::EndOfTrackShader m_shader; std::unique_ptr m_pEndOfTrackControl; std::unique_ptr m_pTimeRemainingControl; QColor m_color; PerformanceTimer m_timer; + bool preprocessInner(); + DISALLOW_COPY_AND_ASSIGN(WaveformRendererEndOfTrack); }; From f0879fe6dca9fe217dfbf0b28eba92a0486fd083 Mon Sep 17 00:00:00 2001 From: m0dB Date: Mon, 9 Dec 2024 17:57:01 +0100 Subject: [PATCH 02/22] use geometrynodes for waveformrender beat (cherry picked from commit f6a7fe420db120eb3476cae448851a2d65fabe1f) --- .../allshader/waveformrenderbeat.cpp | 83 +++++++++---------- .../renderers/allshader/waveformrenderbeat.h | 24 +++--- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/waveform/renderers/allshader/waveformrenderbeat.cpp b/src/waveform/renderers/allshader/waveformrenderbeat.cpp index 39da6e21401..aa7de4ecfe5 100644 --- a/src/waveform/renderers/allshader/waveformrenderbeat.cpp +++ b/src/waveform/renderers/allshader/waveformrenderbeat.cpp @@ -2,34 +2,49 @@ #include +#include "rendergraph/geometry.h" +#include "rendergraph/material/unicolormaterial.h" +#include "rendergraph/vertexupdaters/vertexupdater.h" #include "skin/legacy/skincontext.h" #include "track/track.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "widget/wskincolor.h" +using namespace rendergraph; + namespace allshader { WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type) - : WaveformRenderer(waveformWidget), + : ::WaveformRendererAbstract(waveformWidget), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { + initForRectangles(0); + setUsePreprocess(true); } -void WaveformRenderBeat::initializeGL() { - m_shader.init(); +void WaveformRenderBeat::setup(const QDomNode& node, const SkinContext& skinContext) { + m_color = QColor(skinContext.selectString(node, "BeatColor")); + m_color = WSkinColor::getCorrectColor(m_color).toRgb(); } -void WaveformRenderBeat::setup(const QDomNode& node, const SkinContext& context) { - m_color = QColor(context.selectString(node, "BeatColor")); - m_color = WSkinColor::getCorrectColor(m_color).toRgb(); +void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* event) { + Q_UNUSED(painter); + Q_UNUSED(event); + DEBUG_ASSERT(false); } -void WaveformRenderBeat::paintGL() { - TrackPointer trackInfo = m_waveformRenderer->getTrackInfo(); +void WaveformRenderBeat::preprocess() { + if (!preprocessInner()) { + geometry().allocate(0); + markDirtyGeometry(); + } +} + +bool WaveformRenderBeat::preprocessInner() { + const TrackPointer trackInfo = m_waveformRenderer->getTrackInfo(); if (!trackInfo || (m_isSlipRenderer && !m_waveformRenderer->isSlipActive())) { - return; + return false; } auto positionType = m_isSlipRenderer ? ::WaveformRendererAbstract::Slip @@ -37,24 +52,21 @@ void WaveformRenderBeat::paintGL() { mixxx::BeatsPointer trackBeats = trackInfo->getBeats(); if (!trackBeats) { - return; + return false; } int alpha = m_waveformRenderer->getBeatGridAlpha(); if (alpha == 0) { - return; + return false; } const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - m_color.setAlphaF(alpha / 100.0f); const double trackSamples = m_waveformRenderer->getTrackSamples(); - if (trackSamples <= 0) { - return; + if (trackSamples <= 0.0) { + return false; } const double firstDisplayedPosition = @@ -68,7 +80,7 @@ void WaveformRenderBeat::paintGL() { lastDisplayedPosition * trackSamples); if (!startPosition.isValid() || !endPosition.isValid()) { - return; + return false; } const float rendererBreadth = m_waveformRenderer->getBreadth(); @@ -87,8 +99,9 @@ void WaveformRenderBeat::paintGL() { } const int reserved = numBeatsInRange * numVerticesPerLine; - m_vertices.clear(); - m_vertices.reserve(reserved); + geometry().allocate(reserved); + + VertexUpdater vertexUpdater{geometry().vertexDataAs()}; for (auto it = trackBeats->iteratorFrom(startPosition); it != trackBeats->cend() && *it <= endPosition; @@ -103,33 +116,17 @@ void WaveformRenderBeat::paintGL() { const float x1 = static_cast(xBeatPoint); const float x2 = x1 + 1.f; - m_vertices.addRectangle(x1, - 0.f, - x2, - m_isSlipRenderer ? rendererBreadth / 2 : rendererBreadth); + vertexUpdater.addRectangle({x1, 0.f}, + {x2, m_isSlipRenderer ? rendererBreadth / 2 : rendererBreadth}); } + markDirtyGeometry(); - DEBUG_ASSERT(reserved == m_vertices.size()); - - const int positionLocation = m_shader.positionLocation(); - const int matrixLocation = m_shader.matrixLocation(); - const int colorLocation = m_shader.colorLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); - - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, false); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, m_vertices.constData(), 2); - - m_shader.setUniformValue(matrixLocation, matrix); - m_shader.setUniformValue(colorLocation, m_color); + DEBUG_ASSERT(reserved == vertexUpdater.index()); - glDrawArrays(GL_TRIANGLES, 0, m_vertices.size()); + material().setUniform(1, m_color); + markDirtyMaterial(); - m_shader.disableAttributeArray(positionLocation); - m_shader.release(); + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrenderbeat.h b/src/waveform/renderers/allshader/waveformrenderbeat.h index 9433ac6e39e..f14520d8d39 100644 --- a/src/waveform/renderers/allshader/waveformrenderbeat.h +++ b/src/waveform/renderers/allshader/waveformrenderbeat.h @@ -1,12 +1,11 @@ #pragma once #include +#include -#include "rendergraph/openglnode.h" -#include "shaders/unicolorshader.h" +#include "rendergraph/geometrynode.h" #include "util/class.h" -#include "waveform/renderers/allshader/vertexdata.h" -#include "waveform/renderers/allshader/waveformrenderer.h" +#include "waveform/renderers/waveformrendererabstract.h" class QDomNode; class SkinContext; @@ -16,23 +15,26 @@ class WaveformRenderBeat; } class allshader::WaveformRenderBeat final - : public allshader::WaveformRenderer, - public rendergraph::OpenGLNode { + : public ::WaveformRendererAbstract, + public rendergraph::GeometryNode { public: explicit WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play); + // Pure virtual from WaveformRendererAbstract, not used + void draw(QPainter* painter, QPaintEvent* event) override final; + void setup(const QDomNode& node, const SkinContext& skinContext) override; - void paintGL() override; - void initializeGL() override; + + // Virtuals for rendergraph::Node + void preprocess() override; private: - mixxx::UnicolorShader m_shader; QColor m_color; - VertexData m_vertices; - bool m_isSlipRenderer; + bool preprocessInner(); + DISALLOW_COPY_AND_ASSIGN(WaveformRenderBeat); }; From 3c34e5e78ad9351ad93a4e882d73d45702a211b3 Mon Sep 17 00:00:00 2001 From: m0dB Date: Mon, 9 Dec 2024 17:51:11 +0100 Subject: [PATCH 03/22] use geometrynode for waveformrendererpreroll (cherry picked from commit 67ce4058b12e8ab219055ce21b6184f9fd7c785e) --- .../allshader/waveformrendererpreroll.cpp | 148 ++++++++---------- .../allshader/waveformrendererpreroll.h | 28 ++-- .../renderers/waveformwidgetrenderer.cpp | 18 +-- .../renderers/waveformwidgetrenderer.h | 21 ++- 4 files changed, 104 insertions(+), 111 deletions(-) diff --git a/src/waveform/renderers/allshader/waveformrendererpreroll.cpp b/src/waveform/renderers/allshader/waveformrendererpreroll.cpp index 89948347d87..47caa6f3300 100644 --- a/src/waveform/renderers/allshader/waveformrendererpreroll.cpp +++ b/src/waveform/renderers/allshader/waveformrendererpreroll.cpp @@ -1,12 +1,13 @@ #include "waveform/renderers/allshader/waveformrendererpreroll.h" #include -#include #include #include +#include "rendergraph/geometry.h" +#include "rendergraph/material/patternmaterial.h" +#include "rendergraph/vertexupdaters/texturedvertexupdater.h" #include "skin/legacy/skincontext.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "widget/wskincolor.h" @@ -20,7 +21,7 @@ QImage drawPrerollImage(float markerLength, const float imageW = static_cast(imagePixelW) / devicePixelRatio; const float imageH = static_cast(imagePixelH) / devicePixelRatio; - QImage image(imagePixelW, imagePixelH, QImage::Format_ARGB32_Premultiplied); + QImage image(imagePixelW, imagePixelH, QImage::Format_RGBA8888_Premultiplied); image.setDevicePixelRatio(devicePixelRatio); const float penWidth = 1.5f; @@ -47,7 +48,7 @@ QImage drawPrerollImage(float markerLength, path.lineTo(p0); path.closeSubpath(); QColor fillColor = color; - fillColor.setAlphaF(0.5f); + fillColor.setAlphaF(0.25f); painter.fillPath(path, QBrush(fillColor)); painter.drawPath(path); @@ -57,31 +58,52 @@ QImage drawPrerollImage(float markerLength, } } // anonymous namespace +using namespace rendergraph; + namespace allshader { WaveformRendererPreroll::WaveformRendererPreroll( WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type) - : WaveformRenderer(waveformWidget), + : ::WaveformRendererAbstract(waveformWidget), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { + setGeometry(std::make_unique(PatternMaterial::attributes(), 0)); + setMaterial(std::make_unique()); + setUsePreprocess(true); + geometry().setDrawingMode(Geometry::DrawingMode::Triangles); } WaveformRendererPreroll::~WaveformRendererPreroll() = default; void WaveformRendererPreroll::setup( - const QDomNode& node, const SkinContext& context) { - m_color = QColor(context.selectString(node, "SignalColor")); + const QDomNode& node, const SkinContext& skinContext) { + m_color = QColor(skinContext.selectString(node, "SignalColor")); m_color = WSkinColor::getCorrectColor(m_color); } -void WaveformRendererPreroll::initializeGL() { - m_shader.init(); +void WaveformRendererPreroll::draw(QPainter* painter, QPaintEvent* event) { + Q_UNUSED(painter); + Q_UNUSED(event); + DEBUG_ASSERT(false); +} + +void WaveformRendererPreroll::preprocess() { + if (!preprocessInner()) { + if (geometry().vertexCount() != 0) { + geometry().allocate(0); + markDirtyGeometry(); + } + } else { + markDirtyMaterial(); + markDirtyGeometry(); + } } -void WaveformRendererPreroll::paintGL() { - const TrackPointer track = m_waveformRenderer->getTrackInfo(); - if (!track || (m_isSlipRenderer && !m_waveformRenderer->isSlipActive())) { - return; +bool WaveformRendererPreroll::preprocessInner() { + const TrackPointer trackInfo = m_waveformRenderer->getTrackInfo(); + + if (!trackInfo || (m_isSlipRenderer && !m_waveformRenderer->isSlipActive())) { + return false; } auto positionType = m_isSlipRenderer ? ::WaveformRendererAbstract::Slip @@ -95,11 +117,15 @@ void WaveformRendererPreroll::paintGL() { // to indicate the respective zones. const bool preRollVisible = firstDisplayedPosition < 0; const bool postRollVisible = lastDisplayedPosition > 1; + const int numVerticesPerRectangle = 6; - if (!(preRollVisible || postRollVisible)) { - return; + if (!preRollVisible && !postRollVisible) { + return false; } + const int reserved = (preRollVisible ? numVerticesPerRectangle : 0) + + (postRollVisible ? numVerticesPerRectangle : 0); + const double playMarkerPosition = m_waveformRenderer->getPlayMarkerPosition(); const double vSamplesPerPixel = m_waveformRenderer->getVisualSamplePerPixel(); const double numberOfVSamples = m_waveformRenderer->getLength() * vSamplesPerPixel; @@ -125,36 +151,20 @@ void WaveformRendererPreroll::paintGL() { // has changed size last time. m_markerLength = markerLength; m_markerBreadth = markerBreadth; - m_texture.setData(drawPrerollImage(m_markerLength, - m_markerBreadth, - m_waveformRenderer->getDevicePixelRatio(), - m_color)); + dynamic_cast(material()) + .setTexture(std::make_unique(m_waveformRenderer->getContext(), + drawPrerollImage(m_markerLength, + m_markerBreadth, + m_waveformRenderer->getDevicePixelRatio(), + m_color))); } - if (!m_texture.isStorageAllocated()) { - return; - } - - const int matrixLocation = m_shader.matrixLocation(); - const int textureLocation = m_shader.textureLocation(); - const int positionLocation = m_shader.positionLocation(); - const int texcoordLocation = m_shader.texcoordLocation(); - - // Set up the shader - m_shader.bind(); - - m_shader.enableAttributeArray(positionLocation); - m_shader.enableAttributeArray(texcoordLocation); - - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, false); - - m_shader.setUniformValue(matrixLocation, matrix); - m_shader.setUniformValue(textureLocation, 0); - - m_texture.bind(); + geometry().allocate(reserved); const float end = m_waveformRenderer->getLength(); + TexturedVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + if (preRollVisible) { // VSample position of the right-most triangle's tip const double triangleTipVSamplePosition = @@ -168,11 +178,14 @@ void WaveformRendererPreroll::paintGL() { x -= std::ceil((x - limit) / markerLength) * markerLength; } - drawPattern(x, - halfBreadth - halfMarkerBreadth, - 0.f, - m_isSlipRenderer ? halfBreadth : halfBreadth + halfMarkerBreadth, - x / markerLength); + const float repetitions = x / markerLength; + + vertexUpdater.addRectangle({x, halfBreadth - halfMarkerBreadth}, + {0, + m_isSlipRenderer ? halfBreadth + : halfBreadth + halfMarkerBreadth}, + {0.f, 0.f}, + {repetitions, m_isSlipRenderer ? 0.5f : 1.f}); } if (postRollVisible) { @@ -189,44 +202,19 @@ void WaveformRendererPreroll::paintGL() { x += std::ceil((limit - x) / markerLength) * markerLength; } - drawPattern(x, - halfBreadth - halfMarkerBreadth, - end, - m_isSlipRenderer ? halfBreadth : halfBreadth + halfMarkerBreadth, - (end - x) / markerLength); - } + const float repetitions = (end - x) / markerLength; - m_texture.release(); + vertexUpdater.addRectangle({x, halfBreadth - halfMarkerBreadth}, + {end, + m_isSlipRenderer ? halfBreadth + : halfBreadth + halfMarkerBreadth}, + {0.f, 0.f}, + {repetitions, m_isSlipRenderer ? 0.5f : 1.f}); + } - m_shader.disableAttributeArray(positionLocation); - m_shader.disableAttributeArray(texcoordLocation); - m_shader.release(); -} + DEBUG_ASSERT(reserved == vertexUpdater.index()); -void WaveformRendererPreroll::drawPattern( - float x1, float y1, float x2, float y2, float repetitions) { - // Draw a large rectangle with a repeating pattern of the texture - const int repetitionsLocation = m_shader.repetitionsLocation(); - const int positionLocation = m_shader.positionLocation(); - const int texcoordLocation = m_shader.texcoordLocation(); - - const std::array positionArray = {x1, y1, x2, y1, x1, y2, x2, y2}; - const std::array texcoordArray = {0.f, - 0.f, - 1.f, - 0.f, - 0.f, - m_isSlipRenderer ? 0.5f : 1.f, - 1.f, - m_isSlipRenderer ? 0.5f : 1.f}; - m_shader.setUniformValue(repetitionsLocation, QVector2D(repetitions, 1.0)); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, positionArray.data(), 2); - m_shader.setAttributeArray( - texcoordLocation, GL_FLOAT, texcoordArray.data(), 2); - - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendererpreroll.h b/src/waveform/renderers/allshader/waveformrendererpreroll.h index 0fd2cc59942..eb28459ef96 100644 --- a/src/waveform/renderers/allshader/waveformrendererpreroll.h +++ b/src/waveform/renderers/allshader/waveformrendererpreroll.h @@ -1,27 +1,22 @@ #pragma once #include -#include #include -#include "rendergraph/openglnode.h" -#include "shaders/patternshader.h" +#include "rendergraph/geometrynode.h" #include "util/class.h" -#include "util/opengltexture2d.h" -#include "waveform/renderers/allshader/vertexdata.h" -#include "waveform/renderers/allshader/waveformrenderer.h" +#include "waveform/renderers/waveformrendererabstract.h" class QDomNode; class SkinContext; -class QOpenGLTexture; namespace allshader { class WaveformRendererPreroll; } class allshader::WaveformRendererPreroll final - : public allshader::WaveformRenderer, - public rendergraph::OpenGLNode { + : public ::WaveformRendererAbstract, + public rendergraph::GeometryNode { public: explicit WaveformRendererPreroll( WaveformWidgetRenderer* waveformWidget, @@ -29,20 +24,21 @@ class allshader::WaveformRendererPreroll final ::WaveformRendererAbstract::Play); ~WaveformRendererPreroll() override; + // Pure virtual from WaveformRendererAbstract, not used + void draw(QPainter* painter, QPaintEvent* event) override final; + void setup(const QDomNode& node, const SkinContext& skinContext) override; - void paintGL() override; - void initializeGL() override; - private: - void drawPattern(float x1, float y1, float x2, float y2, float repetitions); + // Virtual for rendergraph::Node + void preprocess() override; - mixxx::PatternShader m_shader; + private: QColor m_color; float m_markerBreadth{}; float m_markerLength{}; - OpenGLTexture2D m_texture; - bool m_isSlipRenderer; + bool preprocessInner(); + DISALLOW_COPY_AND_ASSIGN(WaveformRendererPreroll); }; diff --git a/src/waveform/renderers/waveformwidgetrenderer.cpp b/src/waveform/renderers/waveformwidgetrenderer.cpp index 8059711b8a2..1fb0ad27834 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.cpp +++ b/src/waveform/renderers/waveformwidgetrenderer.cpp @@ -39,13 +39,11 @@ WaveformWidgetRenderer::WaveformWidgetRenderer(const QString& group) // Really create some to manage those; m_visualPlayPosition(nullptr), m_totalVSamples(0), - m_pRateRatioCO(nullptr), - m_pGainControlObject(nullptr), m_gain(1.0), - m_pTrackSamplesControlObject(nullptr), - m_trackSamples(0), + m_trackSamples(0.0), m_scaleFactor(1.0), m_playMarkerPosition(s_defaultPlayMarkerPosition), + m_pContext(nullptr), m_passthroughEnabled(false) { //qDebug() << "WaveformWidgetRenderer"; for (int type = ::WaveformRendererAbstract::Play; @@ -79,10 +77,6 @@ WaveformWidgetRenderer::~WaveformWidgetRenderer() { delete m_rendererStack[i]; } - delete m_pRateRatioCO; - delete m_pGainControlObject; - delete m_pTrackSamplesControlObject; - #ifdef WAVEFORMWIDGETRENDERER_DEBUG delete m_timer; #endif @@ -93,11 +87,11 @@ bool WaveformWidgetRenderer::init() { m_visualPlayPosition = VisualPlayPosition::getVisualPlayPosition(m_group); - m_pRateRatioCO = new ControlProxy( + m_pRateRatioCO = std::make_unique( m_group, "rate_ratio"); - m_pGainControlObject = new ControlProxy( + m_pGainControlObject = std::make_unique( m_group, "total_gain"); - m_pTrackSamplesControlObject = new ControlProxy( + m_pTrackSamplesControlObject = std::make_unique( m_group, "track_samples"); for (int i = 0; i < m_rendererStack.size(); ++i) { @@ -428,7 +422,7 @@ void WaveformWidgetRenderer::selectStem(mixxx::StemChannelSelection stemMask) { void WaveformWidgetRenderer::setTrack(TrackPointer track) { m_pTrack = track; //used to postpone first display until track sample is actually available - m_trackSamples = -1; + m_trackSamples = -1.0; for (int i = 0; i < m_rendererStack.size(); ++i) { m_rendererStack[i]->onSetTrack(); diff --git a/src/waveform/renderers/waveformwidgetrenderer.h b/src/waveform/renderers/waveformwidgetrenderer.h index 33a948bca65..42d02f49e0c 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.h +++ b/src/waveform/renderers/waveformwidgetrenderer.h @@ -15,6 +15,10 @@ class VSyncThread; class QPainter; class WaveformRendererAbstract; +namespace rendergraph { +class Context; +} + class WaveformWidgetRenderer { public: static const double s_waveformMinZoom; @@ -201,6 +205,14 @@ class WaveformWidgetRenderer { return m_trackSamples <= 0.0 || m_pos[::WaveformRendererAbstract::Play] == -1; } + void setContext(rendergraph::Context* pContext) { + m_pContext = pContext; + } + + rendergraph::Context* getContext() const { + return m_pContext; + } + protected: const QString m_group; TrackPointer m_pTrack; @@ -231,14 +243,17 @@ class WaveformWidgetRenderer { QSharedPointer m_visualPlayPosition; int m_posVSample[2]; int m_totalVSamples; - ControlProxy* m_pRateRatioCO; - ControlProxy* m_pGainControlObject; + std::unique_ptr m_pRateRatioCO; + std::unique_ptr m_pGainControlObject; + std::unique_ptr m_pTrackSamplesControlObject; double m_gain; - ControlProxy* m_pTrackSamplesControlObject; double m_trackSamples; double m_scaleFactor; double m_playMarkerPosition; // 0.0 - left, 0.5 - center, 1.0 - right + // used by allshader waveformrenderers when used with rendergraph nodes + rendergraph::Context* m_pContext; + #ifdef WAVEFORMWIDGETRENDERER_DEBUG PerformanceTimer* m_timer; int m_lastFrameTime; From db12eb736f41955eb908c0c4ab08f01796544210 Mon Sep 17 00:00:00 2001 From: m0dB Date: Mon, 9 Dec 2024 20:41:40 +0100 Subject: [PATCH 04/22] use geometrynodes for waveform signals rgb, hsv, simple and filtered/stacked (cherry picked from commit 58fb26b3f29b9aea921064e3b7391e01335ff213) --- .../allshader/waveformrendererfiltered.cpp | 196 +++++++++--------- .../allshader/waveformrendererfiltered.h | 15 +- .../allshader/waveformrendererhsv.cpp | 113 +++++----- .../renderers/allshader/waveformrendererhsv.h | 17 +- .../allshader/waveformrendererrgb.cpp | 124 ++++++----- .../renderers/allshader/waveformrendererrgb.h | 21 +- .../allshader/waveformrenderersimple.cpp | 148 ++++++------- .../allshader/waveformrenderersimple.h | 15 +- 8 files changed, 307 insertions(+), 342 deletions(-) diff --git a/src/waveform/renderers/allshader/waveformrendererfiltered.cpp b/src/waveform/renderers/allshader/waveformrendererfiltered.cpp index d7c40cd01ed..00863e8e586 100644 --- a/src/waveform/renderers/allshader/waveformrendererfiltered.cpp +++ b/src/waveform/renderers/allshader/waveformrendererfiltered.cpp @@ -1,56 +1,70 @@ #include "waveform/renderers/allshader/waveformrendererfiltered.h" +#include "rendergraph/material/rgbmaterial.h" +#include "rendergraph/vertexupdaters/rgbvertexupdater.h" #include "track/track.h" #include "util/math.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveform.h" +using namespace rendergraph; + namespace allshader { WaveformRendererFiltered::WaveformRendererFiltered( WaveformWidgetRenderer* waveformWidget, bool bRgbStacked) : WaveformRendererSignalBase(waveformWidget), m_bRgbStacked(bRgbStacked) { + initForRectangles(0); + setUsePreprocess(true); } void WaveformRendererFiltered::onSetup(const QDomNode&) { } -void WaveformRendererFiltered::initializeGL() { - m_shader.init(); +void WaveformRendererFiltered::preprocess() { + if (!preprocessInner()) { + if (geometry().vertexCount() != 0) { + geometry().allocate(0); + markDirtyGeometry(); + } + } } -void WaveformRendererFiltered::paintGL() { +bool WaveformRendererFiltered::preprocessInner() { TrackPointer pTrack = m_waveformRenderer->getTrackInfo(); + if (!pTrack) { - return; + return false; } ConstWaveformPointer waveform = pTrack->getWaveform(); if (waveform.isNull()) { - return; + return false; } const int dataSize = waveform->getDataSize(); if (dataSize <= 1) { - return; + return false; } const WaveformData* data = waveform->data(); if (data == nullptr) { - return; + return false; } #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); // If this track is a stem track, skip the rendering if (!stemInfo.isEmpty() && waveform->hasStem()) { - return; + return false; } #endif const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); - const int length = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const int length = static_cast(m_waveformRenderer->getLength()); + const int pixelLength = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const float invDevicePixelRatio = 1.f / devicePixelRatio; + const float halfPixelSize = 0.5 / devicePixelRatio; // See waveformrenderersimple.cpp for a detailed explanation of the frame and index calculation const int visualFramesSize = dataSize / 2; @@ -61,14 +75,14 @@ void WaveformRendererFiltered::paintGL() { // Represents the # of visual frames per horizontal pixel. const double visualIncrementPerPixel = - (lastVisualFrame - firstVisualFrame) / static_cast(length); + (lastVisualFrame - firstVisualFrame) / static_cast(pixelLength); // Per-band gain from the EQ knobs. - float allGain{1.0}; + float allGain(1.0); float bandGain[3] = {1.0, 1.0, 1.0}; getGains(&allGain, true, &bandGain[0], &bandGain[1], &bandGain[2]); - const float breadth = static_cast(m_waveformRenderer->getBreadth()) * devicePixelRatio; + const float breadth = static_cast(m_waveformRenderer->getBreadth()); const float halfBreadth = breadth / 2.0f; const float heightFactor = allGain * halfBreadth / m_maxValue; @@ -79,28 +93,55 @@ void WaveformRendererFiltered::paintGL() { const int numVerticesPerLine = 6; // 2 triangles - int reserved[4]; - // low, mid, high - for (int bandIndex = 0; bandIndex < 3; bandIndex++) { - m_vertices[bandIndex].clear(); - reserved[bandIndex] = numVerticesPerLine * length; - m_vertices[bandIndex].reserve(reserved[bandIndex]); - } + // low, mid, high + horizontal axis + int reserved = numVerticesPerLine * (pixelLength * 3 + 1); - // the horizontal line - reserved[3] = numVerticesPerLine; - m_vertices[3].clear(); - m_vertices[3].reserve(reserved[3]); + geometry().setDrawingMode(Geometry::DrawingMode::Triangles); + geometry().allocate(reserved); + markDirtyGeometry(); - m_vertices[3].addRectangle( - 0.f, - halfBreadth - 0.5f * devicePixelRatio, - static_cast(length), - halfBreadth + 0.5f * devicePixelRatio); + QVector3D rgb[3]; + if (m_bRgbStacked) { + rgb[0] = QVector3D(static_cast(m_rgbLowColor_r), + static_cast(m_rgbLowColor_g), + static_cast(m_rgbLowColor_b)); + rgb[1] = QVector3D(static_cast(m_rgbMidColor_r), + static_cast(m_rgbMidColor_g), + static_cast(m_rgbMidColor_b)); + rgb[2] = QVector3D(static_cast(m_rgbHighColor_r), + static_cast(m_rgbHighColor_g), + static_cast(m_rgbHighColor_b)); + } else { + rgb[0] = QVector3D(static_cast(m_lowColor_r), + static_cast(m_lowColor_g), + static_cast(m_lowColor_b)); + rgb[1] = QVector3D(static_cast(m_midColor_r), + static_cast(m_midColor_g), + static_cast(m_midColor_b)); + rgb[2] = QVector3D(static_cast(m_highColor_r), + static_cast(m_highColor_g), + static_cast(m_highColor_b)); + } + RGBVertexUpdater axisVertexUpdater{geometry().vertexDataAs()}; + axisVertexUpdater.addRectangle({0.f, + halfBreadth - 0.5f}, + {static_cast(length), + halfBreadth + 0.5f}, + {static_cast(m_axesColor_r), + static_cast(m_axesColor_g), + static_cast(m_axesColor_b)}); + + RGBVertexUpdater vertexUpdater[3]{ + {geometry().vertexDataAs() + + numVerticesPerLine}, + {geometry().vertexDataAs() + + numVerticesPerLine * (1 + pixelLength)}, + {geometry().vertexDataAs() + + numVerticesPerLine * (1 + pixelLength * 2)}}; const double maxSamplingRange = visualIncrementPerPixel / 2.0; - for (int pos = 0; pos < length; ++pos) { + for (int pos = 0; pos < pixelLength; ++pos) { const int visualFrameStart = std::lround(xVisualFrame - maxSamplingRange); const int visualFrameStop = std::lround(xVisualFrame + maxSamplingRange); @@ -108,90 +149,51 @@ void WaveformRendererFiltered::paintGL() { const int visualIndexStop = std::min(std::max(visualFrameStop, visualFrameStart + 1) * 2, dataSize - 1); - const float fpos = static_cast(pos); + const float fpos = static_cast(pos) * invDevicePixelRatio; // 3 bands, 2 channels float max[3][2]{}; - - for (int i = visualIndexStart; i < visualIndexStop; i += 2) { - for (int chn = 0; chn < 2; chn++) { - const WaveformData& waveformData = data[i + chn]; - const float filteredLow = static_cast(waveformData.filtered.low); - const float filteredMid = static_cast(waveformData.filtered.mid); - const float filteredHigh = static_cast(waveformData.filtered.high); - - max[0][chn] = math_max(max[0][chn], filteredLow); - max[1][chn] = math_max(max[1][chn], filteredMid); - max[2][chn] = math_max(max[2][chn], filteredHigh); + uchar u8max[3][2]{}; + for (int chn = 0; chn < 2; chn++) { + for (int i = visualIndexStart + chn; i < visualIndexStop + chn; i += 2) { + const WaveformData& waveformData = data[i]; + + u8max[0][chn] = math_max(u8max[0][chn], waveformData.filtered.low); + u8max[1][chn] = math_max(u8max[1][chn], waveformData.filtered.mid); + u8max[2][chn] = math_max(u8max[2][chn], waveformData.filtered.high); } + // Cast to float + max[0][chn] = static_cast(u8max[0][chn]); + max[1][chn] = static_cast(u8max[1][chn]); + max[2][chn] = static_cast(u8max[2][chn]); } + // TODO: this can be optimized by using one geometrynode per band + // + one for the horizontal axis, and uniform color materials, + // instead of passing constant color as vertex. + for (int bandIndex = 0; bandIndex < 3; bandIndex++) { max[bandIndex][0] *= bandGain[bandIndex]; max[bandIndex][1] *= bandGain[bandIndex]; - // lines are thin rectangles - m_vertices[bandIndex].addRectangle( - fpos - 0.5f, - halfBreadth - heightFactor * max[bandIndex][0], - fpos + 0.5f, - halfBreadth + heightFactor * max[bandIndex][1]); + vertexUpdater[bandIndex].addRectangle( + {fpos - halfPixelSize, + halfBreadth - heightFactor * max[bandIndex][0]}, + {fpos + halfPixelSize, + halfBreadth + heightFactor * max[bandIndex][1]}, + {rgb[bandIndex]}); } xVisualFrame += visualIncrementPerPixel; } - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, true); + DEBUG_ASSERT(reserved == + vertexUpdater[0].index() + vertexUpdater[1].index() + + vertexUpdater[2].index()); - const int matrixLocation = m_shader.matrixLocation(); - const int colorLocation = m_shader.colorLocation(); - const int positionLocation = m_shader.positionLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); - - m_shader.setUniformValue(matrixLocation, matrix); - - QColor colors[4]; - if (m_bRgbStacked) { - colors[0].setRgbF(static_cast(m_rgbLowColor_r), - static_cast(m_rgbLowColor_g), - static_cast(m_rgbLowColor_b)); - colors[1].setRgbF(static_cast(m_rgbMidColor_r), - static_cast(m_rgbMidColor_g), - static_cast(m_rgbMidColor_b)); - colors[2].setRgbF(static_cast(m_rgbHighColor_r), - static_cast(m_rgbHighColor_g), - static_cast(m_rgbHighColor_b)); - } else { - colors[0].setRgbF(static_cast(m_lowColor_r), - static_cast(m_lowColor_g), - static_cast(m_lowColor_b)); - colors[1].setRgbF(static_cast(m_midColor_r), - static_cast(m_midColor_g), - static_cast(m_midColor_b)); - colors[2].setRgbF(static_cast(m_highColor_r), - static_cast(m_highColor_g), - static_cast(m_highColor_b)); - } - colors[3].setRgbF(static_cast(m_axesColor_r), - static_cast(m_axesColor_g), - static_cast(m_axesColor_b), - static_cast(m_axesColor_a)); - - // 3 bands + 1 extra for the horizontal line - - for (int i = 0; i < 4; i++) { - DEBUG_ASSERT(reserved[i] == m_vertices[i].size()); - m_shader.setUniformValue(colorLocation, colors[i]); - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, m_vertices[i].constData(), 2); - - glDrawArrays(GL_TRIANGLES, 0, m_vertices[i].size()); - } + markDirtyMaterial(); - m_shader.disableAttributeArray(positionLocation); - m_shader.release(); + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendererfiltered.h b/src/waveform/renderers/allshader/waveformrendererfiltered.h index 43441adafa5..00ee18f7019 100644 --- a/src/waveform/renderers/allshader/waveformrendererfiltered.h +++ b/src/waveform/renderers/allshader/waveformrendererfiltered.h @@ -1,9 +1,7 @@ #pragma once -#include "rendergraph/openglnode.h" -#include "shaders/unicolorshader.h" +#include "rendergraph/geometrynode.h" #include "util/class.h" -#include "waveform/renderers/allshader/vertexdata.h" #include "waveform/renderers/allshader/waveformrenderersignalbase.h" namespace allshader { @@ -12,20 +10,19 @@ class WaveformRendererFiltered; class allshader::WaveformRendererFiltered final : public allshader::WaveformRendererSignalBase, - public rendergraph::OpenGLNode { + public rendergraph::GeometryNode { public: explicit WaveformRendererFiltered(WaveformWidgetRenderer* waveformWidget, bool rgbStacked); - // override ::WaveformRendererSignalBase + // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; - void initializeGL() override; - void paintGL() override; + // Virtuals for rendergraph::Node + void preprocess() override; private: const bool m_bRgbStacked; - mixxx::UnicolorShader m_shader; - VertexData m_vertices[4]; + bool preprocessInner(); DISALLOW_COPY_AND_ASSIGN(WaveformRendererFiltered); }; diff --git a/src/waveform/renderers/allshader/waveformrendererhsv.cpp b/src/waveform/renderers/allshader/waveformrendererhsv.cpp index 60ec42822be..40c8a5dbbb6 100644 --- a/src/waveform/renderers/allshader/waveformrendererhsv.cpp +++ b/src/waveform/renderers/allshader/waveformrendererhsv.cpp @@ -1,56 +1,69 @@ #include "waveform/renderers/allshader/waveformrendererhsv.h" +#include "rendergraph/material/rgbmaterial.h" +#include "rendergraph/vertexupdaters/rgbvertexupdater.h" #include "track/track.h" #include "util/colorcomponents.h" #include "util/math.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveform.h" +using namespace rendergraph; + namespace allshader { -WaveformRendererHSV::WaveformRendererHSV( - WaveformWidgetRenderer* waveformWidget) +WaveformRendererHSV::WaveformRendererHSV(WaveformWidgetRenderer* waveformWidget) : WaveformRendererSignalBase(waveformWidget) { + initForRectangles(0); + setUsePreprocess(true); } void WaveformRendererHSV::onSetup(const QDomNode&) { } -void WaveformRendererHSV::initializeGL() { - m_shader.init(); +void WaveformRendererHSV::preprocess() { + if (!preprocessInner()) { + if (geometry().vertexCount() != 0) { + geometry().allocate(0); + markDirtyGeometry(); + } + } } -void WaveformRendererHSV::paintGL() { +bool WaveformRendererHSV::preprocessInner() { TrackPointer pTrack = m_waveformRenderer->getTrackInfo(); + if (!pTrack) { - return; + return false; } ConstWaveformPointer waveform = pTrack->getWaveform(); if (waveform.isNull()) { - return; + return false; } const int dataSize = waveform->getDataSize(); if (dataSize <= 1) { - return; + return false; } const WaveformData* data = waveform->data(); if (data == nullptr) { - return; + return false; } #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); // If this track is a stem track, skip the rendering if (!stemInfo.isEmpty() && waveform->hasStem()) { - return; + return false; } #endif const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); - const int length = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const int length = static_cast(m_waveformRenderer->getLength()); + const int pixelLength = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const float invDevicePixelRatio = 1.f / devicePixelRatio; + const float halfPixelSize = 0.5 / devicePixelRatio; // See waveformrenderersimple.cpp for a detailed explanation of the frame and index calculation const int visualFramesSize = dataSize / 2; @@ -61,7 +74,7 @@ void WaveformRendererHSV::paintGL() { // Represents the # of visual frames per horizontal pixel. const double visualIncrementPerPixel = - (lastVisualFrame - firstVisualFrame) / static_cast(length); + (lastVisualFrame - firstVisualFrame) / static_cast(pixelLength); float allGain(1.0); getGains(&allGain, false, nullptr, nullptr, nullptr); @@ -70,7 +83,7 @@ void WaveformRendererHSV::paintGL() { float h, s, v; getHsvF(m_pColors->getLowColor(), &h, &s, &v); - const float breadth = static_cast(m_waveformRenderer->getBreadth()) * devicePixelRatio; + const float breadth = static_cast(m_waveformRenderer->getBreadth()); const float halfBreadth = breadth / 2.0f; const float heightFactor = allGain * halfBreadth / m_maxValue; @@ -81,25 +94,24 @@ void WaveformRendererHSV::paintGL() { const int numVerticesPerLine = 6; // 2 triangles - const int reserved = numVerticesPerLine * (length + 1); + const int reserved = numVerticesPerLine * (pixelLength + 1); - m_vertices.clear(); - m_vertices.reserve(reserved); - m_colors.clear(); - m_colors.reserve(reserved); + geometry().setDrawingMode(Geometry::DrawingMode::Triangles); + geometry().allocate(reserved); + markDirtyGeometry(); - m_vertices.addRectangle(0.f, - halfBreadth - 0.5f * devicePixelRatio, - static_cast(length), - halfBreadth + 0.5f * devicePixelRatio); - m_colors.addForRectangle( - static_cast(m_axesColor_r), - static_cast(m_axesColor_g), - static_cast(m_axesColor_b)); + RGBVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + vertexUpdater.addRectangle({0.f, + halfBreadth - 0.5f}, + {static_cast(length), + halfBreadth + 0.5f}, + {static_cast(m_axesColor_r), + static_cast(m_axesColor_g), + static_cast(m_axesColor_b)}); const double maxSamplingRange = visualIncrementPerPixel / 2.0; - for (int pos = 0; pos < length; ++pos) { + for (int pos = 0; pos < pixelLength; ++pos) { const int visualFrameStart = std::lround(xVisualFrame - maxSamplingRange); const int visualFrameStop = std::lround(xVisualFrame + maxSamplingRange); @@ -107,7 +119,7 @@ void WaveformRendererHSV::paintGL() { const int visualIndexStop = std::min(std::max(visualFrameStop, visualFrameStart + 1) * 2, dataSize - 1); - const float fpos = static_cast(pos); + const float fpos = static_cast(pos) * invDevicePixelRatio; // per channel float maxLow[2]{}; @@ -161,45 +173,24 @@ void WaveformRendererHSV::paintGL() { QColor color; color.setHsvF(h, 1.0f - hi, 1.0f - lo); - // lines are thin rectangles + // Lines are thin rectangles // maxAll[0] is for left channel, maxAll[1] is for right channel - m_vertices.addRectangle(fpos - 0.5f, - halfBreadth - heightFactor * maxAll[0], - fpos + 0.5f, - halfBreadth + heightFactor * maxAll[1]); - m_colors.addForRectangle( - static_cast(color.redF()), - static_cast(color.greenF()), - static_cast(color.blueF())); + vertexUpdater.addRectangle({fpos - halfPixelSize, + halfBreadth - heightFactor * maxAll[0]}, + {fpos + halfPixelSize, + halfBreadth + heightFactor * maxAll[1]}, + {static_cast(color.redF()), + static_cast(color.greenF()), + static_cast(color.blueF())}); xVisualFrame += visualIncrementPerPixel; } - DEBUG_ASSERT(reserved == m_vertices.size()); - DEBUG_ASSERT(reserved == m_colors.size()); - - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, true); - - const int matrixLocation = m_shader.matrixLocation(); - const int positionLocation = m_shader.positionLocation(); - const int colorLocation = m_shader.colorLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); - m_shader.enableAttributeArray(colorLocation); - - m_shader.setUniformValue(matrixLocation, matrix); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, m_vertices.constData(), 2); - m_shader.setAttributeArray( - colorLocation, GL_FLOAT, m_colors.constData(), 3); + DEBUG_ASSERT(reserved == vertexUpdater.index()); - glDrawArrays(GL_TRIANGLES, 0, m_vertices.size()); + markDirtyMaterial(); - m_shader.disableAttributeArray(positionLocation); - m_shader.disableAttributeArray(colorLocation); - m_shader.release(); + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendererhsv.h b/src/waveform/renderers/allshader/waveformrendererhsv.h index a6fdbe95f51..65acbcdaf09 100644 --- a/src/waveform/renderers/allshader/waveformrendererhsv.h +++ b/src/waveform/renderers/allshader/waveformrendererhsv.h @@ -1,10 +1,7 @@ #pragma once -#include "rendergraph/openglnode.h" -#include "shaders/rgbshader.h" +#include "rendergraph/geometrynode.h" #include "util/class.h" -#include "waveform/renderers/allshader/rgbdata.h" -#include "waveform/renderers/allshader/vertexdata.h" #include "waveform/renderers/allshader/waveformrenderersignalbase.h" namespace allshader { @@ -13,20 +10,18 @@ class WaveformRendererHSV; class allshader::WaveformRendererHSV final : public allshader::WaveformRendererSignalBase, - public rendergraph::OpenGLNode { + public rendergraph::GeometryNode { public: explicit WaveformRendererHSV(WaveformWidgetRenderer* waveformWidget); - // override ::WaveformRendererSignalBase + // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; - void initializeGL() override; - void paintGL() override; + // Virtuals for rendergraph::Node + void preprocess() override; private: - mixxx::RGBShader m_shader; - VertexData m_vertices; - RGBData m_colors; + bool preprocessInner(); DISALLOW_COPY_AND_ASSIGN(WaveformRendererHSV); }; diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.cpp b/src/waveform/renderers/allshader/waveformrendererrgb.cpp index 6d7a3d7393f..de0700c2662 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.cpp +++ b/src/waveform/renderers/allshader/waveformrendererrgb.cpp @@ -1,11 +1,14 @@ #include "waveform/renderers/allshader/waveformrendererrgb.h" +#include "rendergraph/material/rgbmaterial.h" +#include "rendergraph/vertexupdaters/rgbvertexupdater.h" #include "track/track.h" #include "util/math.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveform.h" +using namespace rendergraph; + namespace allshader { namespace { @@ -20,19 +23,27 @@ WaveformRendererRGB::WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, : WaveformRendererSignalBase(waveformWidget), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), m_options(options) { + initForRectangles(0); + setUsePreprocess(true); } void WaveformRendererRGB::onSetup(const QDomNode&) { } -void WaveformRendererRGB::initializeGL() { - m_shader.init(); +void WaveformRendererRGB::preprocess() { + if (!preprocessInner()) { + if (geometry().vertexCount() != 0) { + geometry().allocate(0); + markDirtyGeometry(); + } + } } -void WaveformRendererRGB::paintGL() { +bool WaveformRendererRGB::preprocessInner() { TrackPointer pTrack = m_waveformRenderer->getTrackInfo(); + if (!pTrack || (m_isSlipRenderer && !m_waveformRenderer->isSlipActive())) { - return; + return false; } auto positionType = m_isSlipRenderer ? ::WaveformRendererAbstract::Slip @@ -40,28 +51,31 @@ void WaveformRendererRGB::paintGL() { ConstWaveformPointer waveform = pTrack->getWaveform(); if (waveform.isNull()) { - return; + return false; } const int dataSize = waveform->getDataSize(); if (dataSize <= 1) { - return; + return false; } const WaveformData* data = waveform->data(); if (data == nullptr) { - return; + return false; } #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); // If this track is a stem track, skip the rendering if (!stemInfo.isEmpty() && waveform->hasStem()) { - return; + return false; } #endif const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); - const int length = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const int length = static_cast(m_waveformRenderer->getLength()); + const int pixelLength = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const float invDevicePixelRatio = 1.f / devicePixelRatio; + const float halfPixelSize = 0.5 / devicePixelRatio; // See waveformrenderersimple.cpp for a detailed explanation of the frame and index calculation const int visualFramesSize = dataSize / 2; @@ -72,14 +86,14 @@ void WaveformRendererRGB::paintGL() { // Represents the # of visual frames per horizontal pixel. const double visualIncrementPerPixel = - (lastVisualFrame - firstVisualFrame) / static_cast(length); + (lastVisualFrame - firstVisualFrame) / static_cast(pixelLength); // Per-band gain from the EQ knobs. float allGain(1.0), lowGain(1.0), midGain(1.0), highGain(1.0); // applyCompensation = false, as we scale to match filtered.all getGains(&allGain, false, &lowGain, &midGain, &highGain); - const float breadth = static_cast(m_waveformRenderer->getBreadth()) * devicePixelRatio; + const float breadth = static_cast(m_waveformRenderer->getBreadth()); const float halfBreadth = breadth / 2.0f; const float heightFactorAbs = allGain * halfBreadth / m_maxValue; @@ -104,25 +118,24 @@ void WaveformRendererRGB::paintGL() { const int reserved = numVerticesPerLine * // Slip rendere only render a single channel, so the vertices count doesn't change - ((splitLeftRight && !m_isSlipRenderer ? length * 2 : length) + 1); - - m_vertices.clear(); - m_vertices.reserve(reserved); - m_colors.clear(); - m_colors.reserve(reserved); - - m_vertices.addRectangle(0.f, - halfBreadth - 0.5f * devicePixelRatio, - static_cast(length), - m_isSlipRenderer ? halfBreadth : halfBreadth + 0.5f * devicePixelRatio); - m_colors.addForRectangle( - static_cast(m_axesColor_r), - static_cast(m_axesColor_g), - static_cast(m_axesColor_b)); + ((splitLeftRight && !m_isSlipRenderer ? pixelLength * 2 : pixelLength) + 1); + + geometry().setDrawingMode(Geometry::DrawingMode::Triangles); + geometry().allocate(reserved); + markDirtyGeometry(); + + RGBVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + vertexUpdater.addRectangle({0.f, + halfBreadth - 0.5f}, + {static_cast(length), + m_isSlipRenderer ? halfBreadth : halfBreadth + 0.5f}, + {static_cast(m_axesColor_r), + static_cast(m_axesColor_g), + static_cast(m_axesColor_b)}); const double maxSamplingRange = visualIncrementPerPixel / 2.0; - for (int pos = 0; pos < length; ++pos) { + for (int pos = 0; pos < pixelLength; ++pos) { const int visualFrameStart = std::lround(xVisualFrame - maxSamplingRange); const int visualFrameStop = std::lround(xVisualFrame + maxSamplingRange); @@ -130,7 +143,7 @@ void WaveformRendererRGB::paintGL() { const int visualIndexStop = std::min(std::max(visualFrameStop, visualFrameStart + 1) * 2, dataSize - 1); - const float fpos = static_cast(pos); + const float fpos = static_cast(pos) * invDevicePixelRatio; // Find the max values for low, mid, high and all in the waveform data. // - Max of left and right @@ -213,51 +226,36 @@ void WaveformRendererRGB::paintGL() { // Lines are thin rectangles if (!splitLeftRight) { - m_vertices.addRectangle(fpos - 0.5f, - halfBreadth - heightFactorAbs * maxAllChn[0], - fpos + 0.5f, - m_isSlipRenderer - ? halfBreadth - : halfBreadth + heightFactorAbs * maxAllChn[1]); + vertexUpdater.addRectangle({fpos - halfPixelSize, + halfBreadth - heightFactorAbs * maxAllChn[0]}, + {fpos + halfPixelSize, + m_isSlipRenderer + ? halfBreadth + : halfBreadth + heightFactorAbs * maxAllChn[1]}, + {red, + green, + blue}); } else { // note: heightFactor is the same for left and right, // but negative for left (chn 0) and positive for right (chn 1) - m_vertices.addRectangle(fpos - 0.5f, - halfBreadth, - fpos + 0.5f, - halfBreadth + heightFactor[chn] * maxAllChn[chn]); + vertexUpdater.addRectangle({fpos - halfPixelSize, + halfBreadth}, + {fpos + halfPixelSize, + halfBreadth + heightFactor[chn] * maxAllChn[chn]}, + {red, + green, + blue}); } - m_colors.addForRectangle(red, green, blue); } xVisualFrame += visualIncrementPerPixel; } - DEBUG_ASSERT(reserved == m_vertices.size()); - DEBUG_ASSERT(reserved == m_colors.size()); - - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, true); - - const int matrixLocation = m_shader.matrixLocation(); - const int positionLocation = m_shader.positionLocation(); - const int colorLocation = m_shader.colorLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); - m_shader.enableAttributeArray(colorLocation); - - m_shader.setUniformValue(matrixLocation, matrix); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, m_vertices.constData(), 2); - m_shader.setAttributeArray( - colorLocation, GL_FLOAT, m_colors.constData(), 3); + DEBUG_ASSERT(reserved == vertexUpdater.index()); - glDrawArrays(GL_TRIANGLES, 0, m_vertices.size()); + markDirtyMaterial(); - m_shader.disableAttributeArray(positionLocation); - m_shader.disableAttributeArray(colorLocation); - m_shader.release(); + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.h b/src/waveform/renderers/allshader/waveformrendererrgb.h index 10f2262a751..a829cf3af2f 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.h +++ b/src/waveform/renderers/allshader/waveformrendererrgb.h @@ -1,10 +1,7 @@ #pragma once -#include "rendergraph/openglnode.h" -#include "shaders/rgbshader.h" +#include "rendergraph/geometrynode.h" #include "util/class.h" -#include "waveform/renderers/allshader/rgbdata.h" -#include "waveform/renderers/allshader/vertexdata.h" #include "waveform/renderers/allshader/waveformrenderersignalbase.h" namespace allshader { @@ -13,30 +10,28 @@ class WaveformRendererRGB; class allshader::WaveformRendererRGB final : public allshader::WaveformRendererSignalBase, - public rendergraph::OpenGLNode { + public rendergraph::GeometryNode { public: explicit WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play, WaveformRendererSignalBase::Options options = WaveformRendererSignalBase::Option::None); - // override ::WaveformRendererSignalBase + // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; - void initializeGL() override; - void paintGL() override; - bool supportsSlip() const override { return true; } - private: - mixxx::RGBShader m_shader; - VertexData m_vertices; - RGBData m_colors; + // Virtuals for rendergraph::Node + void preprocess() override; + private: bool m_isSlipRenderer; WaveformRendererSignalBase::Options m_options; + bool preprocessInner(); + DISALLOW_COPY_AND_ASSIGN(WaveformRendererRGB); }; diff --git a/src/waveform/renderers/allshader/waveformrenderersimple.cpp b/src/waveform/renderers/allshader/waveformrenderersimple.cpp index 8cc06626f07..ea850eb5268 100644 --- a/src/waveform/renderers/allshader/waveformrenderersimple.cpp +++ b/src/waveform/renderers/allshader/waveformrenderersimple.cpp @@ -1,172 +1,162 @@ #include "waveform/renderers/allshader/waveformrenderersimple.h" +#include "rendergraph/material/rgbmaterial.h" +#include "rendergraph/vertexupdaters/rgbvertexupdater.h" #include "track/track.h" #include "util/math.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveform.h" +using namespace rendergraph; + namespace allshader { -WaveformRendererSimple::WaveformRendererSimple( - WaveformWidgetRenderer* waveformWidget) +WaveformRendererSimple::WaveformRendererSimple(WaveformWidgetRenderer* waveformWidget) : WaveformRendererSignalBase(waveformWidget) { + initForRectangles(0); + setUsePreprocess(true); } void WaveformRendererSimple::onSetup(const QDomNode&) { } -void WaveformRendererSimple::initializeGL() { - m_shader.init(); +void WaveformRendererSimple::preprocess() { + if (!preprocessInner()) { + if (geometry().vertexCount() != 0) { + geometry().allocate(0); + markDirtyGeometry(); + } + } } -void WaveformRendererSimple::paintGL() { +bool WaveformRendererSimple::preprocessInner() { TrackPointer pTrack = m_waveformRenderer->getTrackInfo(); + if (!pTrack) { - return; + return false; } ConstWaveformPointer waveform = pTrack->getWaveform(); if (waveform.isNull()) { - return; + return false; } const int dataSize = waveform->getDataSize(); if (dataSize <= 1) { - return; + return false; } const WaveformData* data = waveform->data(); if (data == nullptr) { - return; + return false; } +#ifdef __STEM__ + auto stemInfo = pTrack->getStemInfo(); + // If this track is a stem track, skip the rendering + if (!stemInfo.isEmpty() && waveform->hasStem()) { + return false; + } +#endif const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); - const int length = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const int length = static_cast(m_waveformRenderer->getLength()); + const int pixelLength = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const float invDevicePixelRatio = 1.f / devicePixelRatio; + const float halfPixelSize = 0.5 / devicePixelRatio; // Note that waveform refers to the visual waveform, not to audio samples. // // WaveformData* data contains the L and R waveform values interleaved. In the calculations // below, 'frame' refers to the index of such an L-R pair. const int visualFramesSize = dataSize / 2; - // Calculate the first and last frame to draw, from the normalized display position const double firstVisualFrame = m_waveformRenderer->getFirstDisplayedPosition() * visualFramesSize; const double lastVisualFrame = m_waveformRenderer->getLastDisplayedPosition() * visualFramesSize; - // Calculate the number of visual frames per horizontal pixel. + // Represents the # of visual frames per horizontal pixel. const double visualIncrementPerPixel = - (lastVisualFrame - firstVisualFrame) / static_cast(length); + (lastVisualFrame - firstVisualFrame) / static_cast(pixelLength); // Per-band gain from the EQ knobs. float allGain{1.0}; float bandGain[3] = {1.0, 1.0, 1.0}; getGains(&allGain, false, &bandGain[0], &bandGain[1], &bandGain[2]); - const float breadth = static_cast(m_waveformRenderer->getBreadth()) * devicePixelRatio; + const float breadth = static_cast(m_waveformRenderer->getBreadth()); const float halfBreadth = breadth / 2.0f; const float heightFactor = allGain * halfBreadth / m_maxValue; - // Effective visual frame for x, which we will increment for each pixel advanced + // Effective visual frame for x double xVisualFrame = qRound(firstVisualFrame / visualIncrementPerPixel) * visualIncrementPerPixel; const int numVerticesPerLine = 6; // 2 triangles - int reserved[2]; - - reserved[0] = numVerticesPerLine * length; - m_vertices[0].clear(); - m_vertices[0].reserve(reserved[0]); + const int reserved = numVerticesPerLine * (pixelLength + 1); - // the horizontal line - reserved[1] = numVerticesPerLine; - m_vertices[1].clear(); - m_vertices[1].reserve(reserved[1]); + geometry().setDrawingMode(Geometry::DrawingMode::Triangles); + geometry().allocate(reserved); + markDirtyGeometry(); - m_vertices[1].addRectangle( - 0.f, - halfBreadth - 0.5f * devicePixelRatio, - static_cast(length), - halfBreadth + 0.5f * devicePixelRatio); + RGBVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + vertexUpdater.addRectangle({0.f, + halfBreadth - 0.5f}, + {static_cast(length), + halfBreadth + 0.5f}, + {static_cast(m_axesColor_r), + static_cast(m_axesColor_g), + static_cast(m_axesColor_b)}); - // We will iterate over a range of waveform data, centered around xVisualFrame const double maxSamplingRange = visualIncrementPerPixel / 2.0; - for (int pos = 0; pos < length; ++pos) { - // Calculate the start and end of the range of waveform data, centered around xVisualFrame + const QVector3D signalColor{static_cast(m_signalColor_r), + static_cast(m_signalColor_g), + static_cast(m_signalColor_b)}; + + for (int pos = 0; pos < pixelLength; ++pos) { const int visualFrameStart = std::lround(xVisualFrame - maxSamplingRange); const int visualFrameStop = std::lround(xVisualFrame + maxSamplingRange); - // Calculate the actual (deinterleaved) indices. - // - // Make sure we stay inside data at the lower boundary const int visualIndexStart = std::max(visualFrameStart * 2, 0); - // and at the upper boundary. - // Note: * dataSize - 1, because below we add chn = 1 - // * visualFrameStart + 1, because we want to have at least 1 value const int visualIndexStop = std::min(std::max(visualFrameStop, visualFrameStart + 1) * 2, dataSize - 1); - // 2 channels - float max[2]{}; + const float fpos = static_cast(pos) * invDevicePixelRatio; + // - Per channel + uchar u8maxAllChn[2]{}; for (int chn = 0; chn < 2; chn++) { // data is interleaved left / right for (int i = visualIndexStart + chn; i < visualIndexStop + chn; i += 2) { const WaveformData& waveformData = data[i]; - const float filteredAll = static_cast(waveformData.filtered.all); - max[chn] = math_max(max[chn], filteredAll); + u8maxAllChn[chn] = math_max(u8maxAllChn[chn], waveformData.filtered.all); } } + float maxAllChn[2]{static_cast(u8maxAllChn[0]), static_cast(u8maxAllChn[1])}; - const float fpos = static_cast(pos); + // TODO: use two geometrynodes, with uniform material, + // one for the axis, one for the signal - // lines are thin rectangles - m_vertices[0].addRectangle( - fpos - 0.5f, - halfBreadth - heightFactor * max[0], - fpos + 0.5f, - halfBreadth + heightFactor * max[1]); + // note: heightFactor is the same for left and right, + // but negative for left (chn 0) and positive for right (chn 1) + vertexUpdater.addRectangle({fpos - halfPixelSize, + halfBreadth - heightFactor * maxAllChn[0]}, + {fpos + halfPixelSize, + halfBreadth + heightFactor * maxAllChn[0]}, + signalColor); xVisualFrame += visualIncrementPerPixel; } - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, true); - - const int matrixLocation = m_shader.matrixLocation(); - const int colorLocation = m_shader.colorLocation(); - const int positionLocation = m_shader.positionLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); + DEBUG_ASSERT(reserved == vertexUpdater.index()); - m_shader.setUniformValue(matrixLocation, matrix); - - QColor colors[2]; - colors[0].setRgbF(static_cast(m_signalColor_r), - static_cast(m_signalColor_g), - static_cast(m_signalColor_b)); - colors[1].setRgbF(static_cast(m_axesColor_r), - static_cast(m_axesColor_g), - static_cast(m_axesColor_b), - static_cast(m_axesColor_a)); - - for (int i = 0; i < 2; i++) { - DEBUG_ASSERT(reserved[i] == m_vertices[i].size()); - m_shader.setUniformValue(colorLocation, colors[i]); - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, m_vertices[i].constData(), 2); - - glDrawArrays(GL_TRIANGLES, 0, m_vertices[i].size()); - } + markDirtyMaterial(); - m_shader.disableAttributeArray(positionLocation); - m_shader.release(); + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrenderersimple.h b/src/waveform/renderers/allshader/waveformrenderersimple.h index ba79752539c..fe3cfb3f431 100644 --- a/src/waveform/renderers/allshader/waveformrenderersimple.h +++ b/src/waveform/renderers/allshader/waveformrenderersimple.h @@ -1,9 +1,7 @@ #pragma once -#include "rendergraph/openglnode.h" -#include "shaders/unicolorshader.h" +#include "rendergraph/geometrynode.h" #include "util/class.h" -#include "waveform/renderers/allshader/vertexdata.h" #include "waveform/renderers/allshader/waveformrenderersignalbase.h" namespace allshader { @@ -12,19 +10,18 @@ class WaveformRendererSimple; class allshader::WaveformRendererSimple final : public allshader::WaveformRendererSignalBase, - public rendergraph::OpenGLNode { + public rendergraph::GeometryNode { public: explicit WaveformRendererSimple(WaveformWidgetRenderer* waveformWidget); - // override ::WaveformRendererSignalBase + // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; - void initializeGL() override; - void paintGL() override; + // Virtuals for rendergraph::Node + void preprocess() override; private: - mixxx::UnicolorShader m_shader; - VertexData m_vertices[2]; + bool preprocessInner(); DISALLOW_COPY_AND_ASSIGN(WaveformRendererSimple); }; From 7b53e87323c154e8f5d22aeb46ae1a2760829b0d Mon Sep 17 00:00:00 2001 From: m0dB Date: Mon, 9 Dec 2024 20:49:34 +0100 Subject: [PATCH 05/22] use geometrynode for waveformrendererslipmode (cherry picked from commit dbe04f57adfd106a97e0e2ea9031d70a0bcc976a) --- .../allshader/waveformrendererslipmode.cpp | 130 ++++++++++-------- .../allshader/waveformrendererslipmode.h | 22 +-- 2 files changed, 87 insertions(+), 65 deletions(-) diff --git a/src/waveform/renderers/allshader/waveformrendererslipmode.cpp b/src/waveform/renderers/allshader/waveformrendererslipmode.cpp index 8ddf0be80e5..d548455a7dc 100644 --- a/src/waveform/renderers/allshader/waveformrendererslipmode.cpp +++ b/src/waveform/renderers/allshader/waveformrendererslipmode.cpp @@ -1,9 +1,14 @@ #include "waveform/renderers/allshader/waveformrendererslipmode.h" #include +#include #include #include "control/controlproxy.h" +#include "rendergraph/geometry.h" +#include "rendergraph/material/rgbamaterial.h" +#include "rendergraph/vertexupdaters/rgbavertexupdater.h" +#include "util/colorcomponents.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveformwidgetfactory.h" #include "widget/wskincolor.h" @@ -11,106 +16,119 @@ namespace { constexpr int kBlinkingPeriodMillis = 1600; -// Position matrix passed to OpenGL when drawing the shader -constexpr float positionArray[] = {-1.f, -1.f, 1.f, -1.f, -1.f, 1.f, 1.f, 1.f}; // Used as default outline color in case no value is provided in the theme - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -constexpr -#else -const -#endif - QColor kDefaultColor = QColor(224, 224, 224); +constexpr QColor kDefaultColor = QColor(224, 224, 224); } // anonymous namespace +using namespace rendergraph; + namespace allshader { WaveformRendererSlipMode::WaveformRendererSlipMode( WaveformWidgetRenderer* waveformWidget) - : WaveformRenderer(waveformWidget), + : ::WaveformRendererAbstract(waveformWidget), m_slipBorderTopOutlineSize(10.f), m_slipBorderBottomOutlineSize(10.f) { + initForRectangles(0); + setUsePreprocess(true); +} + +void WaveformRendererSlipMode::draw(QPainter* painter, QPaintEvent* event) { + Q_UNUSED(painter); + Q_UNUSED(event); + DEBUG_ASSERT(false); } bool WaveformRendererSlipMode::init() { m_timer.restart(); - m_pSlipMode = std::make_unique( - m_waveformRenderer->getGroup(), "slip_enabled"); + m_pSlipModeControl.reset(new ControlProxy( + m_waveformRenderer->getGroup(), "slip_enabled")); return true; } -void WaveformRendererSlipMode::setup(const QDomNode& node, const SkinContext& context) { - const QString slipModeOutlineColorName = context.selectString(node, "SlipBorderOutlineColor"); +void WaveformRendererSlipMode::setup(const QDomNode& node, const SkinContext& skinContext) { + const QString slipModeOutlineColorName = + skinContext.selectString(node, "SlipBorderOutlineColor"); if (!slipModeOutlineColorName.isNull()) { m_color = WSkinColor::getCorrectColor(QColor(slipModeOutlineColorName)); } else { m_color = kDefaultColor; } - const float slipBorderTopOutlineSize = context.selectFloat( + const float slipBorderTopOutlineSize = skinContext.selectFloat( node, "SlipBorderTopOutlineSize", m_slipBorderTopOutlineSize); if (slipBorderTopOutlineSize >= 0) { m_slipBorderTopOutlineSize = slipBorderTopOutlineSize; } - const float slipBorderBottomOutlineSize = context.selectFloat( + const float slipBorderBottomOutlineSize = skinContext.selectFloat( node, "SlipBorderBottomOutlineSize", m_slipBorderBottomOutlineSize); if (slipBorderBottomOutlineSize >= 0) { m_slipBorderBottomOutlineSize = slipBorderBottomOutlineSize; } } -void WaveformRendererSlipMode::initializeGL() { - m_shader.init(); +void WaveformRendererSlipMode::preprocess() { + if (!preprocessInner()) { + geometry().allocate(0); + markDirtyGeometry(); + } } -void WaveformRendererSlipMode::paintGL() { - if (!m_pSlipMode->toBool() || !m_waveformRenderer->isSlipActive()) { - return; +bool WaveformRendererSlipMode::preprocessInner() { + if (!m_pSlipModeControl->toBool() || !m_waveformRenderer->isSlipActive()) { + return false; } const int elapsed = m_timer.elapsed().toIntegerMillis() % kBlinkingPeriodMillis; - const double blinkIntensity = - static_cast(2 * abs(elapsed - kBlinkingPeriodMillis / 2)) / + const float blinkIntensity = + static_cast( + 2 * std::abs(elapsed - kBlinkingPeriodMillis / 2)) / kBlinkingPeriodMillis; - const double alpha = 0.25 + 0.5 * blinkIntensity; - - if (alpha != 0.0) { - QColor color = m_color; - color.setAlphaF(static_cast(alpha)); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - const int colorLocation = m_shader.colorLocation(); - const int borderLocation = m_shader.boarderLocation(); - const int positionLocation = m_shader.positionLocation(); - const int gradientLocation = m_shader.dimensionLocation(); + const float alpha = std::clamp(0.25 + 0.5 * blinkIntensity, 0.0, 1.0); + + const float posx1 = 0.f; + const float posx2 = m_waveformRenderer->getLength(); + const float posy1 = 0.f; + const float posy2 = m_waveformRenderer->getBreadth() / 2.f; + + const float sideBorderOutlineSide = m_slipBorderTopOutlineSize; + + const QVector4D bgColor{0.f, 0.f, 0.f, 1.f}; + const QVector4D borderColor{m_color.redF(), m_color.greenF(), m_color.blueF(), alpha}; + const QVector4D borderColor0{m_color.redF(), m_color.greenF(), m_color.blueF(), 0.f}; + + const int numVerticesPerLine = 6; // 2 triangles + geometry().allocate(numVerticesPerLine * 5); // border on 4 sides + bgColor filler in center + + RGBAVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + + vertexUpdater.addRectangle( + {posx1, posy1}, + {posx2, posy2}, + bgColor); + + vertexUpdater.addRectangle( + {posx1, posy1}, {posx2, posy1 + m_slipBorderTopOutlineSize}, borderColor); + vertexUpdater.addRectangle({posx1, posy1 + m_slipBorderTopOutlineSize}, + {posx1 + sideBorderOutlineSide, + posy2}, + borderColor); + vertexUpdater.addRectangle({posx2 - sideBorderOutlineSide, posy1 + m_slipBorderTopOutlineSize}, + {posx2, posy2}, + borderColor); + vertexUpdater.addRectangleVGradient({posx1, posy2}, + {posx2, posy2 + m_slipBorderBottomOutlineSize}, + borderColor, + borderColor0); + markDirtyGeometry(); + markDirtyMaterial(); - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); - - m_shader.setUniformValue(colorLocation, color); - m_shader.setUniformValue(borderLocation, - m_slipBorderTopOutlineSize, - m_slipBorderBottomOutlineSize); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, positionArray, 2); - - m_shader.setUniformValue(gradientLocation, - static_cast(m_waveformRenderer->getLength()) / 2, - static_cast(m_waveformRenderer->getBreadth()) / 2); - - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - m_shader.disableAttributeArray(positionLocation); - m_shader.release(); - } + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendererslipmode.h b/src/waveform/renderers/allshader/waveformrendererslipmode.h index b481142797f..345beb792dc 100644 --- a/src/waveform/renderers/allshader/waveformrendererslipmode.h +++ b/src/waveform/renderers/allshader/waveformrendererslipmode.h @@ -3,11 +3,11 @@ #include #include -#include "rendergraph/openglnode.h" -#include "shaders/slipmodeshader.h" +#include "rendergraph/geometrynode.h" +#include "rendergraph/opacitynode.h" #include "util/class.h" #include "util/performancetimer.h" -#include "waveform/renderers/allshader/waveformrenderer.h" +#include "waveform/renderers/waveformrendererabstract.h" class ControlProxy; class QDomNode; @@ -18,22 +18,24 @@ class WaveformRendererSlipMode; } class allshader::WaveformRendererSlipMode final - : public allshader::WaveformRenderer, - public rendergraph::OpenGLNode { + : public ::WaveformRendererAbstract, + public rendergraph::GeometryNode { public: explicit WaveformRendererSlipMode( WaveformWidgetRenderer* waveformWidget); + // Pure virtual from WaveformRendererAbstract, not used + void draw(QPainter* painter, QPaintEvent* event) override final; + void setup(const QDomNode& node, const SkinContext& skinContext) override; bool init() override; - void initializeGL() override; - void paintGL() override; + // Virtual for rendergraph::Node + void preprocess() override; private: - mixxx::SlipModeShader m_shader; - std::unique_ptr m_pSlipMode; + std::unique_ptr m_pSlipModeControl; float m_slipBorderTopOutlineSize; float m_slipBorderBottomOutlineSize; @@ -41,5 +43,7 @@ class allshader::WaveformRendererSlipMode final QColor m_color; PerformanceTimer m_timer; + bool preprocessInner(); + DISALLOW_COPY_AND_ASSIGN(WaveformRendererSlipMode); }; From ff325bdcacc2e691fe13c951948f7896b6b02818 Mon Sep 17 00:00:00 2001 From: m0dB Date: Mon, 9 Dec 2024 20:49:34 +0100 Subject: [PATCH 06/22] use geometrynode for waveformrendererstem (cherry picked from commit 3f4c1631756a09dacf1506bcea078007b7997623) --- .../allshader/waveformrendererstem.cpp | 126 +++++++++--------- .../allshader/waveformrendererstem.h | 32 +++-- 2 files changed, 80 insertions(+), 78 deletions(-) diff --git a/src/waveform/renderers/allshader/waveformrendererstem.cpp b/src/waveform/renderers/allshader/waveformrendererstem.cpp index c73e170d397..b6929da800a 100644 --- a/src/waveform/renderers/allshader/waveformrendererstem.cpp +++ b/src/waveform/renderers/allshader/waveformrendererstem.cpp @@ -5,13 +5,20 @@ #include #include "engine/engine.h" +#include "rendergraph/material/rgbamaterial.h" +#include "rendergraph/vertexupdaters/rgbavertexupdater.h" #include "track/track.h" +#include "util/assert.h" #include "util/math.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" -#include "waveform/renderers/allshader/rgbdata.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveform.h" +using namespace rendergraph; + +namespace { +constexpr int kMaxSupportedStems = 4; +} // anonymous namespace + namespace allshader { WaveformRendererStem::WaveformRendererStem( @@ -19,65 +26,82 @@ WaveformRendererStem::WaveformRendererStem( ::WaveformRendererAbstract::PositionSource type) : WaveformRendererSignalBase(waveformWidget), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { + initForRectangles(0); + setUsePreprocess(true); } void WaveformRendererStem::onSetup(const QDomNode&) { } -void WaveformRendererStem::initializeGL() { - m_shader.init(); - m_textureShader.init(); - auto group = m_pEQEnabled->getKey().group; - for (int stemIdx = 1; stemIdx <= mixxx::kMaxSupportedStems; stemIdx++) { +bool WaveformRendererStem::init() { + auto group = m_waveformRenderer->getGroup(); + VERIFY_OR_DEBUG_ASSERT(!group.isEmpty()) { + return false; + } + for (int stemIdx = 1; stemIdx <= kMaxSupportedStems; stemIdx++) { DEBUG_ASSERT(group.endsWith("]")); QString stemGroup = QStringLiteral("%1Stem%2]") .arg(group.left(group.size() - 1), QString::number(stemIdx)); m_pStemGain.emplace_back( - std::make_unique(stemGroup, + std::make_unique(stemGroup, QStringLiteral("volume"))); - m_pStemMute.emplace_back(std::make_unique( + m_pStemMute.emplace_back(std::make_unique( stemGroup, QStringLiteral("mute"))); } + return true; +} + +void WaveformRendererStem::preprocess() { + if (!preprocessInner()) { + if (geometry().vertexCount() != 0) { + geometry().allocate(0); + markDirtyGeometry(); + } + } } -void WaveformRendererStem::paintGL() { +bool WaveformRendererStem::preprocessInner() { TrackPointer pTrack = m_waveformRenderer->getTrackInfo(); + if (!pTrack || (m_isSlipRenderer && !m_waveformRenderer->isSlipActive())) { - return; + return false; } auto stemInfo = pTrack->getStemInfo(); // If this track isn't a stem track, skip the rendering if (stemInfo.isEmpty()) { - return; + return false; } auto positionType = m_isSlipRenderer ? ::WaveformRendererAbstract::Slip : ::WaveformRendererAbstract::Play; ConstWaveformPointer waveform = pTrack->getWaveform(); if (waveform.isNull()) { - return; + return false; } const int dataSize = waveform->getDataSize(); if (dataSize <= 1) { - return; + return false; } const WaveformData* data = waveform->data(); if (data == nullptr) { - return; + return false; } // If this waveform doesn't contain stem data, skip the rendering if (!waveform->hasStem()) { - return; + return false; } uint selectedStems = m_waveformRenderer->getSelectedStems(); const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); - const int length = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const int length = static_cast(m_waveformRenderer->getLength()); + const int pixelLength = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + const float invDevicePixelRatio = 1.f / devicePixelRatio; + const float halfPixelSize = 0.5 / devicePixelRatio; // See waveformrenderersimple.cpp for a detailed explanation of the frame and index calculation const int visualFramesSize = dataSize / 2; @@ -88,14 +112,14 @@ void WaveformRendererStem::paintGL() { // Represents the # of visual frames per horizontal pixel. const double visualIncrementPerPixel = - (lastVisualFrame - firstVisualFrame) / static_cast(length); + (lastVisualFrame - firstVisualFrame) / static_cast(pixelLength); // Per-band gain from the EQ knobs. float allGain(1.0); // applyCompensation = true, as we scale to match filtered.all getGains(&allGain, false, nullptr, nullptr, nullptr); - const float breadth = static_cast(m_waveformRenderer->getBreadth()) * devicePixelRatio; + const float breadth = static_cast(m_waveformRenderer->getBreadth()); const float halfBreadth = breadth / 2.0f; const float heightFactor = allGain * halfBreadth / m_maxValue; @@ -106,22 +130,22 @@ void WaveformRendererStem::paintGL() { const int numVerticesPerLine = 6; // 2 triangles - const int reserved = numVerticesPerLine * (8 * length + 1); + const int reserved = numVerticesPerLine * (8 * pixelLength + 1); - m_vertices.clear(); - m_vertices.reserve(reserved); - m_colors.clear(); - m_colors.reserve(reserved); + geometry().setDrawingMode(Geometry::DrawingMode::Triangles); + geometry().allocate(reserved); + markDirtyGeometry(); - m_vertices.addRectangle(0.f, - halfBreadth - 0.5f * devicePixelRatio, - static_cast(length), - m_isSlipRenderer ? halfBreadth : halfBreadth + 0.5f * devicePixelRatio); - m_colors.addForRectangle(0.f, 0.f, 0.f, 0.f); + RGBAVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + vertexUpdater.addRectangle({0.f, + halfBreadth - 0.5f}, + {static_cast(length), + m_isSlipRenderer ? halfBreadth : halfBreadth + 0.5f}, + {0.f, 0.f, 0.f, 0.f}); const double maxSamplingRange = visualIncrementPerPixel / 2.0; - for (int visualIdx = 0; visualIdx < length; ++visualIdx) { + for (int visualIdx = 0; visualIdx < pixelLength; ++visualIdx) { for (int stemIdx = 0; stemIdx < mixxx::kMaxSupportedStems; stemIdx++) { // Stem is drawn twice with different opacity level, this allow to // see the maximum signal by transparency @@ -138,7 +162,7 @@ void WaveformRendererStem::paintGL() { const int visualIndexStop = std::min(std::max(visualFrameStop, visualFrameStart + 1) * 2, dataSize - 1); - const float fVisualIdx = static_cast(visualIdx); + const float fVisualIdx = static_cast(visualIdx) * invDevicePixelRatio; // Find the max values for current eq in the waveform data. // - Max of left and right @@ -165,43 +189,23 @@ void WaveformRendererStem::paintGL() { } // Lines are thin rectangles - // shawdow - m_vertices.addRectangle(fVisualIdx - 0.5f, - halfBreadth - heightFactor * max, - fVisualIdx + 0.5f, - m_isSlipRenderer ? halfBreadth : halfBreadth + heightFactor * max); - - m_colors.addForRectangle(color_r, color_g, color_b, color_a); + // shadow + vertexUpdater.addRectangle({fVisualIdx - halfPixelSize, + halfBreadth - heightFactor * max}, + {fVisualIdx + halfPixelSize, + m_isSlipRenderer ? halfBreadth : halfBreadth + heightFactor * max}, + {color_r, color_g, color_b, color_a}); } } + xVisualFrame += visualIncrementPerPixel; } - DEBUG_ASSERT(reserved == m_vertices.size()); - DEBUG_ASSERT(reserved == m_colors.size()); - - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, true); - - const int matrixLocation = m_shader.matrixLocation(); - const int positionLocation = m_shader.positionLocation(); - const int colorLocation = m_shader.colorLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); - m_shader.enableAttributeArray(colorLocation); - - m_shader.setUniformValue(matrixLocation, matrix); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, m_vertices.constData(), 2); - m_shader.setAttributeArray( - colorLocation, GL_FLOAT, m_colors.constData(), 4); + DEBUG_ASSERT(reserved == vertexUpdater.index()); - glDrawArrays(GL_TRIANGLES, 0, m_vertices.size()); + markDirtyMaterial(); - m_shader.disableAttributeArray(positionLocation); - m_shader.disableAttributeArray(colorLocation); - m_shader.release(); + return true; } } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendererstem.h b/src/waveform/renderers/allshader/waveformrendererstem.h index 99728194574..1b8881d0cc6 100644 --- a/src/waveform/renderers/allshader/waveformrendererstem.h +++ b/src/waveform/renderers/allshader/waveformrendererstem.h @@ -2,12 +2,9 @@ #include -#include "rendergraph/openglnode.h" -#include "shaders/rgbashader.h" -#include "shaders/textureshader.h" +#include "control/pollingcontrolproxy.h" +#include "rendergraph/geometrynode.h" #include "util/class.h" -#include "waveform/renderers/allshader/rgbadata.h" -#include "waveform/renderers/allshader/vertexdata.h" #include "waveform/renderers/allshader/waveformrenderersignalbase.h" class QOpenGLTexture; @@ -18,30 +15,31 @@ class WaveformRendererStem; class allshader::WaveformRendererStem final : public allshader::WaveformRendererSignalBase, - public rendergraph::OpenGLNode { + public rendergraph::GeometryNode { public: explicit WaveformRendererStem(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play); - // override ::WaveformRendererSignalBase + // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; - void initializeGL() override; - void paintGL() override; + bool init() override; - private: - mixxx::RGBAShader m_shader; - mixxx::TextureShader m_textureShader; - VertexData m_vertices; - RGBAData m_colors; + bool supportsSlip() const override { + return true; + } + + // Virtuals for rendergraph::Node + void preprocess() override; + private: bool m_isSlipRenderer; - std::vector> m_pStemGain; - std::vector> m_pStemMute; + std::vector> m_pStemGain; + std::vector> m_pStemMute; - void drawTexture(float x, float y, QOpenGLTexture* texture); + bool preprocessInner(); DISALLOW_COPY_AND_ASSIGN(WaveformRendererStem); }; From c714626e1a98d3f50b86e4650eb642a19170199e Mon Sep 17 00:00:00 2001 From: m0dB Date: Mon, 9 Dec 2024 17:57:01 +0100 Subject: [PATCH 07/22] use geometrynodes for waveformrender markrange (cherry picked from commit 6ab1e003ec40681b8bb0c22ce92ed5973209f5e5) --- .../allshader/waveformrendermarkrange.cpp | 104 +++++++++--------- .../allshader/waveformrendermarkrange.h | 34 ++++-- .../widgets/allshader/waveformwidget.cpp | 2 + 3 files changed, 77 insertions(+), 63 deletions(-) diff --git a/src/waveform/renderers/allshader/waveformrendermarkrange.cpp b/src/waveform/renderers/allshader/waveformrendermarkrange.cpp index eaacffdbdba..1de0c3c88af 100644 --- a/src/waveform/renderers/allshader/waveformrendermarkrange.cpp +++ b/src/waveform/renderers/allshader/waveformrendermarkrange.cpp @@ -1,70 +1,46 @@ #include "waveform/renderers/allshader/waveformrendermarkrange.h" +#include "rendergraph/geometry.h" +#include "rendergraph/geometrynode.h" +#include "rendergraph/material/unicolormaterial.h" +#include "rendergraph/vertexupdaters/vertexupdater.h" #include "skin/legacy/skincontext.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" #include "waveform/renderers/waveformwidgetrenderer.h" -allshader::WaveformRenderMarkRange::WaveformRenderMarkRange(WaveformWidgetRenderer* waveformWidget) - : WaveformRenderer(waveformWidget) { -} - -void allshader::WaveformRenderMarkRange::initializeGL() { - m_shader.init(); -} - -void allshader::WaveformRenderMarkRange::fillRect( - const QRectF& rect, QColor color) { - const float posx1 = static_cast(rect.x()); - const float posx2 = static_cast(rect.x() + rect.width()); - const float posy1 = static_cast(rect.y()); - const float posy2 = static_cast(rect.y() + rect.height()); - - const float posarray[] = {posx1, posy1, posx2, posy1, posx1, posy2, posx2, posy2}; +using namespace rendergraph; - const int colorLocation = m_shader.colorLocation(); - const int positionLocation = m_shader.positionLocation(); +namespace allshader { - m_shader.setUniformValue(colorLocation, color); - - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, posarray, 2); - - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +WaveformRenderMarkRange::WaveformRenderMarkRange(WaveformWidgetRenderer* waveformWidget) + : ::WaveformRendererAbstract(waveformWidget) { } -void allshader::WaveformRenderMarkRange::setup(const QDomNode& node, const SkinContext& context) { +void WaveformRenderMarkRange::setup(const QDomNode& node, const SkinContext& skinContext) { m_markRanges.clear(); - m_markRanges.reserve(1); QDomNode child = node.firstChild(); while (!child.isNull()) { if (child.nodeName() == "MarkRange") { - m_markRanges.push_back( - WaveformMarkRange( - m_waveformRenderer->getGroup(), - child, - context, - *m_waveformRenderer->getWaveformSignalColors())); + addRange(WaveformMarkRange( + m_waveformRenderer->getGroup(), + child, + skinContext, + *m_waveformRenderer->getWaveformSignalColors())); } child = child.nextSibling(); } } -void allshader::WaveformRenderMarkRange::paintGL() { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, false); - - const int positionLocation = m_shader.positionLocation(); - const int matrixLocation = m_shader.matrixLocation(); - - m_shader.bind(); - m_shader.enableAttributeArray(positionLocation); +void WaveformRenderMarkRange::draw(QPainter* painter, QPaintEvent* event) { + Q_UNUSED(painter); + Q_UNUSED(event); + DEBUG_ASSERT(false); +} - m_shader.setUniformValue(matrixLocation, matrix); +void WaveformRenderMarkRange::update() { + GeometryNode* pChild = static_cast(firstChild()); - for (auto&& markRange : m_markRanges) { + for (const auto& markRange : m_markRanges) { // If the mark range is not active we should not draw it. if (!markRange.active()) { continue; @@ -88,8 +64,6 @@ void allshader::WaveformRenderMarkRange::paintGL() { startPosition = std::floor(startPosition); endPosition = std::floor(endPosition); - const double span = std::max(endPosition - startPosition, 1.0); - // range not in the current display if (startPosition > m_waveformRenderer->getLength() || endPosition < 0) { continue; @@ -98,8 +72,36 @@ void allshader::WaveformRenderMarkRange::paintGL() { QColor color = markRange.enabled() ? markRange.m_activeColor : markRange.m_disabledColor; color.setAlphaF(0.3f); - fillRect(QRectF(startPosition, 0, span, m_waveformRenderer->getBreadth()), color); + if (!pChild) { + auto pNode = std::make_unique(); + pChild = pNode.get(); + pChild->initForRectangles(1); + appendChildNode(std::move(pNode)); + } + + updateNode(pChild, + color, + {static_cast(startPosition), 0.f}, + {static_cast(endPosition) + 1.f, + static_cast(m_waveformRenderer->getBreadth())}); + + pChild = static_cast(pChild->nextSibling()); } - m_shader.disableAttributeArray(positionLocation); - m_shader.release(); + while (pChild) { + auto pNode = detachChildNode(pChild); + pChild = static_cast(pChild->nextSibling()); + } +} + +void WaveformRenderMarkRange::updateNode(GeometryNode* pChild, + QColor color, + QVector2D lt, + QVector2D rb) { + VertexUpdater vertexUpdater{pChild->geometry().vertexDataAs()}; + vertexUpdater.addRectangle(lt, rb); + pChild->material().setUniform(1, color); + pChild->markDirtyGeometry(); + pChild->markDirtyMaterial(); } + +} // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrendermarkrange.h b/src/waveform/renderers/allshader/waveformrendermarkrange.h index ac2cb7ac24b..140eca76c34 100644 --- a/src/waveform/renderers/allshader/waveformrendermarkrange.h +++ b/src/waveform/renderers/allshader/waveformrendermarkrange.h @@ -1,36 +1,46 @@ #pragma once #include -#include +#include -#include "rendergraph/openglnode.h" -#include "shaders/unicolorshader.h" +#include "rendergraph/node.h" #include "util/class.h" -#include "waveform/renderers/allshader/waveformrenderer.h" #include "waveform/renderers/waveformmarkrange.h" +#include "waveform/renderers/waveformrendererabstract.h" class QDomNode; class SkinContext; +namespace rendergraph { +class GeometryNode; +} // namespace rendergraph + namespace allshader { class WaveformRenderMarkRange; -} +} // namespace allshader -class allshader::WaveformRenderMarkRange final - : public allshader::WaveformRenderer, - public rendergraph::OpenGLNode { +class allshader::WaveformRenderMarkRange final : public ::WaveformRendererAbstract, + public rendergraph::Node { public: explicit WaveformRenderMarkRange(WaveformWidgetRenderer* waveformWidget); + void addRange(WaveformMarkRange&& range) { + m_markRanges.push_back(std::move(range)); + } + + // Pure virtual from WaveformRendererAbstract, not used + void draw(QPainter* painter, QPaintEvent* event) override final; + void setup(const QDomNode& node, const SkinContext& skinContext) override; - void initializeGL() override; - void paintGL() override; + void update(); private: - void fillRect(const QRectF& rect, QColor color); + void updateNode(rendergraph::GeometryNode* pChild, + QColor color, + QVector2D lt, + QVector2D rb); - mixxx::UnicolorShader m_shader; std::vector m_markRanges; DISALLOW_COPY_AND_ASSIGN(WaveformRenderMarkRange); diff --git a/src/waveform/widgets/allshader/waveformwidget.cpp b/src/waveform/widgets/allshader/waveformwidget.cpp index 035863716f3..8074c294cfa 100644 --- a/src/waveform/widgets/allshader/waveformwidget.cpp +++ b/src/waveform/widgets/allshader/waveformwidget.cpp @@ -146,6 +146,8 @@ void WaveformWidget::paintGL() { // opacity of 0.f effectively skips the subtree rendering m_pOpacityNode->setOpacity(shouldOnlyDrawBackground() ? 0.f : 1.f); + m_pWaveformRenderMarkRange->update(); + m_pEngine->preprocess(); m_pEngine->render(); } From d2c26cbff74f16a114c63280abb0889f0463654a Mon Sep 17 00:00:00 2001 From: m0dB Date: Mon, 9 Dec 2024 18:08:06 +0100 Subject: [PATCH 08/22] use geometrynodes for waveformrendermark (cherry picked from commit 97db5c762567bfd83470c3c53eb0455b3914a620) --- .../renderers/allshader/digitsrenderer.cpp | 126 +++--- .../renderers/allshader/digitsrenderer.h | 44 +- .../allshader/waveformrendermark.cpp | 403 ++++++++++-------- .../renderers/allshader/waveformrendermark.h | 54 ++- src/waveform/renderers/waveformmark.h | 2 +- src/waveform/renderers/waveformmarkrange.h | 2 +- .../renderers/waveformrendermarkbase.cpp | 4 + .../renderers/waveformrendermarkbase.h | 2 + .../widgets/allshader/waveformwidget.cpp | 1 + 9 files changed, 361 insertions(+), 277 deletions(-) diff --git a/src/waveform/renderers/allshader/digitsrenderer.cpp b/src/waveform/renderers/allshader/digitsrenderer.cpp index 212613c34c8..9e4b56e556d 100644 --- a/src/waveform/renderers/allshader/digitsrenderer.cpp +++ b/src/waveform/renderers/allshader/digitsrenderer.cpp @@ -5,18 +5,21 @@ #include #include #include -#include #include #include #include #include "util/assert.h" #include "util/roundtopixel.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" -#include "waveform/renderers/allshader/vertexdata.h" +#include "rendergraph/context.h" +#include "rendergraph/geometry.h" +#include "rendergraph/material/texturematerial.h" +#include "rendergraph/vertexupdaters/texturedvertexupdater.h" // Render digits using a texture (generated) with digits with blurred dark outline +using namespace rendergraph; + namespace { // The texture will contain 12 characters: 10 digits, colon and dot @@ -57,23 +60,22 @@ static_assert(checkCharToIndex()); } // namespace -allshader::DigitsRenderer::~DigitsRenderer() = default; - -void allshader::DigitsRenderer::init() { - initializeOpenGLFunctions(); - m_shader.init(); +allshader::DigitsRenderNode::DigitsRenderNode() { + setGeometry(std::make_unique(TextureMaterial::attributes(), 0)); + setMaterial(std::make_unique()); + geometry().setDrawingMode(Geometry::DrawingMode::Triangles); } -float allshader::DigitsRenderer::height() const { +allshader::DigitsRenderNode::~DigitsRenderNode() = default; + +float allshader::DigitsRenderNode::height() const { return m_height; } -void allshader::DigitsRenderer::updateTexture( - float fontPointSize, float maxHeight, float devicePixelRatio) { - if (std::lround(maxHeight * devicePixelRatio) <= 0) { - return; - } - +void allshader::DigitsRenderNode::updateTexture(rendergraph::Context* pContext, + float fontPointSize, + float maxHeight, + float devicePixelRatio) { if (fontPointSize == m_fontPointSize && maxHeight == m_maxHeight) { return; } @@ -184,12 +186,12 @@ void allshader::DigitsRenderer::updateTexture( blur->setBlurRadius(static_cast(m_penWidth) / 3); QGraphicsScene scene; - auto item = std::make_unique(); - item->setPixmap(QPixmap::fromImage(image)); - item->setGraphicsEffect(blur.release()); + QGraphicsPixmapItem item; + item.setPixmap(QPixmap::fromImage(image)); + item.setGraphicsEffect(blur.release()); image.fill(Qt::transparent); QPainter painter(&image); - scene.addItem(item.release()); + scene.addItem(&item); scene.render(&painter, QRectF(), QRectF(0, 0, image.width(), image.height())); } @@ -210,64 +212,66 @@ void allshader::DigitsRenderer::updateTexture( painter.drawPath(path); } - m_texture.setData(image); + dynamic_cast(material()) + .setTexture(std::make_unique(pContext, image)); } -float allshader::DigitsRenderer::draw(const QMatrix4x4& matrix, +void allshader::DigitsRenderNode::update( + float x, + float y, + bool multiLine, + const QString& s1, + const QString& s2) { + const int numVerticesPerRectangle = 6; + const int reserved = (s1.length() + s2.length()) * numVerticesPerRectangle; + geometry().allocate(reserved); + TexturedVertexUpdater vertexUpdater{geometry().vertexDataAs()}; + + const float ch = height(); + if (!s1.isEmpty()) { + const auto w = addVertices(vertexUpdater, + x, + y, + s1); + if (multiLine) { + y += ch; + } else { + x += w + ch * 0.75f; + } + } + if (!s2.isEmpty()) { + addVertices(vertexUpdater, + x, + y, + s2); + } + + DEBUG_ASSERT(reserved == vertexUpdater.index()); +} + +void allshader::DigitsRenderNode::clear() { + geometry().allocate(0); +} + +float allshader::DigitsRenderNode::addVertices(TexturedVertexUpdater& vertexUpdater, float x, float y, const QString& s) { - const int n = s.length(); const float x0 = x; const float space = static_cast(m_penWidth) / 2; - VertexData posVertices; - VertexData texVertices; - - posVertices.reserve(n * 6); // two triangles per character - texVertices.reserve(n * 6); - for (QChar c : s) { if (x != x0) { x -= space; } int index = charToIndex(c); - texVertices.addRectangle(m_offset[index], 0.f, m_offset[index + 1], 1.f); - posVertices.addRectangle(x, - y, - x + m_width[index], - y + height()); + vertexUpdater.addRectangle({x, y}, + {x + m_width[index], y + height()}, + {m_offset[index], 0.f}, + {m_offset[index + 1], 1.f}); x += m_width[index]; } - m_shader.bind(); - - const int matrixLocation = m_shader.uniformLocation("matrix"); - const int textureLocation = m_shader.uniformLocation("texture"); - const int positionLocation = m_shader.attributeLocation("position"); - const int texcoordLocation = m_shader.attributeLocation("texcoord"); - - m_shader.setUniformValue(matrixLocation, matrix); - - m_shader.enableAttributeArray(positionLocation); - m_shader.setAttributeArray( - positionLocation, GL_FLOAT, posVertices.constData(), 2); - m_shader.enableAttributeArray(texcoordLocation); - m_shader.setAttributeArray( - texcoordLocation, GL_FLOAT, texVertices.constData(), 2); - - m_shader.setUniformValue(textureLocation, 0); - - m_texture.bind(); - - glDrawArrays(GL_TRIANGLES, 0, posVertices.size()); - - m_texture.release(); - - m_shader.disableAttributeArray(positionLocation); - m_shader.disableAttributeArray(texcoordLocation); - m_shader.release(); - return x - x0; } diff --git a/src/waveform/renderers/allshader/digitsrenderer.h b/src/waveform/renderers/allshader/digitsrenderer.h index 4d280bbdaad..9e7c1884e45 100644 --- a/src/waveform/renderers/allshader/digitsrenderer.h +++ b/src/waveform/renderers/allshader/digitsrenderer.h @@ -1,30 +1,44 @@ #pragma once -#include +#include "rendergraph/context.h" +#include "rendergraph/geometrynode.h" +#include "util/class.h" -#include "shaders/textureshader.h" -#include "util/opengltexture2d.h" +namespace rendergraph { +class TexturedVertexUpdater; +} // namespace rendergraph namespace allshader { -class DigitsRenderer; -} +class DigitsRenderNode; +} // namespace allshader -class allshader::DigitsRenderer : public QOpenGLFunctions { +class allshader::DigitsRenderNode : public rendergraph::GeometryNode { public: - DigitsRenderer() = default; - ~DigitsRenderer(); + DigitsRenderNode(); + ~DigitsRenderNode(); - void init(); - void updateTexture(float fontPointSize, float maxHeight, float devicePixelRatio); - float draw(const QMatrix4x4& matrix, + void updateTexture(rendergraph::Context* pContext, + float fontPointSize, + float maxHeight, + float devicePixelRatio); + + void update( float x, float y, - const QString& s); + bool multiLine, + const QString& s1, + const QString& s2); + + void clear(); + float height() const; private: - mixxx::TextureShader m_shader; - OpenGLTexture2D m_texture; + float addVertices(rendergraph::TexturedVertexUpdater& vertexUpdater, + float x, + float y, + const QString& s); + int m_penWidth; float m_offset[13]; float m_width[12]; @@ -32,5 +46,5 @@ class allshader::DigitsRenderer : public QOpenGLFunctions { float m_height{}; float m_maxHeight{}; float m_adjustedFontPointSize{}; - DISALLOW_COPY_AND_ASSIGN(DigitsRenderer); + DISALLOW_COPY_AND_ASSIGN(DigitsRenderNode); }; diff --git a/src/waveform/renderers/allshader/waveformrendermark.cpp b/src/waveform/renderers/allshader/waveformrendermark.cpp index 2f85b24149c..35796ad8e9a 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.cpp +++ b/src/waveform/renderers/allshader/waveformrendermark.cpp @@ -1,17 +1,24 @@ #include "waveform/renderers/allshader/waveformrendermark.h" -#include #include +#include "rendergraph/context.h" +#include "rendergraph/geometry.h" +#include "rendergraph/geometrynode.h" +#include "rendergraph/material/rgbamaterial.h" +#include "rendergraph/material/texturematerial.h" +#include "rendergraph/texture.h" +#include "rendergraph/vertexupdaters/rgbavertexupdater.h" +#include "rendergraph/vertexupdaters/texturedvertexupdater.h" #include "track/track.h" #include "util/colorcomponents.h" #include "util/roundtopixel.h" -#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" -#include "waveform/renderers/allshader/rgbadata.h" -#include "waveform/renderers/allshader/vertexdata.h" +#include "waveform/renderers/allshader/digitsrenderer.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveformwidgetfactory.h" +using namespace rendergraph; + // On the use of QPainter: // // The renderers in this folder are optimized to use GLSL shaders and refrain @@ -23,17 +30,80 @@ namespace { -class TextureGraphics : public WaveformMark::Graphics { +class WaveformMarkNode : public rendergraph::GeometryNode { public: - TextureGraphics(const QImage& image) { - m_texture.setData(image); + WaveformMark* m_pOwner{}; + + WaveformMarkNode(WaveformMark* pOwner, rendergraph::Context* pContext, const QImage& image) + : m_pOwner(pOwner) { + initForRectangles(1); + updateTexture(pContext, image); + } + void updateTexture(rendergraph::Context* pContext, const QImage& image) { + dynamic_cast(material()) + .setTexture(std::make_unique(pContext, image)); + m_textureWidth = image.width(); + m_textureHeight = image.height(); + } + void update(float x, float y, float devicePixelRatio) { + [[maybe_unused]] const float epsilon = 1e-6f; + DEBUG_ASSERT(std::abs(x - RoundToPixel(devicePixelRatio)(x)) < epsilon); + DEBUG_ASSERT(std::abs(y - RoundToPixel(devicePixelRatio)(y)) < epsilon); + TexturedVertexUpdater vertexUpdater{ + geometry().vertexDataAs()}; + vertexUpdater.addRectangle({x, y}, + {x + m_textureWidth / devicePixelRatio, + y + m_textureHeight / devicePixelRatio}, + {0.f, 0.f}, + {1.f, 1.f}); } - QOpenGLTexture* texture() { - return &m_texture; + float textureWidth() const { + return m_textureWidth; + } + float textureHeight() const { + return m_textureHeight; + } + + public: + float m_textureWidth{}; + float m_textureHeight{}; +}; + +class WaveformMarkNodeGraphics : public WaveformMark::Graphics { + public: + WaveformMarkNodeGraphics(WaveformMark* pOwner, + rendergraph::Context* pContext, + const QImage& image) + : m_pNode(std::make_unique( + pOwner, pContext, image)) { + } + void updateTexture(rendergraph::Context* pContext, const QImage& image) { + waveformMarkNode()->updateTexture(pContext, image); + } + void update(float x, float y, float devicePixelRatio) { + waveformMarkNode()->update(x, y, devicePixelRatio); + } + float textureWidth() const { + return waveformMarkNode()->textureWidth(); + } + float textureHeight() const { + return waveformMarkNode()->textureHeight(); + } + void attachNode(std::unique_ptr pNode) { + DEBUG_ASSERT(!m_pNode); + m_pNode = std::move(pNode); + } + std::unique_ptr detachNode() { + return std::move(m_pNode); } private: - OpenGLTexture2D m_texture; + WaveformMarkNode* waveformMarkNode() const { + DEBUG_ASSERT(m_pNode); + return static_cast(m_pNode.get()); + } + + std::unique_ptr m_pNode; }; constexpr float kPlayPosWidth{11.f}; @@ -70,87 +140,49 @@ allshader::WaveformRenderMark::WaveformRenderMark( m_beatsUntilMark(0), m_timeUntilMark(0.0), m_pTimeRemainingControl(nullptr), - m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { -} + m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), + m_playPosHeight(0.f), + m_playPosDevicePixelRatio(0.f) { + { + auto pNode = std::make_unique(); + m_pRangeNodesParent = pNode.get(); + appendChildNode(std::move(pNode)); + } -bool allshader::WaveformRenderMark::init() { - m_pTimeRemainingControl = std::make_unique( - m_waveformRenderer->getGroup(), "time_remaining"); - return true; + { + auto pNode = std::make_unique(); + m_pMarkNodesParent = pNode.get(); + appendChildNode(std::move(pNode)); + } + + { + auto pNode = std::make_unique(); + m_pDigitsRenderNode = pNode.get(); + appendChildNode(std::move(pNode)); + } + + { + auto pNode = std::make_unique(); + m_pPlayPosNode = pNode.get(); + m_pPlayPosNode->initForRectangles(1); + appendChildNode(std::move(pNode)); + } } void allshader::WaveformRenderMark::draw(QPainter*, QPaintEvent*) { DEBUG_ASSERT(false); } -void allshader::WaveformRenderMark::initializeGL() { - m_digitsRenderer.init(); - m_rgbaShader.init(); - m_textureShader.init(); - - // Will create textures so requires OpenGL context - updateMarkImages(); - updatePlayPosMarkTexture(); - const auto untilMarkTextPointSize = - WaveformWidgetFactory::instance()->getUntilMarkTextPointSize(); - const auto untilMarkTextHeightLimit = - WaveformWidgetFactory::instance() - ->getUntilMarkTextHeightLimit(); // proportion of waveform - // height - const auto untilMarkMaxHeightForText = getMaxHeightForText(untilMarkTextHeightLimit); - - m_digitsRenderer.updateTexture(untilMarkTextPointSize, - untilMarkMaxHeightForText, - m_waveformRenderer->getDevicePixelRatio()); -} - -void allshader::WaveformRenderMark::drawTexture( - const QMatrix4x4& matrix, float x, float y, QOpenGLTexture* pTexture) { - const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); - const float texx1 = 0.f; - const float texy1 = 0.f; - const float texx2 = 1.f; - const float texy2 = 1.f; - - const float posx1 = x; - const float posx2 = x + static_cast(pTexture->width() / devicePixelRatio); - const float posy1 = y; - const float posy2 = y + static_cast(pTexture->height() / devicePixelRatio); - - const float posarray[] = {posx1, posy1, posx2, posy1, posx1, posy2, posx2, posy2}; - const float texarray[] = {texx1, texy1, texx2, texy1, texx1, texy2, texx2, texy2}; - - m_textureShader.bind(); - - const int matrixLocation = m_textureShader.uniformLocation("matrix"); - const int textureLocation = m_textureShader.uniformLocation("texture"); - const int positionLocation = m_textureShader.attributeLocation("position"); - const int texcoordLocation = m_textureShader.attributeLocation("texcoord"); - - m_textureShader.setUniformValue(matrixLocation, matrix); - - m_textureShader.enableAttributeArray(positionLocation); - m_textureShader.setAttributeArray( - positionLocation, GL_FLOAT, posarray, 2); - m_textureShader.enableAttributeArray(texcoordLocation); - m_textureShader.setAttributeArray( - texcoordLocation, GL_FLOAT, texarray, 2); - - m_textureShader.setUniformValue(textureLocation, 0); - - pTexture->bind(); - - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - pTexture->release(); - - m_textureShader.disableAttributeArray(positionLocation); - m_textureShader.disableAttributeArray(texcoordLocation); - m_textureShader.release(); +bool allshader::WaveformRenderMark::init() { + m_pTimeRemainingControl = std::make_unique( + m_waveformRenderer->getGroup(), "time_remaining"); + ::WaveformRenderMarkBase::init(); + return true; } -void allshader::WaveformRenderMark::drawMark( - const QMatrix4x4& matrix, const QRectF& rect, QColor color) { +void allshader::WaveformRenderMark::updateRangeNode(GeometryNode* pNode, + const QRectF& rect, + QColor color) { // draw a gradient towards transparency at the upper and lower 25% of the waveform view const float qh = static_cast(std::floor(rect.height() * 0.25)); @@ -165,43 +197,41 @@ void allshader::WaveformRenderMark::drawMark( getRgbF(color, &r, &g, &b, &a); - VertexData vertices; - vertices.reserve(12); // 4 triangles - vertices.addRectangle(posx1, posy1, posx2, posy2); - vertices.addRectangle(posx1, posy4, posx2, posy3); - - RGBAData rgbaData; - rgbaData.reserve(12); // 4 triangles - rgbaData.addForRectangleGradient(r, g, b, a, r, g, b, 0.f); - rgbaData.addForRectangleGradient(r, g, b, a, r, g, b, 0.f); - - m_rgbaShader.bind(); - - const int matrixLocation = m_rgbaShader.matrixLocation(); - const int positionLocation = m_rgbaShader.positionLocation(); - const int colorLocation = m_rgbaShader.colorLocation(); - - m_rgbaShader.setUniformValue(matrixLocation, matrix); - - m_rgbaShader.enableAttributeArray(positionLocation); - m_rgbaShader.setAttributeArray( - positionLocation, GL_FLOAT, vertices.constData(), 2); - m_rgbaShader.enableAttributeArray(colorLocation); - m_rgbaShader.setAttributeArray( - colorLocation, GL_FLOAT, rgbaData.constData(), 4); - - glDrawArrays(GL_TRIANGLES, 0, vertices.size()); + RGBAVertexUpdater vertexUpdater{pNode->geometry().vertexDataAs()}; + vertexUpdater.addRectangleVGradient( + {posx1, posy1}, {posx2, posy2}, {r, g, b, a}, {r, g, b, 0.f}); + vertexUpdater.addRectangleVGradient( + {posx1, posy4}, {posx2, posy3}, {r, g, b, a}, {r, g, b, 0.f}); +} - m_rgbaShader.disableAttributeArray(positionLocation); - m_rgbaShader.disableAttributeArray(colorLocation); - m_rgbaShader.release(); +bool allshader::WaveformRenderMark::isSubtreeBlocked() const { + return m_isSlipRenderer && !m_waveformRenderer->isSlipActive(); } -void allshader::WaveformRenderMark::paintGL() { - if (m_isSlipRenderer && !m_waveformRenderer->isSlipActive()) { +void allshader::WaveformRenderMark::update() { + if (isSubtreeBlocked()) { return; } + // For each WaveformMark we create a GeometryNode with Texture + // (in updateMarkImage). Of these GeometryNodes, we append the + // the ones that need to be shown on screen as children to + // m_pMarkNodesParent (transferring ownership). + // + // At the beginning of a new frame, we remove all the child nodes + // from m_pMarkNodesParent and store each with their mark + // (transferring ownership). Later in this function we move the + // visible nodes back to m_pMarkNodesParent children. + while (auto pChild = m_pMarkNodesParent->firstChild()) { + auto pNode = m_pMarkNodesParent->detachChildNode(pChild); + WaveformMarkNode* pWaveformMarkNode = static_cast(pNode.get()); + // Determine its WaveformMark + auto pMark = pWaveformMarkNode->m_pOwner; + auto pGraphics = static_cast(pMark->m_pGraphics.get()); + // Store the node with the WaveformMark + pGraphics->attachNode(std::move(pNode)); + } + auto positionType = m_isSlipRenderer ? ::WaveformRendererAbstract::Slip : ::WaveformRendererAbstract::Play; bool slipActive = m_waveformRenderer->isSlipActive(); @@ -209,23 +239,25 @@ void allshader::WaveformRenderMark::paintGL() { const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); QList marksOnScreen; - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - auto roundToPixel = createFunctionRoundToPixel(devicePixelRatio); for (const auto& pMark : std::as_const(m_marks)) { pMark->setBreadth(slipActive ? m_waveformRenderer->getBreadth() / 2 : m_waveformRenderer->getBreadth()); } - // Will create textures so requires OpenGL context - updateMarkImages(); - QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, false); + updatePlayPosMarkTexture(m_waveformRenderer->getContext()); + + // Generate initial node or update its texture if needed for each of + // the WaveformMarks (in which case updateMarkImage is called) + // (Will create textures so requires OpenGL context) + updateMarkImages(); const double playPosition = m_waveformRenderer->getTruePosSample(positionType); double nextMarkPosition = std::numeric_limits::max(); + GeometryNode* pRangeChild = static_cast(m_pRangeNodesParent->firstChild()); + for (const auto& pMark : std::as_const(m_marks)) { if (!pMark->isValid()) { continue; @@ -237,18 +269,10 @@ void allshader::WaveformRenderMark::paintGL() { continue; } - QOpenGLTexture* pTexture = - static_cast(pMark->m_pGraphics.get()) - ->texture(); - - if (!pTexture) { - continue; - } - - if (!pTexture->isCreated()) { - // This happens if the height is zero + auto pMarkGraphics = pMark->m_pGraphics.get(); + auto pMarkNodeGraphics = static_cast(pMarkGraphics); + if (!pMarkGraphics) // is this even possible? continue; - } const float currentMarkPos = static_cast( m_waveformRenderer->transformSamplePositionInRendererWorld( @@ -260,19 +284,23 @@ void allshader::WaveformRenderMark::paintGL() { } const double sampleEndPosition = pMark->getSampleEndPosition(); - const float markWidth = pTexture->width() / devicePixelRatio; + const float markWidth = pMarkNodeGraphics->textureWidth() / devicePixelRatio; const float drawOffset = currentMarkPos + pMark->getOffset(); bool visible = false; // Check if the current point needs to be displayed. if (drawOffset > -markWidth && drawOffset < m_waveformRenderer->getLength()) { - drawTexture(matrix, + pMarkNodeGraphics->update( roundToPixel(drawOffset), !m_isSlipRenderer && slipActive ? roundToPixel(m_waveformRenderer->getBreadth() / 2.f) : 0, - pTexture); + devicePixelRatio); + + // transfer back to m_pMarkNodesParent children, for rendering + m_pMarkNodesParent->appendChildNode(pMarkNodeGraphics->detachNode()); + visible = true; } @@ -286,13 +314,22 @@ void allshader::WaveformRenderMark::paintGL() { QColor color = pMark->fillColor(); color.setAlphaF(0.4f); - drawMark(matrix, - QRectF(QPointF(roundToPixel(currentMarkPos), 0), + // Reuse, or create new when needed + if (!pRangeChild) { + auto pNode = std::make_unique(); + pNode->initForRectangles(2); + pRangeChild = pNode.get(); + m_pRangeNodesParent->appendChildNode(std::move(pNode)); + } + + updateRangeNode(pRangeChild, + QRectF(QPointF(roundToPixel(currentMarkPos), 0.f), QPointF(roundToPixel(currentMarkEndPos), - m_waveformRenderer - ->getBreadth())), + roundToPixel(m_waveformRenderer->getBreadth()))), color); + visible = true; + pRangeChild = static_cast(pRangeChild->nextSibling()); } } @@ -302,24 +339,36 @@ void allshader::WaveformRenderMark::paintGL() { pMark, static_cast(drawOffset)}); } } + + // Remove unused nodes + while (pRangeChild) { + auto pNode = m_pRangeNodesParent->detachChildNode(pRangeChild); + pRangeChild = static_cast(pRangeChild->nextSibling()); + } + m_waveformRenderer->setMarkPositions(marksOnScreen); const float playMarkerPos = static_cast(m_waveformRenderer->getPlayMarkerPosition() * m_waveformRenderer->getLength()); - if (m_playPosMarkTexture.isStorageAllocated()) { + { const float drawOffset = roundToPixel(playMarkerPos + kPlayPosOffset); - - drawTexture(matrix, drawOffset, 0.f, &m_playPosMarkTexture); + TexturedVertexUpdater vertexUpdater{ + m_pPlayPosNode->geometry() + .vertexDataAs()}; + vertexUpdater.addRectangle({drawOffset, 0.f}, + {drawOffset + kPlayPosWidth, static_cast(m_waveformRenderer->getBreadth())}, + {0.f, 0.f}, + {1.f, 1.f}); } if (WaveformWidgetFactory::instance()->getUntilMarkShowBeats() || WaveformWidgetFactory::instance()->getUntilMarkShowTime()) { updateUntilMark(playPosition, nextMarkPosition); - drawUntilMark(matrix, roundToPixel(playMarkerPos + 20.f)); + updateDigitsNodeForUntilMark(roundToPixel(playMarkerPos + 20.f)); } } -void allshader::WaveformRenderMark::drawUntilMark(const QMatrix4x4& matrix, float x) { +void allshader::WaveformRenderMark::updateDigitsNodeForUntilMark(float x) { const bool untilMarkShowBeats = WaveformWidgetFactory::instance()->getUntilMarkShowBeats(); const bool untilMarkShowTime = WaveformWidgetFactory::instance()->getUntilMarkShowTime(); const auto untilMarkAlign = WaveformWidgetFactory::instance()->getUntilMarkAlign(); @@ -332,14 +381,16 @@ void allshader::WaveformRenderMark::drawUntilMark(const QMatrix4x4& matrix, floa // height const auto untilMarkMaxHeightForText = getMaxHeightForText(untilMarkTextHeightLimit); - m_digitsRenderer.updateTexture(untilMarkTextPointSize, + m_pDigitsRenderNode->updateTexture(m_waveformRenderer->getContext(), + untilMarkTextPointSize, untilMarkMaxHeightForText, m_waveformRenderer->getDevicePixelRatio()); if (m_timeUntilMark == 0.0) { + m_pDigitsRenderNode->clear(); return; } - const float ch = m_digitsRenderer.height(); + const float ch = m_pDigitsRenderNode->height(); float y = untilMarkAlign == Qt::AlignTop ? 0.f : untilMarkAlign == Qt::AlignBottom @@ -360,40 +411,35 @@ void allshader::WaveformRenderMark::drawUntilMark(const QMatrix4x4& matrix, floa } } - if (untilMarkShowBeats) { - const auto w = m_digitsRenderer.draw(matrix, - x, - y, - QString::number(m_beatsUntilMark)); - if (multiLine) { - y += ch; - } else { - x += w + ch * 0.75f; - } - } - - if (untilMarkShowTime) { - m_digitsRenderer.draw(matrix, - x, - y, - timeSecToString(m_timeUntilMark)); - } + m_pDigitsRenderNode->update( + x, + y, + multiLine, + untilMarkShowBeats ? QString::number(m_beatsUntilMark) : QString{}, + untilMarkShowTime ? timeSecToString(m_timeUntilMark) : QString{}); } // Generate the texture used to draw the play position marker. // Note that in the legacy waveform widgets this is drawn directly // in the WaveformWidgetRenderer itself. Doing it here is cleaner. -void allshader::WaveformRenderMark::updatePlayPosMarkTexture() { - const float imgHeight = m_waveformRenderer->getBreadth(); - const float imgWidth = kPlayPosWidth; +void allshader::WaveformRenderMark::updatePlayPosMarkTexture(rendergraph::Context* pContext) { + float imgWidth; + float imgHeight; - if (imgHeight == 0.0f) { + const float height = m_waveformRenderer->getBreadth(); + const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); + + if (height == m_playPosHeight && devicePixelRatio == m_playPosDevicePixelRatio) { return; } + m_playPosHeight = height; + m_playPosDevicePixelRatio = devicePixelRatio; - const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); const float lineX = 5.5f; + imgWidth = kPlayPosWidth; + imgHeight = height; + const QSize size{static_cast(std::lround(imgWidth * devicePixelRatio)), static_cast(std::lround(imgHeight * devicePixelRatio))}; @@ -451,7 +497,8 @@ void allshader::WaveformRenderMark::updatePlayPosMarkTexture() { } painter.end(); - m_playPosMarkTexture.setData(image); + dynamic_cast(m_pPlayPosNode->material()) + .setTexture(std::make_unique(pContext, image)); } void allshader::WaveformRenderMark::drawTriangle(QPainter* painter, @@ -468,15 +515,19 @@ void allshader::WaveformRenderMark::drawTriangle(QPainter* painter, painter->fillPath(triangle, fillColor); } -void allshader::WaveformRenderMark::resizeGL(int, int) { - // Will create textures so requires OpenGL context - updateMarkImages(); - updatePlayPosMarkTexture(); -} - void allshader::WaveformRenderMark::updateMarkImage(WaveformMarkPointer pMark) { - pMark->m_pGraphics = std::make_unique( - pMark->generateImage(m_waveformRenderer->getDevicePixelRatio())); + if (!pMark->m_pGraphics) { + pMark->m_pGraphics = + std::make_unique(pMark.get(), + m_waveformRenderer->getContext(), + pMark->generateImage( + m_waveformRenderer->getDevicePixelRatio())); + } else { + auto pGraphics = static_cast(pMark->m_pGraphics.get()); + pGraphics->updateTexture(m_waveformRenderer->getContext(), + pMark->generateImage( + m_waveformRenderer->getDevicePixelRatio())); + } } void allshader::WaveformRenderMark::updateUntilMark( diff --git a/src/waveform/renderers/allshader/waveformrendermark.h b/src/waveform/renderers/allshader/waveformrendermark.h index eba21888e3c..511dbaa8993 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.h +++ b/src/waveform/renderers/allshader/waveformrendermark.h @@ -2,40 +2,42 @@ #include -#include "rendergraph/openglnode.h" -#include "shaders/rgbashader.h" -#include "shaders/textureshader.h" -#include "util/opengltexture2d.h" -#include "waveform/renderers/allshader/digitsrenderer.h" +#include "rendergraph/geometrynode.h" +#include "rendergraph/node.h" #include "waveform/renderers/waveformrendermarkbase.h" class QDomNode; -class SkinContext; -class QOpenGLTexture; + +namespace rendergraph { +class GeometryNode; +class Context; +} // namespace rendergraph namespace allshader { +class DigitsRenderNode; class WaveformRenderMark; -} +} // namespace allshader -class allshader::WaveformRenderMark final - : public ::WaveformRenderMarkBase, - public rendergraph::OpenGLNode { +class allshader::WaveformRenderMark : public ::WaveformRenderMarkBase, + public rendergraph::Node { public: explicit WaveformRenderMark(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play); + // Pure virtual from WaveformRendererAbstract, not used + void draw(QPainter* painter, QPaintEvent* event) override final; + bool init() override; - void draw(QPainter* painter, QPaintEvent* event) override; - void initializeGL() override; - void paintGL() override; - void resizeGL(int w, int h) override; + void update(); + + bool isSubtreeBlocked() const override; private: void updateMarkImage(WaveformMarkPointer pMark) override; - void updatePlayPosMarkTexture(); + void updatePlayPosMarkTexture(rendergraph::Context* pContext); void drawTriangle(QPainter* painter, const QBrush& fillColor, @@ -43,16 +45,13 @@ class allshader::WaveformRenderMark final QPointF p2, QPointF p3); - void drawMark(const QMatrix4x4& matrix, const QRectF& rect, QColor color); - void drawTexture(const QMatrix4x4& matrix, float x, float y, QOpenGLTexture* pTexture); void updateUntilMark(double playPosition, double markerPosition); - void drawUntilMark(const QMatrix4x4& matrix, float x); + void updateDigitsNodeForUntilMark(float x); float getMaxHeightForText(float proportion) const; + void updateRangeNode(rendergraph::GeometryNode* pNode, + const QRectF& rect, + QColor color); - mixxx::RGBAShader m_rgbaShader; - mixxx::TextureShader m_textureShader; - OpenGLTexture2D m_playPosMarkTexture; - DigitsRenderer m_digitsRenderer; int m_beatsUntilMark; double m_timeUntilMark; double m_currentBeatPosition; @@ -61,5 +60,14 @@ class allshader::WaveformRenderMark final bool m_isSlipRenderer; + rendergraph::Node* m_pRangeNodesParent{}; + rendergraph::Node* m_pMarkNodesParent{}; + + rendergraph::GeometryNode* m_pPlayPosNode; + float m_playPosHeight; + float m_playPosDevicePixelRatio; + + DigitsRenderNode* m_pDigitsRenderNode{}; + DISALLOW_COPY_AND_ASSIGN(WaveformRenderMark); }; diff --git a/src/waveform/renderers/waveformmark.h b/src/waveform/renderers/waveformmark.h index d01d73b795f..c40af8927d9 100644 --- a/src/waveform/renderers/waveformmark.h +++ b/src/waveform/renderers/waveformmark.h @@ -5,10 +5,10 @@ #include "control/controlproxy.h" #include "track/cue.h" +#include "waveform/renderers/waveformsignalcolors.h" #include "waveform/waveformmarklabel.h" class SkinContext; -class WaveformSignalColors; class QOpenGLTexture; namespace allshader { diff --git a/src/waveform/renderers/waveformmarkrange.h b/src/waveform/renderers/waveformmarkrange.h index 15100bf8f03..5ac530e86af 100644 --- a/src/waveform/renderers/waveformmarkrange.h +++ b/src/waveform/renderers/waveformmarkrange.h @@ -56,7 +56,7 @@ class WaveformMarkRange { WaveformMarkLabel m_durationLabel; private: - void generateImage(int weidth, int height); + void generateImage(int width, int height); std::unique_ptr m_markStartPointControl; std::unique_ptr m_markEndPointControl; diff --git a/src/waveform/renderers/waveformrendermarkbase.cpp b/src/waveform/renderers/waveformrendermarkbase.cpp index f250b73acc1..b2293d1448d 100644 --- a/src/waveform/renderers/waveformrendermarkbase.cpp +++ b/src/waveform/renderers/waveformrendermarkbase.cpp @@ -14,9 +14,13 @@ WaveformRenderMarkBase::WaveformRenderMarkBase( void WaveformRenderMarkBase::setup(const QDomNode& node, const SkinContext& context) { WaveformSignalColors signalColors = *m_waveformRenderer->getWaveformSignalColors(); m_marks.setup(m_waveformRenderer->getGroup(), node, context, signalColors); +} + +bool WaveformRenderMarkBase::init() { m_marks.connectSamplePositionChanged(this, &WaveformRenderMarkBase::onMarkChanged); m_marks.connectSampleEndPositionChanged(this, &WaveformRenderMarkBase::onMarkChanged); m_marks.connectVisibleChanged(this, &WaveformRenderMarkBase::onMarkChanged); + return true; } void WaveformRenderMarkBase::onSetTrack() { diff --git a/src/waveform/renderers/waveformrendermarkbase.h b/src/waveform/renderers/waveformrendermarkbase.h index b5e4d51dd25..8efc35aebd4 100644 --- a/src/waveform/renderers/waveformrendermarkbase.h +++ b/src/waveform/renderers/waveformrendermarkbase.h @@ -17,6 +17,8 @@ class WaveformRenderMarkBase : public QObject, public WaveformRendererAbstract { void setup(const QDomNode& node, const SkinContext& context) override; + bool init() override; + // Called when a new track is loaded. void onSetTrack() override; diff --git a/src/waveform/widgets/allshader/waveformwidget.cpp b/src/waveform/widgets/allshader/waveformwidget.cpp index 8074c294cfa..13aab534621 100644 --- a/src/waveform/widgets/allshader/waveformwidget.cpp +++ b/src/waveform/widgets/allshader/waveformwidget.cpp @@ -147,6 +147,7 @@ void WaveformWidget::paintGL() { m_pOpacityNode->setOpacity(shouldOnlyDrawBackground() ? 0.f : 1.f); m_pWaveformRenderMarkRange->update(); + m_pWaveformRenderMark->update(); m_pEngine->preprocess(); m_pEngine->render(); From 9584f013487ba3ad3d557148950f64d2c189b073 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Mon, 21 Oct 2024 23:31:12 +0100 Subject: [PATCH 09/22] fix: prevent use-after-free with a texture buffer deepcopy --- src/rendergraph/opengl/texture.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rendergraph/opengl/texture.cpp b/src/rendergraph/opengl/texture.cpp index 88921bde9df..45550222944 100644 --- a/src/rendergraph/opengl/texture.cpp +++ b/src/rendergraph/opengl/texture.cpp @@ -1,4 +1,7 @@ #include "rendergraph/texture.h" +#include +#include +#include "rendergraph/assert.h" #include #include From caf9ff42159b9ac2165b2175b4a989883083f035 Mon Sep 17 00:00:00 2001 From: m0dB Date: Tue, 22 Oct 2024 23:44:25 +0100 Subject: [PATCH 10/22] scrolling qml waveformdisplay working --- res/qml/Mixxx/Controls/WaveformDisplay.qml | 7 +++++ src/main.cpp | 31 ++++++++++++++++++++-- src/qml/qmlplayermanagerproxy.cpp | 2 +- src/qml/qmlplayermanagerproxy.h | 2 +- src/waveform/isynctimeprovider.h | 9 +++++++ src/waveform/visualplayposition.cpp | 27 +++++++------------ src/waveform/visualplayposition.h | 9 ++++--- src/waveform/vsyncthread.h | 11 +++++--- 8 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 res/qml/Mixxx/Controls/WaveformDisplay.qml create mode 100644 src/waveform/isynctimeprovider.h diff --git a/res/qml/Mixxx/Controls/WaveformDisplay.qml b/res/qml/Mixxx/Controls/WaveformDisplay.qml new file mode 100644 index 00000000000..3ed11593cad --- /dev/null +++ b/res/qml/Mixxx/Controls/WaveformDisplay.qml @@ -0,0 +1,7 @@ +import Mixxx 1.0 as Mixxx + +Mixxx.WaveformDisplay { + id: root + + player: Mixxx.PlayerManager.getPlayer(root.group) +} diff --git a/src/main.cpp b/src/main.cpp index 60cb824a7ef..9041dbf0f96 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "config.h" @@ -16,7 +17,11 @@ #include "errordialoghandler.h" #include "mixxxapplication.h" #ifdef MIXXX_USE_QML +#include "mixer/playermanager.h" #include "qml/qmlapplication.h" +#include "waveform/guitick.h" +#include "waveform/visualsmanager.h" +#include "waveform/waveformwidgetfactory.h" #endif #include "mixxxmainwindow.h" #if defined(__WINDOWS__) @@ -57,8 +62,30 @@ int runMixxx(MixxxApplication* pApp, const CmdlineArgs& args) { int exitCode; #ifdef MIXXX_USE_QML if (args.isQml()) { - mixxx::qml::QmlApplication qmlApplication(pApp, pCoreServices); - exitCode = pApp->exec(); + auto pTick = std::make_unique(); + auto pVisuals = std::make_unique(); + WaveformWidgetFactory::createInstance(); // takes a long time + WaveformWidgetFactory::instance()->setConfig(pCoreServices->getSettings()); + WaveformWidgetFactory::instance()->startVSync(pTick.get(), pVisuals.get()); + { + mixxx::qml::QmlApplication qmlApplication(pApp, pCoreServices); + const QStringList visualGroups = + pCoreServices->getPlayerManager()->getVisualPlayerGroups(); + for (const QString& group : visualGroups) { + pVisuals->addDeck(group); + } + pCoreServices->getPlayerManager()->connect(pCoreServices->getPlayerManager().get(), + &PlayerManager::numberOfDecksChanged, + &qmlApplication, + [&pVisuals](int decks) { + for (int i = 0; i < decks; ++i) { + QString group = PlayerManager::groupForDeck(i); + pVisuals->addDeckIfNotExist(group); + } + }); + exitCode = pApp->exec(); + } + WaveformWidgetFactory::destroy(); } else #endif { diff --git a/src/qml/qmlplayermanagerproxy.cpp b/src/qml/qmlplayermanagerproxy.cpp index d8c3fc4930e..2544b34f3ad 100644 --- a/src/qml/qmlplayermanagerproxy.cpp +++ b/src/qml/qmlplayermanagerproxy.cpp @@ -14,7 +14,7 @@ QmlPlayerManagerProxy::QmlPlayerManagerProxy( : QObject(parent), m_pPlayerManager(pPlayerManager) { } -QObject* QmlPlayerManagerProxy::getPlayer(const QString& group) { +QmlPlayerProxy* QmlPlayerManagerProxy::getPlayer(const QString& group) { BaseTrackPlayer* pPlayer = m_pPlayerManager->getPlayer(group); if (!pPlayer) { qWarning() << "PlayerManagerProxy failed to find player for group" << group; diff --git a/src/qml/qmlplayermanagerproxy.h b/src/qml/qmlplayermanagerproxy.h index 690088dc881..5649c39baf7 100644 --- a/src/qml/qmlplayermanagerproxy.h +++ b/src/qml/qmlplayermanagerproxy.h @@ -18,7 +18,7 @@ class QmlPlayerManagerProxy : public QObject { std::shared_ptr pPlayerManager, QObject* parent = nullptr); - Q_INVOKABLE QObject* getPlayer(const QString& deck); + Q_INVOKABLE QmlPlayerProxy* getPlayer(const QString& deck); Q_INVOKABLE void loadLocationIntoNextAvailableDeck(const QString& location, bool play = false); Q_INVOKABLE void loadLocationUrlIntoNextAvailableDeck( const QUrl& locationUrl, bool play = false); diff --git a/src/waveform/isynctimeprovider.h b/src/waveform/isynctimeprovider.h new file mode 100644 index 00000000000..9432249702b --- /dev/null +++ b/src/waveform/isynctimeprovider.h @@ -0,0 +1,9 @@ +#pragma once + +#include "util/performancetimer.h" + +class ISyncTimeProvider { + public: + virtual int fromTimerToNextSyncMicros(const PerformanceTimer& timer) = 0; + virtual int getSyncIntervalTimeMicros() const = 0; +}; diff --git a/src/waveform/visualplayposition.cpp b/src/waveform/visualplayposition.cpp index 825101aece5..e7c44baa365 100644 --- a/src/waveform/visualplayposition.cpp +++ b/src/waveform/visualplayposition.cpp @@ -3,7 +3,7 @@ #include "moc_visualplayposition.cpp" #include "util/cmdlineargs.h" #include "util/math.h" -#include "waveform/vsyncthread.h" +#include "waveform/isynctimeprovider.h" //static QMap> VisualPlayPosition::m_listVisualPlayPosition; @@ -57,20 +57,10 @@ void VisualPlayPosition::set( } double VisualPlayPosition::calcOffsetAtNextVSync( - VSyncThread* pVSyncThread, const VisualPlayPositionData& data) { + ISyncTimeProvider* pSyncTimeProvider, const VisualPlayPositionData& data) { if (data.m_audioBufferMicroS != 0.0) { - int refToVSync; - int syncIntervalTimeMicros; -#ifdef MIXXX_USE_QML - if (CmdlineArgs::Instance().isQml()) { - refToVSync = 0; - syncIntervalTimeMicros = 0; - } else -#endif - { - refToVSync = pVSyncThread->fromTimerToNextSyncMicros(data.m_referenceTime); - syncIntervalTimeMicros = pVSyncThread->getSyncIntervalTimeMicros(); - } + int refToVSync = pSyncTimeProvider->fromTimerToNextSyncMicros(data.m_referenceTime); + int syncIntervalTimeMicros = pSyncTimeProvider->getSyncIntervalTimeMicros(); // The positive offset is limited to the audio buffer + 2 x waveform sync interval // This should be sufficient to compensate jitter, but does not continue // in case of underflows. @@ -161,22 +151,23 @@ double VisualPlayPosition::determinePlayPosInLoopBoundries( return interpolatedPlayPos; } -double VisualPlayPosition::getAtNextVSync(VSyncThread* pVSyncThread) { +double VisualPlayPosition::getAtNextVSync(ISyncTimeProvider* pSyncTimeProvider) { if (m_valid.load()) { const VisualPlayPositionData data = m_data.getValue(); - const double offset = calcOffsetAtNextVSync(pVSyncThread, data); + const double offset = calcOffsetAtNextVSync(pSyncTimeProvider, data); return determinePlayPosInLoopBoundries(data, offset); } return -1; } -void VisualPlayPosition::getPlaySlipAtNextVSync(VSyncThread* pVSyncThread, +void VisualPlayPosition::getPlaySlipAtNextVSync( + ISyncTimeProvider* pSyncTimeProvider, double* pPlayPosition, double* pSlipPosition) { if (m_valid.load()) { const VisualPlayPositionData data = m_data.getValue(); - const double offset = calcOffsetAtNextVSync(pVSyncThread, data); + const double offset = calcOffsetAtNextVSync(pSyncTimeProvider, data); double interpolatedPlayPos = determinePlayPosInLoopBoundries(data, offset); *pPlayPosition = interpolatedPlayPos; diff --git a/src/waveform/visualplayposition.h b/src/waveform/visualplayposition.h index a222799e1a9..258c398dbca 100644 --- a/src/waveform/visualplayposition.h +++ b/src/waveform/visualplayposition.h @@ -10,7 +10,7 @@ #include "util/performancetimer.h" class ControlProxy; -class VSyncThread; +class ISyncTimeProvider; // This class is for synchronizing the sound device DAC time with the waveforms, displayed on the // graphic device, using the CPU time @@ -68,8 +68,8 @@ class VisualPlayPosition : public QObject { double tempoTrackSeconds, double audioBufferMicroS); - double getAtNextVSync(VSyncThread* pVSyncThread); - void getPlaySlipAtNextVSync(VSyncThread* pVSyncThread, + double getAtNextVSync(ISyncTimeProvider* pSyncTimeProvider); + void getPlaySlipAtNextVSync(ISyncTimeProvider* pSyncTimeProvider, double* playPosition, double* slipPosition); double determinePlayPosInLoopBoundries( @@ -92,7 +92,8 @@ class VisualPlayPosition : public QObject { } private: - double calcOffsetAtNextVSync(VSyncThread* pVSyncThread, const VisualPlayPositionData& data); + double calcOffsetAtNextVSync(ISyncTimeProvider* pSyncTimeProvider, + const VisualPlayPositionData& data); ControlValueAtomic m_data; std::atomic m_valid; QString m_key; diff --git a/src/waveform/vsyncthread.h b/src/waveform/vsyncthread.h index fb30e71ee4d..f1fb4c7c877 100644 --- a/src/waveform/vsyncthread.h +++ b/src/waveform/vsyncthread.h @@ -6,10 +6,11 @@ #include #include "util/performancetimer.h" +#include "waveform/isynctimeprovider.h" class WGLWidget; -class VSyncThread : public QThread { +class VSyncThread : public QThread, public ISyncTimeProvider { Q_OBJECT public: enum VSyncMode { @@ -26,20 +27,22 @@ class VSyncThread : public QThread { VSyncThread(QObject* pParent, VSyncMode vSyncMode); ~VSyncThread(); - void run(); + void run() override; bool waitForVideoSync(WGLWidget* glw); int elapsed(); void setSyncIntervalTimeMicros(int usSyncTimer); int droppedFrames(); void setSwapWait(int sw); - int fromTimerToNextSyncMicros(const PerformanceTimer& timer); + // ISyncTimerProvider + int fromTimerToNextSyncMicros(const PerformanceTimer& timer) override; void vsyncSlotFinished(); void getAvailableVSyncTypes(QList>* list); void setupSync(WGLWidget* glw, int index); void waitUntilSwap(WGLWidget* glw); mixxx::Duration sinceLastSwap() const; - int getSyncIntervalTimeMicros() const { + // ISyncTimerProvider + int getSyncIntervalTimeMicros() const override { return m_syncIntervalTimeMicros; } void updatePLL(); From c8d762faf979d6fdeede4e6239bf3bedfa98f081 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Tue, 22 Oct 2024 23:45:01 +0100 Subject: [PATCH 11/22] feat: add scrolling waveform in QML using scenegraph --- CMakeLists.txt | 50 +- res/qml/WaveformDisplay.qml | 238 +++++++++ res/qml/main.qml | 8 +- src/qml/qmlwaveformdisplay.cpp | 253 +++++++++ src/qml/qmlwaveformdisplay.h | 126 +++++ src/qml/qmlwaveformrenderer.cpp | 152 ++++++ src/qml/qmlwaveformrenderer.h | 486 ++++++++++++++++++ .../allshader/waveformrenderbeat.cpp | 6 +- .../renderers/allshader/waveformrenderbeat.h | 3 +- .../allshader/waveformrendererendoftrack.cpp | 5 +- .../allshader/waveformrendererendoftrack.h | 2 +- .../allshader/waveformrendererpreroll.cpp | 4 +- .../allshader/waveformrendererpreroll.h | 3 +- .../allshader/waveformrendererrgb.cpp | 13 + .../renderers/allshader/waveformrendererrgb.h | 7 + .../allshader/waveformrendermark.cpp | 34 +- .../renderers/allshader/waveformrendermark.h | 18 + src/waveform/renderers/waveformmark.cpp | 69 +++ src/waveform/renderers/waveformmark.h | 14 + src/waveform/renderers/waveformmarkrange.cpp | 60 +++ src/waveform/renderers/waveformmarkrange.h | 12 + src/waveform/renderers/waveformmarkset.cpp | 65 ++- src/waveform/renderers/waveformmarkset.h | 22 +- .../renderers/waveformrendererabstract.h | 4 + .../renderers/waveformrendermarkbase.h | 12 + .../renderers/waveformwidgetrenderer.cpp | 27 +- .../renderers/waveformwidgetrenderer.h | 22 +- src/waveform/waveformwidgetfactory.cpp | 20 +- .../widgets/waveformwidgetabstract.cpp | 1 + src/widget/wspinnybase.cpp | 1 + src/widget/wwaveformviewer.cpp | 4 +- 31 files changed, 1690 insertions(+), 51 deletions(-) create mode 100644 res/qml/WaveformDisplay.qml create mode 100644 src/qml/qmlwaveformdisplay.cpp create mode 100644 src/qml/qmlwaveformdisplay.h create mode 100644 src/qml/qmlwaveformrenderer.cpp create mode 100644 src/qml/qmlwaveformrenderer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a70b1266dcd..b62ef16c45a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1762,6 +1762,7 @@ else() ) endif() if(QOPENGL) + target_compile_definitions(mixxx-lib PRIVATE __RENDERGRAPH_IS_OPENGL) target_sources( mixxx-lib PRIVATE @@ -1776,7 +1777,6 @@ if(QOPENGL) src/shaders/vinylqualityshader.cpp src/util/opengltexture2d.cpp src/waveform/renderers/allshader/digitsrenderer.cpp - src/waveform/renderers/allshader/matrixforwidgetgeometry.cpp src/waveform/renderers/allshader/waveformrenderbackground.cpp src/waveform/renderers/allshader/waveformrenderbeat.cpp src/waveform/renderers/allshader/waveformrenderer.cpp @@ -2105,7 +2105,7 @@ if(QT6) # below that takes care of the correct object order in the resulting binary # According to https://doc.qt.io/qt-6/qt-finalize-target.html it is importand for # builds with Qt < 3.21 - qt_add_executable(mixxx WIN32 src/main.cpp MANUAL_FINALIZATION) + qt_add_executable(mixxx WIN32 MACOSX_BUNDLE src/main.cpp MANUAL_FINALIZATION) else() find_package(Qt5 COMPONENTS Core) # For Qt Core cmake functions # This is the first package form the environment, if this fails give hints how to install the environment @@ -3248,9 +3248,11 @@ if(QML) res/qml/Mixxx/Controls/WaveformOverviewHotcueMarker.qml res/qml/Mixxx/Controls/WaveformOverviewMarker.qml res/qml/Mixxx/Controls/WaveformOverview.qml + res/qml/Mixxx/Controls/WaveformDisplay.qml ) target_link_libraries(mixxx-qml-lib PRIVATE mixxx-qml-mixxxcontrolsplugin) + target_compile_definitions(mixxx-qml-lib PRIVATE __RENDERGRAPH_IS_SCENEGRAPH) target_sources( mixxx-qml-lib PRIVATE @@ -3272,7 +3274,22 @@ if(QML) src/qml/qmlvisibleeffectsmodel.cpp src/qml/qmlchainpresetmodel.cpp src/qml/qmlwaveformoverview.cpp - src/qml/qmlmixxxcontrollerscreen.cpp + src/qml/qmlwaveformdisplay.cpp + src/qml/qmlwaveformrenderer.cpp + src/waveform/renderers/allshader/digitsrenderer.cpp + src/waveform/renderers/allshader/waveformrenderbeat.cpp + src/waveform/renderers/allshader/waveformrenderer.cpp + src/waveform/renderers/allshader/waveformrendererendoftrack.cpp + src/waveform/renderers/allshader/waveformrendererpreroll.cpp + src/waveform/renderers/allshader/waveformrendererrgb.cpp + src/waveform/renderers/allshader/waveformrenderersignalbase.cpp + src/waveform/renderers/allshader/waveformrendermark.cpp + src/waveform/renderers/allshader/waveformrendermarkrange.cpp + # FIXME depends on rendergraph/openglnode.h + # src/waveform/renderers/allshader/waveformrendererslipmode.cpp + # src/waveform/renderers/allshader/waveformrendererfiltered.cpp + # src/waveform/renderers/allshader/waveformrendererhsv.cpp + # src/waveform/renderers/allshader/waveformrenderersimple.cpp # The following sources need to be in this target to get QML_ELEMENT properly interpreted src/control/controlmodel.cpp src/control/controlsortfiltermodel.cpp @@ -4193,7 +4210,12 @@ if(STEM) endif() if(QML) target_compile_definitions(mixxx-qml-lib PUBLIC __STEM__) - target_sources(mixxx-qml-lib PRIVATE src/qml/qmlstemsmodel.cpp) + target_sources( + mixxx-qml-lib + PRIVATE + src/waveform/renderers/allshader/waveformrendererstem.cpp + src/qml/qmlstemsmodel.cpp + ) endif() endif() @@ -4573,9 +4595,25 @@ if(VINYLCONTROL) endif() # rendergraph -add_subdirectory(src/rendergraph) +add_subdirectory(src/rendergraph/opengl) +add_subdirectory(res/shaders/rendergraph) +target_compile_definitions( + rendergraph_gl + PUBLIC $<$:MIXXX_DEBUG_ASSERTIONS_ENABLED> +) target_link_libraries(mixxx-lib PUBLIC rendergraph_gl) -target_compile_definitions(mixxx-lib PUBLIC rendergraph=rendergraph_gl) +target_compile_definitions(mixxx-lib PRIVATE rendergraph=rendergraph_gl) +target_compile_definitions(mixxx-lib PRIVATE allshader=allshader_gl) +if(QML) + add_subdirectory(src/rendergraph/scenegraph) + target_compile_definitions( + rendergraph_sg + PUBLIC $<$:MIXXX_DEBUG_ASSERTIONS_ENABLED> + ) + target_link_libraries(mixxx-qml-lib PRIVATE rendergraph_sg) + target_compile_definitions(mixxx-qml-lib PRIVATE rendergraph=rendergraph_sg) + target_compile_definitions(mixxx-qml-lib PRIVATE allshader=allshader_sg) +endif() # WavPack audio file support find_package(wavpack) diff --git a/res/qml/WaveformDisplay.qml b/res/qml/WaveformDisplay.qml new file mode 100644 index 00000000000..22209a8719e --- /dev/null +++ b/res/qml/WaveformDisplay.qml @@ -0,0 +1,238 @@ +import "." as Skin +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls +import QtQuick 2.12 +import "Theme" + +Item { + id: root + + required property string group + + enum MouseStatus { + Normal, + Bending, + Scratching + } + + MixxxControls.WaveformDisplay { + anchors.fill: parent + group: root.group + zoom: zoomControl.value + backgroundColor: "#5e000000" + + Mixxx.WaveformRendererEndOfTrack { + color: '#ff8872' + } + + Mixxx.WaveformRendererPreroll { + color: '#ff8872' + } + + Mixxx.WaveformRendererMarkRange { + // + Mixxx.WaveformMarkRange { + startControl: "loop_start_position" + endControl: "loop_end_position" + enabledControl: "loop_enabled" + color: '#00b400' + opacity: 0.7 + disabledColor: '#FFFFFF' + disabledOpacity: 0.6 + } + // + Mixxx.WaveformMarkRange { + startControl: "intro_start_position" + endControl: "intro_end_position" + color: '#2c5c9a' + opacity: 0.6 + durationTextColor: '#ffffff' + durationTextLocation: 'after' + } + // + Mixxx.WaveformMarkRange { + startControl: "outro_start_position" + endControl: "outro_end_position" + color: '#2c5c9a' + opacity: 0.6 + durationTextColor: '#ffffff' + durationTextLocation: 'before' + } + } + + Mixxx.WaveformRendererRGB { + axesColor: '#a1a1a1a1' + lowColor: '#ff2154d7' + midColor: '#cfb26606' + highColor: '#e5029c5c' + } + + Mixxx.WaveformRendererStem { } + + Mixxx.WaveformRendererBeat { + color: '#a1a1a1a1' + } + + Mixxx.WaveformRendererMark { + playMarkerColor: 'cyan' + playMarkerBackground: 'orange' + defaultMark: Mixxx.WaveformMark { + align: "bottom|right" + color: "#00d9ff" + textColor: "#1a1a1a" + text: " %1 " + } + + untilMark.showTime: true + untilMark.showBeats: true + untilMark.align: Mixxx.WaveformUntilMark.AlignBottom + untilMark.textSize: 11 + + Mixxx.WaveformMark { + control: "cue_point" + text: 'CUE' + align: 'top|right' + color: 'red' + textColor: '#1a1a1a' + } + Mixxx.WaveformMark { + control: "loop_start_position" + text: '↻' + align: 'top|left' + color: 'green' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "loop_end_position" + align: 'bottom|right' + color: 'green' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "intro_start_position" + align: 'top|right' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "intro_end_position" + text: '◢' + align: 'top|left' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "outro_start_position" + text: '◣' + align: 'top|right' + color: 'blue' + textColor: '#FFFFFF' + } + Mixxx.WaveformMark { + control: "outro_end_position" + align: 'top|left' + color: 'blue' + textColor: '#FFFFFF' + } + } + } + + 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" + } + + Mixxx.ControlProxy { + id: rateRatioControl + + group: root.group + key: "rate_ratio" + } + + Mixxx.ControlProxy { + id: zoomControl + + group: root.group + key: "waveform_zoom" + } + readonly property real effectiveZoomFactor: (1 / rateRatioControl.value) * (100 / zoomControl.value) + + MouseArea { + property int mouseStatus: WaveformDisplay.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 == WaveformDisplay.MouseStatus.Bending) + wheelControl.parameter = 0.5; + + mouseStatus = WaveformDisplay.MouseStatus.Scratching; + scratchPositionEnableControl.value = 1; + // TODO: Calculate position properly + scratchPositionControl.value = -mouse.x * zoomControl.value * 50; + } else { + if (mouseStatus == WaveformDisplay.MouseStatus.Scratching) + scratchPositionEnableControl.value = 0; + + wheelControl.parameter = 0.5; + mouseStatus = WaveformDisplay.MouseStatus.Bending; + } + } + onPositionChanged: { + switch (mouseStatus) { + case WaveformDisplay.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 / root.width); + // clamp to [0.0, 1.0] + wheelControl.parameter = Math.max(Math.min(v, 1), 0); + break; + }; + case WaveformDisplay.MouseStatus.Scratching: + // TODO: Calculate position properly + scratchPositionControl.value = -mouse.x * zoomControl.value * 50; + break; + } + } + onReleased: { + switch (mouseStatus) { + case WaveformDisplay.MouseStatus.Bending: + wheelControl.parameter = 0.5; + break; + case WaveformDisplay.MouseStatus.Scratching: + scratchPositionEnableControl.value = 0; + break; + } + mouseStatus = WaveformDisplay.MouseStatus.Normal; + } + + onWheel: { + if (wheel.angleDelta.y < 0 && zoomControl.value > 1) { + zoomControl.value -= 1; + } else if (wheel.angleDelta.y > 0 && zoomControl.value < 10.0) { + zoomControl.value += 1; + } + } + } +} diff --git a/res/qml/main.qml b/res/qml/main.qml index 15578d303cb..5270fa054fa 100644 --- a/res/qml/main.qml +++ b/res/qml/main.qml @@ -98,7 +98,7 @@ ApplicationWindow { } } - WaveformRow { + Skin.WaveformDisplay { id: deck3waveform group: "[Channel3]" @@ -111,7 +111,7 @@ ApplicationWindow { } } - WaveformRow { + Skin.WaveformDisplay { id: deck1waveform group: "[Channel1]" @@ -124,7 +124,7 @@ ApplicationWindow { } } - WaveformRow { + Skin.WaveformDisplay { id: deck2waveform group: "[Channel2]" @@ -137,7 +137,7 @@ ApplicationWindow { } } - WaveformRow { + Skin.WaveformDisplay { id: deck4waveform group: "[Channel4]" diff --git a/src/qml/qmlwaveformdisplay.cpp b/src/qml/qmlwaveformdisplay.cpp new file mode 100644 index 00000000000..266f3b516f1 --- /dev/null +++ b/src/qml/qmlwaveformdisplay.cpp @@ -0,0 +1,253 @@ +#include "qml/qmlwaveformdisplay.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mixer/basetrackplayer.h" +#include "moc_qmlwaveformdisplay.cpp" +#include "qml/qmlplayerproxy.h" +#include "rendergraph/context.h" +#include "rendergraph/node.h" +#include "waveform/renderers/allshader/waveformrendermark.h" +#include "waveform/renderers/allshader/waveformrendermarkrange.h" + +using namespace allshader; + +namespace mixxx { +namespace qml { + +QmlWaveformDisplay::QmlWaveformDisplay(QQuickItem* parent) + : QQuickItem(parent), + WaveformWidgetRenderer(), + m_pPlayer(nullptr) { + setFlag(QQuickItem::ItemHasContents, true); + + connect(this, + &QmlWaveformDisplay::windowChanged, + this, + &QmlWaveformDisplay::slotWindowChanged, + Qt::DirectConnection); +} + +QmlWaveformDisplay::~QmlWaveformDisplay() { + // The stack contains references to Renderer that are owned and cleared by a BaseNode + m_rendererStack.clear(); +} + +void QmlWaveformDisplay::componentComplete() { + qDebug() << "QmlWaveformDisplay ready for group" << getGroup() << "with" + << m_waveformRenderers.count() << "renderer(s)"; + QQuickItem::componentComplete(); +} + +void QmlWaveformDisplay::slotWindowChanged(QQuickWindow* window) { + m_rendererStack.clear(); + + m_dirtyFlag |= DirtyFlag::Window; + if (window) { + connect(window, &QQuickWindow::afterFrameEnd, this, &QmlWaveformDisplay::slotFrameSwapped); + } + m_timer.restart(); +} + +int QmlWaveformDisplay::fromTimerToNextSyncMicros(const PerformanceTimer& timer) { + // TODO @m0dB probably better to use a singleton instead of deriving QmlWaveformDisplay from + // ISyncTimeProvider and have each keep track of this. + int difference = static_cast(m_timer.difference(timer).toIntegerMicros()); + // int math is fine here, because we do not expect times > 4.2 s + + return difference + m_syncIntervalTimeMicros; +} + +void QmlWaveformDisplay::slotFrameSwapped() { + m_timer.restart(); + + // continuous redraw + update(); +} + +void QmlWaveformDisplay::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { + m_dirtyFlag |= DirtyFlag::Geometry; + update(); + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +QSGNode* QmlWaveformDisplay::updatePaintNode(QSGNode* node, UpdatePaintNodeData*) { + auto* bgNode = dynamic_cast(node); + static rendergraph::GeometryNode* pPreRoll; + + if (m_dirtyFlag.testFlag(DirtyFlag::Window)) { + delete bgNode; + auto* pContext = getContext(); + if (pContext) { + delete pContext; + } + m_dirtyFlag.setFlag(DirtyFlag::Window, false); + } + if (!bgNode) { + bgNode = new QSGSimpleRectNode(); + bgNode->setRect(boundingRect()); + + if (getContext()) { + delete getContext(); + } + setContext(new rendergraph::Context(window())); + m_pTopNode = new rendergraph::Node; + + m_rendererStack.clear(); + for (auto* pQmlRenderer : m_waveformRenderers) { + auto renderer = pQmlRenderer->create(this); + if (!renderer.renderer) { + continue; + } + addRenderer(renderer.renderer); + m_pTopNode->appendChildNode(std::unique_ptr(renderer.node)); + auto* pWaveformRenderMark = + dynamic_cast( + renderer.renderer); + if (pWaveformRenderMark) { + m_waveformRenderMark = pWaveformRenderMark; + } + auto* pWaveformRenderMarkRange = + dynamic_cast( + renderer.renderer); + if (pWaveformRenderMarkRange) { + m_waveformRenderMarkRange = pWaveformRenderMarkRange; + } + } + + bgNode->appendChildNode(m_pTopNode); + init(); + } + + if (m_dirtyFlag.testFlag(DirtyFlag::Background)) { + m_dirtyFlag.setFlag(DirtyFlag::Background, false); + bgNode->setColor(m_backgroundColor); + } + + if (m_dirtyFlag.testFlag(DirtyFlag::Geometry)) { + m_dirtyFlag.setFlag(DirtyFlag::Geometry, false); + resizeRenderer(boundingRect().width(), + boundingRect().height(), + window()->devicePixelRatio()); + bgNode->setRect(boundingRect()); + + auto rect = QRectF(boundingRect().x() + + boundingRect().width() * m_playMarkerPosition - 1.0, + boundingRect().y(), + 2.0, + boundingRect().height()); + } + + m_waveformRenderMark->update(); + m_waveformRenderMarkRange->update(); + + onPreRender(this); + bgNode->markDirty(QSGNode::DirtyForceUpdate); + + return bgNode; +} + +QmlPlayerProxy* QmlWaveformDisplay::getPlayer() const { + return m_pPlayer; +} + +void QmlWaveformDisplay::setPlayer(QmlPlayerProxy* pPlayer) { + if (m_pPlayer == pPlayer) { + return; + } + + if (m_pPlayer != nullptr) { + m_pPlayer->internalTrackPlayer()->disconnect(this); + } + + m_pPlayer = pPlayer; + + if (m_pPlayer != nullptr) { + setCurrentTrack(m_pPlayer->internalTrackPlayer()->getLoadedTrack()); + connect(m_pPlayer->internalTrackPlayer(), + &BaseTrackPlayer::newTrackLoaded, + this, + &QmlWaveformDisplay::slotTrackLoaded); + connect(m_pPlayer->internalTrackPlayer(), + &BaseTrackPlayer::loadingTrack, + this, + &QmlWaveformDisplay::slotTrackLoading); + connect(m_pPlayer->internalTrackPlayer(), + &BaseTrackPlayer::playerEmpty, + this, + &QmlWaveformDisplay::slotTrackUnloaded); + } + + emit playerChanged(); + update(); +} + +void QmlWaveformDisplay::setGroup(const QString& group) { + if (getGroup() == group) { + return; + } + + WaveformWidgetRenderer::setGroup(group); + emit groupChanged(group); +} + +void QmlWaveformDisplay::slotTrackLoaded(TrackPointer pTrack) { + // TODO: Investigate if it's a bug that this debug assertion fails when + // passing tracks on the command line + // DEBUG_ASSERT(m_pCurrentTrack == pTrack); + setCurrentTrack(pTrack); +} + +void QmlWaveformDisplay::slotTrackLoading(TrackPointer pNewTrack, TrackPointer pOldTrack) { + Q_UNUSED(pOldTrack); // only used in DEBUG_ASSERT + DEBUG_ASSERT(getTrackInfo() == pOldTrack); + setCurrentTrack(pNewTrack); +} + +void QmlWaveformDisplay::slotTrackUnloaded() { + setCurrentTrack(nullptr); +} + +void QmlWaveformDisplay::setCurrentTrack(TrackPointer pTrack) { + auto pCurrentTrack = getTrackInfo(); + // TODO: Check if this is actually possible + if (pCurrentTrack == pTrack) { + return; + } + + if (pCurrentTrack != nullptr) { + disconnect(pCurrentTrack.get(), nullptr, this, nullptr); + } + + setTrack(pTrack); + if (pTrack != nullptr) { + connect(pTrack.get(), + &Track::waveformSummaryUpdated, + this, + &QmlWaveformDisplay::slotWaveformUpdated); + } + slotWaveformUpdated(); +} + +void QmlWaveformDisplay::slotWaveformUpdated() { + update(); +} + +QQmlListProperty QmlWaveformDisplay::renderers() { + return {this, &m_waveformRenderers}; +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlwaveformdisplay.h b/src/qml/qmlwaveformdisplay.h new file mode 100644 index 00000000000..6ab0f526af1 --- /dev/null +++ b/src/qml/qmlwaveformdisplay.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "qml/qmlplayerproxy.h" +#include "qml/qmlwaveformrenderer.h" +#include "track/track.h" +#include "util/performancetimer.h" +#include "waveform/isynctimeprovider.h" +#include "waveform/renderers/waveformwidgetrenderer.h" + +class WaveformRendererAbstract; + +namespace allshader { +class WaveformWidget; +class WaveformRenderMark; +class WaveformRenderMarkRange; +} // namespace allshader +namespace rendergraph { +class Node; +class OpacityNode; +class TreeNode; +} // namespace rendergraph + +namespace mixxx { +namespace qml { + +class QmlPlayerProxy; + +class QmlWaveformDisplay : public QQuickItem, ISyncTimeProvider, public WaveformWidgetRenderer { + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(QmlPlayerProxy* player READ getPlayer WRITE setPlayer + NOTIFY playerChanged REQUIRED) + Q_PROPERTY(QString group READ getGroup WRITE setGroup NOTIFY groupChanged REQUIRED) + Q_PROPERTY(QQmlListProperty renderers READ renderers) + Q_PROPERTY(double zoom READ getZoom WRITE setZoom NOTIFY zoomChanged) + Q_PROPERTY(QColor backgroundColor READ getBackgroundColor WRITE + setBackgroundColor NOTIFY backgroundColorChanged) + Q_CLASSINFO("DefaultProperty", "renderers") + QML_NAMED_ELEMENT(WaveformDisplay) + + public: + QmlWaveformDisplay(QQuickItem* parent = nullptr); + ~QmlWaveformDisplay() override; + + void setPlayer(QmlPlayerProxy* player); + QmlPlayerProxy* getPlayer() const; + + QColor getBackgroundColor() const { + return m_backgroundColor; + } + void setBackgroundColor(QColor color) { + m_backgroundColor = color; + m_dirtyFlag.setFlag(DirtyFlag::Background, true); + emit backgroundColorChanged(); + } + + void setGroup(const QString& group) override; + void setZoom(double zoom) { + WaveformWidgetRenderer::setZoom(zoom); + emit zoomChanged(); + } + + int fromTimerToNextSyncMicros(const PerformanceTimer& timer) override; + int getSyncIntervalTimeMicros() const override { + return m_syncIntervalTimeMicros; + } + + virtual void componentComplete() override; + + QQmlListProperty renderers(); + + protected: + QSGNode* updatePaintNode(QSGNode* old, QQuickItem::UpdatePaintNodeData*) override; + void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; + private slots: + void slotTrackLoaded(TrackPointer pLoadedTrack); + void slotTrackLoading(TrackPointer pNewTrack, TrackPointer pOldTrack); + void slotTrackUnloaded(); + void slotWaveformUpdated(); + + void slotFrameSwapped(); + void slotWindowChanged(QQuickWindow* window); + signals: + void playerChanged(); + void zoomChanged(); + void groupChanged(const QString& group); + void backgroundColorChanged(); + + private: + void setCurrentTrack(TrackPointer pTrack); + + // Properties + QPointer m_pPlayer; + QColor m_backgroundColor{QColor(0, 0, 0, 255)}; + + PerformanceTimer m_timer; + + int m_syncIntervalTimeMicros{1000000 / 10}; // TODO don't hardcode + + enum class DirtyFlag : int { + None = 0x0, + Geometry = 0x1, + Window = 0x2, + Background = 0x4, + }; + Q_DECLARE_FLAGS(DirtyFlags, DirtyFlag) + + DirtyFlags m_dirtyFlag{DirtyFlag::None}; + QList m_waveformRenderers; + + // Owned by the QML scene? + rendergraph::Node* m_pTopNode; + allshader::WaveformRenderMark* m_waveformRenderMark; + allshader::WaveformRenderMarkRange* m_waveformRenderMarkRange; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlwaveformrenderer.cpp b/src/qml/qmlwaveformrenderer.cpp new file mode 100644 index 00000000000..610264ec3d5 --- /dev/null +++ b/src/qml/qmlwaveformrenderer.cpp @@ -0,0 +1,152 @@ +#include "qml/qmlwaveformrenderer.h" + +#include + +#include "moc_qmlwaveformrenderer.cpp" +#include "util/assert.h" +#include "waveform/renderers/allshader/waveformrenderbeat.h" +#include "waveform/renderers/allshader/waveformrendererendoftrack.h" +#include "waveform/renderers/allshader/waveformrendererpreroll.h" +#include "waveform/renderers/allshader/waveformrendererrgb.h" +#ifdef __STEM__ +#include "waveform/renderers/allshader/waveformrendererstem.h" +#endif +#include "waveform/renderers/allshader/waveformrendermark.h" +#include "waveform/renderers/allshader/waveformrendermarkrange.h" + +using namespace allshader; + +namespace mixxx { +namespace qml { + +QmlWaveformRendererEndOfTrack::QmlWaveformRendererEndOfTrack() { +} + +QmlWaveformRendererPreroll::QmlWaveformRendererPreroll() { +} + +QmlWaveformRendererRGB::QmlWaveformRendererRGB() { +} + +QmlWaveformRendererBeat::QmlWaveformRendererBeat() { +} + +QmlWaveformRendererMarkRange::QmlWaveformRendererMarkRange() { +} + +QmlWaveformRendererStem::QmlWaveformRendererStem() { +} + +QmlWaveformRendererMark::QmlWaveformRendererMark() + : m_defaultMark(nullptr), + m_untilMark(std::make_unique()) { +} + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererEndOfTrack::create( + WaveformWidgetRenderer* waveformWidget) const { + auto* renderer = new WaveformRendererEndOfTrack(waveformWidget, m_color); + return QmlWaveformRendererFactory::Renderer{renderer, renderer}; +} + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererPreroll::create( + WaveformWidgetRenderer* waveformWidget) const { + auto* renderer = new WaveformRendererPreroll( + waveformWidget, WaveformRendererAbstract::Play, m_color); + return QmlWaveformRendererFactory::Renderer{renderer, renderer}; +} + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererRGB::create( + WaveformWidgetRenderer* waveformWidget) const { + auto* renderer = new WaveformRendererRGB(waveformWidget, + m_axesColor, + m_lowColor, + m_midColor, + m_highColor, + ::WaveformRendererAbstract::Play); + return QmlWaveformRendererFactory::Renderer{renderer, renderer}; +} + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererBeat::create( + WaveformWidgetRenderer* waveformWidget) const { + auto* renderer = new WaveformRenderBeat( + waveformWidget, ::WaveformRendererAbstract::Play, m_color); + return QmlWaveformRendererFactory::Renderer{renderer, renderer}; +} + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererMarkRange::create( + WaveformWidgetRenderer* waveformWidget) const { + auto* renderer = new WaveformRenderMarkRange( + waveformWidget); + + for (auto* pMark : m_ranges) { + renderer->addRange(WaveformMarkRange( + waveformWidget->getGroup(), + pMark->color(), + pMark->disabledColor(), + pMark->opacity(), + pMark->disabledOpacity(), + pMark->durationTextColor(), + pMark->startControl(), + pMark->endControl(), + pMark->enabledControl(), + pMark->visibilityControl(), + pMark->durationTextLocation())); + } + return QmlWaveformRendererFactory::Renderer{renderer, renderer}; +} + +#ifdef __STEM__ +QmlWaveformRendererFactory::Renderer QmlWaveformRendererStem::create( + WaveformWidgetRenderer* waveformWidget) const { + auto* renderer = new WaveformRendererStem( + waveformWidget, ::WaveformRendererAbstract::Play); + return QmlWaveformRendererFactory::Renderer{renderer, renderer}; +} +#endif + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererMark::create( + WaveformWidgetRenderer* waveformWidget) const { + VERIFY_OR_DEBUG_ASSERT(!!m_untilMark) { + return QmlWaveformRendererFactory::Renderer{}; + } + auto* renderer = new WaveformRenderMark(waveformWidget, + m_playMarkerColor, + m_playMarkerBackground, + m_untilMark->showTime(), + m_untilMark->showBeats(), + static_cast(m_untilMark->align()), + m_untilMark->textSize(), + ::WaveformRendererAbstract::Play); + int priority = 0; + for (auto* pMark : m_marks) { + renderer->addMark(WaveformMarkPointer(new WaveformMark( + waveformWidget->getGroup(), + pMark->control(), + pMark->visibilityControl(), + pMark->textColor(), + pMark->align(), + pMark->text(), + pMark->pixmap(), + pMark->icon(), + pMark->color(), + --priority))); + } + auto* pMark = defaultMark(); + if (pMark != nullptr) { + renderer->setDefaultMark( + waveformWidget->getGroup(), + WaveformMarkSet::Seed{ + pMark->control(), + pMark->visibilityControl(), + pMark->textColor(), + pMark->align(), + pMark->text(), + pMark->pixmap(), + pMark->icon(), + pMark->color(), + }); + } + return QmlWaveformRendererFactory::Renderer{renderer, renderer}; +} +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlwaveformrenderer.h b/src/qml/qmlwaveformrenderer.h new file mode 100644 index 00000000000..3375f667013 --- /dev/null +++ b/src/qml/qmlwaveformrenderer.h @@ -0,0 +1,486 @@ +#pragma once + +#include +#include + +#include "rendergraph/node.h" +#include "waveform/renderers/waveformrendererabstract.h" +#include "waveform/renderers/waveformwidgetrenderer.h" + +class WaveformWidgetRenderer; + +namespace allshaders { +class WaveformRendererEndOfTrack; +class WaveformRendererPreroll; +class WaveformRendererRGB; +class WaveformRenderBeat; +} // namespace allshaders + +namespace mixxx { +namespace qml { + +class QmlWaveformRendererFactory : public QObject { + Q_OBJECT + QML_ANONYMOUS + public: + struct Renderer { + ::WaveformRendererAbstract* renderer{nullptr}; + rendergraph::BaseNode* node{nullptr}; + }; + + virtual Renderer create(WaveformWidgetRenderer* waveformWidget) const = 0; +}; + +class QmlWaveformRendererEndOfTrack + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged REQUIRED) + QML_NAMED_ELEMENT(WaveformRendererEndOfTrack) + + public: + QmlWaveformRendererEndOfTrack(); + + const QColor& color() const { + return m_color; + } + void setColor(QColor color) { + m_color = color; + emit colorChanged(m_color); + } + + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + + signals: + void colorChanged(const QColor&); + + private: + QColor m_color; +}; + +class QmlWaveformRendererPreroll + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged REQUIRED) + QML_NAMED_ELEMENT(WaveformRendererPreroll) + + public: + QmlWaveformRendererPreroll(); + + const QColor& color() const { + return m_color; + } + void setColor(QColor color) { + m_color = color; + emit colorChanged(m_color); + } + + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + signals: + void colorChanged(const QColor&); + + private: + QColor m_color; +}; + +class QmlWaveformRendererRGB + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(QColor axesColor READ axesColor WRITE setAxesColor NOTIFY axesColorChanged REQUIRED) + Q_PROPERTY(QColor lowColor READ lowColor WRITE setLowColor NOTIFY lowColorChanged REQUIRED) + Q_PROPERTY(QColor midColor READ midColor WRITE setMidColor NOTIFY midColorChanged REQUIRED) + Q_PROPERTY(QColor highColor READ highColor WRITE setHighColor NOTIFY highColorChanged REQUIRED) + QML_NAMED_ELEMENT(WaveformRendererRGB) + + public: + QmlWaveformRendererRGB(); + + const QColor& axesColor() const { + return m_axesColor; + } + void setAxesColor(QColor color) { + m_axesColor = color; + emit axesColorChanged(m_axesColor); + } + + const QColor& lowColor() const { + return m_lowColor; + } + void setLowColor(QColor color) { + m_lowColor = color; + emit lowColorChanged(m_lowColor); + } + + const QColor& midColor() const { + return m_lowColor; + } + void setMidColor(QColor color) { + m_midColor = color; + emit midColorChanged(m_lowColor); + } + + const QColor& highColor() const { + return m_lowColor; + } + void setHighColor(QColor color) { + m_highColor = color; + emit highColorChanged(m_lowColor); + } + + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + signals: + void axesColorChanged(const QColor&); + void lowColorChanged(const QColor&); + void midColorChanged(const QColor&); + void highColorChanged(const QColor&); + + private: + QColor m_axesColor; + QColor m_lowColor; + QColor m_midColor; + QColor m_highColor; +}; + +class QmlWaveformRendererBeat + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged REQUIRED) + QML_NAMED_ELEMENT(WaveformRendererBeat) + + public: + QmlWaveformRendererBeat(); + + const QColor& color() const { + return m_color; + } + void setColor(QColor color) { + m_color = color; + emit colorChanged(m_color); + } + + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + signals: + void colorChanged(const QColor&); + + private: + QColor m_color; +}; + +class QmlWaveformMarkRange : public QObject { + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor) + Q_PROPERTY(QColor disabledColor READ disabledColor WRITE setDisabledColor) + Q_PROPERTY(double opacity READ opacity WRITE setOpacity) + Q_PROPERTY(double disabledOpacity READ disabledOpacity WRITE setDisabledOpacity) + Q_PROPERTY(QColor durationTextColor READ durationTextColor WRITE setDurationTextColor) + Q_PROPERTY(QString startControl READ startControl WRITE setStartControl) + Q_PROPERTY(QString endControl READ endControl WRITE setEndControl) + Q_PROPERTY(QString enabledControl READ enabledControl WRITE setEnabledControl) + Q_PROPERTY(QString visibilityControl READ visibilityControl WRITE setVisibilityControl) + Q_PROPERTY(QString durationTextLocation READ durationTextLocation WRITE setDurationTextLocation) + QML_NAMED_ELEMENT(WaveformMarkRange) + + public: + QColor color() const { + return m_color; + } + + void setColor(const QColor& value) { + m_color = value; + } + + QColor disabledColor() const { + return m_disabledColor; + } + + void setDisabledColor(const QColor& value) { + m_disabledColor = value; + } + + double opacity() const { + return m_opacity; + } + + void setOpacity(double value) { + m_opacity = value; + } + + double disabledOpacity() const { + return m_disabledOpacity; + } + + void setDisabledOpacity(double value) { + m_disabledOpacity = value; + } + + QColor durationTextColor() const { + return m_durationTextColor; + } + + void setDurationTextColor(const QColor& value) { + m_durationTextColor = value; + } + + QString startControl() const { + return m_startControl; + } + + void setStartControl(const QString& value) { + m_startControl = value; + } + + QString endControl() const { + return m_endControl; + } + + void setEndControl(const QString& value) { + m_endControl = value; + } + + QString enabledControl() const { + return m_enabledControl; + } + + void setEnabledControl(const QString& value) { + m_enabledControl = value; + } + + QString visibilityControl() const { + return m_visibilityControl; + } + + void setVisibilityControl(const QString& value) { + m_visibilityControl = value; + } + + QString durationTextLocation() const { + return m_durationTextLocation; + } + + void setDurationTextLocation(const QString& value) { + m_durationTextLocation = value; + } + + private: + double m_opacity{0.5}; + double m_disabledOpacity{0.5}; + QColor m_color; + QColor m_disabledColor; + QColor m_durationTextColor; + QString m_startControl; + QString m_endControl; + QString m_enabledControl; + QString m_visibilityControl; + QString m_durationTextLocation; +}; + +class QmlWaveformMark : public QObject { + Q_OBJECT + Q_PROPERTY(QString control READ control WRITE setControl) + Q_PROPERTY(QString visibilityControl READ visibilityControl WRITE setVisibilityControl) + Q_PROPERTY(QString color READ color WRITE setColor) + Q_PROPERTY(QString textColor READ textColor WRITE setTextColor) + Q_PROPERTY(QString align READ align WRITE setAlign) + Q_PROPERTY(QString text READ text WRITE setText) + Q_PROPERTY(QString pixmap READ pixmap WRITE setPixmap) + Q_PROPERTY(QString icon READ icon WRITE setIcon) + QML_NAMED_ELEMENT(WaveformMark) + public: + QString control() const { + return m_control; + } + void setControl(const QString& value) { + m_control = value; + } + QString visibilityControl() const { + return m_visibilityControl; + } + void setVisibilityControl(const QString& value) { + m_visibilityControl = value; + } + QString color() const { + return m_color; + } + void setColor(const QString& value) { + m_color = value; + } + QString textColor() const { + return m_textColor; + } + void setTextColor(const QString& value) { + m_textColor = value; + } + QString align() const { + return m_align; + } + void setAlign(const QString& value) { + m_align = value; + } + QString text() const { + return m_text; + } + void setText(const QString& value) { + m_text = value; + } + QString pixmap() const { + return m_pixmap; + } + void setPixmap(const QString& value) { + m_pixmap = value; + } + QString icon() const { + return m_icon; + } + void setIcon(const QString& value) { + m_icon = value; + } + + private: + QString m_control; + QString m_visibilityControl; + QString m_color; + QString m_textColor; + QString m_align; + QString m_text; + QString m_pixmap; + QString m_icon; +}; + +class QmlWaveformUntilMark : public QObject { + Q_OBJECT + Q_PROPERTY(bool showTime READ showTime WRITE setShowTime) + Q_PROPERTY(bool showBeats READ showBeats WRITE setShowBeats) + Q_PROPERTY(HAlignment align READ align WRITE setAlign) + Q_PROPERTY(int textSize READ textSize WRITE setTextSize) + + QML_NAMED_ELEMENT(WaveformUntilMark) + public: + enum HAlignment { AlignTop = Qt::AlignTop, + AlignCenter = Qt::AlignCenter, + AlignBottom = Qt::AlignBottom }; + Q_ENUM(HAlignment) + + bool showTime() const { + return m_showTime; + } + void setShowTime(bool showTime) { + m_showTime = showTime; + } + bool showBeats() const { + return m_showBeats; + } + void setShowBeats(bool showBeats) { + m_showBeats = showBeats; + } + HAlignment align() const { + return m_align; + } + void setAlign(HAlignment align) { + m_align = align; + } + int textSize() const { + return m_textSize; + } + void setTextSize(int textSize) { + m_textSize = textSize; + } + + private: + bool m_showTime; + bool m_showBeats; + HAlignment m_align; + int m_textSize; +}; + +class QmlWaveformRendererMarkRange + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(QQmlListProperty ranges READ ranges) + Q_CLASSINFO("DefaultProperty", "ranges") + QML_NAMED_ELEMENT(WaveformRendererMarkRange) + + public: + QmlWaveformRendererMarkRange(); + + QQmlListProperty ranges() { + return {this, &m_ranges}; + } + + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + + private: + QList m_ranges; +}; + +class QmlWaveformRendererStem + : public QmlWaveformRendererFactory { + Q_OBJECT + QML_NAMED_ELEMENT(WaveformRendererStem) + + public: + QmlWaveformRendererStem(); + +#ifdef __STEM__ + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; +#else + Renderer create(WaveformWidgetRenderer* waveformWidget) const override { + return Renderer{}; + } + +#endif +}; + +class QmlWaveformRendererMark + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(QQmlListProperty marks READ marks) + Q_PROPERTY(QColor playMarkerColor READ playMarkerColor WRITE setPlayMarkerColor) + Q_PROPERTY(QColor playMarkerBackground READ playMarkerBackground WRITE setPlayMarkerBackground) + Q_PROPERTY(QmlWaveformMark* defaultMark READ defaultMark WRITE setDefaultMark) + Q_PROPERTY(QmlWaveformUntilMark* untilMark READ untilMark FINAL) + Q_CLASSINFO("DefaultProperty", "marks") + QML_NAMED_ELEMENT(WaveformRendererMark) + + public: + QmlWaveformRendererMark(); + + QmlWaveformMark* defaultMark() const { + return m_defaultMark; + } + + QmlWaveformUntilMark* untilMark() const { + return m_untilMark.get(); + } + void setDefaultMark(QmlWaveformMark* defaultMark) { + m_defaultMark = defaultMark; + } + + const QColor& playMarkerColor() const { + return m_playMarkerColor; + } + void setPlayMarkerColor(const QColor& playMarkerColor) { + m_playMarkerColor = playMarkerColor; + } + + const QColor& playMarkerBackground() const { + return m_playMarkerBackground; + } + void setPlayMarkerBackground(const QColor& playMarkerBackground) { + m_playMarkerBackground = playMarkerBackground; + } + + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + + QQmlListProperty marks() { + return {this, &m_marks}; + } + + private: + QColor m_playMarkerColor; + QColor m_playMarkerBackground; + QList m_marks; + QmlWaveformMark* m_defaultMark; + std::unique_ptr m_untilMark; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/waveform/renderers/allshader/waveformrenderbeat.cpp b/src/waveform/renderers/allshader/waveformrenderbeat.cpp index aa7de4ecfe5..2be51f23971 100644 --- a/src/waveform/renderers/allshader/waveformrenderbeat.cpp +++ b/src/waveform/renderers/allshader/waveformrenderbeat.cpp @@ -15,9 +15,11 @@ using namespace rendergraph; namespace allshader { WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget, - ::WaveformRendererAbstract::PositionSource type) + ::WaveformRendererAbstract::PositionSource type, + QColor color) : ::WaveformRendererAbstract(waveformWidget), - m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { + m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), + m_color(color) { initForRectangles(0); setUsePreprocess(true); } diff --git a/src/waveform/renderers/allshader/waveformrenderbeat.h b/src/waveform/renderers/allshader/waveformrenderbeat.h index f14520d8d39..406dac2188e 100644 --- a/src/waveform/renderers/allshader/waveformrenderbeat.h +++ b/src/waveform/renderers/allshader/waveformrenderbeat.h @@ -20,7 +20,8 @@ class allshader::WaveformRenderBeat final public: explicit WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = - ::WaveformRendererAbstract::Play); + ::WaveformRendererAbstract::Play, + QColor color = QColor()); // Pure virtual from WaveformRendererAbstract, not used void draw(QPainter* painter, QPaintEvent* event) override final; diff --git a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp index 23756e74f0d..acaaccdc280 100644 --- a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp +++ b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp @@ -24,10 +24,11 @@ using namespace rendergraph; namespace allshader { WaveformRendererEndOfTrack::WaveformRendererEndOfTrack( - WaveformWidgetRenderer* waveformWidget) + WaveformWidgetRenderer* waveformWidget, QColor color) : ::WaveformRendererAbstract(waveformWidget), m_pEndOfTrackControl(nullptr), - m_pTimeRemainingControl(nullptr) { + m_pTimeRemainingControl(nullptr), + m_color(color) { initForRectangles(0); setUsePreprocess(true); } diff --git a/src/waveform/renderers/allshader/waveformrendererendoftrack.h b/src/waveform/renderers/allshader/waveformrendererendoftrack.h index d43d901f677..9ecb4a9d155 100644 --- a/src/waveform/renderers/allshader/waveformrendererendoftrack.h +++ b/src/waveform/renderers/allshader/waveformrendererendoftrack.h @@ -22,7 +22,7 @@ class allshader::WaveformRendererEndOfTrack final public rendergraph::GeometryNode { public: explicit WaveformRendererEndOfTrack( - WaveformWidgetRenderer* waveformWidget); + WaveformWidgetRenderer* waveformWidget, QColor color = QColor()); // Pure virtual from WaveformRendererAbstract, not used void draw(QPainter* painter, QPaintEvent* event) override final; diff --git a/src/waveform/renderers/allshader/waveformrendererpreroll.cpp b/src/waveform/renderers/allshader/waveformrendererpreroll.cpp index 47caa6f3300..a8ca7db4ae6 100644 --- a/src/waveform/renderers/allshader/waveformrendererpreroll.cpp +++ b/src/waveform/renderers/allshader/waveformrendererpreroll.cpp @@ -64,8 +64,10 @@ namespace allshader { WaveformRendererPreroll::WaveformRendererPreroll( WaveformWidgetRenderer* waveformWidget, - ::WaveformRendererAbstract::PositionSource type) + ::WaveformRendererAbstract::PositionSource type, + QColor color) : ::WaveformRendererAbstract(waveformWidget), + m_color(color), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { setGeometry(std::make_unique(PatternMaterial::attributes(), 0)); setMaterial(std::make_unique()); diff --git a/src/waveform/renderers/allshader/waveformrendererpreroll.h b/src/waveform/renderers/allshader/waveformrendererpreroll.h index eb28459ef96..24af49a457d 100644 --- a/src/waveform/renderers/allshader/waveformrendererpreroll.h +++ b/src/waveform/renderers/allshader/waveformrendererpreroll.h @@ -21,7 +21,8 @@ class allshader::WaveformRendererPreroll final explicit WaveformRendererPreroll( WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = - ::WaveformRendererAbstract::Play); + ::WaveformRendererAbstract::Play, + QColor color = QColor(200, 25, 20)); ~WaveformRendererPreroll() override; // Pure virtual from WaveformRendererAbstract, not used diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.cpp b/src/waveform/renderers/allshader/waveformrendererrgb.cpp index de0700c2662..c144c7a60c0 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.cpp +++ b/src/waveform/renderers/allshader/waveformrendererrgb.cpp @@ -18,11 +18,24 @@ inline float math_pow2(float x) { } // namespace WaveformRendererRGB::WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + QColor axesColor, + QColor lowColor, + QColor midColor, + QColor highColor, +#endif ::WaveformRendererAbstract::PositionSource type, WaveformRendererSignalBase::Options options) : WaveformRendererSignalBase(waveformWidget), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), m_options(options) { +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + getRgbF(axesColor, &m_axesColor_r, &m_axesColor_g, &m_axesColor_b, &m_axesColor_a); + + getRgbF(lowColor, &m_rgbLowColor_r, &m_rgbLowColor_g, &m_rgbLowColor_b); + getRgbF(midColor, &m_rgbMidColor_r, &m_rgbMidColor_g, &m_rgbMidColor_b); + getRgbF(highColor, &m_rgbHighColor_r, &m_rgbHighColor_g, &m_rgbHighColor_b); +#endif initForRectangles(0); setUsePreprocess(true); } diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.h b/src/waveform/renderers/allshader/waveformrendererrgb.h index a829cf3af2f..ed44f3fbfad 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.h +++ b/src/waveform/renderers/allshader/waveformrendererrgb.h @@ -2,6 +2,7 @@ #include "rendergraph/geometrynode.h" #include "util/class.h" +#include "util/colorcomponents.h" #include "waveform/renderers/allshader/waveformrenderersignalbase.h" namespace allshader { @@ -13,6 +14,12 @@ class allshader::WaveformRendererRGB final public rendergraph::GeometryNode { public: explicit WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + QColor axesColor, + QColor lowColor, + QColor midColor, + QColor highColor, +#endif ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play, WaveformRendererSignalBase::Options options = WaveformRendererSignalBase::Option::None); diff --git a/src/waveform/renderers/allshader/waveformrendermark.cpp b/src/waveform/renderers/allshader/waveformrendermark.cpp index 35796ad8e9a..b7ff61244d4 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.cpp +++ b/src/waveform/renderers/allshader/waveformrendermark.cpp @@ -135,11 +135,27 @@ QString timeSecToString(double timeSec) { allshader::WaveformRenderMark::WaveformRenderMark( WaveformWidgetRenderer* waveformWidget, +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + QColor fgPlayColor, + QColor bgPlayColor, + bool untilMarkShowBeats, + bool untilMarkShowTime, + Qt::Alignment untilMarkAlign, + int untilMarkTextSize, +#endif ::WaveformRendererAbstract::PositionSource type) : ::WaveformRenderMarkBase(waveformWidget, false), m_beatsUntilMark(0), m_timeUntilMark(0.0), m_pTimeRemainingControl(nullptr), +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + m_fgPlayColor(fgPlayColor), + m_bgPlayColor(bgPlayColor), + m_untilMarkShowBeats(untilMarkShowBeats), + m_untilMarkShowTime(untilMarkShowTime), + m_untilMarkAlign(untilMarkAlign), + m_untilMarkTextSize(untilMarkTextSize), +#endif m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), m_playPosHeight(0.f), m_playPosDevicePixelRatio(0.f) { @@ -361,14 +377,25 @@ void allshader::WaveformRenderMark::update() { {1.f, 1.f}); } +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + if (m_untilMarkShowBeats || m_untilMarkShowTime) +#else if (WaveformWidgetFactory::instance()->getUntilMarkShowBeats() || - WaveformWidgetFactory::instance()->getUntilMarkShowTime()) { + WaveformWidgetFactory::instance()->getUntilMarkShowTime()) +#endif + { updateUntilMark(playPosition, nextMarkPosition); updateDigitsNodeForUntilMark(roundToPixel(playMarkerPos + 20.f)); } } void allshader::WaveformRenderMark::updateDigitsNodeForUntilMark(float x) { +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + const bool untilMarkShowBeats = m_untilMarkShowBeats; + const bool untilMarkShowTime = m_untilMarkShowTime; + const auto untilMarkAlign = m_untilMarkAlign; + const auto untilMarkTextPointSize = m_untilMarkTextSize; +#else const bool untilMarkShowBeats = WaveformWidgetFactory::instance()->getUntilMarkShowBeats(); const bool untilMarkShowTime = WaveformWidgetFactory::instance()->getUntilMarkShowTime(); const auto untilMarkAlign = WaveformWidgetFactory::instance()->getUntilMarkAlign(); @@ -462,8 +489,13 @@ void allshader::WaveformRenderMark::updatePlayPosMarkTexture(rendergraph::Contex painter.setWorldMatrixEnabled(false); +#ifdef __RENDERGRAPH_IS_OPENGL const QColor fgColor{m_waveformRenderer->getWaveformSignalColors()->getPlayPosColor()}; const QColor bgColor{m_waveformRenderer->getWaveformSignalColors()->getBgColor()}; +#else + const QColor& fgColor = m_fgPlayColor; + const QColor& bgColor = m_bgPlayColor; +#endif // draw dim outlines to increase playpos/waveform contrast painter.setPen(bgColor); diff --git a/src/waveform/renderers/allshader/waveformrendermark.h b/src/waveform/renderers/allshader/waveformrendermark.h index 511dbaa8993..5b8fef1cc4e 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.h +++ b/src/waveform/renderers/allshader/waveformrendermark.h @@ -22,6 +22,14 @@ class allshader::WaveformRenderMark : public ::WaveformRenderMarkBase, public rendergraph::Node { public: explicit WaveformRenderMark(WaveformWidgetRenderer* waveformWidget, +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + QColor fgPlayColor, + QColor bgPlayColor, + bool untilMarkShowBeats, + bool untilMarkShowTime, + Qt::Alignment untilMarkAlign, + int untilMarkTextSize, +#endif ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play); @@ -69,5 +77,15 @@ class allshader::WaveformRenderMark : public ::WaveformRenderMarkBase, DigitsRenderNode* m_pDigitsRenderNode{}; +#ifdef __RENDERGRAPH_IS_SCENEGRAPH + QColor m_fgPlayColor; + QColor m_bgPlayColor; + + bool m_untilMarkShowBeats; + bool m_untilMarkShowTime; + Qt::Alignment m_untilMarkAlign; + int m_untilMarkTextSize; +#endif + DISALLOW_COPY_AND_ASSIGN(WaveformRenderMark); }; diff --git a/src/waveform/renderers/waveformmark.cpp b/src/waveform/renderers/waveformmark.cpp index e90f2f4e02b..8200a6e6a95 100644 --- a/src/waveform/renderers/waveformmark.cpp +++ b/src/waveform/renderers/waveformmark.cpp @@ -93,6 +93,75 @@ bool isShowUntilNextPositionControl(const QString& positionControl) { } // anonymous namespace +WaveformMark::WaveformMark(const QString& group, + QString positionControl, + QString visibilityControl, + QString textColor, + QString markAlign, + QString text, + QString pixmapPath, + QString iconPath, + QColor color, + int priority, + int hotCue, + const WaveformSignalColors& signalColors) + : m_textColor(textColor), + m_pixmapPath(pixmapPath), + m_iconPath(iconPath), + m_linePosition{}, + m_breadth{}, + m_level{}, + m_iPriority(priority), + m_iHotCue(hotCue), + m_showUntilNext{} { + QString endPositionControl; + QString typeControl; + if (hotCue != Cue::kNoHotCue) { + positionControl = "hotcue_" + QString::number(hotCue + 1) + "_position"; + endPositionControl = "hotcue_" + QString::number(hotCue + 1) + "_endposition"; + typeControl = "hotcue_" + QString::number(hotCue + 1) + "_type"; + m_showUntilNext = true; + } else { + m_showUntilNext = isShowUntilNextPositionControl(positionControl); + } + + if (!positionControl.isEmpty()) { + m_pPositionCO = std::make_unique(group, positionControl); + } + if (!endPositionControl.isEmpty()) { + m_pEndPositionCO = std::make_unique(group, endPositionControl); + m_pTypeCO = std::make_unique(group, typeControl); + } + + if (!visibilityControl.isEmpty()) { + ConfigKey key = ConfigKey::parseCommaSeparated(visibilityControl); + m_pVisibleCO = std::make_unique(key); + } + + if (!color.isValid()) { + // As a fallback, grab the color from the parent's AxesColor + // color = signalColors.getAxesColor(); + qDebug() << "Didn't get mark :" << color; + } else { + color = WSkinColor::getCorrectColor(color); + } + int dimBrightThreshold = signalColors.getDimBrightThreshold(); + setBaseColor(color, dimBrightThreshold); + + if (!m_textColor.isValid()) { + // Read the text color, otherwise use the parent's BgColor. + m_textColor = signalColors.getBgColor(); + qDebug() << "Didn't get mark , using parent's :" << m_textColor; + } + + m_align = decodeAlignmentFlags(markAlign, Qt::AlignBottom | Qt::AlignHCenter); + + // Hotcue text is set by the cue's label in the database, not by the skin. + if (hotCue == Cue::kNoHotCue) { + m_text = text; + } +} + WaveformMark::WaveformMark(const QString& group, const QDomNode& node, const SkinContext& context, diff --git a/src/waveform/renderers/waveformmark.h b/src/waveform/renderers/waveformmark.h index c40af8927d9..c02e3a01404 100644 --- a/src/waveform/renderers/waveformmark.h +++ b/src/waveform/renderers/waveformmark.h @@ -31,6 +31,20 @@ class WaveformMark { int priority, const WaveformSignalColors& signalColors, int hotCue = Cue::kNoHotCue); + + WaveformMark( + const QString& group, + QString positionControl, + QString visibilityControl, + QString textColor, + QString markAlign, + QString text, + QString pixmapPath, + QString iconPath, + QColor color, + int priority, + int hotCue = Cue::kNoHotCue, + const WaveformSignalColors& signalColors = {}); ~WaveformMark(); // Disable copying diff --git a/src/waveform/renderers/waveformmarkrange.cpp b/src/waveform/renderers/waveformmarkrange.cpp index 1df2c6c7979..4cc3d07a4ec 100644 --- a/src/waveform/renderers/waveformmarkrange.cpp +++ b/src/waveform/renderers/waveformmarkrange.cpp @@ -75,6 +75,66 @@ WaveformMarkRange::WaveformMarkRange( } } +WaveformMarkRange::WaveformMarkRange( + const QString& group, + const QColor& activeColor, + const QColor& disabledColor, + double enabledOpacity, + double disabledOpacity, + const QColor& durationTextColor, + const QString& startControl, + const QString& endControl, + const QString& enabledControl, + const QString& visibilityControl, + const QString& durationTextLocation) + : m_activeColor(activeColor), + m_disabledColor(disabledColor), + m_enabledOpacity(enabledOpacity), + m_disabledOpacity(disabledOpacity), + m_durationTextColor(durationTextColor) { + if (!startControl.isEmpty()) { + DEBUG_ASSERT(!m_markStartPointControl); // has not been created yet + m_markStartPointControl = std::make_unique(group, startControl); + } + if (!endControl.isEmpty()) { + DEBUG_ASSERT(!m_markEndPointControl); // has not been created yet + m_markEndPointControl = std::make_unique(group, endControl); + } + + if (!enabledControl.isEmpty()) { + DEBUG_ASSERT(!m_markEnabledControl); // has not been created yet + m_markEnabledControl = std::make_unique(group, enabledControl); + } + if (!visibilityControl.isEmpty()) { + DEBUG_ASSERT(!m_markVisibleControl); // has not been created yet + ConfigKey key = ConfigKey::parseCommaSeparated(visibilityControl); + m_markVisibleControl = std::make_unique(key); + } + + if (durationTextLocation == "before") { + m_durationTextLocation = DurationTextLocation::Before; + } else { + m_durationTextLocation = DurationTextLocation::After; + } + + m_activeColor = WSkinColor::getCorrectColor(m_activeColor); + + if (!m_disabledColor.isValid()) { + if (enabledControl.isEmpty()) { + m_disabledColor = QColor(Qt::transparent); + } else { + // Show warning only when there's no EnabledControl, + // like for intro & outro ranges. + QString rangeSuffix = QStringLiteral("_start_position"); + QString rangeName = QString(startControl).remove(rangeSuffix); + int gray = qGray(m_activeColor.rgb()); + m_disabledColor = QColor(gray, gray, gray); + qDebug() << "Didn't get DisabledColor for mark range" << rangeName + << "- using desaturated Color:" << m_disabledColor; + } + } +} + bool WaveformMarkRange::active() const { const double startValue = start(); const double endValue = end(); diff --git a/src/waveform/renderers/waveformmarkrange.h b/src/waveform/renderers/waveformmarkrange.h index 5ac530e86af..63c0fe42f53 100644 --- a/src/waveform/renderers/waveformmarkrange.h +++ b/src/waveform/renderers/waveformmarkrange.h @@ -24,6 +24,18 @@ class WaveformMarkRange { const QDomNode& node, const SkinContext& context, const WaveformSignalColors& signalColors); + WaveformMarkRange( + const QString& group, + const QColor& activeColor, + const QColor& disabledColor, + double enabledOpacity, + double disabledOpacity, + const QColor& durationTextColor, + const QString& startControl, + const QString& endControl, + const QString& enabledControl, + const QString& visibilityControl, + const QString& durationTextLocation); // This class is only moveable, but not copiable! WaveformMarkRange(WaveformMarkRange&&) = default; WaveformMarkRange(const WaveformMarkRange&) = delete; diff --git a/src/waveform/renderers/waveformmarkset.cpp b/src/waveform/renderers/waveformmarkset.cpp index 8fe24730146..0fa78e5e1e4 100644 --- a/src/waveform/renderers/waveformmarkset.cpp +++ b/src/waveform/renderers/waveformmarkset.cpp @@ -22,14 +22,21 @@ void WaveformMarkSet::setup(const QString& group, const QDomNode& node, bool hasDefaultMark = false; QDomNode child = node.firstChild(); - QDomNode defaultChild; + Seed defaultModel; int priority = 0; while (!child.isNull()) { if (child.nodeName() == "DefaultMark") { - m_pDefaultMark = WaveformMarkPointer(new WaveformMark( - group, child, context, --priority, signalColors)); + defaultModel = Seed{ + context.selectString(node, "Control"), + context.selectString(node, "VisibilityControl"), + context.selectString(node, "TextColor"), + context.selectString(node, "Align"), + context.selectString(node, "Text"), + context.selectString(node, "Pixmap"), + context.selectString(node, "Icon"), + context.selectString(node, "Color"), + }; hasDefaultMark = true; - defaultChild = child; } else if (child.nodeName() == "Mark") { WaveformMarkPointer pMark(new WaveformMark( group, child, context, --priority, signalColors)); @@ -39,7 +46,7 @@ void WaveformMarkSet::setup(const QString& group, const QDomNode& node, if (!controlItemSet.insert(item).second) { qWarning() << "WaveformRenderMark::setup - redefinition of" << item; } else { - m_marks.push_back(pMark); + addMark(pMark); if (pMark->getHotCue() >= 0) { m_hotCueMarks.insert(pMark->getHotCue(), pMark); } @@ -52,14 +59,46 @@ void WaveformMarkSet::setup(const QString& group, const QDomNode& node, // check if there is a default mark and compare declared // and to create all missing hot_cues if (hasDefaultMark) { - for (int i = 0; i < NUM_HOT_CUES; ++i) { - if (m_hotCueMarks.value(i).isNull()) { - //qDebug() << "WaveformRenderMark::setup - Automatic mark" << hotCueControlItem; - WaveformMarkPointer pMark(new WaveformMark( - group, defaultChild, context, i, signalColors, i)); - m_marks.push_front(pMark); - m_hotCueMarks.insert(pMark->getHotCue(), pMark); - } + setDefault(group, defaultModel, signalColors); + } +} + +void WaveformMarkSet::setDefault(const QString& group, + const Seed& model, + const WaveformSignalColors& signalColors) { + m_pDefaultMark = WaveformMarkPointer(new WaveformMark( + + group, + model.positionControl, + model.visibilityControl, + model.textColor, + model.markAlign, + model.text, + model.pixmapPath, + model.iconPath, + model.color, + 0, + Cue::kNoHotCue, + signalColors)); + for (int i = 0; i < NUM_HOT_CUES; ++i) { + if (m_hotCueMarks.value(i).isNull()) { + // qDebug() << "WaveformRenderMark::setup - Automatic mark" << hotCueControlItem; + WaveformMarkPointer pMark(new WaveformMark( + + group, + model.positionControl, + model.visibilityControl, + model.textColor, + model.markAlign, + model.text, + model.pixmapPath, + model.iconPath, + model.color, + i, + i, + signalColors)); + m_marks.push_front(pMark); + m_hotCueMarks.insert(pMark->getHotCue(), pMark); } } } diff --git a/src/waveform/renderers/waveformmarkset.h b/src/waveform/renderers/waveformmarkset.h index 2ab37568f2f..d6e5b0d158b 100644 --- a/src/waveform/renderers/waveformmarkset.h +++ b/src/waveform/renderers/waveformmarkset.h @@ -11,6 +11,17 @@ // rendered. class WaveformMarkSet { public: + struct Seed { + QString positionControl; + QString visibilityControl; + QString textColor; + QString markAlign; + QString text; + QString pixmapPath; + QString iconPath; + QColor color; + }; + WaveformMarkSet(); virtual ~WaveformMarkSet(); @@ -67,11 +78,20 @@ class WaveformMarkSet { void setBreadth(float breadth); - private: void clear() { m_marks.clear(); m_marksToRender.clear(); } + + void addMark(WaveformMarkPointer pMark) { + m_marks.push_back(pMark); + } + + void setDefault(const QString& group, + const Seed& model, + const WaveformSignalColors& signalColors = {}); + + private: WaveformMarkPointer m_pDefaultMark; QList m_marks; // List of visible WaveformMarks sorted by the order they appear in the track diff --git a/src/waveform/renderers/waveformrendererabstract.h b/src/waveform/renderers/waveformrendererabstract.h index 7fc2b78529d..9d4f84a629e 100644 --- a/src/waveform/renderers/waveformrendererabstract.h +++ b/src/waveform/renderers/waveformrendererabstract.h @@ -6,6 +6,10 @@ QT_FORWARD_DECLARE_CLASS(QDomNode) QT_FORWARD_DECLARE_CLASS(QPaintEvent) QT_FORWARD_DECLARE_CLASS(QPainter) +namespace rendergraph { +class Node; +} + class SkinContext; class WaveformWidgetRenderer; diff --git a/src/waveform/renderers/waveformrendermarkbase.h b/src/waveform/renderers/waveformrendermarkbase.h index 8efc35aebd4..bc242b9aa2f 100644 --- a/src/waveform/renderers/waveformrendermarkbase.h +++ b/src/waveform/renderers/waveformrendermarkbase.h @@ -24,6 +24,18 @@ class WaveformRenderMarkBase : public QObject, public WaveformRendererAbstract { void onResize() override; + void clearMarks() { + m_marks.clear(); + } + + void setDefaultMark(const QString& group, const WaveformMarkSet::Seed& model) { + m_marks.setDefault(group, model); + } + + void addMark(WaveformMarkPointer pMark) { + m_marks.addMark(pMark); + } + public slots: // Called when the loaded track's cues are added, deleted or modified and // when a new track is loaded. diff --git a/src/waveform/renderers/waveformwidgetrenderer.cpp b/src/waveform/renderers/waveformwidgetrenderer.cpp index 1fb0ad27834..11b4e709b60 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.cpp +++ b/src/waveform/renderers/waveformwidgetrenderer.cpp @@ -5,9 +5,12 @@ #include "control/controlproxy.h" #include "track/track.h" +#include "util/assert.h" #include "util/math.h" +#include "waveform/isynctimeprovider.h" #include "waveform/renderers/waveformrendererabstract.h" #include "waveform/visualplayposition.h" +#include "waveform/vsyncthread.h" #include "waveform/waveform.h" const double WaveformWidgetRenderer::s_waveformMinZoom = 1.0; @@ -83,7 +86,27 @@ WaveformWidgetRenderer::~WaveformWidgetRenderer() { } bool WaveformWidgetRenderer::init() { - //qDebug() << "WaveformWidgetRenderer::init, m_group=" << m_group; + m_trackPixelCount = 0.0; + m_zoomFactor = 1.0; + m_visualSamplePerPixel = 1.0; + m_audioSamplePerPixel = 1.0; + m_totalVSamples = 0; + m_gain = 1.0; + m_trackSamples = 0.0; + + for (int type = ::WaveformRendererAbstract::Play; + type <= ::WaveformRendererAbstract::Slip; + type++) { + m_firstDisplayedPosition[type] = 0.0; + m_lastDisplayedPosition[type] = 0.0; + m_posVSample[type] = 0.0; + m_pos[type] = -1.0; // disable renderers + m_truePosSample[type] = -1.0; + } + + VERIFY_OR_DEBUG_ASSERT(!m_group.isEmpty()) { + return false; + } m_visualPlayPosition = VisualPlayPosition::getVisualPlayPosition(m_group); @@ -102,7 +125,7 @@ bool WaveformWidgetRenderer::init() { return true; } -void WaveformWidgetRenderer::onPreRender(VSyncThread* vsyncThread) { +void WaveformWidgetRenderer::onPreRender(ISyncTimeProvider* vsyncThread) { if (m_passthroughEnabled) { // disables renderers in draw() for (int type = ::WaveformRendererAbstract::Play; diff --git a/src/waveform/renderers/waveformwidgetrenderer.h b/src/waveform/renderers/waveformwidgetrenderer.h index 42d02f49e0c..c0c40cecd5e 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.h +++ b/src/waveform/renderers/waveformwidgetrenderer.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "track/track_decl.h" #include "util/class.h" #include "waveform/renderers/waveformmark.h" @@ -11,7 +13,7 @@ class ControlProxy; class VisualPlayPosition; -class VSyncThread; +class ISyncTimeProvider; class QPainter; class WaveformRendererAbstract; @@ -32,20 +34,25 @@ class WaveformWidgetRenderer { }; public: - explicit WaveformWidgetRenderer(const QString& group); + explicit WaveformWidgetRenderer(const QString& group = {}); virtual ~WaveformWidgetRenderer(); bool init(); virtual bool onInit() {return true;} void setup(const QDomNode& node, const SkinContext& context); - void onPreRender(VSyncThread* vsyncThread); + void onPreRender(ISyncTimeProvider* vsyncThread); void draw(QPainter* painter, QPaintEvent* event); const QString& getGroup() const { return m_group; } + virtual void setGroup(const QString& group) { + m_group = group; + init(); + } + const TrackPointer& getTrackInfo() const { return m_pTrack; } @@ -119,7 +126,7 @@ class WaveformWidgetRenderer { int getTotalVSample() const { return m_totalVSamples; } - double getZoomFactor() const { + double getZoom() const { return m_zoomFactor; } double getGain(bool applyCompensation) const { @@ -183,6 +190,11 @@ class WaveformWidgetRenderer { #ifdef __STEM__ void selectStem(mixxx::StemChannelSelection stemMask); #endif + + void addRenderer(WaveformRendererAbstract* renderer) { + m_rendererStack.push_back(renderer); + } + void setTrack(TrackPointer track); void setMarkPositions(const QList& markPositions) { m_markPositions = markPositions; @@ -214,7 +226,7 @@ class WaveformWidgetRenderer { } protected: - const QString m_group; + QString m_group; TrackPointer m_pTrack; #ifdef __STEM__ uint m_selectedStems; diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index 467060ebcce..b7ea1faba51 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -609,7 +609,7 @@ bool WaveformWidgetFactory::setWidgetTypeFromHandle(int handleIndex, bool force) WaveformWidgetAbstract* previousWidget = holder.m_waveformWidget; TrackPointer pTrack = previousWidget->getTrackInfo(); //previousWidget->hold(); - double previousZoom = previousWidget->getZoomFactor(); + double previousZoom = previousWidget->getZoom(); double previousPlayMarkerPosition = previousWidget->getPlayMarkerPosition(); int previousbeatgridAlpha = previousWidget->getBeatGridAlpha(); delete previousWidget; @@ -656,7 +656,7 @@ void WaveformWidgetFactory::setZoomSync(bool sync) { return; } - double refZoom = m_waveformWidgetHolders[0].m_waveformWidget->getZoomFactor(); + double refZoom = m_waveformWidgetHolders[0].m_waveformWidget->getZoom(); for (const auto& holder : std::as_const(m_waveformWidgetHolders)) { holder.m_waveformViewer->setZoom(refZoom); } @@ -711,7 +711,7 @@ void WaveformWidgetFactory::notifyZoomChange(WWaveformViewer* viewer) { if (pWaveformWidget == nullptr || !isZoomSync()) { return; } - double refZoom = pWaveformWidget->getZoomFactor(); + double refZoom = pWaveformWidget->getZoom(); for (const auto& holder : std::as_const(m_waveformWidgetHolders)) { if (holder.m_waveformViewer != viewer) { @@ -1185,12 +1185,14 @@ void WaveformWidgetFactory::startVSync(GuiTick* pGuiTick, VisualsManager* pVisua #ifdef MIXXX_USE_QOPENGL if (m_vsyncThread->vsyncMode() == VSyncThread::ST_PLL) { WGLWidget* widget = SharedGLContext::getWidget(); - connect(widget->getOpenGLWindow(), - &QOpenGLWindow::frameSwapped, - this, - &WaveformWidgetFactory::slotFrameSwapped, - Qt::DirectConnection); - widget->show(); + if (widget) { + connect(widget->getOpenGLWindow(), + &QOpenGLWindow::frameSwapped, + this, + &WaveformWidgetFactory::slotFrameSwapped, + Qt::DirectConnection); + widget->show(); + } } #endif diff --git a/src/waveform/widgets/waveformwidgetabstract.cpp b/src/waveform/widgets/waveformwidgetabstract.cpp index d678d4635f4..5b83ca58790 100644 --- a/src/waveform/widgets/waveformwidgetabstract.cpp +++ b/src/waveform/widgets/waveformwidgetabstract.cpp @@ -3,6 +3,7 @@ #include #include "waveform/renderers/waveformwidgetrenderer.h" +#include "waveform/vsyncthread.h" WaveformWidgetAbstract::WaveformWidgetAbstract(const QString& group) : WaveformWidgetRenderer(group), diff --git a/src/widget/wspinnybase.cpp b/src/widget/wspinnybase.cpp index a0baf58f70e..14414fecf32 100644 --- a/src/widget/wspinnybase.cpp +++ b/src/widget/wspinnybase.cpp @@ -15,6 +15,7 @@ #include "util/fpclassify.h" #include "vinylcontrol/vinylcontrolmanager.h" #include "waveform/visualplayposition.h" +#include "waveform/vsyncthread.h" #include "wimagestore.h" // The SampleBuffers format enables antialiasing. diff --git a/src/widget/wwaveformviewer.cpp b/src/widget/wwaveformviewer.cpp index f9e41713aca..9b390bcd936 100644 --- a/src/widget/wwaveformviewer.cpp +++ b/src/widget/wwaveformviewer.cpp @@ -195,9 +195,9 @@ void WWaveformViewer::mouseReleaseEvent(QMouseEvent* /*event*/) { void WWaveformViewer::wheelEvent(QWheelEvent* event) { if (m_waveformWidget) { if (event->angleDelta().y() > 0) { - onZoomChange(m_waveformWidget->getZoomFactor() / 1.05); + onZoomChange(m_waveformWidget->getZoom() / 1.05); } else if (event->angleDelta().y() < 0) { - onZoomChange(m_waveformWidget->getZoomFactor() * 1.05); + onZoomChange(m_waveformWidget->getZoom() * 1.05); } } } From d4a5281c000ca98bdc27d6a09fb32dcfce684004 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 1 Dec 2024 22:27:35 +0000 Subject: [PATCH 12/22] fixup! feat: add scrolling waveform in QML using scenegraph --- src/qml/qmlwaveformrenderer.cpp | 19 +++++++---- .../allshader/waveformrenderbeat.cpp | 6 ++-- .../renderers/allshader/waveformrenderbeat.h | 7 ++-- .../allshader/waveformrendererendoftrack.cpp | 5 ++- .../allshader/waveformrendererendoftrack.h | 6 +++- .../allshader/waveformrendererpreroll.cpp | 4 +-- .../allshader/waveformrendererpreroll.h | 7 ++-- .../allshader/waveformrendererrgb.cpp | 18 +++++----- .../renderers/allshader/waveformrendererrgb.h | 11 +++--- .../allshader/waveformrendermark.cpp | 34 +++++++++---------- .../renderers/allshader/waveformrendermark.h | 15 ++++---- 11 files changed, 69 insertions(+), 63 deletions(-) diff --git a/src/qml/qmlwaveformrenderer.cpp b/src/qml/qmlwaveformrenderer.cpp index 610264ec3d5..3aee771a97d 100644 --- a/src/qml/qmlwaveformrenderer.cpp +++ b/src/qml/qmlwaveformrenderer.cpp @@ -44,32 +44,36 @@ QmlWaveformRendererMark::QmlWaveformRendererMark() QmlWaveformRendererFactory::Renderer QmlWaveformRendererEndOfTrack::create( WaveformWidgetRenderer* waveformWidget) const { - auto* renderer = new WaveformRendererEndOfTrack(waveformWidget, m_color); + auto* renderer = new WaveformRendererEndOfTrack(waveformWidget); + renderer->setup(m_color); return QmlWaveformRendererFactory::Renderer{renderer, renderer}; } QmlWaveformRendererFactory::Renderer QmlWaveformRendererPreroll::create( WaveformWidgetRenderer* waveformWidget) const { auto* renderer = new WaveformRendererPreroll( - waveformWidget, WaveformRendererAbstract::Play, m_color); + waveformWidget, WaveformRendererAbstract::Play); + renderer->setup(m_color); return QmlWaveformRendererFactory::Renderer{renderer, renderer}; } QmlWaveformRendererFactory::Renderer QmlWaveformRendererRGB::create( WaveformWidgetRenderer* waveformWidget) const { auto* renderer = new WaveformRendererRGB(waveformWidget, + ::WaveformRendererAbstract::Play); + renderer->setup( m_axesColor, m_lowColor, m_midColor, - m_highColor, - ::WaveformRendererAbstract::Play); + m_highColor); return QmlWaveformRendererFactory::Renderer{renderer, renderer}; } QmlWaveformRendererFactory::Renderer QmlWaveformRendererBeat::create( WaveformWidgetRenderer* waveformWidget) const { auto* renderer = new WaveformRenderBeat( - waveformWidget, ::WaveformRendererAbstract::Play, m_color); + waveformWidget, ::WaveformRendererAbstract::Play); + renderer->setup(m_color); return QmlWaveformRendererFactory::Renderer{renderer, renderer}; } @@ -110,13 +114,14 @@ QmlWaveformRendererFactory::Renderer QmlWaveformRendererMark::create( return QmlWaveformRendererFactory::Renderer{}; } auto* renderer = new WaveformRenderMark(waveformWidget, + ::WaveformRendererAbstract::Play); + renderer->setup( m_playMarkerColor, m_playMarkerBackground, m_untilMark->showTime(), m_untilMark->showBeats(), static_cast(m_untilMark->align()), - m_untilMark->textSize(), - ::WaveformRendererAbstract::Play); + m_untilMark->textSize()); int priority = 0; for (auto* pMark : m_marks) { renderer->addMark(WaveformMarkPointer(new WaveformMark( diff --git a/src/waveform/renderers/allshader/waveformrenderbeat.cpp b/src/waveform/renderers/allshader/waveformrenderbeat.cpp index 2be51f23971..aa7de4ecfe5 100644 --- a/src/waveform/renderers/allshader/waveformrenderbeat.cpp +++ b/src/waveform/renderers/allshader/waveformrenderbeat.cpp @@ -15,11 +15,9 @@ using namespace rendergraph; namespace allshader { WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget, - ::WaveformRendererAbstract::PositionSource type, - QColor color) + ::WaveformRendererAbstract::PositionSource type) : ::WaveformRendererAbstract(waveformWidget), - m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), - m_color(color) { + m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { initForRectangles(0); setUsePreprocess(true); } diff --git a/src/waveform/renderers/allshader/waveformrenderbeat.h b/src/waveform/renderers/allshader/waveformrenderbeat.h index 406dac2188e..2d572ccbdf1 100644 --- a/src/waveform/renderers/allshader/waveformrenderbeat.h +++ b/src/waveform/renderers/allshader/waveformrenderbeat.h @@ -20,14 +20,17 @@ class allshader::WaveformRenderBeat final public: explicit WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = - ::WaveformRendererAbstract::Play, - QColor color = QColor()); + ::WaveformRendererAbstract::Play); // Pure virtual from WaveformRendererAbstract, not used void draw(QPainter* painter, QPaintEvent* event) override final; void setup(const QDomNode& node, const SkinContext& skinContext) override; + void setup(const QColor& color) { + m_color = color; + } + // Virtuals for rendergraph::Node void preprocess() override; diff --git a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp index acaaccdc280..23756e74f0d 100644 --- a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp +++ b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp @@ -24,11 +24,10 @@ using namespace rendergraph; namespace allshader { WaveformRendererEndOfTrack::WaveformRendererEndOfTrack( - WaveformWidgetRenderer* waveformWidget, QColor color) + WaveformWidgetRenderer* waveformWidget) : ::WaveformRendererAbstract(waveformWidget), m_pEndOfTrackControl(nullptr), - m_pTimeRemainingControl(nullptr), - m_color(color) { + m_pTimeRemainingControl(nullptr) { initForRectangles(0); setUsePreprocess(true); } diff --git a/src/waveform/renderers/allshader/waveformrendererendoftrack.h b/src/waveform/renderers/allshader/waveformrendererendoftrack.h index 9ecb4a9d155..7a9b9583f95 100644 --- a/src/waveform/renderers/allshader/waveformrendererendoftrack.h +++ b/src/waveform/renderers/allshader/waveformrendererendoftrack.h @@ -22,13 +22,17 @@ class allshader::WaveformRendererEndOfTrack final public rendergraph::GeometryNode { public: explicit WaveformRendererEndOfTrack( - WaveformWidgetRenderer* waveformWidget, QColor color = QColor()); + WaveformWidgetRenderer* waveformWidge); // Pure virtual from WaveformRendererAbstract, not used void draw(QPainter* painter, QPaintEvent* event) override final; void setup(const QDomNode& node, const SkinContext& skinContext) override; + void setup(const QColor& color) { + m_color = color; + } + bool init() override; // Virtual for rendergraph::Node diff --git a/src/waveform/renderers/allshader/waveformrendererpreroll.cpp b/src/waveform/renderers/allshader/waveformrendererpreroll.cpp index a8ca7db4ae6..47caa6f3300 100644 --- a/src/waveform/renderers/allshader/waveformrendererpreroll.cpp +++ b/src/waveform/renderers/allshader/waveformrendererpreroll.cpp @@ -64,10 +64,8 @@ namespace allshader { WaveformRendererPreroll::WaveformRendererPreroll( WaveformWidgetRenderer* waveformWidget, - ::WaveformRendererAbstract::PositionSource type, - QColor color) + ::WaveformRendererAbstract::PositionSource type) : ::WaveformRendererAbstract(waveformWidget), - m_color(color), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip) { setGeometry(std::make_unique(PatternMaterial::attributes(), 0)); setMaterial(std::make_unique()); diff --git a/src/waveform/renderers/allshader/waveformrendererpreroll.h b/src/waveform/renderers/allshader/waveformrendererpreroll.h index 24af49a457d..970f81c7262 100644 --- a/src/waveform/renderers/allshader/waveformrendererpreroll.h +++ b/src/waveform/renderers/allshader/waveformrendererpreroll.h @@ -21,8 +21,7 @@ class allshader::WaveformRendererPreroll final explicit WaveformRendererPreroll( WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = - ::WaveformRendererAbstract::Play, - QColor color = QColor(200, 25, 20)); + ::WaveformRendererAbstract::Play); ~WaveformRendererPreroll() override; // Pure virtual from WaveformRendererAbstract, not used @@ -30,6 +29,10 @@ class allshader::WaveformRendererPreroll final void setup(const QDomNode& node, const SkinContext& skinContext) override; + void setup(const QColor& color) { + m_color = color; + } + // Virtual for rendergraph::Node void preprocess() override; diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.cpp b/src/waveform/renderers/allshader/waveformrendererrgb.cpp index c144c7a60c0..caf950feacc 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.cpp +++ b/src/waveform/renderers/allshader/waveformrendererrgb.cpp @@ -18,26 +18,24 @@ inline float math_pow2(float x) { } // namespace WaveformRendererRGB::WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, -#ifdef __RENDERGRAPH_IS_SCENEGRAPH - QColor axesColor, - QColor lowColor, - QColor midColor, - QColor highColor, -#endif ::WaveformRendererAbstract::PositionSource type, WaveformRendererSignalBase::Options options) : WaveformRendererSignalBase(waveformWidget), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), m_options(options) { -#ifdef __RENDERGRAPH_IS_SCENEGRAPH + initForRectangles(0); + setUsePreprocess(true); +} + +void WaveformRendererRGB::setup(const QColor& axesColor, + const QColor& lowColor, + const QColor& midColor, + const QColor& highColor) { getRgbF(axesColor, &m_axesColor_r, &m_axesColor_g, &m_axesColor_b, &m_axesColor_a); getRgbF(lowColor, &m_rgbLowColor_r, &m_rgbLowColor_g, &m_rgbLowColor_b); getRgbF(midColor, &m_rgbMidColor_r, &m_rgbMidColor_g, &m_rgbMidColor_b); getRgbF(highColor, &m_rgbHighColor_r, &m_rgbHighColor_g, &m_rgbHighColor_b); -#endif - initForRectangles(0); - setUsePreprocess(true); } void WaveformRendererRGB::onSetup(const QDomNode&) { diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.h b/src/waveform/renderers/allshader/waveformrendererrgb.h index ed44f3fbfad..6e634d4de0c 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.h +++ b/src/waveform/renderers/allshader/waveformrendererrgb.h @@ -14,12 +14,6 @@ class allshader::WaveformRendererRGB final public rendergraph::GeometryNode { public: explicit WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, -#ifdef __RENDERGRAPH_IS_SCENEGRAPH - QColor axesColor, - QColor lowColor, - QColor midColor, - QColor highColor, -#endif ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play, WaveformRendererSignalBase::Options options = WaveformRendererSignalBase::Option::None); @@ -27,6 +21,11 @@ class allshader::WaveformRendererRGB final // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; + void setup(const QColor& axesColor, + const QColor& lowColor, + const QColor& midColor, + const QColor& highColor); + bool supportsSlip() const override { return true; } diff --git a/src/waveform/renderers/allshader/waveformrendermark.cpp b/src/waveform/renderers/allshader/waveformrendermark.cpp index b7ff61244d4..879ab924c5a 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.cpp +++ b/src/waveform/renderers/allshader/waveformrendermark.cpp @@ -135,27 +135,11 @@ QString timeSecToString(double timeSec) { allshader::WaveformRenderMark::WaveformRenderMark( WaveformWidgetRenderer* waveformWidget, -#ifdef __RENDERGRAPH_IS_SCENEGRAPH - QColor fgPlayColor, - QColor bgPlayColor, - bool untilMarkShowBeats, - bool untilMarkShowTime, - Qt::Alignment untilMarkAlign, - int untilMarkTextSize, -#endif ::WaveformRendererAbstract::PositionSource type) : ::WaveformRenderMarkBase(waveformWidget, false), m_beatsUntilMark(0), m_timeUntilMark(0.0), m_pTimeRemainingControl(nullptr), -#ifdef __RENDERGRAPH_IS_SCENEGRAPH - m_fgPlayColor(fgPlayColor), - m_bgPlayColor(bgPlayColor), - m_untilMarkShowBeats(untilMarkShowBeats), - m_untilMarkShowTime(untilMarkShowTime), - m_untilMarkAlign(untilMarkAlign), - m_untilMarkTextSize(untilMarkTextSize), -#endif m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), m_playPosHeight(0.f), m_playPosDevicePixelRatio(0.f) { @@ -185,7 +169,23 @@ allshader::WaveformRenderMark::WaveformRenderMark( } } -void allshader::WaveformRenderMark::draw(QPainter*, QPaintEvent*) { +void allshader::WaveformRenderMark::setup(const QColor& fgPlayColor, + const QColor& bgPlayColor, + bool untilMarkShowBeats, + bool untilMarkShowTime, + Qt::Alignment untilMarkAlign, + int untilMarkTextSize) { + m_fgPlayColor = fgPlayColor; + m_bgPlayColor = bgPlayColor; + m_untilMarkShowBeats = untilMarkShowBeats; + m_untilMarkShowTime = untilMarkShowTime; + m_untilMarkAlign = untilMarkAlign; + m_untilMarkTextSize = untilMarkTextSize; +} + +void allshader::WaveformRenderMark::draw(QPainter* painter, QPaintEvent* event) { + Q_UNUSED(painter); + Q_UNUSED(event); DEBUG_ASSERT(false); } diff --git a/src/waveform/renderers/allshader/waveformrendermark.h b/src/waveform/renderers/allshader/waveformrendermark.h index 5b8fef1cc4e..bc5ed0ccf3b 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.h +++ b/src/waveform/renderers/allshader/waveformrendermark.h @@ -22,20 +22,19 @@ class allshader::WaveformRenderMark : public ::WaveformRenderMarkBase, public rendergraph::Node { public: explicit WaveformRenderMark(WaveformWidgetRenderer* waveformWidget, -#ifdef __RENDERGRAPH_IS_SCENEGRAPH - QColor fgPlayColor, - QColor bgPlayColor, - bool untilMarkShowBeats, - bool untilMarkShowTime, - Qt::Alignment untilMarkAlign, - int untilMarkTextSize, -#endif ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play); // Pure virtual from WaveformRendererAbstract, not used void draw(QPainter* painter, QPaintEvent* event) override final; + void setup(const QColor& fgPlayColor, + const QColor& bgPlayColor, + bool untilMarkShowBeats, + bool untilMarkShowTime, + Qt::Alignment untilMarkAlign, + int untilMarkTextSize); + bool init() override; void update(); From 06b117ee93b24feb2bbb85ab3d68742a8e2bd4f6 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 1 Dec 2024 22:46:19 +0000 Subject: [PATCH 13/22] fixup! feat: add scrolling waveform in QML using scenegraph --- res/qml/Deck.qml | 48 +++++++++++++++++++ res/qml/IntroOutroButton.qml | 45 +++++++++++++++++ res/qml/WaveformDisplay.qml | 3 +- src/main.cpp | 5 +- src/mixxxmainwindow.cpp | 2 +- src/qml/qmlwaveformdisplay.cpp | 9 +++- .../allshader/waveformrendermark.cpp | 2 + .../renderers/allshader/waveformrendermark.h | 2 + src/waveform/waveformwidgetfactory.cpp | 9 ++-- src/waveform/waveformwidgetfactory.h | 2 +- 10 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 res/qml/IntroOutroButton.qml diff --git a/res/qml/Deck.qml b/res/qml/Deck.qml index 46ff28bd465..68b575973b7 100644 --- a/res/qml/Deck.qml +++ b/res/qml/Deck.qml @@ -326,6 +326,54 @@ Item { activeColor: Theme.deckActiveColor } + Row { + anchors.left: playButton.right + anchors.leftMargin: 10 + anchors.bottom: playButton.bottom + anchors.topMargin: 5 + spacing: -1 + + Skin.IntroOutroButton { + keyPrefix: "intro_start" + group: root.group + + text: "Intro\nStart" + + width: playButton.height * 2 - 1 + height: playButton.height + } + + Skin.IntroOutroButton { + keyPrefix: "intro_end" + group: root.group + + text: "Intro\nEnd" + + width: playButton.height * 2 - 1 + height: playButton.height + } + + Skin.IntroOutroButton { + keyPrefix: "outro_start" + group: root.group + + text: "Outro\nStart" + + width: playButton.height * 2 - 1 + height: playButton.height + } + + Skin.IntroOutroButton { + keyPrefix: "outro_end" + group: root.group + + text: "Outro\nEnd" + + width: playButton.height * 2 - 1 + height: playButton.height + } + } + Row { anchors.left: cueButton.right anchors.top: parent.top diff --git a/res/qml/IntroOutroButton.qml b/res/qml/IntroOutroButton.qml new file mode 100644 index 00000000000..9d585d3d25b --- /dev/null +++ b/res/qml/IntroOutroButton.qml @@ -0,0 +1,45 @@ +import "." as Skin +import Mixxx 1.0 as Mixxx +import QtQuick 2.12 + +import "Theme" + +Skin.Button { + id: root + + required property string keyPrefix + required property string group + + activeColor: Theme.deckActiveColor + highlight: enabledControl.value + + Mixxx.ControlProxy { + id: control + + group: root.group + key: `${root.keyPrefix}_activate` + value: root.down + } + + Mixxx.ControlProxy { + id: enabledControl + + group: root.group + key: `${root.keyPrefix}_enabled` + } + + Mixxx.ControlProxy { + id: cleanControl + + group: root.group + key: `${root.keyPrefix}_clear` + value: mousearea.pressed && enabledControl.value + } + + MouseArea { + id: mousearea + + anchors.fill: parent + acceptedButtons: Qt.RightButton + } +} diff --git a/res/qml/WaveformDisplay.qml b/res/qml/WaveformDisplay.qml index 22209a8719e..bfb974d9429 100644 --- a/res/qml/WaveformDisplay.qml +++ b/res/qml/WaveformDisplay.qml @@ -110,6 +110,7 @@ Item { } Mixxx.WaveformMark { control: "intro_start_position" + text: '◢' align: 'top|right' color: 'blue' textColor: '#FFFFFF' @@ -130,6 +131,7 @@ Item { } Mixxx.WaveformMark { control: "outro_end_position" + text: '◣' align: 'top|left' color: 'blue' textColor: '#FFFFFF' @@ -171,7 +173,6 @@ Item { group: root.group key: "waveform_zoom" } - readonly property real effectiveZoomFactor: (1 / rateRatioControl.value) * (100 / zoomControl.value) MouseArea { property int mouseStatus: WaveformDisplay.MouseStatus.Normal diff --git a/src/main.cpp b/src/main.cpp index 9041dbf0f96..74c5f6c07bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,7 +57,7 @@ constexpr int kPixmapCacheLimitAt100PercentZoom = 32 * 1024; // 32 MByte int runMixxx(MixxxApplication* pApp, const CmdlineArgs& args) { CmdlineArgs::Instance().parseForUserFeedback(); - const auto pCoreServices = std::make_shared(args, pApp); + auto pCoreServices = std::make_shared(args, pApp); int exitCode; #ifdef MIXXX_USE_QML @@ -66,7 +66,7 @@ int runMixxx(MixxxApplication* pApp, const CmdlineArgs& args) { auto pVisuals = std::make_unique(); WaveformWidgetFactory::createInstance(); // takes a long time WaveformWidgetFactory::instance()->setConfig(pCoreServices->getSettings()); - WaveformWidgetFactory::instance()->startVSync(pTick.get(), pVisuals.get()); + WaveformWidgetFactory::instance()->startVSync(pTick.get(), pVisuals.get(), true); { mixxx::qml::QmlApplication qmlApplication(pApp, pCoreServices); const QStringList visualGroups = @@ -85,6 +85,7 @@ int runMixxx(MixxxApplication* pApp, const CmdlineArgs& args) { }); exitCode = pApp->exec(); } + pCoreServices.reset(); WaveformWidgetFactory::destroy(); } else #endif diff --git a/src/mixxxmainwindow.cpp b/src/mixxxmainwindow.cpp index 3d0edcef530..6a81fdd0e7c 100644 --- a/src/mixxxmainwindow.cpp +++ b/src/mixxxmainwindow.cpp @@ -273,7 +273,7 @@ void MixxxMainWindow::initialize() { WaveformWidgetFactory::createInstance(); // takes a long time WaveformWidgetFactory::instance()->setConfig(m_pCoreServices->getSettings()); - WaveformWidgetFactory::instance()->startVSync(m_pGuiTick, m_pVisualsManager); + WaveformWidgetFactory::instance()->startVSync(m_pGuiTick, m_pVisualsManager, false); connect(this, &MixxxMainWindow::skinLoaded, diff --git a/src/qml/qmlwaveformdisplay.cpp b/src/qml/qmlwaveformdisplay.cpp index 266f3b516f1..2649e2bd3b5 100644 --- a/src/qml/qmlwaveformdisplay.cpp +++ b/src/qml/qmlwaveformdisplay.cpp @@ -38,6 +38,7 @@ QmlWaveformDisplay::QmlWaveformDisplay(QQuickItem* parent) this, &QmlWaveformDisplay::slotWindowChanged, Qt::DirectConnection); + slotWindowChanged(window()); } QmlWaveformDisplay::~QmlWaveformDisplay() { @@ -150,8 +151,12 @@ QSGNode* QmlWaveformDisplay::updatePaintNode(QSGNode* node, UpdatePaintNodeData* boundingRect().height()); } - m_waveformRenderMark->update(); - m_waveformRenderMarkRange->update(); + if (m_waveformRenderMark != nullptr) { + m_waveformRenderMark->update(); + } + if (m_waveformRenderMarkRange != nullptr) { + m_waveformRenderMarkRange->update(); + } onPreRender(this); bgNode->markDirty(QSGNode::DirtyForceUpdate); diff --git a/src/waveform/renderers/allshader/waveformrendermark.cpp b/src/waveform/renderers/allshader/waveformrendermark.cpp index 879ab924c5a..7e8ca48967f 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.cpp +++ b/src/waveform/renderers/allshader/waveformrendermark.cpp @@ -169,6 +169,7 @@ allshader::WaveformRenderMark::WaveformRenderMark( } } +#ifdef __RENDERGRAPH_IS_SCENEGRAPH void allshader::WaveformRenderMark::setup(const QColor& fgPlayColor, const QColor& bgPlayColor, bool untilMarkShowBeats, @@ -182,6 +183,7 @@ void allshader::WaveformRenderMark::setup(const QColor& fgPlayColor, m_untilMarkAlign = untilMarkAlign; m_untilMarkTextSize = untilMarkTextSize; } +#endif void allshader::WaveformRenderMark::draw(QPainter* painter, QPaintEvent* event) { Q_UNUSED(painter); diff --git a/src/waveform/renderers/allshader/waveformrendermark.h b/src/waveform/renderers/allshader/waveformrendermark.h index bc5ed0ccf3b..767bff148cd 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.h +++ b/src/waveform/renderers/allshader/waveformrendermark.h @@ -28,12 +28,14 @@ class allshader::WaveformRenderMark : public ::WaveformRenderMarkBase, // Pure virtual from WaveformRendererAbstract, not used void draw(QPainter* painter, QPaintEvent* event) override final; +#ifdef __RENDERGRAPH_IS_SCENEGRAPH void setup(const QColor& fgPlayColor, const QColor& bgPlayColor, bool untilMarkShowBeats, bool untilMarkShowTime, Qt::Alignment untilMarkAlign, int untilMarkTextSize); +#endif bool init() override; diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index b7ea1faba51..4afb32d42ba 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -1172,9 +1172,12 @@ int WaveformWidgetFactory::findIndexOf(WWaveformViewer* viewer) const { return -1; } -void WaveformWidgetFactory::startVSync(GuiTick* pGuiTick, VisualsManager* pVisualsManager) { - const auto vSyncMode = static_cast( - m_config->getValue(ConfigKey("[Waveform]", "VSync"), 0)); +void WaveformWidgetFactory::startVSync( + GuiTick* pGuiTick, VisualsManager* pVisualsManager, bool useQML) { + const auto vSyncMode = useQML + ? VSyncThread::ST_TIMER + : static_cast( + m_config->getValue(ConfigKey("[Waveform]", "VSync"), 0)); m_pGuiTick = pGuiTick; m_pVisualsManager = pVisualsManager; diff --git a/src/waveform/waveformwidgetfactory.h b/src/waveform/waveformwidgetfactory.h index c80585fd2e1..9250f537529 100644 --- a/src/waveform/waveformwidgetfactory.h +++ b/src/waveform/waveformwidgetfactory.h @@ -211,7 +211,7 @@ class WaveformWidgetFactory : public QObject, public Singleton Date: Sun, 19 Jan 2025 02:20:19 +0000 Subject: [PATCH 14/22] fixup! feat: add scrolling waveform in QML using scenegraph --- CMakeLists.txt | 7 +++---- src/rendergraph/CMakeLists.txt | 5 +---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b62ef16c45a..dbad8b0eb55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1777,6 +1777,7 @@ if(QOPENGL) src/shaders/vinylqualityshader.cpp src/util/opengltexture2d.cpp src/waveform/renderers/allshader/digitsrenderer.cpp + src/waveform/renderers/allshader/matrixforwidgetgeometry.cpp src/waveform/renderers/allshader/waveformrenderbackground.cpp src/waveform/renderers/allshader/waveformrenderbeat.cpp src/waveform/renderers/allshader/waveformrenderer.cpp @@ -4595,17 +4596,15 @@ if(VINYLCONTROL) endif() # rendergraph -add_subdirectory(src/rendergraph/opengl) -add_subdirectory(res/shaders/rendergraph) +add_subdirectory(src/rendergraph) target_compile_definitions( rendergraph_gl PUBLIC $<$:MIXXX_DEBUG_ASSERTIONS_ENABLED> ) target_link_libraries(mixxx-lib PUBLIC rendergraph_gl) -target_compile_definitions(mixxx-lib PRIVATE rendergraph=rendergraph_gl) +target_compile_definitions(mixxx-lib PUBLIC rendergraph=rendergraph_gl) target_compile_definitions(mixxx-lib PRIVATE allshader=allshader_gl) if(QML) - add_subdirectory(src/rendergraph/scenegraph) target_compile_definitions( rendergraph_sg PUBLIC $<$:MIXXX_DEBUG_ASSERTIONS_ENABLED> diff --git a/src/rendergraph/CMakeLists.txt b/src/rendergraph/CMakeLists.txt index 23569ef30ca..8f14cb2f313 100644 --- a/src/rendergraph/CMakeLists.txt +++ b/src/rendergraph/CMakeLists.txt @@ -39,8 +39,5 @@ set( ) add_subdirectory(opengl) -################################# -# TODO: uncomment in follow-up PR -# add_subdirectory(scenegraph) -################################# +add_subdirectory(scenegraph) add_subdirectory(shaders) From 141a5e2dd225f19d4b2b4a96c27239bc1257bf0e Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sun, 19 Jan 2025 02:29:16 +0000 Subject: [PATCH 15/22] fixup! feat: add scrolling waveform in QML using scenegraph --- CMakeLists.txt | 1 + src/waveform/renderers/allshader/waveformrendermark.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index dbad8b0eb55..3c8d5c7179b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3275,6 +3275,7 @@ if(QML) src/qml/qmlvisibleeffectsmodel.cpp src/qml/qmlchainpresetmodel.cpp src/qml/qmlwaveformoverview.cpp + src/qml/qmlmixxxcontrollerscreen.cpp src/qml/qmlwaveformdisplay.cpp src/qml/qmlwaveformrenderer.cpp src/waveform/renderers/allshader/digitsrenderer.cpp diff --git a/src/waveform/renderers/allshader/waveformrendermark.cpp b/src/waveform/renderers/allshader/waveformrendermark.cpp index 7e8ca48967f..c92d4df2a13 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.cpp +++ b/src/waveform/renderers/allshader/waveformrendermark.cpp @@ -404,6 +404,7 @@ void allshader::WaveformRenderMark::updateDigitsNodeForUntilMark(float x) { const auto untilMarkTextPointSize = WaveformWidgetFactory::instance()->getUntilMarkTextPointSize(); +#endif const auto untilMarkTextHeightLimit = WaveformWidgetFactory::instance() ->getUntilMarkTextHeightLimit(); // proportion of waveform From efb1a03d98bf5c1207bd333d6744b6c6f9134645 Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sun, 19 Jan 2025 02:36:01 +0000 Subject: [PATCH 16/22] fixup! feat: add scrolling waveform in QML using scenegraph --- CMakeLists.txt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c8d5c7179b..92c6fc01c9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3287,11 +3287,10 @@ if(QML) src/waveform/renderers/allshader/waveformrenderersignalbase.cpp src/waveform/renderers/allshader/waveformrendermark.cpp src/waveform/renderers/allshader/waveformrendermarkrange.cpp - # FIXME depends on rendergraph/openglnode.h - # src/waveform/renderers/allshader/waveformrendererslipmode.cpp - # src/waveform/renderers/allshader/waveformrendererfiltered.cpp - # src/waveform/renderers/allshader/waveformrendererhsv.cpp - # src/waveform/renderers/allshader/waveformrenderersimple.cpp + src/waveform/renderers/allshader/waveformrendererslipmode.cpp + src/waveform/renderers/allshader/waveformrendererfiltered.cpp + src/waveform/renderers/allshader/waveformrendererhsv.cpp + src/waveform/renderers/allshader/waveformrenderersimple.cpp # The following sources need to be in this target to get QML_ELEMENT properly interpreted src/control/controlmodel.cpp src/control/controlsortfiltermodel.cpp From bb5c4f84fe578dc20eae93724379a14412918f29 Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sun, 19 Jan 2025 02:37:25 +0000 Subject: [PATCH 17/22] fixup! feat: add scrolling waveform in QML using scenegraph --- src/rendergraph/opengl/texture.cpp | 3 --- src/waveform/renderers/allshader/digitsrenderer.cpp | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/rendergraph/opengl/texture.cpp b/src/rendergraph/opengl/texture.cpp index 45550222944..88921bde9df 100644 --- a/src/rendergraph/opengl/texture.cpp +++ b/src/rendergraph/opengl/texture.cpp @@ -1,7 +1,4 @@ #include "rendergraph/texture.h" -#include -#include -#include "rendergraph/assert.h" #include #include diff --git a/src/waveform/renderers/allshader/digitsrenderer.cpp b/src/waveform/renderers/allshader/digitsrenderer.cpp index 9e4b56e556d..9cab55a95f4 100644 --- a/src/waveform/renderers/allshader/digitsrenderer.cpp +++ b/src/waveform/renderers/allshader/digitsrenderer.cpp @@ -9,12 +9,12 @@ #include #include -#include "util/assert.h" -#include "util/roundtopixel.h" #include "rendergraph/context.h" #include "rendergraph/geometry.h" #include "rendergraph/material/texturematerial.h" #include "rendergraph/vertexupdaters/texturedvertexupdater.h" +#include "util/assert.h" +#include "util/roundtopixel.h" // Render digits using a texture (generated) with digits with blurred dark outline From e4a726eff2110e5d6d2cf573bfa796c1b6977eed Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 15 Sep 2024 01:57:38 +0100 Subject: [PATCH 18/22] feat: add screen rendering for S4Mk3 --- .../Traktor Kontrol S4 MK3.bulk.xml | 945 ++++++++++++++++++ .../Traktor Kontrol S4 MK3.hid.xml | 12 +- res/controllers/Traktor-Kontrol-S4-MK3.js | 870 ++++++++++++---- .../TraktorKontrolS4MK3Screens.qml | 217 ++++ .../S4MK3/BPMIndicator.qml | 77 ++ .../S4MK3/HotcuePoint.qml | 199 ++++ .../S4MK3/KeyIndicator.qml | 127 +++ .../S4MK3/Keyboard.qml | 140 +++ .../S4MK3/LoopSizeIndicator.qml | 75 ++ .../S4MK3/OnAirTrack.qml | 81 ++ .../S4MK3/Progression.qml | 53 + .../S4MK3/SplashOff.qml | 15 + .../S4MK3/StockScreen.qml | 498 +++++++++ .../S4MK3/TimeAndBeatloopIndicator.qml | 107 ++ .../S4MK3/WaveformOverview.qml | 165 +++ .../TraktorKontrolS4MK3Screens/S4MK3/qmldir | 12 + res/qml/WaveformRow.qml | 5 +- src/controllers/bulk/bulksupported.h | 1 + tools/README | 8 + tools/clang_format.py | 2 +- tools/traktor_s4_mk3_screen_test.c | 110 ++ 21 files changed, 3497 insertions(+), 222 deletions(-) create mode 100644 res/controllers/Traktor Kontrol S4 MK3.bulk.xml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir create mode 100644 tools/traktor_s4_mk3_screen_test.c diff --git a/res/controllers/Traktor Kontrol S4 MK3.bulk.xml b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml new file mode 100644 index 00000000000..c7bc44712ea --- /dev/null +++ b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml @@ -0,0 +1,945 @@ + + + + Traktor Kontrol S4 MK3 (Screens) + A. Colombier + Mapping for Traktor Kontrol S4 MK3 screens + native_instruments_traktor_kontrol_s4_mk3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index a254772e6c5..4efc26d7041 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -10,6 +10,16 @@ + + + - + diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index a36353cb66a..8f4fcc0a021 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -21,6 +21,30 @@ const LedColors = { white: 68, }; +const LedColorMap = { + 0xCC0000: LedColors.red, + 0xCC5E00: LedColors.carrot, + 0xCC7800: LedColors.orange, + 0xCC9200: LedColors.honey, + + 0xCCCC00: LedColors.yellow, + 0x81CC00: LedColors.lime, + 0x00CC00: LedColors.green, + 0x00CC49: LedColors.aqua, + + 0x00CCCC: LedColors.celeste, + 0x0091CC: LedColors.sky, + 0x0000CC: LedColors.blue, + 0xCC00CC: LedColors.purple, + + 0xAD65FF: LedColors.fuscia, + 0xCC0079: LedColors.magenta, + 0xCC477E: LedColors.azalea, + 0xCC4761: LedColors.salmon, + + 0xCCCCCC: LedColors.white, +}; + // This define the sequence of color to use for pad button when in keyboard mode. This should make them look like an actual keyboard keyboard octave, except for C, which is green to help spotting it. const KeyboardColors = [ @@ -145,6 +169,8 @@ const SoftwareMixerHeadphone = !!engine.getSetting("softwareMixerHeadphone"); // Define custom default layout used by the pads, instead of intro/outro and first 4 hotcues. const DefaultPadLayout = engine.getSetting("defaultPadLayout"); +// Whether or not to use the ShareDataAPI, available in the PR 12199. +const UseSharedDataAPI = engine.getSetting("useSharedDataAPI"); // The LEDs only support 16 base colors. Adding 1 in addition to // the normal 2 for Button.prototype.brightnessOn changes the color @@ -189,6 +215,60 @@ const SamplerCrossfaderAssign = true; const MotorWindUpMilliseconds = 1200; const MotorWindDownMilliseconds = 900; +/* + * Kontrol S4 Mk3 hardware-specific constants + */ +const wheelRelativeMax = 2 ** 32 - 1; +const wheelAbsoluteMax = 2879; + +const wheelTimerMax = 2 ** 32 - 1; +const wheelTimerTicksPerSecond = 100000000; // One tick every 10ns + +const baseRevolutionsPerSecond = BaseRevolutionsPerMinute / 60; +const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; + +// The active tab ID. This is used when SharedDataAPI is active, to communicate with the screens which tab is currently selected. +const ActiveTabPadID = { + record: 8, + samples: 4, + mute: 7, + stems: 5, + cue: 11, +}; + +const wheelLEDmodes = { + off: 0, + dimFlash: 1, + spot: 2, + ringFlash: 3, + dimSpot: 4, + individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values +}; + +// The mode available, which the wheel can be used for. +const wheelModes = { + jog: 0, + vinyl: 1, + motor: 2, + loopIn: 3, + loopOut: 4, +}; + +const moveModes = { + beat: 0, + bpm: 1, + grid: 2, + keyboard: 3, + hotcueColor: 4, +}; + +// tracks state across input reports +let wheelTimer = null; +// This is a global variable so the S4Mk3Deck Components have access +// to it and it is guaranteed to be calculated before processing +// input for the Components. +let wheelTimerDelta = 0; + /* * HID report parsing library */ @@ -328,13 +408,13 @@ class Component { this.send(value); } outConnect() { - if (this.outKey !== undefined && this.group !== undefined) { + if (this.outKey !== undefined && this.group !== undefined && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); // This is useful for case where effect would have been fully disabled in Mixxx. This appears to be the case during unit tests. if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { - console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); + console.warn(`Unable to connect '${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } } } @@ -346,6 +426,7 @@ class Component { } outTrigger() { for (const connection of this.outConnections) { + if (!connection) { continue; } connection.trigger(); } } @@ -373,6 +454,9 @@ class ComponentContainer extends Component { } reconnectComponents(callback) { for (const component of this) { + if (typeof component.unshift === "function" && component.unshift.length === 0) { + component.unshift(); + } if (typeof component.outDisconnect === "function" && component.outDisconnect.length === 0) { component.outDisconnect(); } @@ -429,6 +513,7 @@ class Deck extends ComponentContainer { this.color = colors[0]; } this.secondDeckModes = null; + this.selectedHotcue = null; } toggleDeck() { if (this.decks === undefined) { @@ -441,14 +526,28 @@ class Deck extends ComponentContainer { newDeckIndex = 0; } - this.switchDeck(Deck.groupForNumber(this.decks[newDeckIndex])); + this.switchDeck(this.decks[newDeckIndex]); } - switchDeck(newGroup) { + switchDeck(newDeck) { + const newGroup = Deck.groupForNumber(newDeck); + + switch (this.moveMode) { + case moveModes.beat: + case moveModes.bpm: + case moveModes.grid: + case moveModes.hotcueColor: + this.moveMode = null; + this.selectedHotcue = null; + break; + } + const currentModes = { wheelMode: this.wheelMode, moveMode: this.moveMode, }; + this.selectedStem.fill(false); + engine.setValue(this.group, "scratch2_enable", false); this.group = newGroup; this.color = this.groupsToColors[newGroup]; @@ -470,12 +569,22 @@ class Deck extends ComponentContainer { } else if (component.group.search(script.eqRegEx) !== -1) { component.group = `[EqualizerRack1_${newGroup}_Effect1]`; } else if (component.group.search(script.quickEffectRegEx) !== -1) { - component.group = `[QuickEffectRack1_${newGroup}]`; + component.group = quickFxChannel(newGroup); } component.color = this.groupsToColors[newGroup]; }); this.secondDeckModes = currentModes; + this.currentDeckNumber = newDeck; + + if (!UseSharedDataAPI) { + return; + } + + const data = engine.getSharedData() || {}; + if (!data.group) { return; } + data.group[this.decks[0] === 1 ? "leftdeck":"rightdeck"] = this.group; + engine.setSharedData(data); } static groupForNumber(deckNumber) { return `[Channel${deckNumber}]`; @@ -488,13 +597,19 @@ class Button extends Component { super(options); - if (this.input === undefined) { + if (this.input === undefined + || (typeof this.onLongPress === "function" && this.onLongPress.length === 0) + || (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0) + || (typeof this.onShortPress === "function" && this.onShortPress.length === 0) + || (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) + || (typeof this.onPress === "function" && this.onPress.length === 0) + || (typeof this.onRelease === "function" && this.onRelease.length === 0)) { this.input = this.defaultInput; - if (typeof this.input === "function" - && this.inReport instanceof HIDInputReport - && this.input.length === 0) { - this.inConnect(); - } + } + if (typeof this.input === "function" + && this.inReport instanceof HIDInputReport + && this.input.length === 0) { + this.inConnect(); } if (this.longPressTimeOutMillis === undefined) { @@ -554,24 +669,32 @@ class Button extends Component { } } defaultInput(pressed) { + this.pressed = pressed; if (pressed) { + this.isShortPress = true; this.isLongPress = false; + if (typeof this.onPress === "function" && this.onPress.length === 0) { this.onPress(); } if (typeof this.onShortPress === "function" && this.onShortPress.length === 0) { this.onShortPress(); } if ((typeof this.onLongPress === "function" && this.onLongPress.length === 0) || (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0)) { this.longPressTimer = engine.beginTimer(this.longPressTimeOutMillis, () => { this.isLongPress = true; + this.isShortPress = false; this.longPressTimer = 0; if (typeof this.onLongPress !== "function") { return; } this.onLongPress(this); }, true); } } else if (this.isLongPress) { + this.isLongPress = false; + if (typeof this.onRelease === "function" && this.onRelease.length === 0) { this.onRelease(); } if (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0) { this.onLongRelease(); } } else { + this.isShortPress = false; if (this.longPressTimer !== 0) { engine.stopTimer(this.longPressTimer); this.longPressTimer = 0; } + if (typeof this.onRelease === "function" && this.onRelease.length === 0) { this.onRelease(); } if (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) { this.onShortRelease(); } } } @@ -610,8 +733,6 @@ class TriggerButton extends Button { class PowerWindowButton extends Button { constructor(options) { super(options); - this.isLongPressed = false; - this.longPressTimer = 0; } onShortPress() { script.toggleControl(this.group, this.inKey); @@ -717,8 +838,21 @@ class HotcueButton extends PushButton { this.inKey = `hotcue_${this.number}_clear`; } input(pressed) { - engine.setValue(this.group, "scratch2_enable", false); - engine.setValue(this.group, this.inKey, pressed); + if (this.deck.moveMode === moveModes.hotcueColor) { + this.deck.selectedHotcue = pressed ? this.number : null; + if (UseSharedDataAPI) { + const data = engine.getSharedData() || {}; + if (!data.selectedHotcue) { return; } + data.selectedHotcue[this.group] = this.deck.selectedHotcue; + engine.setSharedData(data); + } + + } else if (this.deck.libraryPlayButton.pressed) { + engine.setValue(this.deck.libraryPlayButton.group, this.inKey, pressed); + } else { + engine.setValue(this.group, "scratch2_enable", false); + engine.setValue(this.group, this.inKey, pressed); + } } output(value) { if (value) { @@ -728,10 +862,10 @@ class HotcueButton extends PushButton { } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } @@ -740,7 +874,7 @@ class HotcueButton extends PushButton { this.output(engine.getValue(this.group, this.outKey)); }); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.${this.colorKey}' to the controller output. The control appears to be unavailable.`); } @@ -796,17 +930,17 @@ class KeyboardButton extends PushButton { if (this.number + offset < 1 || this.number + offset > 24) { this.send(0); } else { - this.send(color + (value ? this.brightnessOn : this.brightnessOff)); + this.send(value ? LedColors.yellow : color); } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, "key", (key) => { const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0); this.output(key === this.number + offset); }); if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.key' to the controller output. The control appears to be unavailable.`); } @@ -814,6 +948,90 @@ class KeyboardButton extends PushButton { } } +/* + * Represent a pad button that acts as a stem controller. It will be used to mute or unmute a stem or select it for other operation such as volume or quick effect control + */ +class StemButton extends PushButton { + constructor(options) { + super(options); + if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 4) { + throw Error("StemButton must have a number property of an integer between 1 and 4"); + } + if (this.deck === undefined) { + throw Error("StemButton must have a deck attached to it"); + } + if (this.deck.mixer === undefined) { + throw Error("StemButton must have a deck with a mixer attached to it"); + } + this.color = 0; + this.muted = 0; + this.outConnect(); + } + unshift() { + this.outTrigger(); + } + shift() { + this.outTrigger(); + } + input(pressed) { + if (!this.enabled) { + return; + } + if (this.shifted && pressed) { + script.toggleControl(stemChannel(this.group, this.number), "mute"); + } + if (!this.shifted) { + this.deck.selectedStem[this.number] = pressed; + } + if (!this.shifted && pressed && this.deck.mixer.firstPressedFxSelector !== null) { + const presetNumber = this.deck.mixer.calculatePresetNumber(); + this.color = QuickEffectPresetColors[presetNumber - 1]; + engine.setValue(quickFxChannel(stemChannel(this.group, this.number)), "loaded_chain_preset", presetNumber + 1); + this.deck.mixer.firstPressedFxSelector = null; + this.deck.mixer.secondPressedFxSelector = null; + this.deck.mixer.resetFxSelectorColors(); + } + } + output() { + if (!this.color || !this.enabled) { + this.send(0); + } else { + this.send(this.color + (this.muted ? this.brightnessOff : this.brightnessOn)); + } + } + outConnect() { + if (undefined !== this.group) { + const muteConnection = engine.makeConnection(stemChannel(this.group, this.number), "mute", (mute) => { + this.muted = mute; + this.output(); + }); + if (muteConnection) { + this.outConnections[0] = muteConnection; + } else { + console.warn(`Unable to connect '${stemChannel(this.group, this.number)}.mute' to the controller output. The control appears to be unavailable.`); + } + const colorConnection = engine.makeConnection(stemChannel(this.group, this.number), "color", (color) => { + this.color = this.colorMap.getValueForNearestColor(color); + this.output(); + }); + if (colorConnection) { + this.outConnections[1] = colorConnection; + } else { + console.warn(`Unable to connect '${stemChannel(this.group, this.number)}.color' to the controller output. The control appears to be unavailable.`); + } + const enabledConnection = engine.makeConnection(this.group, "stem_count", (count) => { + this.enabled = count >= this.number; + this.output(); + }); + if (enabledConnection) { + this.outConnections[2] = enabledConnection; + } else { + console.warn(`Unable to connect '${this.group}.stem_count' to the controller output. The control appears to be unavailable.`); + } + } + } +} + /* * Represent a pad button that will trigger a pre-defined beatloop size as set in BeatLoopRolls. */ @@ -833,16 +1051,24 @@ class BeatLoopRollButton extends TriggerButton { } options.key = `beatlooproll_${size}_activate`; options.onShortPress = function() { - if (!this.deck.beatloopSize) { - this.deck.beatloopSize = engine.getValue(this.group, "beatloop_size"); + if (!this.deck.beatloop) { + this.deck.beatloop = { + size: engine.getValue(this.group, "beatloop_size"), + start: engine.getValue(this.group, "loop_start_position"), + end: engine.getValue(this.group, "loop_end_position"), + enabled: engine.getValue(this.group, "loop_enabled"), + }; } engine.setValue(this.group, this.inKey, true); }; options.onShortRelease = function() { engine.setValue(this.group, this.inKey, false); - if (this.deck.beatloopSize) { - engine.setValue(this.group, "beatloop_size", this.deck.beatloopSize); - this.deck.beatloopSize = undefined; + if (this.deck.beatloop) { + engine.setValue(this.group, "loop_start_position", this.deck.beatloop.start); + engine.setValue(this.group, "loop_end_position", this.deck.beatloop.end); + engine.setValue(this.group, "beatloop_size", this.deck.beatloop.size); + engine.setValue(this.group, "loop_enabled", this.deck.beatloop.enabled); + this.deck.beatloop = undefined; } }; } @@ -877,7 +1103,12 @@ class SamplerButton extends Button { onShortPress() { if (!this.shifted) { if (engine.getValue(this.group, "track_loaded") === 0) { - engine.setValue(this.group, "LoadSelectedTrack", 1); + if (this.deck.samplerStemSelection !== null) { + engine.setValue(this.group, "load_selected_track_stems", this.deck.samplerStemSelection); + this.deck.samplerStemSelection = null; + } else { + engine.setValue(this.group, "LoadSelectedTrack", 1); + } } else { engine.setValue(this.group, "cue_gotoandplay", 1); } @@ -909,16 +1140,16 @@ class SamplerButton extends Button { } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, "play", this.output.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.play' to the controller output. The control appears to be unavailable.`); } const connection1 = engine.makeConnection(this.group, "track_loaded", this.output.bind(this)); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.track_loaded' to the controller output. The control appears to be unavailable.`); } @@ -1099,14 +1330,12 @@ class Mixer extends ComponentContainer { this.resetFxSelectorColors(); this.quantizeButton = new Button({ - input: function(pressed) { - if (pressed) { - this.globalQuantizeOn = !this.globalQuantizeOn; - for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { - engine.setValue(`[Channel${deckIdx}]`, "quantize", this.globalQuantizeOn); - } - this.send(this.globalQuantizeOn ? 127 : 0); + onPress: function() { + this.globalQuantizeOn = !this.globalQuantizeOn; + for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { + engine.setValue(`[Channel${deckIdx}]`, "quantize", this.globalQuantizeOn); } + this.send(this.globalQuantizeOn ? 127 : 0); }, globalQuantizeOn: false, inByte: 11, @@ -1287,7 +1516,7 @@ class QuickEffectButton extends Button { if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1) { throw Error("number attribute must be an integer >= 1"); } - this.group = `[QuickEffectRack1_[Channel${this.number}]]`; + this.group = quickFxChannel(`[Channel${this.number}]`); this.outConnect(); } onShortPress() { @@ -1321,16 +1550,16 @@ class QuickEffectButton extends Button { this.outConnections[1].trigger(); } outConnect() { - if (this.group !== undefined) { + if (this.group !== undefined && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, "loaded_chain_preset", this.presetLoaded.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.loaded_chain_preset' to the controller output. The control appears to be unavailable.`); } const connection1 = engine.makeConnection(this.group, "enabled", this.output.bind(this)); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.enabled' to the controller output. The control appears to be unavailable.`); } @@ -1339,7 +1568,7 @@ class QuickEffectButton extends Button { } /* - * Kontrol S4 Mk3 hardware-specific constants + * Kontrol S4 Mk3 hardware-specific member constants */ Pot.prototype.max = 2 ** 12 - 1; @@ -1347,6 +1576,7 @@ Pot.prototype.inBit = 0; Pot.prototype.inBitLength = 16; Encoder.prototype.inBitLength = 4; +Encoder.prototype.tickDelta = 1 / (2 << Encoder.prototype.inBitLength); // valid range 0 - 3, but 3 makes some colors appear whitish Button.prototype.brightnessOff = 0; @@ -1358,71 +1588,20 @@ Button.prototype.uncoloredOutput = function(value) { const color = (value > 0) ? (this.color || LedColors.white) + this.brightnessOn : LedColors.off; this.send(color); }; -Button.prototype.colorMap = new ColorMapper({ - 0xCC0000: LedColors.red, - 0xCC5E00: LedColors.carrot, - 0xCC7800: LedColors.orange, - 0xCC9200: LedColors.honey, - - 0xCCCC00: LedColors.yellow, - 0x81CC00: LedColors.lime, - 0x00CC00: LedColors.green, - 0x00CC49: LedColors.aqua, - - 0x00CCCC: LedColors.celeste, - 0x0091CC: LedColors.sky, - 0x0000CC: LedColors.blue, - 0xCC00CC: LedColors.purple, - - 0xCC0091: LedColors.fuscia, - 0xCC0079: LedColors.magenta, - 0xCC477E: LedColors.azalea, - 0xCC4761: LedColors.salmon, - - 0xCCCCCC: LedColors.white, -}); - -const wheelRelativeMax = 2 ** 32 - 1; -const wheelAbsoluteMax = 2879; - -const wheelTimerMax = 2 ** 32 - 1; -const wheelTimerTicksPerSecond = 100000000; // One tick every 10ns +Button.prototype.colorMap = new ColorMapper(LedColorMap); -const baseRevolutionsPerSecond = BaseRevolutionsPerMinute / 60; -const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; - -const wheelLEDmodes = { - off: 0, - dimFlash: 1, - spot: 2, - ringFlash: 3, - dimSpot: 4, - individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values -}; +/* + * helper function + */ -// The mode available, which the wheel can be used for. -const wheelModes = { - jog: 0, - vinyl: 1, - motor: 2, - loopIn: 3, - loopOut: 4, +const quickFxChannel = (group) => { + return `[QuickEffectRack1_${group}]`; }; -const moveModes = { - beat: 0, - bpm: 1, - grid: 2, - keyboard: 3, +const stemChannel = (group, idx) => { + return `${group.substr(0, group.length - 1)}Stem${idx}]`; }; -// tracks state across input reports -let wheelTimer = null; -// This is a global variable so the S4Mk3Deck Components have access -// to it and it is guaranteed to be calculated before processing -// input for the Components. -let wheelTimerDelta = 0; - /* * Kontrol S4 Mk3 hardware specific mapping logic */ @@ -1460,14 +1639,14 @@ class S4Mk3EffectUnit extends ComponentContainer { this.group = undefined; this.output(false); }, - input: function(pressed) { + onPress: function() { if (!this.shifted) { for (const index of [0, 1, 2]) { const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; - engine.setValue(effectGroup, "enabled", pressed); + engine.setValue(effectGroup, "enabled", true); } - this.output(pressed); - } else if (pressed) { + this.output(true); + } else { if (this.unit.focusedEffect !== null) { this.unit.setFocusedEffect(null); } else { @@ -1475,6 +1654,15 @@ class S4Mk3EffectUnit extends ComponentContainer { this.shift(); } } + }, + onRelease: function() { + if (!this.shifted) { + for (const index of [0, 1, 2]) { + const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; + engine.setValue(effectGroup, "enabled", false); + } + this.output(false); + } } }); @@ -1746,10 +1934,10 @@ class S4Mk3Deck extends Deck { this.setKey("loop_enabled"); }, outConnect: function() { - if (this.outKey !== undefined && this.group !== undefined) { + if (this.outKey !== undefined && this.group !== undefined && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } @@ -1767,7 +1955,7 @@ class S4Mk3Deck extends Deck { this.indicator(false); const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; - controller.sendOutputReport(wheelOutput.buffer, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); if (!skipRestore) { this.deck.wheelMode = this.previousWheelMode; } @@ -1824,14 +2012,11 @@ class S4Mk3Deck extends Deck { this.output(false); } : undefined, onShortPress: function() { - this.deck.libraryEncoder.gridButtonPressed = true; - if (this.shift) { engine.setValue(this.group, "bpm_tap", true); } }, onLongPress: function() { - this.deck.libraryEncoder.gridButtonPressed = true; this.previousMoveMode = this.deck.moveMode; if (this.shifted) { @@ -1843,7 +2028,6 @@ class S4Mk3Deck extends Deck { this.indicator(true); }, onLongRelease: function() { - this.deck.libraryEncoder.gridButtonPressed = false; if (this.previousMoveMode !== null) { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; @@ -1851,7 +2035,6 @@ class S4Mk3Deck extends Deck { this.indicator(false); }, onShortRelease: function() { - this.deck.libraryEncoder.gridButtonPressed = false; script.triggerControl(this.group, "beats_translate_curpos"); if (this.shift) { @@ -1864,7 +2047,7 @@ class S4Mk3Deck extends Deck { deck: this, input: function(value) { if (value) { - this.deck.switchDeck(Deck.groupForNumber(decks[0])); + this.deck.switchDeck(decks[0]); this.outReport.data[io.deckButtonOutputByteOffset] = colors[0] + this.brightnessOn; // turn off the other deck selection button's LED this.outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + this.brightnessOff : 0; @@ -1876,7 +2059,7 @@ class S4Mk3Deck extends Deck { deck: this, input: function(value) { if (value) { - this.deck.switchDeck(Deck.groupForNumber(decks[1])); + this.deck.switchDeck(decks[1]); // turn off the other deck selection button's LED this.outReport.data[io.deckButtonOutputByteOffset] = DeckSelectAlwaysBacklit ? colors[0] + this.brightnessOff : 0; this.outReport.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn; @@ -1899,18 +2082,52 @@ class S4Mk3Deck extends Deck { shift: function() { this.output(true); }, - input: function(pressed) { - if (pressed) { - this.deck.shift(); - } else { - this.deck.unshift(); + onPress: function() { + this.deck.shift.call(this.deck); + + if (!UseSharedDataAPI) { + return; } - } + + const data = engine.getSharedData() || {}; + if (!data.shift) { return; } + data.shift[decks[0] === 1 ? "leftdeck":"rightdeck"] = true; + engine.setSharedData(data); + }, + onRelease: function() { + this.deck.unshift.call(this.deck); + + if (!UseSharedDataAPI) { + return; + } + + const data = engine.getSharedData() || {}; + if (!data.shift) { return; } + data.shift[decks[0] === 1 ? "leftdeck":"rightdeck"] = false; + engine.setSharedData(data); + }, }); this.leftEncoder = new Encoder({ deck: this, onChange: function(right) { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") + (right ? this.tickDelta : -this.tickDelta)); + }); + return; + } + + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") + (right ? this.tickDelta : -this.tickDelta)); + }); + return; + } switch (this.deck.moveMode) { case moveModes.grid: @@ -1918,18 +2135,34 @@ class S4Mk3Deck extends Deck { break; case moveModes.keyboard: if ( - this.deck.keyboard[0].offset === (right ? 16 : 0) + this.deck.pads[0].offset === (right ? 16 : 0) ) { return; } this.deck.keyboardOffset += (right ? 1 : -1); - this.deck.keyboard.forEach(function(pad) { + this.deck.pads.forEach(function(pad) { pad.outTrigger(); }); break; case moveModes.bpm: script.triggerControl(this.group, right ? "beats_translate_later" : "beats_translate_earlier"); break; + case moveModes.hotcueColor:{ + if (this.deck.selectedHotcue === null) { + return; + } + const currentColor = Button.prototype.colorMap.getValueForNearestColor(engine.getValue(this.deck.group, `hotcue_${this.deck.selectedHotcue}_color`)); + let currentColorIdx = Object.keys(LedColorMap).indexOf(Object.keys(LedColorMap).find(key => LedColorMap[key] === currentColor)); + currentColorIdx = Math.max( + Math.min( + Object.keys(LedColorMap).length - 2, // Last color is reserved for loop hotcue + currentColorIdx + (right ? 1:-1) + ), + 0 + ); + engine.setValue(this.deck.group, `hotcue_${this.deck.selectedHotcue}_color`, Object.keys(LedColorMap)[currentColorIdx]); + break; + } default: if (!this.shifted) { if (!this.deck.leftEncoderPress.pressed) { @@ -1959,17 +2192,50 @@ class S4Mk3Deck extends Deck { } }); this.leftEncoderPress = new PushButton({ - input: function(pressed) { - this.pressed = pressed; - if (pressed) { + deck: this, + onPress: function() { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") === 1.0 ? 0 : 1); + }); + return; + } + if (this.shifted) { script.toggleControl(this.group, "pitch_adjust_set_default"); } + + if (!UseSharedDataAPI) { + return; + } + + + const data = engine.getSharedData() || {}; + if (!data.displayBeatloopSize) { return; } + data.displayBeatloopSize[this.group] = true; + engine.setSharedData(data); }, + onRelease: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.displayBeatloopSize) { return; } + data.displayBeatloopSize[this.group] = false; + engine.setSharedData(data); + } : undefined }); this.rightEncoder = new Encoder({ deck: this, onChange: function(right) { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(quickFxChannel(stemChannel(this.group, stemIdx)), "super1", engine.getValue(quickFxChannel(stemChannel(this.group, stemIdx)), "super1") + (right ? this.tickDelta : -this.tickDelta)); + }); + return; + } + if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { const moveFactor = this.shifted ? LoopEncoderShiftMoveFactor : LoopEncoderMoveFactor; const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor); @@ -1984,8 +2250,14 @@ class S4Mk3Deck extends Deck { } }); this.rightEncoderPress = new PushButton({ - input: function(pressed) { - if (!pressed) { + deck: this, + onPress: function() { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + script.toggleControl(quickFxChannel(stemChannel(this.group, stemIdx)), "enabled"); + }); return; } const loopEnabled = engine.getValue(this.group, "loop_enabled"); @@ -1998,26 +2270,22 @@ class S4Mk3Deck extends Deck { }); this.libraryEncoder = new Encoder({ - libraryPlayButtonPressed: false, - gridButtonPressed: false, - starButtonPressed: false, - libraryViewButtonPressed: false, - libraryPlaylistButtonPressed: false, + deck: this, currentSortedColumnIdx: -1, onChange: function(right) { - if (this.libraryViewButtonPressed) { + if (this.deck.libraryViewButton.pressed) { this.currentSortedColumnIdx = (LibrarySortableColumns.length + this.currentSortedColumnIdx + (right ? 1 : -1)) % LibrarySortableColumns.length; engine.setValue("[Library]", "sort_column", LibrarySortableColumns[this.currentSortedColumnIdx]); - } else if (this.starButtonPressed) { + } else if (this.deck.libraryStarButton.pressed) { if (this.shifted) { // FIXME doesn't exist, feature request needed script.triggerControl(this.group, right ? "track_color_prev" : "track_color_next"); } else { script.triggerControl(this.group, right ? "stars_up" : "stars_down"); } - } else if (this.gridButtonPressed) { + } else if (this.deck.gridButton.pressed) { script.triggerControl(this.group, right ? "waveform_zoom_up" : "waveform_zoom_down"); - } else if (this.libraryPlayButtonPressed) { + } else if (this.deck.libraryPlayButton.pressed) { script.triggerControl("[PreviewDeck1]", right ? "beatjump_16_forward" : "beatjump_16_backward"); } else { // FIXME there is a bug where this action has no effect when the Mixxx window has no focused. https://github.com/mixxxdj/mixxx/issues/11285 @@ -2037,9 +2305,9 @@ class S4Mk3Deck extends Deck { } }); this.libraryEncoderPress = new Button({ - libraryViewButtonPressed: false, + deck: this, onShortPress: function() { - if (this.libraryViewButtonPressed) { + if (this.deck.libraryViewButton.pressed) { script.toggleControl("[Library]", "sort_order"); } else { const currentlyFocusWidget = engine.getValue("[Library]", "focused_widget"); @@ -2047,7 +2315,11 @@ class S4Mk3Deck extends Deck { if (this.shifted && currentlyFocusWidget === 0) { script.triggerControl("[Playlist]", "ToggleSelectedSidebarItem"); } else if (currentlyFocusWidget === 3 || currentlyFocusWidget === 0) { - script.triggerControl(this.group, "LoadSelectedTrack"); + if (this.deck.hasSelectedStem()) { + engine.setValue(this.group, "load_selected_track_stems", this.deck.stemSelection()); + } else { + script.triggerControl(this.group, "LoadSelectedTrack"); + } } else { script.triggerControl("[Library]", "GoToItem"); } @@ -2060,15 +2332,17 @@ class S4Mk3Deck extends Deck { }); this.libraryPlayButton = new PushButton({ group: "[PreviewDeck1]", - libraryEncoder: this.libraryEncoder, - input: function(pressed) { - if (pressed) { - script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); + deck: this, + onPress: function() { + if (this.shifted) { + engine.setValue(this.group, "CloneFromDeck", this.deck.currentDeckNumber); } else { - engine.setValue(this.group, "play", 0); - script.triggerControl(this.group, "eject"); + script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); } - this.libraryEncoder.libraryPlayButtonPressed = pressed; + }, + onRelease: function() { + engine.setValue(this.group, "play", 0); + script.triggerControl(this.group, "eject"); }, outKey: "play", }); @@ -2078,12 +2352,6 @@ class S4Mk3Deck extends Deck { onShortRelease: function() { script.triggerControl(this.group, this.shifted ? "track_color_prev" : "track_color_next"); }, - onLongPress: function() { - this.libraryEncoder.starButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.starButtonPressed = false; - }, }); // FIXME there is no feature about playlist at the moment, so we use this button to control the context menu, which has playlist control this.libraryPlaylistButton = new Button({ @@ -2096,7 +2364,7 @@ class S4Mk3Deck extends Deck { }); // This is useful for case where effect would have been fully disabled in Mixxx. This appears to be the case during unit tests. if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.focused_widget' to the controller output. The control appears to be unavailable.`); } @@ -2109,18 +2377,11 @@ class S4Mk3Deck extends Deck { return; } script.toggleControl("[Library]", "show_track_menu"); - this.libraryEncoder.libraryPlayButtonPressed = false; if (currentlyFocusWidget === 4) { engine.setValue("[Library]", "focused_widget", 3); } }, - onShortPress: function() { - this.libraryEncoder.libraryPlayButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.libraryPlayButtonPressed = false; - }, onLongPress: function() { engine.setValue("[Library]", "clear_search", 1); } @@ -2133,14 +2394,7 @@ class S4Mk3Deck extends Deck { onShortRelease: function() { script.toggleControl(this.group, this.inKey, true); }, - onLongPress: function() { - this.libraryEncoder.libraryViewButtonPressed = true; - this.libraryEncoderPress.libraryViewButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.libraryViewButtonPressed = false; - this.libraryEncoderPress.libraryViewButtonPressed = false; - } + onLongPress: function() {}, // This is needed to make difference between a shot and long press }); this.keyboardPlayMode = null; @@ -2161,28 +2415,58 @@ class S4Mk3Deck extends Deck { cueBaseName: "outro_end", }), new HotcueButton({ - number: 1 + number: 1, deck: this }), new HotcueButton({ - number: 2 + number: 2, deck: this }), new HotcueButton({ - number: 3 + number: 3, deck: this }), new HotcueButton({ - number: 4 + number: 4, deck: this }) ]; const hotcuePage2 = Array(8).fill({}); const hotcuePage3 = Array(8).fill({}); const samplerOrBeatloopRollPage = Array(8).fill({}); - this.keyboard = Array(8).fill({}); + const keyboard = Array(8).fill({}); + const stem = [ + new StemButton({ + number: 1, + deck: this, + }), + new StemButton({ + number: 2, + deck: this, + }), + new StemButton({ + number: 3, + deck: this, + }), + new StemButton({ + number: 4, + deck: this, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + new Component({ + outConnect: function() { this.send(0); }, + }), + ]; let i = 0; /* eslint no-unused-vars: "off" */ for (const pad of hotcuePage2) { // start with hotcue 5; hotcues 1-4 are in defaultPadLayer - hotcuePage2[i] = new HotcueButton({number: i + 1}); - hotcuePage3[i] = new HotcueButton({number: i + 13}); + hotcuePage2[i] = new HotcueButton({number: i + 1, deck: this}); + hotcuePage3[i] = new HotcueButton({number: i + 13, deck: this}); if (UseBeatloopRollInsteadOfSampler) { samplerOrBeatloopRollPage[i] = new BeatLoopRollButton({ number: i, @@ -2199,6 +2483,7 @@ class S4Mk3Deck extends Deck { } samplerOrBeatloopRollPage[i] = new SamplerButton({ number: samplerNumber, + deck: this, }); if (SamplerCrossfaderAssign) { engine.setValue( @@ -2208,7 +2493,7 @@ class S4Mk3Deck extends Deck { ); } } - this.keyboard[i] = new KeyboardButton({ + keyboard[i] = new KeyboardButton({ number: i + 1, deck: this, }); @@ -2217,9 +2502,13 @@ class S4Mk3Deck extends Deck { const switchPadLayer = (deck, newLayer) => { let index = 0; + if (newLayer === samplerOrBeatloopRollPage && deck.hasSelectedStem()) { + deck.samplerStemSelection = deck.stemSelection(); + } for (let pad of deck.pads) { pad.outDisconnect(); pad.inDisconnect(); + const shifted = pad.shifted; pad = newLayer[index]; Object.assign(pad, io.pads[index]); @@ -2233,6 +2522,11 @@ class S4Mk3Deck extends Deck { if (pad.inReport === undefined) { pad.inReport = inReports[1]; } + if (shifted && typeof pad.shift === "function" && pad.shift.length === 0) { + pad.shift(); + } else if (typeof pad.unshift === "function" && pad.unshift.length === 0) { + pad.unshift(); + } pad.outReport = outReport; pad.inConnect(); pad.outConnect(); @@ -2248,6 +2542,7 @@ class S4Mk3Deck extends Deck { hotcuePage3: 2, samplerPage: 3, keyboard: 5, + stem: 6, }; switch (DefaultPadLayout) { case DefaultPadLayoutHotcue: @@ -2270,7 +2565,7 @@ class S4Mk3Deck extends Deck { this.hotcuePadModeButton = new Button({ deck: this, - onShortPress: function() { + onShortRelease: function() { if (!this.shifted) { if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage2) { switchPadLayer(this.deck, hotcuePage2); @@ -2287,34 +2582,86 @@ class S4Mk3Deck extends Deck { } }, + onShortPress: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.cue; + engine.setSharedData(data); + } : undefined, + onLongPress: function() { + this.previousMoveMode = this.deck.moveMode; + this.deck.moveMode = moveModes.hotcueColor; + + }, + onLongRelease: function() { + this.deck.moveMode = this.previousMoveMode; + this.previousMoveMode = null; + }, // hack to switch the LED color when changing decks outTrigger: function() { this.deck.lightPadMode(); } }); // The record button doesn't have a mapping by default, but you can add yours here - // this.recordPadModeButton = new Button({ - // ... - // }); + this.recordPadModeButton = new Button({ + deck: this, + onShortPress: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.record; + engine.setSharedData(data); + this.output(data.scrollingWavefom[this.deck.group]); + } : undefined, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } + }); this.samplesPadModeButton = new Button({ + pressed: false, deck: this, onShortPress: function() { + engine.setValue(this.deck.group, "loop_anchor", 1); + + if (!UseSharedDataAPI) { + return; + } + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.samples; + engine.setSharedData(data); + }, + onShortRelease: function() { if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage) { switchPadLayer(this.deck, samplerOrBeatloopRollPage); - engine.setValue("[Samplers]", "show_samplers", true); + engine.setValue("[Skin]", "show_samplers", true); this.deck.currentPadLayer = this.deck.padLayers.samplerPage; } else { switchPadLayer(this.deck, defaultPadLayer); - engine.setValue("[Samplers]", "show_samplers", false); + engine.setValue("[Skin]", "show_samplers", false); this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; } this.deck.lightPadMode(); + engine.setValue(this.deck.group, "loop_anchor", 0); }, + onLongRelease: function() { + engine.setValue(this.deck.group, "loop_anchor", 0); + } }); // The mute button doesn't have a mapping by default, but you can add yours here - // this.mutePadModeButton = new Button({ - // ... - // }); + this.mutePadModeButton = new Button({ + deck: this, + onShortPress: UseSharedDataAPI ? function() { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.mute; + engine.setSharedData(data); + } : undefined, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } + }); this.stemsPadModeButton = new Button({ deck: this, @@ -2326,6 +2673,12 @@ class S4Mk3Deck extends Deck { } }, onShortPress: function() { + if (UseSharedDataAPI) { + const data = engine.getSharedData() || {}; + if (!data.padsMode) { return; } + data.padsMode[this.deck.group] = ActiveTabPadID.stems; + engine.setSharedData(data); + } if (this.previousMoveMode === null) { this.previousMoveMode = this.deck.moveMode; this.deck.moveMode = moveModes.keyboard; @@ -2336,12 +2689,19 @@ class S4Mk3Deck extends Deck { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; } - if (this.deck.currentPadLayer === this.deck.padLayers.keyboard) { + let targetLayer = this.deck.padLayers.stem; + if (this.shifted) { + targetLayer = this.deck.padLayers.keyboard; + } + if (this.deck.currentPadLayer === targetLayer) { switchPadLayer(this.deck, defaultPadLayer); this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - } else if (this.deck.currentPadLayer !== this.deck.padLayers.keyboard) { - switchPadLayer(this.deck, this.deck.keyboard); - this.deck.currentPadLayer = this.deck.padLayers.keyboard; + } else if (targetLayer === this.deck.padLayers.stem) { + switchPadLayer(this.deck, stem); + this.deck.currentPadLayer = targetLayer; + } else if (targetLayer === this.deck.padLayers.keyboard) { + switchPadLayer(this.deck, keyboard); + this.deck.currentPadLayer = targetLayer; } this.deck.lightPadMode(); }, @@ -2358,21 +2718,22 @@ class S4Mk3Deck extends Deck { }); this.wheelMode = wheelModes.vinyl; - this.turntableButton = UseMotors ? new Button({ + this.turntableButton = new Button({ deck: this, - input: function(press) { - if (press) { - this.deck.reverseButton.loopModeOff(true); - this.deck.fluxButton.loopModeOff(true); - if (this.deck.wheelMode === wheelModes.motor) { - this.deck.wheelMode = wheelModes.vinyl; - engine.setValue(this.group, "scratch2_enable", false); - } else { - this.deck.wheelMode = wheelModes.motor; - const group = this.group; - } - this.outTrigger(); + onPress: function() { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); + if (this.deck.wheelMode === wheelModes.motor) { + this.deck.wheelMode = wheelModes.vinyl; + engine.setValue(this.group, "scratch2_enable", false); + } else { + this.deck.wheelMode = wheelModes.motor; + const group = this.group; + engine.beginTimer(MotorWindUpMilliseconds, () => { + engine.setValue(group, "scratch2_enable", true); + }, true); } + this.outTrigger(); }, outTrigger: function() { const motorOn = this.deck.wheelMode === wheelModes.motor; @@ -2383,18 +2744,16 @@ class S4Mk3Deck extends Deck { }) : undefined; this.jogButton = new Button({ deck: this, - input: function(press) { - if (press) { - this.deck.reverseButton.loopModeOff(true); - this.deck.fluxButton.loopModeOff(true); - if (this.deck.wheelMode === wheelModes.vinyl) { - this.deck.wheelMode = wheelModes.jog; - } else { - this.deck.wheelMode = wheelModes.vinyl; - } - engine.setValue(this.group, "scratch2_enable", false); - this.outTrigger(); + onPress: function() { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); + if (this.deck.wheelMode === wheelModes.vinyl) { + this.deck.wheelMode = wheelModes.jog; + } else { + this.deck.wheelMode = wheelModes.vinyl; } + engine.setValue(this.group, "scratch2_enable", false); + this.outTrigger(); }, outTrigger: function() { const vinylOn = this.deck.wheelMode === wheelModes.vinyl; @@ -2512,6 +2871,7 @@ class S4Mk3Deck extends Deck { engine.setValue(this.group, "scratch2", this.speed); } else { engine.setValue(this.group, "jog", this.speed); + console.log(this.speed) } break; default: @@ -2582,6 +2942,9 @@ class S4Mk3Deck extends Deck { } }); + this.selectedStem = new Array(4).fill(false); + this.samplerStemSelection = null; + for (const property in this) { if (Object.prototype.hasOwnProperty.call(this, property)) { const component = this[property]; @@ -2611,6 +2974,17 @@ class S4Mk3Deck extends Deck { } } + hasSelectedStem() { + return this.selectedStem.some((stemSelected) => stemSelected); + } + + stemSelection() { + return [...this.selectedStem].reverse().reduce( + (acc, curr) => (curr + acc * 2), + 0, + ); + } + assignKeyboardPlayMode(group, action) { this.keyboardPlayMode = { group: group, @@ -2628,21 +3002,26 @@ class S4Mk3Deck extends Deck { this.hotcuePadModeButton.send(this.hotcuePadModeButton.color + this.hotcuePadModeButton.brightnessOff); } + const data = (UseSharedDataAPI ? engine.getSharedData() : false) || {}; + // unfortunately the other pad mode buttons only have one LED color // const recordPadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage3; - // this.recordPadModeButton.send(recordPadModeLEDOn ? 127 : 0); + this.recordPadModeButton.output(data.scrollingWavefom && data.scrollingWavefom[this.group]); const samplesPadModeLEDOn = this.currentPadLayer === this.padLayers.samplerPage; this.samplesPadModeButton.send(samplesPadModeLEDOn ? 127 : 0); // this.mutePadModeButtonLEDOn = this.currentPadLayer === this.padLayers.samplerPage2; - // const mutedModeButton.send(mutePadModeButtonLEDOn ? 127 : 0); + this.mutePadModeButton.output(data.viewArtwork && data.viewArtwork[this.group]); if (this.keyboardPlayMode !== null) { this.stemsPadModeButton.send(LedColors.green + this.stemsPadModeButton.brightnessOn); } else { - const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard; + const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard || this.currentPadLayer === this.padLayers.stem; this.stemsPadModeButton.send(this.stemsPadModeButton.color + (keyboardPadModeLEDOn ? this.stemsPadModeButton.brightnessOn : this.stemsPadModeButton.brightnessOff)); } + if (!UseSharedDataAPI || !data.keyboardMode) { return; } + data.keyboardMode[this.group] = this.currentPadLayer === this.padLayers.keyboard; + engine.setSharedData(data); } } @@ -2750,7 +3129,7 @@ class S4Mk3MixerColumn extends ComponentContainer { inKey: "parameter1", }); this.quickEffectKnob = new Pot({ - group: `[QuickEffectRack1_${this.group}]`, + group: quickFxChannel(this.group), inKey: "super1", }); this.volume = new Pot({ @@ -2848,6 +3227,7 @@ class S4Mk3MixerColumn extends ComponentContainer { if (!alternativeInput) { return; } + console.log(shifted ? alternativeInput : `[Channel${this.idx}]`); this.group = shifted ? alternativeInput : `[Channel${this.idx}]`; for (const property of ["gain", "volume", "pfl", "crossfaderSwitch"]) { const component = this[property]; @@ -3121,6 +3501,13 @@ class S4MK3 { wheelLEDinitReport[0] = 1; controller.sendOutputReport(48, wheelLEDinitReport.buffer); + const motorData = new Uint8Array([ + 1, 0x20, 1, 0, 0, + 1, 0x20, 1, 0, 0, + + ]); + controller.sendOutputReport(49, motorData.buffer); + // Init wheel timer data wheelTimer = null; wheelTimerDelta = 0; @@ -3129,6 +3516,51 @@ class S4MK3 { for (const repordId of [0x01, 0x02]) { this.inReports[repordId].handleInput(controller.getInputReport(repordId)); } + + if (!UseSharedDataAPI) { + return; + } + + engine.setSharedData({ + group: { + "leftdeck": "[Channel1]", + "rightdeck": "[Channel2]", + }, + shift: { + "leftdeck": false, + "rightdeck": false, + }, + scrollingWavefom: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + viewArtwork: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + keyboardMode: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + displayBeatloopSize: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + padsMode: { + "[Channel1]": 0, + "[Channel2]": 0, + "[Channel3]": 0, + "[Channel4]": 0, + }, + }); } shutdown() { // button LEDs diff --git a/res/controllers/TraktorKontrolS4MK3Screens.qml b/res/controllers/TraktorKontrolS4MK3Screens.qml new file mode 100644 index 00000000000..fe522503278 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens.qml @@ -0,0 +1,217 @@ +import QtQuick 2.15 +import QtQuick.Window 2.3 + +import QtQuick.Controls 2.15 +import QtQuick.Shapes 1.11 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.15 + +import Qt5Compat.GraphicalEffects + +import "." as Skin +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +import S4MK3 as S4MK3 + +Mixxx.ControllerScreen { + id: root + + required property string screenId + property color fontColor: Qt.rgba(242/255,242/255,242/255, 1) + property color smallBoxBorder: Qt.rgba(44/255,44/255,44/255, 1) + + property string group: screenId == "rightdeck" ? "[Channel2]" : "[Channel1]" + property string theme: engine.getSetting("theme") + + readonly property bool isStockTheme: theme == "stock" + + property var lastFrame: null + + init: function(_controllerName, isDebug) { + console.log(`Screen ${root.screenId} has started with theme ${root.theme}`) + root.state = "Live" + } + + shutdown: function() { + console.log(`Screen ${root.screenId} is stopping`) + root.state = "Stop" + } + + transformFrame: function(input, timestamp) { + let updated = new Uint8Array(320*240); + updated.fill(0) + + let updatedPixelCount = 0; + let updated_zones = []; + + if (!root.lastFrame) { + root.lastFrame = new ArrayBuffer(input.byteLength); + updatedPixelCount = input.byteLength / 2; + updated_zones.push({ + x: 0, + y: 0, + width: 320, + height: 240, + }) + } else { + const view_input = new Uint8Array(input); + const view_last = new Uint8Array(root.lastFrame); + + for (let i = 0; i < 320 * 240; i++) { + } + + let current_rect = null; + + for (let y = 0; y < 240; y++) { + let line_changed = false; + for (let x = 0; x < 320; x++) { + let i = y * 320 + x; + if (view_input[2 * i] != view_last[2 * i] || view_input[2 * i + 1] != view_last[2 * i + 1]) { + line_changed = true; + updatedPixelCount++; + break; + } + } + if (current_rect !== null && line_changed) { + current_rect.height++; + } else if (current_rect !== null) { + updated_zones.push(current_rect); + current_rect = null; + } else if (current_rect === null && line_changed) { + current_rect = { + x: 0, + y, + width: 320, + height: 1, + }; + } + } + if (current_rect !== null) { + updated_zones.push(current_rect); + } + } + new Uint8Array(root.lastFrame).set(new Uint8Array(input)); + + if (!updatedPixelCount) { + return new ArrayBuffer(0); + } else if (root.renderDebug) { + console.log(`Pixel updated: ${updatedPixelCount}, ${updated_zones.length} areas`); + } + + // No redraw needed, stop right there + + let totalPixelToDraw = 0; + for (const area of updated_zones) { + area.x -= Math.min(2, area.x); + area.y -= Math.min(2, area.y); + area.width += Math.min(4, 320 - area.x - area.width); + area.height += Math.min(4, 240 - area.y - area.height); + totalPixelToDraw += area.width*area.height; + } + + if (totalPixelToDraw != 320*240 && (totalPixelToDraw > 320 * 180 || updated_zones.length > 20)) { + if (root.renderDebug) { + console.log(`Full redraw instead of ${totalPixelToDraw} pixels/${updated_zones.length} areas`) + } + totalPixelToDraw = 320*240 + updated_zones = [{ + x: 0, + y: 0, + width: 320, + height: 240, + }] + } else if (root.renderDebug) { + console.log(`Redrawing ${totalPixelToDraw} pixels`) + } + + const screenIdx = screenId === "leftdeck" ? 0 : 1; + + const outputData = new ArrayBuffer(totalPixelToDraw*2 + 20*updated_zones.length); // Number of pixel + 20 (header/footer size) x the number of region + let offset = 0; + + for (const area of updated_zones) { + const header = new Uint8Array(outputData, offset, 16); + const payload = new Uint8Array(outputData, offset + 16, area.width*area.height*2); + const footer = new Uint8Array(outputData, offset + area.width*area.height*2 + 16, 4); + + header.fill(0) + footer.fill(0) + header[0] = 0x84; + header[2] = screenIdx; + header[3] = 0x21; + + header[8] = area.x >> 8; + header[9] = area.x & 0xff; + header[10] = area.y >> 8; + header[11] = area.y & 0xff; + + header[12] = area.width >> 8; + header[13] = area.width & 0xff; + header[14] = area.height >> 8; + header[15] = area.height & 0xff; + + if (area.x === 0 && area.width === 320) { + payload.set(new Uint8Array(input, area.y * 320 * 2, area.width*area.height*2)); + } else { + for (let y = 0; y < area.height; y++) { + payload.set( + new Uint8Array(input, ((area.y + y) * 320 + area.x) * 2, area.width * 2), + y * area.width * 2); + } + } + footer[0] = 0x40; + footer[2] = screenIdx; + offset += area.width*area.height*2 + 20 + } + if (root.renderDebug) { + console.log(`Generated ${offset} bytes to be sent`) + } + // return new ArrayBuffer(0); + return outputData; + } + + Component { + id: splashOff + S4MK3.SplashOff { + anchors.fill: parent + } + } + Component { + id: stockLive + S4MK3.StockScreen { + group: root.group + screenId: root.screenId + anchors.fill: parent + } + } + Component { + id: advancedLive + S4MK3.AdvancedScreen { + isLeftScreen: root.screenId == "leftdeck" + } + } + + Loader { + id: loader + anchors.fill: parent + sourceComponent: splashOff + } + + states: [ + State { + name: "Live" + PropertyChanges { + target: loader + sourceComponent: isStockTheme ? stockLive : advancedLive + } + }, + State { + name: "Stop" + PropertyChanges { + target: loader + sourceComponent: splashOff + } + } + ] +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml new file mode 100755 index 00000000000..c49800618f2 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/BPMIndicator.qml @@ -0,0 +1,77 @@ +/* +This module is used to define the top right section, right under the label. +Currently this section is dedicated to BPM and tempo fader information. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + required property color borderColor + + property real value: 0 + + color: "transparent" + radius: 6 + border.color: smallBoxBorder + border.width: 2 + + signal updated + + Text { + id: indicator + text: "-" + font.pixelSize: 17 + color: fontColor + anchors.centerIn: parent + + Mixxx.ControlProxy { + group: root.group + key: "bpm" + onValueChanged: (value) => { + const newValue = value.toFixed(2); + if (newValue === indicator.text) return; + indicator.text = newValue; + root.updated() + } + } + } + + Text { + id: range + font.pixelSize: 9 + color: fontColor + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.topMargin: 2 + + horizontalAlignment: Text.AlignHCenter + + Mixxx.ControlProxy { + group: root.group + key: "rateRange" + onValueChanged: (value) => { + const newValue = `-/+ \n${(value * 100).toFixed()}%`; + if (range.text === newValue) return; + range.text = newValue; + root.updated(); + } + } + } + + states: State { + name: "compacted" + + PropertyChanges { + target: range + visible: false + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml new file mode 100644 index 00000000000..f39bdc1ca79 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/HotcuePoint.qml @@ -0,0 +1,199 @@ +/* +This module is used to define markers element as render over the the overview waveform. +When this is written, Mixxx QML doesn't have waveform overview marker ready to be used, so this +is an attempt to provide fully functional markers for the controller screen, while the Mixxx QML +interface is still being worked on. +Consider replacing this with native overview marker in the future. +*/ +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Window 2.15 + +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx + +Item { + required property real position + required property int type + + property int number: 1 + property color color: 'blue' + + enum Type { + OneShot, + Loop, + IntroIn, + IntroOut, + OutroIn, + OutroOut, + LoopIn, + LoopOut + } + + property variant typeWithNumber: [ + HotcuePoint.Type.OneShot, + HotcuePoint.Type.Loop + ] + + x: position * (Window.width - 16) + width: 21 + + // One shot + Shape { + visible: type == HotcuePoint.Type.OneShot + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: color + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 0; startY: 0 + + PathLine { x: 12; y: 0 } + PathLine { x: 18; y: 6 } + PathLine { x: 18; y: 7 } + PathLine { x: 12; y: 13 } + PathLine { x: 2; y: 13 } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + } + } + + // Intro/Outro entry marker + Shape { + visible: type == HotcuePoint.Type.IntroIn || type == HotcuePoint.Type.OutroIn + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6e6e6e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 0; startY: 0 + + PathLine { x: 11; y: 0 } + PathLine { x: 2; y: 13 } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + } + } + + // Intro/Outro exit marker + Shape { + visible: type == HotcuePoint.Type.IntroOut || type == HotcuePoint.Type.OutroOut + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6e6e6e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 2; startY: 0 + + PathLine { x: 0; y: 0 } + PathLine { x: 0; y: 67 } + PathLine { x: -9; y: 80 } + PathLine { x: 2; y: 80 } + PathLine { x: 2; y: 0 } + } + } + + // Loop + Shape { + visible: type == HotcuePoint.Type.Loop + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6ef36e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 13; startY: 0 + + PathArc { + x: 2; y: 13 + radiusX: 9; radiusY: 9 + direction: PathArc.Clockwise + } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + PathLine { x: 21; y: 0 } + } + } + + // Loop in + Shape { + visible: type == HotcuePoint.Type.LoopIn + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6ef36e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 0; startY: 0 + + PathLine { x: 8; y: 0 } + PathLine { x: 2; y: 10 } + PathLine { x: 2; y: 80 } + PathLine { x: 0; y: 80 } + PathLine { x: 0; y: 0 } + } + } + + // Loop out + Shape { + visible: type == HotcuePoint.Type.LoopOut + anchors.fill: parent + antialiasing: true + + ShapePath { + strokeWidth: 1 + strokeColor: Qt.rgba(0, 0, 0, 0.5) + fillColor: "#6ef36e" + strokeStyle: ShapePath.SolidLine + // dashPattern: [ 1, 4 ] + startX: 2; startY: 0 + + PathLine { x: -6; y: 0 } + PathLine { x: 0; y: 10 } + PathLine { x: 0; y: 80 } + PathLine { x: 2; y: 80 } + PathLine { x: 2; y: 0 } + } + } + + Shape { + visible: type in typeWithNumber + anchors.fill: parent + antialiasing: true + + ShapePath { + fillColor: "black" + strokeColor: "black" + PathText { + x: 4 + y: 3 + font.family: "Arial" + font.pixelSize: 11 + font.weight: Font.Medium + text: `${number}` + } + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml new file mode 100755 index 00000000000..5d2c1b4c582 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/KeyIndicator.qml @@ -0,0 +1,127 @@ +/* +This module is used to define the top left section, right under the label. +Currently this section is dedicated to key/pitch information. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + + enum Key { + NoKey, + OneD, + EightD, + ThreeD, + TenD, + FiveD, + TwelveD, + SevenD, + SecondD, + NineD, + FourD, + ElevenD, + SixD, + TenM, + FiveM, + TwelveM, + SevenM, + TwoM, + NineM, + FourM, + ElevenM, + SixM, + OneM, + EightM, + ThreeM + } + + property variant colorsMap: [ + "#b09840", // No key + "#b960a2",// 1d + "#9fc516", // 8d + "#527fc0", // 3d + "#f28b2e", // 10d + "#5bc1cf", // 5d + "#e84c4d", // 12d + "#73b629", // 7d + "#8269ab", // 2d + "#fdd615", // 9d + "#3cc0f0", // 4d + "#4cb686", // 11d + "#4cb686", // 6d + "#f5a158", // 10m + "#7bcdd9", // 5m + "#ed7171", // 12m + "#8fc555", // 7m + "#9b86be", // 2m + "#fcdf45", // 9m + "#63cdf4", // 4m + "#f1845f", // 11m + "#70c4a0", // 6m + "#c680b6", // 1m + "#b2d145", // 8m + "#7499cd" // 3m + ] + + property variant textMap: [ + "No key", + "1d", + "8d", + "3d", + "10d", + "5d", + "12d", + "7d", + "2d", + "9d", + "4d", + "11d", + "6d", + "10m", + "5m", + "12m", + "7m", + "2m", + "9m", + "4m", + "11m", + "6m", + "1m", + "8m", + "3m" + ] + + required property color borderColor + + property int key: KeyIndicator.Key.NoKey + + radius: 6 + border.color: colorsMap[key] + border.width: 2 + + color: colorsMap[key] + signal updated + + Mixxx.ControlProxy { + group: root.group + key: "key" + onValueChanged: (value) => { + if (value === root.key) return; + root.key = value; + root.updated() + } + } + + Text { + text: textMap[key] + font.pixelSize: 17 + color: fontColor + anchors.centerIn: parent + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml new file mode 100644 index 00000000000..2e3b87e453d --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Keyboard.qml @@ -0,0 +1,140 @@ +/* +This module is used render the keyboard scale, originating from C major (do). +*/ +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Layouts 1.3 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +import "." as S4MK3 + +Item { + id: root + + required property string group + + property int key: -1 + + signal updated + + Mixxx.ControlProxy { + group: root.group + key: "key" + onValueChanged: (value) => { + if (value === root.key) return; + root.key = value; + root.updated() + } + } + + RowLayout { + anchors.fill: parent + spacing: 0 + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + Repeater { + id: whiteKeys + + model: 7 + + property variant keyMap: [ + S4MK3.KeyIndicator.Key.OneD, + S4MK3.KeyIndicator.Key.ThreeD, + S4MK3.KeyIndicator.Key.FiveD, + S4MK3.KeyIndicator.Key.TwelveD, + S4MK3.KeyIndicator.Key.SecondD, + S4MK3.KeyIndicator.Key.FourD, + S4MK3.KeyIndicator.Key.SixD, + S4MK3.KeyIndicator.Key.TenM, + S4MK3.KeyIndicator.Key.TwelveM, + S4MK3.KeyIndicator.Key.TwoM, + S4MK3.KeyIndicator.Key.NineM, + S4MK3.KeyIndicator.Key.ElevenM, + S4MK3.KeyIndicator.Key.OneM, + S4MK3.KeyIndicator.Key.ThreeM + ] + + Rectangle { + Layout.preferredWidth: 21 + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + radius: 2 + border.width: 1 + border.color: root.key == whiteKeys.keyMap[index] || root.key == whiteKeys.keyMap[index + 7] ? "red" : "black" + color: root.key == whiteKeys.keyMap[index] || root.key == whiteKeys.keyMap[index + 7] ? "#aaaaaa" : "white" + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + } + RowLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + Repeater { + id: blackKeys + + model: 5 + + property variant keyMap: [ + S4MK3.KeyIndicator.Key.EightD, + S4MK3.KeyIndicator.Key.TenD, + S4MK3.KeyIndicator.Key.SevenD, + S4MK3.KeyIndicator.Key.NineD, + S4MK3.KeyIndicator.Key.ElevenD, + S4MK3.KeyIndicator.Key.FiveM, + S4MK3.KeyIndicator.Key.SevenM, + S4MK3.KeyIndicator.Key.FourM, + S4MK3.KeyIndicator.Key.SixM, + S4MK3.KeyIndicator.Key.EightM, + ] + + Item { + Layout.fillHeight: true + Layout.preferredWidth: index == 1 ? 42 : index == 4 ? 12 : 21 + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 12 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + color: "transparent" + ColumnLayout { + anchors.fill: parent + spacing: 0 + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + radius: 2 + border.width: 1 + // border.color: root.key == blackKeys.keyMap[index] || root.key == blackKeys.keyMap[index + blackKeys.model] ? "red" : "black" + color: root.key == blackKeys.keyMap[index] || root.key == blackKeys.keyMap[index + blackKeys.model] ? "#aaaaaa" : "black" + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + } + } + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { anchors.fill: parent; color: "transparent" } + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml new file mode 100755 index 00000000000..7f0bdec7d01 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/LoopSizeIndicator.qml @@ -0,0 +1,75 @@ +/* +This module is used to define the center right section, above the waveform. +Currently this section is dedicated to display loop state information such as loop state, anchor mode or size. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + + property color loopReverseOffBoxColor: Qt.rgba(255/255,113/255,9/255, 1) + property color loopOffBoxColor: Qt.rgba(67/255,70/255,66/255, 1) + property color loopOffFontColor: "white" + property color loopOnBoxColor: Qt.rgba(125/255,246/255,64/255, 1) + property color loopOnFontColor: "black" + + property bool on: true + signal updated + + radius: 6 + border.width: 2 + border.color: (loopSizeIndicator.on ? loopOnBoxColor : (loop_anchor.value == 0 ? loopOffBoxColor : loopReverseOffBoxColor)) + color: (loopSizeIndicator.on ? loopOnBoxColor : (loop_anchor.value == 0 ? loopOffBoxColor : loopReverseOffBoxColor)) + + Text { + id: indicator + anchors.centerIn: parent + font.pixelSize: 46 + color: (loopSizeIndicator.on ? loopOnFontColor : loopOffFontColor) + + Mixxx.ControlProxy { + group: root.group + key: "beatloop_size" + onValueChanged: (value) => { + const newValue = (value < 1 ? `1/${1 / value}` : `${value}`); + if (newValue === indicator.text) return; + indicator.text = newValue; + root.updated() + } + } + } + + Mixxx.ControlProxy { + group: root.group + key: "loop_enabled" + onValueChanged: (value) => { + if (value === root.on) return; + root.on = value; + root.updated() + } + } + + Mixxx.ControlProxy { + group: root.group + key: "loop_anchor" + id: loop_anchor + onValueChanged: (value) => { + root.updated() + } + } + + states: State { + name: "compacted" + + PropertyChanges { + target: indicator + font.pixelSize: 17 + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml new file mode 100644 index 00000000000..0d034066a6e --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/OnAirTrack.qml @@ -0,0 +1,81 @@ +/* +This module is used to define the top section o the screen. +Currently this section is dedicated to display title and artist of the track loaded on the deck. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Item { + id: root + + required property string group + property var deckPlayer: Mixxx.PlayerManager.getPlayer(root.group) + property bool scrolling: true + + property real speed: 1.7 + property real spacing: 30 + + Rectangle { + id: frame + anchors.top: root.top + anchors.bottom: root.bottom + width: parent.width + x: 6 + color: 'transparent' + + readonly property string fulltext: !trackLoadedControl.value || root.deckPlayer.title.trim().length + root.deckPlayer.artist.trim().length == 0 ? qsTr("No Track Loaded") : `${root.deckPlayer.title} - ${root.deckPlayer.artist}`.trim() + + Text { + id: text1 + text: frame.fulltext + font.pixelSize: 24 + font.family: "Noto Sans" + font.letterSpacing: -1 + color: fontColor + } + Text { + id: text2 + visible: root.width < text1.implicitWidth + anchors.left: text1.right + anchors.leftMargin: spacing + text: frame.fulltext + font.pixelSize: 24 + font.family: "Noto Sans" + font.letterSpacing: -1 + color: fontColor + } + } + + Mixxx.ControlProxy { + id: trackLoadedControl + + group: root.group + key: "track_loaded" + } + + Timer { + id: timer + + property int modifier: -root.speed + + repeat: true + interval: 15 + running: root.width < text1.implicitWidth && root.scrolling + + onTriggered: { + frame.x += modifier; + if (frame.x <= -text1.implicitWidth - spacing) { + frame.x = 0; + } + } + + onRunningChanged: { + if (!running) { + frame.x = 6; + } + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml new file mode 100755 index 00000000000..82d8f8b3f89 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/Progression.qml @@ -0,0 +1,53 @@ +/* +This module is used to draw an overlay on the waveform overview in order to highlight better the playback progression. +As the native Mixxx QML component involves, this component might become redundant and should be replaces with native modules. +*/ +import QtQuick 2.15 +import QtQuick.Window 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Item { + id: root + + required property string group + + property real windowWidth: Window.width + + width: 0 + signal updated + + Mixxx.ControlProxy { + group: root.group + key: "track_loaded" + onValueChanged: (value) => { + if (value === root.visible) return; + root.visible = value + root.updated() + } + } + + Mixxx.ControlProxy { + group: root.group + key: "playposition" + onValueChanged: (value) => { + const newValue = Math.round(value * (320 - 12)); + if (newValue === root.width) return; + root.width = newValue; + root.updated() + } + } + + clip: true + + Rectangle { + anchors.fill: parent + anchors.leftMargin: -border.width + anchors.topMargin: -border.width + anchors.bottomMargin: -border.width + border.width: 2 + border.color:"black" + color: Qt.rgba(0.39, 0.80, 0.96, 0.3) + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml new file mode 100644 index 00000000000..a3937436c79 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/SplashOff.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 + +Rectangle { + id: root + anchors.fill: parent + color: "black" + + Image { + anchors.centerIn: parent + width: root.width*0.8 + height: root.height + fillMode: Image.PreserveAspectFit + source: engine.getSetting("idleBackground") == "mask" ? "./Screens/Images/logo.png" : "../../../images/templates/logo_mixxx.png" + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml new file mode 100644 index 00000000000..6833d6c8740 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/StockScreen.qml @@ -0,0 +1,498 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.3 + +import "../../../qml" as Skin +import Mixxx 1.0 as Mixxx + +import S4MK3 as S4MK3 + +Rectangle { + id: root + + required property string group + required property string screenId + + readonly property bool useSharedApi: engine.getSetting("useSharedDataAPI") + + anchors.fill: parent + color: "black" + + function onSharedDataUpdate(data) { + if (!root) return; + + console.log(`Received data on screen#${root.screenId} while currently bind to ${root.group}: ${JSON.stringify(data)}`); + if (typeof data === "object" && typeof data.group[root.screenId] === "string" && root.group !== data.group[root.screenId]) { + root.group = data.group[root.screenId] + waveformOverview.player = Mixxx.PlayerManager.getPlayer(root.group) + artwork.player = Mixxx.PlayerManager.getPlayer(root.group) + console.log(`Changed group for screen ${root.screenId} to ${root.group}`); + } + var shouldBeCompacted = false; + if (typeof data.padsMode === "object") { + scrollingWaveform.visible = data.padsMode[root.group] === 4 + artworkSpacer.visible = data.padsMode[root.group] === 1 + shouldBeCompacted |= scrollingWaveform.visible || artworkSpacer.visible + } + if (typeof data.keyboardMode === "object") { + shouldBeCompacted |= data.keyboardMode[root.group] + keyboard.visible = !!data.keyboardMode[root.group] + } + deckInfo.state = shouldBeCompacted ? "compacted" : "" + if (typeof data.displayBeatloopSize === "object") { + timeIndicator.mode = data.displayBeatloopSize[root.group] ? S4MK3.TimeAndBeatloopIndicator.Mode.BeetjumpSize : S4MK3.TimeAndBeatloopIndicator.Mode.RemainingTime + timeIndicator.update() + } + } + + Mixxx.ControlProxy { + id: trackLoadedControl + + group: root.group + key: "track_loaded" + + onValueChanged: (value) => { + if (!value && deckInfo) { + deckInfo.state = "" + scrollingWaveform.visible = false + } + } + } + + Timer { + id: channelchange + + interval: 5000 + repeat: true + running: false + + onTriggered: { + root.onSharedDataUpdate({ + group: { + "leftdeck": screenId === "leftdeck" && trackLoadedControl.group === "[Channel1]" ? "[Channel3]" : "[Channel1]", + "rightdeck": screenId === "rightdeck" && trackLoadedControl.group === "[Channel2]" ? "[Channel4]" : "[Channel2]", + }, + scrollingWaveform: { + "[Channel1]": true, + "[Channel2]": true, + "[Channel3]": true, + "[Channel4]": true, + }, + keyboardMode: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + displayBeatloopSize: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + }); + } + } + + Component.onCompleted: { + if (!root.useSharedApi) { + return; + } + + engine.makeSharedDataConnection(root.onSharedDataUpdate) + + root.onSharedDataUpdate({ + group: { + "leftdeck": "[Channel1]", + "rightdeck": "[Channel2]", + }, + scrollingWaveform: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + keyboardMode: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + displayBeatloopSize: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + }); + } + + Rectangle { + anchors.fill: parent + color: "transparent" + + Image { + id: artwork + anchors.fill: parent + + property var player: Mixxx.PlayerManager.getPlayer(root.group) + + source: player.coverArtUrl + height: 100 + width: 100 + fillMode: Image.PreserveAspectFit + + opacity: artworkSpacer.visible ? 1 : 0.2 + z: -1 + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 6 + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 + color: "transparent" + + RowLayout { + anchors.fill: parent + spacing: 1 + + S4MK3.OnAirTrack { + id: onAir + group: root.group + Layout.fillWidth: true + Layout.fillHeight: true + + scrolling: !scrollingWaveform.visible + } + } + } + + // Indicator + Rectangle { + id: deckInfo + + Layout.fillWidth: true + Layout.preferredHeight: 105 + Layout.leftMargin: 6 + Layout.rightMargin: 6 + color: "transparent" + + GridLayout { + id: gridLayout + anchors.fill: parent + columnSpacing: 6 + rowSpacing: 6 + columns: 2 + + // Section: Key + S4MK3.KeyIndicator { + id: keyIndicator + group: root.group + borderColor: smallBoxBorder + + Layout.fillWidth: true + Layout.fillHeight: true + } + + // Section: Bpm + S4MK3.BPMIndicator { + id: bpmIndicator + group: root.group + borderColor: smallBoxBorder + + Layout.fillWidth: true + Layout.fillHeight: true + } + + // Section: Key + S4MK3.TimeAndBeatloopIndicator { + id: timeIndicator + group: root.group + + Layout.fillWidth: true + Layout.preferredHeight: 72 + timeColor: smallBoxBorder + } + + // Section: Bpm + S4MK3.LoopSizeIndicator { + id: loopSizeIndicator + group: root.group + + Layout.fillWidth: true + Layout.preferredHeight: 72 + } + } + states: State { + name: "compacted" + + PropertyChanges { + target:deckInfo + Layout.preferredHeight: 28 + } + PropertyChanges { + target: gridLayout + columns: 4 + } + PropertyChanges { + target: bpmIndicator + state: "compacted" + } + PropertyChanges { + target: timeIndicator + Layout.preferredHeight: -1 + Layout.fillHeight: true + state: "compacted" + } + PropertyChanges { + target: loopSizeIndicator + Layout.preferredHeight: -1 + Layout.fillHeight: true + state: "compacted" + } + } + } + + Item { + id: scrollingWaveform + + Layout.fillWidth: true + Layout.minimumHeight: scrollingWaveform.visible ? 120 : 0 + Layout.leftMargin: 0 + Layout.rightMargin: 0 + + visible: false + + Skin.WaveformRow { + group: root.group + x: 0 + width: 320 + height: 100 + zoomControlRatio: 200 + } + } + + Mixxx.ControlProxy { + id: deckScratching + + group: root.group + key: "scratch2_enable" + + onValueChanged: { + if (root.useSharedApi) { + return; + } + + if (value) { + waveformTimer.running = false; + scrollingWaveform.visible = true; + deckInfo.state = scrollingWaveform.visible ? "compacted" : "" + } else { + waveformTimer.running = true; + waveformTimer.restart() + } + } + } + + Timer { + id: waveformTimer + + interval: 4000 + repeat: false + running: false + + onTriggered: { + scrollingWaveform.visible = false; + deckInfo.state = scrollingWaveform.visible ? "compacted" : "" + } + } + + // Spacer + Item { + id: artworkSpacer + + Layout.fillWidth: true + Layout.minimumHeight: artworkSpacer.visible ? 120 : 0 + Layout.leftMargin: 6 + Layout.rightMargin: 6 + + visible: false + + Rectangle { + color: "transparent" + visible: parent.visible + anchors.top: parent.top + anchors.bottom: parent.bottom + x: 153 + width: 2 + } + } + + // Track progress + Item { + id: waveform + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 6 + Layout.rightMargin: 6 + layer.enabled: true + + S4MK3.Progression { + id: progression + group: root.group + + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + } + + Mixxx.WaveformOverview { + id: waveformOverview + anchors.fill: parent + player: Mixxx.PlayerManager.getPlayer(root.group) + } + + Mixxx.ControlProxy { + id: samplesControl + + group: root.group + key: "track_samples" + } + + // Hotcue + Repeater { + model: 16 + + S4MK3.HotcuePoint { + required property int index + + Mixxx.ControlProxy { + id: samplesControl + + group: root.group + key: "track_samples" + } + + Mixxx.ControlProxy { + id: hotcueEnabled + group: root.group + key: `hotcue_${index + 1}_status` + } + + Mixxx.ControlProxy { + id: hotcuePosition + group: root.group + key: `hotcue_${index + 1}_position` + } + + Mixxx.ControlProxy { + id: hotcueColor + group: root.group + key: `hotcue_${number}_color` + } + + anchors.top: parent.top + // anchors.left: parent.left + anchors.bottom: parent.bottom + visible: hotcueEnabled.value + + number: this.index + 1 + type: S4MK3.HotcuePoint.Type.OneShot + position: hotcuePosition.value / samplesControl.value + color: `#${(hotcueColor.value >> 16).toString(16).padStart(2, '0')}${((hotcueColor.value >> 8) & 255).toString(16).padStart(2, '0')}${(hotcueColor.value & 255).toString(16).padStart(2, '0')}` + } + } + + // Intro + S4MK3.HotcuePoint { + + Mixxx.ControlProxy { + id: introStartEnabled + group: root.group + key: `intro_start_enabled` + } + + Mixxx.ControlProxy { + id: introStartPosition + group: root.group + key: `intro_start_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: introStartEnabled.value + + type: S4MK3.HotcuePoint.Type.IntroIn + position: introStartPosition.value / samplesControl.value + } + + // Extro + S4MK3.HotcuePoint { + + Mixxx.ControlProxy { + id: introEndEnabled + group: root.group + key: `intro_end_enabled` + } + + Mixxx.ControlProxy { + id: introEndPosition + group: root.group + key: `intro_end_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: introEndEnabled.value + + type: S4MK3.HotcuePoint.Type.IntroOut + position: introEndPosition.value / samplesControl.value + } + + // Loop in + S4MK3.HotcuePoint { + Mixxx.ControlProxy { + id: loopStartPosition + group: root.group + key: `loop_start_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: loopStartPosition.value > 0 + + type: S4MK3.HotcuePoint.Type.LoopIn + position: loopStartPosition.value / samplesControl.value + } + + // Loop out + S4MK3.HotcuePoint { + Mixxx.ControlProxy { + id: loopEndPosition + group: root.group + key: `loop_end_position` + } + + anchors.top: parent.top + anchors.bottom: parent.bottom + visible: loopEndPosition.value > 0 + + type: S4MK3.HotcuePoint.Type.LoopOut + position: loopEndPosition.value / samplesControl.value + } + } + + S4MK3.Keyboard { + id: keyboard + group: root.group + visible: false + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 6 + Layout.rightMargin: 6 + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml new file mode 100755 index 00000000000..26302f4073c --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/TimeAndBeatloopIndicator.qml @@ -0,0 +1,107 @@ +/* +This module is used to define the center left section, above the waveform. +Currently this section is dedicated to show the remaining time as well as the beatloop when changing. +*/ +import QtQuick 2.14 +import QtQuick.Controls 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Rectangle { + id: root + + required property string group + + property color timeColor: Qt.rgba(67/255,70/255,66/255, 1) + property color beatjumpColor: 'yellow' + + enum Mode { + RemainingTime, + BeetjumpSize + } + + property int mode: TimeAndBeatloopIndicator.Mode.RemainingTime + + radius: 6 + border.color: timeColor + border.width: 2 + color: timeColor + signal updated + + function update() { + let newValue = ""; + if (root.mode === TimeAndBeatloopIndicator.Mode.RemainingTime) { + var seconds = ((1.0 - progression.value) * duration.value); + var mins = parseInt(seconds / 60).toString(); + seconds = parseInt(seconds % 60).toString(); + + newValue = `-${mins.padStart(2, '0')}:${seconds.padStart(2, '0')}`; + } else { + newValue = (beatjump.value < 1 ? `1/${1 / beatjump.value}` : `${beatjump.value}`); + } + if (newValue === indicator.text) return; + indicator.text = newValue; + root.updated() + } + + Text { + id: indicator + anchors.centerIn: parent + text: "0.00" + + font.pixelSize: 46 + color: fontColor + + Mixxx.ControlProxy { + id: progression + group: root.group + key: "playposition" + } + + Mixxx.ControlProxy { + id: duration + group: root.group + key: "duration" + } + + Mixxx.ControlProxy { + id: beatjump + group: root.group + key: "beatjump_size" + } + + Mixxx.ControlProxy { + id: endoftrack + group: root.group + key: "end_of_track" + onValueChanged: (value) => { + root.border.color = value ? 'red' : timeColor + root.color = value ? 'red' : timeColor + root.updated() + } + } + } + + Component.onCompleted: { + progression.onValueChanged.connect(update) + duration.onValueChanged.connect(update) + beatjump.onValueChanged.connect(update) + update() + } + + states: State { + name: "compacted" + + PropertyChanges { + target: indicator + font.pixelSize: 17 + } + } + + onModeChanged: () => { + border.color = root.mode == TimeAndBeatloopIndicator.Mode.BeetjumpSize ? beatjumpColor : timeColor + color = root.mode == TimeAndBeatloopIndicator.Mode.BeetjumpSize ? beatjumpColor : timeColor + indicator.color = root.mode == TimeAndBeatloopIndicator.Mode.BeetjumpSize ? 'black' : 'white' + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml new file mode 100755 index 00000000000..ade89743521 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/WaveformOverview.qml @@ -0,0 +1,165 @@ +/* +This module is used to waveform overview, at the bottom of the screen. It is reusing component definition of `WaveformOverview.qml` but remove +the link to markers and provide hooks with screen update/redraw, needed for partial updates. +Currently this section is dedicated to BPM and tempo fader information. +*/ +import QtQuick 2.15 +import QtQuick.Window 2.15 + +import Mixxx 1.0 as Mixxx +import Mixxx.Controls 1.0 as MixxxControls + +Item { + id: root + + required property string group + property var deckPlayer: Mixxx.PlayerManager.getPlayer(root.group) + property real scale: 0.2 + + signal updated + + visible: false + antialiasing: true + anchors.fill: parent + + Connections { + onGroupChanged: { + deckPlayer = Mixxx.PlayerManager.getPlayer(root.group) + console.log("Group changed!!") + root.updated() + } + } + + Rectangle { + color: "white" + anchors.top: parent.top + anchors.bottom: parent.bottom + x: 153 + width: 2 + } + 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" + } + + Item { + id: waveformBeat + + property real effectiveZoomFactor: (zoomControl.value * rateRatioControl.value / root.scale) * 6 + + width: waveformContainer.duration * effectiveZoomFactor + height: parent.height + x: 0.5 * waveformContainer.width - playPositionControl.value * width + visible: true + + Shape { + id: preroll + + property real triangleHeight: waveformBeat.height + property real triangleWidth: 0.25 * waveformBeat.effectiveZoomFactor + property int numTriangles: Math.ceil(width / triangleWidth) + + anchors.top: waveformBeat.top + anchors.right: waveformBeat.left + width: Math.max(0, waveformBeat.x) + height: waveformBeat.height + + ShapePath { + strokeColor: 'red' + 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: waveformBeat.height + property real triangleWidth: 0.25 * waveformBeat.effectiveZoomFactor + property int numTriangles: Math.ceil(width / triangleWidth) + + anchors.top: waveformBeat.top + anchors.left: waveformBeat.right + width: waveformContainer.width / 2 + height: waveformBeat.height + + ShapePath { + strokeColor: 'red' + 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; + } + } + } + } + } + + MixxxControls.WaveformOverview { + id: waveformOverview + // property real duration: samplesControl.value / sampleRateControl.onValueChanged + + player: root.player + anchors.fill: parent + channels: Mixxx.WaveformOverview.Channels.BothChannels + renderer: Mixxx.WaveformOverview.Renderer.RGB + colorHigh: 'white' + colorMid: 'blue' + colorLow: 'green' + } + } +} diff --git a/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir new file mode 100644 index 00000000000..6c76347ed73 --- /dev/null +++ b/res/controllers/TraktorKontrolS4MK3Screens/S4MK3/qmldir @@ -0,0 +1,12 @@ +module S4MK3 +BPMIndicator 1.0 BPMIndicator.qml +HotcuePoint 1.0 HotcuePoint.qml +Keyboard 1.0 Keyboard.qml +KeyIndicator 1.0 KeyIndicator.qml +LoopSizeIndicator 1.0 LoopSizeIndicator.qml +OnAirTrack 1.0 OnAirTrack.qml +Progression 1.0 Progression.qml +TimeAndBeatloopIndicator 1.0 TimeAndBeatloopIndicator.qml +WaveformOverview 1.0 WaveformOverview.qml +SplashOff 1.0 SplashOff.qml +StockScreen 1.0 StockScreen.qml diff --git a/res/qml/WaveformRow.qml b/res/qml/WaveformRow.qml index be129fc42bf..82a8eb43d14 100644 --- a/res/qml/WaveformRow.qml +++ b/res/qml/WaveformRow.qml @@ -15,6 +15,8 @@ Item { property string group // required property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) + property int zoomControlRatio: 100 + property alias shader: shader Item { id: waveformContainer @@ -118,7 +120,7 @@ Item { Item { id: waveform - property real effectiveZoomFactor: (1 / rateRatioControl.value) * (100 / zoomControl.value) + property real effectiveZoomFactor: (1 / rateRatioControl.value) * (root.zoomControlRatio / zoomControl.value) width: waveformContainer.duration * effectiveZoomFactor height: parent.height @@ -126,6 +128,7 @@ Item { visible: root.deckPlayer.isLoaded WaveformShader { + id: shader group: root.group anchors.fill: parent } diff --git a/src/controllers/bulk/bulksupported.h b/src/controllers/bulk/bulksupported.h index a7bf6c916d6..f10673e87ba 100644 --- a/src/controllers/bulk/bulksupported.h +++ b/src/controllers/bulk/bulksupported.h @@ -28,4 +28,5 @@ constexpr static bulk_support_lookup bulk_supported[] = { {{0x06f8, 0xb107}, {0x83, 0x03, std::nullopt}}, // Hercules Mk4 {{0x06f8, 0xb100}, {0x86, 0x06, std::nullopt}}, // Hercules Mk2 {{0x06f8, 0xb120}, {0x82, 0x03, std::nullopt}}, // Hercules MP3 LE / Glow + {{0x17cc, 0x1720}, {0x00, 0x03, 0x04}}, // Traktor NI S4 Mk3 }; diff --git a/tools/README b/tools/README index def2a3d4040..3055fbed113 100644 --- a/tools/README +++ b/tools/README @@ -20,3 +20,11 @@ cd build && gcc ../tools/dummy_hid_device.c -lhidapi-hidraw -o dummy_hid_device # Allow the created hidraw device to be accessed by the user. You may also set the write udev rules. Finally, you can also run Mixxx as root, but that's not recommended. sudo chown "$USER" "$(ls -1t /dev/hidraw* | head -n 1)" ``` + +## Traktor S4 Mk3 Screen drawing + +This small program can be used directly to draw arbitrary rectangles on the Traktor S4 Mk3 screens. It may also be useful for one to perform tests on top of the existing reversed engineered protocol. + +```sh +cd build && gcc ../tools/traktor_s4_mk3_screen_test.c `pkg-config --cflags --libs libusb-1.0` -o traktor_s4_mk3_screen_test && ./traktor_s4_mk3_screen_test +``` diff --git a/tools/clang_format.py b/tools/clang_format.py index 16076470c91..baeface76d2 100755 --- a/tools/clang_format.py +++ b/tools/clang_format.py @@ -49,7 +49,7 @@ def run_clang_format_on_lines(rootdir, file_to_format, stylepath=None): ", ".join("{}-{}".format(*x) for x in file_to_format.lines), ) - filename = os.path.join(rootdir, file_to_format.filename) + filename = os.path.join(rootdir, file_to_format.filename).strip() cmd = [ "clang-format", "--style=file", diff --git a/tools/traktor_s4_mk3_screen_test.c b/tools/traktor_s4_mk3_screen_test.c new file mode 100644 index 00000000000..cf14fd3833a --- /dev/null +++ b/tools/traktor_s4_mk3_screen_test.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#define VENDOR_ID 0x17cc +#define PRODUCT_ID 0x1720 +#define IN_EPADDR 0x00 +#define OUT_EPADDR 0x03 + +static const uint8_t header_data[] = { + 0x84, 0x0, 0x0, 0x21, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // Draw offset (y=0, x=0) + 0x1, + 0x40, + 0x0, + 0xf0, // Draw dimenssion (width=320, height=240) +}; +static const uint8_t footer_data[] = { + 0x40, 0x00, 0x00, 0x00}; + +int main(int argc, char** argv) { + libusb_context* context; + int transferred; + + if (argc != 7) { + fprintf(stderr, "Usage: %s \n", *argv); + return -1; + } + + libusb_init(&context); + + libusb_device_handle* handle = libusb_open_device_with_vid_pid( + context, VENDOR_ID, PRODUCT_ID); + + if (!handle) { + fprintf(stderr, "Unable to open USB Bulk device\n"); + return -1; + } + + uint8_t screen_idx = atoi(argv[1]); + + if (screen_idx != 0 && screen_idx != 1) { + fprintf(stderr, "Invalid screen ID %d\n", screen_idx); + return -1; + } + + uint16_t x = atoi(argv[2]); + uint16_t y = atoi(argv[3]); + uint16_t width = atoi(argv[4]); + uint16_t height = atoi(argv[5]); + uint16_t color = strtol(argv[6], NULL, 2); + + uint8_t* data = malloc(width * height * sizeof(uint16_t) + + sizeof(header_data) + sizeof(footer_data)); + uint8_t* header = data; + + memcpy(header, header_data, sizeof(header_data)); + + header[2] = screen_idx; + + header[8] = x >> 8; + header[9] = x & 0xff; + header[10] = y >> 8; + header[11] = y & 0xff; + + header[12] = width >> 8; + header[13] = width & 0xff; + header[14] = height >> 8; + header[15] = height & 0xff; + + printf("draw x=%d,y=%d,width=%d,height=%d with color %x\n", x, y, width, height, color); + + size_t payload_size = width * height * sizeof(uint16_t) + + sizeof(header_data) + sizeof(footer_data); + uint8_t* payload = data + sizeof(header_data); + uint8_t* footer = payload + width * height * sizeof(uint16_t); + + for (int px = 0; px < width * height; px++) { + payload[px * sizeof(uint16_t)] = color >> 8; + payload[px * sizeof(uint16_t) + 1] = color & 0xff; + } + + memcpy(footer, footer_data, sizeof(footer_data)); + + footer[2] = screen_idx; + + clock_t start, end; + double cpu_time_used; + + start = clock(); + int ret = libusb_bulk_transfer(handle, OUT_EPADDR, data, payload_size, &transferred, 0); + end = clock(); + cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC; + + if (ret < 0) { + fprintf(stderr, "Unable to send to USB Bulk device\n"); + + } else { + fprintf(stderr, "Sent %d bytes in %f ms\n", transferred, cpu_time_used); + } + + libusb_close(handle); + libusb_exit(context); + + return 0; +} From 073c43a3284c6c818c0d4430491e814ee1308d48 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Thu, 19 Sep 2024 10:16:29 +0100 Subject: [PATCH 19/22] feat: add Joe Easton's inspired theme for S4Mk3 screens --- .../Traktor Kontrol S4 MK3.bulk.xml | 406 +---- .../Traktor Kontrol S4 MK3.hid.xml | 183 ++ res/controllers/Traktor-Kontrol-S4-MK3.js | 274 ++- .../TraktorKontrolS4MK3Screens.qml | 2 +- .../S4MK3/AdvancedScreen.qml | 63 + .../AdvancedScreen/Browser/BrowserFooter.qml | 340 ++++ .../AdvancedScreen/Browser/BrowserHeader.qml | 223 +++ .../AdvancedScreen/Browser/ListDelegate.qml | 262 +++ .../AdvancedScreen/Browser/ListHighlight.qml | 19 + .../AdvancedScreen/Browser/TrackFooter.qml | 353 ++++ .../AdvancedScreen/Browser/TrackView.qml | 202 +++ .../S4MK3/AdvancedScreen/Browser/Triangle.qml | 29 + .../S4MK3/AdvancedScreen/DeckScreen.qml | 184 ++ .../S4MK3/AdvancedScreen/Defines/Colors.qml | 549 ++++++ .../AdvancedScreen/Defines/Durations.qml | 8 + .../S4MK3/AdvancedScreen/Defines/Font.qml | 17 + .../S4MK3/AdvancedScreen/Defines/Margins.qml | 7 + .../S4MK3/AdvancedScreen/Defines/Settings.qml | 288 +++ .../S4MK3/AdvancedScreen/Defines/Utils.qml | 126 ++ .../AdvancedScreen/Overlays/BankInfo.qml | 193 ++ .../Overlays/BankInfoDetails.qml | 77 + .../S4MK3/AdvancedScreen/Overlays/CueInfo.qml | 152 ++ .../Overlays/CueInfoDetails.qml | 73 + .../AdvancedScreen/Overlays/FXInfoDetails.qml | 66 + .../AdvancedScreen/Overlays/GridControls.qml | 209 +++ .../Overlays/GridInfoDetails.qml | 160 ++ .../AdvancedScreen/Overlays/JumpControls.qml | 239 +++ .../Overlays/JumpInfoDetails.qml | 45 + .../AdvancedScreen/Overlays/LoopControls.qml | 230 +++ .../Overlays/LoopInfoDetails.qml | 57 + .../Overlays/QuickFXSelector.qml | 151 ++ .../AdvancedScreen/Overlays/RollControls.qml | 230 +++ .../AdvancedScreen/Overlays/ToneControls.qml | 247 +++ .../Overlays/ToneInfoDetails.qml | 44 + .../AdvancedScreen/Overlays/TopControls.qml | 348 ++++ .../Overlays/TopInfoDetails.qml | 152 ++ .../S4MK3/AdvancedScreen/ViewModels/Cell.qml | 54 + .../AdvancedScreen/ViewModels/DeckInfo.qml | 1563 +++++++++++++++++ .../AdvancedScreen/ViewModels/HotCue.qml | 42 + .../AdvancedScreen/ViewModels/HotCues.qml | 65 + .../AdvancedScreen/Views/BrowserView.qml | 322 ++++ .../S4MK3/AdvancedScreen/Views/Dimensions.qml | 15 + .../S4MK3/AdvancedScreen/Views/EmptyDeck.qml | 47 + .../S4MK3/AdvancedScreen/Views/StemDeck.qml | 28 + .../S4MK3/AdvancedScreen/Views/TrackDeck.qml | 222 +++ .../Waveform/StemColorIndicators.qml | 81 + .../AdvancedScreen/Waveform/StemWaveforms.qml | 59 + .../Waveform/WaveformContainer.qml | 85 + .../Waveform/WaveformOverview.qml | 226 +++ .../AdvancedScreen/Widgets/BpmDisplay.qml | 27 + .../AdvancedScreen/Widgets/DeckHeader.qml | 90 + .../AdvancedScreen/Widgets/KeyDisplay.qml | 46 + .../S4MK3/AdvancedScreen/Widgets/LoopSize.qml | 87 + .../AdvancedScreen/Widgets/PhaseMeter.qml | 94 + .../AdvancedScreen/Widgets/ProgressBar.qml | 60 + .../S4MK3/AdvancedScreen/Widgets/Slider.qml | 111 ++ .../S4MK3/AdvancedScreen/Widgets/StateBar.qml | 34 + .../AdvancedScreen/Widgets/StemOverlay.qml | 257 +++ .../AdvancedScreen/Widgets/TempoAdjust.qml | 184 ++ .../AdvancedScreen/Widgets/TrackRating.qml | 42 + .../S4MK3/BPMIndicator.qml | 39 +- .../S4MK3/KeyIndicator.qml | 19 +- .../S4MK3/Keyboard.qml | 13 +- .../S4MK3/LoopSizeIndicator.qml | 54 +- .../S4MK3/Progression.qml | 18 +- .../S4MK3/SplashOff.qml | 2 +- .../S4MK3/StockScreen.qml | 2 +- .../S4MK3/TimeAndBeatloopIndicator.qml | 32 +- .../S4MK3/WaveformOverview.qml | 3 - .../TraktorKontrolS4MK3Screens/S4MK3/qmldir | 1 + 70 files changed, 9747 insertions(+), 485 deletions(-) create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/BrowserFooter.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/BrowserHeader.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/ListDelegate.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/ListHighlight.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/TrackFooter.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/TrackView.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Browser/Triangle.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/DeckScreen.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Colors.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Durations.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Font.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Margins.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Settings.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Defines/Utils.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/BankInfo.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/BankInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/CueInfo.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/CueInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/FXInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/GridControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/GridInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/JumpControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/JumpInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/LoopControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/LoopInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/QuickFXSelector.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/RollControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/ToneControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/ToneInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/TopControls.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Overlays/TopInfoDetails.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/Cell.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/DeckInfo.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/HotCue.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/ViewModels/HotCues.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/BrowserView.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/Dimensions.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/EmptyDeck.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/StemDeck.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Views/TrackDeck.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/StemColorIndicators.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/StemWaveforms.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/WaveformContainer.qml create mode 100644 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Waveform/WaveformOverview.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/BpmDisplay.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/DeckHeader.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/KeyDisplay.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/LoopSize.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/PhaseMeter.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/ProgressBar.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/Slider.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/StateBar.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/StemOverlay.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/TempoAdjust.qml create mode 100755 res/controllers/TraktorKontrolS4MK3Screens/S4MK3/AdvancedScreen/Widgets/TrackRating.qml diff --git a/res/controllers/Traktor Kontrol S4 MK3.bulk.xml b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml index c7bc44712ea..6fc6d3d952a 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.bulk.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.bulk.xml @@ -39,18 +39,28 @@ advanced - - - + + + + + + @@ -63,167 +73,13 @@ cd - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + label="alwaysShowTempoInfo"/> + label="amount of time the bpm overlay will stay on the screen"/> - + + label="enableBpmTextColor"/> + label="enableMasterBpmTextColor"/> + label="enableTempoTextColor"/> @@ -359,20 +211,17 @@ variable="enableBpmOffsetTextColor" type="boolean" default="false" - label="enableBpmOffsetTextColor"> - + label="enableBpmOffsetTextColor"/> + label="enableTempoOffsetTextColor"/> + label="enableMasterDeckTextColor"/> @@ -380,14 +229,12 @@ variable="hideWaveforms" type="boolean" default="false" - label="hideWaveforms"> - + label="hideWaveforms"/> + label="Keep the loop size indicator visible"/> + label="amount of time the loop overlay will stay on the screen"/> - + label="hideBeatgrid"/> + label="enableBrowserMode"/> + label="adjacentKeys"/> + label="enable camelot key"/> @@ -454,20 +287,17 @@ variable="disablePreviewPlayerToggle" type="boolean" default="false" - label="disablePreviewPlayerToggle"> - + label="disablePreviewPlayerToggle"/> + label="showBrowserOnFavourites"/> + label="swapViewButtons"/> @@ -475,14 +305,12 @@ variable="showBrowserOnFullScreen" type="boolean" default="true" - label="showBrowserOnFullScreen"> - + label="showBrowserOnFullScreen"/> + label="disableSortButtonOutput"/> @@ -533,8 +361,7 @@ variable="browserEncoderShiftScroll" type="boolean" default="false" - label="browserEncoderShiftScroll"> - + label="browserEncoderShiftScroll"/> + label="browserShift"/> @@ -556,8 +382,7 @@ variable="swapArtistTitleColumns" type="boolean" default="false" - label="swapArtistTitleColumns"> - + label="swapArtistTitleColumns"/> @@ -565,32 +390,27 @@ variable="hideBPM" type="boolean" default="false" - label="hideBPM"> - + label="hideBPM"/> + label="hideKey"/> + label="hideAlbumArt"/> + label="showArtistColumn"/> + label="showTrackTitleColumn"/> @@ -598,8 +418,7 @@ variable="browserFontSize" type="integer" default="15" - label="browserFontSize"> - + label="browserFontSize"/> @@ -607,8 +426,7 @@ variable="raiseBrowserFooter" type="boolean" default="false" - label="raiseBrowserFooter"> - + label="raiseBrowserFooter"/> @@ -616,8 +434,7 @@ variable="bpmBrowserTextColor" type="boolean" default="true" - label="determine the bpm text colour in the browser"> - + label="determine the bpm text colour in the browser"/> @@ -625,8 +442,7 @@ variable="hideStripe" type="boolean" default="false" - label="hideStripe"> - + label="hideStripe"/> @@ -634,8 +450,7 @@ variable="shiftStripe" type="boolean" default="false" - label="shiftStripe"> - + label="shiftStripe"/> @@ -643,8 +458,7 @@ variable="shiftStripeMaster" type="boolean" default="false" - label="shiftStripeMaster"> - + label="shiftStripeMaster"/> @@ -652,8 +466,7 @@ variable="timeBox" type="integer" default="0" - label="timeBox"> - + label="timeBox"/> @@ -672,8 +485,7 @@ variable="timeTextColourChange" type="boolean" default="false" - label="timeTextColourChange"> - + label="timeTextColourChange"/> @@ -725,8 +537,7 @@ variable="hidePhase" type="boolean" default="false" - label="hidePhase"> - + label="hidePhase"/> @@ -734,17 +545,7 @@ variable="hidePhrase" type="boolean" default="true" - label="hidePhrase"> - - - - - + label="hidePhrase"/> @@ -752,8 +553,7 @@ variable="showBPMGridAdjust" type="boolean" default="true" - label="showBPMGridAdjust"> - + label="showBPMGridAdjust"/> @@ -764,8 +564,7 @@ min="100" max="15000" step="1000" - label="Rate and waveform adjust timer"> - + label="Rate and waveform adjust timer"/> @@ -773,17 +572,7 @@ variable="hideHotcueOverlay" type="boolean" default="false" - label="hideHotcueOverlay"> - - - - - + label="hideHotcueOverlay"/> @@ -797,50 +586,6 @@ - - - - - - - - - - - - - - - - + label="fxOverlays"/> @@ -880,8 +624,7 @@ variable="hideEffectsOverlay1" type="boolean" default="false" - label="hideEffectsOverlay1"> - + label="hideEffectsOverlay1"/> @@ -889,8 +632,7 @@ variable="hideEffectsOverlay2" type="boolean" default="false" - label="hideEffectsOverlay2"> - + label="hideEffectsOverlay2"/> @@ -898,8 +640,7 @@ variable="hideToneOverlay" type="boolean" default="false" - label="hideToneOverlay"> - + label="hideToneOverlay"/> @@ -907,8 +648,7 @@ variable="hideLoopOverlay" type="boolean" default="false" - label="hideLoopOverlay"> - + label="hideLoopOverlay"/> @@ -916,17 +656,15 @@ variable="hideRollOverlay" type="boolean" default="false" - label="hideRollOverlay"> - + label="hideRollOverlay"/> + label="disable the tone pads overlay appearing"/> diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index 4efc26d7041..7d6883a2e23 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -395,6 +395,189 @@ + + + + + + + + + + + + + + +