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