]> git.sesse.net Git - kdenlive/blob - src/audioscopes/spectrogram.cpp
b3eb46afd9d708d18abd656a0596d93773b15764
[kdenlive] / src / audioscopes / spectrogram.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 <QPainter>
12
13 #include "spectrogram.h"
14
15 // Defines the number of FFT samples to store.
16 // Around 4 kB for a window size of 2000. Should be at least as large as the
17 // highest vertical screen resolution available for complete reconstruction.
18 // Can be less as a pre-rendered image is kept in space.
19 #define SPECTROGRAM_HISTORY_SIZE 1000
20
21 //#define DEBUG_SPECTROGRAM
22 #ifdef DEBUG_SPECTROGRAM
23 #include <QDebug>
24 #endif
25
26 Spectrogram::Spectrogram(QWidget *parent) :
27         AbstractAudioScopeWidget(false, parent),
28         m_fftTools(),
29         m_fftHistory(),
30         m_fftHistoryImg()
31 {
32     ui = new Ui::Spectrogram_UI;
33     ui->setupUi(this);
34
35
36
37
38     ui->windowSize->addItem("256", QVariant(256));
39     ui->windowSize->addItem("512", QVariant(512));
40     ui->windowSize->addItem("1024", QVariant(1024));
41     ui->windowSize->addItem("2048", QVariant(2048));
42
43     ui->windowFunction->addItem(i18n("Rectangular window"), FFTTools::Window_Rect);
44     ui->windowFunction->addItem(i18n("Triangular window"), FFTTools::Window_Triangle);
45     ui->windowFunction->addItem(i18n("Hamming window"), FFTTools::Window_Hamming);
46
47     bool b = true;
48     b &= connect(ui->windowFunction, SIGNAL(currentIndexChanged(int)), this, SLOT(forceUpdate()));
49     Q_ASSERT(b);
50
51     AbstractScopeWidget::init();
52 }
53
54 Spectrogram::~Spectrogram()
55 {
56     writeConfig();
57 }
58
59 void Spectrogram::readConfig()
60 {
61     AbstractScopeWidget::readConfig();
62
63     KSharedConfigPtr config = KGlobal::config();
64     KConfigGroup scopeConfig(config, AbstractScopeWidget::configName());
65
66     ui->windowSize->setCurrentIndex(scopeConfig.readEntry("windowSize", 0));
67     ui->windowFunction->setCurrentIndex(scopeConfig.readEntry("windowFunction", 0));
68     m_dBmax = scopeConfig.readEntry("dBmax", 0);
69     m_dBmin = scopeConfig.readEntry("dBmin", -70);
70     m_freqMax = scopeConfig.readEntry("freqMax", 0);
71
72     if (m_freqMax == 0) {
73         m_customFreq = false;
74         m_freqMax = 10000;
75     } else {
76         m_customFreq = true;
77     }
78 }
79 void Spectrogram::writeConfig()
80 {
81     KSharedConfigPtr config = KGlobal::config();
82     KConfigGroup scopeConfig(config, AbstractScopeWidget::configName());
83
84     scopeConfig.writeEntry("windowSize", ui->windowSize->currentIndex());
85     scopeConfig.writeEntry("windowFunction", ui->windowFunction->currentIndex());
86     scopeConfig.writeEntry("dBmax", m_dBmax);
87     scopeConfig.writeEntry("dBmin", m_dBmin);
88
89     if (m_customFreq) {
90         scopeConfig.writeEntry("freqMax", m_freqMax);
91     } else {
92         scopeConfig.writeEntry("freqMax", 0);
93     }
94
95     scopeConfig.sync();
96 }
97
98 QString Spectrogram::widgetName() const { return QString("Spectrogram"); }
99
100 QRect Spectrogram::scopeRect()
101 {
102     m_scopeRect = QRect(
103             QPoint(
104                     10,                                     // Left
105                     ui->verticalSpacer->geometry().top()+6  // Top
106             ),
107             AbstractAudioScopeWidget::rect().bottomRight()
108     );
109     m_innerScopeRect = QRect(
110             QPoint(
111                     m_scopeRect.left()+6,                   // Left
112                     m_scopeRect.top()+6                     // Top
113             ), QPoint(
114                     ui->verticalSpacer->geometry().right()-70,
115                     ui->verticalSpacer->geometry().bottom()-40
116             )
117     );
118     return m_scopeRect;
119 }
120
121 QImage Spectrogram::renderHUD(uint) { return QImage(); }
122 QImage Spectrogram::renderAudioScope(uint, const QVector<int16_t> audioFrame, const int freq,
123                                      const int num_channels, const int num_samples) {
124     if (audioFrame.size() > 63) {
125         if (!m_customFreq) {
126             m_freqMax = freq / 2;
127         }
128
129         QTime start = QTime::currentTime();
130
131         int fftWindow = ui->windowSize->itemData(ui->windowSize->currentIndex()).toInt();
132         if (fftWindow > num_samples) {
133             fftWindow = num_samples;
134         }
135         if ((fftWindow & 1) == 1) {
136             fftWindow--;
137         }
138
139         // Show the window size used, for information
140         ui->labelFFTSizeNumber->setText(QVariant(fftWindow).toString());
141
142
143         // Get the spectral power distribution of the input samples,
144         // using the given window size and function
145         float freqSpectrum[fftWindow/2];
146         FFTTools::WindowType windowType = (FFTTools::WindowType) ui->windowFunction->itemData(ui->windowFunction->currentIndex()).toInt();
147         m_fftTools.fftNormalized(audioFrame, 0, num_channels, freqSpectrum, windowType, fftWindow, 0);
148
149         QVector<float> spectrumVector(fftWindow/2);
150         memcpy(spectrumVector.data(), &freqSpectrum[0], fftWindow/2 * sizeof(float));
151         m_fftHistory.prepend(spectrumVector);
152
153         // Limit the maximum history size to avoid wasting space
154         while (m_fftHistory.size() > SPECTROGRAM_HISTORY_SIZE) {
155             m_fftHistory.removeLast();
156         }
157
158         // Draw the spectrum
159         QImage spectrum(m_scopeRect.size(), QImage::Format_ARGB32);
160         spectrum.fill(qRgba(0,0,0,0));
161         QPainter davinci(&spectrum);
162         const uint w = m_innerScopeRect.width();
163         const uint h = m_innerScopeRect.height();
164         const uint leftDist = m_innerScopeRect.left() - m_scopeRect.left();
165         const uint topDist = m_innerScopeRect.top() - m_scopeRect.top();
166         float f;
167         float x;
168         float x_prev = 0;
169         float val;
170         uint windowSize;
171         uint xi;
172         uint y = topDist;
173         bool completeRedraw = true;
174
175         if (m_fftHistoryImg.size() == m_scopeRect.size()) {
176             // The size of the widget has not changed since last time, so we can re-use it,
177             // shift it by one pixel, and render the single remaining line. Usually about
178             // 10 times faster for a widget height of around 400 px.
179             davinci.drawImage(0, -1, m_fftHistoryImg);
180             completeRedraw = false;
181         }
182
183         for (QList<QVector<float> >::iterator it = m_fftHistory.begin(); it != m_fftHistory.end(); it++) {
184
185             windowSize = (*it).size();
186
187             for (uint i = 0; i < w; i++) {
188
189                 // i:   Pixel coordinate
190                 // f:   Target frequency
191                 // x:   Frequency array index (float!) corresponding to the pixel
192                 // xi:  floor(x)
193                 // val: dB value at position x (Range: [-inf,0])
194
195                 f = i/((float) w-1.0) * m_freqMax;
196                 x = 2*f/freq * (windowSize - 1);
197                 xi = (int) floor(x);
198
199                 if (x >= windowSize) {
200                     break;
201                 }
202
203                 // Use linear interpolation in order to get smoother display
204                 if (i == 0 || xi == windowSize-1) {
205                     // ... except if we are at the left or right border of the display or the spectrum
206                     val = (*it)[xi];
207                 } else {
208
209                     if ((*it)[xi] > (*it)[xi+1]
210                         && x_prev < xi) {
211                         // This is a hack to preserve peaks.
212                         // Consider f = {0, 100, 0}
213                         //          x = {0.5,  1.5}
214                         // Then x is 50 both times, and the 100 peak is lost.
215                         // Get it back here for the first x after the peak.
216                         val = (*it)[xi];
217                     } else {
218                         val =   (xi+1 - x) * (*it)[xi]
219                               + (x - xi)   * (*it)[xi+1];
220                     }
221                 }
222
223                 // Normalize to [0 1], 1 corresponding to 0 dB and 0 to dbMin dB
224                 val = -val/m_dBmin + 1;
225                 if (val < 0) {
226                     val = 0;
227                 }
228
229                 spectrum.setPixel(leftDist + i, topDist + h-y-1, qRgba(255, 255, 255, val * 255));
230
231                 x_prev = x;
232             }
233
234             y++;
235             if (y >= topDist + m_innerScopeRect.height()) {
236                 break;
237             }
238             if (!completeRedraw) {
239                 break;
240             }
241         }
242
243 #ifdef DEBUG_SPECTROGRAM
244         qDebug() << "Rendered " << y-topDist << "lines from " << m_fftHistory.size() << " available samples in " << start.elapsed() << " ms"
245                 << (completeRedraw ? " (re-used old image)" : "");
246         uint storedBytes = 0;
247         for (QList< QVector<float> >::iterator it = m_fftHistory.begin(); it != m_fftHistory.end(); it++) {
248             storedBytes += (*it).size() * sizeof((*it)[0]);
249         }
250         qDebug() << QString("Total storage used: %1 kB").arg((double)storedBytes/1000, 0, 'f', 2);
251 #endif
252
253         m_fftHistoryImg = spectrum;
254
255         emit signalScopeRenderingFinished(start.elapsed(), 1);
256         return spectrum;
257     } else {
258         emit signalScopeRenderingFinished(0, 1);
259         return QImage();
260     }
261 }
262 QImage Spectrogram::renderBackground(uint) { return QImage(); }
263
264 bool Spectrogram::isHUDDependingOnInput() const { return false; }
265 bool Spectrogram::isScopeDependingOnInput() const { return false; }
266 bool Spectrogram::isBackgroundDependingOnInput() const { return false; }
267
268 #undef SPECTROGRAM_HISTORY_SIZE
269 #ifdef DEBUG_SPECTROGRAM
270 #undef DEBUG_SPECTROGRAM
271 #endif