| 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 | |