1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the examples of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:BSD$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** BSD License Usage |
18 | ** Alternatively, you may use this file under the terms of the BSD license |
19 | ** as follows: |
20 | ** |
21 | ** "Redistribution and use in source and binary forms, with or without |
22 | ** modification, are permitted provided that the following conditions are |
23 | ** met: |
24 | ** * Redistributions of source code must retain the above copyright |
25 | ** notice, this list of conditions and the following disclaimer. |
26 | ** * Redistributions in binary form must reproduce the above copyright |
27 | ** notice, this list of conditions and the following disclaimer in |
28 | ** the documentation and/or other materials provided with the |
29 | ** distribution. |
30 | ** * Neither the name of The Qt Company Ltd nor the names of its |
31 | ** contributors may be used to endorse or promote products derived |
32 | ** from this software without specific prior written permission. |
33 | ** |
34 | ** |
35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
46 | ** |
47 | ** $QT_END_LICENSE$ |
48 | ** |
49 | ****************************************************************************/ |
50 | |
51 | #include "waveform.h" |
52 | #include "utils.h" |
53 | #include <QPainter> |
54 | #include <QResizeEvent> |
55 | #include <QDebug> |
56 | |
57 | //#define PAINT_EVENT_TRACE |
58 | #ifdef PAINT_EVENT_TRACE |
59 | # define WAVEFORM_PAINT_DEBUG qDebug() |
60 | #else |
61 | # define WAVEFORM_PAINT_DEBUG nullDebug() |
62 | #endif |
63 | |
64 | Waveform::Waveform(QWidget *parent) |
65 | : QWidget(parent) |
66 | , m_bufferPosition(0) |
67 | , m_bufferLength(0) |
68 | , m_audioPosition(0) |
69 | , m_active(false) |
70 | , m_tileLength(0) |
71 | , m_tileArrayStart(0) |
72 | , m_windowPosition(0) |
73 | , m_windowLength(0) |
74 | { |
75 | setSizePolicy(hor: QSizePolicy::Preferred, ver: QSizePolicy::Fixed); |
76 | setMinimumHeight(50); |
77 | } |
78 | |
79 | Waveform::~Waveform() |
80 | { |
81 | deletePixmaps(); |
82 | } |
83 | |
84 | void Waveform::paintEvent(QPaintEvent * /*event*/) |
85 | { |
86 | QPainter painter(this); |
87 | |
88 | painter.fillRect(r: rect(), c: Qt::black); |
89 | |
90 | if (m_active) { |
91 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" |
92 | << "windowPosition" << m_windowPosition |
93 | << "windowLength" << m_windowLength; |
94 | qint64 pos = m_windowPosition; |
95 | const qint64 windowEnd = m_windowPosition + m_windowLength; |
96 | int destLeft = 0; |
97 | int destRight = 0; |
98 | while (pos < windowEnd) { |
99 | const TilePoint point = tilePoint(position: pos); |
100 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" << "pos" << pos |
101 | << "tileIndex" << point.index |
102 | << "positionOffset" << point.positionOffset |
103 | << "pixelOffset" << point.pixelOffset; |
104 | |
105 | if (point.index != NullIndex) { |
106 | const Tile &tile = m_tiles[point.index]; |
107 | if (tile.painted) { |
108 | const qint64 sectionLength = qMin(a: (m_tileLength - point.positionOffset), |
109 | b: (windowEnd - pos)); |
110 | Q_ASSERT(sectionLength > 0); |
111 | |
112 | const int sourceRight = tilePixelOffset(positionOffset: point.positionOffset + sectionLength); |
113 | destRight = windowPixelOffset(positionOffset: pos - m_windowPosition + sectionLength); |
114 | |
115 | QRect destRect = rect(); |
116 | destRect.setLeft(destLeft); |
117 | destRect.setRight(destRight); |
118 | |
119 | QRect sourceRect(QPoint(), m_pixmapSize); |
120 | sourceRect.setLeft(point.pixelOffset); |
121 | sourceRect.setRight(sourceRight); |
122 | |
123 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" << "tileIndex" << point.index |
124 | << "source" << point.pixelOffset << sourceRight |
125 | << "dest" << destLeft << destRight; |
126 | |
127 | painter.drawPixmap(targetRect: destRect, pixmap: *tile.pixmap, sourceRect); |
128 | |
129 | destLeft = destRight; |
130 | |
131 | if (point.index < m_tiles.count()) { |
132 | pos = tilePosition(index: point.index + 1); |
133 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" << "pos ->" << pos; |
134 | } else { |
135 | // Reached end of tile array |
136 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" << "reached end of tile array" ; |
137 | break; |
138 | } |
139 | } else { |
140 | // Passed last tile which is painted |
141 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" << "tile" << point.index << "not painted" ; |
142 | break; |
143 | } |
144 | } else { |
145 | // pos is past end of tile array |
146 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" << "pos" << pos << "past end of tile array" ; |
147 | break; |
148 | } |
149 | } |
150 | |
151 | WAVEFORM_PAINT_DEBUG << "Waveform::paintEvent" << "final pos" << pos << "final x" << destRight; |
152 | } |
153 | } |
154 | |
155 | void Waveform::resizeEvent(QResizeEvent *event) |
156 | { |
157 | if (event->size() != event->oldSize()) |
158 | createPixmaps(newSize: event->size()); |
159 | } |
160 | |
161 | void Waveform::initialize(const QAudioFormat &format, qint64 audioBufferSize, qint64 windowDurationUs) |
162 | { |
163 | WAVEFORM_DEBUG << "Waveform::initialize" |
164 | << "audioBufferSize" << audioBufferSize |
165 | << "windowDurationUs" << windowDurationUs; |
166 | |
167 | reset(); |
168 | |
169 | m_format = format; |
170 | |
171 | // Calculate tile size |
172 | m_tileLength = audioBufferSize; |
173 | |
174 | // Calculate window size |
175 | m_windowLength = audioLength(format: m_format, microSeconds: windowDurationUs); |
176 | |
177 | // Calculate number of tiles required |
178 | int nTiles; |
179 | if (m_tileLength > m_windowLength) { |
180 | nTiles = 2; |
181 | } else { |
182 | nTiles = m_windowLength / m_tileLength + 1; |
183 | if (m_windowLength % m_tileLength) |
184 | ++nTiles; |
185 | } |
186 | |
187 | WAVEFORM_DEBUG << "Waveform::initialize" |
188 | << "tileLength" << m_tileLength |
189 | << "windowLength" << m_windowLength |
190 | << "nTiles" << nTiles; |
191 | |
192 | m_pixmaps.fill(from: 0, asize: nTiles); |
193 | m_tiles.resize(asize: nTiles); |
194 | |
195 | createPixmaps(newSize: rect().size()); |
196 | |
197 | m_active = true; |
198 | } |
199 | |
200 | void Waveform::reset() |
201 | { |
202 | WAVEFORM_DEBUG << "Waveform::reset" ; |
203 | |
204 | m_bufferPosition = 0; |
205 | m_buffer = QByteArray(); |
206 | m_audioPosition = 0; |
207 | m_format = QAudioFormat(); |
208 | m_active = false; |
209 | deletePixmaps(); |
210 | m_tiles.clear(); |
211 | m_tileLength = 0; |
212 | m_tileArrayStart = 0; |
213 | m_windowPosition = 0; |
214 | m_windowLength = 0; |
215 | } |
216 | |
217 | void Waveform::bufferChanged(qint64 position, qint64 length, const QByteArray &buffer) |
218 | { |
219 | WAVEFORM_DEBUG << "Waveform::bufferChanged" |
220 | << "audioPosition" << m_audioPosition |
221 | << "bufferPosition" << position |
222 | << "bufferLength" << length; |
223 | m_bufferPosition = position; |
224 | m_bufferLength = length; |
225 | m_buffer = buffer; |
226 | paintTiles(); |
227 | } |
228 | |
229 | void Waveform::audioPositionChanged(qint64 position) |
230 | { |
231 | WAVEFORM_DEBUG << "Waveform::audioPositionChanged" |
232 | << "audioPosition" << position |
233 | << "bufferPosition" << m_bufferPosition |
234 | << "bufferLength" << m_bufferLength; |
235 | |
236 | if (position >= m_bufferPosition) { |
237 | if (position + m_windowLength > m_bufferPosition + m_bufferLength) |
238 | position = qMax(a: qint64(0), b: m_bufferPosition + m_bufferLength - m_windowLength); |
239 | m_audioPosition = position; |
240 | setWindowPosition(position); |
241 | } |
242 | } |
243 | |
244 | void Waveform::deletePixmaps() |
245 | { |
246 | qDeleteAll(c: qExchange(t&: m_pixmaps, newValue: {})); |
247 | } |
248 | |
249 | void Waveform::createPixmaps(const QSize &widgetSize) |
250 | { |
251 | m_pixmapSize = widgetSize; |
252 | m_pixmapSize.setWidth(qreal(widgetSize.width()) * m_tileLength / m_windowLength); |
253 | |
254 | WAVEFORM_DEBUG << "Waveform::createPixmaps" |
255 | << "widgetSize" << widgetSize |
256 | << "pixmapSize" << m_pixmapSize; |
257 | |
258 | Q_ASSERT(m_tiles.count() == m_pixmaps.count()); |
259 | |
260 | // (Re)create pixmaps |
261 | for (int i=0; i<m_pixmaps.size(); ++i) { |
262 | delete m_pixmaps[i]; |
263 | m_pixmaps[i] = 0; |
264 | m_pixmaps[i] = new QPixmap(m_pixmapSize); |
265 | } |
266 | |
267 | // Update tile pixmap pointers, and mark for repainting |
268 | for (int i=0; i<m_tiles.count(); ++i) { |
269 | m_tiles[i].pixmap = m_pixmaps[i]; |
270 | m_tiles[i].painted = false; |
271 | } |
272 | } |
273 | |
274 | void Waveform::setWindowPosition(qint64 position) |
275 | { |
276 | WAVEFORM_DEBUG << "Waveform::setWindowPosition" |
277 | << "old" << m_windowPosition << "new" << position |
278 | << "tileArrayStart" << m_tileArrayStart; |
279 | |
280 | const qint64 oldPosition = m_windowPosition; |
281 | m_windowPosition = position; |
282 | |
283 | if ((m_windowPosition >= oldPosition) && |
284 | (m_windowPosition - m_tileArrayStart < (m_tiles.count() * m_tileLength))) { |
285 | // Work out how many tiles need to be shuffled |
286 | const qint64 offset = m_windowPosition - m_tileArrayStart; |
287 | const int nTiles = offset / m_tileLength; |
288 | shuffleTiles(n: nTiles); |
289 | } else { |
290 | resetTiles(newStartPos: m_windowPosition); |
291 | } |
292 | |
293 | if (!paintTiles() && m_windowPosition != oldPosition) |
294 | update(); |
295 | } |
296 | |
297 | qint64 Waveform::tilePosition(int index) const |
298 | { |
299 | return m_tileArrayStart + index * m_tileLength; |
300 | } |
301 | |
302 | Waveform::TilePoint Waveform::tilePoint(qint64 position) const |
303 | { |
304 | TilePoint result; |
305 | if (position >= m_tileArrayStart) { |
306 | const qint64 tileArrayEnd = m_tileArrayStart + m_tiles.count() * m_tileLength; |
307 | if (position < tileArrayEnd) { |
308 | const qint64 offsetIntoTileArray = position - m_tileArrayStart; |
309 | result.index = offsetIntoTileArray / m_tileLength; |
310 | Q_ASSERT(result.index >= 0 && result.index <= m_tiles.count()); |
311 | result.positionOffset = offsetIntoTileArray % m_tileLength; |
312 | result.pixelOffset = tilePixelOffset(positionOffset: result.positionOffset); |
313 | Q_ASSERT(result.pixelOffset >= 0 && result.pixelOffset <= m_pixmapSize.width()); |
314 | } |
315 | } |
316 | |
317 | return result; |
318 | } |
319 | |
320 | int Waveform::tilePixelOffset(qint64 positionOffset) const |
321 | { |
322 | Q_ASSERT(positionOffset >= 0 && positionOffset <= m_tileLength); |
323 | const int result = (qreal(positionOffset) / m_tileLength) * m_pixmapSize.width(); |
324 | return result; |
325 | } |
326 | |
327 | int Waveform::windowPixelOffset(qint64 positionOffset) const |
328 | { |
329 | Q_ASSERT(positionOffset >= 0 && positionOffset <= m_windowLength); |
330 | const int result = (qreal(positionOffset) / m_windowLength) * rect().width(); |
331 | return result; |
332 | } |
333 | |
334 | bool Waveform::paintTiles() |
335 | { |
336 | WAVEFORM_DEBUG << "Waveform::paintTiles" ; |
337 | bool updateRequired = false; |
338 | |
339 | for (int i=0; i<m_tiles.count(); ++i) { |
340 | const Tile &tile = m_tiles[i]; |
341 | if (!tile.painted) { |
342 | const qint64 tileStart = m_tileArrayStart + i * m_tileLength; |
343 | const qint64 tileEnd = tileStart + m_tileLength; |
344 | if (m_bufferPosition <= tileStart && m_bufferPosition + m_bufferLength >= tileEnd) { |
345 | paintTile(index: i); |
346 | updateRequired = true; |
347 | } |
348 | } |
349 | } |
350 | |
351 | if (updateRequired) |
352 | update(); |
353 | |
354 | return updateRequired; |
355 | } |
356 | |
357 | void Waveform::paintTile(int index) |
358 | { |
359 | const qint64 tileStart = m_tileArrayStart + index * m_tileLength; |
360 | |
361 | WAVEFORM_DEBUG << "Waveform::paintTile" |
362 | << "index" << index |
363 | << "bufferPosition" << m_bufferPosition |
364 | << "bufferLength" << m_bufferLength |
365 | << "start" << tileStart |
366 | << "end" << tileStart + m_tileLength; |
367 | |
368 | Q_ASSERT(m_bufferPosition <= tileStart); |
369 | Q_ASSERT(m_bufferPosition + m_bufferLength >= tileStart + m_tileLength); |
370 | |
371 | Tile &tile = m_tiles[index]; |
372 | Q_ASSERT(!tile.painted); |
373 | |
374 | const qint16* base = reinterpret_cast<const qint16*>(m_buffer.constData()); |
375 | const qint16* buffer = base + ((tileStart - m_bufferPosition) / 2); |
376 | const int numSamples = m_tileLength / (2 * m_format.channelCount()); |
377 | |
378 | QPainter painter(tile.pixmap); |
379 | |
380 | painter.fillRect(r: tile.pixmap->rect(), c: Qt::black); |
381 | |
382 | QPen pen(Qt::white); |
383 | painter.setPen(pen); |
384 | |
385 | // Calculate initial PCM value |
386 | qint16 previousPcmValue = 0; |
387 | if (buffer > base) |
388 | previousPcmValue = *(buffer - m_format.channelCount()); |
389 | |
390 | // Calculate initial point |
391 | const qreal previousRealValue = pcmToReal(pcm: previousPcmValue); |
392 | const int originY = ((previousRealValue + 1.0) / 2) * m_pixmapSize.height(); |
393 | const QPoint origin(0, originY); |
394 | |
395 | QLine line(origin, origin); |
396 | |
397 | for (int i=0; i<numSamples; ++i) { |
398 | const qint16* ptr = buffer + i * m_format.channelCount(); |
399 | |
400 | const int offset = reinterpret_cast<const char*>(ptr) - m_buffer.constData(); |
401 | Q_ASSERT(offset >= 0); |
402 | Q_ASSERT(offset < m_bufferLength); |
403 | Q_UNUSED(offset); |
404 | |
405 | const qint16 pcmValue = *ptr; |
406 | const qreal realValue = pcmToReal(pcm: pcmValue); |
407 | |
408 | const int x = tilePixelOffset(positionOffset: i * 2 * m_format.channelCount()); |
409 | const int y = ((realValue + 1.0) / 2) * m_pixmapSize.height(); |
410 | |
411 | line.setP2(QPoint(x, y)); |
412 | painter.drawLine(line); |
413 | line.setP1(line.p2()); |
414 | } |
415 | |
416 | tile.painted = true; |
417 | } |
418 | |
419 | void Waveform::shuffleTiles(int n) |
420 | { |
421 | WAVEFORM_DEBUG << "Waveform::shuffleTiles" << "n" << n; |
422 | |
423 | while (n--) { |
424 | Tile tile = m_tiles.first(); |
425 | tile.painted = false; |
426 | m_tiles.erase(pos: m_tiles.begin()); |
427 | m_tiles += tile; |
428 | m_tileArrayStart += m_tileLength; |
429 | } |
430 | |
431 | WAVEFORM_DEBUG << "Waveform::shuffleTiles" << "tileArrayStart" << m_tileArrayStart; |
432 | } |
433 | |
434 | void Waveform::resetTiles(qint64 newStartPos) |
435 | { |
436 | WAVEFORM_DEBUG << "Waveform::resetTiles" << "newStartPos" << newStartPos; |
437 | |
438 | QVector<Tile>::iterator i = m_tiles.begin(); |
439 | for ( ; i != m_tiles.end(); ++i) |
440 | i->painted = false; |
441 | |
442 | m_tileArrayStart = newStartPos; |
443 | } |
444 | |
445 | |