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 // Allocate PCM data for the entire file
126 QByteArray pcmData;
127 pcmData.resizeForOverwrite(wav.totalPCMFrameCount * bytesPerFrame);
128
129 // Read all frames at once using appropriate format
130 uint64_t framesRead = 0;
131
132 switch (outputFormat.sampleFormat()) {
133 case QAudioFormat::UInt8:
134 framesRead = drwav_read_pcm_frames(&wav, wav.totalPCMFrameCount,
135 reinterpret_cast<uint8_t *>(pcmData.data()));
136 break;
137 case QAudioFormat::Int16:
138 framesRead = drwav_read_pcm_frames_s16(&wav, wav.totalPCMFrameCount,
139 reinterpret_cast<int16_t *>(pcmData.data()));
140 break;
141 case QAudioFormat::Int32:
142 framesRead = drwav_read_pcm_frames_s32(&wav, wav.totalPCMFrameCount,
143 reinterpret_cast<int32_t *>(pcmData.data()));
144 break;
145 case QAudioFormat::Float:
146 framesRead = drwav_read_pcm_frames_f32(&wav, wav.totalPCMFrameCount,
147 reinterpret_cast<float *>(pcmData.data()));
148 break;
149 default:
150 return makeFormatError(QAudioDecoder::tr("Unsupported sample format"));
151 }
152
153 if (framesRead != wav.totalPCMFrameCount) {
154 qCDebug(qLcDrWavDecoder) << "Failed to read all frames:"
155 << "expected" << wav.totalPCMFrameCount << "got" << framesRead;
156 return makeFormatError(QAudioDecoder::tr("Unable to read audio data"));
157 }
158
159 QAudioBuffer buffer(pcmData, outputFormat);
160 if (!buffer.isValid()) {
161 return makeFormatError(QAudioDecoder::tr("Failed to create audio buffer"));
162 }
163
164 qCDebug(qLcDrWavDecoder) << "Successfully decoded:" << buffer.sampleCount() << "samples";
165
166 return buffer;
167}
168
169} // namespace QtMultimediaPrivate
170
171QDrWavAudioDecoder::QDrWavAudioDecoder(QAudioDecoder *parent)
173{
174}
175
176QDrWavAudioDecoder::~QDrWavAudioDecoder()
177{
178 stop();
179}
180
181void QDrWavAudioDecoder::setSource(const QUrl &fileName)
182{
183 stop();
184 m_sourceDevice = nullptr;
185 if (std::exchange(m_source, fileName) != fileName)
186 sourceChanged();
187}
188
189void QDrWavAudioDecoder::setSourceDevice(QIODevice *device)
190{
191 stop();
192 m_source.clear();
193 if (std::exchange(m_sourceDevice, device) != device)
194 sourceChanged();
195}
196
197void QDrWavAudioDecoder::setAudioFormat(const QAudioFormat &format)
198{
199 if (isDecoding())
200 return;
201 m_audioFormat = format;
202}
203
204
205void QDrWavAudioDecoder::start()
206{
207 if (isDecoding())
208 return;
209
210 if (m_source.isEmpty() && !m_sourceDevice) {
211 error(QAudioDecoder::ResourceError, tr("No audio source specified"));
212 return;
213 }
214
215 m_decodingStopped = std::make_shared<std::atomic_bool>(false);
216
217 auto threadPool = QThreadPool::globalInstance();
218 threadPool->start([source = m_source, sourceDevice = m_sourceDevice,
219 requestedFormat = m_audioFormat, decodingStopped = m_decodingStopped, this] {
220 auto state = loadAndDecodeFile(source, sourceDevice, requestedFormat, decodingStopped);
221
222 QMetaObject::invokeMethod(this,
223 [this, decodingStopped, state = std::move(state)]() mutable {
224 if (*decodingStopped) {
225 qCDebug(qLcDrWavDecoder) << "Decoding was stopped, discarding result";
226 return;
227 }
228 onDecodeFinished(std::move(state), *decodingStopped);
229 });
230 });
231}
232
233void QDrWavAudioDecoder::stop()
234{
235 if (!isDecoding())
236 return;
237
238 qCDebug(qLcDrWavDecoder) << "stop() called";
239
240 // Prevent the continuation from delivering the buffer
241 if (m_decodingStopped) {
242 *m_decodingStopped = true;
243 m_decodingStopped.reset();
244 }
245
246 m_buffer = QAudioBuffer();
247 finished();
248}
249
250QAudioBuffer QDrWavAudioDecoder::read()
251{
252 if (m_buffer.isValid()) {
253 QAudioBuffer buffer = std::exchange(m_buffer, QAudioBuffer());
254 positionChanged(duration());
255 bufferAvailableChanged(false);
256 return buffer;
257 }
258 return QAudioBuffer();
259}
260
261auto QDrWavAudioDecoder::loadAndDecodeFile(const QUrl &source, QIODevice *sourceDevice,
262 const QAudioFormat &requestedFormat,
263 std::shared_ptr<std::atomic_bool> decodingStopped)
264 -> QDrWavDecodeResult
265{
266 using namespace QtMultimediaPrivate;
267
268 QByteArray fileData;
269
270 // Load file data based on source type
271 if (!source.isEmpty()) {
272 QString scheme = source.scheme();
273
274 if (scheme.isEmpty() || scheme == u"file") {
275 // Local file
276 QString filePath = source.isLocalFile() ? source.toLocalFile()
277 : source.path();
278 QFile file(filePath);
279 if (!file.open(QFile::ReadOnly))
280 return makeResourceError(QAudioDecoder::tr("Cannot open audio file"));
281 fileData = file.readAll();
282 } else if (scheme == u"qrc") {
283 // QRC resource
284 QString filePath = u":" + source.toString(QUrl::RemoveScheme);
285 QFile file(filePath);
286 if (!file.open(QFile::ReadOnly))
287 return makeResourceError(QAudioDecoder::tr("Cannot open audio resource"));
288 fileData = file.readAll();
289 }
290#if QT_CONFIG(network)
291 else {
292 // Network URL
293 QNetworkAccessManager networkAccessManager;
294 QNetworkReply *reply = networkAccessManager.get(QNetworkRequest(source));
295
296 if (reply->error() != QNetworkReply::NoError) {
297 reply->deleteLater();
298 return makeResourceError(QAudioDecoder::tr("Failed to download audio file"));
299 }
300
301 // Wait for network reply to finish
302 QEventLoop loop;
303 QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
304 QObject::connect(reply, &QNetworkReply::errorOccurred, &loop, &QEventLoop::quit);
305
306 loop.exec();
307
308 if (reply->error() != QNetworkReply::NoError) {
309 reply->deleteLater();
310 return makeResourceError(QAudioDecoder::tr("Network error while downloading audio"));
311 }
312
313 if (*decodingStopped) {
314 reply->deleteLater();
315 return QAudioBuffer{};
316 }
317
318 fileData = reply->readAll();
319 reply->deleteLater();
320 }
321#endif
322 } else if (sourceDevice) {
323 if (!sourceDevice->isOpen()) {
324 if (!sourceDevice->open(QIODevice::ReadOnly))
325 return makeResourceError(QAudioDecoder::tr("Cannot open audio device"));
326 }
327 fileData = sourceDevice->readAll();
328 } else {
329 return makeResourceError(QAudioDecoder::tr("No audio source specified"));
330 }
331
332 if (fileData.isEmpty())
333 return makeResourceError(QAudioDecoder::tr("Audio file is empty"));
334
335 if (*decodingStopped)
336 return QAudioBuffer{};
337
338 QMetaObject::invokeMethod(this, [this, decodingStopped] {
339 if (!*decodingStopped)
340 setIsDecoding(true);
341 }, Qt::QueuedConnection);
342
343 return QtMultimediaPrivate::loadWaveAndDecodeData(
344 as_bytes(QSpan{ fileData.constData(), fileData.size() }), requestedFormat);
345}
346
347void QDrWavAudioDecoder::onDecodeFinished(QDrWavDecodeResult result,
348 const std::atomic_bool &decodingStopped)
349{
350 using namespace std::chrono;
351 if (!result.has_value()) {
352 auto [errorCode, errorString] = result.error();
353 error(errorCode, errorString);
354 setIsDecoding(false);
355 return;
356 }
357
358 m_buffer = std::move(*result);
359 // QAudioBuffer::duration() is in microseconds; convert to milliseconds
360 durationChanged(duration_cast<milliseconds>(microseconds{ m_buffer.duration() }));
361 positionChanged(0ms);
362
363 bufferAvailableChanged(true);
364 bufferReady();
365 if (decodingStopped)
366 return;
367
368 finished();
369}
370
371QT_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)