Skip to content

Commit

Permalink
MythMusic: fix handleKeyPress of visualizers (#801)
Browse files Browse the repository at this point in the history
* MythMusic: call handleKeyPress of visualizers so they can have options

   So far only SELECT is used by AlbumArt, WaveForm and Spectrogram

* MythMusic: suppress text from WaveForm, hit SELECT to toggle

* MythMusic: let SELECT toggle frequency display in Spectrum

* MythMusic: add music NOTES to Spectrum overlay on SELECT key

* MythMusic: let Qt center the note/Hz text in Spectrum

* MythMusic: move notes into Mel Scale, let Qt center the notes on pixels

* MythMusic: skip sharps in bass to fit more notes in Spectrum overlay
  • Loading branch information
twitham1 committed Oct 9, 2023
1 parent 81fbd37 commit a0bbac9
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 54 deletions.
167 changes: 123 additions & 44 deletions mythplugins/mythmusic/mythmusic/visualize.cpp
Expand Up @@ -165,6 +165,10 @@ void MelScale::setMax(int maxscale, int maxrange, int maxfreq)
m_indices.clear();
m_indices.resize(maxrange, 0);

int note = 0; // first note is C0
double freq = 16.35; // frequency of first note
double next = pow(2.0, 1.0 / 12.0); // factor separating notes

double maxmel = hz2mel(maxfreq);
double hzperbin = (double) maxfreq / (double) maxscale;

Expand All @@ -176,6 +180,12 @@ void MelScale::setMax(int maxscale, int maxrange, int maxfreq)
m_indices[i] = bin;
// LOG(VB_PLAYBACK, LOG_INFO, QString("Mel maxmel=%1, hzperbin=%2, hz=%3, i=%4, bin=%5")
// .arg(maxmel).arg(hzperbin).arg(hz).arg(i).arg(bin));

if (hz > freq) { // map note to pixel location for note labels
m_freqs[note] = int(freq + 0.5);
m_pixels[note++] = i;
freq *= next;
}
}
}

Expand All @@ -184,6 +194,27 @@ int MelScale::operator[](int index)
return m_indices[index];
}

QString MelScale::note(int note)
{
if (note < 0 || note > 125)
return QString("");
return m_notes[note % 12];
}

int MelScale::pixel(int note)
{
if (note < 0 || note > 125)
return 0;
return m_pixels[note];
}

int MelScale::freq(int note)
{
if (note < 0 || note > 125)
return 0;
return m_freqs[note];
}

///////////////////////////////////////////////////////////////////////////////
// StereoScope

Expand Down Expand Up @@ -815,10 +846,8 @@ bool WaveForm::draw( QPainter *p, const QColor &back )

