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
9#include <utility>
10
11QT_BEGIN_NAMESPACE
12
13namespace QtMultimediaPrivate {
14
15namespace {
16
17QSpan<const float> toFloatSpan(QSpan<const char> byteArray)
18{
19 return QSpan{
20 reinterpret_cast<const float *>(byteArray.data()),
21 qsizetype(byteArray.size_bytes() / sizeof(float)),
22 };
23}
24
25struct AudioDeviceFormatLess
26{
27 bool operator()(const std::pair<QAudioDevice, QAudioFormat> &lhs,
28 const std::pair<QAudioDevice, QAudioFormat> &rhs) const
29 {
30 auto cmp = qCompareThreeWay(lhs.first.id(), rhs.first.id());
31 if (cmp == Qt::strong_ordering::less)
32 return true;
33 if (cmp == Qt::strong_ordering::greater)
34 return false;
35
36 return std::tuple(lhs.second.sampleRate(), lhs.second.sampleFormat(),
37 lhs.second.channelCount())
38 < std::tuple(rhs.second.sampleRate(), rhs.second.sampleFormat(),
39 rhs.second.channelCount());
40 }
41};
42
43} // namespace
44
45// we keep a pool of engines with one engine per device/format
46std::shared_ptr<QRtAudioEngine>
47QSoundEffectPrivateWithPlayer::getEngineFor(const QAudioDevice &device, const QAudioFormat &format)
48{
49 if (device.isNull()) {
50 qWarning() << "QRtAudioEngine needs to be called with a valid device";
51 return nullptr;
52 }
53
54 if (format.sampleFormat() != QAudioFormat::Float) {
55 qWarning() << "QRtAudioEngine requires floating point samples";
56 return nullptr;
57 }
58
59 if (!device.isFormatSupported(format)) {
60 qWarning() << "QRtAudioEngine needs to be called with a supported fromat";
61 return nullptr;
62 }
63
64 static QMutex s_playerRegistryMutex;
65 static std::map<std::pair<QAudioDevice, QAudioFormat>, std::weak_ptr<QRtAudioEngine>,
66 AudioDeviceFormatLess>
67 s_playerRegistry;
68
69 auto guard = std::lock_guard{ s_playerRegistryMutex };
70
71 auto key = std::pair{ device, format };
72 auto found = s_playerRegistry.find(key);
73 if (found != s_playerRegistry.end()) {
74 auto player = found->second.lock();
75 if (player)
76 return player;
77 }
78
79 // lazy clean up
80 q20::erase_if(s_playerRegistry, [](auto &&keyValuePair) {
81 return keyValuePair.second.expired();
82 });
83
84 // SoundEffect can prevent the stream from appear in an OS mixer
85 auto endpointRole = AudioEndpointRole::SoundEffect;
86
87 auto player = std::shared_ptr<QRtAudioEngine>(
88 new QRtAudioEngine{ device, format, endpointRole }, [](QRtAudioEngine *engine) {
89 if (engine->thread()->isCurrentThread())
90 delete engine;
91 else
92 engine->deleteLater();
93 });
94 s_playerRegistry.emplace(key, player);
95
96 return player;
97}
98
99///////////////////////////////////////////////////////////////////////////////////////////////////
100
101QSoundEffectVoice::QSoundEffectVoice(VoiceId voiceId, std::shared_ptr<const QSample> sample,
102 float volume, bool muted, int totalLoopCount,
103 QAudioFormat engineFormat)
104 : QRtAudioEngineVoice{ voiceId },
105 m_sample{ std::move(sample) },
106 m_engineFormat{ engineFormat },
107 m_volume{ volume },
108 m_muted{ muted },
109 m_loopsRemaining{ totalLoopCount }
110{
111}
112
114
139
141{
145
148
150
151 const int sampleCh = format.channelCount();
158
160 const ConversionType conversion = [&] {
161 if (sampleCh == engineCh)
162 return SameChannels;
163 if (sampleCh == 1 && engineCh == 2)
164 return MonoToStereo;
165 if (sampleCh == 2 && engineCh == 1)
166 return StereoToMono;
168 }();
169
170 if (m_muted || m_volume == 0.f) {
172 return framesToPlay;
173 }
174
175 // later: (auto)vectorize?
176 switch (conversion) {
177 case SameChannels:
178 for (qsizetype frame = 0; frame < framesToPlay; ++frame) {
181 for (int ch = 0; ch < sampleCh; ++ch) {
183 }
184 }
185 break;
186 case MonoToStereo:
187 for (qsizetype frame = 0; frame < framesToPlay; ++frame) {
190 const float val = playbackRange[sampleBase] * m_volume;
193 }
194 break;
195 case StereoToMono:
196 float scale = 0.5f * m_volume;
197 for (qsizetype frame = 0; frame < framesToPlay; ++frame) {
200 const float val = (playbackRange[sampleBase] + playbackRange[sampleBase + 1]) * scale;
202 }
203 break;
204 }
205
206 return framesToPlay;
207}
208
210{
212 return true;
213
214 return loopsRemaining() != 0;
215}
216
219{
223
224 // caveat: reading frame is not atomic, so we may have a race here ... is is rare, though,
225 // not sure if we really care
227 return clone;
228}
229
230///////////////////////////////////////////////////////////////////////////////////////////////////
231
251
258
260{
261 if (device == m_audioDevice)
262 return false;
263
266 return true;
267}
268
270{
272 return;
273
275
276 if (m_player)
277 for (const auto &voice : m_voices)
279
282 };
283 m_voices.clear();
284
285 if (m_sample) {
287 if (!hasPlayer) {
289 return;
290 }
291
292 for (const auto &voice : voices)
293 // we re-allocate a new voice ID and play on the new player
295
297
298 for (const auto &voice : voices)
299 // we re-allocate a new voice ID and play on the new player
300 play(voice->clone());
301 } else {
303 }
304}
305
307{
308 if (m_audioDevice.isNull())
311}
312
317
319{
320 if (m_sampleLoadFuture) {
323 }
324
325 if (m_player) {
328 // we keep the player referenced for a little longer, so that later calls to
329 // getEngineFor will be able to reuse the existing instance
332 }
333
334 m_sample = {};
335
336 if (url.isEmpty()) {
338 return;
339 }
340
341 if (!url.isValid()) {
343 return;
344 }
345
347
350 if (result) {
352 qWarning("QSoundEffect: QSoundEffect only supports mono or stereo files");
354 return;
355 }
356
359 if (!hasPlayer) {
360 qWarning("QSoundEffect: playback of this format is not supported on the selected "
361 "audio device");
363 return;
364 }
365
367 if (std::exchange(m_playPending, false)) {
368 play();
369 }
370 } else {
371 qWarning("QSoundEffect: Error decoding source %ls", qUtf16Printable(url.toString()));
373 }
374 });
375}
376
378{
379 if (status == m_status)
380 return;
383}
384
389
391{
392 return m_loopCount;
393}
394
396{
397 if (loopCount == 0)
398 loopCount = 1;
399
400 if (loopCount == m_loopCount)
401 return false;
402
404
405 if (m_voices.empty())
406 return true;
407
410
412
413 return true;
414}
415
417{
418 if (m_voices.empty())
419 return 0;
420
421 return m_loopsRemaining;
422}
423
425{
426 return m_volume;
427}
428
430{
431 if (m_volume == volume)
432 return false;
433
435 for (const auto &voice : m_voices) {
438 });
439 }
440 return true;
441}
442
444{
445 return m_muted;
446}
447
449{
450 if (m_muted == muted)
451 return false;
452
453 m_muted = muted;
454 for (const auto &voice : m_voices) {
457 });
458 }
459 return true;
460}
461
463{
464 if (!m_sample) {
465 m_playPending = true;
466 return;
467 }
468
469 if (status() != QSoundEffect::Ready)
470 return;
471
473
474 // each `play` will start a new voice
478
479 play(std::move(voice));
480}
481
483{
485 for (const auto &voice : m_voices)
488
489 m_voices.clear();
490 m_playPending = false;
491 if (activeVoices)
493}
494
496{
497 return !m_voices.empty();
498}
499
501{
503 [this, voiceId = voice->voiceId()] {
505 if (foundVoice == m_voices.end())
506 return;
507
508 if (voiceId != activeVoice())
509 return;
510
512 });
513
517 if (m_voices.size() == 1)
519}
520
522{
526
527 m_player = {};
529 return false;
530
533 if (player)
534 return player;
535
537 switch (sample->format().channelCount()) {
538 case 1:
540 break;
541 case 2:
543 break;
544 default:
546 }
547
549 }();
550
551 if (!m_player)
552 return false;
553
555 this, [this](VoiceId voiceId) {
556 if (voiceId == activeVoice())
558
559 auto found = m_voices.find(voiceId);
560 if (found != m_voices.end()) {
562 if (m_voices.empty())
564 }
565 });
566 return true;
567}
568
570{
571 if (m_voices.empty())
572 return std::nullopt;
573 return (*m_voices.rbegin())->voiceId();
574}
575
577{
578 switch (fmt.channelCount()) {
579 case 1:
580 case 2:
581 return true;
582 default:
583 return false;
584 }
585}
586
588{
590 return;
593}
594
595} // namespace QtMultimediaPrivate
596
597QT_END_NAMESPACE
Q_MULTIMEDIA_EXPORT ~QSoundEffectVoice()