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 long w = 0;
1527 QRect *rectspL = m_rectsL.data();
1528 QRect *rectspR = m_rectsR.data();
1529 float *magnitudesp = m_magnitudes.data();
1530
1531 int index = 1; // frequency index of this pixel
1532 int prev = 0; // frequency index of previous pixel
1533 float adjHeight = m_size.height() / 2.0F;
1534
1535 for (int i = 0; i < m_rectsL.size(); i++, w += m_analyzerBarWidth)
1536 {
1537 float magL = 0; // modified from Spectrogram
1538 float magR = 0;
1539 float tmp = 0;
1540 for (ptrdiff_t j = prev + 1; j <= index; j++) // log scale!
1541 { // for the freqency bins of this pixel, find peak or mean
1542 tmp = sq(m_dftL[2 * j]) + sq(m_dftL[(2 * j) + 1]);
1543 magL = tmp > magL ? tmp : magL;
1544 tmp = sq(m_dftR[2 * j]) + sq(m_dftR[(2 * j) + 1]);
1545 magR = tmp > magR ? tmp : magR;
1546 }
1547 magL = 10 * std::log10(magL) * m_scaleFactor;
1548 magR = 10 * std::log10(magR) * m_scaleFactor;
1549
1550 magL = std::min(magL, adjHeight);
1551 if (magL < magnitudesp[i])
1552 {
1553 tmp = magnitudesp[i] - m_falloff;
1554 tmp = std::max(tmp, magL);
1555 magL = tmp;
1556 }
1557 magL = std::max<float>(magL, 1);
1558
1559 magR = std::min(magR, adjHeight);
1560 if (magR < magnitudesp[i + m_scale.range()])
1561 {
1562 tmp = magnitudesp[i + m_scale.range()] - m_falloff;
1563 tmp = std::max(tmp, magR);
1564 magR = tmp;
1565 }
1566 magR = std::max<float>(magR, 1);
1567
1568 magnitudesp[i] = magL;
1569 magnitudesp[i + m_scale.range()] = magR;
1570 rectspL[i].setTop( (m_size.height() / 2) - int( magL ) );
1571 rectspR[i].setBottom( (m_size.height() / 2) + int( magR ) );
1572
1573 prev = index; // next pixel is FFT bins from here
1574 index = m_scale[i]; // to the next bin by LOG scale
1575 (prev < index) || (prev = index -1);
1576 }
1577
1578 return false;
1579}
1580
1581double Spectrum::clamp(double cur, double max, double min)
1582{
1583 return std::clamp(cur, min, max);
1584}
1585
1586bool Spectrum::draw(QPainter *p, const QColor &back)
1587{
1588 // This draws on a pixmap owned by MainVisual.
1589 //
1590 // In other words, this is not a Qt Widget, it
1591 // just uses some Qt methods to draw on a pixmap.
1592 // MainVisual then bitblts that onto the screen.
1593
1594 QRect *rectspL = m_rectsL.data();
1595 QRect *rectspR = m_rectsR.data();
1596
1597 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
1598 for (uint i = 0; i < (uint)m_rectsL.size(); i++)
1599 {
1600 double per = ( rectspL[i].height() - 2. ) / (m_size.height() / 2.);
1601
1602 per = clamp(per, 1.0, 0.0);
1603
1604 double r = m_startColor.red() +
1605 ((m_targetColor.red() - m_startColor.red()) * (per * per));
1606 double g = m_startColor.green() +
1607 ((m_targetColor.green() - m_startColor.green()) * (per * per));
1608 double b = m_startColor.blue() +
1609 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
1610
1611 r = clamp(r, 255.0, 0.0);
1612 g = clamp(g, 255.0, 0.0);
1613 b = clamp(b, 255.0, 0.0);
1614
1615 if(rectspL[i].height() > 4)
1616 p->fillRect(rectspL[i], QColor(int(r), int(g), int(b)));
1617
1618 per = ( rectspR[i].height() - 2. ) / (m_size.height() / 2.);
1619
1620 per = clamp(per, 1.0, 0.0);
1621
1622 r = m_startColor.red() +
1623 ((m_targetColor.red() - m_startColor.red()) * (per * per));
1624 g = m_startColor.green() +
1625 ((m_targetColor.green() - m_startColor.green()) * (per * per));
1626 b = m_startColor.blue() +
1627 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
1628
1629 r = clamp(r, 255.0, 0.0);
1630 g = clamp(g, 255.0, 0.0);
1631 b = clamp(b, 255.0, 0.0);
1632
1633 if(rectspR[i].height() > 4)
1634 p->fillRect(rectspR[i], QColor(int(r), int(g), int(b)));
1635 }
1636
1637 return true;
1638}
1639
1640static class SpectrumFactory : public VisFactory
1641{
1642 public:
1643 const QString &name(void) const override // VisFactory
1644 {
1645 static QString s_name = QCoreApplication::translate("Visualizers",
1646 "SpectrumBars");
1647 return s_name;
1648 }
1649
1650 uint plugins(QStringList *list) const override // VisFactory
1651 {
1652 *list << name();
1653 return 1;
1654 }
1655
1656 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
1657 {
1658 return new Spectrum();
1659 }
1661
1663// Squares
1664//
1665
1667{
1669}
1670
1671void Squares::resize (const QSize &newsize) {
1672 // Trick the spectrum analyzer into calculating 16 rectangles
1674 // We have our own copy, Spectrum has it's own...
1675 m_actualSize = newsize;
1676}
1677
1678void Squares::drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h)
1679{
1680 double per = NAN;
1681 int correction = (m_actualSize.width() % m_rectsL.size ());
1682 int x = ((i / 2) * w) + correction;
1683 int y = 0;
1684
1685 if (i % 2 == 0)
1686 {
1687 y = c - h;
1688 per = double(m_fakeHeight - rect->top()) / double(m_fakeHeight);
1689 }
1690 else
1691 {
1692 y = c;
1693 per = double(rect->bottom()) / double(m_fakeHeight);
1694 }
1695
1696 per = clamp(per, 1.0, 0.0);
1697
1698 double r = m_startColor.red() +
1699 ((m_targetColor.red() - m_startColor.red()) * (per * per));
1700 double g = m_startColor.green() +
1701 ((m_targetColor.green() - m_startColor.green()) * (per * per));
1702 double b = m_startColor.blue() +
1703 ((m_targetColor.blue() - m_startColor.blue()) * (per * per));
1704
1705 r = clamp(r, 255.0, 0.0);
1706 g = clamp(g, 255.0, 0.0);
1707 b = clamp(b, 255.0, 0.0);
1708
1709 p->fillRect (x, y, w, h, QColor (int(r), int(g), int(b)));
1710}
1711
1712bool Squares::draw(QPainter *p, const QColor &back)
1713{
1714 p->fillRect (0, 0, m_actualSize.width(), m_actualSize.height(), back);
1715 int w = m_actualSize.width() / (m_rectsL.size() / 2);
1716 int h = w;
1717 int center = m_actualSize.height() / 2;
1718
1719 QRect *rectsp = m_rectsL.data();
1720 for (uint i = 0; i < (uint)m_rectsL.size() * 2; i += 2)
1721 drawRect(p, &(rectsp[i]), i, center, w, h);
1722 rectsp = m_rectsR.data();
1723 for (uint i = 1; i < ((uint)m_rectsR.size() * 2) + 1; i += 2)
1724 drawRect(p, &(rectsp[i]), i, center, w, h);
1725
1726 return true;
1727}
1728
1729static class SquaresFactory : public VisFactory
1730{
1731 public:
1732 const QString &name(void) const override // VisFactory
1733 {
1734 static QString s_name = QCoreApplication::translate("Visualizers",
1735 "Squares");
1736 return s_name;
1737 }
1738
1739 uint plugins(QStringList *list) const override // VisFactory
1740 {
1741 *list << name();
1742 return 1;
1743 }
1744
1745 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
1746 {
1747 return new Squares();
1748 }
1750
1751
1753{
1754 // Setup the "magical" audio coefficients
1755 // required by the Goetzel Algorithm
1756
1757 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Being Initialised"));
1758
1759 double sample_rate = 44100.0; // TODO : This should be obtained from gPlayer (likely candidate...)
1760
1761 m_fps = 20; // This is the display frequency. We're capturing all audio chunks by defining .process_undisplayed() though.
1762
1763 double concert_A = 440.0;
1764 double semi_tone = pow(2.0, 1.0/12.0);
1765
1766 /* Lowest note on piano is 4 octaves below concert A */
1767 double bottom_A = concert_A / 2.0 / 2.0 / 2.0 / 2.0;
1768
1769 double current_freq = bottom_A;
1770 for (uint key = 0; key < kPianoNumKeys; key++)
1771 {
1772 // This is constant through time
1773 m_pianoData[key].coeff = (goertzel_data)(2.0 * cos(2.0 * M_PI * current_freq / sample_rate));
1774
1775 // Want 20 whole cycles of the current waveform at least
1776 double samples_required = sample_rate/current_freq * 20.0;
1777 // For the really low notes, 4 updates a second is good enough...
1778 // For the high notes, use as many samples as we need in a display_fps
1779 samples_required = std::clamp(samples_required,
1780 sample_rate/(double)m_fps * 0.75,
1781 sample_rate/4.0);
1782 m_pianoData[key].samples_process_before_display_update = (int)samples_required;
1783 m_pianoData[key].is_black_note = false; // Will be put right in .resize()
1784
1785 current_freq *= semi_tone;
1786 }
1787
1788 zero_analysis();
1789}
1790
1792{
1793 for (uint key = 0; key < kPianoNumKeys; key++)
1794 {
1795 // These get updated continously, and must be stored between chunks of audio data
1796 m_pianoData[key].q2 = 0.0F;
1797 m_pianoData[key].q1 = 0.0F;
1798 m_pianoData[key].magnitude = 0.0F;
1799 m_pianoData[key].max_magnitude_seen =
1800 (goertzel_data)(kPianoRmsNegligible * kPianoRmsNegligible); // This is a guess - will be quickly overwritten
1801
1802 m_pianoData[key].samples_processed = 0;
1803 }
1804 m_offsetProcessed = 0ms;
1805}
1806
1807void Piano::resize(const QSize &newsize)
1808{
1809 // Just change internal data about the
1810 // size of the pixmap to be drawn (ie. the
1811 // size of the screen) and the logically
1812 // ensuing number of up/down bars to hold
1813 // the audio magnitudes
1814
1815 m_size = newsize;
1816
1817 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Being Resized"));
1818
1819 zero_analysis();
1820
1821 // There are 88-36=52 white notes on piano keyboard
1822 double key_unit_size = (double)m_size.width() / 54.0; // One white key extra spacing, if possible
1823 key_unit_size = std::max(key_unit_size, 10.0); // Keys have to be at least this many pixels wide
1824
1825 double white_width_pct = .8;
1826 double black_width_pct = .6;
1827 double black_offset_pct = .05;
1828
1829 double white_height_pct = 6;
1830 double black_height_pct = 4;
1831
1832 // This is the starting position of the keyboard (may be beyond LHS)
1833 // - actually position of C below bottom A (will be added to...). This is 4 octaves below middle C.
1834 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
1835 double top_of_keys = ((double)m_size.height() / 2.0) - (key_unit_size * white_height_pct / 2.0); // Vertically center keys
1836
1837 m_rects.resize(kPianoNumKeys);
1838
1839 for (uint key = 0; key < kPianoNumKeys; key++)
1840 {
1841 int note = ((int)key - 3 + 12) % 12; // This means that C=0, C#=1, D=2, etc (since lowest note is bottom A)
1842 if (note == 0) // If we're on a 'C', move the left 'cursor' over an octave
1843 {
1844 left += key_unit_size*7.0;
1845 }
1846
1847 double center = 0.0;
1848 double offset = 0.0;
1849 bool is_black = false;
1850
1851 switch (note)
1852 {
1853 case 0: center = 0.5; break;
1854 case 1: center = 1.0; is_black = true; offset = -1; break;
1855 case 2: center = 1.5; break;
1856 case 3: center = 2.0; is_black = true; offset = +1; break;
1857 case 4: center = 2.5; break;
1858 case 5: center = 3.5; break;
1859 case 6: center = 4.0; is_black = true; offset = -2; break;
1860 case 7: center = 4.5; break;
1861 case 8: center = 5.0; is_black = true; offset = 0; break;
1862 case 9: center = 5.5; break;
1863 case 10: center = 6.0; is_black = true; offset = 2; break;
1864 case 11: center = 6.5; break;
1865 }
1866 m_pianoData[key].is_black_note = is_black;
1867
1868 double width = (is_black ? black_width_pct:white_width_pct) * key_unit_size;
1869 double height = (is_black? black_height_pct:white_height_pct) * key_unit_size;
1870
1871 m_rects[key].setRect(
1872 left + (center * key_unit_size) // Basic position of left side of key
1873 - (width / 2.0) // Less half the width
1874 + (is_black ? (offset * black_offset_pct * key_unit_size):0.0), // And jiggle the positions of the black keys for aethetic reasons
1875 top_of_keys, // top
1876 width, // width
1877 height // height
1878 );
1879 }
1880
1881 m_magnitude.resize(kPianoNumKeys);
1882 for (double & key : m_magnitude)
1883 key = 0.0;
1884}
1885
1886unsigned long Piano::getDesiredSamples(void)
1887{
1888 // We want all the data! (within reason)
1889 // typical observed values are 882 -
1890 // 12.5 chunks of data per second from 44100Hz signal : Sampled at 50Hz, lots of 4, see :
1891 // mythtv/libs/libmythtv/audio/audiooutputbase.cpp :: AudioOutputBase::AddData
1892 // See : mythtv/mythplugins/mythmusic/mythmusic/avfdecoder.cpp "20ms worth"
1893 return (unsigned long) kPianoAudioSize; // Maximum we can be given
1894}
1895
1897{
1898 //LOG(VB_GENERAL, LOG_INFO, QString("Piano : Processing undisplayed node"));
1899 return process_all_types(node, false);
1900}
1901
1903{
1904 //LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Processing node for DISPLAY"));
1905
1906 // See WaveForm::process* above
1907 // return process_all_types(node, true);
1908
1909 return false;
1910}
1911
1912bool Piano::process_all_types(VisualNode *node, bool /*this_will_be_displayed*/)
1913{
1914 // Take a bunch of data in *node and break it down into piano key spectrum values
1915 // NB: Remember the state data between calls, so as to accumulate more accurate results.
1916 bool allZero = true;
1917 uint n = 0;
1918
1919 if (node)
1920 {
1921 piano_audio short_to_bounded = 32768.0F;
1922
1923 // Detect start of new song (current node more than 10s earlier than already seen)
1924 if (node->m_offset + 10s < m_offsetProcessed)
1925 {
1926 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Node offset=%1 too far backwards : NEW SONG").arg(node->m_offset.count()));
1927 zero_analysis();
1928 }
1929
1930 // Check whether we've seen this node (more recently than 10secs ago)
1931 if (node->m_offset <= m_offsetProcessed)
1932 {
1933 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Already seen node offset=%1, returning without processing").arg(node->m_offset.count()));
1934 return allZero; // Nothing to see here - the server can stop if it wants to
1935 }
1936
1937 //LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Processing node offset=%1, size=%2").arg(node->m_offset).arg(node->m_length));
1938 n = node->m_length;
1939
1940 if (node->m_right) // Preprocess the data into a combined middle channel, if we have stereo data
1941 {
1942 for (uint i = 0; i < n; i++)
1943 {
1944 m_audioData[i] = ((piano_audio)node->m_left[i] + (piano_audio)node->m_right[i]) / 2.0F / short_to_bounded;
1945 }
1946 }
1947 else // This is only one channel of data
1948 {
1949 for (uint i = 0; i < n; i++)
1950 {
1951 m_audioData[i] = (piano_audio)node->m_left[i] / short_to_bounded;
1952 }
1953 }
1954 }
1955 else
1956 {
1957 LOG(VB_GENERAL, LOG_DEBUG, QString("Hit an empty node, and returning empty-handed"));
1958 return allZero; // Nothing to see here - the server can stop if it wants to
1959 }
1960
1961 for (uint key = 0; key < kPianoNumKeys; key++)
1962 {
1963 goertzel_data coeff = m_pianoData[key].coeff;
1964 goertzel_data q2 = m_pianoData[key].q2;
1965 goertzel_data q1 = m_pianoData[key].q1;
1966
1967 for (uint i = 0; i < n; i++)
1968 {
1969 goertzel_data q0 = (coeff * q1) - q2 + m_audioData[i];
1970 q2 = q1;
1971 q1 = q0;
1972 }
1973 m_pianoData[key].q2 = q2;
1974 m_pianoData[key].q1 = q1;
1975
1976 m_pianoData[key].samples_processed += n;
1977
1978 int n_samples = m_pianoData[key].samples_processed;
1979
1980 // Only do this update if we've processed enough chunks for this key...
1981 if (n_samples > m_pianoData[key].samples_process_before_display_update)
1982 {
1983 goertzel_data magnitude2 = (q1*q1) + (q2*q2) - (q1*q2*coeff);
1984
1985#if 0
1986 // This is RMS of signal
1987 goertzel_data magnitude_av =
1988 sqrt(magnitude2)/(goertzel_data)n_samples; // Should be 0<magnitude_av<.5
1989#else
1990 // This is pure magnitude of signal
1991 goertzel_data magnitude_av =
1992 magnitude2/(goertzel_data)n_samples/(goertzel_data)n_samples; // Should be 0<magnitude_av<.25
1993#endif
1994
1995#if 0
1996 // Take logs everywhere, and shift up to [0, ??]
1997 if(magnitude_av > 0.0F)
1998 {
1999 magnitude_av = log(magnitude_av);
2000 }
2001 else
2002 {
2003 magnitude_av = kPianoMinVol;
2004 }
2005 magnitude_av -= kPianoMinVol;
2006
2007 if (magnitude_av < 0.0F)
2008 {
2009 magnitude_av = 0.0;
2010 }
2011#endif
2012
2013 if (magnitude_av > (goertzel_data)0.01)
2014 {
2015 allZero = false;
2016 }
2017
2018 m_pianoData[key].magnitude = magnitude_av; // Store this for later : We'll do the colours from this...
2019 m_pianoData[key].max_magnitude_seen =
2020 std::max(m_pianoData[key].max_magnitude_seen, magnitude_av);
2021 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Updated Key %1 from %2 samples, magnitude=%3")
2022 .arg(key).arg(n_samples).arg(magnitude_av));
2023
2024 m_pianoData[key].samples_processed = 0; // Reset the counts, now that we've set the magnitude...
2025 m_pianoData[key].q1 = (goertzel_data)0.0;
2026 m_pianoData[key].q2 = (goertzel_data)0.0;
2027 }
2028 }
2029
2030 if (node)
2031 {
2032 // All done now - record that we've done this offset
2034 }
2035
2036 return allZero;
2037}
2038
2039double Piano::clamp(double cur, double max, double min)
2040{
2041 return std::clamp(cur, min, max);
2042}
2043
2044bool Piano::draw(QPainter *p, const QColor &back)
2045{
2046 // This draws on a pixmap owned by MainVisual.
2047 //
2048 // In other words, this is not a Qt Widget, it
2049 // just uses some Qt methods to draw on a pixmap.
2050 // MainVisual then bitblts that onto the screen.
2051
2052 QRect *rectsp = (m_rects).data();
2053 double *magnitudep = (m_magnitude).data();
2054
2055 unsigned int n = kPianoNumKeys;
2056
2057 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
2058
2059 // Protect maximum array length
2060 n = std::min(n, (uint)m_rects.size());
2061
2062 // Sweep up across the keys, making sure the max_magnitude_seen is at minimum X% of its neighbours
2063 double mag = kPianoRmsNegligible;
2064 for (uint key = 0; key < n; key++)
2065 {
2066 if (m_pianoData[key].max_magnitude_seen < static_cast<float>(mag))
2067 {
2068 // Spread the previous value to this key
2069 m_pianoData[key].max_magnitude_seen = mag;
2070 }
2071 else
2072 {
2073 // This key has seen better peaks, use this for the next one
2074 mag = m_pianoData[key].max_magnitude_seen;
2075 }
2077 }
2078
2079 // Similarly, down, making sure the max_magnitude_seen is at minimum X% of its neighbours
2080 mag = kPianoRmsNegligible;
2081 for (int key_i = n - 1; key_i >= 0; key_i--)
2082 {
2083 uint key = key_i; // Wow, this is to avoid a zany error for ((unsigned)0)--
2084 if (m_pianoData[key].max_magnitude_seen < static_cast<float>(mag))
2085 {
2086 // Spread the previous value to this key
2087 m_pianoData[key].max_magnitude_seen = mag;
2088 }
2089 else
2090 {
2091 // This key has seen better peaks, use this for the next one
2092 mag = m_pianoData[key].max_magnitude_seen;
2093 }
2095 }
2096
2097 // Now find the key that has been hit the hardest relative to its experience, and renormalize...
2098 // Set a minimum, to prevent divide-by-zero (and also all-pressed when music very quiet)
2099 double magnitude_max = kPianoRmsNegligible;
2100 for (uint key = 0; key < n; key++)
2101 {
2102 mag = m_pianoData[key].magnitude / m_pianoData[key].max_magnitude_seen;
2103 magnitude_max = std::max(magnitude_max, mag);
2104
2105 magnitudep[key] = mag;
2106 }
2107
2108 // Deal with all the white keys first
2109 for (uint key = 0; key < n; key++)
2110 {
2111 if (m_pianoData[key].is_black_note)
2112 continue;
2113
2114 double per = magnitudep[key] / magnitude_max;
2115 per = clamp(per, 1.0, 0.0); // By construction, this should be unnecessary
2116
2117 if (per < kPianoKeypressTooLight)
2118 per = 0.0; // Clamp to zero for lightly detected keys
2119 LOG(VB_GENERAL, LOG_DEBUG, QString("Piano : Display key %1, magnitude=%2, seen=%3")
2120 .arg(key).arg(per*100.0).arg(m_pianoData[key].max_magnitude_seen));
2121
2122 double r = m_whiteStartColor.red() + ((m_whiteTargetColor.red() - m_whiteStartColor.red()) * per);
2123 double g = m_whiteStartColor.green() + ((m_whiteTargetColor.green() - m_whiteStartColor.green()) * per);
2124 double b = m_whiteStartColor.blue() + ((m_whiteTargetColor.blue() - m_whiteStartColor.blue()) * per);
2125
2126 p->fillRect(rectsp[key], QColor(int(r), int(g), int(b)));
2127 }
2128
2129 // Then overlay the black keys
2130 for (uint key = 0; key < n; key++)
2131 {
2132 if (!m_pianoData[key].is_black_note)
2133 continue;
2134
2135 double per = magnitudep[key]/magnitude_max;
2136 per = clamp(per, 1.0, 0.0); // By construction, this should be unnecessary
2137
2138 if (per < kPianoKeypressTooLight)
2139 per = 0.0; // Clamp to zero for lightly detected keys
2140
2141 double r = m_blackStartColor.red() + ((m_blackTargetColor.red() - m_blackStartColor.red()) * per);
2142 double g = m_blackStartColor.green() + ((m_blackTargetColor.green() - m_blackStartColor.green()) * per);
2143 double b = m_blackStartColor.blue() + ((m_blackTargetColor.blue() - m_blackStartColor.blue()) * per);
2144
2145 p->fillRect(rectsp[key], QColor(int(r), int(g), int(b)));
2146 }
2147
2148 return true;
2149}
2150
2151static class PianoFactory : public VisFactory
2152{
2153 public:
2154 const QString &name(void) const override // VisFactory
2155 {
2156 static QString s_name = QCoreApplication::translate("Visualizers",
2157 "Piano");
2158 return s_name;
2159 }
2160
2161 uint plugins(QStringList *list) const override // VisFactory
2162 {
2163 *list << name();
2164 return 1;
2165 }
2166
2167 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
2168 {
2169 return new Piano();
2170 }
2172
2174 m_lastCycle(QDateTime::currentDateTime())
2175{
2177 m_fps = 1;
2178}
2179
2181{
2183 return;
2184
2185 // if a front cover image is available show that first
2187 if (albumArt->getImage(IT_FRONTCOVER))
2189 else
2190 {
2191 // not available so just show the first image available
2192 if (albumArt->getImageCount() > 0)
2193 m_currImageType = albumArt->getImageAt(0)->m_imageType;
2194 else
2196 }
2197}
2198
2199static int nextType (int type)
2200{
2201 type++;
2202 return (type != IT_LAST) ? type : IT_UNKNOWN;
2203}
2204
2206{
2208 return false;
2209
2211
2212 // If we only have one image there is nothing to cycle
2213 if (albumArt->getImageCount() < 2)
2214 return false;
2215
2216 int newType = nextType(m_currImageType);
2217 while (!albumArt->getImage((ImageType) newType))
2218 newType = nextType(newType);
2219
2220 if (newType != m_currImageType)
2221 {
2222 m_currImageType = (ImageType) newType;
2223 m_lastCycle = QDateTime::currentDateTime();
2224 return true;
2225 }
2226
2227 return false;
2228}
2229
2230void AlbumArt::resize(const QSize &newsize)
2231{
2232 m_size = newsize;
2233}
2234
2236{
2237 return false;
2238}
2239
2241{
2242 if (action == "SELECT")
2243 {
2245 {
2247 int newType = m_currImageType;
2248
2249 if (albumArt.getImageCount() > 0)
2250 {
2251 newType++;
2252
2253 while (!albumArt.getImage((ImageType) newType))
2254 {
2255 newType++;
2256 if (newType == IT_LAST)
2257 newType = IT_UNKNOWN;
2258 }
2259 }
2260
2261 if (newType != m_currImageType)
2262 {
2263 m_currImageType = (ImageType) newType;
2264 // force an update
2265 m_cursize = QSize(0, 0);
2266 }
2267 }
2268 }
2269}
2270
2272static constexpr qint64 ALBUMARTCYCLETIME { 10 };
2273
2275{
2276 // if the track has changed we need to update the image
2278 {
2281 return true;
2282 }
2283
2284 // if it's time to cycle to the next image we need to update the image
2285 if (m_lastCycle.addSecs(ALBUMARTCYCLETIME) < QDateTime::currentDateTime())
2286 {
2287 if (cycleImage())
2288 return true;
2289 }
2290
2291 return false;
2292}
2293
2294bool AlbumArt::draw(QPainter *p, const QColor &back)
2295{
2296 if (needsUpdate())
2297 {
2298 QImage art;
2299 QString imageFilename = gPlayer->getCurrentMetadata()->getAlbumArtFile(m_currImageType);
2300
2301 if (imageFilename.startsWith("myth://"))
2302 {
2303 auto *rf = new RemoteFile(imageFilename, false, false, 0ms);
2304
2305 QByteArray data;
2306 bool ret = rf->SaveAs(data);
2307
2308 delete rf;
2309
2310 if (ret)
2311 art.loadFromData(data);
2312 }
2313 else
2314 if (!imageFilename.isEmpty())
2315 {
2316 art.load(imageFilename);
2317 }
2318
2319 if (art.isNull())
2320 {
2321 m_cursize = m_size;
2322 m_image = QImage();
2323 }
2324 else
2325 {
2326 m_image = art.scaled(m_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
2327 }
2328 }
2329
2330 if (m_image.isNull())
2331 {
2332 drawWarning(p, back, m_size, tr("?"), 100);
2333 return true;
2334 }
2335
2336 // Paint the image
2337 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
2338 p->drawImage((m_size.width() - m_image.width()) / 2,
2339 (m_size.height() - m_image.height()) / 2,
2340 m_image);
2341
2342 // Store our new size
2343 m_cursize = m_size;
2344
2345 return true;
2346}
2347
2348static class AlbumArtFactory : public VisFactory
2349{
2350 public:
2351 const QString &name(void) const override // VisFactory
2352 {
2353 static QString s_name = QCoreApplication::translate("Visualizers",
2354 "AlbumArt");
2355 return s_name;
2356 }
2357
2358 uint plugins(QStringList *list) const override // VisFactory
2359 {
2360 *list << name();
2361 return 1;
2362 }
2363
2364 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
2365 {
2366 return new AlbumArt();
2367 }
2369
2371 : VisualBase(true)
2372{
2373 m_fps = 1;
2374}
2375
2376void Blank::resize(const QSize &newsize)
2377{
2378 m_size = newsize;
2379}
2380
2381
2383{
2384 return false;
2385}
2386
2387bool Blank::draw(QPainter *p, const QColor &back)
2388{
2389 // Took me hours to work out this algorithm
2390 p->fillRect(0, 0, m_size.width(), m_size.height(), back);
2391 return true;
2392}
2393
2394static class BlankFactory : public VisFactory
2395{
2396 public:
2397 const QString &name(void) const override // VisFactory
2398 {
2399 static QString s_name = QCoreApplication::translate("Visualizers",
2400 "Blank");
2401 return s_name;
2402 }
2403
2404 uint plugins(QStringList *list) const override // VisFactory
2405 {
2406 *list << name();
2407 return 1;
2408 }
2409
2410 VisualBase *create(MainVisual */*parent*/, const QString &/*pluginName*/) const override // VisFactory
2411 {
2412 return new Blank();
2413 }
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:2364
const QString & name(void) const override
Definition: visualize.cpp:2351
uint plugins(QStringList *list) const override
Definition: visualize.cpp:2358
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:2230
void findFrontCover(void)
Definition: visualize.cpp:2180
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:2294
AlbumArt(void)
Definition: visualize.cpp:2173
bool process(VisualNode *node=nullptr) override
Definition: visualize.cpp:2235
bool needsUpdate(void)
Definition: visualize.cpp:2274
void handleKeyPress(const QString &action) override
Definition: visualize.cpp:2240
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:2205
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:2404
const QString & name(void) const override
Definition: visualize.cpp:2397
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:2410
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:2387
void resize(const QSize &size) override
Definition: visualize.cpp:2376
bool process(VisualNode *node=nullptr) override
Definition: visualize.cpp:2382
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:2161
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:2167
const QString & name(void) const override
Definition: visualize.cpp:2154
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:1886
QSize m_size
Definition: visualize.h:393
void zero_analysis(void)
Definition: visualize.cpp:1791
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:2044
std::array< piano_audio, kPianoAudioSize > m_audioData
Definition: visualize.h:398
bool processUndisplayed(VisualNode *node) override
Definition: visualize.cpp:1896
bool process(VisualNode *node) override
Definition: visualize.cpp:1902
static double clamp(double cur, double max, double min)
Definition: visualize.cpp:2039
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:1807
std::vector< QRect > m_rects
Definition: visualize.h:392
bool process_all_types(VisualNode *node, bool this_will_be_displayed)
Definition: visualize.cpp:1912
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:1650
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:1656
const QString & name(void) const override
Definition: visualize.cpp:1643
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:1586
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:1581
VisualBase * create(MainVisual *, const QString &) const override
Definition: visualize.cpp:1745
const QString & name(void) const override
Definition: visualize.cpp:1732
uint plugins(QStringList *list) const override
Definition: visualize.cpp:1739
bool draw(QPainter *p, const QColor &back=Qt::black) override
Definition: visualize.cpp:1712
QSize m_actualSize
Definition: visualize.h:333
void resize(const QSize &newsize) override
Definition: visualize.cpp:1671
int m_numberOfSquares
Definition: visualize.h:335
void drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h)
Definition: visualize.cpp:1678
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:201
VERBOSE_PREAMBLE Most true
Definition: verbosedefs.h:86
static int nextType(int type)
Definition: visualize.cpp:2199
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:2272
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