summaryrefslogtreecommitdiffstats
path: root/src/app/analyzer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/analyzer.cpp')
-rw-r--r--src/app/analyzer.cpp611
1 files changed, 559 insertions, 52 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 <math.h> //interpolate()
+#include <cmath> //interpolate()
+#include <tdeglobalsettings.h>
#include <tqevent.h> //event()
#include "xineEngine.h"
#include "fht.cpp"
template<class W>
-Analyzer::Base<W>::Base( TQWidget *parent, uint timeout )
+Analyzer::Base<W>::Base( TQWidget *parent, uint timeout, uint scopeSize )
: W( parent, "Analyzer" )
, m_timeout( timeout )
+ , m_fht(new FHT(scopeSize))
{}
+template<class W> void
+Analyzer::Base<W>::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<class W>
+void Analyzer::Base<W>::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<double>(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 <class W>
+void Analyzer::Base<W>::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<double>(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<class W> bool
Analyzer::Base<W>::event( TQEvent *e )
{
@@ -36,98 +125,516 @@ Analyzer::Base<W>::event( TQEvent *e )
}
-Analyzer::Base2D::Base2D( TQWidget *parent, uint timeout )
- : Base<TQWidget>( parent, timeout )
+Analyzer::Base2D::Base2D( TQWidget *parent, uint timeout, uint scopeSize )
+ : Base<TQWidget>( 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 <[email protected]>, (C) 2003
// Copyright: See COPYING file that comes with this distribution
#include <tqpainter.h>
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<float*>( &s.front() );
- float *front = static_cast<float*>( &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<float>(y) > m_store[x])
+ {
+ y = static_cast<int>(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<int>(_amount)
+// #define STAMP debug() << (TQValueList<int>() << fh << fs << fv) << endl;
+// #define STAMP1( string ) debug() << string << ": " << (TQValueList<int>() << fh << fs << fv) << endl;
+// #define STAMP2( string, value ) debug() << string << "=" << value << ": " << (TQValueList<int>() << 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<float>(FADE_SIZE) - y) / log10(static_cast<float>(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"