diff --git a/src/context/applets/analyzer/plugin/AnalyzerWorker.cpp b/src/context/applets/analyzer/plugin/AnalyzerWorker.cpp index e323dbc450..9e42911823 100644 --- a/src/context/applets/analyzer/plugin/AnalyzerWorker.cpp +++ b/src/context/applets/analyzer/plugin/AnalyzerWorker.cpp @@ -1,357 +1,372 @@ /* * Copyright 2018 Malte Veerman * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "AnalyzerWorker.h" #include "core/support/Debug.h" #include "EngineController.h" #include #include Analyzer::Worker::Worker() : m_currentScope( QVector( 1, 0.0 ) ) , m_size( 0 ) , m_windowFunction( Base::Hann ) , m_expectedDataTime( 20 ) , m_demoT( 201 ) , m_lastUpdate( QTime::currentTime() ) , m_demoTimer( new QTimer( this ) ) , m_processTimer( new QTimer( this ) ) { m_in = (double*) fftw_malloc( m_size * sizeof( double ) ); m_out = (std::complex*) fftw_malloc( ( m_size / 2 + 1 ) * sizeof( std::complex ) ); m_plan = fftw_plan_dft_r2c_1d( m_size, m_in, reinterpret_cast( m_out ), FFTW_ESTIMATE ); m_demoTimer->setInterval( Analyzer::Base::DEMO_INTERVAL ); m_processTimer->setInterval( PROCESSING_INTERVAL ); if( EngineController::instance()->isPlaying() ) m_processTimer->start(); else m_demoTimer->start(); connect( m_demoTimer, &QTimer::timeout, this, &Worker::demo ); connect( m_processTimer, &QTimer::timeout, this, &Worker::processData ); } Analyzer::Worker::~Worker() { fftw_destroy_plan( m_plan ); fftw_free( m_in ); fftw_free( m_out ); } void Analyzer::Worker::receiveData( const QMap > &newData ) { const int newDataSize = EngineController::DATAOUTPUT_DATA_SIZE; if( newData.isEmpty() || newData[Phonon::AudioDataOutput::LeftChannel].size() != newDataSize ) return; if( m_size < newDataSize ) { debug() << "Data size mismatch in analyzer!"; return; } m_rawInMutex.lock(); for( int x = 0; x < newDataSize; x++ ) { if( newData.size() == 1 ) // Mono { m_rawIn << double( newData[Phonon::AudioDataOutput::LeftChannel][x] ); } else // Anything > Mono is treated as Stereo { m_rawIn << ( double( newData[Phonon::AudioDataOutput::LeftChannel][x] ) + double( newData[Phonon::AudioDataOutput::RightChannel][x] ) ) / 2; // Average between the channels } m_rawIn.last() /= ( 1 << 15 ); // Scale to [0, 1] } while( m_rawIn.size() > (int)m_size + DATA_BUFFER_SIZE * newDataSize ) m_rawIn.removeFirst(); m_rawInMutex.unlock(); } void Analyzer::Worker::processData() { int timeElapsed = m_lastUpdate.elapsed(); // Delay if processing is too fast if( timeElapsed < m_expectedDataTime ) QThread::currentThread()->msleep( m_expectedDataTime - timeElapsed ); applyWindowFunction(); } void Analyzer::Worker::applyWindowFunction() { m_rawInMutex.lock(); if( m_rawIn.size() < (int)m_size ) { m_rawInMutex.unlock(); return; } const int newDataSize = EngineController::DATAOUTPUT_DATA_SIZE; // Apply window function for( uint i = 0; i < m_size; i++ ) { double windowFactor; switch( m_windowFunction ) { case Base::Rectangular: { windowFactor = 1.0; break; } case Base::Hann: { windowFactor = ( 1.0 - cos( 2.0 * M_PI * i / ( m_size - 1 ) ) ) / 2.0; break; } case Base::Nuttall: { const double a = 0.355768; const double b = 0.487396 * cos( 2 * M_PI * i / ( m_size - 1 ) ); const double c = 0.144232 * cos( 4 * M_PI * i / ( m_size - 1 ) ); const double d = 0.012604 * cos( 6 * M_PI * i / ( m_size - 1 ) ); windowFactor = a - b + c - d; break; } case Base::Lanczos: { const double x = 2.0 * i / ( m_size - 1 ) - 1; windowFactor = sin( M_PI * x ) / M_PI / x; break; } case Base::Sine: { windowFactor = ( M_PI * i ) / ( m_size - 1 ); break; } }; if( i < newDataSize ) m_in[i] = m_rawIn.takeFirst() * windowFactor; else m_in[i] = m_rawIn.at( i - newDataSize ) * windowFactor; } m_rawInMutex.unlock(); fftw_execute( m_plan ); makeScope(); } void Analyzer::Worker::makeScope() { for( const auto& band : m_notInterpolatedScopeBands ) { m_currentScope[band.scopeIndex] = 0.0; uint numValues = 0; for( long k = std::lround( std::ceil( band.lowerK ) ); k <= std::lround( std::floor( band.upperK ) ); k++ ) { m_currentScope[band.scopeIndex] += std::abs( m_out[k] ) * sqrt( k ); numValues++; } m_currentScope[band.scopeIndex] /= numValues; m_currentScope[band.scopeIndex] /= m_size / 2; } // monotone cubic interpolation - QVector data; - for( uint k = 0; k < m_size / 2 + 1 && k <= m_interpolatedScopeBands.last().midK; k++ ) + if( !m_interpolatedScopeBands.isEmpty() ) { - data << QPointF( k, std::abs( m_out[k] ) * sqrt( k ) / m_size * 2 ); - } - // Get consecutive differences and slopes - QVector dys, dxs, ms; - for( int i = 0; i < data.size() - 1; i++ ) - { - double dx = data[i + 1].x() - data[i].x(); - double dy = data[i + 1].y() - data[i].y(); - dxs << dx; - dys << dy; - ms << dy / dx; - } - // Get degree-1 coefficients - QVector c1s = QVector() << ms[0]; - for( int i = 0; i < dxs.size() - 1; i++) - { - double m = ms[i], mNext = ms[i + 1]; - if( m * mNext <= 0 ) - c1s << 0.0; - else + QVector data; + for( uint k = 0; k < m_size / 2 + 1 && k <= m_interpolatedScopeBands.last().midK; k++ ) { - double dx_ = dxs[i], dxNext = dxs[i + 1], common = dx_ + dxNext; - c1s << ( 3 * common / ( ( common + dxNext ) / m + ( common + dx_ ) / mNext ) ); + data << QPointF( k, std::abs( m_out[k] ) * sqrt( k ) / m_size * 2 ); } - } - c1s << ms.last(); - // Get degree-2 and degree-3 coefficients - QVector c2s, c3s; - for( int i = 0; i < c1s.size() - 1; i++ ) - { - double c1 = c1s[i], m_ = ms[i], invDx = 1 / dxs[i], common_ = c1 + c1s[i + 1] - m_ - m_; - c2s << ( m_ - c1 - common_ ) * invDx; - c3s << common_ * invDx * invDx; - } - // write interpolated data to scope - for( auto &band : m_interpolatedScopeBands ) - { - const double x = band.midK; - auto &scope = m_currentScope[band.scopeIndex]; - - // Search for the interval x is in, returning the corresponding y if x is one of the original xs - int low = 0, mid, high = c3s.size() - 1; - while ( low <= high ) + // Get consecutive differences and slopes + QVector dys, dxs, ms; + for( int i = 0; i < data.size() - 1; i++ ) { - mid = std::floor( 0.5 * ( low + high ) ); - double xHere = data[mid].x(); - if( xHere < x ) - low = mid + 1; - else if( xHere > x ) - high = mid - 1; + double dx = data[i + 1].x() - data[i].x(); + double dy = data[i + 1].y() - data[i].y(); + dxs << dx; + dys << dy; + ms << dy / dx; + } + // Get degree-1 coefficients + QVector c1s = QVector() << ms[0]; + for( int i = 0; i < dxs.size() - 1; i++) + { + double m = ms[i], mNext = ms[i + 1]; + if( m * mNext <= 0 ) + c1s << 0.0; else - scope = data[mid].y(); + { + double dx_ = dxs[i], dxNext = dxs[i + 1], common = dx_ + dxNext; + c1s << ( 3 * common / ( ( common + dxNext ) / m + ( common + dx_ ) / mNext ) ); + } } - int i = qMax( 0, high ); + c1s << ms.last(); + // Get degree-2 and degree-3 coefficients + QVector c2s, c3s; + for( int i = 0; i < c1s.size() - 1; i++ ) + { + double c1 = c1s[i], m_ = ms[i], invDx = 1 / dxs[i], common_ = c1 + c1s[i + 1] - m_ - m_; + c2s << ( m_ - c1 - common_ ) * invDx; + c3s << common_ * invDx * invDx; + } + // write interpolated data to scope + for( auto &band : m_interpolatedScopeBands ) + { + const double x = band.midK; + auto &scope = m_currentScope[band.scopeIndex]; - // Interpolate - double diff = x - data[i].x(), diffSq = diff * diff; - scope = qMax( 0.0, data[i].y() + c1s[i] * diff + c2s[i] * diffSq + c3s[i] * diff * diffSq ); + // Search for the interval x is in, returning the corresponding y if x is one of the original xs + int low = 0, mid, high = c3s.size() - 1; + while ( low <= high ) + { + mid = std::floor( 0.5 * ( low + high ) ); + double xHere = data[mid].x(); + if( xHere < x ) + low = mid + 1; + else if( xHere > x ) + high = mid - 1; + else + scope = data[mid].y(); + } + int i = qMax( 0, high ); + + // Interpolate + double diff = x - data[i].x(), diffSq = diff * diff; + scope = qMax( 0.0, data[i].y() + c1s[i] * diff + c2s[i] * diffSq + c3s[i] * diff * diffSq ); + } } analyze(); } void Analyzer::Worker::setSampleSize( uint size ) { if( m_size == size ) return; m_size = size; fftw_destroy_plan( m_plan ); fftw_free( m_in ); fftw_free( m_out ); m_in = (double*) fftw_malloc( m_size * sizeof( double ) ); m_out = (std::complex*) fftw_malloc( ( m_size / 2 + 1 ) * sizeof( std::complex ) ); m_plan = fftw_plan_dft_r2c_1d( m_size, m_in, reinterpret_cast( m_out ), FFTW_ESTIMATE ); } void Analyzer::Worker::setWindowFunction( Base::WindowFunction windowFunction ) { if( m_windowFunction == windowFunction ) return; m_windowFunction = windowFunction; } void Analyzer::Worker::setScopeSize( int size ) { m_currentScope.resize( size ); } void Analyzer::Worker::calculateExpFactor( qreal minFreq, qreal maxFreq, int sampleRate ) { DEBUG_BLOCK + if( minFreq <= 0.0 ) + { + warning() << "Minimum frequency must be greater than zero!"; + minFreq = 1.0; + } + if( minFreq >= maxFreq ) { warning() << "Minimum frequency must be smaller than maximum frequency!"; - return; + maxFreq = minFreq + 1.0; + } + + if( sampleRate == 0 ) + { + debug() << "Reported impossible sample rate of zero. Assuming 44.1KHz."; + sampleRate = 44100; } m_expFactor = pow( maxFreq / minFreq, 1.0 / m_currentScope.size() ); m_expectedDataTime = std::floor( (qreal)EngineController::DATAOUTPUT_DATA_SIZE * 1000.0 / sampleRate ); m_interpolatedScopeBands.clear(); m_notInterpolatedScopeBands.clear(); const uint outputSize = m_size / 2 + 1; for( int scopeIndex = 0; scopeIndex < m_currentScope.size(); scopeIndex++ ) { BandInfo newBandInfo; newBandInfo.lowerFreq = minFreq * pow( m_expFactor, double( scopeIndex ) - 0.5 ); newBandInfo.midFreq = minFreq * pow( m_expFactor, scopeIndex ); newBandInfo.upperFreq = minFreq * pow( m_expFactor, double( scopeIndex ) + 0.5 ); newBandInfo.lowerK = newBandInfo.lowerFreq / ( sampleRate / 2 ) * outputSize; newBandInfo.midK = newBandInfo.midFreq / ( sampleRate / 2 ) * outputSize; newBandInfo.upperK = newBandInfo.upperFreq / ( sampleRate / 2 ) * outputSize; newBandInfo.scopeIndex = scopeIndex; if ( std::floor( newBandInfo.upperK ) >= std::ceil( newBandInfo.lowerK ) ) m_notInterpolatedScopeBands << newBandInfo; else m_interpolatedScopeBands << newBandInfo; } } void Analyzer::Worker::demo() { if( m_demoT > 300 ) m_demoT = 1; //0 = wasted calculations if( m_demoT < 201 ) { const double dt = double( m_demoT ) / 200; for( int i = 0; i < m_currentScope.size(); ++i ) { m_currentScope[i] = dt * ( sin( M_PI + ( i * M_PI ) / m_currentScope.size() ) + 1.0 ); } } else { for( int i = 0; i < m_currentScope.size(); ++i ) { m_currentScope[i] = 0.0; } } ++m_demoT; int timeElapsed = m_lastUpdate.elapsed(); // Delay if interval is too low if( timeElapsed < Analyzer::Base::DEMO_INTERVAL - 1 ) QThread::currentThread()->msleep( Analyzer::Base::DEMO_INTERVAL - 1 - timeElapsed ); m_lastUpdate.restart(); analyze(); } void Analyzer::Worker::playbackStateChanged() { bool playing = EngineController::instance()->isPlaying(); playing ? m_demoTimer->stop() : m_demoTimer->start(); playing ? m_processTimer->start() : m_processTimer->stop(); resetDemo(); }