1 /*
2  * Copyright (c) 2015-2020 Meltytech, LLC
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "audiowaveformscopewidget.h"
19 
20 #include <Logger.h>
21 
22 #include <QMouseEvent>
23 #include <QPainter>
24 #include <QResizeEvent>
25 #include <QToolTip>
26 #include <cmath>
27 
28 static const qreal MAX_AMPLITUDE = 32768.0;
29 
graphHeight(const QSize & widgetSize,int maxChan,int padding)30 static int graphHeight(const QSize& widgetSize, int maxChan, int padding)
31 {
32     int totalPadding = padding + (padding * maxChan);
33     return (widgetSize.height() - totalPadding) / maxChan;
34 }
35 
graphBottomY(const QSize & widgetSize,int channel,int maxChan,int padding)36 static int graphBottomY(const QSize& widgetSize, int channel, int maxChan, int padding)
37 {
38     int gHeight = graphHeight(widgetSize, maxChan, padding);
39     return padding + (gHeight + padding) * channel;
40 }
41 
graphTopY(const QSize & widgetSize,int channel,int maxChan,int padding)42 static int graphTopY(const QSize& widgetSize, int channel, int maxChan, int padding)
43 {
44     int gHeight = graphHeight(widgetSize, maxChan, padding);
45     return graphBottomY(widgetSize, channel, maxChan, padding) + gHeight;
46 }
47 
graphCenterY(const QSize & widgetSize,int channel,int maxChan,int padding)48 static int graphCenterY(const QSize& widgetSize, int channel, int maxChan, int padding)
49 {
50     int gHeight = graphHeight(widgetSize, maxChan, padding);
51     return graphBottomY(widgetSize, channel, maxChan, padding) + gHeight / 2;
52 }
53 
AudioWaveformScopeWidget()54 AudioWaveformScopeWidget::AudioWaveformScopeWidget()
55   : ScopeWidget("AudioWaveform")
56   , m_renderWave()
57   , m_graphTopPadding(0)
58   , m_channels(0)
59   , m_cursorPos(-1)
60   , m_mutex(QMutex::NonRecursive)
61   , m_displayWave()
62   , m_displayGrid()
63 {
64     LOG_DEBUG() << "begin";
65     setAutoFillBackground(true);
66     setMinimumSize(100, 100);
67     setMouseTracking(true);
68     LOG_DEBUG() << "end";
69 }
70 
~AudioWaveformScopeWidget()71 AudioWaveformScopeWidget::~AudioWaveformScopeWidget()
72 {
73 }
74 
refreshScope(const QSize & size,bool full)75 void AudioWaveformScopeWidget::refreshScope(const QSize& size, bool full)
76 {
77     m_mutex.lock();
78     QSize prevSize = m_displayWave.size();
79     while (m_queue.count() > 0) {
80         m_frame = m_queue.pop();
81     }
82     m_mutex.unlock();
83 
84     // Check if a full refresh should be forced.
85     int channels = 2;
86     if (m_frame.is_valid() && m_frame.get_audio_channels() > 0) {
87         channels = m_frame.get_audio_channels();
88     }
89     if (prevSize != size || channels != m_channels) {
90         m_channels = channels;
91         full = true;
92     }
93 
94     if (full) {
95         createGrid(size);
96     }
97 
98     if (m_renderWave.size() != size) {
99         m_renderWave = QImage(size, QImage::Format_ARGB32_Premultiplied);
100     }
101 
102     m_renderWave.fill(Qt::transparent);
103 
104     QPainter p(&m_renderWave);
105     p.setRenderHint(QPainter::Antialiasing, true);
106     QColor penColor(palette().text().color());
107     penColor.setAlpha(255/2);
108     QPen pen(penColor);
109     pen.setWidth(0);
110     p.setPen(pen);
111 
112     if (m_frame.is_valid() && m_frame.get_audio_samples() > 0) {
113         int samples = m_frame.get_audio_samples();
114         int16_t* audio = (int16_t*)m_frame.get_audio();
115         int waveAmplitude = graphHeight(size, m_channels, m_graphTopPadding) / 2;
116         qreal scaleFactor = (qreal)waveAmplitude / (qreal)MAX_AMPLITUDE;
117 
118         for (int c = 0; c < m_channels; c++)
119         {
120             p.save();
121             int y = graphCenterY(size, c, m_channels, m_graphTopPadding);
122             p.translate(0, y);
123 
124             // For each x position on the waveform, find the min and max sample
125             // values that apply to that position. Draw a vertical line from the
126             // min value to the max value.
127             QPoint high;
128             QPoint low;
129             int lastX = 0;
130             const int16_t* q = audio + c;
131             // Invert the polarity because QT draws from top to bottom.
132             int16_t value = *q * -1;
133             qreal max = value;
134             qreal min = value;
135 
136             for (int i = 0; i <= samples; i++)
137             {
138                 int x = ( i * size.width() ) / samples;
139                 if (x != lastX) {
140                     // The min and max have been determined for the previous x
141                     // So draw the line
142                     high.setX(lastX);
143                     high.setY(max * scaleFactor);
144                     low.setX(lastX);
145                     low.setY(min * scaleFactor);
146                     if (high.y() == low.y()) {
147                         p.drawPoint(high);
148                     } else {
149                         p.drawLine(low, high);
150                     }
151                     lastX = x;
152 
153                     // Swap max and min so that the next line picks up where
154                     // this one left off.
155                     int tmp = max;
156                     max = min;
157                     min = tmp;
158                 }
159 
160                 if (value > max) max = value;
161                 if (value < min) min = value;
162                 q += m_channels;
163                 value = *q * -1;
164             }
165             p.restore();
166         }
167     }
168 
169     p.end();
170 
171     m_mutex.lock();
172     m_displayWave.swap(m_renderWave);
173     m_mutex.unlock();
174 }
175 
createGrid(const QSize & size)176 void AudioWaveformScopeWidget::createGrid(const QSize& size)
177 {
178     QFont font = QWidget::font();
179     int fontSize = font.pointSize() - (font.pointSize() > 10? 2 : (font.pointSize() > 8? 1 : 0));
180     font.setPointSize(fontSize);
181     QFontMetrics fm(font);
182     QString zeroLabel = tr("0");
183     QString infinityLabel = tr("-inf");
184     QRect textRect = fm.tightBoundingRect( infinityLabel );
185     int labelHeight = textRect.height();
186     m_graphTopPadding = fm.height();
187     m_graphLeftPadding = textRect.width() + 6;
188 
189     m_mutex.lock();
190 
191     m_displayGrid = QImage(size, QImage::Format_ARGB32_Premultiplied);
192     m_displayGrid.fill(Qt::transparent);
193     QPainter p(&m_displayGrid);
194     p.setPen(palette().text().color().rgb());
195     p.setFont(font);
196 
197     for (int c = 0; c < m_channels; c++) {
198         QPoint textLoc(0, 0);
199         QPoint lineBegin(m_graphLeftPadding, 0);
200         QPoint lineEnd(size.width() - 1, 0);
201         int y = 0;
202 
203         y = graphBottomY(size, c, m_channels, m_graphTopPadding);
204         textLoc.setY(y + labelHeight / 2);
205         textLoc.setX( (m_graphLeftPadding - fm.width(zeroLabel)) / 2);
206         p.drawText( textLoc, zeroLabel );
207         lineBegin.setY(y);
208         lineEnd.setY(y);
209         p.drawLine(lineBegin, lineEnd);
210 
211         y = graphCenterY(size, c, m_channels, m_graphTopPadding);
212         textLoc.setY(y + labelHeight / 2);
213         textLoc.setX( (m_graphLeftPadding - fm.width(infinityLabel)) / 2);
214         p.drawText( textLoc, infinityLabel );
215         lineBegin.setY(y);
216         lineEnd.setY(y);
217         p.drawLine(lineBegin, lineEnd);
218 
219         y = graphTopY(size, c, m_channels, m_graphTopPadding);
220         textLoc.setY(y + labelHeight / 2);
221         textLoc.setX( (m_graphLeftPadding - fm.width(zeroLabel)) / 2);
222         p.drawText( textLoc, zeroLabel );
223         lineBegin.setY(y);
224         lineEnd.setY(y);
225         p.drawLine(lineBegin, lineEnd);
226     }
227 
228     p.end();
229 
230     m_mutex.unlock();
231 }
232 
paintEvent(QPaintEvent *)233 void AudioWaveformScopeWidget::paintEvent(QPaintEvent*)
234 {
235     if (!isVisible())
236         return;
237 
238     QPainter p(this);
239     m_mutex.lock();
240     p.drawImage(rect(), m_displayGrid, m_displayGrid.rect());
241     p.drawImage(rect(), m_displayWave, m_displayWave.rect());
242     m_mutex.unlock();
243 
244     if (m_cursorPos > -1) {
245         p.setPen(palette().text().color().rgb());
246         p.drawLine(m_cursorPos, 0, m_cursorPos, height());
247     }
248 
249     p.end();
250 }
251 
mouseMoveEvent(QMouseEvent * event)252 void AudioWaveformScopeWidget::mouseMoveEvent(QMouseEvent *event)
253 {
254     QMutexLocker locker(&m_mutex);
255     if (!m_frame.is_valid()) return;
256 
257     int channels = m_frame.get_audio_channels();
258     int samples = m_frame.get_audio_samples();
259     int16_t* audio = (int16_t*)m_frame.get_audio();
260     if (samples < 10 || channels < 1) return;
261 
262     qreal position = (qreal)event->pos().x() / (qreal)width();
263     int sample = (qreal)samples * position;
264     QString text = tr("Sample: %1\n").arg(QString::number(sample+1));
265 
266     for (int c = 0; c < channels; c++)
267     {
268         const int16_t* q = audio + (channels * sample) + c;
269         qreal scaledValue = (qreal)*q / MAX_AMPLITUDE;
270         qreal dbValue = 20 * log(fabs(scaledValue));
271         if (dbValue < 0.01 && dbValue > -0.01) dbValue = 0.0;
272         text += tr("Ch: %1: %2 (%3 dBFS)").arg(QString::number(c+1)).arg(QString::number(scaledValue, 'f', 2)).arg(QString::number(dbValue, 'f', 2));
273         if ( c != channels -1 )
274         {
275             text += "\n";
276         }
277     }
278 
279     locker.unlock();
280 
281     m_cursorPos = event->pos().x();
282     QToolTip::showText(event->globalPos(), text);
283     update();
284 }
285 
leaveEvent(QEvent * event)286 void AudioWaveformScopeWidget::leaveEvent(QEvent *event)
287 {
288     Q_UNUSED(event);
289     m_cursorPos = -1;
290     update();
291 }
292 
getTitle()293 QString AudioWaveformScopeWidget::getTitle()
294 {
295    return tr("Audio Waveform");
296 }
297