// (c) 2004 Max Howell (max.howell@methylblue.com) // See COPYING file for licensing information #include "analyzer.h" #include "../codeine.h" #include "../debug.h" #include //interpolate() #include #include //event() #include "xineEngine.h" #include "fht.cpp" template Analyzer::Base::Base( TQWidget *parent, uint timeout, uint scopeSize ) : W( parent, "Analyzer" ) , m_timeout( timeout ) , m_fht(new FHT(scopeSize)) {} template void Analyzer::Base::transform(Scope &scope) //virtual { // This is a standard transformation that should give // an FFT scope that has bands for pretty analyzers // NOTE: resizing here is redundant as FHT routines only calculate FHT::size() values // scope.resize( m_fht->size() ); float *front = &scope.front(); auto *f = new float[m_fht->size()]; m_fht->copy(&f[0], front); m_fht->logSpectrum(front, &f[0]); m_fht->scale(front, 1.0 / 20); scope.resize(m_fht->size() / 2); //second half of values are rubbish delete[] f; } template void Analyzer::Base::drawFrame() { switch(Codeine::engine()->state()) { case Engine::Playing: { const Engine::Scope &theScope = Codeine::engine()->scope(); static Scope scope(512); int i = 0; // Convert to mono. // The Analyzer requires mono, but xine reports interleaved PCM. for (int x = 0; x < m_fht->size(); ++x) { // Average between the channels. scope[x] = static_cast(theScope[i] + theScope[i + 1]) / (2 * (1 << 15)); i += 2; } transform(scope); analyze(scope); scope.resize(m_fht->size()); break; } case Engine::Paused: { break; } default: { demo(); break; } } } template void Analyzer::Base::demo() { static int t = 201; //FIXME make static to namespace perhaps if (t > 999) { // 0 = wasted calculations t = 1; } if (t < 201) { Scope s(32); const auto dt = static_cast(t) / 200.0; for (unsigned i = 0; i < s.size(); ++i) { s[i] = dt * (sin(M_PI + (i * M_PI) / s.size()) + 1.0); } analyze(s); } else { analyze(Scope(32, 0)); } ++t; } template bool Analyzer::Base::event( TQEvent *e ) { switch( e->type() ) { case TQEvent::Hide: m_timer.stop(); break; case TQEvent::Show: m_timer.start( timeout() ); break; default: ; } return TQWidget::event( e ); } Analyzer::Base2D::Base2D( TQWidget *parent, uint timeout, uint scopeSize ) : Base( parent, timeout, scopeSize ) { setWFlags( TQt::WNoAutoErase ); //no flicker connect( &m_timer, TQ_SIGNAL(timeout()), TQ_SLOT(draw()) ); } void Analyzer::Base2D::resizeEvent( TQResizeEvent *e) { m_background.resize(size()); m_canvas.resize(size()); m_background.fill(backgroundColor()); eraseCanvas(); TQWidget::resizeEvent(e); } void Analyzer::Base2D::paletteChange(const TQPalette&) { m_background.fill(backgroundColor()); eraseCanvas(); } // Author: Max Howell , (C) 2003 // Copyright: See COPYING file that comes with this distribution #include Analyzer::Block::Block( TQWidget *parent ) : Analyzer::Base2D(parent, 20, 9) , m_scope(MIN_COLUMNS) , m_barPixmap(1, 1) , m_topBarPixmap(WIDTH, HEIGHT) , m_store(1 << 8, 0) , m_fadeBars(FADE_SIZE) , m_fadeIntensity(1 << 8, 32) , m_fadePos(1 << 8, 50) , m_columns(0) , m_rows(0) , m_y(0) , m_step(0) { // -1 is padding, no drawing takes place there setMinimumSize(MIN_COLUMNS * (WIDTH + 1) - 1, MIN_ROWS * (HEIGHT + 1) - 1); setMaximumWidth(MAX_COLUMNS * (WIDTH + 1) - 1); for (auto &m_fadeBar : m_fadeBars) { m_fadeBar.resize(1, 1); } } void Analyzer::Block::transform( Analyzer::Scope &s ) //pure virtual { for( uint x = 0; x < s.size(); ++x ) s[x] *= 2; float *front = static_cast( &s.front() ); m_fht->spectrum( front ); m_fht->scale( front, 1.0 / 20 ); //the second half is pretty dull, so only show it if the user has a large analyzer //by setting to m_scope.size() if large we prevent interpolation of large analyzers, this is good! s.resize( m_scope.size() <= MAX_COLUMNS/2 ? MAX_COLUMNS/2 : m_scope.size() ); } void Analyzer::Block::analyze( const Analyzer::Scope &s ) { // y = 2 3 2 1 0 2 // . . . . # . // . . . # # . // # . # # # # // # # # # # # // // visual aid for how this analyzer works. // y represents the number of blanks // y starts from the top and increases in units of blocks // m_yscale looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 } // if it contains 6 elements there are 5 rows in the analyzer interpolate(s, m_scope); // Paint the background bitBlt(canvas(), 0, 0, background()); unsigned y; for (unsigned x = 0; x < m_scope.size(); ++x) { if (m_yScale.empty()) { return; } // determine y for (y = 0; m_scope[x] < m_yScale[y]; ++y) ; // this is opposite to what you'd think, higher than y // means the bar is lower than y (physically) if (static_cast(y) > m_store[x]) { y = static_cast(m_store[x] += m_step); } else { m_store[x] = y; } // if y is lower than m_fade_pos, then the bar has exceeded the height of the fadeout // if the fadeout is quite faded now, then display the new one if (y <= m_fadePos[x] /*|| m_fadeIntensity[x] < FADE_SIZE / 3*/ ) { m_fadePos[x] = y; m_fadeIntensity[x] = FADE_SIZE; } if (m_fadeIntensity[x] > 0) { const unsigned offset = --m_fadeIntensity[x]; const unsigned y = m_y + (m_fadePos[x] * (HEIGHT + 1)); bitBlt(canvas(), x * (WIDTH + 1), y, &m_fadeBars[offset], 0, 0, WIDTH, height() - y ); } if (m_fadeIntensity[x] == 0) { m_fadePos[x] = m_rows; } // REMEMBER: y is a number from 0 to m_rows, 0 means all blocks are glowing, m_rows means none are bitBlt(canvas(), x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, bar(), 0, y * (HEIGHT + 1)); } for (unsigned x = 0; x < m_store.size(); ++x) { bitBlt(canvas(), x * (WIDTH + 1), int(m_store[x]) * (HEIGHT + 1) + m_y, &m_topBarPixmap); } } static void adjustToLimits(const int &b, int &f, unsigned &amount) { // with a range of 0-255 and maximum adjustment of amount, // maximise the difference between f and b if (b < f) { if (b > 255 - f) { amount -= f; f = 0; } else { amount -= (255 - f); f = 255; } } else { if (f > 255 - b) { amount -= f; f = 0; } else { amount -= (255 - f); f = 255; } } } /** * Clever contrast function * * It will try to adjust the foreground color such that it contrasts well with the background * It won't modify the hue of fg unless absolutely necessary * @return the adjusted form of fg */ TQColor ensureContrast(const TQColor &bg, const TQColor &fg, unsigned _amount = 150) { class OutputOnExit { public: explicit OutputOnExit(const TQColor &color) : c(color) { } ~OutputOnExit() { int h, s, v; c.getHsv(&h, &s, &v); } private: const TQColor &c; }; // hack so I don't have to cast everywhere #define amount static_cast(_amount) // #define STAMP debug() << (TQValueList() << fh << fs << fv) << endl; // #define STAMP1( string ) debug() << string << ": " << (TQValueList() << fh << fs << fv) << endl; // #define STAMP2( string, value ) debug() << string << "=" << value << ": " << (TQValueList() << fh << fs << fv) << endl; OutputOnExit allocateOnTheStack(fg); int bh, bs, bv; int fh, fs, fv; bg.getHsv(&bh, &bs, &bv); fg.getHsv(&fh, &fs, &fv); int dv = abs(bv - fv); // STAMP2( "DV", dv ); // value is the best measure of contrast // if there is enough difference in value already, return fg unchanged if (dv > amount) { return fg; } int ds = abs(bs - fs); // STAMP2( "DS", ds ); // saturation is good enough too. But not as good. TODO adapt this a little if (ds > amount) { return fg; } int dh = abs(bh - fh); // STAMP2( "DH", dh ); if (dh > 120) { // a third of the colour wheel automatically guarantees contrast // but only if the values are high enough and saturations significant enough // to allow the colours to be visible and not be shades of grey or black // check the saturation for the two colours is sufficient that hue alone can // provide sufficient contrast if (ds > amount / 2 && (bs > 125 && fs > 125)) { // STAMP1( "Sufficient saturation difference, and hues are complimentary" ); return fg; } if (dv > amount / 2 && (bv > 125 && fv > 125)) { // STAMP1( "Sufficient value difference, and hues are complimentary" ); return fg; } // STAMP1( "Hues are complimentary but we must modify the value or saturation of the contrasting colour" ); // but either the colours are two desaturated, or too dark // so we need to adjust the system, although not as much ///_amount /= 2; } if (fs < 50 && ds < 40) { // low saturation on a low saturation is sad const int tmp = 50 - fs; fs = 50; if (amount > tmp) { _amount -= tmp; } else { _amount = 0; } } // test that there is available value to honor our contrast requirement if (255 - dv < amount) { // we have to modify the value and saturation of fg //adjustToLimits( bv, fv, amount ); // STAMP // see if we need to adjust the saturation if (amount > 0) { adjustToLimits(bs, fs, _amount); } // STAMP // see if we need to adjust the hue if (amount > 0) { fh += amount; // cycles around } // STAMP return TQColor(fh, fs, fv, TQColor::Hsv); } // STAMP if (fv > bv && bv > amount) { return TQColor( fh, fs, bv - amount, TQColor::Hsv); } // STAMP if (fv < bv && fv > amount) { return TQColor(fh, fs, fv - amount, TQColor::Hsv); } // STAMP if (fv > bv && (255 - fv > amount)) { return TQColor(fh, fs, fv + amount, TQColor::Hsv); } // STAMP if (fv < bv && (255 - bv > amount)) { return TQColor(fh, fs, bv + amount, TQColor::Hsv); } // STAMP // debug() << "Something went wrong!\n"; return TQt::blue; #undef amount // #undef STAMP } void Analyzer::Block::paletteChange(const TQPalette&) { const TQColor bg = palette().active().background(); const TQColor fg = ensureContrast(bg, TDEGlobalSettings::activeTitleColor()); m_topBarPixmap.fill(fg); const double dr = 15 * double(bg.red() - fg.red()) / (m_rows * 16); const double dg = 15 * double(bg.green() - fg.green()) / (m_rows * 16); const double db = 15 * double(bg.blue() - fg.blue()) / (m_rows * 16); const int r = fg.red(), g = fg.green(), b = fg.blue(); bar()->fill(bg); TQPainter p(bar()); for (int y = 0; (uint)y < m_rows; ++y) { // graduate the fg color p.fillRect(0, y * (HEIGHT + 1), WIDTH, HEIGHT, TQColor(r + int(dr * y), g + int(dg * y), b + int(db * y))); } { const TQColor bg = palette().active().background().dark(112); // make a complimentary fadebar colour // TODO dark is not always correct, dumbo! int h, s, v; palette().active().background().dark(150).getHsv(&h, &s, &v); const TQColor fg(h + 120, s, v, TQColor::Hsv); const double dr = fg.red() - bg.red(); const double dg = fg.green() - bg.green(); const double db = fg.blue() - bg.blue(); const int r = bg.red(), g = bg.green(), b = bg.blue(); // Precalculate all fade-bar pixmaps for (int y = 0; y < FADE_SIZE; ++y) { m_fadeBars[y].fill(palette().active().background()); TQPainter f(&m_fadeBars[y]); for (int z = 0; (uint)z < m_rows; ++z) { const double Y = 1.0 - (log10(static_cast(FADE_SIZE) - y) / log10(static_cast(FADE_SIZE))); f.fillRect(0, z * (HEIGHT + 1), WIDTH, HEIGHT, TQColor(r + int(dr * Y), g + int(dg * Y), b + int(db * Y))); } } } drawBackground(); } void Analyzer::Block::resizeEvent(TQResizeEvent *e) { TQWidget::resizeEvent(e); canvas()->resize(size()); background()->resize(size()); const uint oldRows = m_rows; // all is explained in analyze().. // +1 to counter -1 in maxSizes, trust me we need this! m_columns = kMax(uint(double(width() + 1) / (WIDTH + 1)), (uint)MAX_COLUMNS); m_rows = uint(double(height() + 1) / (HEIGHT + 1)); // this is the y-offset for drawing from the top of the widget m_y = (height() - (m_rows * (HEIGHT + 1)) + 2) / 2; m_scope.resize(m_columns); if (m_rows != oldRows) { m_barPixmap.resize(WIDTH, m_rows * (HEIGHT + 1)); for (uint i = 0; i < FADE_SIZE; ++i ) { m_fadeBars[i].resize(WIDTH, m_rows * (HEIGHT + 1)); } m_yScale.resize(m_rows + 1); const uint PRE = 1, PRO = 1; //PRE and PRO allow us to restrict the range somewhat for (uint z = 0; z < m_rows; ++z) { m_yScale[z] = 1 - (log10(PRE + z) / log10(PRE + m_rows + PRO)); } m_yScale[m_rows] = 0; determineStep(); paletteChange( palette() ); } else if (width() > e->oldSize().width() || height() > e->oldSize().height()) { drawBackground(); } analyze(m_scope); } void Analyzer::Block::determineStep() { // falltime is dependent on rowcount due to our digital resolution (ie we have boxes/blocks of pixels) // I calculated the value 30 based on some trial and error const double fallTime = 30 * m_rows; // The number of milliseconds between signals with audio data is about 80, // however, basing the step off of that value causes some undersireable // effects in the analyzer (high-end blocks constantly appearing/disappearing). // 44 seems to be a good mid-point. m_step = double(m_rows * 44) / fallTime; } void Analyzer::Block::drawBackground() { const TQColor bg = palette().active().background(); const TQColor bgdark = bg.dark(112); background()->fill(bg); TQPainter p(background()); for (int x = 0; (uint)x < m_columns; ++x) { for (int y = 0; (uint)y < m_rows; ++y) { p.fillRect(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, WIDTH, HEIGHT, bgdark); } } setErasePixmap(*background()); } void Analyzer::interpolate(const Scope& inVec, Scope& outVec) { double pos = 0.0; const double step = (double)inVec.size() / (double)outVec.size(); for (uint i = 0; i < outVec.size(); ++i, pos += step) { const double error = pos - std::floor(pos); const unsigned long offset = (unsigned long)pos; unsigned long indexLeft = offset + 0; if (indexLeft >= inVec.size()) { indexLeft = inVec.size() - 1; } unsigned long indexRight = offset + 1; if (indexRight >= inVec.size()) { indexRight = inVec.size() - 1; } outVec[i] = inVec[indexLeft ] * (1.0 - error) + inVec[indexRight] * error; } } #include "analyzer.moc"