void WaveForm::handleKeyPress(const QString &action)
{
LOG(VB_PLAYBACK, LOG_INFO, QString("WF keypress = %1").arg(action));
LOG(VB_PLAYBACK, LOG_DEBUG, QString("WF keypress = %1").arg(action));

// I'd like to toggle overlay text upon certain key hit, but
// mythfrontend doesn't appear to call this. Bug?
if (action == "SELECT")
{
m_showtext = ! m_showtext;
Expand Down Expand Up @@ -927,6 +956,8 @@ Spectrogram::Spectrogram(bool hist)

m_fps = 40; // getting 1152 samples / 44100 = 38.28125 fps

m_color = gCoreContext->GetNumSetting("MusicSpectrogramColor", 0);

if (s_image.isNull()) // static histogram survives resize/restart
{
s_image = QImage(
Expand Down Expand Up @@ -1008,34 +1039,62 @@ unsigned long Spectrogram::getDesiredSamples(void)

bool Spectrogram::process(VisualNode */*node*/)
{
// TODO: label certain frequencies instead: 50, 100, 200, 500,
// 1000, 2000, 5000, 10000

// QPainter painter(m_image);
// painter.setPen(Qt::cyan);
// QFont font = QApplication::font();
// font.setPixelSize(14);
// painter.setFont(font);
// if (m_history)
// {
// for (auto h = m_sgsize.height(); h > 0; h -= m_sgsize.height() / 2)
// {
// for (auto i = 0; i < m_sgsize.height() / 2; i += 20)
// {
// painter.drawText(0, h - i,
// QString("...%1.%2.%3...").arg(i).arg(m_scale[i])
// .arg(m_scale[i] * 22050 / 8192)); // hack!!!
// }
// }
// } else {
// painter.rotate(90);
// for (auto i = 0; i < m_sgsize.width(); i += 20)
// {
// painter.drawText(0, -1 * i,
// QString("...%1.%2.%3...").arg(i).arg(m_scale[i])
// .arg(m_scale[i] * 22050 / 8192)); // hack!!!
// }
// }
if (!m_showtext)
return false;

QPainter painter(m_image);
painter.setPen(Qt::white);
QFont font = QApplication::font();
font.setPixelSize(16);
painter.setFont(font);
int half = m_sgsize.height() / 2;

if (m_history) // currently unused in v34...
{
for (auto h = m_sgsize.height(); h > 0; h -= half)
{
for (auto i = 0; i < half; i += 20)
{
painter.drawText(0, h - i,
QString("...%1.%2.%3...").arg(i).arg(m_scale[i])
.arg(m_scale[i] * 22050 / (m_fftlen/2))); // hack!!!
}
}
} else {
// static std::array<int, 5> treble = {52, 55, 59, 62, 65};
// for (auto i = 0; i < 5; i++) {
// painter.drawLine(m_scale.pixel(treble[i]), 0,
// m_scale.pixel(treble[i]), m_sgsize.height());
// }
for (auto i = 0; i < 125; i++) // 125 notes fit in 22050 Hz
{ // let Qt center the note text on the pixel
painter.drawText(m_scale.pixel(i) - 20, half - (i % 12) * 15 - 40,
40, 40, Qt::AlignCenter, m_scale.note(i));
if (i % 12 == 5) // octave numbers
painter.drawText(m_scale.pixel(i) - 20, half - 220,
40, 40, Qt::AlignCenter,
QString("%1").arg(int(i / 12)));
}
painter.rotate(90); // frequency in Hz draws down
int prev = -30;
for (auto i = 0; i < 125; i++) // 125 notes fit in 22050 Hz
{
if (i < 72 && m_scale.note(i) == ".") // skip low sharps
continue;
int now = m_scale.pixel(i);
if (now >= prev + 20) { // skip until good spacing
painter.drawText(half + 20, -1 * now - 40,
80, 80, Qt::AlignVCenter|Qt::AlignLeft,
QString("%1").arg(m_scale.freq(i)));
if (m_scale.note(i) != ".")
painter.drawText(half + 100, -1 * now - 40,
80, 80, Qt::AlignVCenter|Qt::AlignLeft,
QString("%1%2").arg(m_scale.note(i))
.arg(int(i / 12)));
prev = now;
}
}
}
return false;
}

Expand Down Expand Up @@ -1076,8 +1135,8 @@ bool Spectrogram::processUndisplayed(VisualNode *node)
int end = m_fftlen / 40; // ramp window ends down to zero crossing
for (int k = 0; k < m_fftlen; k++)
{
mult = k < end ? (float)k / (float)end
: k > m_fftlen - end ?
mult = k < end ? (float)k / (float)end
: k > m_fftlen - end ?
(float)(m_fftlen - k) / (float)end : 1;
m_dftL[k] = m_sigL[k] * mult;
m_dftR[k] = m_sigR[k] * mult;
Expand Down Expand Up @@ -1141,8 +1200,16 @@ bool Spectrogram::processUndisplayed(VisualNode *node)
h = m_sgsize.height() / 2;
painter.drawLine(s_offset, h - i, s_offset + mag, h - i);
painter.drawLine(s_offset - w, h - i, s_offset - w + mag, h - i);
left > 255 ? painter.setPen(Qt::yellow) :
painter.setPen(qRgb(mag, mag, mag));
if (m_color & 0x01) // left in color?
{
if (left > 255)
painter.setPen(Qt::white);
if (mag == 0)
painter.setPen(Qt::black);
} else {
left > 255 ? painter.setPen(Qt::yellow) :
painter.setPen(qRgb(mag, mag, mag));
}
painter.drawPoint(s_offset, h - i);
} else {
painter.drawLine(i, h / 2, i, h / 2 - h / 2 * mag / 256);
Expand All @@ -1158,16 +1225,24 @@ bool Spectrogram::processUndisplayed(VisualNode *node)
h = m_sgsize.height();
painter.drawLine(s_offset, h - i, s_offset + mag, h - i);
painter.drawLine(s_offset - w, h - i, s_offset - w + mag, h - i);
right > 255 ? painter.setPen(Qt::yellow) :
painter.setPen(qRgb(mag, mag, mag));
if (m_color & 0x02) // right in color?
{
if (left > 255)
painter.setPen(Qt::white);
if (mag == 0)
painter.setPen(Qt::black);
} else {
right > 255 ? painter.setPen(Qt::yellow) :
painter.setPen(qRgb(mag, mag, mag));
}
painter.drawPoint(s_offset, h - i);
} else {
painter.drawLine(i, h / 2, i, h / 2 + h / 2 * mag / 256);
}

prev = index; // next pixel is FFT bins from here
index = m_scale[i]; // to the next bin by LOG scale
prev = std::min(prev, index - 1);
prev = std::min(prev, index - 1);
}
if (m_history && ++s_offset >= w)
{
Expand Down Expand Up @@ -1206,13 +1281,17 @@ bool Spectrogram::draw(QPainter *p, const QColor &back)

void Spectrogram::handleKeyPress(const QString &action)
{
LOG(VB_PLAYBACK, LOG_INFO, QString("SG keypress = %1").arg(action));
LOG(VB_PLAYBACK, LOG_DEBUG, QString("SG keypress = %1").arg(action));

// I'd like to tweak options upon certain key hit, but
// mythfrontend doesn't appear to call this. Bug?
if (action == "SELECT")
{
LOG(VB_PLAYBACK, LOG_INFO, QString("SG keypress SELECT hit"));
if (action == "SELECT") {
if (m_history)
{
m_color = (m_color + 1) & 0x03; // left and right color bits
gCoreContext->SaveSetting("MusicSpectrogramColor",
QString("%1").arg(m_color));
}
else
m_showtext = ! m_showtext;
}
}

Expand Down
24 changes: 14 additions & 10 deletions mythplugins/mythmusic/mythmusic/visualize.h
Expand Up @@ -78,7 +78,7 @@ class VisualBase

virtual bool draw( QPainter *, const QColor & ) = 0;
virtual void resize( const QSize &size ) = 0;
virtual void handleKeyPress(const QString &action) = 0;
virtual void handleKeyPress([[maybe_unused]] const QString &action) { };
virtual int getDesiredFPS(void) { return m_fps; }
// Override this if you need the potential of capturing more data than the default
virtual unsigned long getDesiredSamples(void) { return SAMPLES_DEFAULT_SIZE; }
Expand Down Expand Up @@ -117,7 +117,6 @@ class StereoScope : public VisualBase
void resize( const QSize &size ) override; // VisualBase
bool process( VisualNode *node ) override; // VisualBase
bool draw( QPainter *p, const QColor &back ) override; // VisualBase
void handleKeyPress([[maybe_unused]] const QString &action) override {}; // VisualBase

protected:
QColor m_startColor {Qt::yellow};
Expand Down Expand Up @@ -161,7 +160,7 @@ class WaveForm : public StereoScope
unsigned long m_offset {0}; // node offset for draw
short *m_right {nullptr};
QFont m_font; // optional text overlay
bool m_showtext {true};
bool m_showtext {false};
MusicMetadata *m_currentMetadata {nullptr};
unsigned long m_duration {60000}; // file length in milliseconds
unsigned int m_lastx {1920}; // vert line tracker
Expand Down Expand Up @@ -206,9 +205,16 @@ class MelScale
static double hz2mel(double hz) { return 1127 * log(1 + hz / 700); }
static double mel2hz(double mel) { return 700 * (exp(mel / 1127) - 1); }
int operator[](int index);
QString note(int note); // text of note, 0 - 125
int pixel(int note); // position of note
int freq(int note); // frequency of note

private:
std::vector<int> m_indices;
std::vector<int> m_indices; // FFT bin of each pixel
std::array<QString, 12> m_notes // one octave of notes
= {"C", ".", "D", ".", "E", "F", ".", "G", ".", "A", ".", "B"};
std::array<int,126> m_pixels {0}; // pixel of each note
std::array<int,126> m_freqs {0}; // frequency of each note
int m_scale {0};
int m_range {0};
};
Expand All @@ -231,17 +237,18 @@ class Spectrogram : public VisualBase
bool process( VisualNode *node ) override;
bool draw(QPainter *p, const QColor &back = Qt::black) override;
void handleKeyPress(const QString &action) override;
// {(void) action;}

static QImage s_image; // picture of spectrogram
static int s_offset; // position on screen

protected:
static inline double clamp(double cur, double max, double min);
QImage *m_image; // picture in use
QSize m_sgsize {1920, 1080}; // picture size
QSize m_size; // displayed dize
QSize m_size; // displayed size
MelScale m_scale; // Y-axis
int m_fftlen {16 * 1024}; // window width
int m_color {0}; // color or grayscale
QVector<float> m_sigL; // decaying signal window
QVector<float> m_sigR;
FFTSample* m_dftL { nullptr }; // real in, complex out
Expand All @@ -252,6 +259,7 @@ class Spectrogram : public VisualBase
std::array<int,256*6> m_blue {0};
bool m_binpeak { true }; // peak of bins, else mean
bool m_history { true }; // spectrogram? or spectrum
bool m_showtext {false}; // freq overlay?
};

class Spectrum : public VisualBase
Expand All @@ -268,7 +276,6 @@ class Spectrum : public VisualBase
bool process(VisualNode *node) override; // VisualBase
bool processUndisplayed(VisualNode *node) override; // VisualBase
bool draw(QPainter *p, const QColor &back = Qt::black) override; // VisualBase
void handleKeyPress([[maybe_unused]] const QString &action) override {}; // VisualBase

protected:
static inline double clamp(double cur, double max, double min);
Expand Down Expand Up @@ -303,7 +310,6 @@ class Squares : public Spectrum

void resize (const QSize &newsize) override; // Spectrum
bool draw(QPainter *p, const QColor &back = Qt::black) override; // Spectrum
void handleKeyPress([[maybe_unused]] const QString &action) override {}; // Spectrum

private:
void drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h);
Expand Down Expand Up @@ -355,7 +361,6 @@ struct piano_key_data {
unsigned long getDesiredSamples(void) override; // VisualBase

bool draw(QPainter *p, const QColor &back = Qt::black) override; // VisualBase
void handleKeyPress([[maybe_unused]] const QString &action) override {}; // VisualBase

protected:
static inline double clamp(double cur, double max, double min);
Expand Down Expand Up @@ -415,7 +420,6 @@ class Blank : public VisualBase
void resize(const QSize &size) override; // VisualBase
bool process(VisualNode *node = nullptr) override; // VisualBase
bool draw(QPainter *p, const QColor &back = Qt::black) override; // VisualBase
void handleKeyPress([[maybe_unused]] const QString &action) override {}; // VisualBase

private:
QSize m_size;
Expand Down
4 changes: 4 additions & 0 deletions mythplugins/mythmusic/mythmusic/visualizerview.cpp
Expand Up @@ -18,6 +18,7 @@
#include <libmythui/mythdialogbox.h>

// mythmusic
#include "mainvisual.h"
#include "musiccommon.h"
#include "visualizerview.h"

Expand Down Expand Up @@ -75,6 +76,9 @@ bool VisualizerView::keyPressEvent(QKeyEvent *event)
QString action = actions[i];
handled = true;

if (m_mainvisual && m_mainvisual->visual())
m_mainvisual->visual()->handleKeyPress(action);

// unassgined arrow keys might as well be useful
if (action == "UP")
{
Expand Down

0 comments on commit a0bbac9

Please sign in to comment.