diff --git a/src/app/analyzer.cpp b/src/app/analyzer.cpp index e0f82ec..7a80872 100644 --- a/src/app/analyzer.cpp +++ b/src/app/analyzer.cpp @@ -4,18 +4,107 @@ #include "analyzer.h" #include "../codeine.h" #include "../debug.h" -#include //interpolate() +#include //interpolate() +#include #include //event() #include "xineEngine.h" #include "fht.cpp" template -Analyzer::Base::Base( TQWidget *parent, uint timeout ) +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 ) { @@ -36,98 +125,516 @@ Analyzer::Base::event( TQEvent *e ) } -Analyzer::Base2D::Base2D( TQWidget *parent, uint timeout ) - : Base( parent, timeout ) +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::draw() +Analyzer::Base2D::resizeEvent( TQResizeEvent *e) { - switch( Codeine::engine()->state() ) { - case Engine::Playing: - { - const Engine::Scope &thescope = Codeine::engine()->scope(); - static Analyzer::Scope scope( Analyzer::SCOPE_SIZE ); - - for( int x = 0; x < Analyzer::SCOPE_SIZE; ++x ) - scope[x] = double(thescope[x]) / (1<<15); - - transform( scope ); - analyze( scope ); - - scope.resize( Analyzer::SCOPE_SIZE ); - - bitBlt( this, 0, 0, canvas() ); - break; - } - case Engine::Paused: - break; + m_background.resize(size()); + m_canvas.resize(size()); + m_background.fill(backgroundColor()); + eraseCanvas(); - default: - erase(); - } + TQWidget::resizeEvent(e); } -void -Analyzer::Base2D::resizeEvent( TQResizeEvent* ) +void Analyzer::Base2D::paletteChange(const TQPalette&) { - m_canvas.resize( size() ); - m_canvas.fill( colorGroup().background() ); + 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 ) + : 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) { - setMinimumWidth( 64 ); //-1 is padding, no drawing takes place there - setMaximumWidth( 128 ); + // -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); - //TODO yes, do height for width + for (auto &m_fadeBar : m_fadeBars) + { + m_fadeBar.resize(1, 1); + } } void -Analyzer::Block::transform( Analyzer::Scope &scope ) //pure virtual +Analyzer::Block::transform( Analyzer::Scope &s ) //pure virtual { - static FHT fht( Analyzer::SCOPE_SIZE_EXP ); + for( uint x = 0; x < s.size(); ++x ) + s[x] *= 2; - for( uint x = 0; x < scope.size(); ++x ) - scope[x] *= 2; + float *front = static_cast( &s.front() ); - float *front = static_cast( &scope.front() ); + m_fht->spectrum( front ); + m_fht->scale( front, 1.0 / 20 ); - fht.spectrum( front ); - fht.scale( front, 1.0 / 40 ); + //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 ) { - canvas()->fill( colorGroup().foreground().light() ); + // 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) + { + } - TQPainter p( canvas() ); - p.setPen( colorGroup().background() ); + ~OutputOnExit() + { + int h, s, v; + c.getHsv(&h, &s, &v); + } - const double F = double(height()) / (log10( 256 ) * 1.1 /*<- max. amplitude*/); + private: + const TQColor &c; + }; - for( uint x = 0; x < s.size(); ++x ) - //we draw the blank bit - p.drawLine( x, 0, x, int(height() - log10( s[x] * 256.0 ) * F) ); + // 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; + m_step = double(m_rows * 80) / fallTime; // 80 = ~milliseconds between signals with audio data +} + +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()); } -int -Analyzer::Block::heightForWidth( int w ) const +void Analyzer::interpolate(const Scope& inVec, Scope& outVec) { - return w / 2; + 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" diff --git a/src/app/analyzer.h b/src/app/analyzer.h index 3339c9d..7b8daac 100644 --- a/src/app/analyzer.h +++ b/src/app/analyzer.h @@ -8,6 +8,7 @@ #include #endif +#include "fht.h" #include //stack allocated and convenience #include //stack allocated #include //baseclass @@ -23,10 +24,13 @@ namespace Analyzer uint timeout() const { return m_timeout; } protected: - Base( TQWidget*, uint ); + Base( TQWidget*, uint, uint = 7 ); + ~Base() { delete m_fht; } + void drawFrame(); virtual void transform( Scope& ) = 0; virtual void analyze( const Scope& ) = 0; + virtual void demo(); private: virtual bool event( TQEvent* ); @@ -34,42 +38,91 @@ namespace Analyzer protected: TQTimer m_timer; uint m_timeout; + FHT *m_fht; }; class Base2D : public Base { TQ_OBJECT public: + const TQPixmap *background() const { return &m_background; } const TQPixmap *canvas() const { return &m_canvas; } private slots: - void draw(); + void draw() { drawFrame(); bitBlt(this, 0, 0, canvas()); } protected: - Base2D( TQWidget*, uint timeout ); + Base2D( TQWidget*, uint timeout, uint scopeSize = 7 ); + TQPixmap *background() { return &m_background; } TQPixmap *canvas() { return &m_canvas; } - void paintEvent( TQPaintEvent* ) { if( !m_canvas.isNull() ) bitBlt( this, 0, 0, canvas() ); } - void resizeEvent( TQResizeEvent* ); + void eraseCanvas() + { + bitBlt(canvas(), 0, 0, background()); + } + + void paintEvent( TQPaintEvent* ) override { if( !m_canvas.isNull() ) bitBlt( this, 0, 0, canvas() ); } + void resizeEvent( TQResizeEvent* ) override; + void paletteChange( const TQPalette& ) override; private: + TQPixmap m_background; TQPixmap m_canvas; }; class Block : public Analyzer::Base2D { public: - Block( TQWidget* ); + explicit Block( TQWidget* ); + + static constexpr int HEIGHT = 2; + static constexpr int FADE_SIZE = 90; + static constexpr int MIN_COLUMNS = 32; + static constexpr int MAX_COLUMNS = 256; + static constexpr int MIN_ROWS = 3; + static constexpr int WIDTH = 4; protected: virtual void transform( Analyzer::Scope& ); virtual void analyze( const Analyzer::Scope& ); + void paletteChange(const TQPalette&) override; + void resizeEvent(TQResizeEvent *) override; + + void determineStep(); + void drawBackground(); + + private: + TQPixmap *bar() + { + return &m_barPixmap; + } - virtual int heightForWidth( int ) const; + // So we don't create a vector each frame. + Scope m_scope; + TQPixmap m_barPixmap; + TQPixmap m_topBarPixmap; - virtual void show() {} //TODO temporary as the scope plugin causes freezes + // Current bar heights + std::vector m_store; + std::vector m_yScale; + + std::vector m_fadeBars; + std::vector m_fadeIntensity; + std::vector m_fadePos; + + // Number of rows and columns of blocks + unsigned m_columns; + unsigned m_rows; + + // y-offset from top of widget + unsigned m_y; + + // rows to fall per-step + float m_step; }; + + void interpolate(const Scope&, Scope&); } #endif diff --git a/src/app/xineEngine.cpp b/src/app/xineEngine.cpp index 708ab13..2cb9cd3 100644 --- a/src/app/xineEngine.cpp +++ b/src/app/xineEngine.cpp @@ -615,7 +615,7 @@ VideoWindow::scope() //prune the buffer list and update the m_current_vpts timestamp timerEvent( nullptr ); - // const int64_t pts_per_smpls = 0; //scope_plugin_pts_per_smpls( m_scope ); + const int64_t pts_per_smpls = scope_plugin_pts_per_smpls(m_scope); for( int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_CHANNELS ), frame = 0; frame < SCOPE_SIZE; ) { MyNode *best_node = nullptr; @@ -631,7 +631,7 @@ VideoWindow::scope() diff = m_current_vpts; diff -= best_node->vpts; diff *= 1<<16; - // diff /= pts_per_smpls; + diff /= pts_per_smpls; const int16_t* data16 = best_node->mem;