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
qsoundeffectwithplayer.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtCore/q20map.h>
7#include <QtCore/qmutex.h>
8#include <QtCore/qscopeguard.h>
9#include <QtCore/private/qobject_p.h>
10
11#include <utility>
12
13QT_BEGIN_NAMESPACE
14
15namespace QtMultimediaPrivate {
16
17namespace {
18
19QSpan<const float> toFloatSpan(QSpan<const char> byteArray)
20{
21 return QSpan{
22 reinterpret_cast<const float *>(byteArray.data()),
23 qsizetype(byteArray.size_bytes() / sizeof(float)),
24 };
25}
26
27struct AudioDeviceFormatLess
28{
29 bool operator()(const std::pair<QAudioDevice, QAudioFormat> &lhs,
30 const std::pair<QAudioDevice, QAudioFormat> &rhs) const
31 {
32 auto cmp = qCompareThreeWay(lhs.first.id(), rhs.first.id());
33 if (cmp == Qt::strong_ordering::less)
34 return true;
35 if (cmp == Qt::strong_ordering::greater)
36 return false;
37
38 return std::tuple(lhs.second.sampleRate(), lhs.second.sampleFormat(),
39 lhs.second.channelCount())
40 < std::tuple(rhs.second.sampleRate(), rhs.second.sampleFormat(),
41 rhs.second.channelCount());
42 }
43};
44
45} // namespace
46
47// we keep a pool of engines with one engine per device/format
50{
51 if (device.isNull()) {
52 qWarning() << "QRtAudioEngine needs to be called with a valid device";
53 return nullptr;
54 }
55
57 qWarning() << "QRtAudioEngine requires floating point samples";
58 return nullptr;
59 }
60
62 qWarning() << "QRtAudioEngine needs to be called with a supported fromat";
63 return nullptr;
64 }
65
70
72
73 auto key = std::pair{ device, format };
75 if (found != s_playerRegistry.end()) {
76 auto player = found->second.lock();
78 return player;
79 }
80
81 // lazy clean up
84 });
85
86 // SoundEffect can prevent the stream from appear in an OS mixer
88
94
95 return player;
96}
97
98///////////////////////////////////////////////////////////////////////////////////////////////////
99
100QSoundEffectVoice::QSoundEffectVoice(VoiceId voiceId, std::shared_ptr<const QSample> sample,
101 float volume, bool muted, int totalLoopCount,
102 QAudioFormat engineFormat)
103 : QRtAudioEngineVoice{ voiceId },
104 m_sample{ std::move(sample) },
105 m_engineFormat{ engineFormat },
106 m_volume{ volume },
107 m_muted{ muted },
108 m_loopsRemaining{ totalLoopCount }
109{
110}
111
113
138
140{
144
147
149
150 const int sampleCh = format.channelCount();
156
158 const ConversionType conversion = [&] {
159 if (sampleCh == engineCh)
160 return SameChannels;
161 if (sampleCh == 1 && engineCh == 2)
162 return MonoToStereo;
163 if (sampleCh == 2 && engineCh == 1)
164 return StereoToMono;
166 }();
167
168 if (m_muted || m_volume == 0.f)
169 return framesToPlay;
170
171 // later: (auto)vectorize?
172 switch (conversion) {
173 case SameChannels:
174 for (qsizetype frame = 0; frame < framesToPlay; ++frame) {
177 for (int ch = 0; ch < sampleCh; ++ch) {
179 }
180 }
181 break;
182 case MonoToStereo:
183 for (qsizetype frame = 0; frame < framesToPlay; ++frame) {
186 const float val = playbackRange[sampleBase] * m_volume;
189 }
190 break;
191 case StereoToMono:
192 float scale = 0.5f * m_volume;
193 for (qsizetype frame = 0; frame < framesToPlay; ++frame) {
196 const float val = (playbackRange[sampleBase] + playbackRange[sampleBase + 1]) * scale;
198 }
199 break;
200 }
201
202 return framesToPlay;
203}
204
206{
208 return true;
209
210 return loopsRemaining() != 0;
211}
212
215{
219
220 // caveat: reading frame is not atomic, so we may have a race here ... is is rare, though,
221 // not sure if we really care
223 return clone;
224}
225
226///////////////////////////////////////////////////////////////////////////////////////////////////
227
247
255
257{
258 if (device == m_audioDevice)
259 return false;
260
263 return true;
264}
265
267{
269 return;
270
272
273 if (m_player)
274 for (const auto &voice : m_voices)
276
278 while (!m_voices.empty()) {
281 }
282
283 // the voices are replaced by clones on the new player, the originals are released through the
284 // old player, so that their last reference is dropped on its thread
285 auto oldPlayer = m_player;
286 auto releaseVoices = qScopeGuard([&] {
287 if (oldPlayer)
288 for (auto &voice : voices)
290 });
291
292 if (m_sample) {
294 if (!hasPlayer) {
296 return;
297 }
298
299 for (const auto &voice : voices)
300 // we re-allocate a new voice ID and play on the new player
302
304 } else {
306 }
307}
308
310{
311 if (m_audioDevice.isNull())
314}
315
320
322{
323 if (m_sampleLoadFuture) {
326 }
327
328 if (m_player) {
332 // we keep the player referenced for a little longer, so that later calls to
333 // getEngineFor will be able to reuse the existing instance
336 }
337
338 m_sample = {};
339
340 if (url.isEmpty()) {
342 return;
343 }
344
345 if (!url.isValid()) {
347 return;
348 }
349
351
354 if (result) {
356 qWarning("QSoundEffect: QSoundEffect only supports mono or stereo files");
358 return;
359 }
360
363 if (!hasPlayer) {
364 qWarning("QSoundEffect: playback of this format is not supported on the selected "
365 "audio device");
367 return;
368 }
369
371 if (std::exchange(m_playPending, false)) {
372 play();
373 }
374 } else {
375 qWarning("QSoundEffect: Error decoding source %ls", qUtf16Printable(url.toString()));
377 }
378 });
379}
380
382{
383 if (status == m_status)
384 return;
387}
388
393
395{
396 return m_loopCount;
397}
398
400{
401 if (loopCount == 0)
402 loopCount = 1;
403
404 if (loopCount == m_loopCount)
405 return false;
406
408
409 if (m_voices.empty())
410 return true;
411
414
416
417 return true;
418}
419
421{
422 if (m_voices.empty())
423 return 0;
424
425 return m_loopsRemaining;
426}
427
429{
430 return m_volume;
431}
432
434{
435 if (m_volume == volume)
436 return false;
437
439 for (const auto &voice : m_voices) {
442 });
443 }
444 return true;
445}
446
448{
449 return m_muted;
450}
451
453{
454 if (m_muted == muted)
455 return false;
456
457 m_muted = muted;
458 for (const auto &voice : m_voices) {
461 });
462 }
463 return true;
464}
465
467{
468 if (!m_sample) {
469 m_playPending = true;
470 return;
471 }
472
473 // Error after a failed engine setup: retry, the device may be available again
476
477 if (status() != QSoundEffect::Ready)
478 return;
479
481
484 return;
485 }
486
487 // each `play` will start a new voice
491
492 play(std::move(voice));
493}
494
496{
498 for (const auto &voice : m_voices)
501
502 while (!m_voices.empty()) {
505 }
506 m_playPending = false;
507 if (activeVoices)
509}
510
512{
513 return !m_voices.empty();
514}
515
517{
520 if (foundVoice == m_voices.end())
521 return;
522
523 if (voiceId != activeVoice())
524 return;
525
527 });
528
529 // voice lives on the engline thread. they are not QObject, but may contain QObjects.
531
535 if (m_voices.size() == 1)
537}
538
540{
545
546 m_player = {};
548 return false;
549
552 if (player)
553 return player;
554
556 switch (sample->format().channelCount()) {
557 case 1:
559 break;
560 case 2:
562 break;
563 default:
565 }
566
568 }();
569
571 return false;
572
574 this, [this](VoiceId voiceId) {
575 if (voiceId == activeVoice())
577
578 auto found = m_voices.find(voiceId);
579 if (found != m_voices.end()) {
580 auto node = m_voices.extract(found);
582 if (m_voices.empty())
584 }
585 });
586
588 [this](QtAudio::State state) {
590 return;
591
592 // When the player state fails something must have gone wrong, e.g. the audio device was
593 // removed. We clear all voices and reset the loops remaining to 0.
594 m_voices.clear();
597 });
598 return true;
599}
600
602{
603 if (m_voices.empty())
604 return std::nullopt;
605 return (*m_voices.rbegin())->voiceId();
606}
607
609{
610 switch (fmt.channelCount()) {
611 case 1:
612 case 2:
613 return true;
614 default:
615 return false;
616 }
617}
618
620{
622 return;
625}
626
627} // namespace QtMultimediaPrivate
628
629QT_END_NAMESPACE
Q_MULTIMEDIA_EXPORT ~QSoundEffectVoice()