File indexing completed on 2024-05-12 05:46:31

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