Changeset View
Changeset View
Standalone View
Standalone View
src/context/applets/analyzer/plugin/AnalyzerWorker.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | * Copyright 2018 Malte Veerman <malte.veerman@gmail.com> | ||||
3 | * | ||||
4 | * This program is free software; you can redistribute it and/or | ||||
5 | * modify it under the terms of the GNU General Public License as | ||||
6 | * published by the Free Software Foundation; either version 2 of | ||||
7 | * the License or (at your option) version 3 or any later version | ||||
8 | * accepted by the membership of KDE e.V. (or its successor approved | ||||
9 | * by the membership of KDE e.V.), which shall act as a proxy | ||||
10 | * defined in Section 14 of version 3 of the license. | ||||
11 | * | ||||
12 | * This program is distributed in the hope that it will be useful, | ||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
15 | * GNU General Public License for more details. | ||||
16 | * | ||||
17 | * You should have received a copy of the GNU General Public License | ||||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
19 | */ | ||||
20 | | ||||
21 | #include "AnalyzerWorker.h" | ||||
22 | | ||||
23 | #include "core/support/Debug.h" | ||||
24 | #include "EngineController.h" | ||||
25 | | ||||
26 | #include <QThread> | ||||
27 | #include <QTimer> | ||||
28 | | ||||
29 | | ||||
30 | Analyzer::Worker::Worker() | ||||
31 | : m_currentScope( QVector<double>( 1, 0.0 ) ) | ||||
32 | , m_size( 0 ) | ||||
33 | , m_windowFunction( Base::Hann ) | ||||
34 | , m_expectedDataTime( 20 ) | ||||
35 | , m_demoT( 201 ) | ||||
36 | , m_lastUpdate( QTime::currentTime() ) | ||||
37 | , m_demoTimer( new QTimer( this ) ) | ||||
38 | , m_processTimer( new QTimer( this ) ) | ||||
39 | { | ||||
40 | m_in = (double*) fftw_malloc( m_size * sizeof( double ) ); | ||||
41 | m_out = (std::complex<double>*) fftw_malloc( ( m_size / 2 + 1 ) * sizeof( std::complex<double> ) ); | ||||
42 | m_plan = fftw_plan_dft_r2c_1d( m_size, m_in, reinterpret_cast<fftw_complex*>( m_out ), FFTW_ESTIMATE ); | ||||
43 | | ||||
44 | m_demoTimer->setInterval( Analyzer::Base::DEMO_INTERVAL ); | ||||
45 | m_processTimer->setInterval( PROCESSING_INTERVAL ); | ||||
46 | if( EngineController::instance()->isPlaying() ) | ||||
47 | m_processTimer->start(); | ||||
48 | else | ||||
49 | m_demoTimer->start(); | ||||
50 | | ||||
51 | connect( m_demoTimer, &QTimer::timeout, this, &Worker::demo ); | ||||
52 | connect( m_processTimer, &QTimer::timeout, this, &Worker::processData ); | ||||
53 | } | ||||
54 | | ||||
55 | Analyzer::Worker::~Worker() | ||||
56 | { | ||||
57 | fftw_destroy_plan( m_plan ); | ||||
58 | fftw_free( m_in ); | ||||
59 | fftw_free( m_out ); | ||||
60 | } | ||||
61 | | ||||
62 | void Analyzer::Worker::receiveData( const QMap<Phonon::AudioDataOutput::Channel, QVector<qint16> > &newData ) | ||||
63 | { | ||||
64 | const int newDataSize = EngineController::DATAOUTPUT_DATA_SIZE; | ||||
65 | | ||||
66 | if( newData.isEmpty() || newData[Phonon::AudioDataOutput::LeftChannel].size() != newDataSize ) | ||||
67 | return; | ||||
68 | | ||||
69 | if( m_size < newDataSize ) | ||||
70 | { | ||||
71 | debug() << "Data size mismatch in analyzer!"; | ||||
72 | return; | ||||
73 | } | ||||
74 | | ||||
75 | m_rawInMutex.lock(); | ||||
76 | | ||||
77 | for( int x = 0; x < newDataSize; x++ ) | ||||
78 | { | ||||
79 | if( newData.size() == 1 ) // Mono | ||||
80 | { | ||||
81 | m_rawIn << double( newData[Phonon::AudioDataOutput::LeftChannel][x] ); | ||||
82 | } | ||||
83 | else // Anything > Mono is treated as Stereo | ||||
84 | { | ||||
85 | m_rawIn << ( double( newData[Phonon::AudioDataOutput::LeftChannel][x] ) | ||||
86 | + double( newData[Phonon::AudioDataOutput::RightChannel][x] ) ) | ||||
87 | / 2; // Average between the channels | ||||
88 | } | ||||
89 | m_rawIn.last() /= ( 1 << 15 ); // Scale to [0, 1] | ||||
90 | } | ||||
91 | | ||||
92 | while( m_rawIn.size() > (int)m_size + DATA_BUFFER_SIZE * newDataSize ) | ||||
93 | m_rawIn.removeFirst(); | ||||
94 | | ||||
95 | m_rawInMutex.unlock(); | ||||
96 | } | ||||
97 | | ||||
98 | void Analyzer::Worker::processData() | ||||
99 | { | ||||
100 | int timeElapsed = m_lastUpdate.elapsed(); | ||||
101 | | ||||
102 | // Delay if processing is too fast | ||||
103 | if( timeElapsed < m_expectedDataTime ) | ||||
104 | QThread::currentThread()->msleep( m_expectedDataTime - timeElapsed ); | ||||
105 | | ||||
106 | applyWindowFunction(); | ||||
107 | } | ||||
108 | | ||||
109 | void Analyzer::Worker::applyWindowFunction() | ||||
110 | { | ||||
111 | m_rawInMutex.lock(); | ||||
112 | | ||||
113 | if( m_rawIn.size() < (int)m_size ) | ||||
114 | { | ||||
115 | m_rawInMutex.unlock(); | ||||
116 | return; | ||||
117 | } | ||||
118 | | ||||
119 | const int newDataSize = EngineController::DATAOUTPUT_DATA_SIZE; | ||||
120 | | ||||
121 | // Apply window function | ||||
122 | for( uint i = 0; i < m_size; i++ ) | ||||
123 | { | ||||
124 | double windowFactor; | ||||
125 | switch( m_windowFunction ) | ||||
126 | { | ||||
127 | case Base::Rectangular: | ||||
128 | { | ||||
129 | windowFactor = 1.0; | ||||
130 | break; | ||||
131 | } | ||||
132 | case Base::Hann: | ||||
133 | { | ||||
134 | windowFactor = ( 1.0 - cos( 2.0 * M_PI * i / ( m_size - 1 ) ) ) / 2.0; | ||||
135 | break; | ||||
136 | } | ||||
137 | case Base::Nuttall: | ||||
138 | { | ||||
139 | const double a = 0.355768; | ||||
140 | const double b = 0.487396 * cos( 2 * M_PI * i / ( m_size - 1 ) ); | ||||
141 | const double c = 0.144232 * cos( 4 * M_PI * i / ( m_size - 1 ) ); | ||||
142 | const double d = 0.012604 * cos( 6 * M_PI * i / ( m_size - 1 ) ); | ||||
143 | windowFactor = a - b + c - d; | ||||
144 | break; | ||||
145 | } | ||||
146 | case Base::Lanczos: | ||||
147 | { | ||||
148 | const double x = 2.0 * i / ( m_size - 1 ) - 1; | ||||
149 | windowFactor = sin( M_PI * x ) / M_PI / x; | ||||
150 | break; | ||||
151 | } | ||||
152 | case Base::Sine: | ||||
153 | { | ||||
154 | windowFactor = ( M_PI * i ) / ( m_size - 1 ); | ||||
155 | break; | ||||
156 | } | ||||
157 | }; | ||||
158 | | ||||
159 | if( i < newDataSize ) | ||||
160 | m_in[i] = m_rawIn.takeFirst() * windowFactor; | ||||
161 | else | ||||
162 | m_in[i] = m_rawIn.at( i - newDataSize ) * windowFactor; | ||||
163 | } | ||||
164 | | ||||
165 | m_rawInMutex.unlock(); | ||||
166 | | ||||
167 | fftw_execute( m_plan ); | ||||
168 | makeScope(); | ||||
169 | } | ||||
170 | | ||||
171 | void Analyzer::Worker::makeScope() | ||||
172 | { | ||||
173 | for( const auto& band : m_notInterpolatedScopeBands ) | ||||
174 | { | ||||
175 | m_currentScope[band.scopeIndex] = 0.0; | ||||
176 | uint numValues = 0; | ||||
177 | for( long k = std::lround( std::ceil( band.lowerK ) ); k <= std::lround( std::floor( band.upperK ) ); k++ ) | ||||
178 | { | ||||
179 | m_currentScope[band.scopeIndex] += std::abs( m_out[k] ) * sqrt( k ); | ||||
180 | numValues++; | ||||
181 | } | ||||
182 | m_currentScope[band.scopeIndex] /= numValues; | ||||
183 | m_currentScope[band.scopeIndex] /= m_size / 2; | ||||
184 | } | ||||
185 | | ||||
186 | // monotone cubic interpolation | ||||
187 | QVector<QPointF> data; | ||||
188 | for( uint k = 0; k < m_size / 2 + 1 && k <= m_interpolatedScopeBands.last().midK; k++ ) | ||||
189 | { | ||||
190 | data << QPointF( k, std::abs( m_out[k] ) * sqrt( k ) / m_size * 2 ); | ||||
191 | } | ||||
192 | // Get consecutive differences and slopes | ||||
193 | QVector<double> dys, dxs, ms; | ||||
194 | for( int i = 0; i < data.size() - 1; i++ ) | ||||
195 | { | ||||
196 | double dx = data[i + 1].x() - data[i].x(); | ||||
197 | double dy = data[i + 1].y() - data[i].y(); | ||||
198 | dxs << dx; | ||||
199 | dys << dy; | ||||
200 | ms << dy / dx; | ||||
201 | } | ||||
202 | // Get degree-1 coefficients | ||||
203 | QVector<double> c1s = QVector<double>() << ms[0]; | ||||
204 | for( int i = 0; i < dxs.size() - 1; i++) | ||||
205 | { | ||||
206 | double m = ms[i], mNext = ms[i + 1]; | ||||
207 | if( m * mNext <= 0 ) | ||||
208 | c1s << 0.0; | ||||
209 | else | ||||
210 | { | ||||
211 | double dx_ = dxs[i], dxNext = dxs[i + 1], common = dx_ + dxNext; | ||||
212 | c1s << ( 3 * common / ( ( common + dxNext ) / m + ( common + dx_ ) / mNext ) ); | ||||
213 | } | ||||
214 | } | ||||
215 | c1s << ms.last(); | ||||
216 | // Get degree-2 and degree-3 coefficients | ||||
217 | QVector<double> c2s, c3s; | ||||
218 | for( int i = 0; i < c1s.size() - 1; i++ ) | ||||
219 | { | ||||
220 | double c1 = c1s[i], m_ = ms[i], invDx = 1 / dxs[i], common_ = c1 + c1s[i + 1] - m_ - m_; | ||||
221 | c2s << ( m_ - c1 - common_ ) * invDx; | ||||
222 | c3s << common_ * invDx * invDx; | ||||
223 | } | ||||
224 | // write interpolated data to scope | ||||
225 | for( auto &band : m_interpolatedScopeBands ) | ||||
226 | { | ||||
227 | const double x = band.midK; | ||||
228 | auto &scope = m_currentScope[band.scopeIndex]; | ||||
229 | | ||||
230 | // Search for the interval x is in, returning the corresponding y if x is one of the original xs | ||||
231 | int low = 0, mid, high = c3s.size() - 1; | ||||
232 | while ( low <= high ) | ||||
233 | { | ||||
234 | mid = std::floor( 0.5 * ( low + high ) ); | ||||
235 | double xHere = data[mid].x(); | ||||
236 | if( xHere < x ) | ||||
237 | low = mid + 1; | ||||
238 | else if( xHere > x ) | ||||
239 | high = mid - 1; | ||||
240 | else | ||||
241 | scope = data[mid].y(); | ||||
242 | } | ||||
243 | int i = qMax( 0, high ); | ||||
244 | | ||||
245 | // Interpolate | ||||
246 | double diff = x - data[i].x(), diffSq = diff * diff; | ||||
247 | scope = qMax( 0.0, data[i].y() + c1s[i] * diff + c2s[i] * diffSq + c3s[i] * diff * diffSq ); | ||||
248 | } | ||||
249 | | ||||
250 | analyze(); | ||||
251 | } | ||||
252 | | ||||
253 | void Analyzer::Worker::setSampleSize( uint size ) | ||||
254 | { | ||||
255 | if( m_size == size ) | ||||
256 | return; | ||||
257 | | ||||
258 | m_size = size; | ||||
259 | | ||||
260 | fftw_destroy_plan( m_plan ); | ||||
261 | fftw_free( m_in ); | ||||
262 | fftw_free( m_out ); | ||||
263 | | ||||
264 | m_in = (double*) fftw_malloc( m_size * sizeof( double ) ); | ||||
265 | m_out = (std::complex<double>*) fftw_malloc( ( m_size / 2 + 1 ) * sizeof( std::complex<double> ) ); | ||||
266 | m_plan = fftw_plan_dft_r2c_1d( m_size, m_in, reinterpret_cast<fftw_complex*>( m_out ), FFTW_ESTIMATE ); | ||||
267 | } | ||||
268 | | ||||
269 | void Analyzer::Worker::setWindowFunction( Base::WindowFunction windowFunction ) | ||||
270 | { | ||||
271 | if( m_windowFunction == windowFunction ) | ||||
272 | return; | ||||
273 | | ||||
274 | m_windowFunction = windowFunction; | ||||
275 | } | ||||
276 | | ||||
277 | void Analyzer::Worker::setScopeSize( int size ) | ||||
278 | { | ||||
279 | m_currentScope.resize( size ); | ||||
280 | } | ||||
281 | | ||||
282 | void Analyzer::Worker::calculateExpFactor( qreal minFreq, qreal maxFreq, int sampleRate ) | ||||
283 | { | ||||
284 | DEBUG_BLOCK | ||||
285 | | ||||
286 | if( minFreq >= maxFreq ) | ||||
287 | { | ||||
288 | warning() << "Minimum frequency must be smaller than maximum frequency!"; | ||||
289 | return; | ||||
290 | } | ||||
291 | | ||||
292 | m_expFactor = pow( maxFreq / minFreq, 1.0 / m_currentScope.size() ); | ||||
293 | m_expectedDataTime = std::floor( (qreal)EngineController::DATAOUTPUT_DATA_SIZE * 1000.0 / sampleRate ); | ||||
294 | | ||||
295 | m_interpolatedScopeBands.clear(); | ||||
296 | m_notInterpolatedScopeBands.clear(); | ||||
297 | const uint outputSize = m_size / 2 + 1; | ||||
298 | | ||||
299 | for( int scopeIndex = 0; scopeIndex < m_currentScope.size(); scopeIndex++ ) | ||||
300 | { | ||||
301 | BandInfo newBandInfo; | ||||
302 | newBandInfo.lowerFreq = minFreq * pow( m_expFactor, double( scopeIndex ) - 0.5 ); | ||||
303 | newBandInfo.midFreq = minFreq * pow( m_expFactor, scopeIndex ); | ||||
304 | newBandInfo.upperFreq = minFreq * pow( m_expFactor, double( scopeIndex ) + 0.5 ); | ||||
305 | newBandInfo.lowerK = newBandInfo.lowerFreq / ( sampleRate / 2 ) * outputSize; | ||||
306 | newBandInfo.midK = newBandInfo.midFreq / ( sampleRate / 2 ) * outputSize; | ||||
307 | newBandInfo.upperK = newBandInfo.upperFreq / ( sampleRate / 2 ) * outputSize; | ||||
308 | newBandInfo.scopeIndex = scopeIndex; | ||||
309 | | ||||
310 | if ( std::floor( newBandInfo.upperK ) >= std::ceil( newBandInfo.lowerK ) ) | ||||
311 | m_notInterpolatedScopeBands << newBandInfo; | ||||
312 | else | ||||
313 | m_interpolatedScopeBands << newBandInfo; | ||||
314 | } | ||||
315 | } | ||||
316 | | ||||
317 | void Analyzer::Worker::demo() | ||||
318 | { | ||||
319 | if( m_demoT > 300 ) | ||||
320 | m_demoT = 1; //0 = wasted calculations | ||||
321 | | ||||
322 | if( m_demoT < 201 ) | ||||
323 | { | ||||
324 | const double dt = double( m_demoT ) / 200; | ||||
325 | for( int i = 0; i < m_currentScope.size(); ++i ) | ||||
326 | { | ||||
327 | m_currentScope[i] = dt * ( sin( M_PI + ( i * M_PI ) / m_currentScope.size() ) + 1.0 ); | ||||
328 | } | ||||
329 | } | ||||
330 | else | ||||
331 | { | ||||
332 | for( int i = 0; i < m_currentScope.size(); ++i ) | ||||
333 | { | ||||
334 | m_currentScope[i] = 0.0; | ||||
335 | } | ||||
336 | } | ||||
337 | | ||||
338 | ++m_demoT; | ||||
339 | | ||||
340 | int timeElapsed = m_lastUpdate.elapsed(); | ||||
341 | | ||||
342 | // Delay if interval is too low | ||||
343 | if( timeElapsed < Analyzer::Base::DEMO_INTERVAL - 1 ) | ||||
344 | QThread::currentThread()->msleep( Analyzer::Base::DEMO_INTERVAL - 1 - timeElapsed ); | ||||
345 | | ||||
346 | m_lastUpdate.restart(); | ||||
347 | | ||||
348 | analyze(); | ||||
349 | } | ||||
350 | | ||||
351 | void Analyzer::Worker::playbackStateChanged() | ||||
352 | { | ||||
353 | bool playing = EngineController::instance()->isPlaying(); | ||||
354 | playing ? m_demoTimer->stop() : m_demoTimer->start(); | ||||
355 | playing ? m_processTimer->start() : m_processTimer->stop(); | ||||
356 | resetDemo(); | ||||
357 | } |