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
64Waveform::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
79Waveform::~Waveform()
80{
81 deletePixmaps();
82}
83
84void 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
155void Waveform::resizeEvent(QResizeEvent *event)
156{
157 if (event->size() != event->oldSize())
158 createPixmaps(newSize: event->size());
159}
160
161void 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
200void 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
217void 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
229void 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
244void Waveform::deletePixmaps()
245{
246 qDeleteAll(c: qExchange(t&: m_pixmaps, newValue: {}));
247}
248
249void 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
274void 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
297qint64 Waveform::tilePosition(int index) const
298{
299 return m_tileArrayStart + index * m_tileLength;
300}
301
302Waveform::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
320int 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
327int 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
334bool 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
357void 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
419void 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
434void 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

source code of qtmultimedia/examples/multimedia/spectrum/app/waveform.cpp