]> git.sesse.net Git - kdenlive/blob - src/audioscopes/audiospectrum.cpp
Audio Spectrum: Moved FFT calculation to FFTTools for re-use
[kdenlive] / src / audioscopes / audiospectrum.cpp
1 /***************************************************************************
2  *   Copyright (C) 2010 by Simon Andreas Eugster (simon.eu@gmail.com)      *
3  *   This file is part of kdenlive. See www.kdenlive.org.                  *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  ***************************************************************************/
10
11
12
13 #include "audiospectrum.h"
14 #include "ffttools.h"
15 #include "tools/kiss_fftr.h"
16
17 #include <QMenu>
18 #include <QPainter>
19 #include <QMouseEvent>
20
21 #include <iostream>
22
23 // Enables debugging, like writing a GNU Octave .m file to /tmp
24 //#define DEBUG_AUDIOSPEC
25 #ifdef DEBUG_AUDIOSPEC
26 #include <fstream>
27 #include <QDebug>
28 bool fileWritten = false;
29 #endif
30
31 #define MIN_DB_VALUE -120
32 #define MAX_FREQ_VALUE 96000
33 #define MIN_FREQ_VALUE 1000
34
35 const QString AudioSpectrum::directions[] =  {"North", "Northeast", "East", "Southeast"};
36
37 AudioSpectrum::AudioSpectrum(QWidget *parent) :
38         AbstractAudioScopeWidget(false, parent),
39         m_fftTools(),
40         m_freqMax(10000),
41         m_customFreq(false),
42         m_rescaleMinDist(8),
43         m_rescaleVerticalThreshold(2.0f),
44         m_rescaleActive(false),
45         m_rescalePropertiesLocked(false),
46         m_rescaleScale(1)
47 {
48     ui = new Ui::AudioSpectrum_UI;
49     ui->setupUi(this);
50
51
52     m_aResetHz = new QAction(i18n("Reset maximum frequency to sampling rate"), this);
53
54
55     m_menu->addSeparator();
56     m_menu->addAction(m_aResetHz);
57     m_menu->removeAction(m_aRealtime);
58
59
60     ui->windowSize->addItem("256", QVariant(256));
61     ui->windowSize->addItem("512", QVariant(512));
62     ui->windowSize->addItem("1024", QVariant(1024));
63     ui->windowSize->addItem("2048", QVariant(2048));
64
65     ui->windowFunction->addItem(i18n("Rectangular window"), FFTTools::Window_Rect);
66     ui->windowFunction->addItem(i18n("Triangular window"), FFTTools::Window_Triangle);
67     ui->windowFunction->addItem(i18n("Hamming window"), FFTTools::Window_Hamming);
68
69
70     bool b = true;
71     b &= connect(m_aResetHz, SIGNAL(triggered()), this, SLOT(slotResetMaxFreq()));
72     b &= connect(ui->windowFunction, SIGNAL(currentIndexChanged(int)), this, SLOT(forceUpdate()));
73     Q_ASSERT(b);
74
75
76     ui->labelFFTSize->setToolTip(i18n("The maximum window size is limited by the number of samples per frame."));
77     ui->windowSize->setToolTip(i18n("A bigger window improves the accuracy at the cost of computational power."));
78     ui->windowFunction->setToolTip(i18n("The rectangular window function is good for signals with equal signal strength (narrow peak), but creates more smearing. See Window function on Wikipedia."));
79
80     AbstractScopeWidget::init();
81 }
82 AudioSpectrum::~AudioSpectrum()
83 {
84     writeConfig();
85
86     delete m_aResetHz;
87 }
88
89 void AudioSpectrum::readConfig()
90 {
91     AbstractScopeWidget::readConfig();
92
93     KSharedConfigPtr config = KGlobal::config();
94     KConfigGroup scopeConfig(config, AbstractScopeWidget::configName());
95
96     ui->windowSize->setCurrentIndex(scopeConfig.readEntry("windowSize", 0));
97     ui->windowFunction->setCurrentIndex(scopeConfig.readEntry("windowFunction", 0));
98     m_dBmax = scopeConfig.readEntry("dBmax", 0);
99     m_dBmin = scopeConfig.readEntry("dBmin", -70);
100     m_freqMax = scopeConfig.readEntry("freqMax", 0);
101
102     if (m_freqMax == 0) {
103         m_customFreq = false;
104         m_freqMax = 10000;
105     } else {
106         m_customFreq = true;
107     }
108 }
109 void AudioSpectrum::writeConfig()
110 {
111     KSharedConfigPtr config = KGlobal::config();
112     KConfigGroup scopeConfig(config, AbstractScopeWidget::configName());
113
114     scopeConfig.writeEntry("windowSize", ui->windowSize->currentIndex());
115     scopeConfig.writeEntry("windowFunction", ui->windowFunction->currentIndex());
116     scopeConfig.writeEntry("dBmax", m_dBmax);
117     scopeConfig.writeEntry("dBmin", m_dBmin);
118     if (m_customFreq) {
119         scopeConfig.writeEntry("freqMax", m_freqMax);
120     } else {
121         scopeConfig.writeEntry("freqMax", 0);
122     }
123
124     scopeConfig.sync();
125 }
126
127 QString AudioSpectrum::widgetName() const { return QString("AudioSpectrum"); }
128 bool AudioSpectrum::isBackgroundDependingOnInput() const { return false; }
129 bool AudioSpectrum::isScopeDependingOnInput() const { return true; }
130 bool AudioSpectrum::isHUDDependingOnInput() const { return false; }
131
132 QImage AudioSpectrum::renderBackground(uint) { return QImage(); }
133
134 QImage AudioSpectrum::renderAudioScope(uint, const QVector<int16_t> audioFrame, const int freq, const int num_channels, const int num_samples)
135 {
136     if (audioFrame.size() > 63) {
137         if (!m_customFreq) {
138             m_freqMax = freq / 2;
139         }
140
141         QTime start = QTime::currentTime();
142
143
144         // Determine the window size to use. It should be
145         // * not bigger than the number of samples actually available
146         // * divisible by 2
147         int fftWindow = ui->windowSize->itemData(ui->windowSize->currentIndex()).toInt();
148         if (fftWindow > num_samples) {
149             fftWindow = num_samples;
150         }
151         if ((fftWindow & 1) == 1) {
152             fftWindow--;
153         }
154
155         // Show the window size used, for information
156         ui->labelFFTSizeNumber->setText(QVariant(fftWindow).toString());
157
158
159         // Get the spectral power distribution of the input samples,
160         // using the given window size and function
161         float freqSpectrum[fftWindow/2];
162         FFTTools::WindowType windowType = (FFTTools::WindowType) ui->windowFunction->itemData(ui->windowFunction->currentIndex()).toInt();
163         m_fftTools.fftNormalized(audioFrame, 0, num_channels, freqSpectrum, windowType, fftWindow, 0);
164
165
166         // Draw the spectrum
167         QImage spectrum(m_scopeRect.size(), QImage::Format_ARGB32);
168         spectrum.fill(qRgba(0,0,0,0));
169         const uint w = m_innerScopeRect.width();
170         const uint h = m_innerScopeRect.height();
171         const uint leftDist = m_innerScopeRect.left() - m_scopeRect.left();
172         const uint topDist = m_innerScopeRect.top() - m_scopeRect.top();
173         float f;
174         float x;
175         float x_prev = 0;
176         float val;
177         int xi;
178         for (uint i = 0; i < w; i++) {
179
180             // i:  Pixel coordinate
181             // f: Target frequency
182             // x:  Frequency array index (float!) corresponding to the pixel
183             // xi: floor(x)
184
185             f = i/((float) w-1.0) * m_freqMax;
186             x = 2*f/freq * (fftWindow/2 - 1);
187             xi = (int) floor(x);
188
189             if (x >= fftWindow/2) {
190                 break;
191             }
192
193             // Use linear interpolation in order to get smoother display
194             if (i == 0 || xi == fftWindow/2-1) {
195                 // ... except if we are at the left or right border of the display or the spectrum
196                 val = freqSpectrum[xi];
197             } else {
198
199                 if (freqSpectrum[xi] > freqSpectrum[xi+1]
200                     && x_prev < xi) {
201                     // This is a hack to preserve peaks.
202                     // Consider f = {0, 100, 0}
203                     //          x = {0.5,  1.5}
204                     // Then x is 50 both times, and the 100 peak is lost.
205                     // Get it back here for the first x after the peak.
206                     val = freqSpectrum[xi];
207                 } else {
208                     val =   (xi+1 - x) * freqSpectrum[xi]
209                           + (x - xi)   * freqSpectrum[xi+1];
210                 }
211             }
212
213             // freqSpectrum values range from 0 to -inf as they are relative dB values.
214             for (uint y = 0; y < h*(1 - (val - m_dBmax)/(m_dBmin-m_dBmax)) && y < h; y++) {
215                 spectrum.setPixel(leftDist + i, topDist + h-y-1, qRgba(225, 182, 255, 255));
216             }
217
218             x_prev = x;
219         }
220
221         emit signalScopeRenderingFinished(start.elapsed(), 1);
222
223 #ifdef DEBUG_AUDIOSPEC
224         if (!fileWritten || true) {
225             std::ofstream mFile;
226             mFile.open("/tmp/freq.m");
227             if (!mFile) {
228                 qDebug() << "Opening file failed.";
229             } else {
230                 mFile << "val = [ ";
231
232                 for (int sample = 0; sample < 256; sample++) {
233                     mFile << data[sample] << " ";
234                 }
235                 mFile << " ];\n";
236
237                 mFile << "freq = [ ";
238                 for (int sample = 0; sample < 256; sample++) {
239                     mFile << freqData[sample].r << "+" << freqData[sample].i << "*i ";
240                 }
241                 mFile << " ];\n";
242
243                 mFile.close();
244                 fileWritten = true;
245                 qDebug() << "File written.";
246             }
247         } else {
248             qDebug() << "File already written.";
249         }
250 #endif
251
252         return spectrum;
253     } else {
254         emit signalScopeRenderingFinished(0, 1);
255         return QImage();
256     }
257 }
258 QImage AudioSpectrum::renderHUD(uint)
259 {
260     QTime start = QTime::currentTime();
261
262     // Minimum distance between two lines
263     const uint minDistY = 30;
264     const uint minDistX = 40;
265     const uint textDistX = 10;
266     const uint textDistY = 25;
267     const uint topDist = m_innerScopeRect.top() - m_scopeRect.top();
268     const uint leftDist = m_innerScopeRect.left() - m_scopeRect.left();
269     const uint dbDiff = ceil((float)minDistY/m_innerScopeRect.height() * (m_dBmax-m_dBmin));
270
271     QImage hud(m_scopeRect.size(), QImage::Format_ARGB32);
272     hud.fill(qRgba(0,0,0,0));
273
274     QPainter davinci(&hud);
275     davinci.setPen(AbstractScopeWidget::penLight);
276
277     int y;
278     for (int db = -dbDiff; db > m_dBmin; db -= dbDiff) {
279         y = topDist + m_innerScopeRect.height() * ((float)db)/(m_dBmin - m_dBmax);
280         if (y-topDist > m_innerScopeRect.height()-minDistY+10) {
281             // Abort here, there is still a line left for min dB to paint which needs some room.
282             break;
283         }
284         davinci.drawLine(leftDist, y, leftDist + m_innerScopeRect.width()-1, y);
285         davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, y + 6, i18n("%1 dB", m_dBmax + db));
286     }
287     davinci.drawLine(leftDist, topDist, leftDist + m_innerScopeRect.width()-1, topDist);
288     davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, topDist+6, i18n("%1 dB", m_dBmax));
289     davinci.drawLine(leftDist, topDist+m_innerScopeRect.height()-1, leftDist + m_innerScopeRect.width()-1, topDist+m_innerScopeRect.height()-1);
290     davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, topDist+m_innerScopeRect.height()+6, i18n("%1 dB", m_dBmin));
291
292     const uint hzDiff = ceil( ((float)minDistX)/m_innerScopeRect.width() * m_freqMax / 1000 ) * 1000;
293     int x = 0;
294     const int rightBorder = leftDist + m_innerScopeRect.width()-1;
295     y = topDist + m_innerScopeRect.height() + textDistY;
296     for (uint hz = 0; x <= rightBorder; hz += hzDiff) {
297         davinci.setPen(AbstractScopeWidget::penLight);
298         x = leftDist + m_innerScopeRect.width() * ((float)hz)/m_freqMax;
299
300         if (x <= rightBorder) {
301             davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height()+6);
302         }
303         if (hz < m_freqMax && x+textDistY < leftDist + m_innerScopeRect.width()) {
304             davinci.drawText(x-4, y, QVariant(hz/1000).toString());
305         } else {
306             x = leftDist + m_innerScopeRect.width();
307             davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height()+6);
308             davinci.drawText(x-10, y, i18n("%1 kHz").arg((double)m_freqMax/1000, 0, 'f', 1));
309         }
310
311         if (hz > 0) {
312             // Draw finer lines between the main lines
313             davinci.setPen(AbstractScopeWidget::penLightDots);
314             for (uint dHz = 3; dHz > 0; dHz--) {
315                 x = leftDist + m_innerScopeRect.width() * ((float)hz - dHz * hzDiff/4.0f)/m_freqMax;
316                 if (x > rightBorder) {
317                     break;
318                 }
319                 davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height()-1);
320             }
321         }
322     }
323
324
325     emit signalHUDRenderingFinished(start.elapsed(), 1);
326     return hud;
327 }
328
329 QRect AudioSpectrum::scopeRect() {
330     m_scopeRect = QRect(
331             QPoint(
332                     10,                                     // Left
333                     ui->verticalSpacer->geometry().top()+6  // Top
334             ),
335             AbstractAudioScopeWidget::rect().bottomRight()
336     );
337     m_innerScopeRect = QRect(
338             QPoint(
339                     m_scopeRect.left()+6,                   // Left
340                     m_scopeRect.top()+6                     // Top
341             ), QPoint(
342                     ui->verticalSpacer->geometry().right()-70,
343                     ui->verticalSpacer->geometry().bottom()-40
344             )
345     );
346     return m_scopeRect;
347 }
348
349 void AudioSpectrum::slotResetMaxFreq()
350 {
351     m_customFreq = false;
352     forceUpdateHUD();
353     forceUpdateScope();
354 }
355
356
357 ///// EVENTS /////
358
359 void AudioSpectrum::mouseMoveEvent(QMouseEvent *event)
360 {
361     QPoint movement = event->pos()-m_rescaleStartPoint;
362
363     if (m_rescaleActive) {
364         if (m_rescalePropertiesLocked) {
365             // Direction is known, now adjust parameters
366
367             // Reset the starting point to make the next moveEvent relative to the current one
368             m_rescaleStartPoint = event->pos();
369
370
371             if (!m_rescaleFirstRescaleDone) {
372                 // We have just learned the desired direction; Normalize the movement to one pixel
373                 // to avoid a jump by m_rescaleMinDist
374
375                 if (movement.x() != 0) {
376                     movement.setX(movement.x() / abs(movement.x()));
377                 }
378                 if (movement.y() != 0) {
379                     movement.setY(movement.y() / abs(movement.y()));
380                 }
381
382                 m_rescaleFirstRescaleDone = true;
383             }
384
385             if (m_rescaleClockDirection == AudioSpectrum::North) {
386                 // Nort-South direction: Adjust the dB scale
387
388                 if ((m_rescaleModifiers & Qt::ShiftModifier) == 0) {
389
390                     // By default adjust the min dB value
391                     m_dBmin += movement.y();
392
393                 } else {
394
395                     // Adjust max dB value if Shift is pressed.
396                     m_dBmax += movement.y();
397
398                 }
399
400                 // Ensure the dB values lie in [-100, 0] (or rather [MIN_DB_VALUE, 0])
401                 // 0 is the upper bound, everything below -70 dB is most likely noise
402                 if (m_dBmax > 0) {
403                     m_dBmax = 0;
404                 }
405                 if (m_dBmin < MIN_DB_VALUE) {
406                     m_dBmin = MIN_DB_VALUE;
407                 }
408                 // Ensure there is at least 6 dB between the minimum and the maximum value;
409                 // lower values hardly make sense
410                 if (m_dBmax - m_dBmin < 6) {
411                     if ((m_rescaleModifiers & Qt::ShiftModifier) == 0) {
412                         // min was adjusted; Try to adjust the max value to maintain the
413                         // minimum dB difference of 6 dB
414                         m_dBmax = m_dBmin + 6;
415                         if (m_dBmax > 0) {
416                             m_dBmax = 0;
417                             m_dBmin = -6;
418                         }
419                     } else {
420                         // max was adjusted, adjust min
421                         m_dBmin = m_dBmax - 6;
422                         if (m_dBmin < MIN_DB_VALUE) {
423                             m_dBmin = MIN_DB_VALUE;
424                             m_dBmax = MIN_DB_VALUE+6;
425                         }
426                     }
427                 }
428
429                 forceUpdateHUD();
430                 forceUpdateScope();
431
432             } else if (m_rescaleClockDirection == AudioSpectrum::East) {
433                 // East-West direction: Adjust the maximum frequency
434                 m_freqMax -= 100*movement.x();
435                 if (m_freqMax < MIN_FREQ_VALUE) {
436                     m_freqMax = MIN_FREQ_VALUE;
437                 }
438                 if (m_freqMax > MAX_FREQ_VALUE) {
439                     m_freqMax = MAX_FREQ_VALUE;
440                 }
441                 m_customFreq = true;
442
443                 forceUpdateHUD();
444                 forceUpdateScope();
445             }
446
447
448         } else {
449             // Detect the movement direction here.
450             // This algorithm relies on the aspect ratio of dy/dx (size and signum).
451             if (movement.manhattanLength() > m_rescaleMinDist) {
452                 float diff = ((float) movement.y())/movement.x();
453
454                 if (abs(diff) > m_rescaleVerticalThreshold || movement.x() == 0) {
455                     m_rescaleClockDirection = AudioSpectrum::North;
456                 } else if (abs(diff) < 1/m_rescaleVerticalThreshold) {
457                     m_rescaleClockDirection = AudioSpectrum::East;
458                 } else if (diff < 0) {
459                     m_rescaleClockDirection = AudioSpectrum::Northeast;
460                 } else {
461                     m_rescaleClockDirection = AudioSpectrum::Southeast;
462                 }
463 #ifdef DEBUG_AUDIOSPEC
464                 qDebug() << "Diff is " << diff << "; chose " << directions[m_rescaleClockDirection] << " as direction";
465 #endif
466                 m_rescalePropertiesLocked = true;
467             }
468         }
469     } else {
470         AbstractAudioScopeWidget::mouseMoveEvent(event);
471     }
472 }
473
474 void AudioSpectrum::mousePressEvent(QMouseEvent *event)
475 {
476     if (event->button() == Qt::LeftButton) {
477         // Rescaling mode starts
478         m_rescaleActive = true;
479         m_rescalePropertiesLocked = false;
480         m_rescaleFirstRescaleDone = false;
481         m_rescaleStartPoint = event->pos();
482         m_rescaleModifiers = event->modifiers();
483
484     } else {
485         AbstractAudioScopeWidget::mousePressEvent(event);
486     }
487 }
488
489 void AudioSpectrum::mouseReleaseEvent(QMouseEvent *event)
490 {
491     m_rescaleActive = false;
492     m_rescalePropertiesLocked = false;
493
494     AbstractAudioScopeWidget::mouseReleaseEvent(event);
495 }
496
497 const QString AudioSpectrum::cfgSignature(const int size)
498 {
499     return QString("s%1").arg(size);
500 }
501
502
503 #ifdef DEBUG_AUDIOSPEC
504 #undef DEBUG_AUDIOSPEC
505 #endif
506
507 #undef MIN_DB_VALUE
508 #undef MAX_FREQ_VALUE
509 #undef MIN_FREQ_VALUE