MythTV master
visualize.cpp
Go to the documentation of this file.
1/*
2 visualize.cpp
3
4 (c) 2003 Thor Sigvaldason and Isaac Richards
5 VERY closely based on code from mq3 by Brad Hughes
6
7 Part of the mythTV project
8
9 music visualizers
10*/
11
12// C++
13#include <algorithm>
14#include <cmath>
15#include <iostream>
16
17// Qt
18#include <QtGlobal>
19#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
20#include <QtSystemDetection>
21#endif
22#include <QApplication>
23#include <QCoreApplication>
24#include <QImage>
25#include <QPainter>
26#include <QDir>
27
28// MythTV
33#ifndef __cpp_size_t_suffix
35#endif
40
41// mythmusic
42#include "decoder.h"
43#include "inlines.h"
44#include "mainvisual.h"
45#include "musicplayer.h"
46#include "visualize.h"
47
48extern "C" {
49 #include <libavutil/mem.h>
50 #include <libavutil/tx.h>
51}
52
54
55VisualBase::VisualBase(bool screensaverenable)
56 : m_xscreensaverenable(screensaverenable)
57{
60}
61
63{
64 //
65 // This is only here so
66 // that derived classes
67 // can destruct properly
68 //
71}
72
73
74void VisualBase::drawWarning(QPainter *p, const QColor &back, const QSize size, const QString& warning, int fontSize)
75{
76 p->fillRect(0, 0, size.width(), size.height(), back);
77 p->setPen(Qt::white);
78
79 // Taken from removed MythUIHelper::GetMediumFont
80 QFont font = QApplication::font();
81
82#ifdef Q_OS_WINDOWS
83 // logicalDpiY not supported in Windows.
84 int logicalDpiY = 100;
85 HDC hdc = GetDC(nullptr);
86 if (hdc)
87 {
88 logicalDpiY = GetDeviceCaps(hdc, LOGPIXELSY);
89 ReleaseDC(nullptr, hdc);
90 }
91#else
92 int logicalDpiY = GetMythMainWindow()->logicalDpiY();
93#endif
94
95 // adjust for screen resolution relative to 100 dpi
96 float floatSize = (16 * 100.0F) / logicalDpiY;
97 // adjust for myth GUI size relative to 800x600
98 float dummy = 0.0;
99 float hmult = 0.0;
100 GetMythMainWindow()->GetScalingFactors(hmult, dummy);
101 floatSize = floatSize * hmult;
102 // round and set
103 font.setPointSize(lroundf(floatSize));
104 font.setWeight(QFont::Bold);
105 font.setPointSizeF(fontSize * (size.width() / 800.0));
106 p->setFont(font);
107
108 p->drawText(0, 0, size.width(), size.height(), Qt::AlignVCenter | Qt::AlignHCenter | Qt::TextWordWrap, warning);
109}
110
112// LogScale
113
114LogScale::LogScale(int maxscale, int maxrange)
115{
116 setMax(maxscale, maxrange);
117}
118
119void LogScale::setMax(int maxscale, int maxrange)
120{
121 if (maxscale == 0 || maxrange == 0)
122 return;
123
124 m_scale = maxscale;
125 m_range = maxrange;
126
127 auto domain = (long double) maxscale;
128 auto range = (long double) maxrange;
129 long double x = 1.0;
130 long double dx = 1.0;
131 long double e4 = 1.0E-8;
132
133 m_indices.clear();
134 m_indices.resize(maxrange, 0);
135
136 // initialize log scale
137 for (uint i=0; i<10000 && (std::abs(dx) > e4); i++)
138 {
139 double t = std::log((domain + x) / x);
140 double y = (x * t) - range;
141 double yy = t - (domain / (x + domain));
142 dx = y / yy;
143 x -= dx;
144 }
145
146 double alpha = x;
147 for (int i = 1; i < (int) domain; i++)
148 {
149 int scaled = (int) floor(0.5 + (alpha * log((double(i) + alpha) / alpha)));
150 scaled = std::max(scaled, 1);
151 m_indices[scaled - 1] = std::max(m_indices[scaled - 1], i);
152 }
153}
154
156{
157 return m_indices[index];
158}
159
161// MelScale, example: (8192 fft, 1920 pixels, 22050 hz)
162
163MelScale::MelScale(int maxscale, int maxrange, int maxfreq)
164{
165 setMax(maxscale, maxrange, maxfreq);
166}
167
168void MelScale::setMax(int maxscale, int maxrange, int maxfreq)
169{
170 if (maxscale == 0 || maxrange == 0 || maxfreq == 0)
171 return;
172
173 m_scale = maxscale;
174 m_range = maxrange;
175
176 m_indices.clear();
177 m_indices.resize(maxrange, 0);
178
179 int note = 0; // first note is C0
180 double freq = 16.35; // frequency of first note
181 double next = pow(2.0, 1.0 / 12.0); // factor separating notes
182
183 double maxmel = hz2mel(maxfreq);
184 double hzperbin = (double) maxfreq / (double) maxscale;
185
186 for (int i = 0; i < maxrange; i++)
187 {
188 double mel = maxmel * i / maxrange;
189 double hz = mel2hz(mel);
190 int bin = int(hz / hzperbin);
191 m_indices[i] = bin;
192 // LOG(VB_PLAYBACK, LOG_INFO, QString("Mel maxmel=%1, hzperbin=%2, hz=%3, i=%4, bin=%5")
193 // .arg(maxmel).arg(hzperbin).arg(hz).arg(i).arg(bin));
194
195 if (hz > freq) { // map note to pixel location for note labels
196 m_freqs[note] = lround(freq);
197 m_pixels[note++] = i;
198 freq *= next;
199 }
200 }
201}
202
204{
205 return m_indices[index];
206}
207
208QString MelScale::note(int note)
209{
210 if (note < 0 || note > 125)
211 return {};
212 return m_notes[note % 12];
213}
214
215int MelScale::pixel(int note)
216{
217 if (note < 0 || note > 125)
218 return 0;
219 return m_pixels[note];
220}
221
222int MelScale::freq(int note)
223{
224 if (note < 0 || note > 125)
225 return 0;
226 return m_freqs[note];
227}
228
230// StereoScope
231
233{
234 m_fps = 45;
235}
236
237void StereoScope::resize( const QSize &newsize )
238{
239 m_size = newsize;
240
241 auto os = m_magnitudes.size();
242#ifdef __cpp_size_t_suffix
243 m_magnitudes.resize( m_size.width() * 2UZ );
244#else
245 m_magnitudes.resize( m_size.width() * 2_UZ );
246#endif
247 for ( ; os < m_magnitudes.size(); os++ )
248 m_magnitudes[os] = 0.0;
249}
250
252{
253 bool allZero = true;
254
255
256 if (node)
257 {
258 double index = 0;
259 double const step = (double)SAMPLES_DEFAULT_SIZE / m_size.width();
260 for ( int i = 0; i < m_size.width(); i++)
261 {
262 auto indexTo = (unsigned long)(index + step);
263 if (indexTo == (unsigned long)index)
264 indexTo = (unsigned long)(index + 1);
265
266 double valL = 0;
267 double valR = 0;
268#if RUBBERBAND
269 if ( m_rubberband ) {
270 valL = m_magnitudes[ i ];
271 valR = m_magnitudes[ i + m_size.width() ];
272 if (valL < 0.) {
273 valL += m_falloff;
274 if ( valL > 0. )
275 valL = 0.;
276 }
277 else
278 {
279 valL -= m_falloff;
280 if ( valL < 0. )
281 valL = 0.;
282 }
283 if (valR < 0.)
284 {
285 valR += m_falloff;
286 if ( valR > 0. )
287 valR = 0.;
288 }
289 else
290 {
291 valR -= m_falloff;
292 if ( valR < 0. )
293 valR = 0.;
294 }
295 }
296#endif
297 for (auto s = (unsigned long)index; s < indexTo && s < node->m_length; s++)
298 {
299 double adjHeight = static_cast<double>(m_size.height()) / 4.0;
300 double tmpL = ( ( node->m_left ? static_cast<double>(node->m_left[s]) : 0.) *
301 adjHeight ) / 32768.0;
302 double tmpR = ( ( node->m_right ? static_cast<double>(node->m_right[s]) : 0.) *
303 adjHeight ) / 32768.0;
304 if (tmpL > 0)
305 valL = (tmpL > valL) ? tmpL : valL;
306 else
307 valL = (tmpL < valL) ? tmpL : valL;
308 if (tmpR > 0)
309 valR = (tmpR > valR) ? tmpR : valR;
310 else
311 valR = (tmpR < valR) ? tmpR : valR;
312 }
313
314 if (valL != 0. || valR != 0.)
315 allZero = false;
316
317 m_magnitudes[ i ] = valL;
318 m_magnitudes[ i + m_size.width() ] = valR;
319
320 index = index + step;
321 }
322#if RUBBERBAND
323 }
324 else if (m_rubberband)
325 {
326 for ( int i = 0; i < m_size.width(); i++)
327 {
328 double valL = m_magnitudes[ i ];
329 if (valL < 0) {
330 valL += 2;
331 if (valL > 0.)
332 valL = 0.;
333 } else {
334 valL -= 2;
335 if (valL < 0.)
336 valL = 0.;
337 }
338
339 double valR = m_magnitudes[ i + m_size.width() ];
340 if (valR < 0.) {
341 valR += m_falloff;
342 if (valR > 0.)
343 valR = 0.;
344 }
345 else
346 {
347 valR -= m_falloff;
348 if (valR < 0.)
349 valR = 0.;
350 }
351
352 if (valL != 0. || valR != 0.)
353 allZero = false;
354
355 m_magnitudes[ i ] = valL;
356 m_magnitudes[ i + m_size.width() ] = valR;
357 }
358#endif
359 }
360 else
361 {
362 for ( int i = 0; (unsigned) i < m_magnitudes.size(); i++ )
363 m_magnitudes[ i ] = 0.;
364 }
365
366 return allZero;
367}
368
369bool StereoScope::draw( QPainter *p, const QColor &back )
370{
371 if (back != Qt::green) // hack!!! for WaveForm
372 {
373 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
374 }
375 for ( int i = 1; i < m_size.width(); i++ )
376 {
377#if TWOCOLOUR
378 // left
379 double per = ( static_cast<double>(m_magnitudes[i]) * 2.0 ) /
380 ( static_cast<double>(m_size.height()) / 4.0 );
381 if (per < 0.0)
382 per = -per;
383 if (per > 1.0)
384 per = 1.0;
385 else if (per < 0.0)
386 per = 0.0;
387
388 double r = m_startColor.red() +
389 ((m_targetColor.red() - m_startColor.red()) * (per * per));
390 double g = m_startColor.green() +
391 ((m_targetColor.green() - m_startColor.green()) * (per * per));
392 double b = m_startColor.blue() +
393 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
394
395 if (r > 255.0)
396 r = 255.0;
397 else if (r < 0.0)
398 r = 0;
399
400 if (g > 255.0)
401 g = 255.0;
402 else if (g < 0.0)
403 g = 0;
404
405 if (b > 255.0)
406 b = 255.0;
407 else if (b < 0.0)
408 b = 0;
409
410 p->setPen( QColor( int(r), int(g), int(b) ) );
411#else
412 p->setPen(Qt::red);
413#endif
414 double adjHeight = static_cast<double>(m_size.height()) / 4.0;
415 p->drawLine( i - 1,
416 (int)(adjHeight - m_magnitudes[i - 1]),
417 i,
418 (int)(adjHeight - m_magnitudes[i]));
419
420#if TWOCOLOUR
421 // right
422 per = ( static_cast<double>(m_magnitudes[ i + m_size.width() ]) * 2 ) /
423 adjHeight;
424 if (per < 0.0)
425 per = -per;
426 if (per > 1.0)
427 per = 1.0;
428 else if (per < 0.0)
429 per = 0.0;
430
431 r = m_startColor.red() + ((m_targetColor.red() -
432 m_startColor.red()) * (per * per));
433 g = m_startColor.green() + ((m_targetColor.green() -
434 m_startColor.green()) * (per * per));
435 b = m_startColor.blue() + ((m_targetColor.blue() -
436 m_startColor.blue()) * (per * per));
437
438 if (r > 255.0)
439 r = 255.0;
440 else if (r < 0.0)
441 r = 0;
442
443 if (g > 255.0)
444 g = 255.0;
445 else if (g < 0.0)
446 g = 0;
447
448 if (b > 255.0)
449 b = 255.0;
450 else if (b < 0.0)
451 b = 0;
452
453 p->setPen( QColor( int(r), int(g), int(b) ) );
454#else
455 p->setPen(Qt::red);
456#endif
457 adjHeight = static_cast<double>(m_size.height()) * 3.0 / 4.0;
458 p->drawLine( i - 1,
459 (int)(adjHeight - m_magnitudes[i + m_size.width() - 1]),
460 i,
461 (int)(adjHeight - m_magnitudes[i + m_size.width()]));
462 }
463
464 return true;
465}
466
468// MonoScope
469
471{
472 bool allZero = true;
473
474 if (node)
475 {
476 double index = 0;
477 double const step = (double)SAMPLES_DEFAULT_SIZE / m_size.width();
478 for (int i = 0; i < m_size.width(); i++)
479 {
480 auto indexTo = (unsigned long)(index + step);
481 if (indexTo == (unsigned long)index)
482 indexTo = (unsigned long)(index + 1);
483
484 double val = 0;
485#if RUBBERBAND
486 if ( m_rubberband )
487 {
488 val = m_magnitudes[ i ];
489 if (val < 0.)
490 {
491 val += m_falloff;
492 if ( val > 0. )
493 {
494 val = 0.;
495 }
496 }
497 else
498 {
499 val -= m_falloff;
500 if ( val < 0. )
501 {
502 val = 0.;
503 }
504 }
505 }
506#endif
507 for (auto s = (unsigned long)index; s < indexTo && s < node->m_length; s++)
508 {
509 double tmp = ( static_cast<double>(node->m_left[s]) +
510 ((node->m_right ? static_cast<double>(node->m_right[s])
511 : static_cast<double>(node->m_left[s])) *
512 ( static_cast<double>(m_size.height()) / 2.0 )) ) / 65536.0;
513 if (tmp > 0)
514 {
515 val = (tmp > val) ? tmp : val;
516 }
517 else
518 {
519 val = (tmp < val) ? tmp : val;
520 }
521 }
522
523 if ( val != 0. )
524 {
525 allZero = false;
526 }
527 m_magnitudes[ i ] = val;
528 index = index + step;
529 }
530 }
531#if RUBBERBAND
532 else if (m_rubberband)
533 {
534 for (int i = 0; i < m_size.width(); i++) {
535 double val = m_magnitudes[ i ];
536 if (val < 0) {
537 val += 2;
538 if (val > 0.)
539 val = 0.;
540 } else {
541 val -= 2;
542 if (val < 0.)
543 val = 0.;
544 }
545
546 if ( val != 0. )
547 allZero = false;
548 m_magnitudes[ i ] = val;
549 }
550 }
551#endif
552 else
553 {
554 for (int i = 0; i < m_size.width(); i++ )
555 m_magnitudes[ i ] = 0.;
556 }
557
558 return allZero;
559}
560
561bool MonoScope::draw( QPainter *p, const QColor &back )
562{
563 if (back != Qt::green) // hack!!! for WaveForm
564 {
565 p->fillRect( 0, 0, m_size.width(), m_size.height(), back );
566 }
567 for ( int i = 1; i < m_size.width(); i++ ) {
568#if TWOCOLOUR
569 double per = ( static_cast<double>(m_magnitudes[i]) * 2.0 ) /
570 ( static_cast<double>(m_size.height()) / 4.0 );
571 if (per < 0.0)
572 per = -per;
573 if (per > 1.0)
574 per = 1.0;
575 else if (per < 0.0)
576 per = 0.0;
577
578 double r = m_startColor.red() +
579 ((m_targetColor.red() - m_startColor.red()) * (per * per));
580 double g = m_startColor.green() +
581 ((m_targetColor.green() - m_startColor.green()) * (per * per));
582 double b = m_startColor.blue() +
583 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
584
585 if (r > 255.0)
586 r = 255.0;
587 else if (r < 0.0)
588 r = 0;
589
590 if (g > 255.0)
591 g = 255.0;
592 else if (g < 0.0)
593 g = 0;
594
595 if (b > 255.0)
596 b = 255.0;
597 else if (b < 0.0)
598 b = 0;
599
600 p->setPen(QColor(int(r), int(g), int(b)));
601#else
602 p->setPen(Qt::red);
603#endif
604 double adjHeight = static_cast<double>(m_size.height()) / 2.0;
605 p->drawLine( i - 1,
606 (int)(adjHeight - m_magnitudes[ i - 1 ]),
607 i,
608 (int)(adjHeight - m_magnitudes[ i ] ));
609 }
610
611 return true;
612}
613
615// WaveForm - see whole track - by twitham@sbcglobal.net, 2023/01
616
617// As/of v34, switching from small preview to fullscreen destroys and
618// recreates this object. So I use this static to survive which works
619// only becasue there is 1 instance per process. If mythfrontend ever
620// decides to display more than one visual at a time, it should also
621// be fixed to not destroy the instance but rather to resize it
622// dynamically. At that time, this could return to an m_image class
623// member. But more of this file might also need refactored to enable
624// long-life resizable visuals.
625
626QImage WaveForm::s_image {nullptr}; // picture of full track
627
629{
630 saveload(nullptr);
631 LOG(VB_PLAYBACK, LOG_INFO, QString("WF going down"));
632}
633
634// cache current track, if any, before loading cache of given track
636{
637 QString cache = GetConfDir() + "/MythMusic/WaveForm";
638 QString filename;
640 if (m_currentMetadata) // cache work in progress for next time
641 {
642 QDir dir(cache);
643 if (!dir.exists())
644 {
645 dir.mkpath(cache);
646 }
647 filename = QString("%1/%2.png").arg(cache)
648 .arg(m_stream ? 0 : m_currentMetadata->ID());
649 LOG(VB_PLAYBACK, LOG_INFO, QString("WF saving to %1").arg(filename));
650 if (!s_image.save(filename))
651 {
652 LOG(VB_GENERAL, LOG_ERR,
653 QString("WF saving to %1 failed: " + ENO).arg(filename));
654 }
655 }
656 m_currentMetadata = meta;
657 if (meta) // load previous work from cache
658 {
659 filename = QString("%1/%2.png").arg(cache).arg(m_stream ? 0 : meta->ID());
660 LOG(VB_PLAYBACK, LOG_INFO, QString("WF loading from %1").arg(filename));
661 if (!s_image.load(filename))
662 {
663 LOG(VB_GENERAL, LOG_WARNING,
664 QString("WF loading %1 failed, recreating").arg(filename));
665 }
666 // 60 seconds skips pixels with < 44100 streams like 22050,
667 // but this is now compensated for by drawing wider "pixels"
668 m_duration = m_stream ? 60000 : meta->Length().count(); // millisecs
669 }
670
671 // A track length of zero milliseconds is most likely wrong but
672 // can accidentally happen. Rather than crash on divide by zero
673 // later, let's pretend it is 1 minute long. If the file plays
674 // successfully, then it should record the correct track length
675 // and be more correct next time.
676 if (m_duration <= 0)
677 m_duration = 60000;
678
679 if (s_image.isNull())
680 {
681 s_image = QImage(m_wfsize.width(), m_wfsize.height(), QImage::Format_RGB32);
682 s_image.fill(Qt::black);
683 }
684 m_minl = 0; // drop last pixel, prepare for first
685 m_maxl = 0;
686 m_sqrl = 0;
687 m_minr = 0;
688 m_maxr = 0;
689 m_sqrr = 0;
690 m_position = 0;
691 m_lastx = m_wfsize.width();
692 m_font = QApplication::font();
693 m_font.setPixelSize(18); // small to be mostly unnoticed
694}
695
697{
698 // could be an adjustable class member, but this hard code works well
699 return kWFAudioSize; // maximum samples per update, may get less
700}
701
703{
704 // After 2023/01 bugfix below, processUndisplayed processes this
705 // node again after this process! If that is ever changed in
706 // mainvisual.cpp, then this would need adjusted.
707
708 // StereoScope overlay must process only the displayed nodes
710 return false; // update even when silent
711}
712
714{
715 // In 2023/01, v32 had a bug in mainvisual.cpp that this never
716 // received any nodes. Once I fixed that:
717
718 // < m_vis->processUndisplayed(node);
719 // > m_vis->processUndisplayed(m_nodes.first());
720
721 // now this receives *all* nodes. So we don't need any processing
722 // in ::process above as that would double process.
723
725 if (meta && meta != m_currentMetadata)
726 {
727 saveload(meta);
728 }
729 if (node && !s_image.isNull())
730 {
731 m_offset = node->m_offset.count() % m_duration; // for ::draw below
732 m_right = node->m_right;
733 uint n = node->m_length;
734 // LOG(VB_PLAYBACK, LOG_DEBUG,
735 // QString("WF process %1 samples at %2, display=%3").
736 // arg(n).arg(m_offset).arg(displayed));
737
738// TODO: interpolate timestamps to process correct samples per pixel
739// rather than fitting all we get in 1 or more pixels
740
741 for (uint i = 0; i < n; i++) // find min/max and sum of squares
742 {
743 short int val = node->m_left[i];
744 m_maxl = std::max(val, m_maxl);
745 m_minl = std::min(val, m_minl);
746 m_sqrl += static_cast<long>(val) * static_cast<long>(val);
747 if (m_right)
748 {
749 val = node->m_right[i];
750 m_maxr = std::max(val, m_maxr);
751 m_minr = std::min(val, m_minr);
752 m_sqrr += static_cast<long>(val) * static_cast<long>(val);
753 }
754 m_position++;
755 }
756 uint xx = m_wfsize.width() * m_offset / m_duration;
757 if (xx != m_lastx) // draw one finished line of min/max/rms
758 {
759 if (m_lastx > xx - 1 || // REW seek or right to left edge wrap
760 m_lastx < xx - 5) // FFWD seek
761 {
762 m_lastx = xx - 1;
763 }
764 int h = m_wfsize.height() / 4; // amplitude above or below zero
765 int y = m_wfsize.height() / 4; // left zero line
766 int yr = m_wfsize.height() * 3 / 4; // right zero line
767 if (!m_right)
768 { // mono - drop full waveform below StereoScope
769 y = yr;
770 }
771 // This "loop" runs only once except for short tracks or
772 // low sample rates that need some of the virtual "lines"
773 // to be drawn wider with more actual lines. I'd rather
774 // duplicate the vertical lines than draw rectangles since
775 // lines are the more common case. -twitham
776
777 QPainter painter(&s_image);
778 for (uint x = m_lastx + 1; x <= xx; x++)
779 {
780 // LOG(VB_PLAYBACK, LOG_DEBUG,
781 // QString("WF painting at %1,%2/%3").arg(x).arg(y).arg(yr));
782
783 // clear prior content of this column
784 if (m_stream) // clear 5 seconds of future, with wrap
785 {
786 painter.fillRect(x, 0, 32 * 5,
787 m_wfsize.height(), Qt::black);
788 painter.fillRect(x - m_wfsize.width(), 0, 32 * 5,
789 m_wfsize.height(), Qt::black);
790 } else { // preserve the future, if any
791 painter.fillRect(x, 0, 1,
792 m_wfsize.height(), Qt::black);
793 }
794
795 // Audacity uses 50,50,200 and 100,100,220 - I'm going
796 // darker to better contrast the StereoScope overlay
797 painter.setPen(qRgb(25, 25, 150)); // peak-to-peak
798 painter.drawLine(x, y - (h * m_maxl / 32768),
799 x, y - (h * m_minl / 32768));
800 if (m_right)
801 {
802 painter.drawLine(x, yr - (h * m_maxr / 32768),
803 x, yr - (h * m_minr / 32768));
804 }
805 painter.setPen(qRgb(150, 25, 25)); // RMS
806 int rmsl = sqrt(m_sqrl / m_position) * y / 32768;
807 painter.drawLine(x, y - rmsl, x, y + rmsl);
808 if (m_right)
809 {
810 int rmsr = sqrt(m_sqrr / m_position) * y / 32768;
811 painter.drawLine(x, yr - rmsr, x, yr + rmsr);
812 painter.drawLine(x, m_wfsize.height() / 2, // L / R delta
813 x, (m_wfsize.height() / 2) - rmsl + rmsr);
814 }
815 }
816 m_minl = 0; // reset metrics for next line
817 m_maxl = 0;
818 m_sqrl = 0;
819 m_minr = 0;
820 m_maxr = 0;
821 m_sqrr = 0;
822 m_position = 0;
823 m_lastx = xx;
824 }
825 }
826 return false;
827}
828
829bool WaveForm::draw( QPainter *p, const QColor &back )
830{
831 p->fillRect(0, 0, 0, 0, back); // no clearing, here to suppress warning
832 if (!s_image.isNull())
833 { // background, updated by ::process above
834 p->drawImage(0, 0, s_image.scaled(m_size,
835 Qt::IgnoreAspectRatio,
836 Qt::SmoothTransformation));
837 }
838
839 StereoScope::draw(p, Qt::green); // green == no clearing!
840
841 p->fillRect(m_size.width() * m_offset / m_duration, 0,
842 1, m_size.height(), Qt::darkGray);
843
844 if (m_showtext && m_size.width() > 500) // metrics in corners
845 {
846 p->setPen(Qt::darkGray);
847 p->setFont(m_font);
848 QRect text(5, 5, m_size.width() - 10, m_size.height() - 10);
849 p->drawText(text, Qt::AlignTop | Qt::AlignLeft,
850 QString("%1:%2")
851 .arg(m_offset / 1000 / 60)
852 .arg(m_offset / 1000 % 60, 2, 10, QChar('0')));
853 p->drawText(text, Qt::AlignTop | Qt::AlignHCenter,
854 QString("%1%")
855 .arg(100.0 * m_offset / m_duration, 0, 'f', 0));
856 p->drawText(text, Qt::AlignTop | Qt::AlignRight,
857 QString("%1:%2")
858 .arg(m_duration / 1000 / 60)
859 .arg(m_duration / 1000 % 60, 2, 10, QChar('0')));
860 p->drawText(text, Qt::AlignBottom | Qt::AlignLeft,
861 QString("%1 lines/s")
862 .arg(1000.0 * m_wfsize.width() / m_duration, 0, 'f', 1));
863 p->drawText(text, Qt::AlignBottom | Qt::AlignRight,
864 QString("%1 ms/line")
865 .arg(1.0 * m_duration / m_wfsize.width(), 0, 'f', 1));
866 }
867 return true;
868}
869
871{
872 LOG(VB_PLAYBACK, LOG_DEBUG, QString("WF keypress = %1").arg(action));
873
874 if (action == "SELECT" || action == "2")
875 {
877 }
878 else if (action == "DELETE" && !s_image.isNull())
879 {
880 s_image.fill(Qt::black);
881 }
882}
883
885// StereoScopeFactory
886
887static class StereoScopeFactory : public VisFactory
888{
889 public:
890 const QString &name(void) const override // VisFactory
891 {
892 static QString s_name = QCoreApplication::translate("Visualizers",
893 "StereoScope");
894 return s_name;
895 }
896
897 uint plugins(QStringList *list) const override // VisFactory
898 {
899 *list << name();
900 return 1;
901 }
902
903 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
904 {
905 return new StereoScope();
906 }
908
909
911// MonoScopeFactory
912
913static class MonoScopeFactory : public VisFactory
914{
915 public:
916 const QString &name(void) const override // VisFactory
917 {
918 static QString s_name = QCoreApplication::translate("Visualizers",
919 "MonoScope");
920 return s_name;
921 }
922
923 uint plugins(QStringList *list) const override // VisFactory
924 {
925 *list << name();
926 return 1;
927 }
928
929 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
930 {
931 return new MonoScope();
932 }
934
936// WaveFormFactory
937
938static class WaveFormFactory : public VisFactory
939{
940 public:
941 const QString &name(void) const override // VisFactory
942 {
943 static QString s_name = QCoreApplication::translate("Visualizers",
944 "WaveForm");
945 return s_name;
946 }
947
948 uint plugins(QStringList *list) const override // VisFactory
949 {
950 *list << name();
951 return 1;
952 }
953
955 MainVisual */*parent*/, const QString &/*pluginName*/) const override
956 {
957 return new WaveForm();
958 }
960
962// Spectrogram - by twitham@sbcglobal.net, 2023/05
963//
964// A spectrogram needs the entire sound signal. A wide sliding window
965// is needed to detect low frequencies, 2^14 = 16384 works great. But
966// then the detected frequencies can linger up to 16384/44100=372
967// milliseconds! We solve this by shrinking the signal amplitude over
968// time within the window. This way all frequencies are still there
969// but only recent time has high amplitude. This nicely displays even
970// quick sounds of a few milliseconds.
971
972// Static class members survive size changes for continuous display.
973// See the comment about s_image in WaveForm
974QImage Spectrogram::s_image {nullptr}; // picture of spectrogram
975int Spectrogram::s_offset {0}; // position on screen
976
978{
979 LOG(VB_GENERAL, LOG_INFO,
980 QString("Spectrogram : Being Initialised, history=%1").arg(hist));
981
982 m_history = hist; // historical spectrogram?, else spectrum only
983
984 m_fps = 40; // getting 1152 samples / 44100 = 38.28125 fps
985
986 m_color = gCoreContext->GetNumSetting("MusicSpectrogramColor", 0);
987
988 if (s_image.isNull()) // static histogram survives resize/restart
989 {
990 s_image = QImage(
991 m_sgsize.width(), m_sgsize.height(), QImage::Format_RGB32);
992 s_image.fill(Qt::black);
993 }
994 if (m_history)
995 {
996 m_image = &s_image;
997 }
998 else
999 {
1000 m_image = new QImage(
1001 m_sgsize.width(), m_sgsize.height(), QImage::Format_RGB32);
1002 m_image->fill(Qt::black);
1003 }
1004
1005 m_dftL = static_cast<float*>(av_malloc(sizeof(float) * m_fftlen));
1006 m_dftR = static_cast<float*>(av_malloc(sizeof(float) * m_fftlen));
1007 m_rdftTmp = static_cast<float*>(av_malloc(sizeof(float) * (m_fftlen + 2)));
1008
1009 // should probably check that this succeeds
1010 av_tx_init(&m_rdftContext, &m_rdft, AV_TX_FLOAT_RDFT, 0, m_fftlen, &kTxScale, 0x0);
1011
1012 // hack!!! Should 44100 sample rate be queried or measured?
1013 // Likely close enough for most audio recordings...
1015 m_sgsize.height() / 2 : m_sgsize.width(), 44100/2);
1016 m_sigL.resize(m_fftlen);
1017 m_sigR.resize(m_fftlen);
1018
1019 // TODO: promote this to a separate ColorSpectrum class
1020
1021 // cache a continuous color spectrum ([0-1535] = 256 colors * 6 ramps):
1022 // 0000ff blue from here, G of RGB ramps UP to:
1023 // 00ffff cyan then B ramps down to:
1024 // 00ff00 green then R ramps UP to:
1025 // ffff00 yellow then G ramps down to:
1026 // ff0000 red then B ramps UP to:
1027 // ff00ff magenta then R ramps down to:
1028 // 0000ff blue we end where we started!
1029 static constexpr int UP { 2 };
1030 static constexpr int DN { 3 };
1031 static const std::array<int,6> red { 0, 0, UP, 1, 1, DN }; // 0=OFF, 1=ON
1032 static const std::array<int,6> green { UP, 1, 1, DN, 0, 0 };
1033 static const std::array<int,6> blue { 1, DN, 0, 0, UP, 1 };
1034
1035 auto updateColor = [](int color, int up, int down)
1036 {
1037 if (color == UP)
1038 return up;
1039 if (color == DN)
1040 return down;
1041 return color * 255;
1042 };
1043
1044 for (int i = 0; i < 6; i++) // for 6 color transitions...
1045 {
1046 int r = red[i]; // 0=OFF, 1=ON, UP, or DN
1047 int g = green[i];
1048 int b = blue[i];
1049 for (int u = 0; u < 256; u++) { // u ramps up
1050 int d = 256 - u; // d ramps down
1051 m_red[ (i * 256) + u] = updateColor(r, u, d);
1052 m_green[(i * 256) + u] = updateColor(g, u, d);
1053 m_blue[ (i * 256) + u] = updateColor(b, u, d);
1054 }
1055 }
1056}
1057
1059{
1060 av_freep(reinterpret_cast<void*>(&m_dftL));
1061 av_freep(reinterpret_cast<void*>(&m_dftR));
1062 av_freep(reinterpret_cast<void*>(&m_rdftTmp));
1063 av_tx_uninit(&m_rdftContext);
1064}
1065
1066void Spectrogram::resize(const QSize &newsize)
1067{
1068 m_size = newsize;
1069}
1070
1071// this moved up from Spectrum so both can use it
1072template<typename T> T sq(T a) { return a*a; };
1073
1075{
1076 // maximum samples per update, may get less
1077 return (unsigned long)kSGAudioSize;
1078}
1079
1081{
1082 if (!m_showtext)
1083 return false;
1084
1085 QPainter painter(m_image);
1086 painter.setPen(Qt::white);
1087 QFont font = QApplication::font();
1088 font.setPixelSize(16);
1089 painter.setFont(font);
1090 int half = m_sgsize.height() / 2;
1091
1092 if (m_history)
1093 {
1094 for (auto h = m_sgsize.height(); h > 0; h -= half)
1095 {
1096 for (auto i = 0; i < half; i += 20)
1097 {
1098 painter.drawText(s_offset > m_sgsize.width() - 255
1099 ? s_offset - m_sgsize.width() : s_offset,
1100 h - i - 20, 255, 40,
1101 Qt::AlignRight|Qt::AlignVCenter,
1102 // QString("%1 %2").arg(m_scale[i])
1103 QString("%1")
1104 .arg(m_scale[i] * 22050 / (m_fftlen/2)));
1105 }
1106 }
1107 } else {
1108 // static std::array<int, 5> treble = {52, 55, 59, 62, 65};
1109 // for (auto i = 0; i < 5; i++) {
1110 // painter.drawLine(m_scale.pixel(treble[i]), 0,
1111 // m_scale.pixel(treble[i]), m_sgsize.height());
1112 // }
1113 for (auto i = 0; i < 125; i++) // 125 notes fit in 22050 Hz
1114 { // let Qt center the note text on the pixel
1115 painter.drawText(m_scale.pixel(i) - 20, half - ((i % 12) * 15) - 40,
1116 40, 40, Qt::AlignCenter, m_scale.note(i));
1117 if (i % 12 == 5) // octave numbers
1118 {
1119 painter.drawText(m_scale.pixel(i) - 20, half - 220,
1120 40, 40, Qt::AlignCenter,
1121 QString("%1").arg(i / 12));
1122 }
1123 }
1124 painter.rotate(90); // frequency in Hz draws down
1125 int prev = -30;
1126 for (auto i = 0; i < 125; i++) // 125 notes fit in 22050 Hz
1127 {
1128 if (i < 72 && m_scale.note(i) == ".") // skip low sharps
1129 continue;
1130 int now = m_scale.pixel(i);
1131 if (now >= prev + 20) { // skip until good spacing
1132 painter.drawText(half + 20, (-1 * now) - 40,
1133 80, 80, Qt::AlignVCenter|Qt::AlignLeft,
1134 QString("%1").arg(m_scale.freq(i)));
1135 if (m_scale.note(i) != ".")
1136 {
1137 painter.drawText(half + 100, (-1 * now) - 40,
1138 80, 80, Qt::AlignVCenter|Qt::AlignLeft,
1139 QString("%1%2").arg(m_scale.note(i))
1140 .arg(i / 12));
1141 }
1142 prev = now;
1143 }
1144 }
1145 }
1146 return false;
1147}
1148
1150{
1151 // as of v33, this processes *all* samples, see the comments in WaveForm
1152
1153 int w = m_sgsize.width(); // drawing size
1154 int h = m_sgsize.height();
1155 if (m_image->isNull())
1156 {
1157 m_image = new QImage(w, h, QImage::Format_RGB32);
1158 m_image->fill(Qt::black);
1159 s_offset = 0;
1160 }
1161 if (node) // shift previous samples left, then append new node
1162 {
1163 // LOG(VB_PLAYBACK, LOG_DEBUG, QString("SG got %1 samples").arg(node->m_length));
1164 int i = std::min((int)(node->m_length), m_fftlen);
1165 int start = m_fftlen - i;
1166 float mult = 0.8F; // decay older sound by this much
1167 for (int k = 0; k < start; k++)
1168 { // prior set ramps from mult to 1.0
1169 if (k > start - i && start > i)
1170 {
1171 mult = mult + ((1 - mult) *
1172 (1 - ((float)(start - k) / (float)(start - i))));
1173 }
1174 m_sigL[k] = mult * m_sigL[i + k];
1175 m_sigR[k] = mult * m_sigR[i + k];
1176 }
1177 for (int k = 0; k < i; k++) // append current samples
1178 {
1179 m_sigL[start + k] = node->m_left[k] / 32768.0F; // +/- 1 peak-to-peak
1180 if (node->m_right)
1181 m_sigR[start + k] = node->m_right[k] / 32768.0F;
1182 }
1183 int end = m_fftlen / 40; // ramp window ends down to zero crossing
1184 for (int k = 0; k < m_fftlen; k++)
1185 {
1186 if (k < end)
1187 mult = (float)k / (float)end;
1188 else if (k > m_fftlen - end)
1189 mult = (float)(m_fftlen - k) / (float)end;
1190 else
1191 mult = 1;
1192 m_dftL[k] = m_sigL[k] * mult;
1193 m_dftR[k] = m_sigR[k] * mult;
1194 }
1195 }
1196 // run the real FFT!
1197 m_rdft(m_rdftContext, m_rdftTmp, m_dftL, sizeof(float));
1199 memcpy(m_dftL, m_rdftTmp, m_fftlen * sizeof(float));
1200 m_rdft(m_rdftContext, m_rdftTmp, m_dftR, sizeof(float));
1202 memcpy(m_dftR, m_rdftTmp, m_fftlen * sizeof(float));
1203
1204 QPainter painter(m_image);
1205 painter.setPen(Qt::black); // clear prior content
1206 if (m_history)
1207 {
1208 painter.fillRect(s_offset, 0, 256, h, Qt::black);
1209 painter.fillRect(s_offset - w, 0, 256, h, Qt::black);
1210 } else {
1211 painter.fillRect(0, 0, w, h, Qt::black);
1212 }
1213
1214 int index = 1; // frequency index of this pixel
1215 int prev = 0; // frequency index of previous pixel
1216 float gain = 5.0; // compensate for window function loss
1217 for (int i = 1; i < (m_history ? h / 2 : w); i++)
1218 { // for each pixel of the spectrogram...
1219 float left = 0;
1220 float right = 0;
1221 float tmp = 0;
1222 int count = 0;
1223 for (ptrdiff_t j = prev + 1; j <= index; j++) // log scale!
1224 { // for the freqency bins of this pixel, find peak or mean
1225 tmp = sq(m_dftL[2 * j]) + sq(m_dftL[(2 * j) + 1]);
1226 left = m_binpeak ? std::max(tmp, left) : left + tmp;
1227 tmp = sq(m_dftR[2 * j]) + sq(m_dftR[(2 * j) + 1]);
1228 right = m_binpeak ? std::max(tmp, right) : right + tmp;
1229 count++;
1230 }
1231 if (!m_binpeak && count > 0)
1232 { // mean of the frequency bins
1233 left /= count;
1234 right /= count;
1235 }
1236 // linear magnitude: sqrt(sq(real) + sq(im));
1237 // left = sqrt(left);
1238 // right = sqrt(right);
1239 // power spectrum (dBm): 10 * std::log10(sq(real) + sq(im));
1240 left = 10 * std::log10(left);
1241 right = 10 * std::log10(right);
1242
1243 // float bw = 1. / (16384. / 44100.);
1244 // float freq = bw * index;
1245 // LOG(VB_PLAYBACK, LOG_DEBUG, // verbose - never use in production!!!
1246 // QString("SG i=%1, index=%2 (%3) %4 hz \tleft=%5,\tright=%6")
1247 // .arg(i).arg(index).arg(count).arg(freq).arg(left).arg(right));
1248
1249 left *= gain;
1250 int mag = clamp(left, 255, 0);
1251 int z = mag * 6;
1252 left > 255 ? painter.setPen(Qt::white) :
1253 painter.setPen(qRgb(m_red[z], m_green[z], m_blue[z]));
1254 if (m_history)
1255 {
1256 h = m_sgsize.height() / 2;
1257 painter.drawLine(s_offset, h - i, s_offset + mag, h - i);
1258 painter.drawLine(s_offset - w, h - i, s_offset - w + mag, h - i);
1259 if (m_color & 0x01) // left in color?
1260 {
1261 if (left > 255)
1262 painter.setPen(Qt::white);
1263 if (mag == 0)
1264 painter.setPen(Qt::black);
1265 } else {
1266 left > 255 ? painter.setPen(Qt::yellow) :
1267 painter.setPen(qRgb(mag, mag, mag));
1268 }
1269 painter.drawPoint(s_offset, h - i);
1270 } else {
1271 painter.drawLine(i, h / 2, i, (h / 2) - (h / 2 * mag / 256));
1272 }
1273
1274 right *= gain; // copy of above, s/left/right/g
1275 mag = clamp(right, 255, 0);
1276 z = mag * 6;
1277 right > 255 ? painter.setPen(Qt::white) :
1278 painter.setPen(qRgb(m_red[z], m_green[z], m_blue[z]));
1279 if (m_history)
1280 {
1281 h = m_sgsize.height();
1282 painter.drawLine(s_offset, h - i, s_offset + mag, h - i);
1283 painter.drawLine(s_offset - w, h - i, s_offset - w + mag, h - i);
1284 if (m_color & 0x02) // right in color?
1285 {
1286 if (left > 255)
1287 painter.setPen(Qt::white);
1288 if (mag == 0)
1289 painter.setPen(Qt::black);
1290 } else {
1291 right > 255 ? painter.setPen(Qt::yellow) :
1292 painter.setPen(qRgb(mag, mag, mag));
1293 }
1294 painter.drawPoint(s_offset, h - i);
1295 } else {
1296 painter.drawLine(i, h / 2, i, (h / 2) + (h / 2 * mag / 256));
1297 }
1298
1299 prev = index; // next pixel is FFT bins from here
1300 index = m_scale[i]; // to the next bin by LOG scale
1301 prev = std::min(prev, index - 1);
1302 }
1303 if (m_history && ++s_offset >= w)
1304 {
1305 s_offset = 0;
1306 }
1307 return false;
1308}
1309
1310double Spectrogram::clamp(double cur, double max, double min)
1311{
1312 if (std::isnan(cur)) return 0;
1313 return std::clamp(cur, min, max);
1314}
1315
1316bool Spectrogram::draw(QPainter *p, const QColor &back)
1317{
1318 p->fillRect(0, 0, 0, 0, back); // no clearing, here to suppress warning
1319 if (!m_image->isNull())
1320 { // background, updated by ::process above
1321 p->drawImage(0, 0, m_image->scaled(m_size,
1322 Qt::IgnoreAspectRatio,
1323 Qt::SmoothTransformation));
1324 }
1325 // // DEBUG: see the whole signal going into the FFT:
1326 // if (m_size.height() > 1000) {
1327 // p->setPen(Qt::yellow);
1328 // float h = m_size.height() / 10;
1329 // for (int j = 0; j < m_fftlen; j++) {
1330 // p->drawPoint(j % m_size.width(), int(j / m_size.width()) * h + h - h * m_sigL[j]);
1331 // }
1332 // }
1333 return true;
1334}
1335
1337{
1338 LOG(VB_PLAYBACK, LOG_DEBUG, QString("SG keypress = %1").arg(action));
1339
1340 if (action == "SELECT")
1341 {
1342 if (m_history)
1343 {
1344 m_color = (m_color + 1) & 0x03; // left and right color bits
1345 gCoreContext->SaveSetting("MusicSpectrogramColor",
1346 QString("%1").arg(m_color));
1347 }
1348 else
1349 {
1351 }
1352 }
1353 if (action == "2") // 1/3 is slower/faster, 2 should be unused
1354 {
1356 }
1357}
1358
1359// passing true to the above constructor gives the spectrum with full history:
1360
1361static class SpectrogramFactory : public VisFactory
1362{
1363 public:
1364 const QString &name(void) const override // VisFactory
1365 {
1366 static QString s_name = QCoreApplication::translate("Visualizers",
1367 "Spectrogram");
1368 return s_name;
1369 }
1370
1371 uint plugins(QStringList *list) const override // VisFactory
1372 {
1373 *list << name();
1374 return 1;
1375 }
1376
1377 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
1378 {
1379 return new Spectrogram(true); // history
1380 }
1382
1383// passing false to the above constructor gives the spectrum with no history:
1384
1386{
1387 public:
1388 const QString &name(void) const override // VisFactory
1389 {
1390 static QString s_name = QCoreApplication::translate("Visualizers",
1391 "Spectrum");
1392 return s_name;
1393 }
1394
1395 uint plugins(QStringList *list) const override // VisFactory
1396 {
1397 *list << name();
1398 return 1;
1399 }
1400
1401 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
1402 {
1403 return new Spectrogram(false); // no history
1404 }
1406
1408// Spectrum
1409//
1410
1412{
1413 LOG(VB_GENERAL, LOG_INFO, QString("Spectrum : Being Initialised"));
1414
1415 m_fps = 40; // getting 1152 samples / 44100 = 38.28125 fps
1416
1417 m_dftL = static_cast<float*>(av_malloc(sizeof(float) * m_fftlen));
1418 m_dftR = static_cast<float*>(av_malloc(sizeof(float) * m_fftlen));
1419 m_rdftTmp = static_cast<float*>(av_malloc(sizeof(float) * (m_fftlen + 2)));
1420
1421 // should probably check that this succeeds
1422 av_tx_init(&m_rdftContext, &m_rdft, AV_TX_FLOAT_RDFT, 0, m_fftlen, &kTxScale, 0x0);
1423}
1424
1426{
1427 av_freep(reinterpret_cast<void*>(&m_dftL));
1428 av_freep(reinterpret_cast<void*>(&m_dftR));
1429 av_freep(reinterpret_cast<void*>(&m_rdftTmp));
1430 av_tx_uninit(&m_rdftContext);
1431}
1432
1433void Spectrum::resize(const QSize &newsize)
1434{
1435 // Just change internal data about the
1436 // size of the pixmap to be drawn (ie. the
1437 // size of the screen) and the logically
1438 // ensuing number of up/down bars to hold
1439 // the audio magnitudes
1440
1441 m_size = newsize;
1442
1443 m_analyzerBarWidth = m_size.width() / 128;
1444
1446
1447 m_scale.setMax(m_fftlen/2, m_size.width() / m_analyzerBarWidth, 44100/2);
1448 m_sigL.resize(m_fftlen);
1449 m_sigR.resize(m_fftlen);
1450
1451 m_rectsL.resize( m_scale.range() );
1452 m_rectsR.resize( m_scale.range() );
1453 int w = 0;
1454 // NOLINTNEXTLINE(modernize-loop-convert)
1455 for (uint i = 0; i < (uint)m_rectsL.size(); i++, w += m_analyzerBarWidth)
1456 {
1457 m_rectsL[i].setRect(w, m_size.height() / 2, m_analyzerBarWidth - 1, 1);
1458 m_rectsR[i].setRect(w, m_size.height() / 2, m_analyzerBarWidth - 1, 1);
1459 }
1460
1461 m_magnitudes.resize( m_scale.range() * 2 );
1462 // NOLINTNEXTLINE(modernize-loop-convert)
1463 for (uint os = m_magnitudes.size(); os < (uint)m_magnitudes.size(); os++)
1464 {
1465 m_magnitudes[os] = 0.0;
1466 }
1467
1468 m_scaleFactor = m_size.height() / 2.0F / 42.0F;
1469}
1470
1471// this moved up to Spectrogram so both can use it
1472// template<typename T> T sq(T a) { return a*a; };
1473
1475{
1476 return false;
1477}
1478
1480{
1481 // copied from Spectrogram for better FFT window
1482
1483 if (node) // shift previous samples left, then append new node
1484 {
1485 // LOG(VB_PLAYBACK, LOG_DEBUG, QString("SG got %1 samples").arg(node->m_length));
1486 int i = std::min((int)(node->m_length), m_fftlen);
1487 int start = m_fftlen - i;
1488 float mult = 0.8F; // decay older sound by this much
1489 for (int k = 0; k < start; k++)
1490 { // prior set ramps from mult to 1.0
1491 if (k > start - i && start > i)
1492 {
1493 mult = mult + ((1 - mult) *
1494 (1 - ((float)(start - k) / (float)(start - i))));
1495 }
1496 m_sigL[k] = mult * m_sigL[i + k];
1497 m_sigR[k] = mult * m_sigR[i + k];
1498 }
1499 for (int k = 0; k < i; k++) // append current samples
1500 {
1501 m_sigL[start + k] = node->m_left[k] / 32768.0F; // +/- 1 peak-to-peak
1502 if (node->m_right)
1503 m_sigR[start + k] = node->m_right[k] / 32768.0F;
1504 }
1505 int end = m_fftlen / 40; // ramp window ends down to zero crossing
1506 for (int k = 0; k < m_fftlen; k++)
1507 {
1508 if (k < end)
1509 mult = static_cast<float>(k) / end;
1510 else if (k > m_fftlen - end)
1511 mult = static_cast<float>(m_fftlen - k) / end;
1512 else
1513 mult = 1;
1514 m_dftL[k] = m_sigL[k] * mult;
1515 m_dftR[k] = m_sigR[k] * mult;
1516 }
1517 }
1518 // run the real FFT!
1519 m_rdft(m_rdftContext, m_rdftTmp, m_dftL, sizeof(float));
1521 memcpy(m_dftL, m_rdftTmp, m_fftlen * sizeof(float));
1522 m_rdft(m_rdftContext, m_rdftTmp, m_dftR, sizeof(float));
1524 memcpy(m_dftR, m_rdftTmp, m_fftlen * sizeof(float));
1525
1526 QRect *rectspL = m_rectsL.data();
1527 QRect *rectspR = m_rectsR.data();
1528 float *magnitudesp = m_magnitudes.data();
1529
1530 int index = 1; // frequency index of this pixel
1531 int prev = 0; // frequency index of previous pixel
1532 float adjHeight = m_size.height() / 2.0F;
1533
1534 for (int i = 0; i < m_rectsL.size(); i++)
1535 {
1536 float magL = 0; // modified from Spectrogram
1537 float magR = 0;
1538 float tmp = 0;
1539 for (ptrdiff_t j = prev + 1; j <= index; j++) // log scale!
1540 { // for the freqency bins of this pixel, find peak or mean
1541 tmp = sq(m_dftL[2 * j]) + sq(m_dftL[(2 * j) + 1]);
1542 magL = tmp > magL ? tmp : magL;
1543 tmp = sq(m_dftR[2 * j]) + sq(m_dftR[(2 * j) + 1]);
1544 magR = tmp > magR ? tmp : magR;
1545 }
1546 magL = 10 * std::log10(magL) * m_scaleFactor;
1547 magR = 10 * std::log10(magR) * m_scaleFactor;
1548
1549 magL = std::min(magL, adjHeight);
1550 if (magL < magnitudesp[i])
1551 {
1552 tmp = magnitudesp[i] - m_falloff;
1553 tmp = std::max(tmp, magL);
1554 magL = tmp;
1555 }
1556 magL = std::max<float>(magL, 1);
1557
1558 magR = std::min(magR, adjHeight);
1559 if (magR < magnitudesp[i + m_scale.range()])
1560 {
1561 tmp = magnitudesp[i + m_scale.range()] - m_falloff;
1562 tmp = std::max(tmp, magR);
1563 magR = tmp;
1564 }
1565 magR = std::max<float>(magR, 1);
1566
1567 magnitudesp[i] = magL;
1568 magnitudesp[i + m_scale.range()] = magR;
1569 rectspL[i].setTop( (m_size.height() / 2) - int( magL ) );
1570 rectspR[i].setBottom( (m_size.height() / 2) + int( magR ) );
1571
1572 prev = index; // next pixel is FFT bins from here
1573 index = m_scale[i]; // to the next bin by LOG scale
1574 (prev < index) || (prev = index -1);
1575 }
1576
1577 return false;
1578}
1579
1580double Spectrum::clamp(double cur, double max, double min)
1581{
1582 return std::clamp(cur, min, max);
1583}
1584
1585bool Spectrum::draw(QPainter *p, const QColor &back)
1586{
1587 // This draws on a pixmap owned by MainVisual.
1588 //
1589 // In other words, this is not a Qt Widget, it
1590 // just uses some Qt methods to draw on a pixmap.
1591 // MainVisual then bitblts that onto the screen.
1592
1593 QRect *rectspL = m_rectsL.data();
1594 QRect *rectspR = m_rectsR.data();
1595
1596 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
1597 for (uint i = 0; i < (uint)m_rectsL.size(); i++)
1598 {
1599 double per = ( rectspL[i].height() - 2. ) / (m_size.height() / 2.);
1600
1601 per = clamp(per, 1.0, 0.0);
1602
1603 double r = m_startColor.red() +
1604 ((m_targetColor.red() - m_startColor.red()) * (per * per));
1605 double g = m_startColor.green() +
1606 ((m_targetColor.green() - m_startColor.green()) * (per * per));
1607 double b = m_startColor.blue() +
1608 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
1609
1610 r = clamp(r, 255.0, 0.0);
1611 g = clamp(g, 255.0, 0.0);
1612 b = clamp(b, 255.0, 0.0);
1613
1614 if(rectspL[i].height() > 4)
1615 p->fillRect(rectspL[i], QColor(int(r), int(g), int(b)));
1616
1617 per = ( rectspR[i].height() - 2. ) / (m_size.height() / 2.);
1618
1619 per = clamp(per, 1.0, 0.0);
1620
1621 r = m_startColor.red() +
1622 ((m_targetColor.red() - m_startColor.red()) * (per * per));
1623 g = m_startColor.green() +
1624 ((m_targetColor.green() - m_startColor.green()) * (per * per));
1625 b = m_startColor.blue() +
1626 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
1627
1628 r = clamp(r, 255.0, 0.0);
1629 g = clamp(g, 255.0, 0.0);
1630 b = clamp(b, 255.0, 0.0);
1631
1632 if(rectspR[i].height() > 4)
1633 p->fillRect(rectspR[i], QColor(int(r), int(g), int(b)));
1634 }
1635
1636 return true;
1637}
1638
1639static class SpectrumFactory : public VisFactory
1640{
1641 public:
1642 const QString &name(void) const override // VisFactory
1643 {
1644 static QString s_name = QCoreApplication::translate("Visualizers",
1645 "SpectrumBars");
1646 return s_name;
1647 }
1648
1649 uint plugins(QStringList *list) const override // VisFactory
1650 {
1651 *list << name();
1652 return 1;
1653 }
1654
1655 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
1656 {
1657 return new Spectrum();
1658 }
1660
1662// Squares
1663//
1664
1666{
1668}
1669
1670void Squares::resize (const QSize &newsize) {
1671 // Trick the spectrum analyzer into calculating 16 rectangles
1673 // We have our own copy, Spectrum has it's own...
1674 m_actualSize = newsize;
1675}
1676
1677void Squares::drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h)
1678{
1679 double per = NAN;
1680 int correction = (m_actualSize.width() % m_rectsL.size ());
1681 int x = ((i / 2) * w) + correction;
1682 int y = 0;
1683
1684 if (i % 2 == 0)
1685 {
1686 y = c - h;
1687 per = double(m_fakeHeight - rect->top()) / double(m_fakeHeight);
1688 }
1689 else
1690 {
1691 y = c;
1692 per = double(rect->bottom()) / double(m_fakeHeight);
1693 }
1694
1695 per = clamp(per, 1.0, 0.0);
1696
1697 double r = m_startColor.red() +
1698 ((m_targetColor.red() - m_startColor.red()) * (per * per));
1699 double g = m_startColor.green() +
1700 ((m_targetColor.green() - m_startColor.green()) * (per * per));
1701 double b = m_startColor.blue() +
1702 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
1703
1704 r = clamp(r, 255.0, 0.0);
1705 g = clamp(g, 255.0, 0.0);
1706 b = clamp(b, 255.0, 0.0);
1707
1708 p->fillRect (x, y, w, h, QColor (int(r), int(g), int(b)));
1709}
1710
1711bool Squares::draw(QPainter *p, const QColor &back)
1712{
1713 p->fillRect (0, 0, m_actualSize.width(), m_actualSize.height(), back);
1714 int w = m_actualSize.width() / (m_rectsL.size() / 2);
1715 int h = w;
1716 int center = m_actualSize.height() / 2;
1717
1718 QRect *rectsp = m_rectsL.data();
1719 for (uint i = 0; i < (uint)m_rectsL.size() * 2; i += 2)
1720 drawRect(p, &(rectsp[i]), i, center, w, h);
1721 rectsp = m_rectsR.data();
1722 for (uint i = 1; i < ((uint)m_rectsR.size() * 2) + 1; i += 2)
1723 drawRect(p, &(rectsp[i]), i, center, w, h);
1724
1725 return true;
1726}
1727
1728static class SquaresFactory : public VisFactory
1729{
1730 public:
1731 const QString &name(void) const override // VisFactory
1732 {
1733 static QString s_name = QCoreApplication::translate("Visualizers",
1734 "Squares");
1735 return s_name;
1736 }
1737
1738 uint plugins(QStringList *list) const override // VisFactory
1739 {
1740 *list << name();
1741 return 1;
1742 }
1743
1744 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
1745 {
1746 return new Squares();
1747 }
1749
1750
1752{
1753 // Setup the "magical" audio coefficients
1754 // required by the Goetzel Algorithm
1755
1756 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Being Initialised"));
1757
1758 double sample_rate = 44100.0; // TODO : This should be obtained from gPlayer (likely candidate...)
1759
1760 m_fps = 20; // This is the display frequency. We're capturing all audio chunks by defining .process_undisplayed() though.
1761
1762 double concert_A = 440.0;
1763 double semi_tone = pow(2.0, 1.0/12.0);
1764
1765 /* Lowest note on piano is 4 octaves below concert A */
1766 double bottom_A = concert_A / 2.0 / 2.0 / 2.0 / 2.0;
1767
1768 double current_freq = bottom_A;
1769 for (uint key = 0; key < kPianoNumKeys; key++)
1770 {
1771 // This is constant through time
1772 m_pianoData[key].coeff = (goertzel_data)(2.0 * cos(2.0 * M_PI * current_freq / sample_rate));
1773
1774 // Want 20 whole cycles of the current waveform at least
1775 double samples_required = sample_rate/current_freq * 20.0;
1776 // For the really low notes, 4 updates a second is good enough...
1777 // For the high notes, use as many samples as we need in a display_fps
1778 samples_required = std::clamp(samples_required,
1779 sample_rate/(double)m_fps * 0.75,
1780 sample_rate/4.0);
1781 m_pianoData[key].samples_process_before_display_update = (int)samples_required;
1782 m_pianoData[key].is_black_note = false; // Will be put right in .resize()
1783
1784 current_freq *= semi_tone;
1785 }
1786
1787 zero_analysis();
1788}
1789
1791{
1792 for (uint key = 0; key < kPianoNumKeys; key++)
1793 {
1794 // These get updated continously, and must be stored between chunks of audio data
1795 m_pianoData[key].q2 = 0.0F;
1796 m_pianoData[key].q1 = 0.0F;
1797 m_pianoData[key].magnitude = 0.0F;
1798 m_pianoData[key].max_magnitude_seen =
1799 (goertzel_data)(kPianoRmsNegligible * kPianoRmsNegligible); // This is a guess - will be quickly overwritten
1800
1801 m_pianoData[key].samples_processed = 0;
1802 }
1803 m_offsetProcessed = 0ms;
1804}
1805
1806void Piano::resize(const QSize &newsize)
1807{
1808 // Just change internal data about the
1809 // size of the pixmap to be drawn (ie. the
1810 // size of the screen) and the logically
1811 // ensuing number of up/down bars to hold
1812 // the audio magnitudes
1813
1814 m_size = newsize;
1815
1816 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Being Resized"));
1817
1818 zero_analysis();
1819
1820 // There are 88-36=52 white notes on piano keyboard
1821 double key_unit_size = (double)m_size.width() / 54.0; // One white key extra spacing, if possible
1822 key_unit_size = std::max(key_unit_size, 10.0); // Keys have to be at least this many pixels wide
1823
1824 double white_width_pct = .8;
1825 double black_width_pct = .6;
1826 double black_offset_pct = .05;
1827
1828 double white_height_pct = 6;
1829 double black_height_pct = 4;
1830
1831 // This is the starting position of the keyboard (may be beyond LHS)
1832 // - actually position of C below bottom A (will be added to...). This is 4 octaves below middle C.
1833 double left = ((double)m_size.width() / 2.0) - (((4.0*7.0) + 3.5) * key_unit_size); // The extra 3.5 centers 'F' inthe middle of the screen
1834 double top_of_keys = ((double)m_size.height() / 2.0) - (key_unit_size * white_height_pct / 2.0); // Vertically center keys
1835
1836 m_rects.resize(kPianoNumKeys);
1837
1838 for (uint key = 0; key < kPianoNumKeys; key++)
1839 {
1840 int note = ((int)key - 3 + 12) % 12; // This means that C=0, C#=1, D=2, etc (since lowest note is bottom A)
1841 if (note == 0) // If we're on a 'C', move the left 'cursor' over an octave
1842 {
1843 left += key_unit_size*7.0;
1844 }
1845
1846 double center = 0.0;
1847 double offset = 0.0;
1848 bool is_black = false;
1849
1850 switch (note)
1851 {
1852 case 0: center = 0.5; break;
1853 case 1: center = 1.0; is_black = true; offset = -1; break;
1854 case 2: center = 1.5; break;
1855 case 3: center = 2.0; is_black = true; offset = +1; break;
1856 case 4: center = 2.5; break;
1857 case 5: center = 3.5; break;
1858 case 6: center = 4.0; is_black = true; offset = -2; break;
1859 case 7: center = 4.5; break;
1860 case 8: center = 5.0; is_black = true; offset = 0; break;
1861 case 9: center = 5.5; break;
1862 case 10: center = 6.0; is_black = true; offset = 2; break;
1863 case 11: center = 6.5; break;
1864 }
1865 m_pianoData[key].is_black_note = is_black;
1866
1867 double width = (is_black ? black_width_pct:white_width_pct) * key_unit_size;
1868 double height = (is_black? black_height_pct:white_height_pct) * key_unit_size;
1869
1870 m_rects[key].setRect(
1871 left + (center * key_unit_size) // Basic position of left side of key
1872 - (width / 2.0) // Less half the width
1873 + (is_black ? (offset * black_offset_pct * key_unit_size):0.0), // And jiggle the positions of the black keys for aethetic reasons
1874 top_of_keys, // top
1875 width, // width
1876 height // height
1877 );
1878 }
1879
1880 m_magnitude.resize(kPianoNumKeys);
1881 for (double & key : m_magnitude)
1882 key = 0.0;
1883}
1884
1885unsigned long Piano::getDesiredSamples(void)
1886{
1887 // We want all the data! (within reason)
1888 // typical observed values are 882 -
1889 // 12.5 chunks of data per second from 44100Hz signal : Sampled at 50Hz, lots of 4, see :
1890 // mythtv/libs/libmythtv/audio/audiooutputbase.cpp :: AudioOutputBase::AddData
1891 // See : mythtv/mythplugins/mythmusic/mythmusic/avfdecoder.cpp "20ms worth"
1892 return (unsigned long) kPianoAudioSize; // Maximum we can be given
1893}
1894
1896{
1897 //LOG(VB_GENERAL, LOG_INFO, QString("Piano : Processing undisplayed node"));
1898 return process_all_types(node, false);
1899}
1900
1902{
1903 //LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Processing node for DISPLAY"));
1904
1905 // See WaveForm::process* above
1906 // return process_all_types(node, true);
1907
1908 return false;
1909}
1910
1911bool Piano::process_all_types(VisualNode *node, bool /*this_will_be_displayed*/)
1912{
1913 // Take a bunch of data in *node and break it down into piano key spectrum values
1914 // NB: Remember the state data between calls, so as to accumulate more accurate results.
1915 bool allZero = true;
1916 uint n = 0;
1917
1918 if (node)
1919 {
1920 piano_audio short_to_bounded = 32768.0F;
1921
1922 // Detect start of new song (current node more than 10s earlier than already seen)
1923 if (node->m_offset + 10s < m_offsetProcessed)
1924 {
1925 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Node offset=%1 too far backwards : NEW SONG").arg(node->m_offset.count()));
1926 zero_analysis();
1927 }
1928
1929 // Check whether we've seen this node (more recently than 10secs ago)
1930 if (node->m_offset <= m_offsetProcessed)
1931 {
1932 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Already seen node offset=%1, returning without processing").arg(node->m_offset.count()));
1933 return allZero; // Nothing to see here - the server can stop if it wants to
1934 }
1935
1936 //LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Processing node offset=%1, size=%2").arg(node->m_offset).arg(node->m_length));
1937 n = node->m_length;
1938
1939 if (node->m_right) // Preprocess the data into a combined middle channel, if we have stereo data
1940 {
1941 for (uint i = 0; i < n; i++)
1942 {
1943 m_audioData[i] = ((piano_audio)node->m_left[i] + (piano_audio)node->m_right[i]) / 2.0F / short_to_bounded;
1944 }
1945 }
1946 else // This is only one channel of data
1947 {
1948 for (uint i = 0; i < n; i++)
1949 {
1950 m_audioData[i] = (piano_audio)node->m_left[i] / short_to_bounded;
1951 }
1952 }
1953 }
1954 else
1955 {
1956 LOG(VB_GENERAL, LOG_DEBUG, QString("Hit an empty node, and returning empty-handed"));
1957 return allZero; // Nothing to see here - the server can stop if it wants to
1958 }
1959
1960 for (uint key = 0; key < kPianoNumKeys; key++)
1961 {
1962 goertzel_data coeff = m_pianoData[key].coeff;
1963 goertzel_data q2 = m_pianoData[key].q2;
1964 goertzel_data q1 = m_pianoData[key].q1;
1965
1966 for (uint i = 0; i < n; i++)
1967 {
1968 goertzel_data q0 = (coeff * q1) - q2 + m_audioData[i];
1969 q2 = q1;
1970 q1 = q0;
1971 }
1972 m_pianoData[key].q2 = q2;
1973 m_pianoData[key].q1 = q1;
1974
1975 m_pianoData[key].samples_processed += n;
1976
1977 int n_samples = m_pianoData[key].samples_processed;
1978
1979 // Only do this update if we've processed enough chunks for this key...
1980 if (n_samples > m_pianoData[key].samples_process_before_display_update)
1981 {
1982 goertzel_data magnitude2 = (q1*q1) + (q2*q2) - (q1*q2*coeff);
1983
1984#if 0
1985 // This is RMS of signal
1986 goertzel_data magnitude_av =
1987 sqrt(magnitude2)/(goertzel_data)n_samples; // Should be 0<magnitude_av<.5
1988#else
1989 // This is pure magnitude of signal
1990 goertzel_data magnitude_av =
1991 magnitude2/(goertzel_data)n_samples/(goertzel_data)n_samples; // Should be 0<magnitude_av<.25
1992#endif
1993
1994#if 0
1995 // Take logs everywhere, and shift up to [0, ??]
1996 if(magnitude_av > 0.0F)
1997 {
1998 magnitude_av = log(magnitude_av);
1999 }
2000 else
2001 {
2002 magnitude_av = kPianoMinVol;
2003 }
2004 magnitude_av -= kPianoMinVol;
2005
2006 if (magnitude_av < 0.0F)
2007 {
2008 magnitude_av = 0.0;
2009 }
2010#endif
2011
2012 if (magnitude_av > (goertzel_data)0.01)
2013 {
2014 allZero = false;
2015 }
2016
2017 m_pianoData[key].magnitude = magnitude_av; // Store this for later : We'll do the colours from this...
2018 m_pianoData[key].max_magnitude_seen =
2019 std::max(m_pianoData[key].max_magnitude_seen, magnitude_av);
2020 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Updated Key %1 from %2 samples, magnitude=%3")
2021 .arg(key).arg(n_samples).arg(magnitude_av));
2022
2023 m_pianoData[key].samples_processed = 0; // Reset the counts, now that we've set the magnitude...
2024 m_pianoData[key].q1 = (goertzel_data)0.0;
2025 m_pianoData[key].q2 = (goertzel_data)0.0;
2026 }
2027 }
2028
2029 if (node)
2030 {
2031 // All done now - record that we've done this offset
2033 }
2034
2035 return allZero;
2036}
2037
2038double Piano::clamp(double cur, double max, double min)
2039{
2040 return std::clamp(cur, min, max);
2041}
2042
2043bool Piano::draw(QPainter *p, const QColor &back)
2044{
2045 // This draws on a pixmap owned by MainVisual.
2046 //
2047 // In other words, this is not a Qt Widget, it
2048 // just uses some Qt methods to draw on a pixmap.
2049 // MainVisual then bitblts that onto the screen.
2050
2051 QRect *rectsp = (m_rects).data();
2052 double *magnitudep = (m_magnitude).data();
2053
2054 unsigned int n = kPianoNumKeys;
2055
2056 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
2057
2058 // Protect maximum array length
2059 n = std::min(n, (uint)m_rects.size());
2060
2061 // Sweep up across the keys, making sure the max_magnitude_seen is at minimum X% of its neighbours
2062 double mag = kPianoRmsNegligible;
2063 for (uint key = 0; key < n; key++)
2064 {
2065 if (m_pianoData[key].max_magnitude_seen < static_cast<float>(mag))
2066 {
2067 // Spread the previous value to this key
2068 m_pianoData[key].max_magnitude_seen = mag;
2069 }
2070 else
2071 {
2072 // This key has seen better peaks, use this for the next one
2073 mag = m_pianoData[key].max_magnitude_seen;
2074 }
2076 }
2077
2078 // Similarly, down, making sure the max_magnitude_seen is at minimum X% of its neighbours
2079 mag = kPianoRmsNegligible;
2080 for (int key_i = n - 1; key_i >= 0; key_i--)
2081 {
2082 uint key = key_i; // Wow, this is to avoid a zany error for ((unsigned)0)--
2083 if (m_pianoData[key].max_magnitude_seen < static_cast<float>(mag))
2084 {
2085 // Spread the previous value to this key
2086 m_pianoData[key].max_magnitude_seen = mag;
2087 }
2088 else
2089 {
2090 // This key has seen better peaks, use this for the next one
2091 mag = m_pianoData[key].max_magnitude_seen;
2092 }
2094 }
2095
2096 // Now find the key that has been hit the hardest relative to its experience, and renormalize...
2097 // Set a minimum, to prevent divide-by-zero (and also all-pressed when music very quiet)
2098 double magnitude_max = kPianoRmsNegligible;
2099 for (uint key = 0; key < n; key++)
2100 {
2101 mag = m_pianoData[key].magnitude / m_pianoData[key].max_magnitude_seen;
2102 magnitude_max = std::max(magnitude_max, mag);
2103
2104 magnitudep[key] = mag;
2105 }
2106
2107 // Deal with all the white keys first
2108 for (uint key = 0; key < n; key++)
2109 {
2110 if (m_pianoData[key].is_black_note)
2111 continue;
2112
2113 double per = magnitudep[key] / magnitude_max;
2114 per = clamp(per, 1.0, 0.0); // By construction, this should be unnecessary
2115
2116 if (per < kPianoKeypressTooLight)
2117 per = 0.0; // Clamp to zero for lightly detected keys
2118 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Display key %1, magnitude=%2, seen=%3")
2119 .arg(key).arg(per*100.0).arg(m_pianoData[key].max_magnitude_seen));
2120
2121 double r = m_whiteStartColor.red() + ((m_whiteTargetColor.red() - m_whiteStartColor.red()) * per);
2122 double g = m_whiteStartColor.green() + ((m_whiteTargetColor.green() - m_whiteStartColor.green()) * per);
2123 double b = m_whiteStartColor.blue() + ((m_whiteTargetColor.blue() - m_whiteStartColor.blue()) * per);
2124
2125 p->fillRect(rectsp[key], QColor(int(r), int(g), int(b)));
2126 }
2127
2128 // Then overlay the black keys
2129 for (uint key = 0; key < n; key++)
2130 {
2131 if (!m_pianoData[key].is_black_note)
2132 continue;
2133
2134 double per = magnitudep[key]/magnitude_max;
2135 per = clamp(per, 1.0, 0.0); // By construction, this should be unnecessary
2136
2137 if (per < kPianoKeypressTooLight)
2138 per = 0.0; // Clamp to zero for lightly detected keys
2139
2140 double r = m_blackStartColor.red() + ((m_blackTargetColor.red() - m_blackStartColor.red()) * per);
2141 double g = m_blackStartColor.green() + ((m_blackTargetColor.green() - m_blackStartColor.green()) * per);
2142 double b = m_blackStartColor.blue() + ((m_blackTargetColor.blue() - m_blackStartColor.blue()) * per);
2143
2144 p->fillRect(rectsp[key], QColor(int(r), int(g), int(b)));
2145 }
2146
2147 return true;
2148}
2149
2150static class PianoFactory : public VisFactory
2151{
2152 public:
2153 const QString &name(void) const override // VisFactory
2154 {
2155 static QString s_name = QCoreApplication::translate("Visualizers",
2156 "Piano");
2157 return s_name;
2158 }
2159
2160 uint plugins(QStringList *list) const override // VisFactory
2161 {
2162 *list << name();
2163 return 1;
2164 }
2165
2166 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
2167 {
2168 return new Piano();
2169 }
2171
2173 m_lastCycle(QDateTime::currentDateTime())
2174{
2176 m_fps = 1;
2177}
2178
2180{
2182 return;
2183
2184 // if a front cover image is available show that first
2186 if (albumArt->getImage(IT_FRONTCOVER))
2187 {
2189 }
2190 else
2191 {
2192 // not available so just show the first image available
2193 if (albumArt->getImageCount() > 0)
2194 m_currImageType = albumArt->getImageAt(0)->m_imageType;
2195 else
2197 }
2198}
2199
2200static int nextType (int type)
2201{
2202 type++;
2203 return (type != IT_LAST) ? type : IT_UNKNOWN;
2204}
2205
2207{
2209 return false;
2210
2212
2213 // If we only have one image there is nothing to cycle
2214 if (albumArt->getImageCount() < 2)
2215 return false;
2216
2217 int newType = nextType(m_currImageType);
2218 while (!albumArt->getImage((ImageType) newType))
2219 newType = nextType(newType);
2220
2221 if (newType != m_currImageType)
2222 {
2223 m_currImageType = (ImageType) newType;
2224 m_lastCycle = QDateTime::currentDateTime();
2225 return true;
2226 }
2227
2228 return false;
2229}
2230
2231void AlbumArt::resize(const QSize &newsize)
2232{
2233 m_size = newsize;
2234}
2235
2237{
2238 return false;
2239}
2240
2242{
2243 if (action == "SELECT")
2244 {
2246 {
2248 int newType = m_currImageType;
2249
2250 if (albumArt.getImageCount() > 0)
2251 {
2252 newType++;
2253
2254 while (!albumArt.getImage((ImageType) newType))
2255 {
2256 newType++;
2257 if (newType == IT_LAST)
2258 newType = IT_UNKNOWN;
2259 }
2260 }
2261
2262 if (newType != m_currImageType)
2263 {
2264 m_currImageType = (ImageType) newType;
2265 // force an update
2266 m_cursize = QSize(0, 0);
2267 }
2268 }
2269 }
2270}
2271
2273static constexpr qint64 ALBUMARTCYCLETIME { 10 };
2274
2276{
2277 // if the track has changed we need to update the image
2279 {
2282 return true;
2283 }
2284
2285 // if it's time to cycle to the next image we need to update the image
2286 if (m_lastCycle.addSecs(ALBUMARTCYCLETIME) < QDateTime::currentDateTime())
2287 {
2288 if (cycleImage())
2289 return true;
2290 }
2291
2292 return false;
2293}
2294
2295bool AlbumArt::draw(QPainter *p, const QColor &back)
2296{
2297 if (needsUpdate())
2298 {
2299 QImage art;
2300 QString imageFilename = gPlayer->getCurrentMetadata()->getAlbumArtFile(m_currImageType);
2301
2302 if (imageFilename.startsWith("myth://"))
2303 {
2304 auto *rf = new RemoteFile(imageFilename, false, false, 0ms);
2305
2306 QByteArray data;
2307 bool ret = rf->SaveAs(data);
2308
2309 delete rf;
2310
2311 if (ret)
2312 art.loadFromData(data);
2313 }
2314 else
2315 if (!imageFilename.isEmpty())
2316 {
2317 art.load(imageFilename);
2318 }
2319
2320 if (art.isNull())
2321 {
2322 m_cursize = m_size;
2323 m_image = QImage();
2324 }
2325 else
2326 {
2327 m_image = art.scaled(m_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
2328 }
2329 }
2330
2331 if (m_image.isNull())
2332 {
2333 drawWarning(p, back, m_size, tr("?"), 100);
2334 return true;
2335 }
2336
2337 // Paint the image
2338 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
2339 p->drawImage((m_size.width() - m_image.width()) / 2,
2340 (m_size.height() - m_image.height()) / 2,
2341 m_image);
2342
2343 // Store our new size
2344 m_cursize = m_size;
2345
2346 return true;
2347}
2348
2349static class AlbumArtFactory : public VisFactory
2350{
2351 public:
2352 const QString &name(void) const override // VisFactory
2353 {
2354 static QString s_name = QCoreApplication::translate("Visualizers",
2355 "AlbumArt");
2356 return s_name;
2357 }
2358
2359 uint plugins(QStringList *list) const override // VisFactory
2360 {
2361 *list << name();
2362 return 1;
2363 }
2364
2365 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
2366 {
2367 return new AlbumArt();
2368 }
2370
2372 : VisualBase(true)
2373{
2374 m_fps = 1;
2375}
2376
2377void Blank::resize(const QSize &newsize)
2378{
2379 m_size = newsize;
2380}
2381
2382
2384{
2385 return false;
2386}
2387
2388bool Blank::draw(QPainter *p, const QColor &back)
2389{
2390 // Took me hours to work out this algorithm
2391 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
2392 return true;
2393}
2394
2395static class BlankFactory : public VisFactory
2396{
2397 public:
2398 const QString &name(void) const override // VisFactory
2399 {
2400 static QString s_name = QCoreApplication::translate("Visualizers",
2401 "Blank");
2402 return s_name;
2403 }
2404
2405 uint plugins(QStringList *list) const override // VisFactory
2406 {
2407 *list << name();
2408 return 1;
2409 }
2410
2411 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
2412 {
2413 return new Blank();
2414 }
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:2365
const QString & name(void) const override
Definition: visualize.cpp:2352
uint plugins(QStringList *list) const override
Definition: visualize.cpp:2359
ImageType m_imageType
Definition: musicmetadata.h:53
uint getImageCount()
AlbumArtImage * getImage(ImageType type)
AlbumArtImage * getImageAt(uint index)
void resize(const QSize &size) override
Definition: visualize.cpp:2231
void findFrontCover(void)
Definition: visualize.cpp:2179
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:2295
AlbumArt(void)
Definition: visualize.cpp:2172
bool process(VisualNode *node=nullptr) override
Definition: visualize.cpp:2236
bool needsUpdate(void)
Definition: visualize.cpp:2275
void handleKeyPress(const QString &action) override
Definition: visualize.cpp:2241
QDateTime m_lastCycle
Definition: visualize.h:427
QSize m_cursize
Definition: visualize.h:422
QImage m_image
Definition: visualize.h:424
bool cycleImage(void)
Definition: visualize.cpp:2206
MusicMetadata * m_currentMetadata
Definition: visualize.h:426
QSize m_size
Definition: visualize.h:421
ImageType m_currImageType
Definition: visualize.h:423
uint plugins(QStringList *list) const override
Definition: visualize.cpp:2405
const QString & name(void) const override
Definition: visualize.cpp:2398
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:2411
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:2388
void resize(const QSize &size) override
Definition: visualize.cpp:2377
bool process(VisualNode *node=nullptr) override
Definition: visualize.cpp:2383
QSize m_size
Definition: visualize.h:442
int m_scale
Definition: visualize.h:196
LogScale(int maxscale=0, int maxrange=0)
int operator[](int index) const
std::vector< int > m_indices
int range() const
void setMax(int maxscale, int maxrange)
int m_range
Definition: visualize.h:197
int m_range
Definition: visualize.h:223
std::vector< int > m_indices
Definition: visualize.h:217
int freq(int note)
Definition: visualize.cpp:222
int pixel(int note)
Definition: visualize.cpp:215
MelScale(int maxscale=0, int maxrange=0, int maxfreq=0)
Definition: visualize.cpp:163
int range() const
Definition: visualize.h:206
void setMax(int maxscale, int maxrange, int maxfreq)
Definition: visualize.cpp:168
static double hz2mel(double hz)
Definition: visualize.h:209
std::array< QString, 12 > m_notes
Definition: visualize.h:219
std::array< int, 126 > m_pixels
Definition: visualize.h:220
std::array< int, 126 > m_freqs
Definition: visualize.h:221
int operator[](int index)
Definition: visualize.cpp:203
static double mel2hz(double mel)
Definition: visualize.h:210
int m_scale
Definition: visualize.h:222
QString note(int note)
Definition: visualize.cpp:208
uint plugins(QStringList *list) const override
Definition: visualize.cpp:923
const QString & name(void) const override
Definition: visualize.cpp:916
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:929
bool draw(QPainter *p, const QColor &back) override
Definition: visualize.cpp:561
bool process(VisualNode *node) override
Definition: visualize.cpp:470
std::chrono::milliseconds Length() const
QString getAlbumArtFile(void)
IdType ID() const
AlbumArtImages * getAlbumArtImages(void)
MusicMetadata * getCurrentMetadata(void)
get the metadata for the current track in the playlist
PlayMode getPlayMode(void)
Definition: musicplayer.h:77
void SaveSetting(const QString &key, int newValue)
int GetNumSetting(const QString &key, int defaultval=0)
static void DisableScreensaver()
static void RestoreScreensaver()
void GetScalingFactors(float &Horizontal, float &Vertical) const
uint plugins(QStringList *list) const override
Definition: visualize.cpp:2160
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:2166
const QString & name(void) const override
Definition: visualize.cpp:2153
QColor m_blackStartColor
Definition: visualize.h:389
static constexpr goertzel_data kPianoMinVol
Definition: visualize.h:352
static constexpr unsigned int kPianoNumKeys
Definition: visualize.h:345
static constexpr unsigned long kPianoAudioSize
Definition: visualize.h:344
static constexpr double kPianoSpectrumSmoothing
Definition: visualize.h:351
unsigned long getDesiredSamples(void) override
Definition: visualize.cpp:1885
QSize m_size
Definition: visualize.h:393
void zero_analysis(void)
Definition: visualize.cpp:1790
QColor m_whiteStartColor
Definition: visualize.h:387
std::array< piano_key_data, kPianoNumKeys > m_pianoData
Definition: visualize.h:397
std::chrono::milliseconds m_offsetProcessed
Definition: visualize.h:395
std::vector< double > m_magnitude
Definition: visualize.h:400
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:2043
std::array< piano_audio, kPianoAudioSize > m_audioData
Definition: visualize.h:398
bool processUndisplayed(VisualNode *node) override
Definition: visualize.cpp:1895
bool process(VisualNode *node) override
Definition: visualize.cpp:1901
static double clamp(double cur, double max, double min)
Definition: visualize.cpp:2038
static constexpr double kPianoRmsNegligible
Definition: visualize.h:350
QColor m_blackTargetColor
Definition: visualize.h:390
QColor m_whiteTargetColor
Definition: visualize.h:388
void resize(const QSize &size) override
Definition: visualize.cpp:1806
std::vector< QRect > m_rects
Definition: visualize.h:392
bool process_all_types(VisualNode *node, bool this_will_be_displayed)
Definition: visualize.cpp:1911
static constexpr double kPianoKeypressTooLight
Definition: visualize.h:353
uint plugins(QStringList *list) const override
Definition: visualize.cpp:1371
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:1377
const QString & name(void) const override
Definition: visualize.cpp:1364
bool process(VisualNode *node) override
Definition: visualize.cpp:1080
static constexpr int kSGAudioSize
Definition: visualize.h:231
float * m_rdftTmp
Definition: visualize.h:260
QSize m_sgsize
Definition: visualize.h:251
std::array< int, 256_Z *6 > m_red
Definition: visualize.h:270
bool m_showtext
Definition: visualize.h:276
unsigned long getDesiredSamples(void) override
Definition: visualize.cpp:1074
int m_fftlen
Definition: visualize.h:254
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:1316
~Spectrogram() override
Definition: visualize.cpp:1058
float * m_dftL
Definition: visualize.h:258
float * m_dftR
Definition: visualize.h:259
void handleKeyPress(const QString &action) override
Definition: visualize.cpp:1336
static constexpr float kTxScale
Definition: visualize.h:261
Spectrogram(bool hist)
Definition: visualize.cpp:977
static QImage s_image
Definition: visualize.h:245
bool m_binpeak
Definition: visualize.h:274
std::array< int, 256_Z *6 > m_blue
Definition: visualize.h:272
QSize m_size
Definition: visualize.h:252
av_tx_fn m_rdft
Definition: visualize.h:263
QVector< float > m_sigR
Definition: visualize.h:257
QImage * m_image
Definition: visualize.h:250
void resize(const QSize &size) override
Definition: visualize.cpp:1066
bool processUndisplayed(VisualNode *node) override
Definition: visualize.cpp:1149
static int s_offset
Definition: visualize.h:246
QVector< float > m_sigL
Definition: visualize.h:256
bool m_history
Definition: visualize.h:275
MelScale m_scale
Definition: visualize.h:253
static double clamp(double cur, double max, double min)
Definition: visualize.cpp:1310
AVTXContext * m_rdftContext
Definition: visualize.h:262
std::array< int, 256_Z *6 > m_green
Definition: visualize.h:271
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:1401
const QString & name(void) const override
Definition: visualize.cpp:1388
uint plugins(QStringList *list) const override
Definition: visualize.cpp:1395
uint plugins(QStringList *list) const override
Definition: visualize.cpp:1649
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:1655
const QString & name(void) const override
Definition: visualize.cpp:1642
void resize(const QSize &size) override
Definition: visualize.cpp:1433
int m_fftlen
Definition: visualize.h:311
~Spectrum() override
Definition: visualize.cpp:1425
bool process(VisualNode *node) override
Definition: visualize.cpp:1474
QVector< float > m_magnitudes
Definition: visualize.h:301
QColor m_targetColor
Definition: visualize.h:298
QVector< QRect > m_rectsL
Definition: visualize.h:299
bool processUndisplayed(VisualNode *node) override
Definition: visualize.cpp:1479
AVTXContext * m_rdftContext
Definition: visualize.h:318
MelScale m_scale
Definition: visualize.h:303
int m_analyzerBarWidth
Definition: visualize.h:309
float * m_dftL
Definition: visualize.h:314
float m_falloff
Definition: visualize.h:308
QVector< float > m_sigR
Definition: visualize.h:313
QVector< QRect > m_rectsR
Definition: visualize.h:300
static constexpr float kTxScale
Definition: visualize.h:317
float m_scaleFactor
Definition: visualize.h:307
QVector< float > m_sigL
Definition: visualize.h:312
QSize m_size
Definition: visualize.h:302
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:1585
av_tx_fn m_rdft
Definition: visualize.h:319
float * m_rdftTmp
Definition: visualize.h:316
QColor m_startColor
Definition: visualize.h:297
float * m_dftR
Definition: visualize.h:315
static double clamp(double cur, double max, double min)
Definition: visualize.cpp:1580
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:1744
const QString & name(void) const override
Definition: visualize.cpp:1731
uint plugins(QStringList *list) const override
Definition: visualize.cpp:1738
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:1711
QSize m_actualSize
Definition: visualize.h:333
void resize(const QSize &newsize) override
Definition: visualize.cpp:1670
int m_numberOfSquares
Definition: visualize.h:335
void drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h)
Definition: visualize.cpp:1677
int m_fakeHeight
Definition: visualize.h:334
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:903
uint plugins(QStringList *list) const override
Definition: visualize.cpp:897
const QString & name(void) const override
Definition: visualize.cpp:890
bool process(VisualNode *node) override
Definition: visualize.cpp:251
bool draw(QPainter *p, const QColor &back) override
Definition: visualize.cpp:369
QSize m_size
Definition: visualize.h:129
std::vector< double > m_magnitudes
Definition: visualize.h:128
bool const m_rubberband
Definition: visualize.h:130
QColor m_startColor
Definition: visualize.h:126
QColor m_targetColor
Definition: visualize.h:127
void resize(const QSize &size) override
Definition: visualize.cpp:237
double const m_falloff
Definition: visualize.h:131
static VisFactory * g_pVisFactories
Definition: visualize.h:107
VisualBase(bool screensaverenable=false)
Definition: visualize.cpp:55
static void drawWarning(QPainter *p, const QColor &back, QSize size, const QString &warning, int fontsize=28)
Definition: visualize.cpp:74
virtual ~VisualBase(void)
Definition: visualize.cpp:62
bool m_xscreensaverenable
Definition: visualize.h:91
int m_fps
Definition: visualize.h:90
long m_length
Definition: videovisual.h:38
short * m_left
Definition: videovisual.h:36
short * m_right
Definition: videovisual.h:37
std::chrono::milliseconds m_offset
Definition: videovisual.h:39
const QString & name(void) const override
Definition: visualize.cpp:941
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:954
uint plugins(QStringList *list) const override
Definition: visualize.cpp:948
~WaveForm() override
Definition: visualize.cpp:628
unsigned long getDesiredSamples(void) override
Definition: visualize.cpp:696
bool process(VisualNode *node) override
Definition: visualize.cpp:702
short int m_minl
Definition: visualize.h:172
MusicMetadata * m_currentMetadata
Definition: visualize.h:168
bool processUndisplayed(VisualNode *node) override
Definition: visualize.cpp:713
short int m_maxr
Definition: visualize.h:176
unsigned int m_lastx
Definition: visualize.h:170
short int m_maxl
Definition: visualize.h:173
short int m_minr
Definition: visualize.h:175
unsigned long m_sqrr
Definition: visualize.h:177
static constexpr unsigned long kWFAudioSize
Definition: visualize.h:148
void handleKeyPress(const QString &action) override
Definition: visualize.cpp:870
bool m_showtext
Definition: visualize.h:167
unsigned long m_offset
Definition: visualize.h:164
void saveload(MusicMetadata *meta)
Definition: visualize.cpp:635
static QImage s_image
Definition: visualize.h:149
short * m_right
Definition: visualize.h:165
unsigned long m_duration
Definition: visualize.h:169
unsigned long m_sqrl
Definition: visualize.h:174
bool m_stream
Definition: visualize.h:178
QFont m_font
Definition: visualize.h:166
unsigned int m_position
Definition: visualize.h:171
QSize m_wfsize
Definition: visualize.h:163
bool draw(QPainter *p, const QColor &back) override
Definition: visualize.cpp:829
unsigned int uint
Definition: compat.h:60
static std::vector< uint32_t > back
Definition: goom_core.cpp:27
static constexpr double M_PI
Definition: goom_tools.h:10
static const iso6937table * d
ImageType
Definition: musicmetadata.h:31
@ IT_LAST
Definition: musicmetadata.h:38
@ IT_UNKNOWN
Definition: musicmetadata.h:32
@ IT_FRONTCOVER
Definition: musicmetadata.h:33
MusicPlayer * gPlayer
Definition: musicplayer.cpp:38
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GetConfDir(void)
Definition: mythdirs.cpp:285
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:74
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythMainWindow * GetMythMainWindow(void)
None log(str msg, int level=LOGDEBUG)
Definition: xbmc.py:9
static eu8 clamp(eu8 value, eu8 low, eu8 high)
Definition: pxsup2dast.c:204
VERBOSE_PREAMBLE Most true
Definition: verbosedefs.h:86
static int nextType(int type)
Definition: visualize.cpp:2200
BlankFactory BlankFactory
StereoScopeFactory StereoScopeFactory
SpectrogramFactory SpectrogramFactory
AlbumArtFactory AlbumArtFactory
T sq(T a)
Definition: visualize.cpp:1072
static constexpr qint64 ALBUMARTCYCLETIME
this is the time an image is shown in the albumart visualizer
Definition: visualize.cpp:2273
MonoScopeFactory MonoScopeFactory
SpectrumDetailFactory SpectrumDetailFactory
SpectrumFactory SpectrumFactory
PianoFactory PianoFactory
SquaresFactory SquaresFactory
WaveFormFactory WaveFormFactory
static constexpr uint16_t SAMPLES_DEFAULT_SIZE
Definition: visualize.h:38
#define goertzel_data
Definition: visualize.h:348
#define piano_audio
Definition: visualize.h:347