diff --git a/CMakeLists.txt b/CMakeLists.txt index e3a4cbc5dfa1..180d36d301c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1443,6 +1443,7 @@ else() src/waveform/renderers/allshader/waveformrenderbeat.cpp src/waveform/renderers/allshader/waveformrenderer.cpp src/waveform/renderers/allshader/waveformrendererendoftrack.cpp + src/waveform/renderers/allshader/waveformrenderercentroid.cpp src/waveform/renderers/allshader/waveformrendererfiltered.cpp src/waveform/renderers/allshader/waveformrendererhsv.cpp src/waveform/renderers/allshader/waveformrendererlrrgb.cpp @@ -1452,6 +1453,7 @@ else() src/waveform/renderers/allshader/waveformrenderersimple.cpp src/waveform/renderers/allshader/waveformrendermark.cpp src/waveform/renderers/allshader/waveformrendermarkrange.cpp + src/waveform/widgets/allshader/centroidwaveformwidget.cpp src/waveform/widgets/allshader/filteredwaveformwidget.cpp src/waveform/widgets/allshader/hsvwaveformwidget.cpp src/waveform/widgets/allshader/lrrgbwaveformwidget.cpp diff --git a/src/preferences/upgrade.cpp b/src/preferences/upgrade.cpp index c03ee8b48b88..534cda376424 100644 --- a/src/preferences/upgrade.cpp +++ b/src/preferences/upgrade.cpp @@ -46,6 +46,7 @@ WaveformWidgetType::Type upgradeToAllShaders(WaveformWidgetType::Type waveformTy case WWT::AllShaderFilteredWaveform: case WWT::AllShaderSimpleWaveform: case WWT::AllShaderHSVWaveform: + case WWT::AllShaderCentroidWaveform: case WWT::Count_WaveformwidgetType: return waveformType; case WWT::QtSimpleWaveform: diff --git a/src/waveform/renderers/allshader/waveformrenderercentroid.cpp b/src/waveform/renderers/allshader/waveformrenderercentroid.cpp new file mode 100644 index 000000000000..850524f5e8b0 --- /dev/null +++ b/src/waveform/renderers/allshader/waveformrenderercentroid.cpp @@ -0,0 +1,248 @@ +#include "waveform/renderers/allshader/waveformrenderercentroid.h" + +#include "track/track.h" +#include "util/math.h" +#include "waveform/renderers/allshader/matrixforwidgetgeometry.h" +#include "waveform/waveform.h" +#include "waveform/waveformwidgetfactory.h" +#include "waveform/widgets/allshader/waveformwidget.h" +#include "widget/wskincolor.h" +#include "widget/wwidget.h" + +namespace allshader { + +namespace { +inline float math_pow2(float x) { + return x * x; +} +} // namespace + +WaveformRendererCentroid::WaveformRendererCentroid( + WaveformWidgetRenderer* waveformWidget) + : WaveformRendererSignalBase(waveformWidget) { +} + +void WaveformRendererCentroid::onSetup(const QDomNode& node) { + Q_UNUSED(node); +} + +void WaveformRendererCentroid::initializeGL() { + WaveformRendererSignalBase::initializeGL(); + m_shader.init(); +} + +void WaveformRendererCentroid::paintGL() { + 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 length = static_cast(m_waveformRenderer->getLength() * devicePixelRatio); + + // Not multiplying with devicePixelRatio will also work. In that case, on + // High-DPI-Display the lines will be devicePixelRatio pixels wide (which is + // also what is used for the beat grid and the markers), or in other words + // each block of samples is represented by devicePixelRatio pixels (width). + + 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 visualIncrementPerPixel = + (lastVisualIndex - firstVisualIndex) / static_cast(length); + + // 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); + + const float breadth = static_cast(m_waveformRenderer->getBreadth()) * devicePixelRatio; + const float halfBreadth = breadth / 2.0f; + + const float heightFactor = allGain * halfBreadth / 255.f; + + // Effective visual index of x + double xVisualSampleIndex = firstVisualIndex; + + const int numVerticesPerLine = 6; // 2 triangles + + const int reserved = numVerticesPerLine * (length + 1); + + m_vertices.clear(); + m_vertices.reserve(reserved); + m_colors.clear(); + m_colors.reserve(reserved); + + m_vertices.addRectangle(0.f, + halfBreadth - 0.5f * devicePixelRatio, + static_cast(length), + halfBreadth + 0.5f * devicePixelRatio); + m_colors.addForRectangle( + static_cast(m_axesColor_r), + static_cast(m_axesColor_g), + static_cast(m_axesColor_b)); + + for (int pos = 0; pos < length; ++pos) { + // 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 = visualIncrementPerPixel / 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); + + const float fpos = static_cast(pos); + + // Find the max values for low, mid, high and all in the waveform data. + // - Max of left and right + uchar u8maxLow{}; + uchar u8maxMid{}; + uchar u8maxHigh{}; + // - Per channel + uchar u8maxAllChn[2]{}; + for (int chn = 0; chn < 2; chn++) { + // data is interleaved left / right + for (int i = visualIndexStart + chn; i < visualIndexStop + chn; i += 2) { + const WaveformData& waveformData = data[i]; + + u8maxLow = math_max(u8maxLow, waveformData.filtered.low); + u8maxMid = math_max(u8maxMid, waveformData.filtered.mid); + u8maxHigh = math_max(u8maxHigh, waveformData.filtered.high); + u8maxAllChn[chn] = math_max(u8maxAllChn[chn], waveformData.filtered.all); + } + } + + // Cast to float + float maxLow = static_cast(u8maxLow); + float maxMid = static_cast(u8maxMid); + float maxHigh = static_cast(u8maxHigh); + float maxAllChn[2]{static_cast(u8maxAllChn[0]), static_cast(u8maxAllChn[1])}; + + // Calculate the magnitude of the maxLow, maxMid and maxHigh values + const float magnitude = std::sqrt( + math_pow2(maxLow) + math_pow2(maxMid) + math_pow2(maxHigh)); + + // Apply the gains + maxLow *= lowGain; + maxMid *= midGain; + maxHigh *= highGain; + + // Calculate the magnitude of the gained maxLow, maxMid and maxHigh values + const float magnitudeGained = std::sqrt( + math_pow2(maxLow) + math_pow2(maxMid) + math_pow2(maxHigh)); + + // The maxAll values will be used to draw the amplitude. We scale them according to + // magnitude of the gained maxLow, maxMid and maxHigh values + if (magnitude != 0.f) { + const float factor = magnitudeGained / magnitude; + maxAllChn[0] *= factor; + maxAllChn[1] *= factor; + } + + // Calculate the centroid, between 0 and 1, where 0 corresponds with + // only amplitude in the low band, and 1 with only amplitude in the high + // band. See https://en.wikipedia.org/wiki/Spectral_centroid + const float f[3]{0.f, 0.5f, 1.f}; + const float centroid = + (f[0] * maxLow + f[1] * maxMid + f[2] * maxHigh) / + (maxLow + maxMid + maxHigh); + + // Calculate the spectral flatness. The offset of 1 (on a range 0..255) + // is to avoid a flatness of 0 when any of the bands as amplitude 0. See + // https://en.wikipedia.org/wiki/Spectral_flatness + const float geoMean = std::pow( + (maxLow + 1.f) * (maxMid + 1.f) * (maxHigh + 1.f), 1.f / 3.f); + const float ariMean = (maxLow + 1.f + maxMid + 1.f + maxHigh + 1.f) / 3.f; + const float flatness = geoMean / ariMean; + + // Map the centroid to hue, resulting in a continuous color scale + // (red - yellow - green - cyan - blue) where 0 is red and 2/3 is blue. + // Displace and scale (and clamp) the centroid for more color contrast, + // based on trial error. + const float hue = 2.f / 3.f * std::max(0.f, std::min(1.f, centroid * 2.0f - 0.5f)); + + // See https://doc.qt.io/qt-6/qcolor.html#the-hsv-color-model + // + // Map the flatness to saturation: the flatter the + // spectrum, the less pronounced the centroid color + QColor color; + color.setHsvF(hue, + 1.0f - flatness * 0.75, + 1.0f); + + // Lines are thin rectangles + m_vertices.addRectangle(fpos - 0.5f, + halfBreadth - heightFactor * maxAllChn[0], + fpos + 0.5f, + halfBreadth + heightFactor * maxAllChn[1]); + // m_colors.addForRectangle(red, green, blue); + m_colors.addForRectangle( + static_cast(color.redF()), + static_cast(color.greenF()), + static_cast(color.blueF())); + + xVisualSampleIndex += visualIncrementPerPixel; + } + + DEBUG_ASSERT(reserved == m_vertices.size()); + DEBUG_ASSERT(reserved == m_colors.size()); + + const QMatrix4x4 matrix = matrixForWidgetGeometry(m_waveformRenderer, true); + + const int matrixLocation = m_shader.matrixLocation(); + const int positionLocation = m_shader.positionLocation(); + const int colorLocation = m_shader.colorLocation(); + + m_shader.bind(); + m_shader.enableAttributeArray(positionLocation); + m_shader.enableAttributeArray(colorLocation); + + m_shader.setUniformValue(matrixLocation, matrix); + + m_shader.setAttributeArray( + positionLocation, GL_FLOAT, m_vertices.constData(), 2); + m_shader.setAttributeArray( + colorLocation, GL_FLOAT, m_colors.constData(), 3); + + glDrawArrays(GL_TRIANGLES, 0, m_vertices.size()); + + m_shader.disableAttributeArray(positionLocation); + m_shader.disableAttributeArray(colorLocation); + m_shader.release(); +} + +} // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrenderercentroid.h b/src/waveform/renderers/allshader/waveformrenderercentroid.h new file mode 100644 index 000000000000..eef18dc76565 --- /dev/null +++ b/src/waveform/renderers/allshader/waveformrenderercentroid.h @@ -0,0 +1,29 @@ +#pragma once + +#include "shaders/rgbshader.h" +#include "util/class.h" +#include "waveform/renderers/allshader/rgbdata.h" +#include "waveform/renderers/allshader/vertexdata.h" +#include "waveform/renderers/allshader/waveformrenderersignalbase.h" + +namespace allshader { +class WaveformRendererCentroid; +} + +class allshader::WaveformRendererCentroid final : public allshader::WaveformRendererSignalBase { + public: + explicit WaveformRendererCentroid(WaveformWidgetRenderer* waveformWidget); + + // override ::WaveformRendererSignalBase + void onSetup(const QDomNode& node) override; + + void initializeGL() override; + void paintGL() override; + + private: + mixxx::RGBShader m_shader; + VertexData m_vertices; + RGBData m_colors; + + DISALLOW_COPY_AND_ASSIGN(WaveformRendererCentroid); +}; diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index c111710c888d..83b6dbb9b0b3 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -28,6 +28,7 @@ #include "waveform/visualsmanager.h" #include "waveform/vsyncthread.h" #ifdef MIXXX_USE_QOPENGL +#include "waveform/widgets/allshader/centroidwaveformwidget.h" #include "waveform/widgets/allshader/filteredwaveformwidget.h" #include "waveform/widgets/allshader/hsvwaveformwidget.h" #include "waveform/widgets/allshader/lrrgbwaveformwidget.h" @@ -975,6 +976,13 @@ void WaveformWidgetFactory::evaluateWidgets() { #else setWaveformVarsByType.operator()(); break; +#endif + case WaveformWidgetType::AllShaderCentroidWaveform: +#ifndef MIXXX_USE_QOPENGL + continue; +#else + setWaveformVarsByType.operator()(); + break; #endif default: DEBUG_ASSERT(!"Unexpected WaveformWidgetType"); @@ -1072,6 +1080,9 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createWaveformWidget( case WaveformWidgetType::AllShaderHSVWaveform: widget = new allshader::HSVWaveformWidget(viewer->getGroup(), viewer); break; + case WaveformWidgetType::AllShaderCentroidWaveform: + widget = new allshader::CentroidWaveformWidget(viewer->getGroup(), viewer); + break; #else case WaveformWidgetType::QtSimpleWaveform: widget = new QtSimpleWaveformWidget(viewer->getGroup(), viewer); diff --git a/src/waveform/widgets/allshader/centroidwaveformwidget.cpp b/src/waveform/widgets/allshader/centroidwaveformwidget.cpp new file mode 100644 index 000000000000..d5a24778e043 --- /dev/null +++ b/src/waveform/widgets/allshader/centroidwaveformwidget.cpp @@ -0,0 +1,35 @@ +#include "waveform/widgets/allshader/centroidwaveformwidget.h" + +#include "waveform/renderers/allshader/waveformrenderbackground.h" +#include "waveform/renderers/allshader/waveformrenderbeat.h" +#include "waveform/renderers/allshader/waveformrenderercentroid.h" +#include "waveform/renderers/allshader/waveformrendererendoftrack.h" +#include "waveform/renderers/allshader/waveformrendererpreroll.h" +#include "waveform/renderers/allshader/waveformrendermark.h" +#include "waveform/renderers/allshader/waveformrendermarkrange.h" +#include "waveform/widgets/allshader/moc_centroidwaveformwidget.cpp" + +namespace allshader { + +CentroidWaveformWidget::CentroidWaveformWidget(const QString& group, QWidget* parent) + : WaveformWidget(group, parent) { + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + addRenderer(); + + m_initSuccess = init(); +} + +void CentroidWaveformWidget::castToQWidget() { + m_widget = this; +} + +void CentroidWaveformWidget::paintEvent(QPaintEvent* event) { + Q_UNUSED(event); +} + +} // namespace allshader diff --git a/src/waveform/widgets/allshader/centroidwaveformwidget.h b/src/waveform/widgets/allshader/centroidwaveformwidget.h new file mode 100644 index 000000000000..3891223a183a --- /dev/null +++ b/src/waveform/widgets/allshader/centroidwaveformwidget.h @@ -0,0 +1,44 @@ +#pragma once + +#include "util/class.h" +#include "waveform/widgets/allshader/waveformwidget.h" + +class WaveformWidgetFactory; + +namespace allshader { +class CentroidWaveformWidget; +} + +class allshader::CentroidWaveformWidget final : public allshader::WaveformWidget { + Q_OBJECT + public: + WaveformWidgetType::Type getType() const override { + return WaveformWidgetType::AllShaderCentroidWaveform; + } + + static inline QString getWaveformWidgetName() { + return tr("Centroid"); + } + static constexpr bool useOpenGl() { + return true; + } + static constexpr bool useOpenGles() { + return true; + } + static constexpr bool useOpenGLShaders() { + return true; + } + static constexpr WaveformWidgetCategory category() { + return WaveformWidgetCategory::AllShader; + } + + protected: + void castToQWidget() override; + void paintEvent(QPaintEvent* event) override; + + private: + CentroidWaveformWidget(const QString& group, QWidget* parent); + friend class ::WaveformWidgetFactory; + + DISALLOW_COPY_AND_ASSIGN(CentroidWaveformWidget); +}; diff --git a/src/waveform/widgets/waveformwidgettype.h b/src/waveform/widgets/waveformwidgettype.h index 9a3fcfcb4168..cc7053ba4baa 100644 --- a/src/waveform/widgets/waveformwidgettype.h +++ b/src/waveform/widgets/waveformwidgettype.h @@ -27,6 +27,7 @@ class WaveformWidgetType { AllShaderFilteredWaveform, // 19 Filtered (all-shaders) AllShaderSimpleWaveform, // 20 Simple (all-shaders) AllShaderHSVWaveform, // 21 HSV (all-shaders) + AllShaderCentroidWaveform, // 22 Centroid (all-shaders) Count_WaveformwidgetType // Also used as invalid value }; };