From ceca510b2f5e6bf83bde0fabcf5ac75ff0f700a1 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 --- 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/wspinny.cpp | 824 ++++++++++++++++++ src/widget/qopengl/wspinny.h | 154 ++++ 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 + 43 files changed, 3866 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/wspinny.cpp create mode 100644 src/widget/qopengl/wspinny.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 1559bef91cb..b85baecf6c3 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 00000000000..25e36e60ba0 --- /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 00000000000..5fd380a18ab --- /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 00000000000..e965e6e4dd0 --- /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 00000000000..d6633f2018b --- /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 00000000000..4fec0402eda --- /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 00000000000..f84ab9832fd --- /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 00000000000..31cfd5d925b --- /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 00000000000..71b208e8cdb --- /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 00000000000..2caf59827ec --- /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 00000000000..942e675a334 --- /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 00000000000..d390696bb9a --- /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 00000000000..ce0317b1fd8 --- /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 00000000000..67348f3d635 --- /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 00000000000..f187e8d4a68 --- /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 00000000000..1fe7461104a --- /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 00000000000..21a2c9e9006 --- /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 00000000000..515b9e6d485 --- /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 00000000000..6fe51430d04 --- /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 00000000000..5f89d79f193 --- /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 00000000000..ed2dea5fdd2 --- /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 34e5870fa0a..91f07d566f7 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 60de168e5a3..bd31ccf4427 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 a0009ed18ad..baa35d76e2b 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 2f2f6987185..5f003470dc8 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 8f9c7804962..a7186ef400c 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 00000000000..2ed9ebe4527 --- /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 00000000000..dbbe742cb92 --- /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 00000000000..b14bbdf2d86 --- /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 00000000000..5415a573b5d --- /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 00000000000..4535289076a --- /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 0276184f3a6..b607b74847e 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 aeddeb4f8be..5f15e8f4025 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 18af669b162..221d709b84e 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 01126a2e07e..3dcb185cdf7 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/wspinny.cpp b/src/widget/qopengl/wspinny.cpp new file mode 100644 index 00000000000..4051c38caf0 --- /dev/null +++ b/src/widget/qopengl/wspinny.cpp @@ -0,0 +1,824 @@ +#include "widget/wspinny.h" + +#include +#include +#include +#include +#include +#include + +#include "control/controlobject.h" +#include "control/controlproxy.h" +#include "library/coverartcache.h" +#include "library/coverartutils.h" +#include "track/track.h" +#include "util/dnd.h" +#include "util/fpclassify.h" +#include "vinylcontrol/vinylcontrol.h" +#include "vinylcontrol/vinylcontrolmanager.h" +#include "waveform/visualplayposition.h" +#include "waveform/vsyncthread.h" +#include "widget/qopengl/moc_wspinny.cpp" +#include "widget/wimagestore.h" + +WSpinny::WSpinny( + QWidget* parent, + const QString& group, + UserSettingsPointer pConfig, + VinylControlManager* pVCMan, + BaseTrackPlayer* pPlayer) + : WGLWidget(parent), + WBaseWidget(this), + m_group(group), + m_pConfig(pConfig), + m_pPlayPos(nullptr), + m_pVisualPlayPos(nullptr), + m_pTrackSamples(nullptr), + m_pTrackSampleRate(nullptr), + m_pScratchToggle(nullptr), + m_pScratchPos(nullptr), + m_pVinylControlSpeedType(nullptr), + m_pVinylControlEnabled(nullptr), + m_pSignalEnabled(nullptr), + m_pSlipEnabled(nullptr), + m_bShowCover(true), + m_dInitialPos(0.), + m_iVinylInput(-1), + m_bVinylActive(false), + m_bSignalActive(true), + m_iVinylScopeSize(0), + m_fAngle(0.0f), + m_dAngleCurrentPlaypos(-1), + m_dAngleLastPlaypos(-1), + m_fGhostAngle(0.0f), + m_dGhostAngleCurrentPlaypos(-1), + m_dGhostAngleLastPlaypos(-1), + m_iStartMouseX(-1), + m_iStartMouseY(-1), + m_iFullRotations(0), + m_dPrevTheta(0.), + m_dRotationsPerSecond(MIXXX_VINYL_SPEED_33_NUM / 60), + m_bClampFailedWarning(false), + m_bGhostPlayback(false), + m_pPlayer(pPlayer), + m_pCoverMenu(new WCoverArtMenu(this)), + m_pDlgCoverArt(new DlgCoverArtFullSize(this, pPlayer, m_pCoverMenu)) { +#ifdef __VINYLCONTROL__ + m_pVCManager = pVCMan; +#else + Q_UNUSED(pVCMan); +#endif // __VINYLCONTROL__ + //Drag and drop + setAcceptDrops(true); + qDebug() << "WSpinny(): Created WGLWidget, Context" + << "Valid:" << isContextValid() + << "Sharing:" << isContextSharing(); + makeCurrentIfNeeded(); + + CoverArtCache* pCache = CoverArtCache::instance(); + if (pCache) { + connect(pCache, + &CoverArtCache::coverFound, + this, + &WSpinny::slotCoverFound); + } + + if (m_pPlayer != nullptr) { + connect(m_pPlayer, &BaseTrackPlayer::newTrackLoaded, this, &WSpinny::slotLoadTrack); + connect(m_pPlayer, &BaseTrackPlayer::loadingTrack, this, &WSpinny::slotLoadingTrack); + // just in case a track is already loaded + slotLoadTrack(m_pPlayer->getLoadedTrack()); + } + + connect(m_pCoverMenu, &WCoverArtMenu::coverInfoSelected, this, &WSpinny::slotCoverInfoSelected); + connect(m_pCoverMenu, &WCoverArtMenu::reloadCoverArt, this, &WSpinny::slotReloadCoverArt); +} + +WSpinny::~WSpinny() { +#ifdef __VINYLCONTROL__ + m_pVCManager->removeSignalQualityListener(this); +#endif + + makeCurrentIfNeeded(); + m_pBgTexture.reset(); + m_pMaskTexture.reset(); + m_pFgTextureScaled.reset(); + m_pGhostTextureScaled.reset(); + m_pLoadedCoverTextureScaled.reset(); + m_pQTexture.reset(); + doneCurrent(); +} + +void WSpinny::onVinylSignalQualityUpdate(const VinylSignalQualityReport& report) { +#ifdef __VINYLCONTROL__ + if (!m_bVinylActive || !m_bSignalActive) { + return; + } + // Skip reports for vinyl inputs we don't care about. + if (report.processor != m_iVinylInput) { + return; + } + int r, g, b; + QColor qual_color = QColor(); + float signalQuality = report.timecode_quality; + + // color is related to signal quality + // hsv: s=1, v=1 + // h is the only variable. + // h=0 is red, h=120 is green + qual_color.setHsv(static_cast(120.0 * signalQuality), 255, 255); + qual_color.getRgb(&r, &g, &b); + + for (int y = 0; y < m_iVinylScopeSize; ++y) { + QRgb* line = reinterpret_cast(m_qImage.scanLine(y)); + for (int x = 0; x < m_iVinylScopeSize; ++x) { + // use xwax's bitmap to set alpha data only + // adjust alpha by 3/4 so it's not quite so distracting + // setpixel is slow, use scanlines instead + //m_qImage.setPixel(x, y, qRgba(r,g,b,(int)buf[x+m_iVinylScopeSize*y] * .75)); + *line = qRgba(r, g, b, static_cast(report.scope[x + m_iVinylScopeSize * y] * .75)); + line++; + } + } +#else + Q_UNUSED(report); +#endif +} + +void WSpinny::setup(const QDomNode& node, + const SkinContext& context, + const ConfigKey& showCoverConfigKey) { + // Set images + QDomElement backPathElement = context.selectElement(node, "PathBackground"); + m_pBgImage = WImageStore::getImage(context.getPixmapSource(backPathElement), + context.getScaleFactor()); + Paintable::DrawMode bgmode = context.selectScaleMode(backPathElement, + Paintable::FIXED); + if (m_pBgImage && !m_pBgImage->isNull() && bgmode == Paintable::FIXED) { + setFixedSize(m_pBgImage->size()); + } else { + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + } + m_pMaskImage = WImageStore::getImage( + context.getPixmapSource(context.selectNode(node, "PathMask")), + context.getScaleFactor()); + m_pFgImage = WImageStore::getImage( + context.getPixmapSource(context.selectNode(node, "PathForeground")), + context.getScaleFactor()); + if (m_pFgImage && !m_pFgImage->isNull()) { + m_fgImageScaled = m_pFgImage->scaled( + size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + m_pGhostImage = WImageStore::getImage( + context.getPixmapSource(context.selectNode(node, "PathGhost")), + context.getScaleFactor()); + if (m_pGhostImage && !m_pGhostImage->isNull()) { + m_ghostImageScaled = m_pGhostImage->scaled( + size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + + // Dynamic skin option, set in WSpinny's node. + if (showCoverConfigKey.isValid()) { + m_pShowCoverProxy = new ControlProxy( + showCoverConfigKey, this); + m_pShowCoverProxy->connectValueChanged( + this, + [this](double v) { + m_bShowCover = v > 0.0; + }); + m_bShowCover = m_pShowCoverProxy->get() > 0.0; + } else { + m_bShowCover = context.selectBool(node, "ShowCover", false); + } + +#ifdef __VINYLCONTROL__ + // Find the vinyl input we should listen to reports about. + if (m_pVCManager) { + m_iVinylInput = m_pVCManager->vinylInputFromGroup(m_group); + } + m_iVinylScopeSize = MIXXX_VINYL_SCOPE_SIZE; + m_qImage = QImage(m_iVinylScopeSize, m_iVinylScopeSize, QImage::Format_ARGB32); + // fill with transparent black + m_qImage.fill(qRgba(0, 0, 0, 0)); +#endif + + m_pPlayPos = new ControlProxy( + m_group, "playposition", this, ControlFlag::NoAssertIfMissing); + m_pVisualPlayPos = VisualPlayPosition::getVisualPlayPosition(m_group); + m_pTrackSamples = new ControlProxy( + m_group, "track_samples", this, ControlFlag::NoAssertIfMissing); + m_pTrackSampleRate = new ControlProxy( + m_group, "track_samplerate", this, ControlFlag::NoAssertIfMissing); + + m_pScratchToggle = new ControlProxy( + m_group, "scratch_position_enable", this, ControlFlag::NoAssertIfMissing); + m_pScratchPos = new ControlProxy( + m_group, "scratch_position", this, ControlFlag::NoAssertIfMissing); + + m_pSlipEnabled = new ControlProxy( + m_group, "slip_enabled", this, ControlFlag::NoAssertIfMissing); + m_pSlipEnabled->connectValueChanged(this, &WSpinny::updateSlipEnabled); + +#ifdef __VINYLCONTROL__ + m_pVinylControlSpeedType = new ControlProxy( + m_group, "vinylcontrol_speed_type", this, ControlFlag::NoAssertIfMissing); + // Initialize the rotational speed. + updateVinylControlSpeed(m_pVinylControlSpeedType->get()); + + m_pVinylControlEnabled = new ControlProxy( + m_group, "vinylcontrol_enabled", this, ControlFlag::NoAssertIfMissing); + m_pVinylControlEnabled->connectValueChanged(this, + &WSpinny::updateVinylControlEnabled); + + m_pSignalEnabled = new ControlProxy( + m_group, "vinylcontrol_signal_enabled", this, ControlFlag::NoAssertIfMissing); + m_pSignalEnabled->connectValueChanged(this, + &WSpinny::updateVinylControlSignalEnabled); + + // Match the vinyl control's set RPM so that the spinny widget rotates at + // the same speed as your physical decks, if you're using vinyl control. + m_pVinylControlSpeedType->connectValueChanged(this, + &WSpinny::updateVinylControlSpeed); + +#else + //if no vinyl control, just call it 33 + this->updateVinylControlSpeed(33.0); +#endif +} + +void WSpinny::slotLoadTrack(TrackPointer pTrack) { + if (m_loadedTrack) { + disconnect(m_loadedTrack.get(), + &Track::coverArtUpdated, + this, + &WSpinny::slotTrackCoverArtUpdated); + } + m_lastRequestedCover = CoverInfo(); + m_loadedCover = QPixmap(); + m_loadedCoverScaled = QPixmap(); + makeCurrentIfNeeded(); + m_pLoadedCoverTextureScaled.reset(); + doneCurrent(); + m_loadedTrack = pTrack; + if (m_loadedTrack) { + connect(m_loadedTrack.get(), + &Track::coverArtUpdated, + this, + &WSpinny::slotTrackCoverArtUpdated); + } + + slotTrackCoverArtUpdated(); +} + +void WSpinny::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) { + Q_UNUSED(pNewTrack); + if (m_loadedTrack && pOldTrack == m_loadedTrack) { + disconnect(m_loadedTrack.get(), + &Track::coverArtUpdated, + this, + &WSpinny::slotTrackCoverArtUpdated); + } + m_loadedTrack.reset(); + m_lastRequestedCover = CoverInfo(); + m_loadedCover = QPixmap(); + m_loadedCoverScaled = QPixmap(); + makeCurrentIfNeeded(); + m_pLoadedCoverTextureScaled.reset(); + doneCurrent(); + update(); +} + +void WSpinny::slotTrackCoverArtUpdated() { + if (m_loadedTrack) { + CoverArtCache::requestTrackCover(this, m_loadedTrack); + } +} + +void WSpinny::slotCoverFound( + const QObject* pRequestor, + const CoverInfo& coverInfo, + const QPixmap& pixmap, + mixxx::cache_key_t requestedCacheKey, + bool coverInfoUpdated) { + Q_UNUSED(requestedCacheKey); + Q_UNUSED(coverInfoUpdated); // CoverArtCache has taken care, updating the Track. + if (pRequestor == this && + m_loadedTrack && + m_loadedTrack->getLocation() == coverInfo.trackLocation) { + m_loadedCover = pixmap; + m_loadedCoverScaled = scaledCoverArt(pixmap); + if (!m_loadedCoverScaled.isNull()) { + makeCurrentIfNeeded(); + m_pLoadedCoverTextureScaled.reset(new QOpenGLTexture(m_loadedCoverScaled.toImage())); + doneCurrent(); + } else { + m_pLoadedCoverTextureScaled.reset(); + } + update(); + } +} + +void WSpinny::slotCoverInfoSelected(const CoverInfoRelative& coverInfo) { + if (m_loadedTrack != nullptr) { + // Will trigger slotTrackCoverArtUpdated(). + m_loadedTrack->setCoverInfo(coverInfo); + } +} + +void WSpinny::slotReloadCoverArt() { + if (!m_loadedTrack) { + return; + } + const auto future = guessTrackCoverInfoConcurrently(m_loadedTrack); + // Don't wait for the result and keep running in the background + Q_UNUSED(future) +} + +void WSpinny::paintEvent(QPaintEvent* e) { + Q_UNUSED(e); +} + +void WSpinny::render(VSyncThread* vSyncThread) { + // TODO @m0dB move outside? + if (!shouldRender()) { + return; + } + + makeCurrentIfNeeded(); + + 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_pVisualPlayPos.isNull() && vSyncThread != nullptr) { + m_pVisualPlayPos->getPlaySlipAtNextVSync( + vSyncThread, + &m_dAngleCurrentPlaypos, + &m_dGhostAngleCurrentPlaypos); + } + + if (m_pBgTexture) { + drawTexture(m_pBgTexture.get()); + } + + if (m_bShowCover && m_pLoadedCoverTextureScaled) { + drawTexture(m_pLoadedCoverTextureScaled.get()); + } + + if (m_pMaskTexture) { + drawTexture(m_pMaskTexture.get()); + } + +#ifdef __VINYLCONTROL__ + // Overlay the signal quality drawing if vinyl is active + if (m_bVinylActive && m_bSignalActive) { + // draw the last good image + drawTexture(m_pQTexture.get()); + } +#endif + + // To rotate the foreground image around the center of the image, + // we use the classic trick of translating the coordinate system such that + // the origin is at the center of the image. We then rotate the coordinate system, + // and draw the image at the corner. + //p.translate(width() / 2, height() / 2); + + bool paintGhost = m_bGhostPlayback && m_pGhostTextureScaled; + // if (paintGhost) { + // p.save(); + //} + + if (m_dAngleCurrentPlaypos != m_dAngleLastPlaypos) { + m_fAngle = static_cast(calculateAngle(m_dAngleCurrentPlaypos)); + m_dAngleLastPlaypos = m_dAngleCurrentPlaypos; + } + + if (m_dGhostAngleCurrentPlaypos != m_dGhostAngleLastPlaypos) { + m_fGhostAngle = static_cast(calculateAngle(m_dGhostAngleCurrentPlaypos)); + m_dGhostAngleLastPlaypos = m_dGhostAngleCurrentPlaypos; + } + + if (paintGhost) { + QMatrix4x4 rotate; + rotate.rotate(m_fAngle, 0, 0, -1); + m_shaderProgram.setUniformValue(matrixLocation, rotate); + + drawTexture(m_pGhostTextureScaled.get()); + } + + if (m_pFgTextureScaled) { + QMatrix4x4 rotate; + rotate.rotate(m_fAngle, 0, 0, -1); + m_shaderProgram.setUniformValue(matrixLocation, rotate); + + drawTexture(m_pFgTextureScaled.get()); + } + + doneCurrent(); +} + +void WSpinny::swap() { + // TODO @m0dB move outside? + if (!shouldRender()) { + return; + } + makeCurrentIfNeeded(); + swapBuffers(); + doneCurrent(); +} + +QPixmap WSpinny::scaledCoverArt(const QPixmap& normal) { + if (normal.isNull()) { + return QPixmap(); + } + QPixmap scaled = normal.scaled(size() * devicePixelRatioF(), + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + scaled.setDevicePixelRatio(devicePixelRatioF()); + return scaled; +} + +void WSpinny::resizeEvent(QResizeEvent* event) { + m_loadedCoverScaled = scaledCoverArt(m_loadedCover); + if (m_pFgImage && !m_pFgImage->isNull()) { + m_fgImageScaled = m_pFgImage->scaled( + size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + if (m_pGhostImage && !m_pGhostImage->isNull()) { + m_ghostImageScaled = m_pGhostImage->scaled( + size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + WGLWidget::resizeEvent(event); +} + +/* Convert between a normalized playback position (0.0 - 1.0) and an angle + in our polar coordinate system. + Returns an angle clamped between -180 and 180 degrees. */ +double WSpinny::calculateAngle(double playpos) { + double trackFrames = m_pTrackSamples->get() / 2; + double trackSampleRate = m_pTrackSampleRate->get(); + if (util_isnan(playpos) || util_isnan(trackFrames) || util_isnan(trackSampleRate) || + trackFrames <= 0 || trackSampleRate <= 0) { + return 0.0; + } + + // Convert playpos to seconds. + double t = playpos * trackFrames / trackSampleRate; + + // Bad samplerate or number of track samples. + if (util_isnan(t)) { + return 0.0; + } + + //33 RPM is approx. 0.5 rotations per second. + double angle = 360.0 * m_dRotationsPerSecond * t; + //Clamp within -180 and 180 degrees + //qDebug() << "pc:" << angle; + //angle = ((angle + 180) % 360.) - 180; + //modulo for doubles :) + const double originalAngle = angle; + if (angle > 0) { + const auto x = static_cast((angle + 180) / 360); + angle = angle - (360 * x); + } else { + const auto x = static_cast((angle - 180) / 360); + angle = angle - (360 * x); + } + + if (angle <= -180 || angle > 180) { + // Only warn once per session. This can tank performance since it prints + // like crazy. + if (!m_bClampFailedWarning) { + qDebug() << "Angle clamping failed!" << t << originalAngle << "->" << angle + << "Please file a bug or email mixxx-devel@lists.sourceforge.net"; + m_bClampFailedWarning = true; + } + return 0.0; + } + return angle; +} + +/** Given a normalized playpos, calculate the integer number of rotations + that it would take to wind the vinyl to that position. */ +int WSpinny::calculateFullRotations(double playpos) { + if (util_isnan(playpos)) { + return 0; + } + //Convert playpos to seconds. + double t = playpos * (m_pTrackSamples->get() / 2 / // Stereo audio! + m_pTrackSampleRate->get()); + + //33 RPM is approx. 0.5 rotations per second. + //qDebug() << t; + double angle = 360 * m_dRotationsPerSecond * t; + + return ((static_cast(angle) + 180) / 360); +} + +//Inverse of calculateAngle() +double WSpinny::calculatePositionFromAngle(double angle) { + if (util_isnan(angle)) { + return 0.0; + } + + //33 RPM is approx. 0.5 rotations per second. + double t = angle / (360.0 * m_dRotationsPerSecond); //time in seconds + + double trackFrames = m_pTrackSamples->get() / 2; + double trackSampleRate = m_pTrackSampleRate->get(); + if (util_isnan(trackFrames) || util_isnan(trackSampleRate) || + trackFrames <= 0 || trackSampleRate <= 0) { + return 0.0; + } + + // Convert t from seconds into a normalized playposition value. + double playpos = t * trackSampleRate / trackFrames; + if (util_isnan(playpos)) { + return 0.0; + } + return playpos; +} + +void WSpinny::updateVinylControlSpeed(double rpm) { + m_dRotationsPerSecond = rpm / 60.; +} + +void WSpinny::updateVinylControlSignalEnabled(double enabled) { +#ifdef __VINYLCONTROL__ + if (m_pVCManager == nullptr) { + return; + } + m_bSignalActive = enabled != 0; + + if (m_bSignalActive && m_iVinylInput != -1) { + m_pVCManager->addSignalQualityListener(this); + } else { + m_pVCManager->removeSignalQualityListener(this); + // fill with transparent black + m_qImage.fill(qRgba(0, 0, 0, 0)); + } +#else + Q_UNUSED(enabled); +#endif +} + +void WSpinny::updateVinylControlEnabled(double enabled) { + m_bVinylActive = enabled != 0; +} + +void WSpinny::updateSlipEnabled(double enabled) { + m_bGhostPlayback = static_cast(enabled); +} + +void WSpinny::mouseMoveEvent(QMouseEvent* e) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + int y = e->position().y(); + int x = e->position().x(); +#else + int y = e->y(); + int x = e->x(); +#endif + + // Keeping these around in case we want to switch to control relative + // to the original mouse position. + //int dX = x-m_iStartMouseX; + //int dY = y-m_iStartMouseY; + + //Coordinates from center of widget + double c_x = x - width() / 2; + double c_y = y - height() / 2; + double theta = (180.0 / M_PI) * atan2(c_x, -c_y); + + //qDebug() << "c_x:" << c_x << "c_y:" << c_y << + // "dX:" << dX << "dY:" << dY; + + // When we finish one full rotation (clockwise or anticlockwise), + // we'll need to manually add/sub 360 degrees because atan2()'s range is + // only within -180 to 180 degrees. We need a wider range so your position + // in the song can be tracked. + if (m_dPrevTheta > 100 && theta < 0) { + m_iFullRotations++; + } else if (m_dPrevTheta < -100 && theta > 0) { + m_iFullRotations--; + } + + m_dPrevTheta = theta; + theta += m_iFullRotations * 360; + + //qDebug() << "c t:" << theta << "pt:" << m_dPrevTheta << + // "icr" << m_iFullRotations; + + if (((e->buttons() & Qt::LeftButton) || (e->buttons() & Qt::RightButton)) && + !m_bVinylActive) { + //Convert deltaTheta into a percentage of song length. + double absPos = calculatePositionFromAngle(theta); + double absPosInSamples = absPos * m_pTrackSamples->get(); + m_pScratchPos->set(absPosInSamples - m_dInitialPos); + } else if (e->buttons() & Qt::MiddleButton) { + } else if (e->buttons() & Qt::NoButton) { + setCursor(QCursor(Qt::OpenHandCursor)); + } +} + +void WSpinny::mousePressEvent(QMouseEvent* e) { + if (m_loadedTrack == nullptr) { + return; + } + + if (m_pDlgCoverArt->isVisible()) { + m_pDlgCoverArt->close(); + return; + } + + if (m_pCoverMenu->isVisible()) { + m_pCoverMenu->close(); + return; + } + + if (e->button() == Qt::LeftButton) { + int y = e->y(); + int x = e->x(); + + m_iStartMouseX = x; + m_iStartMouseY = y; + + //don't do anything if vinyl control is active + if (m_bVinylActive) { + return; + } + + if (e->button() == Qt::LeftButton || e->button() == Qt::RightButton) { + QApplication::setOverrideCursor(QCursor(Qt::ClosedHandCursor)); + + // Coordinates from center of widget + double c_x = x - width() / 2; + double c_y = y - height() / 2; + double theta = (180.0 / M_PI) * atan2(c_x, -c_y); + m_dPrevTheta = theta; + m_iFullRotations = calculateFullRotations(m_pPlayPos->get()); + theta += m_iFullRotations * 360.0; + m_dInitialPos = calculatePositionFromAngle(theta) * m_pTrackSamples->get(); + + m_pScratchPos->set(0); + m_pScratchToggle->set(1.0); + + // Trigger a mouse move to immediately line up the vinyl with the cursor + mouseMoveEvent(e); + } + } else { + if (!m_loadedCover.isNull()) { + m_pDlgCoverArt->init(m_loadedTrack); + } else if (!m_pDlgCoverArt->isVisible() && m_bShowCover) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + m_pCoverMenu->popup(e->globalPosition().toPoint()); +#else + m_pCoverMenu->popup(e->globalPos()); +#endif + } + } +} + +void WSpinny::mouseReleaseEvent(QMouseEvent* e) { + if (e->button() == Qt::LeftButton || e->button() == Qt::RightButton) { + QApplication::restoreOverrideCursor(); + m_pScratchToggle->set(0.0); + m_iFullRotations = 0; + } +} + +void WSpinny::showEvent(QShowEvent* event) { + Q_UNUSED(event); + WGLWidget::showEvent(event); +#ifdef __VINYLCONTROL__ + // If we want to draw the VC signal on this widget then register for + // updates. + if (m_bSignalActive && m_iVinylInput != -1 && m_pVCManager) { + m_pVCManager->addSignalQualityListener(this); + } +#endif + WGLWidget::showEvent(event); +} + +void WSpinny::hideEvent(QHideEvent* event) { + Q_UNUSED(event); +#ifdef __VINYLCONTROL__ + // When we are hidden we do not want signal quality updates. + if (m_pVCManager) { + m_pVCManager->removeSignalQualityListener(this); + } +#endif + // fill with transparent black + m_qImage.fill(qRgba(0, 0, 0, 0)); +} + +bool WSpinny::event(QEvent* pEvent) { + if (pEvent->type() == QEvent::ToolTip) { + updateTooltip(); + } + return WGLWidget::event(pEvent); +} + +void WSpinny::dragEnterEvent(QDragEnterEvent* event) { + DragAndDropHelper::handleTrackDragEnterEvent(event, m_group, m_pConfig); +} + +void WSpinny::dropEvent(QDropEvent* event) { + DragAndDropHelper::handleTrackDropEvent(event, *this, m_group, m_pConfig); +} + +void WSpinny::initializeGL() { + if (m_pBgImage && !m_pBgImage->isNull()) + m_pBgTexture.reset(new QOpenGLTexture(*m_pBgImage)); + if (m_pMaskImage && !m_pMaskImage->isNull()) + m_pMaskTexture.reset(new QOpenGLTexture(*m_pMaskImage)); + if (!m_fgImageScaled.isNull()) + m_pFgTextureScaled.reset(new QOpenGLTexture(m_fgImageScaled)); + if (!m_ghostImageScaled.isNull()) + m_pGhostTextureScaled.reset(new QOpenGLTexture(m_ghostImageScaled)); + if (!m_loadedCoverScaled.isNull()) { + m_pLoadedCoverTextureScaled.reset(new QOpenGLTexture(m_loadedCoverScaled.toImage())); + } + if (!m_qImage.isNull()) + m_pQTexture.reset(new QOpenGLTexture(m_qImage)); + + 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 WSpinny::drawTexture(QOpenGLTexture* texture) { + const float texx1 = 0.f; + const float texy1 = 1.f; + const float texx2 = 1.f; + const float texy2 = 0.f; + + const float tw = texture->width(); + const float th = texture->height(); + + // fill centered + const float posx2 = tw >= th ? 1.f : (th - tw) / th; + const float posy2 = th >= tw ? 1.f : (tw - th) / tw; + const float posx1 = -posx2; + const float posy1 = -posy2; + + 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/wspinny.h b/src/widget/qopengl/wspinny.h new file mode 100644 index 00000000000..237393b1caf --- /dev/null +++ b/src/widget/qopengl/wspinny.h @@ -0,0 +1,154 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "library/dlgcoverartfullsize.h" +#include "mixer/basetrackplayer.h" +#include "preferences/usersettings.h" +#include "skin/legacy/skincontext.h" +#include "track/track_decl.h" +#include "vinylcontrol/vinylsignalquality.h" +#include "widget/trackdroptarget.h" +#include "widget/wbasewidget.h" +#include "widget/wcoverartmenu.h" +#include "widget/wglwidget.h" +#include "widget/wwidget.h" + +class ConfigKey; +class ControlProxy; +class VisualPlayPosition; +class VinylControlManager; +class VSyncThread; + +class WSpinny : public WGLWidget, + public WBaseWidget, + public VinylSignalQualityListener, + public TrackDropTarget { + Q_OBJECT + public: + WSpinny(QWidget* parent, + const QString& group, + UserSettingsPointer pConfig, + VinylControlManager* pVCMan, + BaseTrackPlayer* pPlayer); + ~WSpinny() override; + + void onVinylSignalQualityUpdate(const VinylSignalQualityReport& report) override; + + void setup(const QDomNode& node, + const SkinContext& context, + const ConfigKey& showCoverConfigKey); + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + + public slots: + void slotLoadTrack(TrackPointer); + void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); + void updateVinylControlSpeed(double rpm); + void updateVinylControlEnabled(double enabled); + void updateVinylControlSignalEnabled(double enabled); + void updateSlipEnabled(double enabled); + void render(VSyncThread* vSyncThread); + void swap(); + + protected slots: + void slotCoverFound( + const QObject* pRequestor, + const CoverInfo& coverInfo, + const QPixmap& pixmap, + mixxx::cache_key_t requestedCacheKey, + bool coverInfoUpdated); + void slotCoverInfoSelected(const CoverInfoRelative& coverInfo); + void slotReloadCoverArt(); + void slotTrackCoverArtUpdated(); + + signals: + void trackDropped(const QString& filename, const QString& group) override; + void cloneDeck(const QString& sourceGroup, const QString& targetGroup) override; + + protected: + //QWidget: + void paintEvent(QPaintEvent* /*unused*/) override; + void mouseMoveEvent(QMouseEvent* e) override; + void mousePressEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + void resizeEvent(QResizeEvent* /*unused*/) override; + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + bool event(QEvent* pEvent) override; + + double calculateAngle(double playpos); + int calculateFullRotations(double playpos); + double calculatePositionFromAngle(double angle); + QPixmap scaledCoverArt(const QPixmap& normal); + + private: + const QString m_group; + UserSettingsPointer m_pConfig; + std::shared_ptr m_pBgImage; + std::shared_ptr m_pMaskImage; + std::shared_ptr m_pFgImage; + QImage m_fgImageScaled; + std::shared_ptr m_pGhostImage; + QImage m_ghostImageScaled; + ControlProxy* m_pPlayPos; + QSharedPointer m_pVisualPlayPos; + ControlProxy* m_pTrackSamples; + ControlProxy* m_pTrackSampleRate; + ControlProxy* m_pScratchToggle; + ControlProxy* m_pScratchPos; + ControlProxy* m_pVinylControlSpeedType; + ControlProxy* m_pVinylControlEnabled; + ControlProxy* m_pSignalEnabled; + ControlProxy* m_pSlipEnabled; + ControlProxy* m_pShowCoverProxy; + + TrackPointer m_loadedTrack; + QPixmap m_loadedCover; + QPixmap m_loadedCoverScaled; + CoverInfo m_lastRequestedCover; + bool m_bShowCover; + + VinylControlManager* m_pVCManager; + double m_dInitialPos; + + int m_iVinylInput; + bool m_bVinylActive; + bool m_bSignalActive; + QImage m_qImage; + int m_iVinylScopeSize; + + float m_fAngle; //Degrees + double m_dAngleCurrentPlaypos; + double m_dAngleLastPlaypos; + float m_fGhostAngle; + double m_dGhostAngleCurrentPlaypos; + double m_dGhostAngleLastPlaypos; + int m_iStartMouseX; + int m_iStartMouseY; + int m_iFullRotations; + double m_dPrevTheta; + // Speed of the vinyl rotation. + double m_dRotationsPerSecond; + bool m_bClampFailedWarning; + bool m_bGhostPlayback; + + BaseTrackPlayer* m_pPlayer; + WCoverArtMenu* m_pCoverMenu; + DlgCoverArtFullSize* m_pDlgCoverArt; + + void initializeGL() override; + void drawTexture(QOpenGLTexture* texture); + + QOpenGLShaderProgram m_shaderProgram; + std::unique_ptr m_pBgTexture; + std::unique_ptr m_pMaskTexture; + std::unique_ptr m_pFgTextureScaled; + std::unique_ptr m_pGhostTextureScaled; + std::unique_ptr m_pLoadedCoverTextureScaled; + std::unique_ptr m_pQTexture; +}; diff --git a/src/widget/qopengl/wvumetergl.cpp b/src/widget/qopengl/wvumetergl.cpp new file mode 100644 index 00000000000..66f414514a3 --- /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 00000000000..2f61341eba4 --- /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 21b89d20dfd..5cd149ed913 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 a24883ddd08..de4bc334205 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 5bdacac771b..2f76e1ee2c1 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 b5d7bb72928..3a99d804e5f 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