Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qdrawavaudiodecoder_p.h"
++
++ #include <QtMultimedia/qaudiodecoder.h>
++ #include <QtMultimedia/qaudiobuffer.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qeventloop.h>
++ #include <QtCore/qfile.h>
++ #include <QtCore/qiodevice.h>
++ #include <QtCore/qloggingcategory.h>
++ #include <QtCore/qthreadpool.h>
++
++ #if QT_CONFIG(network)
++ # include <QtNetwork/qnetworkaccessmanager.h>
++ # include <QtNetwork/qnetworkreply.h>
++ # include <QtNetwork/qnetworkrequest.h>
++ #endif
++
++ #include <dr_wav.h>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_STATIC_LOGGING_CATEGORY(qLcDrWavDecoder, "qt.multimedia.drwavdecoder")
++
++ namespace QtMultimediaPrivate {
++
++ static QDrWavDecodeResult makeFormatError(const QString &message)
++ {
++ return q23::unexpected(std::pair(QAudioDecoder::FormatError, message));
++ }
++
++ static QDrWavDecodeResult makeResourceError(const QString &message)
++ {
++ return q23::unexpected(std::pair(QAudioDecoder::ResourceError, message));
++ }
++
++ QDrWavDecodeResult loadWaveAndDecodeData(QSpan<const std::byte> data,
++ const QAudioFormat &requestedFormat)
++ {
++ using namespace QtPrivate;
++
++ drwav wav;
++ if (!drwav_init_memory(&wav, data.data(), data.size(), nullptr)) {
++ qCDebug(qLcDrWavDecoder) << "Failed to initialize dr_wav decoder";
++ return makeFormatError(QAudioDecoder::tr("Unable to decode audio file"));
++ }
++
++ auto cleanup = qScopeGuard([&] {
++ drwav_uninit(&wav);
++ });
++
++ // Determine output format
++ QAudioFormat outputFormat;
++ size_t bytesPerFrame;
++
++ if (requestedFormat.isValid()) {
++ // User set a format - check compatibility with file
++ if (requestedFormat.sampleRate() != static_cast<int>(wav.sampleRate))
++ return makeFormatError(
++ QAudioDecoder::tr("Audio file sample rate does not match requested format"));
++ if (requestedFormat.channelCount() != static_cast<int>(wav.channels))
++ return makeFormatError(
++ QAudioDecoder::tr("Audio file channel count does not match requested format"));
++ outputFormat = requestedFormat;
++
++ // Determine bytes per frame based on requested format
++ switch (requestedFormat.sampleFormat()) {
++ case QAudioFormat::UInt8:
++ bytesPerFrame = wav.channels * sizeof(uint8_t);
++ break;
++ case QAudioFormat::Int16:
++ bytesPerFrame = wav.channels * sizeof(int16_t);
++ break;
++ case QAudioFormat::Int32:
++ bytesPerFrame = wav.channels * sizeof(int32_t);
++ break;
++ case QAudioFormat::Float:
++ bytesPerFrame = wav.channels * sizeof(float);
++ break;
++ default:
++ return makeFormatError(QAudioDecoder::tr("Unsupported sample format"));
++ }
++ } else {
++ // Infer format from file header - use native format heuristic
++ QAudioFormat::SampleFormat sampleFormat;
++
++ switch (wav.bitsPerSample) {
++ case 8:
++ sampleFormat = QAudioFormat::UInt8;
++ bytesPerFrame = wav.channels * sizeof(uint8_t);
++ break;
++ case 16:
++ sampleFormat = QAudioFormat::Int16;
++ bytesPerFrame = wav.channels * sizeof(int16_t);
++ break;
++ case 24: // 24-bit → read as Int32
++ sampleFormat = QAudioFormat::Int32;
++ bytesPerFrame = wav.channels * sizeof(int32_t);
++ break;
++ case 32:
++ sampleFormat = QAudioFormat::Int32;
++ bytesPerFrame = wav.channels * sizeof(int32_t);
++ break;
++ default: // unsupported → fallback Float
++ sampleFormat = QAudioFormat::Float;
++ bytesPerFrame = wav.channels * sizeof(float);
++ break;
++ }
++
++ outputFormat.setChannelCount(wav.channels);
++ outputFormat.setSampleFormat(sampleFormat);
++ outputFormat.setSampleRate(wav.sampleRate);
++ outputFormat.setChannelConfig(
++ QAudioFormat::defaultChannelConfigForChannelCount(wav.channels));
++ }
++
++ qCDebug(qLcDrWavDecoder) << "Decoded WAV:"
++ << "channels=" << wav.channels << "sampleRate=" << wav.sampleRate
++ << "bitsPerSample=" << wav.bitsPerSample
++ << "totalPCMFrameCount=" << wav.totalPCMFrameCount
++ << "outputFormat=" << outputFormat.sampleFormat();
++
++ const QAudioFormat::SampleFormat readFormat = outputFormat.sampleFormat();
++ switch (readFormat) {
++ case QAudioFormat::UInt8:
++ case QAudioFormat::Int16:
++ case QAudioFormat::Int32:
++ case QAudioFormat::Float:
++ break;
++ default:
++ return makeFormatError(QAudioDecoder::tr("Unsupported sample format"));
++ }
++
++ std::optional<QByteArray> decoded = readFramesInChunks(
++ wav.totalPCMFrameCount, bytesPerFrame, [&](uint64_t frameCount, char *out) -> uint64_t {
++ switch (readFormat) {
++ case QAudioFormat::UInt8:
++ return drwav_read_pcm_frames(&wav, frameCount, out);
++ case QAudioFormat::Int16:
++ return drwav_read_pcm_frames_s16(&wav, frameCount, reinterpret_cast<int16_t *>(out));
++ case QAudioFormat::Int32:
++ return drwav_read_pcm_frames_s32(&wav, frameCount, reinterpret_cast<int32_t *>(out));
++ case QAudioFormat::Float:
++ return drwav_read_pcm_frames_f32(&wav, frameCount, reinterpret_cast<float *>(out));
++ default:
++ Q_UNREACHABLE_RETURN(0);
++ }
++ });
++
++ if (!decoded) {
++ qCDebug(qLcDrWavDecoder) << "Failed to read all frames:"
++ << "expected" << wav.totalPCMFrameCount;
++ return makeFormatError(QAudioDecoder::tr("Unable to read audio data"));
++ }
++
++ const QByteArray &pcmData = *decoded;
++
++ QAudioBuffer buffer(pcmData, outputFormat);
++ if (!buffer.isValid()) {
++ return makeFormatError(QAudioDecoder::tr("Failed to create audio buffer"));
++ }
++
++ qCDebug(qLcDrWavDecoder) << "Successfully decoded:" << buffer.sampleCount() << "samples";
++
++ return buffer;
++ }
++
++ } // namespace QtMultimediaPrivate
++
++ QDrWavAudioDecoder::QDrWavAudioDecoder(QAudioDecoder *parent)
++ : QPlatformAudioDecoder(parent)
++ {
++ }
++
++ QDrWavAudioDecoder::~QDrWavAudioDecoder()
++ {
++ stop();
++ }
++
++ void QDrWavAudioDecoder::setSource(const QUrl &fileName)
++ {
++ stop();
++ m_sourceDevice = nullptr;
++ if (std::exchange(m_source, fileName) != fileName)
++ sourceChanged();
++ }
++
++ void QDrWavAudioDecoder::setSourceDevice(QIODevice *device)
++ {
++ stop();
++ m_source.clear();
++ if (std::exchange(m_sourceDevice, device) != device)
++ sourceChanged();
++ }
++
++ void QDrWavAudioDecoder::setAudioFormat(const QAudioFormat &format)
++ {
++ if (isDecoding())
++ return;
++ m_audioFormat = format;
++ }
++
++
++ void QDrWavAudioDecoder::start()
++ {
++ if (isDecoding())
++ return;
++
++ if (m_source.isEmpty() && !m_sourceDevice) {
++ error(QAudioDecoder::ResourceError, tr("No audio source specified"));
++ return;
++ }
++
++ m_decodingStopped = std::make_shared<std::atomic_bool>(false);
++
++ auto threadPool = QThreadPool::globalInstance();
++ threadPool->start([source = m_source, sourceDevice = m_sourceDevice,
++ requestedFormat = m_audioFormat, decodingStopped = m_decodingStopped, this] {
++ auto state = loadAndDecodeFile(source, sourceDevice, requestedFormat, decodingStopped);
++
++ QMetaObject::invokeMethod(this,
++ [this, decodingStopped, state = std::move(state)]() mutable {
++ if (*decodingStopped) {
++ qCDebug(qLcDrWavDecoder) << "Decoding was stopped, discarding result";
++ return;
++ }
++ onDecodeFinished(std::move(state), *decodingStopped);
++ });
++ });
++ }
++
++ void QDrWavAudioDecoder::stop()
++ {
++ if (!isDecoding())
++ return;
++
++ qCDebug(qLcDrWavDecoder) << "stop() called";
++
++ // Prevent the continuation from delivering the buffer
++ if (m_decodingStopped) {
++ *m_decodingStopped = true;
++ m_decodingStopped.reset();
++ }
++
++ m_buffer = QAudioBuffer();
++ finished();
++ }
++
++ QAudioBuffer QDrWavAudioDecoder::read()
++ {
++ if (m_buffer.isValid()) {
++ QAudioBuffer buffer = std::exchange(m_buffer, QAudioBuffer());
++ positionChanged(duration());
++ bufferAvailableChanged(false);
++ return buffer;
++ }
++ return QAudioBuffer();
++ }
++
++ auto QDrWavAudioDecoder::loadAndDecodeFile(const QUrl &source, QIODevice *sourceDevice,
++ const QAudioFormat &requestedFormat,
++ std::shared_ptr<std::atomic_bool> decodingStopped)
++ -> QDrWavDecodeResult
++ {
++ using namespace QtMultimediaPrivate;
++
++ QByteArray fileData;
++
++ // Load file data based on source type
++ if (!source.isEmpty()) {
++ QString scheme = source.scheme();
++
++ if (scheme.isEmpty() || scheme == u"file") {
++ // Local file
++ QString filePath = source.isLocalFile() ? source.toLocalFile()
++ : source.path();
++ QFile file(filePath);
++ if (!file.open(QFile::ReadOnly))
++ return makeResourceError(QAudioDecoder::tr("Cannot open audio file"));
++ fileData = file.readAll();
++ } else if (scheme == u"qrc") {
++ // QRC resource
++ QString filePath = u":" + source.toString(QUrl::RemoveScheme);
++ QFile file(filePath);
++ if (!file.open(QFile::ReadOnly))
++ return makeResourceError(QAudioDecoder::tr("Cannot open audio resource"));
++ fileData = file.readAll();
++ }
++ #if QT_CONFIG(network)
++ else {
++ // Network URL
++ QNetworkAccessManager networkAccessManager;
++ QNetworkReply *reply = networkAccessManager.get(QNetworkRequest(source));
++
++ if (reply->error() != QNetworkReply::NoError) {
++ reply->deleteLater();
++ return makeResourceError(QAudioDecoder::tr("Failed to download audio file"));
++ }
++
++ // Wait for network reply to finish
++ QEventLoop loop;
++ QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
++ QObject::connect(reply, &QNetworkReply::errorOccurred, &loop, &QEventLoop::quit);
++
++ loop.exec();
++
++ if (reply->error() != QNetworkReply::NoError) {
++ reply->deleteLater();
++ return makeResourceError(QAudioDecoder::tr("Network error while downloading audio"));
++ }
++
++ if (*decodingStopped) {
++ reply->deleteLater();
++ return QAudioBuffer{};
++ }
++
++ fileData = reply->readAll();
++ reply->deleteLater();
++ }
++ #endif
++ } else if (sourceDevice) {
++ if (!sourceDevice->isOpen()) {
++ if (!sourceDevice->open(QIODevice::ReadOnly))
++ return makeResourceError(QAudioDecoder::tr("Cannot open audio device"));
++ }
++ fileData = sourceDevice->readAll();
++ } else {
++ return makeResourceError(QAudioDecoder::tr("No audio source specified"));
++ }
++
++ if (fileData.isEmpty())
++ return makeResourceError(QAudioDecoder::tr("Audio file is empty"));
++
++ if (*decodingStopped)
++ return QAudioBuffer{};
++
++ QMetaObject::invokeMethod(this, [this, decodingStopped] {
++ if (!*decodingStopped)
++ setIsDecoding(true);
++ }, Qt::QueuedConnection);
++
++ return QtMultimediaPrivate::loadWaveAndDecodeData(
++ as_bytes(QSpan{ fileData.constData(), fileData.size() }), requestedFormat);
++ }
++
++ void QDrWavAudioDecoder::onDecodeFinished(QDrWavDecodeResult result,
++ const std::atomic_bool &decodingStopped)
++ {
++ using namespace std::chrono;
++ if (!result.has_value()) {
++ auto [errorCode, errorString] = result.error();
++ error(errorCode, errorString);
++ setIsDecoding(false);
++ return;
++ }
++
++ m_buffer = std::move(*result);
++ // QAudioBuffer::duration() is in microseconds; convert to milliseconds
++ durationChanged(duration_cast<milliseconds>(microseconds{ m_buffer.duration() }));
++ positionChanged(0ms);
++
++ bufferAvailableChanged(true);
++ bufferReady();
++ if (decodingStopped)
++ return;
++
++ finished();
++ }
++
++ QT_END_NAMESPACE