Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
++
++ #include "qspatialaudiosound_p.h"
++
++ #include <QtSpatialAudio/private/qaudioengine_p.h>
++ #include <QtMultimedia/qaudiosink.h>
++ #include <QtMultimedia/private/qaudio_qspan_support_p.h>
++ #include <QtMultimedia/private/qaudiohelpers_p.h>
++ #include <QtMultimedia/private/qmultimedia_ranges_p.h>
++ #include <QtMultimedia/private/qmultimediautils_p.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qurl.h>
++
++ #include <resonance_audio.h>
++
++ #include <memory>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QSpatialAudioPrivate {
++
++ constexpr int InfiniteLoops = -1;
++
++ QSpatialAudioPlaybackState::QSpatialAudioPlaybackState(QAudioBuffer buffer, bool playing, int loops)
++ : m_playing(playing), m_loops(loops), m_buffer(std::move(buffer))
++ {
++ }
++
++ void QSpatialAudioPlaybackState::getBuffer(QSpan<float> output)
++ {
++ using QtMultimediaPrivate::drop;
++ using QtMultimediaPrivate::take;
++ namespace ranges = QtMultimediaPrivate::ranges;
++
++ if (!m_playing.load(std::memory_order_relaxed)) {
++ ranges::fill(output, 0.f);
++ return;
++ }
++
++ QSpan<const float> wholeSampleBuffer{
++ m_buffer.constData<float>(),
++ m_buffer.sampleCount(),
++ };
++
++ while (!output.empty()) {
++ QSpan remainingSamples = drop(wholeSampleBuffer, m_currentSample);
++ const QSpan samplesToCopy = take(remainingSamples, output.size());
++ ranges::copy(samplesToCopy, output.begin());
++ output = drop(output, samplesToCopy.size());
++ m_currentSample += samplesToCopy.size();
++
++ if (output.empty())
++ return;
++
++ // Reached end of buffer
++ Q_ASSERT(m_currentSample == wholeSampleBuffer.size());
++ m_currentSample = 0;
++
++ switch (m_loops) {
++ case InfiniteLoops:
++ break; // Continue looping
++ case 0:
++ m_playing = false;
++ m_currentLoop = 0;
++ ranges::fill(output, 0.f);
++ return;
++ default:
++ ++m_currentLoop;
++ if (m_currentLoop >= m_loops) {
++ m_playing = false;
++ m_currentLoop = 0;
++ ranges::fill(output, 0.f);
++ return;
++ }
++ break;
++ }
++ }
++ }
++
++ void QSpatialAudioPlaybackState::resume()
++ {
++ m_playing = true;
++ }
++
++ void QSpatialAudioPlaybackState::setLoops(int loops)
++ {
++ m_loops = loops;
++ }
++
++ QAudioFormat QSpatialAudioPlaybackState::format() const
++ {
++ return m_buffer.format();
++ }
++
++ void QSpatialAudioPlaybackState::pause()
++ {
++ m_playing = false;
++ }
++
++ } // namespace QSpatialAudioPrivate
++
++ QSpatialAudioSoundPrivate::QSpatialAudioSoundPrivate(QAudioEngine *engine, int nchannels, int sourceId)
++ : nchannels(nchannels), engine(engine), sourceId(sourceId)
++ {
++ auto *ep = QAudioEnginePrivate::get(engine);
++ if (!ep)
++ return;
++ ep->addSound(this);
++ }
++
++ QSpatialAudioSoundPrivate::~QSpatialAudioSoundPrivate()
++ {
++ auto *ep = QAudioEnginePrivate::get(engine);
++ if (ep)
++ ep->removeSound(this);
++
++ withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
++ api->DestroySource(sourceId);
++ });
++ }
++
++ void QSpatialAudioSoundPrivate::setVolume(float volume)
++ {
++ m_volume = volume;
++ applyVolume();
++ }
++
++ void QSpatialAudioSoundPrivate::setLoops(int loops)
++ {
++ m_loops = loops;
++ if (m_playbackState)
++ m_playbackState->setLoops(loops);
++ }
++
++ void QSpatialAudioSoundPrivate::setAutoPlay(bool enabled)
++ {
++ m_autoPlay = enabled;
++ }
++
++ void QSpatialAudioSoundPrivate::applyVolume()
++ {
++ withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
++ api->SetSourceVolume(sourceId, m_volume);
++ });
++ }
++
++ void QSpatialAudioSoundPrivate::play()
++ {
++ qCDebug(qLcSpatialAudioEngine) << "Playing sound";
++ switch (m_state) {
++ case State::Stopped: {
++ m_state = State::Playing;
++ if (m_buffer) {
++ setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/true,
++ m_loops));
++ }
++ return;
++ }
++ case State::Paused:
++ case State::Playing: {
++ m_state = State::Playing;
++ if (m_playbackState)
++ m_playbackState->resume();
++ return;
++ }
++ }
++ }
++
++ void QSpatialAudioSoundPrivate::pause()
++ {
++ qCDebug(qLcSpatialAudioEngine) << "Pausing sound";
++ switch (m_state) {
++ case State::Stopped: {
++ m_state = State::Paused;
++ setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/false,
++ m_loops));
++ return;
++ }
++ case State::Paused: {
++ return;
++ }
++ case State::Playing: {
++ m_state = State::Paused;
++ setState(nullptr);
++
++ if (m_playbackState)
++ m_playbackState->pause();
++ return;
++ }
++ }
++ }
++
++ void QSpatialAudioSoundPrivate::stop()
++ {
++ qCDebug(qLcSpatialAudioEngine) << "Stopping sound";
++ switch (m_state) {
++ case State::Stopped: {
++ return;
++ }
++ case State::Paused:
++ case State::Playing: {
++ m_state = State::Stopped;
++ setState({});
++ return;
++ }
++ }
++ }
++
++ void QSpatialAudioSoundPrivate::loadUrl(const QUrl &url)
++ {
++ qCDebug(qLcSpatialAudioEngine) << "Loading sound:" << url;
++ m_url = url;
++
++ auto *ep = QAudioEnginePrivate::get(engine);
++ if (!ep)
++ return;
++
++ m_loadFuture.cancelChain();
++ m_sample = {};
++
++ setState(nullptr);
++
++ m_loadFuture = QSampleCache::instance()
++ ->requestSampleFuture(m_sourceResolver->resolve(url), ep->sampleRate())
++ .then(q_ptr, [this](SharedSamplePtr sample) {
++ if (!sample) {
++ qCWarning(qLcSpatialAudioEngine) << "QAmbientSound: cannot load file";
++ return;
++ }
++
++ // Build QAudioBuffer with the correct channel layout for this source.
++ // QSampleCache resampled to engineRate but preserves native channels.
++ // Adapt channel count here: mono<->stereo simple mix.
++ const QAudioFormat &srcFmt = sample->format();
++ const int srcChannels = srcFmt.channelCount();
++
++ QByteArray pcmData;
++ QAudioFormat outFmt = srcFmt;
++ outFmt.setChannelCount(nchannels);
++ outFmt.setChannelConfig(nchannels == 2 ? QAudioFormat::ChannelConfigStereo
++ : QAudioFormat::ChannelConfigMono);
++
++ using namespace QAudioHelperInternal;
++
++ if (srcChannels == nchannels) {
++ pcmData = sample->data();
++
++ m_sample = std::move(sample); // keep the sample in the QSampleCache as long as we live
++ } else if (srcChannels == 1 && nchannels == 2) {
++ qWarning() << "QAmbientSound: upmixing mono source to stereo";
++ pcmData = upmixMonoToStereo(sample->dataAsFloatSpan(), UpmixScaling::Duplicate);
++ } else if (srcChannels == 2 && nchannels == 1) {
++ qWarning() << "QAmbientSound: downmixing stereo source to mono";
++ pcmData = downmixStereoToMono(sample->dataAsFloatSpan(), DownmixScaling::Average);
++ } else {
++ qWarning() << "QAmbientSound: unsupported channel count"
++ << srcChannels << "- rejecting file";
++ return;
++ }
++
++ m_buffer = QAudioBuffer{
++ pcmData,
++ outFmt,
++ };
++
++ qCDebug(qLcSpatialAudioEngine) << "Sound loaded";
++
++ const bool startingPlayback = m_autoPlay || m_state != State::Stopped;
++ if (!startingPlayback)
++ return;
++ const bool startPaused = (m_state == State::Paused) && !m_autoPlay;
++ setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/!startPaused,
++ m_loops));
++
++ m_state = startPaused ? State::Paused : State::Playing;
++ qCDebug(qLcSpatialAudioEngine)
++ << "Sound playback started:" << (startPaused ? "paused" : "playing");
++ });
++ }
++
++ vraudio::ResonanceAudioApi *QSpatialAudioSoundPrivate::getAPI()
++ {
++ auto *ep = QAudioEnginePrivate::get(engine);
++ if (!ep)
++ return nullptr;
++ return ep->resonanceAudio->api.get();
++ }
++
++ void QSpatialAudioSoundPrivate::setState(SharedPlaybackState state)
++ {
++ auto *ep = QAudioEnginePrivate::get(engine);
++ if (ep)
++ ep->setSoundPlaybackData(this, state);
++ m_playbackState = std::move(state);
++ }
++
++ QT_END_NAMESPACE