From 5f342650ae86f52a916c262147069a0818ee8793 Mon Sep 17 00:00:00 2001 From: m0dB <79429057+m0dB@users.noreply.github.com> Date: Fri, 21 Oct 2022 20:15:10 +0200 Subject: [PATCH] add qopengl renderers, qopengl rgbwaveformwidget and qopengl wvumetergl and qopengl wspinny destroy textures in their context (avoids warning) render spinnies with qopengl --- CMakeLists.txt | 19 +- .../renderers/qopengl/fixedpointcalc.h | 62 ++ .../renderers/qopengl/iwaveformrenderer.h | 18 + .../qopengl/waveformrenderbackground.cpp | 73 ++ .../qopengl/waveformrenderbackground.h | 31 + .../renderers/qopengl/waveformrenderbeat.cpp | 149 ++++ .../renderers/qopengl/waveformrenderbeat.h | 31 + .../renderers/qopengl/waveformrenderer.cpp | 11 + .../renderers/qopengl/waveformrenderer.h | 25 + .../qopengl/waveformrendererendoftrack.cpp | 134 ++++ .../qopengl/waveformrendererendoftrack.h | 44 ++ .../qopengl/waveformrendererpreroll.cpp | 173 +++++ .../qopengl/waveformrendererpreroll.h | 31 + .../renderers/qopengl/waveformrendererrgb.cpp | 297 ++++++++ .../renderers/qopengl/waveformrendererrgb.h | 34 + .../qopengl/waveformrenderersignalbase.cpp | 10 + .../qopengl/waveformrenderersignalbase.h | 26 + .../renderers/qopengl/waveformrendermark.cpp | 696 ++++++++++++++++++ .../renderers/qopengl/waveformrendermark.h | 66 ++ .../qopengl/waveformrendermarkrange.cpp | 147 ++++ .../qopengl/waveformrendermarkrange.h | 35 + src/waveform/renderers/waveformmark.cpp | 9 +- src/waveform/renderers/waveformmark.h | 12 + src/waveform/renderers/waveformmarkrange.h | 9 + .../renderers/waveformrendererabstract.h | 12 + src/waveform/waveformwidgetfactory.cpp | 29 + .../widgets/qopengl/iwaveformwidget.h | 10 + .../widgets/qopengl/rgbwaveformwidget.cpp | 36 + .../widgets/qopengl/rgbwaveformwidget.h | 43 ++ .../widgets/qopengl/waveformwidget.cpp | 50 ++ src/waveform/widgets/qopengl/waveformwidget.h | 39 + src/waveform/widgets/waveformwidgetabstract.h | 14 + src/waveform/widgets/waveformwidgettype.h | 37 +- src/widget/paintable.cpp | 4 + src/widget/paintable.h | 1 + src/widget/qopengl/wvumetergl.cpp | 388 ++++++++++ src/widget/qopengl/wvumetergl.h | 88 +++ src/widget/wspinny.cpp | 3 +- src/widget/wspinny.h | 6 + src/widget/wvumetergl.cpp | 9 +- src/widget/wvumetergl.h | 6 + 41 files changed, 2888 insertions(+), 29 deletions(-) create mode 100644 src/waveform/renderers/qopengl/fixedpointcalc.h create mode 100644 src/waveform/renderers/qopengl/iwaveformrenderer.h create mode 100644 src/waveform/renderers/qopengl/waveformrenderbackground.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrenderbackground.h create mode 100644 src/waveform/renderers/qopengl/waveformrenderbeat.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrenderbeat.h create mode 100644 src/waveform/renderers/qopengl/waveformrenderer.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrenderer.h create mode 100644 src/waveform/renderers/qopengl/waveformrendererendoftrack.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrendererendoftrack.h create mode 100644 src/waveform/renderers/qopengl/waveformrendererpreroll.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrendererpreroll.h create mode 100644 src/waveform/renderers/qopengl/waveformrendererrgb.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrendererrgb.h create mode 100644 src/waveform/renderers/qopengl/waveformrenderersignalbase.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrenderersignalbase.h create mode 100644 src/waveform/renderers/qopengl/waveformrendermark.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrendermark.h create mode 100644 src/waveform/renderers/qopengl/waveformrendermarkrange.cpp create mode 100644 src/waveform/renderers/qopengl/waveformrendermarkrange.h create mode 100644 src/waveform/widgets/qopengl/iwaveformwidget.h create mode 100644 src/waveform/widgets/qopengl/rgbwaveformwidget.cpp create mode 100644 src/waveform/widgets/qopengl/rgbwaveformwidget.h create mode 100644 src/waveform/widgets/qopengl/waveformwidget.cpp create mode 100644 src/waveform/widgets/qopengl/waveformwidget.h create mode 100644 src/widget/qopengl/wvumetergl.cpp create mode 100644 src/widget/qopengl/wvumetergl.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1559bef91cbb..b85baecf6c39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1146,19 +1146,34 @@ else() src/widget/woverviewhsv.cpp src/widget/woverviewlmh.cpp src/widget/woverviewrgb.cpp - src/widget/wspinny.cpp - src/widget/wvumetergl.cpp src/widget/wwaveformviewer.cpp ) if(QOPENGL) target_sources(mixxx-lib PRIVATE src/widget/openglwindow.cpp src/widget/wglwidgetqopengl.cpp + src/widget/qopengl/wvumetergl.cpp + src/widget/qopengl/wspinny.cpp + src/waveform/renderers/qopengl/waveformrenderbackground.cpp + src/waveform/renderers/qopengl/waveformrenderbeat.cpp + src/waveform/renderers/qopengl/waveformrenderer.cpp + src/waveform/renderers/qopengl/waveformrendererendoftrack.cpp + src/waveform/renderers/qopengl/waveformrendererpreroll.cpp + src/waveform/renderers/qopengl/waveformrendererrgb.cpp + src/waveform/renderers/qopengl/waveformrenderersignalbase.cpp + src/waveform/renderers/qopengl/waveformrendermark.cpp + src/waveform/renderers/qopengl/waveformrendermarkrange.cpp + src/waveform/widgets/qopengl/waveformwidget.cpp + src/waveform/widgets/qopengl/rgbwaveformwidget.cpp + src/waveform/widgets/qopengl/waveformwidget.cpp + src/waveform/widgets/qopengl/rgbwaveformwidget.cpp ) else() target_sources(mixxx-lib PRIVATE src/waveform/sharedglcontext.cpp src/widget/wglwidgetqglwidget.cpp + src/widget/wvumetergl.cpp + src/widget/wspinny.cpp ) endif() endif() diff --git a/src/waveform/renderers/qopengl/fixedpointcalc.h b/src/waveform/renderers/qopengl/fixedpointcalc.h new file mode 100644 index 000000000000..25e36e60ba04 --- /dev/null +++ b/src/waveform/renderers/qopengl/fixedpointcalc.h @@ -0,0 +1,62 @@ +/// functions to do fixed point calculations, used by qopengl::WaveformRendererRGB + +// float to fixed point with 8 fractional bits, clipped at 4.0 +inline uint32_t toFrac8(float x) { + return std::min(static_cast(std::max(x, 0.f) * 256.f), 4 * 256); +} + +// scaled sqrt lookable table to convert maxAll and maxAllNext as calculated +// in updatePaintNode back to y coordinates +class Frac16SqrtTableSingleton { + public: + static constexpr size_t frac16sqrtTableSize{(3 * 4 * 255 * 256) / 16 + 1}; + + static Frac16SqrtTableSingleton& getInstance() { + static Frac16SqrtTableSingleton instance; + return instance; + } + + inline float get(uint32_t x) const { + // The maximum value of fact16x can be (as uint32_t) 3 * 4 * 255 * 256, + // which would be exessive for the table size. We divide by 16 in order + // to get a more reasonable size. + return m_table[x >> 4]; + } + + private: + float* m_table; + Frac16SqrtTableSingleton() + : m_table(new float[frac16sqrtTableSize]) { + // In the original implementation, the result of sqrt(maxAll) is divided + // by sqrt(3 * 255 * 255); + // We get rid of that division and bake it into this table. + // Additionally, we divide the index for the lookup by 16 (see get(...)), + // so we need to invert that here. + const float f = (3.f * 255.f * 255.f / 16.f); + for (uint32_t i = 0; i < frac16sqrtTableSize; i++) { + m_table[i] = std::sqrt(static_cast(i) / f); + } + } + ~Frac16SqrtTableSingleton() { + delete[] m_table; + } + Frac16SqrtTableSingleton(const Frac16SqrtTableSingleton&) = delete; + Frac16SqrtTableSingleton& operator=(const Frac16SqrtTableSingleton&) = delete; +}; + +inline float frac16_sqrt(uint32_t x) { + return Frac16SqrtTableSingleton::getInstance().get(x); +} + +inline uint32_t frac8Pow2ToFrac16(uint32_t x) { + // x is the result of multiplying two fixedpoint values with 8 fraction bits, + // thus x has 16 fraction bits, which is also what we want to return for this + // function. We would naively return (x * x) >> 16, but x * x would overflow + // the 32 bits for values > 1, so we shift before multiplying. + x >>= 8; + return (x * x); +} + +inline uint32_t math_max_u32(uint32_t a, uint32_t b, uint32_t c) { + return std::max(a, std::max(b, c)); +} diff --git a/src/waveform/renderers/qopengl/iwaveformrenderer.h b/src/waveform/renderers/qopengl/iwaveformrenderer.h new file mode 100644 index 000000000000..5fd380a18ab1 --- /dev/null +++ b/src/waveform/renderers/qopengl/iwaveformrenderer.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +/// Interface for QOpenGL-based waveform renderers + +namespace qopengl { +class IWaveformRenderer; +} + +class qopengl::IWaveformRenderer : public QOpenGLFunctions { + public: + virtual void initializeGL() { + } + virtual void resizeGL(int w, int h) { + } + virtual void renderGL() = 0; +}; diff --git a/src/waveform/renderers/qopengl/waveformrenderbackground.cpp b/src/waveform/renderers/qopengl/waveformrenderbackground.cpp new file mode 100644 index 000000000000..e965e6e4dd01 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderbackground.cpp @@ -0,0 +1,73 @@ +#include "waveform/renderers/qopengl/waveformrenderbackground.h" + +#include "waveform/renderers/waveformwidgetrenderer.h" +#include "widget/wimagestore.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +using namespace qopengl; + +WaveformRenderBackground::WaveformRenderBackground( + WaveformWidgetRenderer* waveformWidgetRenderer) + : WaveformRenderer(waveformWidgetRenderer), + m_backgroundColor(0, 0, 0) { +} + +WaveformRenderBackground::~WaveformRenderBackground() { +} + +void WaveformRenderBackground::setup(const QDomNode& node, + const SkinContext& context) { + m_backgroundColor = m_waveformRenderer->getWaveformSignalColors()->getBgColor(); + QString backgroundPixmapPath = context.selectString(node, "BgPixmap"); + if (!backgroundPixmapPath.isEmpty()) { + m_backgroundPixmapPath = context.makeSkinPath(backgroundPixmapPath); + } + setDirty(true); +} + +void WaveformRenderBackground::renderGL() { + if (isDirty()) { + // TODO @m0dB + // generateImage(); + } + + // If there is no background image, just fill the painter with the + // background color. + if (m_backgroundImage.isNull()) { + glClearColor(m_backgroundColor.redF(), + m_backgroundColor.greenF(), + m_backgroundColor.blueF(), + 1.f); + glClear(GL_COLOR_BUFFER_BIT); + return; + } + + //painter->drawImage(QPoint(0, 0), m_backgroundImage); +} + +void WaveformRenderBackground::generateImage() { + m_backgroundImage = QImage(); + if (!m_backgroundPixmapPath.isEmpty()) { + QImage backgroundImage = *WImageStore::getImage( + m_backgroundPixmapPath, + scaleFactor()); + + if (!backgroundImage.isNull()) { + if (backgroundImage.width() == m_waveformRenderer->getWidth() && + backgroundImage.height() == m_waveformRenderer->getHeight()) { + m_backgroundImage = backgroundImage.convertToFormat(QImage::Format_RGB32); + } else { + m_backgroundImage = QImage(m_waveformRenderer->getWidth(), + m_waveformRenderer->getHeight(), + QImage::Format_RGB32); + QPainter painter(&m_backgroundImage); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + painter.drawImage(m_backgroundImage.rect(), + backgroundImage, + backgroundImage.rect()); + } + } + } + setDirty(false); +} diff --git a/src/waveform/renderers/qopengl/waveformrenderbackground.h b/src/waveform/renderers/qopengl/waveformrenderbackground.h new file mode 100644 index 000000000000..d6633f2018ba --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderbackground.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "util/class.h" +#include "waveform/renderers/qopengl/waveformrenderer.h" + +class QDomNode; +class SkinContext; + +namespace qopengl { +class WaveformRenderBackground; +} +class qopengl::WaveformRenderBackground : public qopengl::WaveformRenderer { + public: + explicit WaveformRenderBackground(WaveformWidgetRenderer* waveformWidgetRenderer); + ~WaveformRenderBackground() override; + + void setup(const QDomNode& node, const SkinContext& context) override; + void renderGL() override; + + private: + void generateImage(); + + QString m_backgroundPixmapPath; + QColor m_backgroundColor; + QImage m_backgroundImage; + + DISALLOW_COPY_AND_ASSIGN(WaveformRenderBackground); +}; diff --git a/src/waveform/renderers/qopengl/waveformrenderbeat.cpp b/src/waveform/renderers/qopengl/waveformrenderbeat.cpp new file mode 100644 index 000000000000..4fec0402edae --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderbeat.cpp @@ -0,0 +1,149 @@ +#include "waveform/renderers/qopengl/waveformrenderbeat.h" + +#include + +#include "control/controlobject.h" +#include "skin/legacy/skincontext.h" +#include "track/track.h" +#include "waveform/widgets/qopengl/waveformwidget.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +using namespace qopengl; + +WaveformRenderBeat::WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget) + : WaveformRenderer(waveformWidget) { + m_beatLineVertices.resize(1024); +} + +WaveformRenderBeat::~WaveformRenderBeat() { +} + +void WaveformRenderBeat::initializeGL() { + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +void main()\n\ +{\n\ + gl_Position = matrix * position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +uniform vec4 color;\n\ +void main()\n\ +{\n\ + gl_FragColor = color;\n\ +}\n"; + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_shaderProgram.link()) { + return; + } + + if (!m_shaderProgram.bind()) { + return; + } +} + +void WaveformRenderBeat::setup(const QDomNode& node, const SkinContext& context) { + m_beatColor.setNamedColor(context.selectString(node, "BeatColor")); + m_beatColor = WSkinColor::getCorrectColor(m_beatColor).toRgb(); +} + +void WaveformRenderBeat::renderGL() { + TrackPointer trackInfo = m_waveformRenderer->getTrackInfo(); + + if (!trackInfo) { + return; + } + + mixxx::BeatsPointer trackBeats = trackInfo->getBeats(); + if (!trackBeats) { + return; + } + + int alpha = m_waveformRenderer->getBeatGridAlpha(); + if (alpha == 0) { + return; + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + m_beatColor.setAlphaF(alpha / 100.0); + + const int trackSamples = m_waveformRenderer->getTrackSamples(); + if (trackSamples <= 0) { + return; + } + + const double firstDisplayedPosition = + m_waveformRenderer->getFirstDisplayedPosition(); + const double lastDisplayedPosition = + m_waveformRenderer->getLastDisplayedPosition(); + + const auto startPosition = mixxx::audio::FramePos::fromEngineSamplePos( + firstDisplayedPosition * trackSamples); + const auto endPosition = mixxx::audio::FramePos::fromEngineSamplePos( + lastDisplayedPosition * trackSamples); + auto it = trackBeats->iteratorFrom(startPosition); + + // TODO @m0dB use rendererWidth for vertical orientation + // and apply a 90 degrees rotation to the matrix + //const float rendererWidth = m_waveformRenderer->getWidth(); + const float rendererHeight = m_waveformRenderer->getHeight(); + + int vertexCount = 0; + + for (; it != trackBeats->cend() && *it <= endPosition; ++it) { + double beatPosition = it->toEngineSamplePos(); + double xBeatPoint = + m_waveformRenderer->transformSamplePositionInRendererWorld(beatPosition); + + xBeatPoint = qRound(xBeatPoint); + + // If we don't have enough space, double the size. + if (vertexCount >= m_beatLineVertices.size()) { + m_beatLineVertices.resize(m_beatLineVertices.size() * 2); + } + + m_beatLineVertices[vertexCount++] = xBeatPoint; + m_beatLineVertices[vertexCount++] = 0.f; + m_beatLineVertices[vertexCount++] = xBeatPoint + 1; + m_beatLineVertices[vertexCount++] = 0.f; + m_beatLineVertices[vertexCount++] = xBeatPoint; + m_beatLineVertices[vertexCount++] = rendererHeight; + m_beatLineVertices[vertexCount++] = xBeatPoint; + m_beatLineVertices[vertexCount++] = rendererHeight; + m_beatLineVertices[vertexCount++] = xBeatPoint + 1; + m_beatLineVertices[vertexCount++] = rendererHeight; + m_beatLineVertices[vertexCount++] = xBeatPoint + 1; + m_beatLineVertices[vertexCount++] = 0.f; + } + m_shaderProgram.bind(); + + int vertexLocation = m_shaderProgram.attributeLocation("position"); + int matrixLocation = m_shaderProgram.uniformLocation("matrix"); + int colorLocation = m_shaderProgram.uniformLocation("color"); + + QMatrix4x4 matrix; + matrix.ortho(QRectF(0, 0, m_waveformRenderer->getWidth(), m_waveformRenderer->getHeight())); + + m_shaderProgram.enableAttributeArray(vertexLocation); + m_shaderProgram.setAttributeArray( + vertexLocation, GL_FLOAT, m_beatLineVertices.constData(), 2); + + m_shaderProgram.setUniformValue(matrixLocation, matrix); + m_shaderProgram.setUniformValue(colorLocation, m_beatColor); + + glDrawArrays(GL_TRIANGLES, 0, vertexCount / 2); +} diff --git a/src/waveform/renderers/qopengl/waveformrenderbeat.h b/src/waveform/renderers/qopengl/waveformrenderbeat.h new file mode 100644 index 000000000000..f84ab9832fdc --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderbeat.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "util/class.h" +#include "waveform/renderers/qopengl/waveformrenderer.h" + +class QDomNode; +class SkinContext; + +namespace qopengl { +class WaveformRenderBeat; +} + +class qopengl::WaveformRenderBeat : public qopengl::WaveformRenderer { + public: + explicit WaveformRenderBeat(WaveformWidgetRenderer* waveformWidget); + ~WaveformRenderBeat() override; + + void setup(const QDomNode& node, const SkinContext& context) override; + void renderGL() override; + void initializeGL() override; + + private: + QColor m_beatColor; + QVector m_beatLineVertices; + QOpenGLShaderProgram m_shaderProgram; + + DISALLOW_COPY_AND_ASSIGN(WaveformRenderBeat); +}; diff --git a/src/waveform/renderers/qopengl/waveformrenderer.cpp b/src/waveform/renderers/qopengl/waveformrenderer.cpp new file mode 100644 index 000000000000..31cfd5d925bd --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderer.cpp @@ -0,0 +1,11 @@ +#include "waveform/renderers/qopengl/waveformrenderer.h" + +#include "waveform/widgets/qopengl/waveformwidget.h" + +using namespace qopengl; + +WaveformRenderer::WaveformRenderer(WaveformWidgetRenderer* widget) + : ::WaveformRendererAbstract(widget) { +} + +WaveformRenderer::~WaveformRenderer() = default; diff --git a/src/waveform/renderers/qopengl/waveformrenderer.h b/src/waveform/renderers/qopengl/waveformrenderer.h new file mode 100644 index 000000000000..71b208e8cdb3 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderer.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "waveform/renderers/qopengl/iwaveformrenderer.h" +#include "waveform/renderers/waveformrendererabstract.h" + +class WaveformWidgetRenderer; + +namespace qopengl { +class WaveformRenderer; +} // namespace qopengl + +class qopengl::WaveformRenderer : public WaveformRendererAbstract, public IWaveformRenderer { + public: + explicit WaveformRenderer(WaveformWidgetRenderer* widget); + ~WaveformRenderer(); + + void draw(QPainter* painter, QPaintEvent* event) override { + } + + IWaveformRenderer* qopenglWaveformRenderer() override { + return this; + } +}; diff --git a/src/waveform/renderers/qopengl/waveformrendererendoftrack.cpp b/src/waveform/renderers/qopengl/waveformrendererendoftrack.cpp new file mode 100644 index 000000000000..2caf59827ec5 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendererendoftrack.cpp @@ -0,0 +1,134 @@ +#include "waveform/renderers/qopengl/waveformrendererendoftrack.h" + +#include +#include +#include + +#include "control/controlobject.h" +#include "control/controlproxy.h" +#include "util/timer.h" +#include "waveform/waveformwidgetfactory.h" +#include "waveform/widgets/qopengl/waveformwidget.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +namespace { + +constexpr int kBlinkingPeriodMillis = 1000; + +} // anonymous namespace + +using namespace qopengl; + +WaveformRendererEndOfTrack::WaveformRendererEndOfTrack( + WaveformWidgetRenderer* waveformWidget) + : WaveformRenderer(waveformWidget), + m_pEndOfTrackControl(nullptr), + m_pTimeRemainingControl(nullptr) { +} + +WaveformRendererEndOfTrack::~WaveformRendererEndOfTrack() { + delete m_pEndOfTrackControl; + delete m_pTimeRemainingControl; +} + +bool WaveformRendererEndOfTrack::init() { + m_timer.restart(); + + m_pEndOfTrackControl = new ControlProxy( + m_waveformRenderer->getGroup(), "end_of_track"); + m_pTimeRemainingControl = new ControlProxy( + m_waveformRenderer->getGroup(), "time_remaining"); + return true; +} + +void WaveformRendererEndOfTrack::setup(const QDomNode& node, const SkinContext& context) { + m_color = QColor(200, 25, 20); + const QString endOfTrackColorName = context.selectString(node, "EndOfTrackColor"); + if (!endOfTrackColorName.isNull()) { + m_color.setNamedColor(endOfTrackColorName); + m_color = WSkinColor::getCorrectColor(m_color); + } + //m_pen = QPen(QBrush(m_color), 2.5 * scaleFactor()); +} + +void WaveformRendererEndOfTrack::initializeGL() { + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +varying vec2 vposition;\n\ +void main()\n\ +{\n\ + vposition = position.xy;\n\ + gl_Position = position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +uniform vec4 color;\n\ +varying vec2 vposition;\n\ +void main()\n\ +{\n\ + gl_FragColor = vec4(color.x, color.y, color.z, color.w * (0.5 + 0.33 * max(0.,vposition.x)));\n\ +}\n"; + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_shaderProgram.addShaderFromSourceCode( + QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_shaderProgram.link()) { + return; + } + + if (!m_shaderProgram.bind()) { + return; + } +} + +void WaveformRendererEndOfTrack::fillWithGradient(QColor color) { + const float posarray[] = {-1.f, -1.f, 1.f, -1.f, -1.f, 1.f, 1.f, 1.f}; + + m_shaderProgram.bind(); + + int colorLocation = m_shaderProgram.uniformLocation("color"); + int positionLocation = m_shaderProgram.attributeLocation("position"); + + m_shaderProgram.setUniformValue(colorLocation, color); + + m_shaderProgram.enableAttributeArray(positionLocation); + m_shaderProgram.setAttributeArray( + positionLocation, GL_FLOAT, posarray, 2); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +void WaveformRendererEndOfTrack::renderGL() { + if (!m_pEndOfTrackControl->toBool()) { + return; + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + const int elapsed = m_timer.elapsed().toIntegerMillis() % kBlinkingPeriodMillis; + + const double blinkIntensity = (double)(2 * abs(elapsed - kBlinkingPeriodMillis / 2)) / + kBlinkingPeriodMillis; + + const double remainingTime = m_pTimeRemainingControl->get(); + const double remainingTimeTriggerSeconds = + WaveformWidgetFactory::instance()->getEndOfTrackWarningTime(); + const double criticalIntensity = (remainingTimeTriggerSeconds - remainingTime) / + remainingTimeTriggerSeconds; + + QColor color = m_color; + color.setAlphaF(criticalIntensity * blinkIntensity); + + fillWithGradient(color); +} diff --git a/src/waveform/renderers/qopengl/waveformrendererendoftrack.h b/src/waveform/renderers/qopengl/waveformrendererendoftrack.h new file mode 100644 index 000000000000..942e675a334c --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendererendoftrack.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "util/class.h" +#include "util/performancetimer.h" +#include "waveform/renderers/qopengl/waveformrenderer.h" + +class ControlObject; +class ControlProxy; +class QDomNode; +class SkinContext; + +namespace qopengl { +class WaveformRendererEndOfTrack; +} + +class qopengl::WaveformRendererEndOfTrack : public qopengl::WaveformRenderer { + public: + explicit WaveformRendererEndOfTrack( + WaveformWidgetRenderer* waveformWidget); + ~WaveformRendererEndOfTrack() override; + + void setup(const QDomNode& node, const SkinContext& context) override; + + bool init() override; + + void initializeGL() override; + void renderGL() override; + + private: + void fillWithGradient(QColor color); + + ControlProxy* m_pEndOfTrackControl; + ControlProxy* m_pTimeRemainingControl; + QOpenGLShaderProgram m_shaderProgram; + + QColor m_color; + PerformanceTimer m_timer; + + DISALLOW_COPY_AND_ASSIGN(WaveformRendererEndOfTrack); +}; diff --git a/src/waveform/renderers/qopengl/waveformrendererpreroll.cpp b/src/waveform/renderers/qopengl/waveformrendererpreroll.cpp new file mode 100644 index 000000000000..d390696bb9a3 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendererpreroll.cpp @@ -0,0 +1,173 @@ +#include "waveform/renderers/qopengl/waveformrendererpreroll.h" + +#include + +#include "skin/legacy/skincontext.h" +#include "track/track.h" +#include "waveform/renderers/waveformwidgetrenderer.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +using namespace qopengl; + +WaveformRendererPreroll::WaveformRendererPreroll(WaveformWidgetRenderer* waveformWidget) + : WaveformRenderer(waveformWidget) { + m_vertices.resize(1024); +} + +WaveformRendererPreroll::~WaveformRendererPreroll() { +} + +void WaveformRendererPreroll::setup( + const QDomNode& node, const SkinContext& context) { + m_color.setNamedColor(context.selectString(node, "SignalColor")); + m_color = WSkinColor::getCorrectColor(m_color); +} + +void WaveformRendererPreroll::initializeGL() { + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +void main()\n\ +{\n\ + gl_Position = matrix * position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +uniform vec4 color;\n\ +void main()\n\ +{\n\ + gl_FragColor = color;\n\ +}\n"; + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_shaderProgram.link()) { + return; + } + + if (!m_shaderProgram.bind()) { + return; + } +} + +void WaveformRendererPreroll::renderGL() { + const TrackPointer track = m_waveformRenderer->getTrackInfo(); + if (!track) { + return; + } + + double firstDisplayedPosition = m_waveformRenderer->getFirstDisplayedPosition(); + double lastDisplayedPosition = m_waveformRenderer->getLastDisplayedPosition(); + + // Check if the pre- or post-roll is on screen. If so, draw little triangles + // to indicate the respective zones. + bool preRollVisible = firstDisplayedPosition < 0; + bool postRollVisible = lastDisplayedPosition > 1; + int vertexCount = 0; + if (preRollVisible || postRollVisible) { + const double playMarkerPositionFrac = m_waveformRenderer->getPlayMarkerPosition(); + const double vSamplesPerPixel = m_waveformRenderer->getVisualSamplePerPixel(); + const double numberOfVSamples = m_waveformRenderer->getLength() * vSamplesPerPixel; + + const int currentVSamplePosition = m_waveformRenderer->getPlayPosVSample(); + const int totalVSamples = m_waveformRenderer->getTotalVSample(); + // qDebug() << "currentVSamplePosition" << currentVSamplePosition + // << "lastDisplayedPosition" << lastDisplayedPosition + // << "vSamplesPerPixel" << vSamplesPerPixel + // << "numberOfVSamples" << numberOfVSamples + // << "totalVSamples" << totalVSamples + // << "WaveformRendererPreroll::playMarkerPosition=" << playMarkerPositionFrac; + + const float halfBreadth = m_waveformRenderer->getBreadth() / 2.0f; + const float halfPolyBreadth = m_waveformRenderer->getBreadth() / 5.0f; + + /* + PainterScope PainterScope(painter); + + painter->setRenderHint(QPainter::Antialiasing); + //painter->setRenderHint(QPainter::HighQualityAntialiasing); + //painter->setBackgroundMode(Qt::TransparentMode); + painter->setWorldMatrixEnabled(false); + painter->setPen(QPen(QBrush(m_color), std::max(1.0, scaleFactor()))); + */ + + const double polyPixelWidth = 40.0 / vSamplesPerPixel; + const double polyPixelOffset = polyPixelWidth; // TODO @m0dB + painter->pen().widthF(); + const double polyVSampleOffset = polyPixelOffset * vSamplesPerPixel; + + // Rotate if drawing vertical waveforms + //if (m_waveformRenderer->getOrientation() == Qt::Vertical) { + // painter->setTransform(QTransform(0, 1, 1, 0, 0, 0)); + //} + + if (preRollVisible) { + // VSample position of the right-most triangle's tip + double triangleTipVSamplePosition = + numberOfVSamples * playMarkerPositionFrac - + currentVSamplePosition; + + float x = triangleTipVSamplePosition / vSamplesPerPixel; + + for (; triangleTipVSamplePosition > 0; + triangleTipVSamplePosition -= polyVSampleOffset) { + m_vertices[vertexCount++] = x; + m_vertices[vertexCount++] = halfBreadth; + m_vertices[vertexCount++] = x - polyPixelWidth; + m_vertices[vertexCount++] = halfBreadth - halfPolyBreadth; + m_vertices[vertexCount++] = x - polyPixelWidth; + m_vertices[vertexCount++] = halfBreadth + halfPolyBreadth; + + x -= polyPixelOffset; + } + } + + if (postRollVisible) { + const int remainingVSamples = totalVSamples - currentVSamplePosition; + // Sample position of the left-most triangle's tip + double triangleTipVSamplePosition = + playMarkerPositionFrac * numberOfVSamples + + remainingVSamples; + + float x = triangleTipVSamplePosition / vSamplesPerPixel; + + for (; triangleTipVSamplePosition < numberOfVSamples; + triangleTipVSamplePosition += polyVSampleOffset) { + m_vertices[vertexCount++] = x; + m_vertices[vertexCount++] = halfBreadth; + m_vertices[vertexCount++] = x + polyPixelWidth; + m_vertices[vertexCount++] = halfBreadth - halfPolyBreadth; + m_vertices[vertexCount++] = x + polyPixelWidth; + m_vertices[vertexCount++] = halfBreadth + halfPolyBreadth; + + x += polyPixelOffset; + } + } + } + + m_shaderProgram.bind(); + + int vertexLocation = m_shaderProgram.attributeLocation("position"); + int matrixLocation = m_shaderProgram.uniformLocation("matrix"); + int colorLocation = m_shaderProgram.uniformLocation("color"); + + QMatrix4x4 matrix; + matrix.ortho(QRectF(0, 0, m_waveformRenderer->getWidth(), m_waveformRenderer->getHeight())); + + m_shaderProgram.enableAttributeArray(vertexLocation); + m_shaderProgram.setAttributeArray( + vertexLocation, GL_FLOAT, m_vertices.constData(), 2); + + m_shaderProgram.setUniformValue(matrixLocation, matrix); + m_shaderProgram.setUniformValue(colorLocation, m_color); + + glDrawArrays(GL_TRIANGLES, 0, vertexCount / 2); +} diff --git a/src/waveform/renderers/qopengl/waveformrendererpreroll.h b/src/waveform/renderers/qopengl/waveformrendererpreroll.h new file mode 100644 index 000000000000..ce0317b1fd84 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendererpreroll.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "util/class.h" +#include "waveform/renderers/qopengl/waveformrenderer.h" + +class QDomNode; +class SkinContext; + +namespace qopengl { +class WaveformRendererPreroll; +} + +class qopengl::WaveformRendererPreroll : public qopengl::WaveformRenderer { + public: + explicit WaveformRendererPreroll(WaveformWidgetRenderer* waveformWidgetRenderer); + ~WaveformRendererPreroll() override; + + void setup(const QDomNode& node, const SkinContext& context) override; + void renderGL() override; + void initializeGL() override; + + private: + QColor m_color; + QVector m_vertices; + QOpenGLShaderProgram m_shaderProgram; + + DISALLOW_COPY_AND_ASSIGN(WaveformRendererPreroll); +}; diff --git a/src/waveform/renderers/qopengl/waveformrendererrgb.cpp b/src/waveform/renderers/qopengl/waveformrendererrgb.cpp new file mode 100644 index 000000000000..67348f3d635f --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendererrgb.cpp @@ -0,0 +1,297 @@ +#include "waveform/renderers/qopengl/waveformrendererrgb.h" + +#include "fixedpointcalc.h" +#include "track/track.h" +#include "util/math.h" +#include "waveform/waveform.h" +#include "waveform/waveformwidgetfactory.h" +#include "waveform/widgets/qopengl/waveformwidget.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +using namespace qopengl; + +WaveformRendererRGB::WaveformRendererRGB( + WaveformWidgetRenderer* waveformWidget) + : WaveformRendererSignalBase(waveformWidget) { +} + +WaveformRendererRGB::~WaveformRendererRGB() { +} + +void WaveformRendererRGB::onSetup(const QDomNode& node) { + Q_UNUSED(node); +} + +void WaveformRendererRGB::initializeGL() { + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +attribute vec3 color;\n\ +varying vec3 vcolor;\n\ +void main()\n\ +{\n\ + vcolor = color;\n\ + gl_Position = matrix * position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +varying vec3 vcolor;\n\ +void main()\n\ +{\n\ + gl_FragColor = vec4(vcolor,1.0);\n\ +}\n"; + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_shaderProgram.link()) { + return; + } + + if (!m_shaderProgram.bind()) { + return; + } +} + +inline void WaveformRendererRGB::addRectangle( + float x1, + float y1, + float x2, + float y2, + float r, + float g, + float b) { + m_lines[m_lineIndex++] = x1; + m_lines[m_lineIndex++] = y1; + + m_lines[m_lineIndex++] = x2; + m_lines[m_lineIndex++] = y1; + + m_lines[m_lineIndex++] = x1; + m_lines[m_lineIndex++] = y2; + + m_lines[m_lineIndex++] = x1; + m_lines[m_lineIndex++] = y2; + + m_lines[m_lineIndex++] = x2; + m_lines[m_lineIndex++] = y2; + + m_lines[m_lineIndex++] = x2; + m_lines[m_lineIndex++] = y1; + + for (int i = 0; i < 6; i++) { + m_colors[m_colorIndex++] = r; + m_colors[m_colorIndex++] = g; + m_colors[m_colorIndex++] = b; + } +} + +void WaveformRendererRGB::renderGL() { + // The source of our data are uint8_t values (waveformData.filtered.low/mid/high). + // We can avoid type conversion by calculating the values needed for drawing + // using fixed point. Since the range of the intermediate values is limited, we + // can take advantage of this and use a lookup table instead of sqrtf. + + uint32_t rgbLowColor_r = toFrac8(m_rgbLowColor_r); + uint32_t rgbMidColor_r = toFrac8(m_rgbMidColor_r); + uint32_t rgbHighColor_r = toFrac8(m_rgbHighColor_r); + uint32_t rgbLowColor_g = toFrac8(m_rgbLowColor_g); + uint32_t rgbMidColor_g = toFrac8(m_rgbMidColor_g); + uint32_t rgbHighColor_g = toFrac8(m_rgbHighColor_g); + uint32_t rgbLowColor_b = toFrac8(m_rgbLowColor_b); + uint32_t rgbMidColor_b = toFrac8(m_rgbMidColor_b); + uint32_t rgbHighColor_b = toFrac8(m_rgbHighColor_b); + + TrackPointer pTrack = m_waveformRenderer->getTrackInfo(); + if (!pTrack) { + return; + } + + ConstWaveformPointer waveform = pTrack->getWaveform(); + if (waveform.isNull()) { + return; + } + + const int dataSize = waveform->getDataSize(); + if (dataSize <= 1) { + return; + } + + const WaveformData* data = waveform->data(); + if (data == nullptr) { + return; + } + + const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); + const int n = static_cast(static_cast( + m_waveformRenderer->getLength() * devicePixelRatio)); + // Not multiplying with devicePixelRatio will also work, and on retina displays 2 pixels will be used + // to represent each block of samples. This is also what is done for the beat grid and the markers. + // const int n = static_cast(static_cast(m_waveformRenderer->getLength())); + + const double firstVisualIndex = m_waveformRenderer->getFirstDisplayedPosition() * dataSize; + const double lastVisualIndex = m_waveformRenderer->getLastDisplayedPosition() * dataSize; + + // Represents the # of waveform data points per horizontal pixel. + const double gain = (lastVisualIndex - firstVisualIndex) / static_cast(n); + + // Per-band gain from the EQ knobs. + float allGain(1.0), lowGain(1.0), midGain(1.0), highGain(1.0); + getGains(&allGain, &lowGain, &midGain, &highGain); + + // gains in 8 bit fractional fixed point + const uint32_t frac8LowGain(toFrac8(lowGain)); + const uint32_t frac8MidGain(toFrac8(midGain)); + const uint32_t frac8HighGain(toFrac8(highGain)); + + const float breadth = static_cast(m_waveformRenderer->getBreadth()); + const float halfBreadth = breadth / 2.0f; + + const float heightFactor = allGain * halfBreadth; + + // Effective visual index of x + double xVisualSampleIndex = firstVisualIndex; + + m_lineIndex = 0; + m_colorIndex = 0; + + m_lines.resize(6 * 2 * (n + 1)); + m_colors.resize(6 * 3 * (n + 1)); + + addRectangle(0.f, + halfBreadth - 0.5f, + static_cast(n), + halfBreadth + 0.5f, + 1.f, + 1.f, + 1.f); + + for (int x = 0; x < n; ++x) { + // Our current pixel (x) corresponds to a number of visual samples + // (visualSamplerPerPixel) in our waveform object. We take the max of + // all the data points on either side of xVisualSampleIndex within a + // window of 'maxSamplingRange' visual samples to measure the maximum + // data point contained by this pixel. + double maxSamplingRange = gain / 2.0; + + // Since xVisualSampleIndex is in visual-samples (e.g. R,L,R,L) we want + // to check +/- maxSamplingRange frames, not samples. To do this, divide + // xVisualSampleIndex by 2. Since frames indices are integers, we round + // to the nearest integer by adding 0.5 before casting to int. + int visualFrameStart = int(xVisualSampleIndex / 2.0 - maxSamplingRange + 0.5); + int visualFrameStop = int(xVisualSampleIndex / 2.0 + maxSamplingRange + 0.5); + const int lastVisualFrame = dataSize / 2 - 1; + + // We now know that some subset of [visualFrameStart, visualFrameStop] + // lies within the valid range of visual frames. Clamp + // visualFrameStart/Stop to within [0, lastVisualFrame]. + visualFrameStart = math_clamp(visualFrameStart, 0, lastVisualFrame); + visualFrameStop = math_clamp(visualFrameStop, 0, lastVisualFrame); + + int visualIndexStart = visualFrameStart * 2; + int visualIndexStop = visualFrameStop * 2; + + visualIndexStart = std::max(visualIndexStart, 0); + visualIndexStop = std::min(visualIndexStop, dataSize); + + uint32_t maxLow = 0; + uint32_t maxMid = 0; + uint32_t maxHigh = 0; + + uint32_t maxAll = 0.; + uint32_t maxAllNext = 0.; + + for (int i = visualIndexStart; i < visualIndexStop; i += 2) { + const WaveformData& waveformData = data[i]; + const WaveformData& waveformDataNext = data[i + 1]; + + maxLow = math_max_u32(maxLow, waveformData.filtered.low, waveformDataNext.filtered.low); + maxMid = math_max_u32(maxMid, waveformData.filtered.mid, waveformDataNext.filtered.mid); + maxHigh = math_max_u32(maxHigh, + waveformData.filtered.high, + waveformDataNext.filtered.high); + + uint32_t all = frac8Pow2ToFrac16(waveformData.filtered.low * frac8LowGain) + + frac8Pow2ToFrac16(waveformData.filtered.mid * frac8MidGain) + + frac8Pow2ToFrac16(waveformData.filtered.high * frac8HighGain); + maxAll = math_max(maxAll, all); + + uint32_t allNext = frac8Pow2ToFrac16(waveformDataNext.filtered.low * frac8LowGain) + + frac8Pow2ToFrac16(waveformDataNext.filtered.mid * frac8MidGain) + + frac8Pow2ToFrac16(waveformDataNext.filtered.high * frac8HighGain); + maxAllNext = math_max(maxAllNext, allNext); + } + + // We can do these integer calculation safely, staying well within the + // 32 bit range, and we will normalize below. + maxLow *= frac8LowGain; + maxMid *= frac8MidGain; + maxHigh *= frac8HighGain; + uint32_t red = maxLow * rgbLowColor_r + maxMid * rgbMidColor_r + + maxHigh * rgbHighColor_r; + uint32_t green = maxLow * rgbLowColor_g + maxMid * rgbMidColor_g + + maxHigh * rgbHighColor_g; + uint32_t blue = maxLow * rgbLowColor_b + maxMid * rgbMidColor_b + + maxHigh * rgbHighColor_b; + + // Normalize red, green, blue to 0..255, using the maximum of the three and + // this fixed point arithmetic trick: + // max / ((max>>8)+1) = 0..255 + uint32_t max = math_max_u32(red, green, blue); + max >>= 8; + + if (max == 0) { + // avoid division by 0 + red = 0; + green = 0; + blue = 0; + } else { + max++; // important, otherwise we normalize to 256 + + red /= max; + green /= max; + blue /= max; + } + + const float fx = static_cast(x); + + // lines are thin rectangles + addRectangle(fx, + halfBreadth - heightFactor * frac16_sqrt(maxAll), + fx + 1.f, + halfBreadth + heightFactor * frac16_sqrt(maxAllNext), + float(red) / 255.f, + float(green) / 255.f, + float(blue) / 255.f); + + xVisualSampleIndex += gain; + } + + QMatrix4x4 matrix; + matrix.ortho(QRectF(0, 0, n, m_waveformRenderer->getHeight())); + + m_shaderProgram.bind(); + + int matrixLocation = m_shaderProgram.uniformLocation("matrix"); + int positionLocation = m_shaderProgram.attributeLocation("position"); + int colorLocation = m_shaderProgram.attributeLocation("color"); + + m_shaderProgram.setUniformValue(matrixLocation, matrix); + + m_shaderProgram.enableAttributeArray(positionLocation); + m_shaderProgram.setAttributeArray( + positionLocation, GL_FLOAT, m_lines.constData(), 2); + m_shaderProgram.enableAttributeArray(colorLocation); + m_shaderProgram.setAttributeArray( + colorLocation, GL_FLOAT, m_colors.constData(), 3); + + glDrawArrays(GL_TRIANGLES, 0, m_lineIndex / 2); +} diff --git a/src/waveform/renderers/qopengl/waveformrendererrgb.h b/src/waveform/renderers/qopengl/waveformrendererrgb.h new file mode 100644 index 000000000000..f187e8d4a68b --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendererrgb.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "waveform/renderers/qopengl/waveformrenderersignalbase.h" + +namespace qopengl { +class WaveformRendererRGB; +} + +class qopengl::WaveformRendererRGB : public qopengl::WaveformRendererSignalBase { + public: + explicit WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget); + ~WaveformRendererRGB() override; + + // override ::WaveformRendererSignalBase + void onSetup(const QDomNode& node) override; + + void initializeGL() override; + void renderGL() override; + + private: + int m_lineIndex; + int m_colorIndex; + + QVector m_lines; + QVector m_colors; + + QOpenGLShaderProgram m_shaderProgram; + + void addRectangle(float x1, float y1, float x2, float y2, float r, float g, float b); + + DISALLOW_COPY_AND_ASSIGN(WaveformRendererRGB); +}; diff --git a/src/waveform/renderers/qopengl/waveformrenderersignalbase.cpp b/src/waveform/renderers/qopengl/waveformrenderersignalbase.cpp new file mode 100644 index 000000000000..1fe7461104a2 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderersignalbase.cpp @@ -0,0 +1,10 @@ +#include "waveform/renderers/qopengl/waveformrenderersignalbase.h" + +#include "waveform/widgets/qopengl/waveformwidget.h" + +using namespace qopengl; + +qopengl::WaveformRendererSignalBase::WaveformRendererSignalBase( + WaveformWidgetRenderer* waveformWidget) + : ::WaveformRendererSignalBase(waveformWidget) { +} diff --git a/src/waveform/renderers/qopengl/waveformrenderersignalbase.h b/src/waveform/renderers/qopengl/waveformrenderersignalbase.h new file mode 100644 index 000000000000..21a2c9e9006e --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrenderersignalbase.h @@ -0,0 +1,26 @@ +#pragma once + +#include "waveform/renderers/qopengl/iwaveformrenderer.h" +#include "waveform/renderers/waveformrenderersignalbase.h" + +class WaveformWidgetRenderer; + +namespace qopengl { +class WaveformRendererSignalBase; +} // namespace qopengl + +class qopengl::WaveformRendererSignalBase : public ::WaveformRendererSignalBase, + public qopengl::IWaveformRenderer { + public: + explicit WaveformRendererSignalBase(WaveformWidgetRenderer* waveformWidget); + ~WaveformRendererSignalBase() override = default; + + void draw(QPainter* painter, QPaintEvent* event) override { + } + + IWaveformRenderer* qopenglWaveformRenderer() override { + return this; + } + + DISALLOW_COPY_AND_ASSIGN(WaveformRendererSignalBase); +}; diff --git a/src/waveform/renderers/qopengl/waveformrendermark.cpp b/src/waveform/renderers/qopengl/waveformrendermark.cpp new file mode 100644 index 000000000000..515b9e6d485a --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendermark.cpp @@ -0,0 +1,696 @@ +#include "waveform/renderers/qopengl/waveformrendermark.h" + +#include +#include + +#include "control/controlobject.h" +#include "engine/controls/cuecontrol.h" +#include "track/track.h" +#include "util/color/color.h" +#include "util/painterscope.h" +#include "waveform/renderers/qopengl/moc_waveformrendermark.cpp" +#include "waveform/waveform.h" +#include "waveform/widgets/qopengl/waveformwidget.h" +#include "widget/wimagestore.h" +#include "widget/wskincolor.h" + +namespace { +constexpr int kMaxCueLabelLength = 23; +} // namespace + +using namespace qopengl; + +WaveformRenderMark::WaveformRenderMark(WaveformWidgetRenderer* waveformWidget) + : WaveformRenderer(waveformWidget) { +} + +WaveformRenderMark::~WaveformRenderMark() { + for (const auto& pMark : m_marks) { + pMark->m_pTexture.reset(); + } +} + +void WaveformRenderMark::setup(const QDomNode& node, const SkinContext& context) { + WaveformSignalColors signalColors = *m_waveformRenderer->getWaveformSignalColors(); + m_marks.setup(m_waveformRenderer->getGroup(), node, context, signalColors); +} + +void WaveformRenderMark::initializeGL() { + initGradientShader(); + initTextureShader(); + + generatePlayPosMarkTexture(); +} + +void WaveformRenderMark::initGradientShader() { + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +attribute vec3 gradient;\n\ +varying vec3 vGradient;\n\ +void main()\n\ +{\n\ + vGradient = gradient;\n\ + gl_Position = matrix * position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +uniform vec4 color;\n\ +varying vec3 vGradient;\n\ +void main()\n\ +{\n\ + gl_FragColor = vec4(color.x, color.y, color.z, color.w * max(0.0, abs((vGradient.x + vGradient.y) * 4.0 - 2.0) - 1.0));\n\ +}\n"; + + if (!m_gradientShaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_gradientShaderProgram.addShaderFromSourceCode( + QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_gradientShaderProgram.link()) { + return; + } + + if (!m_gradientShaderProgram.bind()) { + return; + } +} + +void WaveformRenderMark::initTextureShader() { + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +attribute vec3 texcoor;\n\ +varying vec3 vTexcoor;\n\ +void main()\n\ +{\n\ + vTexcoor = texcoor;\n\ + gl_Position = matrix * position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +uniform sampler2D sampler;\n\ +varying vec3 vTexcoor;\n\ +void main()\n\ +{\n\ + gl_FragColor = texture2D(sampler, vec2(vTexcoor.x, vTexcoor.y));\n\ +}\n"; + + if (!m_textureShaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_textureShaderProgram.addShaderFromSourceCode( + QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_textureShaderProgram.link()) { + return; + } + + if (!m_textureShaderProgram.bind()) { + return; + } +} + +void WaveformRenderMark::drawTexture(int x, int y, QOpenGLTexture* texture) { + 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 + texture->width() / devicePixelRatio; + const float posy1 = y; + const float posy2 = y + texture->height() / devicePixelRatio; + + const float posarray[] = {posx1, posy1, posx2, posy1, posx1, posy2, posx2, posy2}; + const float texarray[] = {texx1, texy1, texx2, texy1, texx1, texy2, texx2, texy2}; + + QMatrix4x4 matrix; + matrix.ortho(QRectF(0, 0, m_waveformRenderer->getWidth(), m_waveformRenderer->getHeight())); + + m_textureShaderProgram.bind(); + + int matrixLocation = m_textureShaderProgram.uniformLocation("matrix"); + int samplerLocation = m_textureShaderProgram.uniformLocation("sampler"); + int positionLocation = m_textureShaderProgram.attributeLocation("position"); + int texcoordLocation = m_textureShaderProgram.attributeLocation("texcoor"); + + m_textureShaderProgram.setUniformValue(matrixLocation, matrix); + + m_textureShaderProgram.enableAttributeArray(positionLocation); + m_textureShaderProgram.setAttributeArray( + positionLocation, GL_FLOAT, posarray, 2); + m_textureShaderProgram.enableAttributeArray(texcoordLocation); + m_textureShaderProgram.setAttributeArray( + texcoordLocation, GL_FLOAT, texarray, 2); + + m_textureShaderProgram.setUniformValue(samplerLocation, 0); + + texture->bind(); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +void WaveformRenderMark::fillRectWithGradient( + const QRectF& rect, QColor color, Qt::Orientation orientation) { + const float grdx1 = 0.f; + const float grdy1 = 0.f; + const float grdx2 = orientation == Qt::Horizontal ? 1.f : 0.f; + const float grdy2 = orientation == Qt::Vertical ? 1.f : 0.f; + + const float posx1 = rect.x(); + const float posx2 = rect.x() + rect.width(); + const float posy1 = rect.y(); + const float posy2 = rect.y() + rect.height(); + + const float posarray[] = {posx1, posy1, posx2, posy1, posx1, posy2, posx2, posy2}; + const float grdarray[] = {grdx1, grdy1, grdx2, grdy1, grdx1, grdy2, grdx2, grdy2}; + + QMatrix4x4 matrix; + matrix.ortho(QRectF(0, 0, m_waveformRenderer->getWidth(), m_waveformRenderer->getHeight())); + m_gradientShaderProgram.bind(); + + int matrixLocation = m_gradientShaderProgram.uniformLocation("matrix"); + int colorLocation = m_gradientShaderProgram.uniformLocation("color"); + int positionLocation = m_gradientShaderProgram.attributeLocation("position"); + int gradientLocation = m_gradientShaderProgram.attributeLocation("gradient"); + + m_gradientShaderProgram.setUniformValue(matrixLocation, matrix); + m_gradientShaderProgram.setUniformValue(colorLocation, color); + + m_gradientShaderProgram.enableAttributeArray(positionLocation); + m_gradientShaderProgram.setAttributeArray( + positionLocation, GL_FLOAT, posarray, 2); + m_gradientShaderProgram.enableAttributeArray(gradientLocation); + m_gradientShaderProgram.setAttributeArray( + gradientLocation, GL_FLOAT, grdarray, 2); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +void WaveformRenderMark::renderGL() { + const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); + QMap marksOnScreen; + + checkCuesUpdated(); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + for (const auto& pMark : m_marks) { + if (!pMark->isValid()) { + continue; + } + + if (pMark->hasVisible() && !pMark->isVisible()) { + continue; + } + + // Generate image on first paint can't be done in setup since we need to + // wait for the render widget to be resized yet. + if (pMark->m_image.isNull()) { + generateMarkImage(pMark); + } + + const double samplePosition = pMark->getSamplePosition(); + if (samplePosition != Cue::kNoPosition) { + double currentMarkPoint = + m_waveformRenderer->transformSamplePositionInRendererWorld(samplePosition); + const double sampleEndPosition = pMark->getSampleEndPosition(); + + currentMarkPoint = qRound(currentMarkPoint); + + if (m_waveformRenderer->getOrientation() == Qt::Horizontal) { + // Pixmaps are expected to have the mark stroke at the center, + // and preferably have an odd width in order to have the stroke + // exactly at the sample position. + const int markHalfWidth = static_cast( + pMark->m_image.width() / 2.0 / devicePixelRatio); + const int drawOffset = static_cast(currentMarkPoint) - markHalfWidth; + + bool visible = false; + // Check if the current point needs to be displayed. + if (currentMarkPoint > -markHalfWidth && + currentMarkPoint < m_waveformRenderer->getWidth() + + markHalfWidth) { + drawTexture(drawOffset, 0, pMark->m_pTexture.get()); + visible = true; + } + + // Check if the range needs to be displayed. + if (samplePosition != sampleEndPosition && sampleEndPosition != Cue::kNoPosition) { + DEBUG_ASSERT(samplePosition < sampleEndPosition); + double currentMarkEndPoint = + m_waveformRenderer->transformSamplePositionInRendererWorld( + sampleEndPosition); + + currentMarkEndPoint = qRound(currentMarkEndPoint); + + if (visible || currentMarkEndPoint > 0) { + QColor color = pMark->fillColor(); + color.setAlphaF(0.4); + + fillRectWithGradient( + QRectF(QPointF(currentMarkPoint, 0), + QPointF(currentMarkEndPoint, + m_waveformRenderer + ->getHeight())), + color, + Qt::Vertical); + visible = true; + } + } + + if (visible) { + marksOnScreen[pMark] = drawOffset; + } + } else { + const int markHalfHeight = static_cast(pMark->m_image.height() / 2.0); + const int drawOffset = static_cast(currentMarkPoint) - markHalfHeight; + + bool visible = false; + // Check if the current point needs to be displayed. + if (currentMarkPoint > -markHalfHeight && + currentMarkPoint < m_waveformRenderer->getHeight() + + markHalfHeight) { + drawTexture(drawOffset, 0, pMark->m_pTexture.get()); + visible = true; + } + + // Check if the range needs to be displayed. + if (samplePosition != sampleEndPosition && sampleEndPosition != Cue::kNoPosition) { + DEBUG_ASSERT(samplePosition < sampleEndPosition); + double currentMarkEndPoint = + m_waveformRenderer + ->transformSamplePositionInRendererWorld( + sampleEndPosition); + if (currentMarkEndPoint < m_waveformRenderer->getHeight()) { + QColor color = pMark->fillColor(); + color.setAlphaF(0.4); + fillRectWithGradient( + QRectF(QPointF(0, currentMarkPoint), + QPointF(m_waveformRenderer->getWidth(), + currentMarkEndPoint)), + color, + Qt::Horizontal); + visible = true; + } + } + + if (visible) { + marksOnScreen[pMark] = drawOffset; + } + } + } + } + m_waveformRenderer->setMarkPositions(marksOnScreen); + + double currentMarkPoint = + qRound(m_waveformRenderer->getPlayMarkerPosition() * + m_waveformRenderer->getWidth()); + const int markHalfWidth = static_cast( + m_pPlayPosMarkTexture->width() / 2.0 / devicePixelRatio); + const int drawOffset = static_cast(currentMarkPoint) - markHalfWidth; + + drawTexture(drawOffset, 0, m_pPlayPosMarkTexture.get()); +} + +void WaveformRenderMark::generatePlayPosMarkTexture() { + float imgwidth; + float imgheight; + + const auto width = m_waveformRenderer->getWidth(); + const auto height = m_waveformRenderer->getHeight(); + const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); + + // const auto playMarkerPosition = m_waveformRenderer->getPlayMarkerPosition(); + const auto orientation = m_waveformRenderer->getOrientation(); + + const int lineX = 5; + const int lineY = 5; + + if (m_waveformRenderer->getOrientation() == Qt::Horizontal) { + imgwidth = 11; + imgheight = m_waveformRenderer->getHeight(); + } else { + imgwidth = m_waveformRenderer->getWidth(); + imgheight = 11; + } + + QImage image(static_cast(imgwidth * devicePixelRatio), + static_cast(imgheight * devicePixelRatio), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(devicePixelRatio); + image.fill(QColor(0, 0, 0, 0).rgba()); + + QPainter painter; + + painter.begin(&image); + + painter.setWorldMatrixEnabled(false); + + // draw dim outlines to increase playpos/waveform contrast + painter.setOpacity(0.5); + painter.setPen(m_waveformRenderer->getWaveformSignalColors()->getBgColor()); + QBrush bgFill = m_waveformRenderer->getWaveformSignalColors()->getBgColor(); + if (orientation == Qt::Horizontal) { + // lines next to playpos + // Note: don't draw lines where they would overlap the triangles, + // otherwise both translucent strokes add up to a darker tone. + painter.drawLine(lineX + 1, 4, lineX + 1, height); + painter.drawLine(lineX - 1, 4, lineX - 1, height); + + // triangle at top edge + // Increase line/waveform contrast + painter.setOpacity(0.8); + QPointF t0 = QPointF(lineX - 5, 0); + QPointF t1 = QPointF(lineX + 5, 0); + QPointF t2 = QPointF(lineX, 6); + drawTriangle(&painter, bgFill, t0, t1, t2); + } else { // vertical waveforms + painter.drawLine(4, lineY + 1, width, lineY + 1); + painter.drawLine(4, lineY - 1, width, lineY - 1); + // triangle at left edge + painter.setOpacity(0.8); + QPointF l0 = QPointF(0, lineY - 5.01); + QPointF l1 = QPointF(0, lineY + 4.99); + QPointF l2 = QPointF(6, lineY); + drawTriangle(&painter, bgFill, l0, l1, l2); + } + + // draw colored play position indicators + painter.setOpacity(1.0); + painter.setPen(m_waveformRenderer->getWaveformSignalColors()->getPlayPosColor()); + QBrush fgFill = m_waveformRenderer->getWaveformSignalColors()->getPlayPosColor(); + if (orientation == Qt::Horizontal) { + // play position line + painter.drawLine(lineX, 0, lineX, height); + // triangle at top edge + QPointF t0 = QPointF(lineX - 4, 0); + QPointF t1 = QPointF(lineX + 4, 0); + QPointF t2 = QPointF(lineX, 5); + drawTriangle(&painter, fgFill, t0, t1, t2); + } else { + // vertical waveforms + painter.drawLine(0, lineY, width, lineY); + // triangle at left edge + QPointF l0 = QPointF(0, lineY - 4.01); + QPointF l1 = QPointF(0, lineY + 4); + QPointF l2 = QPointF(5, lineY); + drawTriangle(&painter, fgFill, l0, l1, l2); + } + painter.end(); + + m_pPlayPosMarkTexture.reset(new QOpenGLTexture(image)); + m_pPlayPosMarkTexture->setMinificationFilter(QOpenGLTexture::Linear); + m_pPlayPosMarkTexture->setMagnificationFilter(QOpenGLTexture::Linear); + m_pPlayPosMarkTexture->setWrapMode(QOpenGLTexture::ClampToBorder); +} + +void WaveformRenderMark::drawTriangle(QPainter* painter, + const QBrush& fillColor, + QPointF p0, + QPointF p1, + QPointF p2) { + QPainterPath triangle; + painter->setPen(Qt::NoPen); + triangle.moveTo(p0); // ° base 1 + triangle.lineTo(p1); // > base 2 + triangle.lineTo(p2); // > peak + triangle.lineTo(p0); // > base 1 + painter->fillPath(triangle, fillColor); +} + +void WaveformRenderMark::resizeGL(int, int) { + // Delete all marks' images. New images will be created on next paint. + for (const auto& pMark : m_marks) { + pMark->m_image = QImage(); + } +} + +void WaveformRenderMark::onSetTrack() { + slotCuesUpdated(); + + TrackPointer trackInfo = m_waveformRenderer->getTrackInfo(); + if (!trackInfo) { + return; + } + connect(trackInfo.get(), + &Track::cuesUpdated, + this, + &WaveformRenderMark::slotCuesUpdated); +} + +void WaveformRenderMark::slotCuesUpdated() { + m_bCuesUpdates = true; +} + +void WaveformRenderMark::checkCuesUpdated() { + if (!m_bCuesUpdates) { + return; + } + // TODO @m0dB use atomic? + m_bCuesUpdates = false; + + TrackPointer trackInfo = m_waveformRenderer->getTrackInfo(); + if (!trackInfo) { + return; + } + + QList loadedCues = trackInfo->getCuePoints(); + for (const CuePointer& pCue : loadedCues) { + int hotCue = pCue->getHotCue(); + if (hotCue == Cue::kNoHotCue) { + continue; + } + + // Here we assume no two cues can have the same hotcue assigned, + // because WaveformMarkSet stores one mark for each hotcue. + WaveformMarkPointer pMark = m_marks.getHotCueMark(hotCue); + if (pMark.isNull()) { + continue; + } + + QString newLabel = pCue->getLabel(); + QColor newColor = mixxx::RgbColor::toQColor(pCue->getColor()); + if (pMark->m_text.isNull() || newLabel != pMark->m_text || + !pMark->fillColor().isValid() || + newColor != pMark->fillColor()) { + pMark->m_text = newLabel; + int dimBrightThreshold = m_waveformRenderer->getDimBrightThreshold(); + pMark->setBaseColor(newColor, dimBrightThreshold); + generateMarkImage(pMark); + } + } +} + +void WaveformRenderMark::generateMarkImage(WaveformMarkPointer pMark) { + // Load the pixmap from file. + // If that succeeds loading the text and stroke is skipped. + const float devicePixelRatio = m_waveformRenderer->getDevicePixelRatio(); + if (!pMark->m_pixmapPath.isEmpty()) { + QString path = pMark->m_pixmapPath; + // Use devicePixelRatio to properly scale the image + QImage image = *WImageStore::getImage(path, devicePixelRatio); + //QImage image = QImage(path); + // If loading the image didn't fail, then we're done. Otherwise fall + // through and render a label. + if (!image.isNull()) { + pMark->m_image = + image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + //WImageStore::correctImageColors(&pMark->m_image); + // Set the pixel/device ratio AFTER loading the image in order to get + // a truly scaled source image. + // See https://doc.qt.io/qt-5/qimage.html#setDevicePixelRatio + // Also, without this some Qt-internal issue results in an offset + // image when calculating the center line of pixmaps in draw(). + pMark->m_image.setDevicePixelRatio(devicePixelRatio); + pMark->m_pTexture.reset(new QOpenGLTexture(pMark->m_image)); + pMark->m_pTexture->setMinificationFilter(QOpenGLTexture::Linear); + pMark->m_pTexture->setMagnificationFilter(QOpenGLTexture::Linear); + pMark->m_pTexture->setWrapMode(QOpenGLTexture::ClampToBorder); + return; + } + } + + { + QPainter painter; + + // Determine mark text. + QString label = pMark->m_text; + if (pMark->getHotCue() >= 0) { + if (!label.isEmpty()) { + label.prepend(": "); + } + label.prepend(QString::number(pMark->getHotCue() + 1)); + if (label.size() > kMaxCueLabelLength) { + label = label.left(kMaxCueLabelLength - 3) + "..."; + } + } + + // This alone would pick the OS default font, or that set by Qt5 Settings (qt5ct) + // respectively. This would mostly not be notable since contemporary OS and distros + // use a proven sans-serif anyway. Though, some user fonts may be lacking glyphs + // we use for the intro/outro markers for example. + QFont font; + // So, let's just use Open Sans which is used by all official skins to achieve + // a consistent skin design. + font.setFamily("Open Sans"); + // Use a pixel size like everywhere else in Mixxx, which can be scaled well + // in general. + // Point sizes would work if only explicit Qt scaling QT_SCALE_FACTORS is used, + // though as soon as other OS-based font and app scaling mechanics join the + // party the resulting font size is hard to predict (affects all supported OS). + font.setPixelSize(13); + font.setWeight(75); // bold + font.setItalic(false); + + QFontMetrics metrics(font); + + //fixed margin ... + QRect wordRect = metrics.tightBoundingRect(label); + constexpr int marginX = 1; + constexpr int marginY = 1; + wordRect.moveTop(marginX + 1); + wordRect.moveLeft(marginY + 1); + wordRect.setHeight(wordRect.height() + (wordRect.height() % 2)); + wordRect.setWidth(wordRect.width() + (wordRect.width()) % 2); + //even wordrect to have an even Image >> draw the line in the middle ! + + int labelRectWidth = wordRect.width() + 2 * marginX + 4; + int labelRectHeight = wordRect.height() + 2 * marginY + 4; + + QRectF labelRect(0, 0, (float)labelRectWidth, (float)labelRectHeight); + + int width; + int height; + + if (m_waveformRenderer->getOrientation() == Qt::Horizontal) { + width = 2 * labelRectWidth + 1; + height = m_waveformRenderer->getHeight(); + } else { + width = m_waveformRenderer->getWidth(); + height = 2 * labelRectHeight + 1; + } + + pMark->m_image = QImage( + static_cast(width * devicePixelRatio), + static_cast(height * devicePixelRatio), + QImage::Format_ARGB32_Premultiplied); + pMark->m_image.setDevicePixelRatio(devicePixelRatio); + + Qt::Alignment markAlignH = pMark->m_align & Qt::AlignHorizontal_Mask; + Qt::Alignment markAlignV = pMark->m_align & Qt::AlignVertical_Mask; + + if (markAlignH == Qt::AlignHCenter) { + labelRect.moveLeft((width - labelRectWidth) / 2); + } else if (markAlignH == Qt::AlignRight) { + labelRect.moveRight(width - 1); + } + + if (markAlignV == Qt::AlignVCenter) { + labelRect.moveTop((height - labelRectHeight) / 2); + } else if (markAlignV == Qt::AlignBottom) { + labelRect.moveBottom(height - 1); + } + + pMark->m_label.setAreaRect(labelRect); + + // Fill with transparent pixels + pMark->m_image.fill(QColor(0, 0, 0, 0).rgba()); + + painter.begin(&pMark->m_image); + painter.setRenderHint(QPainter::TextAntialiasing); + + painter.setWorldMatrixEnabled(false); + + // Draw marker lines + if (m_waveformRenderer->getOrientation() == Qt::Horizontal) { + int middle = width / 2; + pMark->m_linePosition = middle; + if (markAlignH == Qt::AlignHCenter) { + if (labelRect.top() > 0) { + painter.setPen(pMark->fillColor()); + painter.drawLine(QLineF(middle, 0, middle, labelRect.top())); + + painter.setPen(pMark->borderColor()); + painter.drawLine(QLineF(middle - 1, 0, middle - 1, labelRect.top())); + painter.drawLine(QLineF(middle + 1, 0, middle + 1, labelRect.top())); + } + + if (labelRect.bottom() < height) { + painter.setPen(pMark->fillColor()); + painter.drawLine(QLineF(middle, labelRect.bottom(), middle, height)); + + painter.setPen(pMark->borderColor()); + painter.drawLine(QLineF(middle - 1, labelRect.bottom(), middle - 1, height)); + painter.drawLine(QLineF(middle + 1, labelRect.bottom(), middle + 1, height)); + } + } else { // AlignLeft || AlignRight + painter.setPen(pMark->fillColor()); + painter.drawLine(middle, 0, middle, height); + + painter.setPen(pMark->borderColor()); + painter.drawLine(middle - 1, 0, middle - 1, height); + painter.drawLine(middle + 1, 0, middle + 1, height); + } + } else { // Vertical + int middle = height / 2; + pMark->m_linePosition = middle; + if (markAlignV == Qt::AlignVCenter) { + if (labelRect.left() > 0) { + painter.setPen(pMark->fillColor()); + painter.drawLine(QLineF(0, middle, labelRect.left(), middle)); + + painter.setPen(pMark->borderColor()); + painter.drawLine(QLineF(0, middle - 1, labelRect.left(), middle - 1)); + painter.drawLine(QLineF(0, middle + 1, labelRect.left(), middle + 1)); + } + + if (labelRect.right() < width) { + painter.setPen(pMark->fillColor()); + painter.drawLine(QLineF(labelRect.right(), middle, width, middle)); + + painter.setPen(pMark->borderColor()); + painter.drawLine(QLineF(labelRect.right(), middle - 1, width, middle - 1)); + painter.drawLine(QLineF(labelRect.right(), middle + 1, width, middle + 1)); + } + } else { // AlignTop || AlignBottom + painter.setPen(pMark->fillColor()); + painter.drawLine(0, middle, width, middle); + + painter.setPen(pMark->borderColor()); + painter.drawLine(0, middle - 1, width, middle - 1); + painter.drawLine(0, middle + 1, width, middle + 1); + } + } + + // Draw the label rect + painter.setPen(pMark->borderColor()); + painter.setBrush(QBrush(pMark->fillColor())); + painter.drawRoundedRect(labelRect, 2.0, 2.0); + + // Draw text + painter.setBrush(QBrush(QColor(0, 0, 0, 0))); + painter.setFont(font); + painter.setPen(pMark->labelColor()); + painter.drawText(labelRect, Qt::AlignCenter, label); + } + + pMark->m_pTexture.reset(new QOpenGLTexture(pMark->m_image)); + pMark->m_pTexture->setMinificationFilter(QOpenGLTexture::Linear); + pMark->m_pTexture->setMagnificationFilter(QOpenGLTexture::Linear); + pMark->m_pTexture->setWrapMode(QOpenGLTexture::ClampToBorder); +} diff --git a/src/waveform/renderers/qopengl/waveformrendermark.h b/src/waveform/renderers/qopengl/waveformrendermark.h new file mode 100644 index 000000000000..6fe51430d041 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendermark.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include + +#include "util/class.h" +#include "waveform/renderers/qopengl/waveformrenderer.h" +#include "waveform/renderers/waveformmarkset.h" + +class QDomNode; +class SkinContext; +class QOpenGLTexture; + +namespace qopengl { +class WaveformRenderMark; +} + +class qopengl::WaveformRenderMark : public QObject, public qopengl::WaveformRenderer { + Q_OBJECT + public: + explicit WaveformRenderMark(WaveformWidgetRenderer* waveformWidget); + ~WaveformRenderMark() override; + + void setup(const QDomNode& node, const SkinContext& context) override; + + void initializeGL() override; + void renderGL() override; + void resizeGL(int w, int h) override; + + // Called when a new track is loaded. + void onSetTrack() override; + + public slots: + // Called when the loaded track's cues are added, deleted or modified and + // when a new track is loaded. + // It updates the marks' names and regenerates their image if needed. + // This method is used for hotcues. + void slotCuesUpdated(); + + private: + void checkCuesUpdated(); + + void initGradientShader(); + void initTextureShader(); + + void generateMarkImage(WaveformMarkPointer pMark); + void generatePlayPosMarkTexture(); + + void drawTriangle(QPainter* painter, + const QBrush& fillColor, + QPointF p1, + QPointF p2, + QPointF p3); + + WaveformMarkSet m_marks; + QOpenGLShaderProgram m_gradientShaderProgram; + QOpenGLShaderProgram m_textureShaderProgram; + std::unique_ptr m_pPlayPosMarkTexture; + bool m_bCuesUpdates{}; + + void fillRectWithGradient(const QRectF& rect, QColor color, Qt::Orientation orientation); + void drawTexture(int x, int y, QOpenGLTexture* texture); + + DISALLOW_COPY_AND_ASSIGN(WaveformRenderMark); +}; diff --git a/src/waveform/renderers/qopengl/waveformrendermarkrange.cpp b/src/waveform/renderers/qopengl/waveformrendermarkrange.cpp new file mode 100644 index 000000000000..5f89d79f1937 --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendermarkrange.cpp @@ -0,0 +1,147 @@ +#include "waveform/renderers/qopengl/waveformrendermarkrange.h" + +#include +#include +#include +#include +#include +#include + +#include "preferences/usersettings.h" +#include "track/track.h" +#include "util/painterscope.h" +#include "waveform/widgets/qopengl/waveformwidget.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +using namespace qopengl; + +WaveformRenderMarkRange::WaveformRenderMarkRange(WaveformWidgetRenderer* waveformWidget) + : WaveformRenderer(waveformWidget) { +} + +WaveformRenderMarkRange::~WaveformRenderMarkRange() { +} + +void WaveformRenderMarkRange::initializeGL() { + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +void main()\n\ +{\n\ + gl_Position = matrix * position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +uniform vec4 color;\n\ +void main()\n\ +{\n\ + gl_FragColor = color;\n\ +}\n"; + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_shaderProgram.addShaderFromSourceCode( + QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_shaderProgram.link()) { + return; + } + + if (!m_shaderProgram.bind()) { + return; + } +} +void WaveformRenderMarkRange::fillRect( + const QRectF& rect, QColor color) { + const float posx1 = rect.x(); + const float posx2 = rect.x() + rect.width(); + const float posy1 = rect.y(); + const float posy2 = rect.y() + rect.height(); + + const float posarray[] = {posx1, posy1, posx2, posy1, posx1, posy2, posx2, posy2}; + + int colorLocation = m_shaderProgram.uniformLocation("color"); + int positionLocation = m_shaderProgram.attributeLocation("position"); + + m_shaderProgram.setUniformValue(colorLocation, color); + + m_shaderProgram.enableAttributeArray(positionLocation); + m_shaderProgram.setAttributeArray( + positionLocation, GL_FLOAT, posarray, 2); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +void WaveformRenderMarkRange::setup(const QDomNode& node, const SkinContext& context) { + 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())); + } + child = child.nextSibling(); + } +} + +void WaveformRenderMarkRange::renderGL() { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + QMatrix4x4 matrix; + matrix.ortho(QRectF(0, 0, m_waveformRenderer->getWidth(), m_waveformRenderer->getHeight())); + m_shaderProgram.bind(); + + int matrixLocation = m_shaderProgram.uniformLocation("matrix"); + m_shaderProgram.setUniformValue(matrixLocation, matrix); + + for (auto&& markRange : m_markRanges) { + // If the mark range is not active we should not draw it. + if (!markRange.active()) { + continue; + } + + // If the mark range is not visible we should not draw it. + if (!markRange.visible()) { + continue; + } + + // Active mark ranges by definition have starts/ends that are not + // disabled so no need to check. + double startSample = markRange.start(); + double endSample = markRange.end(); + + double startPosition = + m_waveformRenderer->transformSamplePositionInRendererWorld( + startSample); + double endPosition = m_waveformRenderer->transformSamplePositionInRendererWorld(endSample); + + startPosition = qRound(startPosition); + endPosition = qRound(endPosition); + + const double span = std::max(endPosition - startPosition, 1.0); + + //range not in the current display + if (startPosition > m_waveformRenderer->getLength() || endPosition < 0) { + continue; + } + + QColor color = markRange.enabled() ? markRange.m_activeColor : markRange.m_disabledColor; + color.setAlphaF(0.3); + + fillRect(QRectF(startPosition, 0, span, m_waveformRenderer->getHeight()), color); + } +} diff --git a/src/waveform/renderers/qopengl/waveformrendermarkrange.h b/src/waveform/renderers/qopengl/waveformrendermarkrange.h new file mode 100644 index 000000000000..ed2dea5fdd2a --- /dev/null +++ b/src/waveform/renderers/qopengl/waveformrendermarkrange.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "preferences/usersettings.h" +#include "waveform/renderers/qopengl/waveformrenderer.h" +#include "waveform/renderers/waveformmarkrange.h" + +class QDomNode; +class SkinContext; + +namespace qopengl { +class WaveformRenderMarkRange; +} + +class qopengl::WaveformRenderMarkRange : public qopengl::WaveformRenderer { + public: + explicit WaveformRenderMarkRange(WaveformWidgetRenderer* waveformWidget); + ~WaveformRenderMarkRange() override; + + void setup(const QDomNode& node, const SkinContext& context) override; + + void initializeGL() override; + void renderGL() override; + + private: + void fillRect(const QRectF& rect, QColor color); + + QOpenGLShaderProgram m_shaderProgram; + std::vector m_markRanges; + + DISALLOW_COPY_AND_ASSIGN(WaveformRenderMarkRange); +}; diff --git a/src/waveform/renderers/waveformmark.cpp b/src/waveform/renderers/waveformmark.cpp index 34e5870fa0ac..91f07d566f7c 100644 --- a/src/waveform/renderers/waveformmark.cpp +++ b/src/waveform/renderers/waveformmark.cpp @@ -1,11 +1,14 @@ +#include "waveformmark.h" + +#ifdef MIXXX_USE_QOPENGL +#include +#endif #include #include "skin/legacy/skincontext.h" #include "waveform/renderers/waveformsignalcolors.h" #include "widget/wskincolor.h" -#include "waveformmark.h" - namespace { Qt::Alignment decodeAlignmentFlags(const QString& alignString, Qt::Alignment defaultFlags) { QStringList stringFlags = alignString.toLower() @@ -110,6 +113,8 @@ WaveformMark::WaveformMark(const QString& group, } } +WaveformMark::~WaveformMark() = default; + void WaveformMark::setBaseColor(QColor baseColor, int dimBrightThreshold) { m_image = QImage(); m_fillColor = baseColor; diff --git a/src/waveform/renderers/waveformmark.h b/src/waveform/renderers/waveformmark.h index 60de168e5a32..bd31ccf4427c 100644 --- a/src/waveform/renderers/waveformmark.h +++ b/src/waveform/renderers/waveformmark.h @@ -14,6 +14,13 @@ class WaveformSignalColors; class WOverview; +#ifdef MIXXX_USE_QOPENGL +class QOpenGLTexture; +namespace qopengl { +class WaveformRenderMark; +} +#endif + class WaveformMark { public: WaveformMark( @@ -22,6 +29,7 @@ class WaveformMark { const SkinContext& context, const WaveformSignalColors& signalColors, int hotCue = Cue::kNoHotCue); + ~WaveformMark(); // Disable copying WaveformMark(const WaveformMark&) = delete; @@ -101,6 +109,10 @@ class WaveformMark { std::unique_ptr m_pPositionCO; std::unique_ptr m_pEndPositionCO; std::unique_ptr m_pVisibleCO; +#ifdef MIXXX_USE_QOPENGL + std::unique_ptr m_pTexture; // used by qopengl::WaveformRenderMark + friend class qopengl::WaveformRenderMark; +#endif int m_iHotCue; QImage m_image; diff --git a/src/waveform/renderers/waveformmarkrange.h b/src/waveform/renderers/waveformmarkrange.h index a0009ed18ad6..baa35d76e2bf 100644 --- a/src/waveform/renderers/waveformmarkrange.h +++ b/src/waveform/renderers/waveformmarkrange.h @@ -13,6 +13,12 @@ QT_FORWARD_DECLARE_CLASS(QDomNode); class SkinContext; class WaveformSignalColors; +#ifdef MIXXX_USE_QOPENGL +namespace qopengl { +class WaveformRenderMarkRange; +} +#endif + class WaveformMarkRange { public: WaveformMarkRange( @@ -71,5 +77,8 @@ class WaveformMarkRange { DurationTextLocation m_durationTextLocation; friend class WaveformRenderMarkRange; +#ifdef MIXXX_USE_QOPENGL + friend class qopengl::WaveformRenderMarkRange; +#endif friend class WOverview; }; diff --git a/src/waveform/renderers/waveformrendererabstract.h b/src/waveform/renderers/waveformrendererabstract.h index 2f2f6987185e..5f003470dc89 100644 --- a/src/waveform/renderers/waveformrendererabstract.h +++ b/src/waveform/renderers/waveformrendererabstract.h @@ -9,6 +9,12 @@ QT_FORWARD_DECLARE_CLASS(QPainter) class SkinContext; class WaveformWidgetRenderer; +#ifdef MIXXX_USE_QOPENGL +namespace qopengl { +class IWaveformRenderer; +} +#endif + class WaveformRendererAbstract { public: explicit WaveformRendererAbstract( @@ -22,6 +28,12 @@ class WaveformRendererAbstract { virtual void onResize() {} virtual void onSetTrack() {} +#ifdef MIXXX_USE_QOPENGL + virtual qopengl::IWaveformRenderer* qopenglWaveformRenderer() { + return nullptr; + } +#endif + protected: bool isDirty() const { return m_dirty; diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index 8f9c78049622..a7186ef400cb 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -38,6 +38,10 @@ #include "waveform/widgets/qtwaveformwidget.h" #include "waveform/widgets/rgbwaveformwidget.h" #include "waveform/widgets/softwarewaveformwidget.h" +#include "waveform/widgets/waveformwidgetabstract.h" +#ifdef MIXXX_USE_QOPENGL +#include "waveform/widgets/qopengl/rgbwaveformwidget.h" +#endif #include "widget/wvumeter.h" #include "widget/wvumetergl.h" #include "widget/wwaveformviewer.h" @@ -678,7 +682,17 @@ void WaveformWidgetFactory::render() { if (!shouldRenderWaveforms[static_cast(i)]) { continue; } +#ifdef MIXXX_USE_QOPENGL + qopengl::IWaveformWidget* qopenglWaveformWidget = + pWaveformWidget->qopenglWaveformWidget(); + if (qopenglWaveformWidget) { + qopenglWaveformWidget->renderGL(); + } else { + pWaveformWidget->render(); + } +#else pWaveformWidget->render(); +#endif //qDebug() << "render" << i << m_vsyncThread->elapsed(); } } @@ -736,6 +750,7 @@ void WaveformWidgetFactory::swap() { if (glw != nullptr) { glw->makeCurrentIfNeeded(); glw->swapBuffers(); + glw->doneCurrent(); } //qDebug() << "swap x" << m_vsyncThread->elapsed(); } @@ -902,6 +917,15 @@ void WaveformWidgetFactory::evaluateWidgets() { useOpenGLShaders = QtRGBWaveformWidget::useOpenGLShaders(); developerOnly = QtRGBWaveformWidget::developerOnly(); break; +#ifdef MIXXX_USE_QOPENGL + case WaveformWidgetType::QOpenGLRGBWaveform: + widgetName = qopengl::RGBWaveformWidget::getWaveformWidgetName(); + useOpenGl = qopengl::RGBWaveformWidget::useOpenGl(); + useOpenGles = qopengl::RGBWaveformWidget::useOpenGles(); + useOpenGLShaders = qopengl::RGBWaveformWidget::useOpenGLShaders(); + developerOnly = qopengl::RGBWaveformWidget::developerOnly(); + break; +#endif default: DEBUG_ASSERT(!"Unexpected WaveformWidgetType"); continue; @@ -1008,6 +1032,11 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createWaveformWidget( case WaveformWidgetType::QtRGBWaveform: widget = new QtRGBWaveformWidget(viewer->getGroup(), viewer); break; +#ifdef MIXXX_USE_QOPENGL + case WaveformWidgetType::QOpenGLRGBWaveform: + widget = new qopengl::RGBWaveformWidget(viewer->getGroup(), viewer); + break; +#endif default: //case WaveformWidgetType::SoftwareSimpleWaveform: TODO: (vrince) //case WaveformWidgetType::EmptyWaveform: diff --git a/src/waveform/widgets/qopengl/iwaveformwidget.h b/src/waveform/widgets/qopengl/iwaveformwidget.h new file mode 100644 index 000000000000..2ed9ebe4527a --- /dev/null +++ b/src/waveform/widgets/qopengl/iwaveformwidget.h @@ -0,0 +1,10 @@ +#pragma once + +namespace qopengl { +class IWaveformWidget; +} + +class qopengl::IWaveformWidget { + public: + virtual void renderGL() = 0; +}; diff --git a/src/waveform/widgets/qopengl/rgbwaveformwidget.cpp b/src/waveform/widgets/qopengl/rgbwaveformwidget.cpp new file mode 100644 index 000000000000..dbbe742cb92d --- /dev/null +++ b/src/waveform/widgets/qopengl/rgbwaveformwidget.cpp @@ -0,0 +1,36 @@ +#include "waveform/widgets/qopengl/rgbwaveformwidget.h" + +#include "waveform/renderers/qopengl/waveformrenderbackground.h" +#include "waveform/renderers/qopengl/waveformrenderbeat.h" +#include "waveform/renderers/qopengl/waveformrendererendoftrack.h" +#include "waveform/renderers/qopengl/waveformrendererpreroll.h" +#include "waveform/renderers/qopengl/waveformrendererrgb.h" +#include "waveform/renderers/qopengl/waveformrendermark.h" +#include "waveform/renderers/qopengl/waveformrendermarkrange.h" +#include "waveform/widgets/qopengl/moc_rgbwaveformwidget.cpp" + +using namespace qopengl; + +RGBWaveformWidget::RGBWaveformWidget(const QString& group, QWidget* parent) + : WaveformWidget(group, parent) { + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + + m_initSuccess = init(); +} + +RGBWaveformWidget::~RGBWaveformWidget() { +} + +void RGBWaveformWidget::castToQWidget() { + m_widget = this; +} + +void RGBWaveformWidget::paintEvent(QPaintEvent* event) { + Q_UNUSED(event); +} diff --git a/src/waveform/widgets/qopengl/rgbwaveformwidget.h b/src/waveform/widgets/qopengl/rgbwaveformwidget.h new file mode 100644 index 000000000000..b14bbdf2d86b --- /dev/null +++ b/src/waveform/widgets/qopengl/rgbwaveformwidget.h @@ -0,0 +1,43 @@ +#pragma once + +#include "waveform/widgets/qopengl/waveformwidget.h" + +class WaveformWidgetFactory; + +namespace qopengl { +class RGBWaveformWidget; +} + +class qopengl::RGBWaveformWidget : public qopengl::WaveformWidget { + Q_OBJECT + public: + ~RGBWaveformWidget() override; + + WaveformWidgetType::Type getType() const override { + return WaveformWidgetType::QOpenGLRGBWaveform; + } + + static inline QString getWaveformWidgetName() { + return tr("RGB (QOpenGL)"); + } + static inline bool useOpenGl() { + return true; + } + static inline bool useOpenGles() { + return false; + } + static inline bool useOpenGLShaders() { + return true; + } + static inline bool developerOnly() { + return false; + } + + protected: + void castToQWidget() override; + void paintEvent(QPaintEvent* event) override; + + private: + RGBWaveformWidget(const QString& group, QWidget* parent); + friend class ::WaveformWidgetFactory; +}; diff --git a/src/waveform/widgets/qopengl/waveformwidget.cpp b/src/waveform/widgets/qopengl/waveformwidget.cpp new file mode 100644 index 000000000000..5415a573b5de --- /dev/null +++ b/src/waveform/widgets/qopengl/waveformwidget.cpp @@ -0,0 +1,50 @@ +#include "waveform/widgets/qopengl/waveformwidget.h" + +#include "waveform/renderers/qopengl/iwaveformrenderer.h" +#include "widget/wwaveformviewer.h" + +using namespace qopengl; + +WaveformWidget::WaveformWidget(const QString& group, QWidget* parent) + : WGLWidget(parent), WaveformWidgetAbstract(group) { +} + +WaveformWidget::~WaveformWidget() { + makeCurrentIfNeeded(); + for (int i = 0; i < m_rendererStack.size(); ++i) { + delete m_rendererStack[i]; + } + m_rendererStack.clear(); + doneCurrent(); +} + +void WaveformWidget::renderGL() { + makeCurrentIfNeeded(); + + if (shouldOnlyDrawBackground()) { + if (!m_rendererStack.empty()) { + m_rendererStack[0]->qopenglWaveformRenderer()->renderGL(); + } + } else { + for (int i = 0; i < m_rendererStack.size(); ++i) { + m_rendererStack[i]->qopenglWaveformRenderer()->renderGL(); + } + } + doneCurrent(); +} + +void WaveformWidget::initializeGL() { + makeCurrentIfNeeded(); + for (int i = 0; i < m_rendererStack.size(); ++i) { + m_rendererStack[i]->qopenglWaveformRenderer()->initializeOpenGLFunctions(); + m_rendererStack[i]->qopenglWaveformRenderer()->initializeGL(); + } + doneCurrent(); +} + +void WaveformWidget::handleEventFromWindow(QEvent* ev) { + auto viewer = dynamic_cast(parent()); + if (viewer) { + viewer->handleEventFromWindow(ev); + } +} diff --git a/src/waveform/widgets/qopengl/waveformwidget.h b/src/waveform/widgets/qopengl/waveformwidget.h new file mode 100644 index 000000000000..4535289076ae --- /dev/null +++ b/src/waveform/widgets/qopengl/waveformwidget.h @@ -0,0 +1,39 @@ +#pragma once + +#include "waveform/widgets/qopengl/iwaveformwidget.h" +#include "waveform/widgets/waveformwidgetabstract.h" +#include "widget/wglwidget.h" + +class WWaveformViewer; + +namespace qopengl { +class WaveformWidget; +} + +class qopengl::WaveformWidget : public ::WGLWidget, + public ::WaveformWidgetAbstract, + public qopengl::IWaveformWidget { + Q_OBJECT + public: + explicit WaveformWidget(const QString& group, QWidget* parent); + ~WaveformWidget() override; + + qopengl::IWaveformWidget* qopenglWaveformWidget() override { + return this; + } + + // override for IWaveformWidget + void renderGL() override; + + // overrides for WGLWidget + void initializeGL() override; + + virtual WGLWidget* getGLWidget() override { + return this; + } + + private: + // We need to forward events coming from the QOpenGLWindow + // (drag&drop, mouse) to the viewer + void handleEventFromWindow(QEvent* ev) override; +}; diff --git a/src/waveform/widgets/waveformwidgetabstract.h b/src/waveform/widgets/waveformwidgetabstract.h index 0276184f3a61..b607b74847e9 100644 --- a/src/waveform/widgets/waveformwidgetabstract.h +++ b/src/waveform/widgets/waveformwidgetabstract.h @@ -8,6 +8,12 @@ #include "waveformwidgettype.h" #include "widget/wglwidget.h" +#ifdef MIXXX_USE_QOPENGL +namespace qopengl { +class IWaveformWidget; +} +#endif + class VSyncThread; // NOTE(vRince) This class represent objects the waveformwidgetfactory can @@ -30,6 +36,14 @@ class WaveformWidgetAbstract : public WaveformWidgetRenderer { return nullptr; } +#ifdef MIXXX_USE_QOPENGL + // Derived classes that implement the IWaveformWidget + // interface should return this + virtual qopengl::IWaveformWidget* qopenglWaveformWidget() { + return nullptr; + } +#endif + void hold(); void release(); diff --git a/src/waveform/widgets/waveformwidgettype.h b/src/waveform/widgets/waveformwidgettype.h index aeddeb4f8beb..5f15e8f4025c 100644 --- a/src/waveform/widgets/waveformwidgettype.h +++ b/src/waveform/widgets/waveformwidgettype.h @@ -6,22 +6,25 @@ class WaveformWidgetType { // The order must not be changed because the waveforms are referenced // from the sorted preferences by a number. EmptyWaveform = 0, - SoftwareSimpleWaveform, // 1 TODO - SoftwareWaveform, // 2 Filtered - QtSimpleWaveform, // 3 Simple Qt - QtWaveform, // 4 Filtered Qt - GLSimpleWaveform, // 5 Simple GL - GLFilteredWaveform, // 6 Filtered GL - GLSLFilteredWaveform, // 7 Filtered GLSL - HSVWaveform, // 8 HSV - GLVSyncTest, // 9 VSync GL - RGBWaveform, // 10 RGB - GLRGBWaveform, // 11 RGB GL - GLSLRGBWaveform, // 12 RGB GLSL - QtVSyncTest, // 13 VSync Qt - QtHSVWaveform, // 14 HSV Qt - QtRGBWaveform, // 15 RGB Qt - GLSLRGBStackedWaveform, // 16 RGB Stacked - Count_WaveformwidgetType // 17 Also used as invalid value + SoftwareSimpleWaveform, // 1 TODO + SoftwareWaveform, // 2 Filtered + QtSimpleWaveform, // 3 Simple Qt + QtWaveform, // 4 Filtered Qt + GLSimpleWaveform, // 5 Simple GL + GLFilteredWaveform, // 6 Filtered GL + GLSLFilteredWaveform, // 7 Filtered GLSL + HSVWaveform, // 8 HSV + GLVSyncTest, // 9 VSync GL + RGBWaveform, // 10 RGB + GLRGBWaveform, // 11 RGB GL + GLSLRGBWaveform, // 12 RGB GLSL + QtVSyncTest, // 13 VSync Qt + QtHSVWaveform, // 14 HSV Qt + QtRGBWaveform, // 15 RGB Qt + GLSLRGBStackedWaveform, // 16 RGB Stacked +#ifdef MIXXX_USE_QOPENGL + QOpenGLRGBWaveform, // 17 RGB (QOpenGL) +#endif + Count_WaveformwidgetType // Also used as invalid value }; }; diff --git a/src/widget/paintable.cpp b/src/widget/paintable.cpp index 18af669b1621..221d709b84ea 100644 --- a/src/widget/paintable.cpp +++ b/src/widget/paintable.cpp @@ -134,6 +134,10 @@ QRectF Paintable::rect() const { return QRectF(); } +QImage Paintable::toImage() const { + return m_pPixmap.isNull() ? QImage() : m_pPixmap->toImage(); +} + void Paintable::draw(const QRectF& targetRect, QPainter* pPainter) { // The sourceRect is implicitly the entire Paintable. draw(targetRect, pPainter, rect()); diff --git a/src/widget/paintable.h b/src/widget/paintable.h index 01126a2e07ea..3dcb185cdf77 100644 --- a/src/widget/paintable.h +++ b/src/widget/paintable.h @@ -34,6 +34,7 @@ class Paintable { int width() const; int height() const; QRectF rect() const; + QImage toImage() const; DrawMode drawMode() const { return m_drawMode; } diff --git a/src/widget/qopengl/wvumetergl.cpp b/src/widget/qopengl/wvumetergl.cpp new file mode 100644 index 000000000000..66f414514a32 --- /dev/null +++ b/src/widget/qopengl/wvumetergl.cpp @@ -0,0 +1,388 @@ +#include "widget/qopengl/wvumetergl.h" + +#include "util/math.h" +#include "util/timer.h" +#include "util/widgethelper.h" +#include "waveform/vsyncthread.h" +#include "widget/qopengl/moc_wvumetergl.cpp" +#include "widget/wpixmapstore.h" + +#define DEFAULT_FALLTIME 20 +#define DEFAULT_FALLSTEP 1 +#define DEFAULT_HOLDTIME 400 +#define DEFAULT_HOLDSIZE 5 + +WVuMeterGL::WVuMeterGL(QWidget* parent) + : WGLWidget(parent), + WBaseWidget(this), + m_iRendersPending(0), + m_bSwapNeeded(false), + m_dParameter(0), + m_dPeakParameter(0), + m_dLastParameter(0), + m_dLastPeakParameter(0), + m_iPixmapLength(0), + m_bHorizontal(false), + m_iPeakHoldSize(0), + m_iPeakFallStep(0), + m_iPeakHoldTime(0), + m_iPeakFallTime(0), + m_dPeakHoldCountdownMs(0) { +} + +WVuMeterGL::~WVuMeterGL() { + makeCurrentIfNeeded(); + m_pTextureBack.reset(); + m_pTextureVu.reset(); + doneCurrent(); +} + +void WVuMeterGL::setup(const QDomNode& node, const SkinContext& context) { + // Set pixmaps + bool bHorizontal = false; + (void)context.hasNodeSelectBool(node, "Horizontal", &bHorizontal); + + // Set background pixmap if available + QDomElement backPathNode = context.selectElement(node, "PathBack"); + if (!backPathNode.isNull()) { + // The implicit default in <1.12.0 was FIXED so we keep it for backwards + // compatibility. + setPixmapBackground( + context.getPixmapSource(backPathNode), + context.selectScaleMode(backPathNode, Paintable::FIXED), + context.getScaleFactor()); + } + + QDomElement vuNode = context.selectElement(node, "PathVu"); + // The implicit default in <1.12.0 was FIXED so we keep it for backwards + // compatibility. + setPixmaps(context.getPixmapSource(vuNode), + bHorizontal, + context.selectScaleMode(vuNode, Paintable::FIXED), + context.getScaleFactor()); + + m_iPeakHoldSize = context.selectInt(node, "PeakHoldSize"); + if (m_iPeakHoldSize < 0 || m_iPeakHoldSize > 100) { + m_iPeakHoldSize = DEFAULT_HOLDSIZE; + } + + m_iPeakFallStep = context.selectInt(node, "PeakFallStep"); + if (m_iPeakFallStep < 1 || m_iPeakFallStep > 1000) { + m_iPeakFallStep = DEFAULT_FALLSTEP; + } + + m_iPeakHoldTime = context.selectInt(node, "PeakHoldTime"); + if (m_iPeakHoldTime < 1 || m_iPeakHoldTime > 3000) { + m_iPeakHoldTime = DEFAULT_HOLDTIME; + } + + m_iPeakFallTime = context.selectInt(node, "PeakFallTime"); + if (m_iPeakFallTime < 1 || m_iPeakFallTime > 1000) { + m_iPeakFallTime = DEFAULT_FALLTIME; + } + + setFocusPolicy(Qt::NoFocus); +} + +void WVuMeterGL::setPixmapBackground( + const PixmapSource& source, + Paintable::DrawMode mode, + double scaleFactor) { + m_pPixmapBack = WPixmapStore::getPaintable(source, mode, scaleFactor); + if (m_pPixmapBack.isNull()) { + qDebug() << metaObject()->className() + << "Error loading background pixmap:" << source.getPath(); + } else if (mode == Paintable::FIXED) { + setFixedSize(m_pPixmapBack->size()); + } +} + +void WVuMeterGL::setPixmaps(const PixmapSource& source, + bool bHorizontal, + Paintable::DrawMode mode, + double scaleFactor) { + m_pPixmapVu = WPixmapStore::getPaintable(source, mode, scaleFactor); + if (m_pPixmapVu.isNull()) { + qDebug() << "WVuMeterGL: Error loading vu pixmap" << source.getPath(); + } else { + m_bHorizontal = bHorizontal; + if (m_bHorizontal) { + m_iPixmapLength = m_pPixmapVu->width(); + } else { + m_iPixmapLength = m_pPixmapVu->height(); + } + } +} + +void WVuMeterGL::onConnectedControlChanged(double dParameter, double dValue) { + Q_UNUSED(dValue); + m_dParameter = math_clamp(dParameter, 0.0, 1.0); + + if (dParameter > 0.0) { + setPeak(dParameter); + } else { + // A 0.0 value is very unlikely except when the VU Meter is disabled + m_dPeakParameter = 0; + } +} + +void WVuMeterGL::setPeak(double parameter) { + if (parameter > m_dPeakParameter) { + m_dPeakParameter = parameter; + m_dPeakHoldCountdownMs = m_iPeakHoldTime; + } +} + +void WVuMeterGL::updateState(mixxx::Duration elapsed) { + double msecsElapsed = elapsed.toDoubleMillis(); + // If we're holding at a peak then don't update anything + m_dPeakHoldCountdownMs -= msecsElapsed; + if (m_dPeakHoldCountdownMs > 0) { + return; + } else { + m_dPeakHoldCountdownMs = 0; + } + + // Otherwise, decrement the peak position by the fall step size times the + // milliseconds elapsed over the fall time multiplier. The peak will fall + // FallStep times (out of 128 steps) every FallTime milliseconds. + m_dPeakParameter -= static_cast(m_iPeakFallStep) * + msecsElapsed / + static_cast(m_iPeakFallTime * m_iPixmapLength); + m_dPeakParameter = math_clamp(m_dPeakParameter, 0.0, 1.0); +} + +void WVuMeterGL::paintEvent(QPaintEvent* e) { + Q_UNUSED(e); +} + +void WVuMeterGL::showEvent(QShowEvent* e) { + Q_UNUSED(e); + WGLWidget::showEvent(e); + // Find the base color recursively in parent widget. + m_qBgColor = mixxx::widgethelper::findBaseColor(this); + // Force a rerender when the openglwindow is exposed. + // 2 pendings renders, in case we have triple buffering + m_iRendersPending = 2; +} + +void WVuMeterGL::initializeGL() { + if (m_pPixmapBack.isNull()) { + m_pTextureBack.reset(); + } else { + m_pTextureBack.reset(new QOpenGLTexture(m_pPixmapBack->toImage())); + m_pTextureBack->setMinificationFilter(QOpenGLTexture::Linear); + m_pTextureBack->setMagnificationFilter(QOpenGLTexture::Linear); + m_pTextureBack->setWrapMode(QOpenGLTexture::ClampToBorder); + } + + if (m_pPixmapVu.isNull()) { + m_pTextureVu.reset(); + } else { + m_pTextureVu.reset(new QOpenGLTexture(m_pPixmapVu->toImage())); + m_pTextureVu->setMinificationFilter(QOpenGLTexture::Linear); + m_pTextureVu->setMagnificationFilter(QOpenGLTexture::Linear); + m_pTextureVu->setWrapMode(QOpenGLTexture::ClampToBorder); + } + + QString vertexShaderCode = + "\ +uniform mat4 matrix;\n\ +attribute vec4 position;\n\ +attribute vec3 texcoor;\n\ +varying vec3 vTexcoor;\n\ +void main()\n\ +{\n\ + vTexcoor = texcoor;\n\ + gl_Position = matrix * position;\n\ +}\n"; + + QString fragmentShaderCode = + "\ +uniform sampler2D sampler;\n\ +varying vec3 vTexcoor;\n\ +void main()\n\ +{\n\ + gl_FragColor = texture2D(sampler, vec2(vTexcoor.x, vTexcoor.y));\n\ +}\n"; + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderCode)) { + return; + } + + if (!m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderCode)) { + return; + } + + if (!m_shaderProgram.link()) { + return; + } + + if (!m_shaderProgram.bind()) { + return; + } +} + +void WVuMeterGL::render(VSyncThread* vSyncThread) { + ScopedTimer t("WVuMeterGL::render"); + + updateState(vSyncThread->sinceLastSwap()); + + if (m_dParameter != m_dLastParameter || m_dPeakParameter != m_dLastPeakParameter) { + m_iRendersPending = 2; + } + + if (m_iRendersPending == 0 || !shouldRender()) { + return; + } + + makeCurrentIfNeeded(); + drawNativeGL(); + doneCurrent(); +} + +void WVuMeterGL::drawNativeGL() { + glClearColor(m_qBgColor.redF(), m_qBgColor.greenF(), m_qBgColor.blueF(), 1.f); + glClear(GL_COLOR_BUFFER_BIT); + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + m_shaderProgram.bind(); + + QMatrix4x4 matrix; + matrix.ortho(QRectF(0, 0, width(), height())); + + int matrixLocation = m_shaderProgram.uniformLocation("matrix"); + + m_shaderProgram.setUniformValue(matrixLocation, matrix); + + if (m_pTextureBack) { + // Draw background. + QRectF sourceRect(0, 0, m_pPixmapBack->width(), m_pPixmapBack->height()); + drawTexture(m_pTextureBack.get(), rect(), sourceRect); + } + + const double widgetWidth = width(); + const double widgetHeight = height(); + + if (m_pTextureVu) { + const double pixmapWidth = m_pTextureVu->width(); + const double pixmapHeight = m_pTextureVu->height(); + if (m_bHorizontal) { + const double widgetPosition = math_clamp(widgetWidth * m_dParameter, 0.0, widgetWidth); + QRectF targetRect(0, 0, widgetPosition, widgetHeight); + + const double pixmapPosition = math_clamp( + pixmapWidth * m_dParameter, 0.0, pixmapWidth); + QRectF sourceRect(0, 0, pixmapPosition, pixmapHeight); + drawTexture(m_pTextureVu.get(), targetRect, sourceRect); + + if (m_iPeakHoldSize > 0 && m_dPeakParameter > 0.0 && + m_dPeakParameter > m_dParameter) { + const double widgetPeakPosition = math_clamp( + widgetWidth * m_dPeakParameter, 0.0, widgetWidth); + const double pixmapPeakHoldSize = static_cast(m_iPeakHoldSize); + const double widgetPeakHoldSize = widgetWidth * pixmapPeakHoldSize / pixmapWidth; + + QRectF targetRect(widgetPeakPosition - widgetPeakHoldSize, + 0, + widgetPeakHoldSize, + widgetHeight); + + const double pixmapPeakPosition = math_clamp( + pixmapWidth * m_dPeakParameter, 0.0, pixmapWidth); + + QRectF sourceRect = + QRectF(pixmapPeakPosition - pixmapPeakHoldSize, + 0, + pixmapPeakHoldSize, + pixmapHeight); + drawTexture(m_pTextureVu.get(), targetRect, sourceRect); + } + } else { + // vertical + const double widgetPosition = + math_clamp(widgetHeight * m_dParameter, 0.0, widgetHeight); + QRectF targetRect(0, widgetHeight - widgetPosition, widgetWidth, widgetPosition); + + const double pixmapPosition = math_clamp( + pixmapHeight * m_dParameter, 0.0, pixmapHeight); + QRectF sourceRect(0, pixmapHeight - pixmapPosition, pixmapWidth, pixmapPosition); + drawTexture(m_pTextureVu.get(), targetRect, sourceRect); + + if (m_iPeakHoldSize > 0 && m_dPeakParameter > 0.0 && + m_dPeakParameter > m_dParameter) { + const double widgetPeakPosition = math_clamp( + widgetHeight * m_dPeakParameter, 0.0, widgetHeight); + const double pixmapPeakHoldSize = static_cast(m_iPeakHoldSize); + const double widgetPeakHoldSize = widgetHeight * pixmapPeakHoldSize / pixmapHeight; + + QRectF targetRect(0, + widgetHeight - widgetPeakPosition, + widgetWidth, + widgetPeakHoldSize); + + const double pixmapPeakPosition = math_clamp( + pixmapHeight * m_dPeakParameter, 0.0, pixmapHeight); + + QRectF sourceRect = QRectF(0, + pixmapHeight - pixmapPeakPosition, + pixmapWidth, + pixmapPeakHoldSize); + drawTexture(m_pTextureVu.get(), targetRect, sourceRect); + } + } + } + + m_dLastParameter = m_dParameter; + m_dLastPeakParameter = m_dPeakParameter; + m_iRendersPending--; + m_bSwapNeeded = true; +} + +void WVuMeterGL::swap() { + // TODO @m0dB move shouldRender outside? + if (!m_bSwapNeeded || !shouldRender()) { + return; + } + makeCurrentIfNeeded(); + swapBuffers(); + doneCurrent(); + m_bSwapNeeded = false; +} + +void WVuMeterGL::drawTexture(QOpenGLTexture* texture, + const QRectF& targetRect, + const QRectF& sourceRect) { + const float texx1 = sourceRect.x() / texture->width(); + const float texy1 = sourceRect.y() / texture->height(); + const float texx2 = (sourceRect.x() + sourceRect.width()) / texture->width(); + const float texy2 = (sourceRect.y() + sourceRect.height()) / texture->height(); + + const float posx1 = targetRect.x(); + const float posy1 = targetRect.y(); + const float posx2 = targetRect.x() + targetRect.width(); + const float posy2 = targetRect.y() + targetRect.height(); + + const float posarray[] = {posx1, posy1, posx2, posy1, posx1, posy2, posx2, posy2}; + const float texarray[] = {texx1, texy1, texx2, texy1, texx1, texy2, texx2, texy2}; + + int samplerLocation = m_shaderProgram.uniformLocation("sampler"); + int positionLocation = m_shaderProgram.attributeLocation("position"); + int texcoordLocation = m_shaderProgram.attributeLocation("texcoor"); + + m_shaderProgram.enableAttributeArray(positionLocation); + m_shaderProgram.setAttributeArray( + positionLocation, GL_FLOAT, posarray, 2); + m_shaderProgram.enableAttributeArray(texcoordLocation); + m_shaderProgram.setAttributeArray( + texcoordLocation, GL_FLOAT, texarray, 2); + + m_shaderProgram.setUniformValue(samplerLocation, 0); + + texture->bind(); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} diff --git a/src/widget/qopengl/wvumetergl.h b/src/widget/qopengl/wvumetergl.h new file mode 100644 index 000000000000..2f61341eba4d --- /dev/null +++ b/src/widget/qopengl/wvumetergl.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +#include "skin/legacy/skincontext.h" +#include "util/duration.h" +#include "widget/wglwidget.h" +#include "widget/wpixmapstore.h" +#include "widget/wwidget.h" + +class VSyncThread; + +class WVuMeterGL : public WGLWidget, public WBaseWidget { + Q_OBJECT + public: + explicit WVuMeterGL(QWidget* parent = nullptr); + ~WVuMeterGL() override; + + void setup(const QDomNode& node, const SkinContext& context); + void setPixmapBackground( + const PixmapSource& source, + Paintable::DrawMode mode, + double scaleFactor); + void setPixmaps( + const PixmapSource& source, + bool bHorizontal, + Paintable::DrawMode mode, + double scaleFactor); + void onConnectedControlChanged(double dParameter, double dValue) override; + + void initializeGL() override; + + public slots: + void render(VSyncThread* vSyncThread); + void swap(); + + protected slots: + void updateState(mixxx::Duration elapsed); + + private: + void drawNativeGL(); + void drawTexture(QOpenGLTexture* texture, const QRectF& sourceRect, const QRectF& targetRect); + + void paintEvent(QPaintEvent* /*unused*/) override; + void showEvent(QShowEvent* /*unused*/) override; + void setPeak(double parameter); + + // To make sure we render at least once even when we have no signal + int m_iRendersPending; + // To indicate that we rendered so we need to swap + bool m_bSwapNeeded; + // Current parameter and peak parameter. + double m_dParameter; + double m_dPeakParameter; + + // The last parameter and peak parameter values at the time of + // rendering. Used to check whether the widget state has changed since the + // last render in maybeUpdate. + double m_dLastParameter; + double m_dLastPeakParameter; + + // Length of the VU-meter pixmap along the relevant axis. + int m_iPixmapLength; + + // Associated pixmaps + PaintablePointer m_pPixmapBack; + PaintablePointer m_pPixmapVu; + + // True if it's a horizontal vu meter + bool m_bHorizontal; + + int m_iPeakHoldSize; + int m_iPeakFallStep; + int m_iPeakHoldTime; + int m_iPeakFallTime; + + // The peak hold time remaining in milliseconds. + double m_dPeakHoldCountdownMs; + + QColor m_qBgColor; + + std::unique_ptr m_pTextureBack; + std::unique_ptr m_pTextureVu; + QOpenGLShaderProgram m_shaderProgram; +}; diff --git a/src/widget/wspinny.cpp b/src/widget/wspinny.cpp index 21b89d20dfd6..5cd149ed9133 100644 --- a/src/widget/wspinny.cpp +++ b/src/widget/wspinny.cpp @@ -11,7 +11,6 @@ #include "control/controlproxy.h" #include "library/coverartcache.h" #include "library/coverartutils.h" -#include "moc_wspinny.cpp" #include "track/track.h" #include "util/dnd.h" #include "util/fpclassify.h" @@ -19,6 +18,7 @@ #include "vinylcontrol/vinylcontrolmanager.h" #include "waveform/visualplayposition.h" #include "waveform/vsyncthread.h" +#include "widget/moc_wspinny.cpp" #include "wimagestore.h" // The SampleBuffers format enables antialiasing. @@ -682,6 +682,7 @@ void WSpinny::showEvent(QShowEvent* event) { m_pVCManager->addSignalQualityListener(this); } #endif + WGLWidget::showEvent(event); } void WSpinny::hideEvent(QHideEvent* event) { diff --git a/src/widget/wspinny.h b/src/widget/wspinny.h index a24883ddd080..de4bc334205e 100644 --- a/src/widget/wspinny.h +++ b/src/widget/wspinny.h @@ -1,5 +1,9 @@ #pragma once +#ifdef MIXXX_USE_QOPENGL +#include "widget/qopengl/wspinny.h" +#else + #include #include #include @@ -140,3 +144,5 @@ class WSpinny : public WGLWidget, WCoverArtMenu* m_pCoverMenu; DlgCoverArtFullSize* m_pDlgCoverArt; }; + +#endif // not defined MIXXX_USE_QOPENGL diff --git a/src/widget/wvumetergl.cpp b/src/widget/wvumetergl.cpp index 5bdacac771bf..2f76e1ee2c11 100644 --- a/src/widget/wvumetergl.cpp +++ b/src/widget/wvumetergl.cpp @@ -1,11 +1,10 @@ #include "widget/wvumetergl.h" -#include "moc_wvumetergl.cpp" #include "util/math.h" #include "util/timer.h" #include "util/widgethelper.h" -#include "waveform/sharedglcontext.h" #include "waveform/vsyncthread.h" +#include "widget/moc_wvumetergl.cpp" #include "widget/wpixmapstore.h" #define DEFAULT_FALLTIME 20 @@ -29,10 +28,6 @@ WVuMeterGL::WVuMeterGL(QWidget* parent) m_iPeakHoldTime(0), m_iPeakFallTime(0), m_dPeakHoldCountdownMs(0) { - setAttribute(Qt::WA_NoSystemBackground); - setAttribute(Qt::WA_OpaquePaintEvent); - - setUpdatesEnabled(false); } void WVuMeterGL::setup(const QDomNode& node, const SkinContext& context) { @@ -159,7 +154,7 @@ void WVuMeterGL::showEvent(QShowEvent* e) { WGLWidget::showEvent(e); // Find the base color recursively in parent widget. m_qBgColor = mixxx::widgethelper::findBaseColor(this); - // 2 pendings renders, in cause we have triple buffering + // 2 pendings renders, in case we have triple buffering m_iRendersPending = 2; } diff --git a/src/widget/wvumetergl.h b/src/widget/wvumetergl.h index b5d7bb729280..3a99d804e5f5 100644 --- a/src/widget/wvumetergl.h +++ b/src/widget/wvumetergl.h @@ -1,5 +1,9 @@ #pragma once +#ifdef MIXXX_USE_QOPENGL +#include "widget/qopengl/wvumetergl.h" +#else + #include "skin/legacy/skincontext.h" #include "util/duration.h" #include "widget/wglwidget.h" @@ -71,3 +75,5 @@ class WVuMeterGL : public WGLWidget, public WBaseWidget { QColor m_qBgColor; }; + +#endif // not defined MIXXX_USE_QOPENGL