Revision control

++ // Copyright (C) 2016 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 "qsoundeffectsynchronous_p.h"
++
++ #include <QtMultimedia/private/qaudiosystem_p.h>
++ #include <QtMultimedia/private/qplatformaudiodevices_p.h>
++ #include <QtMultimedia/private/qplatformaudioresampler_p.h>
++ #include <QtMultimedia/private/qplatformmediaintegration_p.h>
++ #include <QtMultimedia/qmediadevices.h>
++
++ #ifdef Q_OS_WIN
++ # include <QtMultimedia/private/qwindows_wasapi_warmup_client_p.h>
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ void QSoundEffectPrivateSynchronous::AudioSinkDeleter::operator()(QAudioSink *sink) const
++ {
++ sink->stop();
++ // Investigate:should we just delete?
++ sink->deleteLater();
++ }
++
++ QSoundEffectPrivateSynchronous::QSoundEffectPrivateSynchronous(QSoundEffect *q,
++ const QAudioDevice &audioDevice)
++ : q_ptr(q), m_audioDevice(audioDevice)
++ {
++ open(QIODevice::ReadOnly);
++ }
++
++ QSoundEffectPrivateSynchronous::~QSoundEffectPrivateSynchronous()
++ {
++ m_audioSink.reset();
++ m_sample.reset();
++ }
++
++ qint64 QSoundEffectPrivateSynchronous::readData(char *data, qint64 len)
++ {
++ qCDebug(qLcSoundEffect) << this << "readData" << len << m_runningCount;
++ if (!len)
++ return 0;
++ if (m_sample->state() != QSample::Ready)
++ return 0;
++ if (m_runningCount == 0 || !m_playing)
++ return 0;
++
++ qint64 bytesWritten = 0;
++
++ const int sampleSize = m_audioBuffer.byteCount();
++ const char *sampleData = m_audioBuffer.constData<char>();
++
++ while (len && m_runningCount) {
++ int toWrite = qMax(0, int(qMin(qint64(sampleSize - m_offset), len)));
++ memcpy(data, sampleData + m_offset, toWrite);
++ bytesWritten += toWrite;
++ data += toWrite;
++ len -= toWrite;
++ m_offset += toWrite;
++ if (m_offset >= sampleSize) {
++ if (m_runningCount > 0)
++ setLoopsRemaining(m_runningCount - 1);
++ m_offset = 0;
++ }
++ }
++
++ return bytesWritten;
++ }
++
++ qint64 QSoundEffectPrivateSynchronous::writeData(const char *data, qint64 len)
++ {
++ Q_UNUSED(data);
++ Q_UNUSED(len);
++ return 0;
++ }
++
++ qint64 QSoundEffectPrivateSynchronous::size() const
++ {
++ if (m_sample->state() != QSample::Ready)
++ return 0;
++ return m_loopCount == QSoundEffect::Infinite ? 0 : m_loopCount * m_audioBuffer.byteCount();
++ }
++
++ qint64 QSoundEffectPrivateSynchronous::bytesAvailable() const
++ {
++ if (m_sample->state() != QSample::Ready)
++ return 0;
++ if (m_loopCount == QSoundEffect::Infinite)
++ return std::numeric_limits<qint64>::max();
++ return m_runningCount * m_audioBuffer.byteCount() - m_offset;
++ }
++
++ bool QSoundEffectPrivateSynchronous::isSequential() const
++ {
++ return m_loopCount == QSoundEffect::Infinite;
++ }
++
++ bool QSoundEffectPrivateSynchronous::atEnd() const
++ {
++ return m_runningCount == 0;
++ }
++
++ void QSoundEffectPrivateSynchronous::setLoopsRemaining(int loopsRemaining)
++ {
++ if (m_runningCount == loopsRemaining)
++ return;
++ qCDebug(qLcSoundEffect) << this << "setLoopsRemaining " << loopsRemaining;
++ m_runningCount = loopsRemaining;
++ Q_EMIT q_ptr->loopsRemainingChanged();
++ }
++
++ void QSoundEffectPrivateSynchronous::setStatus(QSoundEffect::Status status)
++ {
++ qCDebug(qLcSoundEffect) << this << "setStatus" << status;
++ if (m_status == status)
++ return;
++ bool oldLoaded = q_ptr->isLoaded();
++ m_status = status;
++ Q_EMIT q_ptr->statusChanged();
++ if (oldLoaded != q_ptr->isLoaded())
++ Q_EMIT q_ptr->loadedChanged();
++ }
++
++ void QSoundEffectPrivateSynchronous::setPlaying(bool playing)
++ {
++ qCDebug(qLcSoundEffect) << this << "setPlaying(" << playing << ")" << m_playing;
++ if (m_audioSink) {
++ m_audioSink->reset();
++ if (playing && !m_sampleReady)
++ return;
++ }
++
++ if (m_playing == playing)
++ return;
++ m_playing = playing;
++
++ if (m_audioSink && playing) {
++ m_audioSink->start(this);
++ #ifdef Q_OS_WIN
++ QtMultimediaPrivate::refreshWarmupClient();
++ #endif
++ }
++
++ Q_EMIT q_ptr->playingChanged();
++ }
++
++ bool QSoundEffectPrivateSynchronous::updateAudioOutput()
++ {
++ const auto audioDevice =
++ m_audioDevice.isNull() ? QMediaDevices::defaultAudioOutput() : m_audioDevice;
++
++ if (audioDevice.isNull()) {
++ // We are likely on a virtual machine, for example in CI
++ qCCritical(qLcSoundEffect) << "Failed to update audio output. No audio devices available.";
++ setStatus(QSoundEffect::Error);
++ return false;
++ }
++
++ if (m_audioDevice.isNull()) {
++ q_ptr->setAudioDevice(audioDevice); // Updates m_audioDevice and emits audioDeviceChanged
++ }
++
++ m_audioBuffer = {};
++
++ Q_ASSERT(m_sample);
++
++ const auto &sampleFormat = m_sample->format();
++ const auto sampleChannelConfig =
++ sampleFormat.channelConfig() == QAudioFormat::ChannelConfigUnknown
++ ? QAudioFormat::defaultChannelConfigForChannelCount(sampleFormat.channelCount())
++ : sampleFormat.channelConfig();
++
++ if (sampleChannelConfig != audioDevice.channelConfiguration()
++ && audioDevice.channelConfiguration() != QAudioFormat::ChannelConfigUnknown) {
++ qCDebug(qLcSoundEffect) << "Create resampler for channels mapping: config"
++ << sampleFormat.channelConfig() << "=> config"
++ << audioDevice.channelConfiguration();
++ auto outputFormat = sampleFormat;
++ outputFormat.setChannelConfig(audioDevice.channelConfiguration());
++
++ const auto resampler = QPlatformMediaIntegration::instance()->createAudioResampler(
++ m_sample->format(), outputFormat);
++ if (resampler)
++ m_audioBuffer = resampler.value()->resample(m_sample->data().constData(),
++ m_sample->data().size());
++ else
++ qCDebug(qLcSoundEffect) << "Cannot create resampler for channels mapping";
++ }
++
++ if (!m_audioBuffer.isValid())
++ m_audioBuffer = QAudioBuffer(m_sample->data(), m_sample->format());
++
++ m_audioSink.reset(new QAudioSink(audioDevice, m_audioBuffer.format()));
++
++ QObject::connect(m_audioSink.get(), &QAudioSink::stateChanged, this,
++ [this](QAudio::State state) {
++ this->stateChanged(state);
++ });
++
++ if (!m_muted)
++ m_audioSink->setVolume(m_volume);
++ else
++ m_audioSink->setVolume(0);
++
++ QPlatformAudioSink *sinkPrivate = QPlatformAudioSink::get(*m_audioSink);
++ sinkPrivate->setRole(QtMultimediaPrivate::AudioEndpointRole::SoundEffect);
++
++ return true;
++ }
++
++ void QSoundEffectPrivateSynchronous::decoderError()
++ {
++ qWarning("QSoundEffect: Error decoding source %ls", qUtf16Printable(m_url.toString()));
++
++ m_playing = false;
++ setStatus(QSoundEffect::Error);
++ }
++
++ void QSoundEffectPrivateSynchronous::sampleReady(SharedSamplePtr sample)
++ {
++ if (m_status == QSoundEffect::Error)
++ return;
++
++ m_sample = std::move(sample);
++
++ qCDebug(qLcSoundEffect) << this << "sampleReady: sample size:" << m_sample->data().size();
++ if (!m_audioSink) {
++ if (!updateAudioOutput()) // Create audio sink
++ return; // Returns if no audio devices are available
++ }
++
++ m_sampleReady = true;
++ setStatus(QSoundEffect::Ready);
++
++ if (m_playing && m_audioSink->state() == QAudio::StoppedState) {
++ qCDebug(qLcSoundEffect) << this << "starting playback on audiooutput";
++ m_audioSink->start(this);
++ }
++ }
++
++ int QSoundEffectPrivateSynchronous::loopCount() const
++ {
++ return m_loopCount;
++ }
++
++ bool QSoundEffectPrivateSynchronous::setLoopCount(int loopCount)
++ {
++ if (loopCount == 0)
++ loopCount = 1;
++
++ if (loopCount == m_loopCount)
++ return false;
++
++ m_loopCount = loopCount;
++ if (m_playing)
++ setLoopsRemaining(loopCount);
++ return true;
++ }
++
++ int QSoundEffectPrivateSynchronous::loopsRemaining() const
++ {
++ return m_runningCount;
++ }
++
++ float QSoundEffectPrivateSynchronous::volume() const
++ {
++ if (m_audioSink && !m_muted)
++ return m_audioSink->volume();
++
++ return m_volume;
++ }
++
++ bool QSoundEffectPrivateSynchronous::setVolume(float volume)
++ {
++ volume = qBound(0.0f, volume, 1.0f);
++ if (m_volume == volume)
++ return false;
++
++ m_volume = volume;
++
++ if (m_audioSink && !m_muted)
++ m_audioSink->setVolume(volume);
++ return true;
++ }
++
++ bool QSoundEffectPrivateSynchronous::muted() const
++ {
++ return m_muted;
++ }
++
++ bool QSoundEffectPrivateSynchronous::setMuted(bool muted)
++ {
++ if (m_muted == muted)
++ return false;
++
++ if (muted && m_audioSink)
++ m_audioSink->setVolume(0);
++ else if (!muted && m_audioSink && m_muted)
++ m_audioSink->setVolume(m_volume);
++
++ m_muted = muted;
++ return true;
++ }
++
++ void QSoundEffectPrivateSynchronous::play()
++ {
++ m_offset = 0;
++ setLoopsRemaining(m_loopCount);
++ qCDebug(qLcSoundEffect) << this << "play" << m_loopCount << m_runningCount;
++ if (m_status == QSoundEffect::Null || m_status == QSoundEffect::Error) {
++ setStatus(QSoundEffect::Null);
++ return;
++ }
++ setPlaying(true);
++ }
++
++ void QSoundEffectPrivateSynchronous::stop()
++ {
++ if (!m_playing)
++ return;
++ qCDebug(qLcSoundEffect) << "stop()";
++ m_offset = 0;
++ setPlaying(false);
++ }
++
++ bool QSoundEffectPrivateSynchronous::playing() const
++ {
++ return m_playing;
++ }
++
++ bool QSoundEffectPrivateSynchronous::setAudioDevice(QAudioDevice device)
++ {
++ if (m_audioDevice == device)
++ return false;
++
++ m_audioDevice = device;
++
++ if (!m_sampleReady)
++ return true; // The audio sink will be recreated later by QSoundEffect::sampleReady()
++
++ bool playing = m_playing;
++ std::chrono::microseconds current_time{ m_audioBuffer.format().durationForBytes(qint32(m_offset)) };
++
++ // Recreate the QAudioSink with the new audio device and current sample
++ if (updateAudioOutput() && playing) {
++ // Resume playback from current position
++ m_offset = m_audioBuffer.format().bytesForDuration(current_time.count());
++ setPlaying(true);
++ }
++ return true;
++ }
++
++ void QSoundEffectPrivateSynchronous::setSource(QUrl url, QSampleCache &sampleCache)
++ {
++ if (m_sampleLoadFuture.isValid())
++ m_sampleLoadFuture.cancel();
++
++ m_url = url;
++ m_sampleReady = false;
++
++ if (url.isEmpty()) {
++ setStatus(QSoundEffect::Null);
++ return;
++ }
++
++ if (!url.isValid()) {
++ setStatus(QSoundEffect::Error);
++ return;
++ }
++
++ setStatus(QSoundEffect::Loading);
++ m_sample = {};
++
++ if (m_audioSink) {
++ QObject::disconnect(m_audioSink.get(), &QAudioSink::stateChanged, this,
++ &QSoundEffectPrivateSynchronous::stateChanged);
++ m_audioSink.reset();
++ }
++
++ m_sampleLoadFuture =
++ sampleCache.requestSampleFuture(url).then(this, [this](SharedSamplePtr result) {
++ if (result)
++ sampleReady(std::move(result));
++ else
++ decoderError();
++ });
++
++ return;
++ }
++
++ QSoundEffect::Status QSoundEffectPrivateSynchronous::status() const
++ {
++ return m_status;
++ }
++
++ QAudioDevice QSoundEffectPrivateSynchronous::audioDevice() const
++ {
++ return m_audioDevice;
++ }
++
++ void QSoundEffectPrivateSynchronous::stateChanged(QAudio::State state)
++ {
++ qCDebug(qLcSoundEffect) << this << "stateChanged " << state;
++ if ((state == QAudio::IdleState && m_runningCount == 0) || state == QAudio::StoppedState)
++ stop();
++ }
++
++ QT_END_NAMESPACE