File indexing completed on 2024-05-12 04:50:07

0001 /*
0002     SPDX-FileCopyrightText: 2003-2005 Max Howell <max.howell@methylblue.com>
0003     SPDX-FileCopyrightText: 2005 Mark Kretschmann <kretschmann@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "blockAnalyzer.h"
0009 
0010 #include <cmath>
0011 
0012 #include <QPainter> //paletteChange()
0013 
0014 static inline uint myMax(uint v1, uint v2)
0015 {
0016     return v1 > v2 ? v1 : v2;
0017 }
0018 
0019 BlockAnalyzer::BlockAnalyzer(QWidget *parent)
0020     : Analyzer::Base2D(parent, 9)
0021     , m_columns(0) // uint
0022     , m_rows(0) // uint
0023     , m_y(0) // uint
0024     , m_barPixmap(1, 1) // null qpixmaps cause crashes
0025     , m_topBarPixmap(WIDTH, HEIGHT)
0026     , m_scope(MIN_COLUMNS) // Scope
0027     , m_store(1 << 8, 0) // vector<uint>
0028     , m_fade_bars(FADE_SIZE) // vector<QPixmap>
0029     , m_fade_pos(1 << 8, 50) // vector<uint>
0030     , m_fade_intensity(1 << 8, 32) // vector<uint>
0031     , m_step(0)
0032 {
0033     setMinimumSize(MIN_COLUMNS * (WIDTH + 1) - 1, MIN_ROWS * (HEIGHT + 1) - 1); //-1 is padding, no drawing takes place there
0034     setMaximumWidth(MAX_COLUMNS * (WIDTH + 1) - 1);
0035     setMaximumHeight(MIN_ROWS * (HEIGHT + 1) - 1);
0036 
0037     // mxcl says null pixmaps cause crashes, so let's play it safe
0038     for (int i = 0; i < FADE_SIZE; ++i)
0039         m_fade_bars[i] = QPixmap(1, 1);
0040 }
0041 
0042 BlockAnalyzer::~BlockAnalyzer()
0043 {
0044 }
0045 
0046 void BlockAnalyzer::resizeEvent(QResizeEvent *e)
0047 {
0048     Analyzer::Base2D::resizeEvent(e);
0049 
0050     const uint oldRows = m_rows;
0051 
0052     // all is explained in analyze()..
0053     //+1 to counter -1 in maxSizes, trust me we need this!
0054     m_columns = qMin<uint>(uint(double(width() + 1) / (WIDTH + 1)), MAX_COLUMNS);
0055     m_rows = uint(double(height() + 1) / (HEIGHT + 1));
0056 
0057     // this is the y-offset for drawing from the top of the widget
0058     m_y = (height() - (m_rows * (HEIGHT + 1)) + 2) / 2;
0059 
0060     m_scope.resize(m_columns);
0061 
0062     if (m_rows != oldRows) {
0063         m_barPixmap = QPixmap(WIDTH, m_rows * (HEIGHT + 1));
0064 
0065         for (int i = 0; i < FADE_SIZE; ++i)
0066             m_fade_bars[i] = QPixmap(WIDTH, m_rows * (HEIGHT + 1));
0067 
0068         m_yscale.resize(m_rows + 1);
0069 
0070         const float PRE = 1, PRO = 1; // PRE and PRO allow us to restrict the range somewhat
0071 
0072         for (uint z = 0; z < m_rows; ++z)
0073             m_yscale[z] = 1 - (log10(PRE + z) / log10(PRE + m_rows + PRO));
0074 
0075         m_yscale[m_rows] = 0;
0076 
0077         determineStep();
0078         paletteChange(palette());
0079     } else if (width() > e->oldSize().width() || height() > e->oldSize().height())
0080         drawBackground();
0081 
0082     analyze(m_scope);
0083 }
0084 
0085 void BlockAnalyzer::determineStep()
0086 {
0087     // falltime is dependent on rowcount due to our digital resolution (ie we have boxes/blocks of pixels)
0088     // I calculated the value 30 based on some trial and error
0089 
0090     const double fallTime = 30 * m_rows;
0091     m_step = double(m_rows * 80) / fallTime; // 80 = ~milliseconds between signals with audio data
0092 }
0093 
0094 void BlockAnalyzer::transform(QVector<float> &s) // pure virtual
0095 {
0096     for (int x = 0; x < s.size(); ++x)
0097         s[x] *= 2;
0098 
0099     float *front = static_cast<float *>(&s.front());
0100 
0101     m_fht->spectrum(front);
0102     m_fht->scale(front, 1.0 / 20);
0103 
0104     // the second half is pretty dull, so only show it if the user has a large analyzer
0105     // by setting to m_scope.size() if large we prevent interpolation of large analyzers, this is good!
0106     s.resize(m_scope.size() <= MAX_COLUMNS / 2 ? MAX_COLUMNS / 2 : m_scope.size());
0107 }
0108 
0109 void BlockAnalyzer::analyze(const QVector<float> &s)
0110 {
0111     Analyzer::interpolate(s, m_scope);
0112     update();
0113 }
0114 
0115 void BlockAnalyzer::paintEvent(QPaintEvent *)
0116 {
0117     // y = 2 3 2 1 0 2
0118     //     . . . . # .
0119     //     . . . # # .
0120     //     # . # # # #
0121     //     # # # # # #
0122     //
0123     // visual aid for how this analyzer works.
0124     // y represents the number of blanks
0125     // y starts from the top and increases in units of blocks
0126 
0127     // m_yscale looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
0128     // if it contains 6 elements there are 5 rows in the analyzer
0129 
0130     QPainter p(this);
0131 
0132     // Paint the background
0133     p.drawPixmap(0, 0, m_background);
0134     //   p.fillRect(rect(), palette().color( QPalette::Active, QPalette::Window ));
0135 
0136     uint y;
0137 
0138     for (int x = 0; x < m_scope.size(); ++x) {
0139         // determine y
0140         for (y = 0; m_scope[x] < m_yscale[y]; ++y)
0141             ;
0142 
0143         // this is opposite to what you'd think, higher than y
0144         // means the bar is lower than y (physically)
0145         if ((float)y > m_store[x])
0146             y = uint(m_store[x] += m_step);
0147         else
0148             m_store[x] = y;
0149 
0150         // if y is lower than m_fade_pos, then the bar has exceeded the height of the fadeout
0151         // if the fadeout is quite faded now, then display the new one
0152         if (y <= m_fade_pos[x] /*|| m_fade_intensity[x] < FADE_SIZE / 3*/) {
0153             m_fade_pos[x] = y;
0154             m_fade_intensity[x] = FADE_SIZE;
0155         }
0156 
0157         if (m_fade_intensity[x] > 0) {
0158             const uint offset = --m_fade_intensity[x];
0159             const uint y = m_y + (m_fade_pos[x] * (HEIGHT + 1));
0160             p.drawPixmap(x * (WIDTH + 1), y, m_fade_bars[offset], 0, 0, WIDTH, height() - y);
0161         }
0162 
0163         if (m_fade_intensity[x] == 0)
0164             m_fade_pos[x] = m_rows;
0165 
0166         // REMEMBER: y is a number from 0 to m_rows, 0 means all blocks are glowing, m_rows means none are
0167         p.drawPixmap(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, *bar(), 0, y * (HEIGHT + 1), -1, -1);
0168     }
0169 
0170     for (uint x = 0; x < m_store.size(); ++x)
0171         p.drawPixmap(x * (WIDTH + 1), int(m_store[x]) * (HEIGHT + 1) + m_y, m_topBarPixmap);
0172 }
0173 
0174 static inline void adjustToLimits(int &b, int &f, uint &amount)
0175 {
0176     // with a range of 0-255 and maximum adjustment of amount,
0177     // maximise the difference between f and b
0178 
0179     if (b < f) {
0180         if (b > 255 - f) {
0181             amount -= f;
0182             f = 0;
0183         } else {
0184             amount -= (255 - f);
0185             f = 255;
0186         }
0187     } else {
0188         if (f > 255 - b) {
0189             amount -= f;
0190             f = 0;
0191         } else {
0192             amount -= (255 - f);
0193             f = 255;
0194         }
0195     }
0196 }
0197 
0198 /**
0199  * Clever contrast function
0200  *
0201  * It will try to adjust the foreground color such that it contrasts well with the background
0202  * It won't modify the hue of fg unless absolutely necessary
0203  * @return the adjusted form of fg
0204  */
0205 QColor ensureContrast(const QColor &bg, const QColor &fg, uint _amount = 150)
0206 {
0207     class OutputOnExit
0208     {
0209     public:
0210         OutputOnExit(const QColor &color)
0211             : c(color)
0212         {
0213         }
0214         ~OutputOnExit()
0215         {
0216             int h, s, v;
0217             c.getHsv(&h, &s, &v);
0218         }
0219 
0220     private:
0221         const QColor &c;
0222     };
0223 
0224     // hack so I don't have to cast everywhere
0225 #define amount static_cast<int>(_amount)
0226     //     #define STAMP debug() << (QValueList<int>() << fh << fs << fv);
0227     //     #define STAMP1( string ) debug() << string << ": " << (QValueList<int>() << fh << fs << fv);
0228     //     #define STAMP2( string, value ) debug() << string << "=" << value << ": " << (QValueList<int>() << fh << fs << fv);
0229 
0230     OutputOnExit allocateOnTheStack(fg);
0231 
0232     int bh, bs, bv;
0233     int fh, fs, fv;
0234 
0235     bg.getHsv(&bh, &bs, &bv);
0236     fg.getHsv(&fh, &fs, &fv);
0237 
0238     int dv = abs(bv - fv);
0239 
0240     //     STAMP2( "DV", dv );
0241 
0242     // value is the best measure of contrast
0243     // if there is enough difference in value already, return fg unchanged
0244     if (dv > amount)
0245         return fg;
0246 
0247     int ds = abs(bs - fs);
0248 
0249     //     STAMP2( "DS", ds );
0250 
0251     // saturation is good enough too. But not as good. TODO adapt this a little
0252     if (ds > amount)
0253         return fg;
0254 
0255     int dh = abs(bh - fh);
0256 
0257     //     STAMP2( "DH", dh );
0258 
0259     if (dh > 120) {
0260         // a third of the colour wheel automatically guarentees contrast
0261         // but only if the values are high enough and saturations significant enough
0262         // to allow the colours to be visible and not be shades of grey or black
0263 
0264         // check the saturation for the two colours is sufficient that hue alone can
0265         // provide sufficient contrast
0266         if (ds > amount / 2 && (bs > 125 && fs > 125))
0267             //             STAMP1( "Sufficient saturation difference, and hues are compliemtary" );
0268             return fg;
0269         else if (dv > amount / 2 && (bv > 125 && fv > 125))
0270             //             STAMP1( "Sufficient value difference, and hues are compliemtary" );
0271             return fg;
0272 
0273         //         STAMP1( "Hues are complimentary but we must modify the value or saturation of the contrasting colour" );
0274 
0275         // but either the colours are two desaturated, or too dark
0276         // so we need to adjust the system, although not as much
0277         ///_amount /= 2;
0278     }
0279 
0280     if (fs < 50 && ds < 40) {
0281         // low saturation on a low saturation is sad
0282         const int tmp = 50 - fs;
0283         fs = 50;
0284         if (amount > tmp)
0285             _amount -= tmp;
0286         else
0287             _amount = 0;
0288     }
0289 
0290     // test that there is available value to honor our contrast requirement
0291     if (255 - dv < amount) {
0292         // we have to modify the value and saturation of fg
0293         // adjustToLimits( bv, fv, amount );
0294 
0295         //         STAMP
0296 
0297         // see if we need to adjust the saturation
0298         if (amount > 0)
0299             adjustToLimits(bs, fs, _amount);
0300 
0301         //         STAMP
0302 
0303         // see if we need to adjust the hue
0304         if (amount > 0)
0305             fh += amount; // cycles around;
0306 
0307         //         STAMP
0308 
0309         return QColor::fromHsv(fh, fs, fv);
0310     }
0311 
0312     //     STAMP
0313 
0314     if (fv > bv && bv > amount)
0315         return QColor::fromHsv(fh, fs, bv - amount);
0316 
0317     //     STAMP
0318 
0319     if (fv < bv && fv > amount)
0320         return QColor::fromHsv(fh, fs, fv - amount);
0321 
0322     //     STAMP
0323 
0324     if (fv > bv && (255 - fv > amount))
0325         return QColor::fromHsv(fh, fs, fv + amount);
0326 
0327     //     STAMP
0328 
0329     if (fv < bv && (255 - bv > amount))
0330         return QColor::fromHsv(fh, fs, bv + amount);
0331 
0332     //     STAMP
0333     //     debug() << "Something went wrong!\n";
0334 
0335     return Qt::blue;
0336 
0337 #undef amount
0338     //     #undef STAMP
0339 }
0340 
0341 void BlockAnalyzer::paletteChange(const QPalette &)
0342 {
0343     const QColor bg = palette().color(QPalette::Active, QPalette::Window);
0344     const QColor fg = ensureContrast(bg, palette().color(QPalette::Active, QPalette::WindowText));
0345 
0346     m_topBarPixmap.fill(fg);
0347 
0348     const double dr = 15 * double(bg.red() - fg.red()) / (m_rows * 16);
0349     const double dg = 15 * double(bg.green() - fg.green()) / (m_rows * 16);
0350     const double db = 15 * double(bg.blue() - fg.blue()) / (m_rows * 16);
0351     const int r = fg.red(), g = fg.green(), b = fg.blue();
0352 
0353     bar()->fill(bg);
0354 
0355     QPainter p(bar());
0356     for (int y = 0; (uint)y < m_rows; ++y)
0357         // graduate the fg color
0358         p.fillRect(0, y * (HEIGHT + 1), WIDTH, HEIGHT, QColor(r + int(dr * y), g + int(dg * y), b + int(db * y)));
0359 
0360     {
0361         const QColor bg = palette().color(QPalette::Active, QPalette::Window).darker(112);
0362 
0363         // make a complimentary fadebar colour
0364         // TODO dark is not always correct, dumbo!
0365         int h, s, v;
0366         palette().color(QPalette::Active, QPalette::Window).darker(150).getHsv(&h, &s, &v);
0367         const QColor fg = QColor::fromHsv(h + 60, s, v);
0368 
0369         const double dr = fg.red() - bg.red();
0370         const double dg = fg.green() - bg.green();
0371         const double db = fg.blue() - bg.blue();
0372         const int r = bg.red(), g = bg.green(), b = bg.blue();
0373 
0374         // Precalculate all fade-bar pixmaps
0375         for (int y = 0; y < FADE_SIZE; ++y) {
0376             m_fade_bars[y].fill(palette().color(QPalette::Active, QPalette::Window));
0377             QPainter f(&m_fade_bars[y]);
0378             for (int z = 0; (uint)z < m_rows; ++z) {
0379                 const double Y = 1.0 - (log10(static_cast<float>(FADE_SIZE) - y) / log10(static_cast<float>(FADE_SIZE)));
0380                 f.fillRect(0, z * (HEIGHT + 1), WIDTH, HEIGHT, QColor(r + int(dr * Y), g + int(dg * Y), b + int(db * Y)));
0381             }
0382         }
0383     }
0384 
0385     drawBackground();
0386 }
0387 
0388 void BlockAnalyzer::drawBackground()
0389 {
0390     const QColor bg = palette().color(QPalette::Active, QPalette::Window);
0391     const QColor bgdark = bg.darker(112);
0392 
0393     m_background.fill(bg);
0394 
0395     QPainter p(&m_background);
0396     for (int x = 0; (uint)x < m_columns; ++x)
0397         for (int y = 0; (uint)y < m_rows; ++y)
0398             p.fillRect(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, WIDTH, HEIGHT, bgdark);
0399 }