Skip to content

Commit

Permalink
waveform type centroid which maps spectral centroid to hue and spectr…
Browse files Browse the repository at this point in the history
…al flatness to saturation
  • Loading branch information
m0dB committed Oct 28, 2023
1 parent f036b0a commit 187c68c
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/preferences/upgrade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
248 changes: 248 additions & 0 deletions src/waveform/renderers/allshader/waveformrenderercentroid.cpp
Original file line number Diff line number Diff line change
@@ -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<int>(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<double>(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<float>(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<float>(length),
halfBreadth + 0.5f * devicePixelRatio);
m_colors.addForRectangle(
static_cast<float>(m_axesColor_r),
static_cast<float>(m_axesColor_g),
static_cast<float>(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<float>(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<float>(u8maxLow);
float maxMid = static_cast<float>(u8maxMid);
float maxHigh = static_cast<float>(u8maxHigh);
float maxAllChn[2]{static_cast<float>(u8maxAllChn[0]), static_cast<float>(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<float>(color.redF()),
static_cast<float>(color.greenF()),
static_cast<float>(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
29 changes: 29 additions & 0 deletions src/waveform/renderers/allshader/waveformrenderercentroid.h
Original file line number Diff line number Diff line change
@@ -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);
};
11 changes: 11 additions & 0 deletions src/waveform/waveformwidgetfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -975,6 +976,13 @@ void WaveformWidgetFactory::evaluateWidgets() {
#else
setWaveformVarsByType.operator()<allshader::HSVWaveformWidget>();
break;
#endif
case WaveformWidgetType::AllShaderCentroidWaveform:
#ifndef MIXXX_USE_QOPENGL
continue;
#else
setWaveformVarsByType.operator()<allshader::CentroidWaveformWidget>();
break;
#endif
default:
DEBUG_ASSERT(!"Unexpected WaveformWidgetType");
Expand Down Expand Up @@ -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);
Expand Down
35 changes: 35 additions & 0 deletions src/waveform/widgets/allshader/centroidwaveformwidget.cpp
Original file line number Diff line number Diff line change
@@ -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<WaveformRenderBackground>();
addRenderer<WaveformRendererEndOfTrack>();
addRenderer<WaveformRendererPreroll>();
addRenderer<WaveformRenderMarkRange>();
addRenderer<WaveformRendererCentroid>();
addRenderer<WaveformRenderBeat>();
addRenderer<WaveformRenderMark>();

m_initSuccess = init();
}

void CentroidWaveformWidget::castToQWidget() {
m_widget = this;
}

void CentroidWaveformWidget::paintEvent(QPaintEvent* event) {
Q_UNUSED(event);
}

} // namespace allshader
Loading

0 comments on commit 187c68c

Please sign in to comment.