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 "audiooutput.h"
52
53#include <QAudioDeviceInfo>
54#include <QAudioOutput>
55#include <QDebug>
56#include <QVBoxLayout>
57#include <qmath.h>
58#include <qendian.h>
59
60Generator::Generator(const QAudioFormat &format
61 , qint64 durationUs
62 , int sampleRate)
63{
64 if (format.isValid())
65 generateData(format, durationUs, sampleRate);
66}
67
68void Generator::start()
69{
70 open(mode: QIODevice::ReadOnly);
71}
72
73void Generator::stop()
74{
75 m_pos = 0;
76 close();
77}
78
79void Generator::generateData(const QAudioFormat &format, qint64 durationUs, int sampleRate)
80{
81 const int channelBytes = format.sampleSize() / 8;
82 const int sampleBytes = format.channelCount() * channelBytes;
83 qint64 length = (format.sampleRate() * format.channelCount() * (format.sampleSize() / 8))
84 * durationUs / 1000000;
85 Q_ASSERT(length % sampleBytes == 0);
86 Q_UNUSED(sampleBytes) // suppress warning in release builds
87
88 m_buffer.resize(size: length);
89 unsigned char *ptr = reinterpret_cast<unsigned char *>(m_buffer.data());
90 int sampleIndex = 0;
91
92 while (length) {
93 // Produces value (-1..1)
94 const qreal x = qSin(v: 2 * M_PI * sampleRate * qreal(sampleIndex++ % format.sampleRate()) / format.sampleRate());
95 for (int i=0; i<format.channelCount(); ++i) {
96 if (format.sampleSize() == 8) {
97 if (format.sampleType() == QAudioFormat::UnSignedInt) {
98 const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255);
99 *reinterpret_cast<quint8 *>(ptr) = value;
100 } else if (format.sampleType() == QAudioFormat::SignedInt) {
101 const qint8 value = static_cast<qint8>(x * 127);
102 *reinterpret_cast<qint8 *>(ptr) = value;
103 }
104 } else if (format.sampleSize() == 16) {
105 if (format.sampleType() == QAudioFormat::UnSignedInt) {
106 quint16 value = static_cast<quint16>((1.0 + x) / 2 * 65535);
107 if (format.byteOrder() == QAudioFormat::LittleEndian)
108 qToLittleEndian<quint16>(src: value, dest: ptr);
109 else
110 qToBigEndian<quint16>(src: value, dest: ptr);
111 } else if (format.sampleType() == QAudioFormat::SignedInt) {
112 qint16 value = static_cast<qint16>(x * 32767);
113 if (format.byteOrder() == QAudioFormat::LittleEndian)
114 qToLittleEndian<qint16>(src: value, dest: ptr);
115 else
116 qToBigEndian<qint16>(src: value, dest: ptr);
117 }
118 }
119
120 ptr += channelBytes;
121 length -= channelBytes;
122 }
123 }
124}
125
126qint64 Generator::readData(char *data, qint64 len)
127{
128 qint64 total = 0;
129 if (!m_buffer.isEmpty()) {
130 while (len - total > 0) {
131 const qint64 chunk = qMin(a: (m_buffer.size() - m_pos), b: len - total);
132 memcpy(dest: data + total, src: m_buffer.constData() + m_pos, n: chunk);
133 m_pos = (m_pos + chunk) % m_buffer.size();
134 total += chunk;
135 }
136 }
137 return total;
138}
139
140qint64 Generator::writeData(const char *data, qint64 len)
141{
142 Q_UNUSED(data);
143 Q_UNUSED(len);
144
145 return 0;
146}
147
148qint64 Generator::bytesAvailable() const
149{
150 return m_buffer.size() + QIODevice::bytesAvailable();
151}
152
153AudioTest::AudioTest()
154 : m_pushTimer(new QTimer(this))
155{
156 initializeWindow();
157 initializeAudio(deviceInfo: QAudioDeviceInfo::defaultOutputDevice());
158}
159
160AudioTest::~AudioTest()
161{
162 m_pushTimer->stop();
163}
164
165void AudioTest::initializeWindow()
166{
167 QWidget *window = new QWidget;
168 QVBoxLayout *layout = new QVBoxLayout;
169
170 m_deviceBox = new QComboBox(this);
171 const QAudioDeviceInfo &defaultDeviceInfo = QAudioDeviceInfo::defaultOutputDevice();
172 m_deviceBox->addItem(atext: defaultDeviceInfo.deviceName(), auserData: QVariant::fromValue(value: defaultDeviceInfo));
173 for (auto &deviceInfo: QAudioDeviceInfo::availableDevices(mode: QAudio::AudioOutput)) {
174 if (deviceInfo != defaultDeviceInfo)
175 m_deviceBox->addItem(atext: deviceInfo.deviceName(), auserData: QVariant::fromValue(value: deviceInfo));
176 }
177 connect(sender: m_deviceBox, signal: QOverload<int>::of(ptr: &QComboBox::activated), receiver: this, slot: &AudioTest::deviceChanged);
178 layout->addWidget(m_deviceBox);
179
180 m_modeButton = new QPushButton(this);
181 connect(sender: m_modeButton, signal: &QPushButton::clicked, receiver: this, slot: &AudioTest::toggleMode);
182 layout->addWidget(m_modeButton);
183
184 m_suspendResumeButton = new QPushButton(this);
185 connect(sender: m_suspendResumeButton, signal: &QPushButton::clicked, receiver: this, slot: &AudioTest::toggleSuspendResume);
186 layout->addWidget(m_suspendResumeButton);
187
188 QHBoxLayout *volumeBox = new QHBoxLayout;
189 m_volumeLabel = new QLabel;
190 m_volumeLabel->setText(tr(s: "Volume:"));
191 m_volumeSlider = new QSlider(Qt::Horizontal);
192 m_volumeSlider->setMinimum(0);
193 m_volumeSlider->setMaximum(100);
194 m_volumeSlider->setSingleStep(10);
195 connect(sender: m_volumeSlider, signal: &QSlider::valueChanged, receiver: this, slot: &AudioTest::volumeChanged);
196 volumeBox->addWidget(m_volumeLabel);
197 volumeBox->addWidget(m_volumeSlider);
198 layout->addLayout(layout: volumeBox);
199
200 window->setLayout(layout);
201
202 setCentralWidget(window);
203 window->show();
204}
205
206void AudioTest::initializeAudio(const QAudioDeviceInfo &deviceInfo)
207{
208 QAudioFormat format;
209 format.setSampleRate(44100);
210 format.setChannelCount(1);
211 format.setSampleSize(16);
212 format.setCodec("audio/pcm");
213 format.setByteOrder(QAudioFormat::LittleEndian);
214 format.setSampleType(QAudioFormat::SignedInt);
215
216 if (!deviceInfo.isFormatSupported(format)) {
217 qWarning() << "Default format not supported - trying to use nearest";
218 format = deviceInfo.nearestFormat(format);
219 }
220
221 const int durationSeconds = 1;
222 const int toneSampleRateHz = 600;
223 m_generator.reset(other: new Generator(format, durationSeconds * 1000000, toneSampleRateHz));
224 m_audioOutput.reset(other: new QAudioOutput(deviceInfo, format));
225 m_generator->start();
226
227 qreal initialVolume = QAudio::convertVolume(volume: m_audioOutput->volume(),
228 from: QAudio::LinearVolumeScale,
229 to: QAudio::LogarithmicVolumeScale);
230 m_volumeSlider->setValue(qRound(d: initialVolume * 100));
231 toggleMode();
232}
233
234void AudioTest::deviceChanged(int index)
235{
236 m_generator->stop();
237 m_audioOutput->stop();
238 m_audioOutput->disconnect(receiver: this);
239 initializeAudio(deviceInfo: m_deviceBox->itemData(index).value<QAudioDeviceInfo>());
240}
241
242void AudioTest::volumeChanged(int value)
243{
244 qreal linearVolume = QAudio::convertVolume(volume: value / qreal(100),
245 from: QAudio::LogarithmicVolumeScale,
246 to: QAudio::LinearVolumeScale);
247
248 m_audioOutput->setVolume(linearVolume);
249}
250
251void AudioTest::toggleMode()
252{
253 m_pushTimer->stop();
254 m_audioOutput->stop();
255 toggleSuspendResume();
256
257 if (m_pullMode) {
258 //switch to pull mode (QAudioOutput pulls from Generator as needed)
259 m_modeButton->setText(tr(s: "Enable push mode"));
260 m_audioOutput->start(device: m_generator.data());
261 } else {
262 //switch to push mode (periodically push to QAudioOutput using a timer)
263 m_modeButton->setText(tr(s: "Enable pull mode"));
264 auto io = m_audioOutput->start();
265 m_pushTimer->disconnect();
266
267 connect(sender: m_pushTimer, signal: &QTimer::timeout, slot: [this, io]() {
268 if (m_audioOutput->state() == QAudio::StoppedState)
269 return;
270
271 QByteArray buffer(32768, 0);
272 int chunks = m_audioOutput->bytesFree() / m_audioOutput->periodSize();
273 while (chunks) {
274 const qint64 len = m_generator->read(data: buffer.data(), maxlen: m_audioOutput->periodSize());
275 if (len)
276 io->write(data: buffer.data(), len);
277 if (len != m_audioOutput->periodSize())
278 break;
279 --chunks;
280 }
281 });
282
283 m_pushTimer->start(msec: 20);
284 }
285
286 m_pullMode = !m_pullMode;
287}
288
289void AudioTest::toggleSuspendResume()
290{
291 if (m_audioOutput->state() == QAudio::SuspendedState || m_audioOutput->state() == QAudio::StoppedState) {
292 m_audioOutput->resume();
293 m_suspendResumeButton->setText(tr(s: "Suspend recording"));
294 } else if (m_audioOutput->state() == QAudio::ActiveState) {
295 m_audioOutput->suspend();
296 m_suspendResumeButton->setText(tr(s: "Resume playback"));
297 } else if (m_audioOutput->state() == QAudio::IdleState) {
298 // no-op
299 }
300}
301
302

source code of qtmultimedia/examples/multimedia/audiooutput/audiooutput.cpp