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
4#include "qsoundeffectwithplayer_p.h"
5
6#include <QtCore/qmutex.h>
7#include <QtCore/q20map.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
25} // namespace
26
27///////////////////////////////////////////////////////////////////////////////////////////////////
28
29QSoundEffectVoice::QSoundEffectVoice(VoiceId voiceId, std::shared_ptr<const QSample> sample,
30 float volume, bool muted, int totalLoopCount)
31 : QRtAudioEngineVoice{ voiceId },
32 m_sample{ std::move(sample) },
33 m_volume{ volume },
34 m_muted{ muted },
35 m_loopsRemaining{ totalLoopCount }
36{
37}
38
39VoicePlayResult QSoundEffectVoice::play(QSpan<float> outputBuffer) noexcept QT_MM_NONBLOCKING
40{
41 const QAudioFormat &format = m_sample->format();
42 int totalSamples = m_totalFrames * format.channelCount();
43 int currentSample = format.channelCount() * m_currentFrame;
44
45 const QSpan fullSample = toFloatSpan(byteArray: m_sample->data());
46 QSpan playbackRange = take(span: drop(span: fullSample, n: currentSample), n: totalSamples);
47
48 Q_ASSERT(!playbackRange.empty());
49
50 // later: (auto)vectorize?
51 qsizetype samplesToPlay = std::min(a: playbackRange.size(), b: outputBuffer.size());
52 if (m_muted || m_volume == 0.f) {
53 auto outputRange = take(span: outputBuffer, n: samplesToPlay);
54 std::fill(first: outputRange.begin(), last: outputRange.end(), value: 0.f);
55 } else if (m_volume == 1.f) {
56 for (qsizetype i = 0; i != samplesToPlay; ++i)
57 outputBuffer[i] += playbackRange[i];
58 } else {
59 for (qsizetype i = 0; i != samplesToPlay; ++i)
60 outputBuffer[i] += playbackRange[i] * m_volume;
61 }
62
63 m_currentFrame += samplesToPlay / format.channelCount();
64
65 if (m_currentFrame == m_totalFrames) {
66 const bool isInfiniteLoop = loopsRemaining() == QSoundEffect::Infinite;
67 bool continuePlaying = isInfiniteLoop;
68
69 if (!isInfiniteLoop)
70 continuePlaying = m_loopsRemaining.fetch_sub(i: 1, m: std::memory_order_relaxed) > 1;
71
72 if (continuePlaying) {
73 if (!isInfiniteLoop)
74 m_currentLoopChanged.set();
75 m_currentFrame = 0;
76 QSpan remainingOutputBuffer = drop(span: outputBuffer, n: samplesToPlay);
77 return play(outputBuffer: remainingOutputBuffer);
78 }
79 return VoicePlayResult::Finished;
80 }
81 return VoicePlayResult::Playing;
82}
83
84bool QSoundEffectVoice::isActive() noexcept QT_MM_NONBLOCKING
85{
86 if (m_currentFrame != m_totalFrames)
87 return true;
88
89 return loopsRemaining() != 0;
90}
91
92std::shared_ptr<QSoundEffectVoice> QSoundEffectVoice::clone() const
93{
94 auto clone = std::make_shared<QSoundEffectVoice>(args: QRtAudioEngine::allocateVoiceId(), args: m_sample,
95 args: m_volume, args: m_muted, args: loopsRemaining());
96
97 // caveat: reading frame is not atomic, so we may have a race here ... is is rare, though,
98 // not sure if we really care
99 clone->m_currentFrame = m_currentFrame;
100 return clone;
101}
102
103///////////////////////////////////////////////////////////////////////////////////////////////////
104
105QSoundEffectPrivateWithPlayer::QSoundEffectPrivateWithPlayer(QSoundEffect *q,
106 QAudioDevice audioDevice)
107 : q_ptr{ q }, m_audioDevice{ std::move(audioDevice) }
108{
109 resolveAudioDevice();
110
111 QObject::connect(sender: &m_mediaDevices, signal: &QMediaDevices::audioOutputsChanged, context: this, slot: [this] {
112 QAudioDevice defaultAudioDevice = QMediaDevices::defaultAudioOutput();
113 if (defaultAudioDevice == m_defaultAudioDevice)
114 return;
115
116 m_defaultAudioDevice = QMediaDevices::defaultAudioOutput();
117 if (m_audioDevice.isNull())
118 setResolvedAudioDevice(m_defaultAudioDevice);
119 });
120}
121
122QSoundEffectPrivateWithPlayer::~QSoundEffectPrivateWithPlayer()
123{
124 stop();
125}
126
127bool QSoundEffectPrivateWithPlayer::setAudioDevice(QAudioDevice device)
128{
129 if (device == m_audioDevice)
130 return false;
131
132 m_audioDevice = std::move(device);
133 resolveAudioDevice();
134 return true;
135}
136
137void QSoundEffectPrivateWithPlayer::setResolvedAudioDevice(QAudioDevice device)
138{
139 if (m_resolvedAudioDevice == device)
140 return;
141
142 m_resolvedAudioDevice = std::move(device);
143
144 if (!m_player)
145 return;
146
147 for (const auto &voice : m_voices)
148 m_player->stop(voice);
149
150 std::vector<std::shared_ptr<QSoundEffectVoice>> voices{
151 std::make_move_iterator(i: m_voices.begin()), std::make_move_iterator(i: m_voices.end())
152 };
153 m_voices.clear();
154
155 bool hasPlayer = updatePlayer();
156 if (!hasPlayer)
157 return;
158
159 for (const auto &voice : voices)
160 // we re-allocate a new voice ID and play on the new player
161 play(voice->clone());
162}
163
164void QSoundEffectPrivateWithPlayer::resolveAudioDevice()
165{
166 if (m_audioDevice.isNull())
167 m_defaultAudioDevice = QMediaDevices::defaultAudioOutput();
168 setResolvedAudioDevice(m_audioDevice.isNull() ? m_defaultAudioDevice : m_audioDevice);
169}
170
171QAudioDevice QSoundEffectPrivateWithPlayer::audioDevice() const
172{
173 return m_audioDevice;
174}
175
176bool QSoundEffectPrivateWithPlayer::setSource(const QUrl &url, QSampleCache &sampleCache)
177{
178 if (m_sampleLoadFuture.isValid())
179 m_sampleLoadFuture.cancel();
180
181 m_url = url;
182 m_sample = {};
183
184 if (url.isEmpty()) {
185 setStatus(QSoundEffect::Null);
186 return false;
187 }
188
189 if (!url.isValid()) {
190 setStatus(QSoundEffect::Error);
191 return false;
192 }
193
194 setStatus(QSoundEffect::Loading);
195
196 m_sampleLoadFuture =
197 sampleCache.requestSampleFuture(url).then(context: this, function: [this](SharedSamplePtr result) {
198 if (result) {
199 if (!formatIsSupported(result->format())) {
200 qWarning(msg: "QSoundEffect: QSoundEffect only supports mono or stereo files");
201 setStatus(QSoundEffect::Error);
202 return;
203 }
204
205 m_sample = std::move(result);
206 setStatus(QSoundEffect::Ready);
207 bool hasPlayer = updatePlayer();
208 if (std::exchange(obj&: m_playPending, new_val: false)) {
209 if (hasPlayer)
210 play();
211 }
212 } else {
213 qWarning(msg: "QSoundEffect: Error decoding source %ls", qUtf16Printable(m_url.toString()));
214 setStatus(QSoundEffect::Error);
215 }
216 });
217
218 return true;
219}
220
221QUrl QSoundEffectPrivateWithPlayer::url() const
222{
223 return m_url;
224}
225
226void QSoundEffectPrivateWithPlayer::setStatus(QSoundEffect::Status status)
227{
228 if (status == m_status)
229 return;
230 m_status = status;
231 emit q_ptr->statusChanged();
232}
233
234QSoundEffect::Status QSoundEffectPrivateWithPlayer::status() const
235{
236 return m_status;
237}
238
239int QSoundEffectPrivateWithPlayer::loopCount() const
240{
241 return m_loopCount;
242}
243
244bool QSoundEffectPrivateWithPlayer::setLoopCount(int loopCount)
245{
246 if (loopCount == 0)
247 loopCount = 1;
248
249 if (loopCount == m_loopCount)
250 return false;
251
252 m_loopCount = loopCount;
253
254 if (m_voices.empty())
255 return true;
256
257 const std::shared_ptr<QSoundEffectVoice> &voice = *m_voices.rbegin();
258 voice->m_loopsRemaining.store(i: loopCount, m: std::memory_order_relaxed);
259
260 setLoopsRemaining(loopCount);
261
262 return true;
263}
264
265int QSoundEffectPrivateWithPlayer::loopsRemaining() const
266{
267 if (m_voices.empty())
268 return 0;
269
270 return m_loopsRemaining;
271}
272
273float QSoundEffectPrivateWithPlayer::volume() const
274{
275 return m_volume;
276}
277
278bool QSoundEffectPrivateWithPlayer::setVolume(float volume)
279{
280 if (m_volume == volume)
281 return false;
282
283 m_volume = volume;
284 for (const auto &voice : m_voices) {
285 m_player->visitVoiceRt(voice, visitor: [volume](QSoundEffectVoice &voice) {
286 voice.m_volume = volume;
287 });
288 }
289 return true;
290}
291
292bool QSoundEffectPrivateWithPlayer::muted() const
293{
294 return m_muted;
295}
296
297bool QSoundEffectPrivateWithPlayer::setMuted(bool muted)
298{
299 if (m_muted == muted)
300 return false;
301
302 m_muted = muted;
303 for (const auto &voice : m_voices) {
304 m_player->visitVoiceRt(voice, visitor: [muted](QSoundEffectVoice &voice) {
305 voice.m_muted = muted;
306 });
307 }
308 return true;
309}
310
311void QSoundEffectPrivateWithPlayer::play()
312{
313 if (!m_sample) {
314 m_playPending = true;
315 return;
316 }
317
318 // each `play` will start a new voice
319 Q_ASSERT(m_player);
320
321 auto voice = std::make_shared<QSoundEffectVoice>(args: QRtAudioEngine::allocateVoiceId(), args&: m_sample,
322 args&: m_volume, args&: m_muted, args&: m_loopCount);
323
324 play(std::move(voice));
325}
326
327void QSoundEffectPrivateWithPlayer::stop()
328{
329 size_t activeVoices = m_voices.size();
330 for (const auto &voice : m_voices)
331 m_player->stop(voice->voiceId());
332 setLoopsRemaining(0);
333
334 m_voices.clear();
335 m_playPending = false;
336 if (activeVoices)
337 emit q_ptr->playingChanged();
338}
339
340bool QSoundEffectPrivateWithPlayer::playing() const
341{
342 return !m_voices.empty();
343}
344
345void QSoundEffectPrivateWithPlayer::play(std::shared_ptr<QSoundEffectVoice> voice)
346{
347 QObject::connect(sender: &voice->m_currentLoopChanged, signal: &QAutoResetEvent::activated, context: this,
348 slot: [this, voiceId = voice->voiceId()] {
349 auto foundVoice = m_voices.find(x: voiceId);
350 if (foundVoice == m_voices.end())
351 return;
352
353 if (voiceId != activeVoice())
354 return;
355
356 setLoopsRemaining((*foundVoice)->loopsRemaining());
357 });
358
359 m_player->play(voice);
360 m_voices.insert(x: std::move(voice));
361 setLoopsRemaining(m_loopCount);
362 if (m_voices.size() == 1)
363 emit q_ptr->playingChanged();
364}
365
366bool QSoundEffectPrivateWithPlayer::updatePlayer()
367{
368 Q_ASSERT(m_voices.empty());
369 QObject::disconnect(m_voiceFinishedConnection);
370
371 m_player = {};
372 if (m_resolvedAudioDevice.isNull())
373 return false;
374
375 auto player = QRtAudioEngine::getEngineFor(m_resolvedAudioDevice, m_sample->format());
376 m_player = player;
377
378 m_voiceFinishedConnection = QObject::connect(sender: m_player.get(), signal: &QRtAudioEngine::voiceFinished,
379 context: this, slot: [this](VoiceId voiceId) {
380 if (voiceId == activeVoice())
381 setLoopsRemaining(0);
382
383 auto found = m_voices.find(x: voiceId);
384 if (found != m_voices.end()) {
385 m_voices.erase(position: found);
386 if (m_voices.empty())
387 emit q_ptr->playingChanged();
388 }
389 });
390 return true;
391}
392
393std::optional<VoiceId> QSoundEffectPrivateWithPlayer::activeVoice() const
394{
395 if (m_voices.empty())
396 return std::nullopt;
397 return (*m_voices.rbegin())->voiceId();
398}
399
400bool QSoundEffectPrivateWithPlayer::formatIsSupported(const QAudioFormat &fmt)
401{
402 switch (fmt.channelCount()) {
403 case 1:
404 case 2:
405 return true;
406 default:
407 return false;
408 }
409}
410
411void QSoundEffectPrivateWithPlayer::setLoopsRemaining(int loopsRemaining)
412{
413 if (loopsRemaining == m_loopsRemaining)
414 return;
415 m_loopsRemaining = loopsRemaining;
416 emit q_ptr->loopsRemainingChanged();
417}
418
419} // namespace QtMultimediaPrivate
420
421QT_END_NAMESPACE
422

source code of qtmultimedia/src/multimedia/audio/qsoundeffectwithplayer.cpp