From a3ea0ee70fe8590a96df03dca43ca77f3f28791e Mon Sep 17 00:00:00 2001 From: mio Date: Sun, 20 Oct 2024 17:27:01 +1000 Subject: [PATCH] Fix the audio analyzer Most of the code was already borrowed from Amarok, but wasn't properly finished. This just updates the code to more closely match what is currently in TDE's Amarok. The Analyzer still sits in the statusBar(), which is cool, but can have some delays when watching a video (the video itself is unaffected). See: TDE/codeine#23 Signed-off-by: mio --- src/app/analyzer.cpp | 611 +++++++++++++++++++++++++++++++++++++---- src/app/analyzer.h | 69 ++++- src/app/xineEngine.cpp | 4 +- 3 files changed, 622 insertions(+), 62 deletions(-) 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;