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 }