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
qambientsound.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
3
6
7#include <QtMultimedia/qaudiodecoder.h>
8#include <QtMultimedia/qaudiosink.h>
9#include <QtMultimedia/private/qaudio_qspan_support_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/qfile.h>
14#include <QtCore/qurl.h>
15
16#include <resonance_audio.h>
17#include <memory>
18
19QT_BEGIN_NAMESPACE
20
22
23QSpatialAudioPlaybackState::QSpatialAudioPlaybackState(QAudioBuffer buffer, bool playing, int loops)
25{
26}
27
28void QSpatialAudioPlaybackState::getBuffer(QSpan<float> output)
29{
30 using QtMultimediaPrivate::drop;
31 using QtMultimediaPrivate::take;
32 namespace ranges = QtMultimediaPrivate::ranges;
33
34 if (!m_playing.load(std::memory_order_relaxed)) {
35 ranges::fill(output, 0.f);
36 return;
37 }
38
39 QSpan<const float> wholeSampleBuffer{
40 m_buffer.constData<float>(),
41 m_buffer.sampleCount(),
42 };
43
44 while (!output.empty()) {
45 QSpan remainingSamples = drop(wholeSampleBuffer, m_currentSample);
46 const QSpan samplesToCopy = take(remainingSamples, output.size());
47 ranges::copy(samplesToCopy, output.begin());
48 output = drop(output, samplesToCopy.size());
49 m_currentSample += samplesToCopy.size();
50
51 if (output.empty())
52 return;
53
54 // Reached end of buffer
55 Q_ASSERT(m_currentSample == wholeSampleBuffer.size());
56 m_currentSample = 0;
57
58 switch (m_loops) {
59 case QAmbientSound::Infinite:
60 break; // Continue looping
61 case 0:
62 m_playing = false;
63 m_currentLoop = 0;
64 ranges::fill(output, 0.f);
65 return;
66 default:
67 ++m_currentLoop;
68 if (m_currentLoop >= m_loops) {
69 m_playing = false;
70 m_currentLoop = 0;
71 ranges::fill(output, 0.f);
72 return;
73 }
74 break;
75 }
76 }
77}
78
80{
81 m_playing = true;
82}
83
85{
86 m_loops = loops;
87}
88
90{
91 return m_buffer.format();
92}
93
95{
96 m_playing = false;
97}
98
99} // namespace QSpatialAudioPrivate
100
101namespace {
102
103std::optional<QAudioBuffer> joinBuffers(QSpan<const QAudioBuffer> buffers)
104{
105 if (buffers.empty())
106 return {};
107
108 QByteArray data;
109 for (const QAudioBuffer &b : buffers) {
110 if (!b.isValid())
111 return {};
112
113 if (b.format() != buffers.front().format()) {
114 qWarning() << "QAmbientSound: all buffers must have the same format";
115 return {};
116 }
117
118 data.append(b.constData<char>(), b.byteCount());
119 }
120
121 return QAudioBuffer{
122 data,
123 buffers.front().format(),
124 buffers.front().startTime(),
125 };
126}
127
128int addStereoSource(QAudioEngine *engine)
129{
130 auto *ep = QAudioEnginePrivate::get(engine);
131 if (!ep)
132 return -1;
133 return ep->resonanceAudio->api->CreateStereoSource(2);
134}
135
136} // namespace
137
138QAmbientSoundPrivate::QAmbientSoundPrivate(QAudioEngine *engine)
140 engine,
141 2,
142 addStereoSource(engine),
143 }
144{
146}
147
148QAmbientSoundPrivate::QAmbientSoundPrivate(QAudioEngine *engine, int nchannels, int sourceId)
149 : nchannels(nchannels), engine(engine), sourceId(sourceId)
150{
151 auto *ep = QAudioEnginePrivate::get(engine);
152 if (!ep)
153 return;
154 ep->addSound(this);
155}
156
158{
159 auto *ep = QAudioEnginePrivate::get(engine);
160 if (ep)
161 ep->removeSound(this);
162
163 withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
164 api->DestroySource(sourceId);
165 });
166}
167
169{
170 m_volume = volume;
172}
173
175{
176 m_loops = loops;
177 if (m_playbackState)
178 m_playbackState->setLoops(loops);
179}
180
182{
183 m_autoPlay = enabled;
184}
185
187{
188 withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
189 api->SetSourceVolume(sourceId, m_volume);
190 });
191}
192
194{
195 switch (m_state) {
196 case State::Stopped: {
197 m_state = State::Playing;
198 if (m_buffer) {
199 setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/true,
200 m_loops));
201 }
202 return;
203 }
204 case State::Paused:
205 case State::Playing: {
206 m_state = State::Playing;
207 if (m_playbackState)
208 m_playbackState->resume();
209 return;
210 }
211 }
212}
213
215{
216 switch (m_state) {
217 case State::Stopped: {
218 m_state = State::Paused;
219 setState(std::make_shared<QSpatialAudioPlaybackState>(*m_buffer, /*playing=*/false,
220 m_loops));
221 return;
222 }
223 case State::Paused: {
224 return;
225 }
226 case State::Playing: {
227 m_state = State::Paused;
228 setState(nullptr);
229
230 if (m_playbackState)
231 m_playbackState->pause();
232 return;
233 }
234 }
235}
236
238{
239 switch (m_state) {
240 case State::Stopped: {
241 return;
242 }
243 case State::Paused:
244 case State::Playing: {
245 m_state = State::Stopped;
246 setState({});
247 return;
248 }
249 }
250}
251
252void QAmbientSoundPrivate::loadUrl(const QUrl &url)
253{
254 m_url = url;
255
256 auto *ep = QAudioEnginePrivate::get(engine);
257 if (!ep)
258 return;
259
260 Q_Q(QAmbientSound);
261
262 m_loadFuture.cancel();
263
264 setState(nullptr);
265
266 QAudioFormat f;
267 f.setSampleFormat(QAudioFormat::Float);
268 f.setSampleRate(ep->sampleRate());
269 f.setChannelConfig(nchannels == 2 ? QAudioFormat::ChannelConfigStereo
270 : QAudioFormat::ChannelConfigMono);
271
272 QUrl resolved = m_sourceResolver->resolve(url);
273 m_loadFuture = load(resolved, f).then(q, [this](QAmbientSoundPrivate::LoadResult result) {
274 if (result) {
275 m_buffer = joinBuffers(*result);
276 if (!m_buffer) {
277 qWarning() << "QAmbientSound: failed to join audio buffers";
278 return;
279 }
280
281 bool startingPlayback = m_autoPlay || m_state != State::Stopped;
282 if (!startingPlayback)
283 return;
284 bool startPaused = (m_state == State::Paused) && !m_autoPlay;
285 setState(std::make_shared<QSpatialAudioPlaybackState>(
286 *m_buffer, /*playing=*/!startPaused, m_loops));
287
288 m_state = startPaused ? State::Paused : State::Playing;
289
290 } else {
291 qWarning() << "QAmbientSound: cannot load file";
292 }
293 });
294}
295
296auto QAmbientSoundPrivate::load(QUrl resolvedUrl, QAudioFormat format) -> QFuture<LoadResult>
297{
298 m_loadFuture.cancelChain();
299
300 auto promise = std::make_shared<QPromise<LoadResult>>();
301 auto future = promise->future();
302
303 m_decoder = std::make_unique<QAudioDecoder>();
304 m_decoder->setAudioFormat(format);
305
306 std::shared_ptr<QIODevice> file; // kept alive until decoding is finished
307 if (resolvedUrl.scheme().compare(u"qrc", Qt::CaseInsensitive) == 0) {
308 file = std::make_unique<QFile>(u':' + resolvedUrl.path());
309 if (!file->open(QFile::ReadOnly)) {
310 promise->start();
311 promise->addResult(q23::unexpected{ QAudioDecoder::Error::ResourceError });
312 promise->finish();
313
314 m_decoder = {};
315
316 return future;
317 }
318 m_decoder->setSourceDevice(file.get());
319 } else {
320 m_decoder->setSource(resolvedUrl);
321 }
322
323 auto accum = std::make_shared<QList<QAudioBuffer>>();
324 QObject::connect(m_decoder.get(), &QAudioDecoder::bufferReady, m_decoder.get(), [accum, this] {
325 accum->append(m_decoder->read());
326 });
327
328 QObject::connect(
329 m_decoder.get(),
330 static_cast<void (QAudioDecoder::*)(QAudioDecoder::Error)>(&QAudioDecoder::error),
331 m_decoder.get(), [promise, file](QAudioDecoder::Error error) {
332 promise->start();
333 promise->addResult(q23::unexpected{ error });
334 promise->finish();
335 });
336
337 QObject::connect(m_decoder.get(), &QAudioDecoder::finished, m_decoder.get(),
338 [promise, file, accum] {
339 promise->start();
340 promise->addResult(std::move(*accum));
341 promise->finish();
342 });
343
344 m_decoder->start();
345 return future;
346}
347
348vraudio::ResonanceAudioApi *QAmbientSoundPrivate::getAPI()
349{
350 auto *ep = QAudioEnginePrivate::get(engine);
351 if (!ep)
352 return nullptr;
353 return ep->resonanceAudio->api.get();
354}
355
356void QAmbientSoundPrivate::setState(SharedPlaybackState state)
357{
358 auto *ep = QAudioEnginePrivate::get(engine);
359 if (ep)
360 ep->setSoundPlaybackData(this, state);
361 m_playbackState = std::move(state);
362}
363
364/*!
365 \class QAmbientSound
366 \inmodule QtSpatialAudio
367 \ingroup spatialaudio
368 \ingroup multimedia_audio
369
370 \brief A stereo overlay sound.
371
372 QAmbientSound represents a position and orientation independent sound.
373 It's commonly used for background sounds (e.g. music) that is supposed to be independent
374 of the listeners position and orientation.
375
376 \note QAmbientSound is only active when the QAudioEngine is using OutputMode::Stereo or
377 OutputMode::Headphone.
378 */
379
380/*!
381 Creates a stereo sound source for \a engine.
382
383 \note Must be called with a valid QAudioEngine
384 */
385QAmbientSound::QAmbientSound(QAudioEngine *engine) : QObject(*new QAmbientSoundPrivate(engine))
386{
387 if (!engine)
388 qWarning() << "Cannot create QAmbientSound without a valid QAudioEngine";
389}
390
391QAmbientSound::~QAmbientSound()
392{
393 Q_D(QAmbientSound);
394 if (d->state() != QAmbientSoundPrivate::State::Stopped)
395 d->stop();
396}
397
398/*!
399 \property QAmbientSound::volume
400
401 Defines the volume of the sound.
402
403 Values between 0 and 1 will attenuate the sound, while values above 1
404 provide an additional gain boost.
405 */
406void QAmbientSound::setVolume(float volume)
407{
408 Q_D(QAmbientSound);
409 if (volume != d->volume()) {
410 d->setVolume(volume);
411 emit volumeChanged();
412 }
413}
414
415float QAmbientSound::volume() const
416{
417 Q_D(const QAmbientSound);
418 return d->volume();
419}
420
421void QAmbientSound::setSource(const QUrl &url)
422{
423 Q_D(QAmbientSound);
424 if (d->url() == url)
425 return;
426 d->loadUrl(url);
427
428 emit sourceChanged();
429}
430
431/*!
432 \property QAmbientSound::source
433
434 The source file for the sound to be played.
435 */
436QUrl QAmbientSound::source() const
437{
438 Q_D(const QAmbientSound);
439 return d->url();
440}
441/*!
442 \enum QAmbientSound::Loops
443
444 Lets you control the playback loop using the following values:
445
446 \value Infinite Loops infinitely
447 \value Once Stops playback after running once
448*/
449/*!
450 \property QAmbientSound::loops
451
452 Determines how many times the sound is played before the player stops.
453 Set to QAmbientSound::Infinite to play the current sound in
454 a loop forever.
455
456 The default value is \c 1.
457 */
458int QAmbientSound::loops() const
459{
460 Q_D(const QAmbientSound);
461 return d->loops();
462}
463
464void QAmbientSound::setLoops(int loops)
465{
466 Q_D(QAmbientSound);
467 if (loops != d->loops()) {
468 d->setLoops(loops);
469 emit loopsChanged();
470 }
471}
472
473/*!
474 \property QAmbientSound::autoPlay
475
476 Determines whether the sound should automatically start playing when a source
477 gets specified.
478
479 The default value is \c true.
480 */
481bool QAmbientSound::autoPlay() const
482{
483 Q_D(const QAmbientSound);
484 return d->autoPlay();
485}
486
487void QAmbientSound::setAutoPlay(bool autoPlay)
488{
489 Q_D(QAmbientSound);
490 if (autoPlay != d->autoPlay()) {
491 d->setAutoPlay(autoPlay);
492 emit autoPlayChanged();
493 }
494}
495
496/*!
497 Starts playing back the sound. Does nothing if the sound is already playing.
498 */
499void QAmbientSound::play()
500{
501 Q_D(QAmbientSound);
502 d->play();
503}
504
505/*!
506 Pauses sound playback. Calling play() will continue playback.
507 */
508void QAmbientSound::pause()
509{
510 Q_D(QAmbientSound);
511 d->pause();
512}
513
514/*!
515 Stops sound playback and resets the current position and current loop count to 0.
516 Calling play() will start playback at the beginning of the sound file.
517 */
518void QAmbientSound::stop()
519{
520 Q_D(QAmbientSound);
521 d->stop();
522}
523
524/*!
525 Returns the engine associated with this sound.
526 */
527QAudioEngine *QAmbientSound::engine() const
528{
529 Q_D(const QAmbientSound);
530
531 return d->engine;
532}
533
534QT_END_NAMESPACE
535
536#include "moc_qambientsound.cpp"
void setVolume(float volume)
void loadUrl(const QUrl &url)
virtual void applyVolume()
QAudioEngine *const engine
QAmbientSoundPrivate(QAudioEngine *engine, int nchannels, int sourceId)
QSpatialAudioPlaybackState(QAudioBuffer buffer, bool playing, int loops)