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