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