| 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 "histogramwidget.h" |
| 52 | #include <QPainter> |
| 53 | #include <QHBoxLayout> |
| 54 | |
| 55 | template <class T> |
| 56 | static QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels); |
| 57 | |
| 58 | class QAudioLevel : public QWidget |
| 59 | { |
| 60 | Q_OBJECT |
| 61 | public: |
| 62 | explicit QAudioLevel(QWidget *parent = nullptr); |
| 63 | |
| 64 | // Using [0; 1.0] range |
| 65 | void setLevel(qreal level); |
| 66 | |
| 67 | protected: |
| 68 | void paintEvent(QPaintEvent *event); |
| 69 | |
| 70 | private: |
| 71 | qreal m_level = 0; |
| 72 | }; |
| 73 | |
| 74 | QAudioLevel::QAudioLevel(QWidget *parent) |
| 75 | : QWidget(parent) |
| 76 | { |
| 77 | setMinimumHeight(15); |
| 78 | setMaximumHeight(50); |
| 79 | } |
| 80 | |
| 81 | void QAudioLevel::setLevel(qreal level) |
| 82 | { |
| 83 | if (m_level != level) { |
| 84 | m_level = level; |
| 85 | update(); |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | void QAudioLevel::paintEvent(QPaintEvent *event) |
| 90 | { |
| 91 | Q_UNUSED(event); |
| 92 | |
| 93 | QPainter painter(this); |
| 94 | // draw level |
| 95 | qreal widthLevel = m_level * width(); |
| 96 | painter.fillRect(x: 0, y: 0, w: widthLevel, h: height(), c: Qt::red); |
| 97 | // clear the rest of the control |
| 98 | painter.fillRect(x: widthLevel, y: 0, w: width(), h: height(), c: Qt::black); |
| 99 | } |
| 100 | |
| 101 | HistogramWidget::HistogramWidget(QWidget *parent) |
| 102 | : QWidget(parent) |
| 103 | { |
| 104 | m_processor.moveToThread(thread: &m_processorThread); |
| 105 | qRegisterMetaType<QVector<qreal>>(typeName: "QVector<qreal>" ); |
| 106 | connect(sender: &m_processor, signal: &FrameProcessor::histogramReady, receiver: this, slot: &HistogramWidget::setHistogram); |
| 107 | m_processorThread.start(QThread::LowestPriority); |
| 108 | setLayout(new QHBoxLayout); |
| 109 | } |
| 110 | |
| 111 | HistogramWidget::~HistogramWidget() |
| 112 | { |
| 113 | m_processorThread.quit(); |
| 114 | m_processorThread.wait(time: 10000); |
| 115 | } |
| 116 | |
| 117 | void HistogramWidget::processFrame(const QVideoFrame &frame) |
| 118 | { |
| 119 | if (m_isBusy && frame.isValid()) |
| 120 | return; //drop frame |
| 121 | |
| 122 | m_isBusy = true; |
| 123 | QMetaObject::invokeMethod(obj: &m_processor, member: "processFrame" , |
| 124 | type: Qt::QueuedConnection, Q_ARG(QVideoFrame, frame), Q_ARG(int, m_levels)); |
| 125 | } |
| 126 | |
| 127 | // This function returns the maximum possible sample value for a given audio format |
| 128 | qreal getPeakValue(const QAudioFormat& format) |
| 129 | { |
| 130 | // Note: Only the most common sample formats are supported |
| 131 | if (!format.isValid()) |
| 132 | return qreal(0); |
| 133 | |
| 134 | if (format.codec() != "audio/pcm" ) |
| 135 | return qreal(0); |
| 136 | |
| 137 | switch (format.sampleType()) { |
| 138 | case QAudioFormat::Unknown: |
| 139 | break; |
| 140 | case QAudioFormat::Float: |
| 141 | if (format.sampleSize() != 32) // other sample formats are not supported |
| 142 | return qreal(0); |
| 143 | return qreal(1.00003); |
| 144 | case QAudioFormat::SignedInt: |
| 145 | if (format.sampleSize() == 32) |
| 146 | return qreal(INT_MAX); |
| 147 | if (format.sampleSize() == 16) |
| 148 | return qreal(SHRT_MAX); |
| 149 | if (format.sampleSize() == 8) |
| 150 | return qreal(CHAR_MAX); |
| 151 | break; |
| 152 | case QAudioFormat::UnSignedInt: |
| 153 | if (format.sampleSize() == 32) |
| 154 | return qreal(UINT_MAX); |
| 155 | if (format.sampleSize() == 16) |
| 156 | return qreal(USHRT_MAX); |
| 157 | if (format.sampleSize() == 8) |
| 158 | return qreal(UCHAR_MAX); |
| 159 | break; |
| 160 | } |
| 161 | |
| 162 | return qreal(0); |
| 163 | } |
| 164 | |
| 165 | // returns the audio level for each channel |
| 166 | QVector<qreal> getBufferLevels(const QAudioBuffer& buffer) |
| 167 | { |
| 168 | QVector<qreal> values; |
| 169 | |
| 170 | if (!buffer.isValid()) |
| 171 | return values; |
| 172 | |
| 173 | if (!buffer.format().isValid() || buffer.format().byteOrder() != QAudioFormat::LittleEndian) |
| 174 | return values; |
| 175 | |
| 176 | if (buffer.format().codec() != "audio/pcm" ) |
| 177 | return values; |
| 178 | |
| 179 | int channelCount = buffer.format().channelCount(); |
| 180 | values.fill(from: 0, asize: channelCount); |
| 181 | qreal peak_value = getPeakValue(format: buffer.format()); |
| 182 | if (qFuzzyCompare(p1: peak_value, p2: qreal(0))) |
| 183 | return values; |
| 184 | |
| 185 | switch (buffer.format().sampleType()) { |
| 186 | case QAudioFormat::Unknown: |
| 187 | case QAudioFormat::UnSignedInt: |
| 188 | if (buffer.format().sampleSize() == 32) |
| 189 | values = getBufferLevels(buffer: buffer.constData<quint32>(), frames: buffer.frameCount(), channels: channelCount); |
| 190 | if (buffer.format().sampleSize() == 16) |
| 191 | values = getBufferLevels(buffer: buffer.constData<quint16>(), frames: buffer.frameCount(), channels: channelCount); |
| 192 | if (buffer.format().sampleSize() == 8) |
| 193 | values = getBufferLevels(buffer: buffer.constData<quint8>(), frames: buffer.frameCount(), channels: channelCount); |
| 194 | for (int i = 0; i < values.size(); ++i) |
| 195 | values[i] = qAbs(t: values.at(i) - peak_value / 2) / (peak_value / 2); |
| 196 | break; |
| 197 | case QAudioFormat::Float: |
| 198 | if (buffer.format().sampleSize() == 32) { |
| 199 | values = getBufferLevels(buffer: buffer.constData<float>(), frames: buffer.frameCount(), channels: channelCount); |
| 200 | for (int i = 0; i < values.size(); ++i) |
| 201 | values[i] /= peak_value; |
| 202 | } |
| 203 | break; |
| 204 | case QAudioFormat::SignedInt: |
| 205 | if (buffer.format().sampleSize() == 32) |
| 206 | values = getBufferLevels(buffer: buffer.constData<qint32>(), frames: buffer.frameCount(), channels: channelCount); |
| 207 | if (buffer.format().sampleSize() == 16) |
| 208 | values = getBufferLevels(buffer: buffer.constData<qint16>(), frames: buffer.frameCount(), channels: channelCount); |
| 209 | if (buffer.format().sampleSize() == 8) |
| 210 | values = getBufferLevels(buffer: buffer.constData<qint8>(), frames: buffer.frameCount(), channels: channelCount); |
| 211 | for (int i = 0; i < values.size(); ++i) |
| 212 | values[i] /= peak_value; |
| 213 | break; |
| 214 | } |
| 215 | |
| 216 | return values; |
| 217 | } |
| 218 | |
| 219 | template <class T> |
| 220 | QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels) |
| 221 | { |
| 222 | QVector<qreal> max_values; |
| 223 | max_values.fill(from: 0, asize: channels); |
| 224 | |
| 225 | for (int i = 0; i < frames; ++i) { |
| 226 | for (int j = 0; j < channels; ++j) { |
| 227 | qreal value = qAbs(t: qreal(buffer[i * channels + j])); |
| 228 | if (value > max_values.at(i: j)) |
| 229 | max_values.replace(i: j, t: value); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | return max_values; |
| 234 | } |
| 235 | |
| 236 | void HistogramWidget::processBuffer(const QAudioBuffer &buffer) |
| 237 | { |
| 238 | if (m_audioLevels.count() != buffer.format().channelCount()) { |
| 239 | qDeleteAll(c: m_audioLevels); |
| 240 | m_audioLevels.clear(); |
| 241 | for (int i = 0; i < buffer.format().channelCount(); ++i) { |
| 242 | QAudioLevel *level = new QAudioLevel(this); |
| 243 | m_audioLevels.append(t: level); |
| 244 | layout()->addWidget(w: level); |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | QVector<qreal> levels = getBufferLevels(buffer); |
| 249 | for (int i = 0; i < levels.count(); ++i) |
| 250 | m_audioLevels.at(i)->setLevel(levels.at(i)); |
| 251 | } |
| 252 | |
| 253 | void HistogramWidget::setHistogram(const QVector<qreal> &histogram) |
| 254 | { |
| 255 | m_isBusy = false; |
| 256 | m_histogram = histogram; |
| 257 | update(); |
| 258 | } |
| 259 | |
| 260 | void HistogramWidget::paintEvent(QPaintEvent *event) |
| 261 | { |
| 262 | Q_UNUSED(event); |
| 263 | |
| 264 | if (!m_audioLevels.isEmpty()) |
| 265 | return; |
| 266 | |
| 267 | QPainter painter(this); |
| 268 | |
| 269 | if (m_histogram.isEmpty()) { |
| 270 | painter.fillRect(x: 0, y: 0, w: width(), h: height(), b: QColor::fromRgb(r: 0, g: 0, b: 0)); |
| 271 | return; |
| 272 | } |
| 273 | |
| 274 | qreal barWidth = width() / (qreal)m_histogram.size(); |
| 275 | |
| 276 | for (int i = 0; i < m_histogram.size(); ++i) { |
| 277 | qreal h = m_histogram[i] * height(); |
| 278 | // draw level |
| 279 | painter.fillRect(x: barWidth * i, y: height() - h, w: barWidth * (i + 1), h: height(), c: Qt::red); |
| 280 | // clear the rest of the control |
| 281 | painter.fillRect(x: barWidth * i, y: 0, w: barWidth * (i + 1), h: height() - h, c: Qt::black); |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | void FrameProcessor::processFrame(QVideoFrame frame, int levels) |
| 286 | { |
| 287 | QVector<qreal> histogram(levels); |
| 288 | |
| 289 | do { |
| 290 | if (!levels) |
| 291 | break; |
| 292 | |
| 293 | if (!frame.map(mode: QAbstractVideoBuffer::ReadOnly)) |
| 294 | break; |
| 295 | |
| 296 | if (frame.pixelFormat() == QVideoFrame::Format_YUV420P || |
| 297 | frame.pixelFormat() == QVideoFrame::Format_NV12) { |
| 298 | // Process YUV data |
| 299 | uchar *b = frame.bits(); |
| 300 | for (int y = 0; y < frame.height(); ++y) { |
| 301 | uchar *lastPixel = b + frame.width(); |
| 302 | for (uchar *curPixel = b; curPixel < lastPixel; curPixel++) |
| 303 | histogram[(*curPixel * levels) >> 8] += 1.0; |
| 304 | b += frame.bytesPerLine(); |
| 305 | } |
| 306 | } else { |
| 307 | QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(format: frame.pixelFormat()); |
| 308 | if (imageFormat != QImage::Format_Invalid) { |
| 309 | // Process RGB data |
| 310 | QImage image(frame.bits(), frame.width(), frame.height(), imageFormat); |
| 311 | image = image.convertToFormat(f: QImage::Format_RGB32); |
| 312 | |
| 313 | const QRgb* b = (const QRgb*)image.bits(); |
| 314 | for (int y = 0; y < image.height(); ++y) { |
| 315 | const QRgb *lastPixel = b + frame.width(); |
| 316 | for (const QRgb *curPixel = b; curPixel < lastPixel; curPixel++) |
| 317 | histogram[(qGray(rgb: *curPixel) * levels) >> 8] += 1.0; |
| 318 | b = (const QRgb*)((uchar*)b + image.bytesPerLine()); |
| 319 | } |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | // find maximum value |
| 324 | qreal maxValue = 0.0; |
| 325 | for (int i = 0; i < histogram.size(); i++) { |
| 326 | if (histogram[i] > maxValue) |
| 327 | maxValue = histogram[i]; |
| 328 | } |
| 329 | |
| 330 | if (maxValue > 0.0) { |
| 331 | for (int i = 0; i < histogram.size(); i++) |
| 332 | histogram[i] /= maxValue; |
| 333 | } |
| 334 | |
| 335 | frame.unmap(); |
| 336 | } while (false); |
| 337 | |
| 338 | emit histogramReady(histogram); |
| 339 | } |
| 340 | |
| 341 | #include "histogramwidget.moc" |
| 342 | |