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 <stigma@disroot.org>
pull/26/head
mio 1 month ago
parent 6eb38b0280
commit a3ea0ee70f

@ -4,18 +4,107 @@
#include "analyzer.h" #include "analyzer.h"
#include "../codeine.h" #include "../codeine.h"
#include "../debug.h" #include "../debug.h"
#include <math.h> //interpolate() #include <cmath> //interpolate()
#include <tdeglobalsettings.h>
#include <tqevent.h> //event() #include <tqevent.h> //event()
#include "xineEngine.h" #include "xineEngine.h"
#include "fht.cpp" #include "fht.cpp"
template<class W> template<class W>
Analyzer::Base<W>::Base( TQWidget *parent, uint timeout ) Analyzer::Base<W>::Base( TQWidget *parent, uint timeout, uint scopeSize )
: W( parent, "Analyzer" ) : W( parent, "Analyzer" )
, m_timeout( timeout ) , 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 template<class W> bool
Analyzer::Base<W>::event( TQEvent *e ) Analyzer::Base<W>::event( TQEvent *e )
{ {
@ -36,98 +125,516 @@ Analyzer::Base<W>::event( TQEvent *e )
} }
Analyzer::Base2D::Base2D( TQWidget *parent, uint timeout ) Analyzer::Base2D::Base2D( TQWidget *parent, uint timeout, uint scopeSize )
: Base<TQWidget>( parent, timeout ) : Base<TQWidget>( parent, timeout, scopeSize )
{ {
setWFlags( TQt::WNoAutoErase ); //no flicker setWFlags( TQt::WNoAutoErase ); //no flicker
connect( &m_timer, TQ_SIGNAL(timeout()), TQ_SLOT(draw()) ); connect( &m_timer, TQ_SIGNAL(timeout()), TQ_SLOT(draw()) );
} }
void void
Analyzer::Base2D::draw() Analyzer::Base2D::resizeEvent( TQResizeEvent *e)
{ {
switch( Codeine::engine()->state() ) { m_background.resize(size());
case Engine::Playing: m_canvas.resize(size());
{ m_background.fill(backgroundColor());
const Engine::Scope &thescope = Codeine::engine()->scope(); eraseCanvas();
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;
default: TQWidget::resizeEvent(e);
erase();
}
} }
void void Analyzer::Base2D::paletteChange(const TQPalette&)
Analyzer::Base2D::resizeEvent( TQResizeEvent* )
{ {
m_canvas.resize( size() ); m_background.fill(backgroundColor());
m_canvas.fill( colorGroup().background() ); eraseCanvas();
} }
// Author: Max Howell <max.howell@methylblue.com>, (C) 2003 // Author: Max Howell <max.howell@methylblue.com>, (C) 2003
// Copyright: See COPYING file that comes with this distribution // Copyright: See COPYING file that comes with this distribution
#include <tqpainter.h> #include <tqpainter.h>
Analyzer::Block::Block( TQWidget *parent ) 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 // -1 is padding, no drawing takes place there
setMaximumWidth( 128 ); 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 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 ) float *front = static_cast<float*>( &s.front() );
scope[x] *= 2;
float *front = static_cast<float*>( &scope.front() ); m_fht->spectrum( front );
m_fht->scale( front, 1.0 / 20 );
fht.spectrum( front ); //the second half is pretty dull, so only show it if the user has a large analyzer
fht.scale( front, 1.0 / 40 ); //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 void
Analyzer::Block::analyze( const Analyzer::Scope &s ) 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() ); ~OutputOnExit()
p.setPen( colorGroup().background() ); {
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 ) // hack so I don't have to cast everywhere
//we draw the blank bit #define amount static_cast<int>(_amount)
p.drawLine( x, 0, x, int(height() - log10( s[x] * 256.0 ) * F) ); // #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 void Analyzer::interpolate(const Scope& inVec, Scope& outVec)
Analyzer::Block::heightForWidth( int w ) const
{ {
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" #include "analyzer.moc"

@ -8,6 +8,7 @@
#include <sys/types.h> #include <sys/types.h>
#endif #endif
#include "fht.h"
#include <tqpixmap.h> //stack allocated and convenience #include <tqpixmap.h> //stack allocated and convenience
#include <tqtimer.h> //stack allocated #include <tqtimer.h> //stack allocated
#include <tqwidget.h> //baseclass #include <tqwidget.h> //baseclass
@ -23,10 +24,13 @@ namespace Analyzer
uint timeout() const { return m_timeout; } uint timeout() const { return m_timeout; }
protected: protected:
Base( TQWidget*, uint ); Base( TQWidget*, uint, uint = 7 );
~Base() { delete m_fht; }
void drawFrame();
virtual void transform( Scope& ) = 0; virtual void transform( Scope& ) = 0;
virtual void analyze( const Scope& ) = 0; virtual void analyze( const Scope& ) = 0;
virtual void demo();
private: private:
virtual bool event( TQEvent* ); virtual bool event( TQEvent* );
@ -34,42 +38,91 @@ namespace Analyzer
protected: protected:
TQTimer m_timer; TQTimer m_timer;
uint m_timeout; uint m_timeout;
FHT *m_fht;
}; };
class Base2D : public Base<TQWidget> class Base2D : public Base<TQWidget>
{ {
TQ_OBJECT TQ_OBJECT
public: public:
const TQPixmap *background() const { return &m_background; }
const TQPixmap *canvas() const { return &m_canvas; } const TQPixmap *canvas() const { return &m_canvas; }
private slots: private slots:
void draw(); void draw() { drawFrame(); bitBlt(this, 0, 0, canvas()); }
protected: protected:
Base2D( TQWidget*, uint timeout ); Base2D( TQWidget*, uint timeout, uint scopeSize = 7 );
TQPixmap *background() { return &m_background; }
TQPixmap *canvas() { return &m_canvas; } TQPixmap *canvas() { return &m_canvas; }
void paintEvent( TQPaintEvent* ) { if( !m_canvas.isNull() ) bitBlt( this, 0, 0, canvas() ); } void eraseCanvas()
void resizeEvent( TQResizeEvent* ); {
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: private:
TQPixmap m_background;
TQPixmap m_canvas; TQPixmap m_canvas;
}; };
class Block : public Analyzer::Base2D class Block : public Analyzer::Base2D
{ {
public: 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: protected:
virtual void transform( Analyzer::Scope& ); virtual void transform( Analyzer::Scope& );
virtual void analyze( const 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<float> m_store;
std::vector<float> m_yScale;
std::vector<TQPixmap> m_fadeBars;
std::vector<int> m_fadeIntensity;
std::vector<unsigned> 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 #endif

@ -615,7 +615,7 @@ VideoWindow::scope()
//prune the buffer list and update the m_current_vpts timestamp //prune the buffer list and update the m_current_vpts timestamp
timerEvent( nullptr ); 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; ) for( int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_CHANNELS ), frame = 0; frame < SCOPE_SIZE; )
{ {
MyNode *best_node = nullptr; MyNode *best_node = nullptr;
@ -631,7 +631,7 @@ VideoWindow::scope()
diff = m_current_vpts; diff = m_current_vpts;
diff -= best_node->vpts; diff -= best_node->vpts;
diff *= 1<<16; diff *= 1<<16;
// diff /= pts_per_smpls; diff /= pts_per_smpls;
const int16_t* const int16_t*
data16 = best_node->mem; data16 = best_node->mem;

Loading…
Cancel
Save