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