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
qspatialaudiosound.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-3.0-only
3
5
6#include <QtMultimedia/qaudiosink.h>
7#include <QtMultimedia/private/qaudio_qspan_support_p.h>
8#include <QtMultimedia/private/qaudiohelpers_p.h>
9#include <QtMultimedia/private/qmultimediautils_p.h>
10#include <QtMultimedia/private/qmultimedia_ranges_p.h>
11#include <QtSpatialAudio/private/qaudioengine_p.h>
12#include <QtCore/qdebug.h>
13#include <QtCore/qurl.h>
14
15#include <resonance_audio.h>
16#include <memory>
17
18QT_BEGIN_NAMESPACE
19
20namespace QSpatialAudioPrivate {
21
22constexpr int InfiniteLoops = -1;
23
24QSpatialAudioPlaybackState::QSpatialAudioPlaybackState(QAudioBuffer buffer, bool playing, int loops)
26{
27}
28
29void QSpatialAudioPlaybackState::getBuffer(QSpan<float> output)
30{
31 using QtMultimediaPrivate::drop;
32 using QtMultimediaPrivate::take;
33 namespace ranges = QtMultimediaPrivate::ranges;
34
35 if (!m_playing.load(std::memory_order_relaxed)) {
36 ranges::fill(output, 0.f);
37 return;
38 }
39
40 QSpan<const float> wholeSampleBuffer{
41 m_buffer.constData<float>(),
42 m_buffer.sampleCount(),
43 };
44
45 while (!output.empty()) {
46 QSpan remainingSamples = drop(wholeSampleBuffer, m_currentSample);
47 const QSpan samplesToCopy = take(remainingSamples, output.size());
48 ranges::copy(samplesToCopy, output.begin());
49 output = drop(output, samplesToCopy.size());
50 m_currentSample += samplesToCopy.size();
51
52 if (output.empty())
53 return;
54
55 // Reached end of buffer
56 Q_ASSERT(m_currentSample == wholeSampleBuffer.size());
57 m_currentSample = 0;
58
59 switch (m_loops) {
60 case InfiniteLoops:
61 break; // Continue looping
62 case 0:
63 m_playing = false;
64 m_currentLoop = 0;
65 ranges::fill(output, 0.f);
66 return;
67 default:
68 ++m_currentLoop;
69 if (m_currentLoop >= m_loops) {
70 m_playing = false;
71 m_currentLoop = 0;
72 ranges::fill(output, 0.f);
73 return;
74 }
75 break;
76 }
77 }
78}
79
81{
82 m_playing = true;
83}
84
86{
87 m_loops = loops;
88}
89
91{
92 return m_buffer.format();
93}
94
96{
97 m_playing = false;
98}
99
100} // namespace QSpatialAudioPrivate
101
102QSpatialAudioSoundPrivate::QSpatialAudioSoundPrivate(QAudioEngine *engine, int nchannels, int sourceId)
103 : nchannels(nchannels), engine(engine), sourceId(sourceId)
104{
105 auto *ep = QAudioEnginePrivate::get(engine);
106 if (!ep)
107 return;
108 ep->addSound(this);
109}
110
112{
113 auto *ep = QAudioEnginePrivate::get(engine);
114 if (ep)
115 ep->removeSound(this);
116
117 withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
118 api->DestroySource(sourceId);
119 });
120}
121
123{
124 m_volume = volume;
126}
127
129{
130 m_loops = loops;
131 if (m_playbackState)
132 m_playbackState->setLoops(loops);
133}
134
136{
137 m_autoPlay = enabled;
138}
139
141{
142 withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
143 api->SetSourceVolume(sourceId, m_volume);
144 });
145}
146
148{
149 switch (m_state) {
150 case State::Stopped: {
151 m_state = State::Playing;
152 if (m_buffer) {
153 setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/true,
154 m_loops));
155 }
156 return;
157 }
158 case State::Paused:
159 case State::Playing: {
160 m_state = State::Playing;
161 if (m_playbackState)
162 m_playbackState->resume();
163 return;
164 }
165 }
166}
167
169{
170 switch (m_state) {
171 case State::Stopped: {
172 m_state = State::Paused;
173 setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/false,
174 m_loops));
175 return;
176 }
177 case State::Paused: {
178 return;
179 }
180 case State::Playing: {
181 m_state = State::Paused;
182 setState(nullptr);
183
184 if (m_playbackState)
185 m_playbackState->pause();
186 return;
187 }
188 }
189}
190
192{
193 switch (m_state) {
194 case State::Stopped: {
195 return;
196 }
197 case State::Paused:
198 case State::Playing: {
199 m_state = State::Stopped;
200 setState({});
201 return;
202 }
203 }
204}
205
206void QSpatialAudioSoundPrivate::loadUrl(const QUrl &url)
207{
208 m_url = url;
209
210 auto *ep = QAudioEnginePrivate::get(engine);
211 if (!ep)
212 return;
213
214 m_loadFuture.cancelChain();
215 m_sample = {};
216
217 setState(nullptr);
218
219 m_loadFuture = QSampleCache::instance()
220 ->requestSampleFuture(m_sourceResolver->resolve(url), ep->sampleRate())
221 .then(q_ptr, [this](SharedSamplePtr sample) {
222 if (!sample) {
223 qWarning() << "QAmbientSound: cannot load file";
224 return;
225 }
226
227 // Build QAudioBuffer with the correct channel layout for this source.
228 // QSampleCache resampled to engineRate but preserves native channels.
229 // Adapt channel count here: mono<->stereo simple mix.
230 const QAudioFormat &srcFmt = sample->format();
231 const int srcChannels = srcFmt.channelCount();
232
233 QByteArray pcmData;
234 QAudioFormat outFmt = srcFmt;
235 outFmt.setChannelCount(nchannels);
236 outFmt.setChannelConfig(nchannels == 2 ? QAudioFormat::ChannelConfigStereo
237 : QAudioFormat::ChannelConfigMono);
238
239 using namespace QAudioHelperInternal;
240
241 if (srcChannels == nchannels) {
242 pcmData = sample->data();
243
244 m_sample = std::move(sample); // keep the sample in the QSampleCache as long as we live
245 } else if (srcChannels == 1 && nchannels == 2) {
246 qWarning() << "QAmbientSound: upmixing mono source to stereo";
247 pcmData = upmixMonoToStereo(sample->dataAsFloatSpan(), UpmixScaling::Duplicate);
248 } else if (srcChannels == 2 && nchannels == 1) {
249 qWarning() << "QAmbientSound: downmixing stereo source to mono";
250 pcmData = downmixStereoToMono(sample->dataAsFloatSpan(), DownmixScaling::Average);
251 } else {
252 qWarning() << "QAmbientSound: unsupported channel count"
253 << srcChannels << "- rejecting file";
254 return;
255 }
256
257 m_buffer = QAudioBuffer{
258 pcmData,
259 outFmt,
260 };
261
262 const bool startingPlayback = m_autoPlay || m_state != State::Stopped;
263 if (!startingPlayback)
264 return;
265 const bool startPaused = (m_state == State::Paused) && !m_autoPlay;
266 setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/!startPaused,
267 m_loops));
268
269 m_state = startPaused ? State::Paused : State::Playing;
270 });
271}
272
273vraudio::ResonanceAudioApi *QSpatialAudioSoundPrivate::getAPI()
274{
275 auto *ep = QAudioEnginePrivate::get(engine);
276 if (!ep)
277 return nullptr;
278 return ep->resonanceAudio->api.get();
279}
280
281void QSpatialAudioSoundPrivate::setState(SharedPlaybackState state)
282{
283 auto *ep = QAudioEnginePrivate::get(engine);
284 if (ep)
285 ep->setSoundPlaybackData(this, state);
286 m_playbackState = std::move(state);
287}
288
289QT_END_NAMESPACE
QSpatialAudioPlaybackState(QAudioBuffer buffer, bool playing, int loops)
void loadUrl(const QUrl &url)
QSpatialAudioSoundPrivate(QAudioEngine *engine, int nchannels, int sourceId)