]> git.sesse.net Git - kdenlive/blob - src/audioscopes/audiospectrum.cpp
044362a142d36cacf88fb639441c008f3902f7e9
[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 #include "audiospectrum.h"
12 #include "tools/kiss_fftr.h"
13
14 #include <QMenu>
15 #include <QPainter>
16 #include <QMouseEvent>
17
18 #include <iostream>
19 #include <fstream>
20
21 bool fileWritten = false;
22
23 const QString AudioSpectrum::directions[] =  {"North", "Northeast", "East", "Southeast"};
24
25 AudioSpectrum::AudioSpectrum(QWidget *parent) :
26         AbstractAudioScopeWidget(false, parent),
27         m_rescaleMinDist(12),
28         m_rescaleVerticalThreshold(2.0f),
29         m_rescaleActive(false),
30         m_rescalePropertiesLocked(false),
31         m_rescaleScale(1)
32 {
33     ui = new Ui::AudioSpectrum_UI;
34     ui->setupUi(this);
35
36     m_distance = QSize(65, 30);
37     m_dBmin = -120;
38     m_dBmax = 0;
39     m_freqMax = 10000;
40
41
42     m_aLin = new QAction(i18n("Linear scale"), this);
43     m_aLin->setCheckable(true);
44     m_aLog = new QAction(i18n("Logarithmic scale"), this);
45     m_aLog->setCheckable(true);
46
47     m_agScale = new QActionGroup(this);
48     m_agScale->addAction(m_aLin);
49     m_agScale->addAction(m_aLog);
50
51     m_aLockHz = new QAction(i18n("Lock maximum frequency"), this);
52     m_aLockHz->setCheckable(true);
53     m_aLockHz->setEnabled(false);
54
55
56 //    m_menu->addSeparator()->setText(i18n("Scale"));
57 //    m_menu->addAction(m_aLin);
58 //    m_menu->addAction(m_aLog);
59     m_menu->addSeparator();
60     m_menu->addAction(m_aLockHz);
61
62
63     ui->windowSize->addItem("256", QVariant(256));
64     ui->windowSize->addItem("512", QVariant(512));
65     ui->windowSize->addItem("1024", QVariant(1024));
66     ui->windowSize->addItem("2048", QVariant(2048));
67
68     m_cfg = kiss_fftr_alloc(ui->windowSize->itemData(ui->windowSize->currentIndex()).toInt(), 0,0,0);
69
70
71     bool b = true;
72     b &= connect(ui->windowSize, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateCfg()));
73     Q_ASSERT(b);
74
75     AbstractScopeWidget::init();
76 }
77 AudioSpectrum::~AudioSpectrum()
78 {
79     free(m_cfg);
80     delete m_agScale;
81     delete m_aLin;
82     delete m_aLog;
83     delete m_aLockHz;
84 }
85
86 void AudioSpectrum::readConfig()
87 {
88     AbstractScopeWidget::readConfig();
89
90     KSharedConfigPtr config = KGlobal::config();
91     KConfigGroup scopeConfig(config, AbstractScopeWidget::configName());
92     QString scale = scopeConfig.readEntry("scale");
93     if (scale == "lin") {
94         m_aLin->setChecked(true);
95     } else {
96         m_aLog->setChecked(true);
97     }
98     m_aLockHz->setChecked(scopeConfig.readEntry("lockHz", false));
99     ui->windowSize->setCurrentIndex(scopeConfig.readEntry("windowSize", 0));
100
101 }
102 void AudioSpectrum::writeConfig()
103 {
104     KSharedConfigPtr config = KGlobal::config();
105     KConfigGroup scopeConfig(config, AbstractScopeWidget::configName());
106     QString scale;
107     if (m_aLin->isChecked()) {
108         scale = "lin";
109     } else {
110         scale = "log";
111     }
112     scopeConfig.writeEntry("scale", scale);
113     scopeConfig.writeEntry("windowSize", ui->windowSize->currentIndex());
114     scopeConfig.writeEntry("lockHz", m_aLockHz->isChecked());
115     scopeConfig.sync();
116 }
117
118 QString AudioSpectrum::widgetName() const { return QString("audiospectrum"); }
119
120 bool AudioSpectrum::isBackgroundDependingOnInput() const { return false; }
121 bool AudioSpectrum::isScopeDependingOnInput() const { return true; }
122 bool AudioSpectrum::isHUDDependingOnInput() const { return false; }
123
124 QImage AudioSpectrum::renderBackground(uint) { return QImage(); }
125 QImage AudioSpectrum::renderAudioScope(uint, const QVector<int16_t> audioFrame, const int freq, const int num_channels, const int num_samples)
126 {
127     if (audioFrame.size() > 63) {
128         m_freqMax = freq / 2;
129
130         QTime start = QTime::currentTime();
131
132         bool customCfg = false;
133         kiss_fftr_cfg myCfg = m_cfg;
134         int fftWindow = ui->windowSize->itemData(ui->windowSize->currentIndex()).toInt();
135         if (fftWindow > num_samples) {
136             fftWindow = num_samples;
137             customCfg = true;
138         }
139         if ((fftWindow & 1) == 1) {
140             fftWindow--;
141             customCfg = true;
142         }
143         if (customCfg) {
144             myCfg = kiss_fftr_alloc(fftWindow, 0,0,0);
145         }
146
147         float data[fftWindow];
148         float freqSpectrum[fftWindow/2];
149
150         int16_t maxSig = 0;
151         for (int i = 0; i < fftWindow; i++) {
152             if (audioFrame.data()[i*num_channels] > maxSig) {
153                 maxSig = audioFrame.data()[i*num_channels];
154             }
155         }
156         qDebug() << "Max audio signal is " << maxSig;
157
158         // The resulting FFT vector is only half as long
159         kiss_fft_cpx freqData[fftWindow/2];
160
161         // Copy the first channel's audio into a vector for the FFT display
162         // (only one channel handled at the moment)
163         for (int i = 0; i < fftWindow; i++) {
164             // Normalize signals to [0,1] to get correct dB values later on
165             data[i] = (float) audioFrame.data()[i*num_channels] / 32767.0f;
166         }
167         // Calculate the Fast Fourier Transform for the input data
168         kiss_fftr(m_cfg, data, freqData);
169
170
171         float val;
172         // Get the minimum and the maximum value of the Fourier transformed (for scaling)
173         for (int i = 0; i < fftWindow/2; i++) {
174             if (m_aLog->isChecked()) {
175                 // Logarithmic scale: 20 * log ( 2 * magnitude / N )
176                 // with N = FFT size (after FFT, 1/2 window size)
177                 val = 20*log(pow(pow(fabs(freqData[i].r),2) + pow(fabs(freqData[i].i),2), .5)/((float)fftWindow/2.0f))/log(10);
178             } else {
179                 // sqrt(r² + i²)
180                 val = pow(pow(fabs(freqData[i].r),2) + pow(fabs(freqData[i].i),2), .5);
181             }
182             freqSpectrum[i] = val;
183         }
184
185
186
187         // Draw the spectrum
188         QImage spectrum(scopeRect().size(), QImage::Format_ARGB32);
189         spectrum.fill(qRgba(0,0,0,0));
190         uint w = scopeRect().size().width();
191         uint h = scopeRect().size().height();
192         float x;
193         for (uint i = 0; i < w; i++) {
194
195             x = i/((float) w) * fftWindow/2;
196
197             // Use linear interpolation in order to get smoother display
198             if (i == 0 || i == w-1) {
199                 val = freqSpectrum[i];
200             } else {
201                 // Use floor(x)+1 instead of ceil(x) as floor(x) == ceil(x) is possible.
202                 val = (floor(x)+1 - x)*freqSpectrum[(int) floor(x)] + (x-floor(x))*freqSpectrum[(int) floor(x)+1];
203             }
204
205             // freqSpectrum values range from 0 to -inf as they are relative dB values.
206 //            qDebug() << val << "/" <<  (1 - (val - m_dBmax)/(m_dBmin-m_dBmax));
207             for (uint y = 0; y < h*(1 - (val - m_dBmax)/(m_dBmin-m_dBmax)) && y < h; y++) {
208                 spectrum.setPixel(i, h-y-1, qRgba(225, 182, 255, 255));
209             }
210         }
211
212         emit signalScopeRenderingFinished(start.elapsed(), 1);
213
214         /*
215         if (!fileWritten || true) {
216             std::ofstream mFile;
217             mFile.open("/tmp/freq.m");
218             if (!mFile) {
219                 qDebug() << "Opening file failed.";
220             } else {
221                 mFile << "val = [ ";
222
223                 for (int sample = 0; sample < 256; sample++) {
224                     mFile << data[sample] << " ";
225                 }
226                 mFile << " ];\n";
227
228                 mFile << "freq = [ ";
229                 for (int sample = 0; sample < 256; sample++) {
230                     mFile << freqData[sample].r << "+" << freqData[sample].i << "*i ";
231                 }
232                 mFile << " ];\n";
233
234                 mFile.close();
235                 fileWritten = true;
236                 qDebug() << "File written.";
237             }
238         } else {
239             qDebug() << "File already written.";
240         }
241         //*/
242
243         if (customCfg) {
244             free(myCfg);
245         }
246
247         return spectrum;
248     } else {
249         emit signalScopeRenderingFinished(0, 1);
250         return QImage();
251     }
252 }
253 QImage AudioSpectrum::renderHUD(uint)
254 {
255     QTime start = QTime::currentTime();
256
257     const QRect rect = scopeRect();
258     // Minimum distance between two lines
259     const uint minDistY = 30;
260     const uint minDistX = 40;
261     const uint textDist = 5;
262     const uint dbDiff = ceil((float)minDistY/rect.height() * (m_dBmax-m_dBmin));
263
264     QImage hud(AbstractAudioScopeWidget::rect().size(), QImage::Format_ARGB32);
265     hud.fill(qRgba(0,0,0,0));
266
267     QPainter davinci(&hud);
268     davinci.setPen(AbstractAudioScopeWidget::penLight);
269
270     int y;
271     for (int db = -dbDiff; db > m_dBmin; db -= dbDiff) {
272         y = rect.height() * ((float)db)/(m_dBmin - m_dBmax);
273         davinci.drawLine(0, y, rect.width()-1, y);
274         davinci.drawText(rect.width() + textDist, y + 8, i18n("%1 dB", m_dBmax + db));
275     }
276
277
278     const uint hzDiff = ceil( ((float)minDistX)/rect.width() * m_freqMax / 1000 ) * 1000;
279     int x;
280     for (uint hz = hzDiff; hz < m_freqMax; hz += hzDiff) {
281         x = rect.width() * ((float)hz)/m_freqMax;
282         davinci.drawLine(x, 0, x, rect.height()+4);
283         davinci.drawText(x-4, rect.height() + 20, QVariant(hz/1000).toString());
284     }
285     davinci.drawText(rect.width(), rect.height() + 20, "[kHz]");
286
287
288     emit signalHUDRenderingFinished(start.elapsed(), 1);
289     return hud;
290 }
291
292 QRect AudioSpectrum::scopeRect() {
293     return QRect(QPoint(0, 0), AbstractAudioScopeWidget::rect().size() - m_distance);
294 }
295
296
297 void AudioSpectrum::slotUpdateCfg()
298 {
299     free(m_cfg);
300     m_cfg = kiss_fftr_alloc(ui->windowSize->itemData(ui->windowSize->currentIndex()).toInt(), 0,0,0);
301 }
302
303
304 ///// EVENTS /////
305
306 void AudioSpectrum::mouseMoveEvent(QMouseEvent *event)
307 {
308 //    qDebug() << "Mouse moved; Modifier: " << event->modifiers() << ", xy: " << event->x() << "/" << event->y()
309 //            << ", Buttons: " << event->buttons();
310
311     QPoint movement = event->pos()-m_rescaleStartPoint;
312
313     if (m_rescaleActive) {
314         if (m_rescalePropertiesLocked) {
315             // Direction is known, now adjust parameters
316
317             // Reset the starting point to make the next moveEvent relative to the current one
318             m_rescaleStartPoint = event->pos();
319
320
321             if (!m_rescaleFirstRescaleDone) {
322                 // We have just learned the desired direction; Normalize the movement to one pixel
323                 // to avoid a jump by m_rescaleMinDist
324
325                 if (movement.x() != 0) {
326                     movement.setX(movement.x() / abs(movement.x()));
327                 }
328                 if (movement.y() != 0) {
329                     movement.setY(movement.y() / abs(movement.y()));
330                 }
331
332                 m_rescaleFirstRescaleDone = true;
333             }
334
335             if (m_rescaleClockDirection == AudioSpectrum::North) {
336                 // Nort-South direction: Adjust the dB scale
337
338                 if ((m_rescaleModifiers & Qt::ShiftModifier) == 0) {
339
340                     // By default adjust the min dB value
341                     m_dBmin += movement.y();
342
343                 } else {
344
345                     // Adjust max dB value if Shift is pressed.
346                     m_dBmax += movement.y();
347
348                 }
349
350                 // Ensure the dB values lie in [-100, 0]
351                 // 0 is the upper bound, everything below -70 dB is most likely noise
352                 if (m_dBmax > 0) {
353                     m_dBmax = 0;
354                 }
355                 if (m_dBmin < -100) {
356                     m_dBmin = -100;
357                 }
358                 // Ensure there is at least 6 dB between the minimum and the maximum value;
359                 // lower values hardly make sense
360                 if (m_dBmax - m_dBmin < 6) {
361                     if ((m_rescaleModifiers & Qt::ShiftModifier) == 0) {
362                         // min was adjusted; Try to adjust the max value to maintain the
363                         // minimum dB difference of 6 dB
364                         m_dBmax = m_dBmin + 6;
365                         if (m_dBmax > 0) {
366                             m_dBmax = 0;
367                             m_dBmin = -6;
368                         }
369                     } else {
370                         // max was adjusted, adjust min
371                         m_dBmin = m_dBmax - 6;
372                         if (m_dBmin < -100) {
373                             m_dBmin = -100;
374                             m_dBmax = -100+6;
375                         }
376                     }
377                 }
378
379                 forceUpdateHUD();
380                 forceUpdateScope();
381
382             }
383
384
385         } else {
386             // Detect the movement direction here.
387             // This algorithm relies on the aspect ratio of dy/dx (size and signum).
388             if (movement.manhattanLength() > m_rescaleMinDist) {
389                 float diff = ((float) movement.y())/movement.x();
390
391                 if (abs(diff) > m_rescaleVerticalThreshold || movement.x() == 0) {
392                     m_rescaleClockDirection = AudioSpectrum::North;
393                 } else if (abs(diff) < 1/m_rescaleVerticalThreshold) {
394                     m_rescaleClockDirection = AudioSpectrum::East;
395                 } else if (diff < 0) {
396                     m_rescaleClockDirection = AudioSpectrum::Northeast;
397                 } else {
398                     m_rescaleClockDirection = AudioSpectrum::Southeast;
399                 }
400 //                qDebug() << "Diff is " << diff << "; chose " << directions[m_rescaleClockDirection] << " as direction";
401                 m_rescalePropertiesLocked = true;
402             }
403         }
404     } else {
405         AbstractAudioScopeWidget::mouseMoveEvent(event);
406     }
407 }
408
409 void AudioSpectrum::mousePressEvent(QMouseEvent *event)
410 {
411     if (event->button() == Qt::LeftButton) {
412         // Rescaling mode starts
413         m_rescaleActive = true;
414         m_rescalePropertiesLocked = false;
415         m_rescaleFirstRescaleDone = false;
416         m_rescaleStartPoint = event->pos();
417         m_rescaleModifiers = event->modifiers();
418
419     } else {
420         AbstractAudioScopeWidget::mousePressEvent(event);
421     }
422 }
423
424 void AudioSpectrum::mouseReleaseEvent(QMouseEvent *event)
425 {
426     m_rescaleActive = false;
427     m_rescalePropertiesLocked = false;
428
429     AbstractAudioScopeWidget::mouseReleaseEvent(event);
430 }