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