Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qdrawavaudiodecoder.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtMultimedia/qaudiodecoder.h>
7#include <QtMultimedia/qaudiobuffer.h>
8#include <QtCore/qdebug.h>
9#include <QtCore/qeventloop.h>
10#include <QtCore/qfile.h>
11#include <QtCore/qiodevice.h>
12#include <QtCore/qloggingcategory.h>
13#include <QtCore/qthreadpool.h>
14
15#if QT_CONFIG(network)
16# include <QtNetwork/qnetworkaccessmanager.h>
17# include <QtNetwork/qnetworkreply.h>
18# include <QtNetwork/qnetworkrequest.h>
19#endif
20
21#include <dr_wav.h>
22
24
25Q_STATIC_LOGGING_CATEGORY(qLcDrWavDecoder, "qt.multimedia.drwavdecoder")
26
27namespace QtMultimediaPrivate {
28
29static QDrWavDecodeResult makeFormatError(const QString &message)
30{
31 return q23::unexpected(std::pair(QAudioDecoder::FormatError, message));
32}
33
34static QDrWavDecodeResult makeResourceError(const QString &message)
35{
36 return q23::unexpected(std::pair(QAudioDecoder::ResourceError, message));
37}
38
39QDrWavDecodeResult loadWaveAndDecodeData(QSpan<const std::byte> data,
40 const QAudioFormat &requestedFormat)
41{
42 using namespace QtPrivate;
43
44 drwav wav;
45 if (!drwav_init_memory(&wav, data.data(), data.size(), nullptr)) {
46 qCDebug(qLcDrWavDecoder) << "Failed to initialize dr_wav decoder";
47 return makeFormatError(QAudioDecoder::tr("Unable to decode audio file"));
48 }
49
50 auto cleanup = qScopeGuard([&] {
51 drwav_uninit(&wav);
52 });
53
54 // Determine output format
55 QAudioFormat outputFormat;
56 size_t bytesPerFrame;
57
58 if (requestedFormat.isValid()) {
59 // User set a format - check compatibility with file
60 if (requestedFormat.sampleRate() != static_cast<int>(wav.sampleRate))
61 return makeFormatError(
62 QAudioDecoder::tr("Audio file sample rate does not match requested format"));
63 if (requestedFormat.channelCount() != static_cast<int>(wav.channels))
64 return makeFormatError(
65 QAudioDecoder::tr("Audio file channel count does not match requested format"));
66 outputFormat = requestedFormat;
67
68 // Determine bytes per frame based on requested format
69 switch (requestedFormat.sampleFormat()) {
70 case QAudioFormat::UInt8:
71 bytesPerFrame = wav.channels * sizeof(uint8_t);
72 break;
73 case QAudioFormat::Int16:
74 bytesPerFrame = wav.channels * sizeof(int16_t);
75 break;
76 case QAudioFormat::Int32:
77 bytesPerFrame = wav.channels * sizeof(int32_t);
78 break;
79 case QAudioFormat::Float:
80 bytesPerFrame = wav.channels * sizeof(float);
81 break;
82 default:
83 return makeFormatError(QAudioDecoder::tr("Unsupported sample format"));
84 }
85 } else {
86 // Infer format from file header - use native format heuristic
87 QAudioFormat::SampleFormat sampleFormat;
88
89 switch (wav.bitsPerSample) {
90 case 8:
91 sampleFormat = QAudioFormat::UInt8;
92 bytesPerFrame = wav.channels * sizeof(uint8_t);
93 break;
94 case 16:
95 sampleFormat = QAudioFormat::Int16;
96 bytesPerFrame = wav.channels * sizeof(int16_t);
97 break;
98 case 24: // 24-bit → read as Int32
99 sampleFormat = QAudioFormat::Int32;
100 bytesPerFrame = wav.channels * sizeof(int32_t);
101 break;
102 case 32:
103 sampleFormat = QAudioFormat::Int32;
104 bytesPerFrame = wav.channels * sizeof(int32_t);
105 break;
106 default: // unsupported → fallback Float
107 sampleFormat = QAudioFormat::Float;
108 bytesPerFrame = wav.channels * sizeof(float);
109 break;
110 }
111
112 outputFormat.setChannelCount(wav.channels);
113 outputFormat.setSampleFormat(sampleFormat);
114 outputFormat.setSampleRate(wav.sampleRate);
115 outputFormat.setChannelConfig(
116 QAudioFormat::defaultChannelConfigForChannelCount(wav.channels));
117 }
118
119 qCDebug(qLcDrWavDecoder) << "Decoded WAV:"
120 << "channels=" << wav.channels << "sampleRate=" << wav.sampleRate
121 << "bitsPerSample=" << wav.bitsPerSample
122 << "totalPCMFrameCount=" << wav.totalPCMFrameCount
123 << "outputFormat=" << outputFormat.sampleFormat();
124
125 const QAudioFormat::SampleFormat readFormat = outputFormat.sampleFormat();
126 switch (readFormat) {
127 case QAudioFormat::UInt8:
128 case QAudioFormat::Int16:
129 case QAudioFormat::Int32:
130 case QAudioFormat::Float:
131 break;
132 default:
133 return makeFormatError(QAudioDecoder::tr("Unsupported sample format"));
134 }
135
136 std::optional<QByteArray> decoded = readFramesInChunks(
137 wav.totalPCMFrameCount, bytesPerFrame, [&](uint64_t frameCount, char *out) -> uint64_t {
138 switch (readFormat) {
139 case QAudioFormat::UInt8:
140 return drwav_read_pcm_frames(&wav, frameCount, out);
141 case QAudioFormat::Int16:
142 return drwav_read_pcm_frames_s16(&wav, frameCount, reinterpret_cast<int16_t *>(out));
143 case QAudioFormat::Int32:
144 return drwav_read_pcm_frames_s32(&wav, frameCount, reinterpret_cast<int32_t *>(out));
145 case QAudioFormat::Float:
146 return drwav_read_pcm_frames_f32(&wav, frameCount, reinterpret_cast<float *>(out));
147 default:
148 Q_UNREACHABLE_RETURN(0);
149 }
150 });
151
152 if (!decoded) {
153 qCDebug(qLcDrWavDecoder) << "Failed to read all frames:"
154 << "expected" << wav.totalPCMFrameCount;
155 return makeFormatError(QAudioDecoder::tr("Unable to read audio data"));
156 }
157
158 const QByteArray &pcmData = *decoded;
159
160 QAudioBuffer buffer(pcmData, outputFormat);
161 if (!buffer.isValid()) {
162 return makeFormatError(QAudioDecoder::tr("Failed to create audio buffer"));
163 }
164
165 qCDebug(qLcDrWavDecoder) << "Successfully decoded:" << buffer.sampleCount() << "samples";
166
167 return buffer;
168}
169
170} // namespace QtMultimediaPrivate
171
172QDrWavAudioDecoder::QDrWavAudioDecoder(QAudioDecoder *parent)
174{
175}
176
177QDrWavAudioDecoder::~QDrWavAudioDecoder()
178{
179 stop();
180}
181
182void QDrWavAudioDecoder::setSource(const QUrl &fileName)
183{
184 stop();
185 m_sourceDevice = nullptr;
186 if (std::exchange(m_source, fileName) != fileName)
187 sourceChanged();
188}
189
190void QDrWavAudioDecoder::setSourceDevice(QIODevice *device)
191{
192 stop();
193 m_source.clear();
194 if (std::exchange(m_sourceDevice, device) != device)
195 sourceChanged();
196}
197
198void QDrWavAudioDecoder::setAudioFormat(const QAudioFormat &format)
199{
200 if (isDecoding())
201 return;
202 m_audioFormat = format;
203}
204
205
206void QDrWavAudioDecoder::start()
207{
208 if (isDecoding())
209 return;
210
211 if (m_source.isEmpty() && !m_sourceDevice) {
212 error(QAudioDecoder::ResourceError, tr("No audio source specified"));
213 return;
214 }
215
216 m_decodingStopped = std::make_shared<std::atomic_bool>(false);
217
218 auto threadPool = QThreadPool::globalInstance();
219 threadPool->start([source = m_source, sourceDevice = m_sourceDevice,
220 requestedFormat = m_audioFormat, decodingStopped = m_decodingStopped, this] {
221 auto state = loadAndDecodeFile(source, sourceDevice, requestedFormat, decodingStopped);
222
223 QMetaObject::invokeMethod(this,
224 [this, decodingStopped, state = std::move(state)]() mutable {
225 if (*decodingStopped) {
226 qCDebug(qLcDrWavDecoder) << "Decoding was stopped, discarding result";
227 return;
228 }
229 onDecodeFinished(std::move(state), *decodingStopped);
230 });
231 });
232}
233
234void QDrWavAudioDecoder::stop()
235{
236 if (!isDecoding())
237 return;
238
239 qCDebug(qLcDrWavDecoder) << "stop() called";
240
241 // Prevent the continuation from delivering the buffer
242 if (m_decodingStopped) {
243 *m_decodingStopped = true;
244 m_decodingStopped.reset();
245 }
246
247 m_buffer = QAudioBuffer();
248 finished();
249}
250
251QAudioBuffer QDrWavAudioDecoder::read()
252{
253 if (m_buffer.isValid()) {
254 QAudioBuffer buffer = std::exchange(m_buffer, QAudioBuffer());
255 positionChanged(duration());
256 bufferAvailableChanged(false);
257 return buffer;
258 }
259 return QAudioBuffer();
260}
261
262auto QDrWavAudioDecoder::loadAndDecodeFile(const QUrl &source, QIODevice *sourceDevice,
263 const QAudioFormat &requestedFormat,
264 std::shared_ptr<std::atomic_bool> decodingStopped)
265 -> QDrWavDecodeResult
266{
267 using namespace QtMultimediaPrivate;
268
269 QByteArray fileData;
270
271 // Load file data based on source type
272 if (!source.isEmpty()) {
273 QString scheme = source.scheme();
274
275 if (scheme.isEmpty() || scheme == u"file") {
276 // Local file
277 QString filePath = source.isLocalFile() ? source.toLocalFile()
278 : source.path();
279 QFile file(filePath);
280 if (!file.open(QFile::ReadOnly))
281 return makeResourceError(QAudioDecoder::tr("Cannot open audio file"));
282 fileData = file.readAll();
283 } else if (scheme == u"qrc") {
284 // QRC resource
285 QString filePath = u":" + source.toString(QUrl::RemoveScheme);
286 QFile file(filePath);
287 if (!file.open(QFile::ReadOnly))
288 return makeResourceError(QAudioDecoder::tr("Cannot open audio resource"));
289 fileData = file.readAll();
290 }
291#if QT_CONFIG(network)
292 else {
293 // Network URL
294 QNetworkAccessManager networkAccessManager;
295 QNetworkReply *reply = networkAccessManager.get(QNetworkRequest(source));
296
297 if (reply->error() != QNetworkReply::NoError) {
298 reply->deleteLater();
299 return makeResourceError(QAudioDecoder::tr("Failed to download audio file"));
300 }
301
302 // Wait for network reply to finish
303 QEventLoop loop;
304 QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
305 QObject::connect(reply, &QNetworkReply::errorOccurred, &loop, &QEventLoop::quit);
306
307 loop.exec();
308
309 if (reply->error() != QNetworkReply::NoError) {
310 reply->deleteLater();
311 return makeResourceError(QAudioDecoder::tr("Network error while downloading audio"));
312 }
313
314 if (*decodingStopped) {
315 reply->deleteLater();
316 return QAudioBuffer{};
317 }
318
319 fileData = reply->readAll();
320 reply->deleteLater();
321 }
322#endif
323 } else if (sourceDevice) {
324 if (!sourceDevice->isOpen()) {
325 if (!sourceDevice->open(QIODevice::ReadOnly))
326 return makeResourceError(QAudioDecoder::tr("Cannot open audio device"));
327 }
328 fileData = sourceDevice->readAll();
329 } else {
330 return makeResourceError(QAudioDecoder::tr("No audio source specified"));
331 }
332
333 if (fileData.isEmpty())
334 return makeResourceError(QAudioDecoder::tr("Audio file is empty"));
335
336 if (*decodingStopped)
337 return QAudioBuffer{};
338
339 QMetaObject::invokeMethod(this, [this, decodingStopped] {
340 if (!*decodingStopped)
341 setIsDecoding(true);
342 }, Qt::QueuedConnection);
343
344 return QtMultimediaPrivate::loadWaveAndDecodeData(
345 as_bytes(QSpan{ fileData.constData(), fileData.size() }), requestedFormat);
346}
347
348void QDrWavAudioDecoder::onDecodeFinished(QDrWavDecodeResult result,
349 const std::atomic_bool &decodingStopped)
350{
351 using namespace std::chrono;
352 if (!result.has_value()) {
353 auto [errorCode, errorString] = result.error();
354 error(errorCode, errorString);
355 setIsDecoding(false);
356 return;
357 }
358
359 m_buffer = std::move(*result);
360 // QAudioBuffer::duration() is in microseconds; convert to milliseconds
361 durationChanged(duration_cast<milliseconds>(microseconds{ m_buffer.duration() }));
362 positionChanged(0ms);
363
364 bufferAvailableChanged(true);
365 bufferReady();
366 if (decodingStopped)
367 return;
368
369 finished();
370}
371
372QT_END_NAMESPACE
void setSource(const QUrl &) override
void setAudioFormat(const QAudioFormat &) override
QAudioBuffer read() override
void setSourceDevice(QIODevice *) override
QDrWavAudioDecoder(QAudioDecoder *parent)
Combined button and popup list for selecting options.
static QDrWavDecodeResult makeResourceError(const QString &message)
static QDrWavDecodeResult makeFormatError(const QString &message)
QDrWavDecodeResult loadWaveAndDecodeData(QSpan< const std::byte > data, const QAudioFormat &requestedFormat)