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 "audiorecorder.h" |
52 | #include "audiolevel.h" |
53 | |
54 | #include "ui_audiorecorder.h" |
55 | |
56 | #include <QAudioProbe> |
57 | #include <QAudioRecorder> |
58 | #include <QDir> |
59 | #include <QFileDialog> |
60 | #include <QMediaRecorder> |
61 | #include <QStandardPaths> |
62 | |
63 | static qreal getPeakValue(const QAudioFormat &format); |
64 | static QVector<qreal> getBufferLevels(const QAudioBuffer &buffer); |
65 | |
66 | template <class T> |
67 | static QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels); |
68 | |
69 | AudioRecorder::AudioRecorder() |
70 | : ui(new Ui::AudioRecorder) |
71 | { |
72 | ui->setupUi(this); |
73 | |
74 | m_audioRecorder = new QAudioRecorder(this); |
75 | m_probe = new QAudioProbe(this); |
76 | connect(sender: m_probe, signal: &QAudioProbe::audioBufferProbed, |
77 | receiver: this, slot: &AudioRecorder::processBuffer); |
78 | m_probe->setSource(m_audioRecorder); |
79 | |
80 | //audio devices |
81 | ui->audioDeviceBox->addItem(atext: tr(s: "Default" ), auserData: QVariant(QString())); |
82 | for (auto &device: m_audioRecorder->audioInputs()) { |
83 | ui->audioDeviceBox->addItem(atext: device, auserData: QVariant(device)); |
84 | } |
85 | |
86 | //audio codecs |
87 | ui->audioCodecBox->addItem(atext: tr(s: "Default" ), auserData: QVariant(QString())); |
88 | for (auto &codecName: m_audioRecorder->supportedAudioCodecs()) { |
89 | ui->audioCodecBox->addItem(atext: codecName, auserData: QVariant(codecName)); |
90 | } |
91 | |
92 | //containers |
93 | ui->containerBox->addItem(atext: tr(s: "Default" ), auserData: QVariant(QString())); |
94 | for (auto &containerName: m_audioRecorder->supportedContainers()) { |
95 | ui->containerBox->addItem(atext: containerName, auserData: QVariant(containerName)); |
96 | } |
97 | |
98 | //sample rate |
99 | ui->sampleRateBox->addItem(atext: tr(s: "Default" ), auserData: QVariant(0)); |
100 | for (int sampleRate: m_audioRecorder->supportedAudioSampleRates()) { |
101 | ui->sampleRateBox->addItem(atext: QString::number(sampleRate), auserData: QVariant( |
102 | sampleRate)); |
103 | } |
104 | |
105 | //channels |
106 | ui->channelsBox->addItem(atext: tr(s: "Default" ), auserData: QVariant(-1)); |
107 | ui->channelsBox->addItem(QStringLiteral("1" ), auserData: QVariant(1)); |
108 | ui->channelsBox->addItem(QStringLiteral("2" ), auserData: QVariant(2)); |
109 | ui->channelsBox->addItem(QStringLiteral("4" ), auserData: QVariant(4)); |
110 | |
111 | //quality |
112 | ui->qualitySlider->setRange(min: 0, max: int(QMultimedia::VeryHighQuality)); |
113 | ui->qualitySlider->setValue(int(QMultimedia::NormalQuality)); |
114 | |
115 | //bitrates: |
116 | ui->bitrateBox->addItem(atext: tr(s: "Default" ), auserData: QVariant(0)); |
117 | ui->bitrateBox->addItem(QStringLiteral("32000" ), auserData: QVariant(32000)); |
118 | ui->bitrateBox->addItem(QStringLiteral("64000" ), auserData: QVariant(64000)); |
119 | ui->bitrateBox->addItem(QStringLiteral("96000" ), auserData: QVariant(96000)); |
120 | ui->bitrateBox->addItem(QStringLiteral("128000" ), auserData: QVariant(128000)); |
121 | |
122 | connect(sender: m_audioRecorder, signal: &QAudioRecorder::durationChanged, receiver: this, slot: &AudioRecorder::updateProgress); |
123 | connect(sender: m_audioRecorder, signal: &QAudioRecorder::statusChanged, receiver: this, slot: &AudioRecorder::updateStatus); |
124 | connect(sender: m_audioRecorder, signal: &QAudioRecorder::stateChanged, receiver: this, slot: &AudioRecorder::onStateChanged); |
125 | connect(sender: m_audioRecorder, signal: QOverload<QMediaRecorder::Error>::of(ptr: &QAudioRecorder::error), receiver: this, |
126 | slot: &AudioRecorder::displayErrorMessage); |
127 | } |
128 | |
129 | void AudioRecorder::updateProgress(qint64 duration) |
130 | { |
131 | if (m_audioRecorder->error() != QMediaRecorder::NoError || duration < 2000) |
132 | return; |
133 | |
134 | ui->statusbar->showMessage(text: tr(s: "Recorded %1 sec" ).arg(a: duration / 1000)); |
135 | } |
136 | |
137 | void AudioRecorder::updateStatus(QMediaRecorder::Status status) |
138 | { |
139 | QString statusMessage; |
140 | |
141 | switch (status) { |
142 | case QMediaRecorder::RecordingStatus: |
143 | statusMessage = tr(s: "Recording to %1" ).arg(a: m_audioRecorder->actualLocation().toString()); |
144 | break; |
145 | case QMediaRecorder::PausedStatus: |
146 | clearAudioLevels(); |
147 | statusMessage = tr(s: "Paused" ); |
148 | break; |
149 | case QMediaRecorder::UnloadedStatus: |
150 | case QMediaRecorder::LoadedStatus: |
151 | clearAudioLevels(); |
152 | statusMessage = tr(s: "Stopped" ); |
153 | default: |
154 | break; |
155 | } |
156 | |
157 | if (m_audioRecorder->error() == QMediaRecorder::NoError) |
158 | ui->statusbar->showMessage(text: statusMessage); |
159 | } |
160 | |
161 | void AudioRecorder::onStateChanged(QMediaRecorder::State state) |
162 | { |
163 | switch (state) { |
164 | case QMediaRecorder::RecordingState: |
165 | ui->recordButton->setText(tr(s: "Stop" )); |
166 | ui->pauseButton->setText(tr(s: "Pause" )); |
167 | break; |
168 | case QMediaRecorder::PausedState: |
169 | ui->recordButton->setText(tr(s: "Stop" )); |
170 | ui->pauseButton->setText(tr(s: "Resume" )); |
171 | break; |
172 | case QMediaRecorder::StoppedState: |
173 | ui->recordButton->setText(tr(s: "Record" )); |
174 | ui->pauseButton->setText(tr(s: "Pause" )); |
175 | break; |
176 | } |
177 | |
178 | ui->pauseButton->setEnabled(m_audioRecorder->state() != QMediaRecorder::StoppedState); |
179 | } |
180 | |
181 | static QVariant boxValue(const QComboBox *box) |
182 | { |
183 | int idx = box->currentIndex(); |
184 | if (idx == -1) |
185 | return QVariant(); |
186 | |
187 | return box->itemData(index: idx); |
188 | } |
189 | |
190 | void AudioRecorder::toggleRecord() |
191 | { |
192 | if (m_audioRecorder->state() == QMediaRecorder::StoppedState) { |
193 | m_audioRecorder->setAudioInput(boxValue(box: ui->audioDeviceBox).toString()); |
194 | |
195 | QAudioEncoderSettings settings; |
196 | settings.setCodec(boxValue(box: ui->audioCodecBox).toString()); |
197 | settings.setSampleRate(boxValue(box: ui->sampleRateBox).toInt()); |
198 | settings.setBitRate(boxValue(box: ui->bitrateBox).toInt()); |
199 | settings.setChannelCount(boxValue(box: ui->channelsBox).toInt()); |
200 | settings.setQuality(QMultimedia::EncodingQuality(ui->qualitySlider->value())); |
201 | settings.setEncodingMode(ui->constantQualityRadioButton->isChecked() ? |
202 | QMultimedia::ConstantQualityEncoding : |
203 | QMultimedia::ConstantBitRateEncoding); |
204 | |
205 | QString container = boxValue(box: ui->containerBox).toString(); |
206 | |
207 | m_audioRecorder->setEncodingSettings(audioSettings: settings, videoSettings: QVideoEncoderSettings(), containerMimeType: container); |
208 | m_audioRecorder->record(); |
209 | } |
210 | else { |
211 | m_audioRecorder->stop(); |
212 | } |
213 | } |
214 | |
215 | void AudioRecorder::togglePause() |
216 | { |
217 | if (m_audioRecorder->state() != QMediaRecorder::PausedState) |
218 | m_audioRecorder->pause(); |
219 | else |
220 | m_audioRecorder->record(); |
221 | } |
222 | |
223 | void AudioRecorder::setOutputLocation() |
224 | { |
225 | #ifdef Q_OS_WINRT |
226 | // UWP does not allow to store outside the sandbox |
227 | const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); |
228 | if (!QDir().mkpath(cacheDir)) { |
229 | qWarning() << "Failed to create cache directory" ; |
230 | return; |
231 | } |
232 | QString fileName = cacheDir + QLatin1String("/output.wav" ); |
233 | #else |
234 | QString fileName = QFileDialog::getSaveFileName(); |
235 | #endif |
236 | m_audioRecorder->setOutputLocation(QUrl::fromLocalFile(localfile: fileName)); |
237 | m_outputLocationSet = true; |
238 | } |
239 | |
240 | void AudioRecorder::displayErrorMessage() |
241 | { |
242 | ui->statusbar->showMessage(text: m_audioRecorder->errorString()); |
243 | } |
244 | |
245 | void AudioRecorder::clearAudioLevels() |
246 | { |
247 | for (int i = 0; i < m_audioLevels.size(); ++i) |
248 | m_audioLevels.at(i)->setLevel(0); |
249 | } |
250 | |
251 | // This function returns the maximum possible sample value for a given audio format |
252 | qreal getPeakValue(const QAudioFormat& format) |
253 | { |
254 | // Note: Only the most common sample formats are supported |
255 | if (!format.isValid()) |
256 | return qreal(0); |
257 | |
258 | if (format.codec() != "audio/pcm" ) |
259 | return qreal(0); |
260 | |
261 | switch (format.sampleType()) { |
262 | case QAudioFormat::Unknown: |
263 | break; |
264 | case QAudioFormat::Float: |
265 | if (format.sampleSize() != 32) // other sample formats are not supported |
266 | return qreal(0); |
267 | return qreal(1.00003); |
268 | case QAudioFormat::SignedInt: |
269 | if (format.sampleSize() == 32) |
270 | return qreal(INT_MAX); |
271 | if (format.sampleSize() == 16) |
272 | return qreal(SHRT_MAX); |
273 | if (format.sampleSize() == 8) |
274 | return qreal(CHAR_MAX); |
275 | break; |
276 | case QAudioFormat::UnSignedInt: |
277 | if (format.sampleSize() == 32) |
278 | return qreal(UINT_MAX); |
279 | if (format.sampleSize() == 16) |
280 | return qreal(USHRT_MAX); |
281 | if (format.sampleSize() == 8) |
282 | return qreal(UCHAR_MAX); |
283 | break; |
284 | } |
285 | |
286 | return qreal(0); |
287 | } |
288 | |
289 | // returns the audio level for each channel |
290 | QVector<qreal> getBufferLevels(const QAudioBuffer& buffer) |
291 | { |
292 | QVector<qreal> values; |
293 | |
294 | if (!buffer.format().isValid() || buffer.format().byteOrder() != QAudioFormat::LittleEndian) |
295 | return values; |
296 | |
297 | if (buffer.format().codec() != "audio/pcm" ) |
298 | return values; |
299 | |
300 | int channelCount = buffer.format().channelCount(); |
301 | values.fill(from: 0, asize: channelCount); |
302 | qreal peak_value = getPeakValue(format: buffer.format()); |
303 | if (qFuzzyCompare(p1: peak_value, p2: qreal(0))) |
304 | return values; |
305 | |
306 | switch (buffer.format().sampleType()) { |
307 | case QAudioFormat::Unknown: |
308 | case QAudioFormat::UnSignedInt: |
309 | if (buffer.format().sampleSize() == 32) |
310 | values = getBufferLevels(buffer: buffer.constData<quint32>(), frames: buffer.frameCount(), channels: channelCount); |
311 | if (buffer.format().sampleSize() == 16) |
312 | values = getBufferLevels(buffer: buffer.constData<quint16>(), frames: buffer.frameCount(), channels: channelCount); |
313 | if (buffer.format().sampleSize() == 8) |
314 | values = getBufferLevels(buffer: buffer.constData<quint8>(), frames: buffer.frameCount(), channels: channelCount); |
315 | for (int i = 0; i < values.size(); ++i) |
316 | values[i] = qAbs(t: values.at(i) - peak_value / 2) / (peak_value / 2); |
317 | break; |
318 | case QAudioFormat::Float: |
319 | if (buffer.format().sampleSize() == 32) { |
320 | values = getBufferLevels(buffer: buffer.constData<float>(), frames: buffer.frameCount(), channels: channelCount); |
321 | for (int i = 0; i < values.size(); ++i) |
322 | values[i] /= peak_value; |
323 | } |
324 | break; |
325 | case QAudioFormat::SignedInt: |
326 | if (buffer.format().sampleSize() == 32) |
327 | values = getBufferLevels(buffer: buffer.constData<qint32>(), frames: buffer.frameCount(), channels: channelCount); |
328 | if (buffer.format().sampleSize() == 16) |
329 | values = getBufferLevels(buffer: buffer.constData<qint16>(), frames: buffer.frameCount(), channels: channelCount); |
330 | if (buffer.format().sampleSize() == 8) |
331 | values = getBufferLevels(buffer: buffer.constData<qint8>(), frames: buffer.frameCount(), channels: channelCount); |
332 | for (int i = 0; i < values.size(); ++i) |
333 | values[i] /= peak_value; |
334 | break; |
335 | } |
336 | |
337 | return values; |
338 | } |
339 | |
340 | template <class T> |
341 | QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels) |
342 | { |
343 | QVector<qreal> max_values; |
344 | max_values.fill(from: 0, asize: channels); |
345 | |
346 | for (int i = 0; i < frames; ++i) { |
347 | for (int j = 0; j < channels; ++j) { |
348 | qreal value = qAbs(t: qreal(buffer[i * channels + j])); |
349 | if (value > max_values.at(i: j)) |
350 | max_values.replace(i: j, t: value); |
351 | } |
352 | } |
353 | |
354 | return max_values; |
355 | } |
356 | |
357 | void AudioRecorder::processBuffer(const QAudioBuffer& buffer) |
358 | { |
359 | if (m_audioLevels.count() != buffer.format().channelCount()) { |
360 | qDeleteAll(c: m_audioLevels); |
361 | m_audioLevels.clear(); |
362 | for (int i = 0; i < buffer.format().channelCount(); ++i) { |
363 | AudioLevel *level = new AudioLevel(ui->centralwidget); |
364 | m_audioLevels.append(t: level); |
365 | ui->levelsLayout->addWidget(level); |
366 | } |
367 | } |
368 | |
369 | QVector<qreal> levels = getBufferLevels(buffer); |
370 | for (int i = 0; i < levels.count(); ++i) |
371 | m_audioLevels.at(i)->setLevel(levels.at(i)); |
372 | } |
373 | |