| 1 | // Copyright (C) 2016 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 <QtMultimedia/private/qtmultimediaglobal_p.h> |
| 5 | #include "qsoundeffect.h" |
| 6 | #include "qsamplecache_p.h" |
| 7 | #include "qaudiodevice.h" |
| 8 | #include "qaudiosink.h" |
| 9 | #include "qmediadevices.h" |
| 10 | #include "qaudiobuffer.h" |
| 11 | #include <QtCore/qloggingcategory.h> |
| 12 | #include <private/qplatformaudiodevices_p.h> |
| 13 | #include <private/qplatformmediaintegration_p.h> |
| 14 | #include <private/qplatformaudioresampler_p.h> |
| 15 | |
| 16 | static Q_LOGGING_CATEGORY(qLcSoundEffect, "qt.multimedia.soundeffect" ) |
| 17 | |
| 18 | QT_BEGIN_NAMESPACE |
| 19 | |
| 20 | Q_GLOBAL_STATIC(QSampleCache, sampleCache) |
| 21 | |
| 22 | namespace |
| 23 | { |
| 24 | struct AudioSinkDeleter |
| 25 | { |
| 26 | void operator ()(QAudioSink* sink) const |
| 27 | { |
| 28 | sink->stop(); |
| 29 | // Investigate:should we just delete? |
| 30 | sink->deleteLater(); |
| 31 | } |
| 32 | }; |
| 33 | |
| 34 | struct SampleDeleter |
| 35 | { |
| 36 | void operator ()(QSample* sample) const |
| 37 | { |
| 38 | sample->release(); |
| 39 | } |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | class QSoundEffectPrivate : public QIODevice |
| 44 | { |
| 45 | public: |
| 46 | QSoundEffectPrivate(QSoundEffect *q, const QAudioDevice &audioDevice = QAudioDevice()); |
| 47 | ~QSoundEffectPrivate() override = default; |
| 48 | |
| 49 | qint64 readData(char *data, qint64 len) override; |
| 50 | qint64 writeData(const char *data, qint64 len) override; |
| 51 | qint64 size() const override { |
| 52 | if (m_sample->state() != QSample::Ready) |
| 53 | return 0; |
| 54 | return m_loopCount == QSoundEffect::Infinite ? 0 : m_loopCount * m_audioBuffer.byteCount(); |
| 55 | } |
| 56 | qint64 bytesAvailable() const override { |
| 57 | if (m_sample->state() != QSample::Ready) |
| 58 | return 0; |
| 59 | if (m_loopCount == QSoundEffect::Infinite) |
| 60 | return std::numeric_limits<qint64>::max(); |
| 61 | return m_runningCount * m_audioBuffer.byteCount() - m_offset; |
| 62 | } |
| 63 | bool isSequential() const override { |
| 64 | return m_loopCount == QSoundEffect::Infinite; |
| 65 | } |
| 66 | bool atEnd() const override { |
| 67 | return m_runningCount == 0; |
| 68 | } |
| 69 | |
| 70 | void setLoopsRemaining(int loopsRemaining); |
| 71 | void setStatus(QSoundEffect::Status status); |
| 72 | void setPlaying(bool playing); |
| 73 | bool updateAudioOutput(); |
| 74 | |
| 75 | public Q_SLOTS: |
| 76 | void sampleReady(QSample *); |
| 77 | void decoderError(QSample *); |
| 78 | void stateChanged(QAudio::State); |
| 79 | |
| 80 | public: |
| 81 | QSoundEffect *q_ptr; |
| 82 | QUrl m_url; |
| 83 | int m_loopCount = 1; |
| 84 | int m_runningCount = 0; |
| 85 | bool m_playing = false; |
| 86 | QSoundEffect::Status m_status = QSoundEffect::Null; |
| 87 | std::unique_ptr<QAudioSink, AudioSinkDeleter> m_audioSink; |
| 88 | std::unique_ptr<QSample, SampleDeleter> m_sample; |
| 89 | QAudioBuffer m_audioBuffer; |
| 90 | bool m_muted = false; |
| 91 | float m_volume = 1.0; |
| 92 | bool m_sampleReady = false; |
| 93 | qint64 m_offset = 0; |
| 94 | QAudioDevice m_audioDevice; |
| 95 | }; |
| 96 | |
| 97 | QSoundEffectPrivate::QSoundEffectPrivate(QSoundEffect *q, const QAudioDevice &audioDevice) |
| 98 | : QIODevice(q) |
| 99 | , q_ptr(q) |
| 100 | , m_audioDevice(audioDevice) |
| 101 | { |
| 102 | open(mode: QIODevice::ReadOnly); |
| 103 | |
| 104 | QPlatformMediaIntegration::instance()->audioDevices()->prepareAudio(); |
| 105 | } |
| 106 | |
| 107 | void QSoundEffectPrivate::sampleReady(QSample *sample) |
| 108 | { |
| 109 | if (sample && sample != m_sample.get()) |
| 110 | return; |
| 111 | |
| 112 | if (m_status == QSoundEffect::Error) |
| 113 | return; |
| 114 | |
| 115 | qCDebug(qLcSoundEffect) << this << "sampleReady: sample size:" << m_sample->data().size(); |
| 116 | disconnect(sender: m_sample.get(), signal: &QSample::error, receiver: this, slot: &QSoundEffectPrivate::decoderError); |
| 117 | disconnect(sender: m_sample.get(), signal: &QSample::ready, receiver: this, slot: &QSoundEffectPrivate::sampleReady); |
| 118 | |
| 119 | if (!m_audioSink) { |
| 120 | if (!updateAudioOutput()) // Create audio sink |
| 121 | return; // Returns if no audio devices are available |
| 122 | } |
| 123 | |
| 124 | m_sampleReady = true; |
| 125 | setStatus(QSoundEffect::Ready); |
| 126 | |
| 127 | if (m_playing && m_audioSink->state() == QAudio::StoppedState) { |
| 128 | qCDebug(qLcSoundEffect) << this << "starting playback on audiooutput" ; |
| 129 | m_audioSink->start(device: this); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | void QSoundEffectPrivate::decoderError(QSample *sample) |
| 134 | { |
| 135 | if (sample && sample != m_sample.get()) |
| 136 | return; |
| 137 | |
| 138 | qWarning(msg: "QSoundEffect(qaudio): Error decoding source %ls" , qUtf16Printable(m_url.toString())); |
| 139 | disconnect(sender: m_sample.get(), signal: &QSample::ready, receiver: this, slot: &QSoundEffectPrivate::sampleReady); |
| 140 | disconnect(sender: m_sample.get(), signal: &QSample::error, receiver: this, slot: &QSoundEffectPrivate::decoderError); |
| 141 | m_playing = false; |
| 142 | setStatus(QSoundEffect::Error); |
| 143 | } |
| 144 | |
| 145 | void QSoundEffectPrivate::stateChanged(QAudio::State state) |
| 146 | { |
| 147 | qCDebug(qLcSoundEffect) << this << "stateChanged " << state; |
| 148 | if ((state == QAudio::IdleState && m_runningCount == 0) || state == QAudio::StoppedState) |
| 149 | q_ptr->stop(); |
| 150 | } |
| 151 | |
| 152 | bool QSoundEffectPrivate::updateAudioOutput() |
| 153 | { |
| 154 | const auto audioDevice = |
| 155 | m_audioDevice.isNull() ? QMediaDevices::defaultAudioOutput() : m_audioDevice; |
| 156 | |
| 157 | if (audioDevice.isNull()) { |
| 158 | // We are likely on a virtual machine, for example in CI |
| 159 | qCCritical(qLcSoundEffect) << "Failed to update audio output. No audio devices available." ; |
| 160 | setStatus(QSoundEffect::Error); |
| 161 | return false; |
| 162 | } |
| 163 | |
| 164 | if (m_audioDevice.isNull()) { |
| 165 | q_ptr->setAudioDevice(audioDevice); // Updates m_audioDevice and emits audioDeviceChanged |
| 166 | } |
| 167 | |
| 168 | m_audioBuffer = {}; |
| 169 | |
| 170 | Q_ASSERT(m_sample); |
| 171 | |
| 172 | const auto &sampleFormat = m_sample->format(); |
| 173 | const auto sampleChannelConfig = |
| 174 | sampleFormat.channelConfig() == QAudioFormat::ChannelConfigUnknown |
| 175 | ? QAudioFormat::defaultChannelConfigForChannelCount(channelCount: sampleFormat.channelCount()) |
| 176 | : sampleFormat.channelConfig(); |
| 177 | |
| 178 | if (sampleChannelConfig != audioDevice.channelConfiguration() |
| 179 | && audioDevice.channelConfiguration() != QAudioFormat::ChannelConfigUnknown) { |
| 180 | qCDebug(qLcSoundEffect) << "Create resampler for channels mapping: config" |
| 181 | << sampleFormat.channelConfig() << "=> config" |
| 182 | << audioDevice.channelConfiguration(); |
| 183 | auto outputFormat = sampleFormat; |
| 184 | outputFormat.setChannelConfig(audioDevice.channelConfiguration()); |
| 185 | |
| 186 | const auto resampler = QPlatformMediaIntegration::instance()->createAudioResampler( |
| 187 | m_sample->format(), outputFormat); |
| 188 | if (resampler) |
| 189 | m_audioBuffer = resampler.value()->resample(data: m_sample->data().constData(), |
| 190 | size: m_sample->data().size()); |
| 191 | else |
| 192 | qCDebug(qLcSoundEffect) << "Cannot create resampler for channels mapping" ; |
| 193 | } |
| 194 | |
| 195 | if (!m_audioBuffer.isValid()) |
| 196 | m_audioBuffer = QAudioBuffer(m_sample->data(), m_sample->format()); |
| 197 | |
| 198 | m_audioSink.reset(p: new QAudioSink(audioDevice, m_audioBuffer.format())); |
| 199 | |
| 200 | connect(sender: m_audioSink.get(), signal: &QAudioSink::stateChanged, context: this, slot: &QSoundEffectPrivate::stateChanged); |
| 201 | if (!m_muted) |
| 202 | m_audioSink->setVolume(m_volume); |
| 203 | else |
| 204 | m_audioSink->setVolume(0); |
| 205 | |
| 206 | return true; |
| 207 | } |
| 208 | |
| 209 | qint64 QSoundEffectPrivate::readData(char *data, qint64 len) |
| 210 | { |
| 211 | qCDebug(qLcSoundEffect) << this << "readData" << len << m_runningCount; |
| 212 | if (!len) |
| 213 | return 0; |
| 214 | if (m_sample->state() != QSample::Ready) |
| 215 | return 0; |
| 216 | if (m_runningCount == 0 || !m_playing) |
| 217 | return 0; |
| 218 | |
| 219 | qint64 bytesWritten = 0; |
| 220 | |
| 221 | const int sampleSize = m_audioBuffer.byteCount(); |
| 222 | const char *sampleData = m_audioBuffer.constData<char>(); |
| 223 | |
| 224 | while (len && m_runningCount) { |
| 225 | int toWrite = qMax(a: 0, b: qMin(a: sampleSize - m_offset, b: len)); |
| 226 | memcpy(dest: data, src: sampleData + m_offset, n: toWrite); |
| 227 | bytesWritten += toWrite; |
| 228 | data += toWrite; |
| 229 | len -= toWrite; |
| 230 | m_offset += toWrite; |
| 231 | if (m_offset >= sampleSize) { |
| 232 | if (m_runningCount > 0) |
| 233 | setLoopsRemaining(m_runningCount - 1); |
| 234 | m_offset = 0; |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | return bytesWritten; |
| 239 | } |
| 240 | |
| 241 | qint64 QSoundEffectPrivate::writeData(const char *data, qint64 len) |
| 242 | { |
| 243 | Q_UNUSED(data); |
| 244 | Q_UNUSED(len); |
| 245 | return 0; |
| 246 | } |
| 247 | |
| 248 | void QSoundEffectPrivate::setLoopsRemaining(int loopsRemaining) |
| 249 | { |
| 250 | if (m_runningCount == loopsRemaining) |
| 251 | return; |
| 252 | qCDebug(qLcSoundEffect) << this << "setLoopsRemaining " << loopsRemaining; |
| 253 | m_runningCount = loopsRemaining; |
| 254 | emit q_ptr->loopsRemainingChanged(); |
| 255 | } |
| 256 | |
| 257 | void QSoundEffectPrivate::setStatus(QSoundEffect::Status status) |
| 258 | { |
| 259 | qCDebug(qLcSoundEffect) << this << "setStatus" << status; |
| 260 | if (m_status == status) |
| 261 | return; |
| 262 | bool oldLoaded = q_ptr->isLoaded(); |
| 263 | m_status = status; |
| 264 | emit q_ptr->statusChanged(); |
| 265 | if (oldLoaded != q_ptr->isLoaded()) |
| 266 | emit q_ptr->loadedChanged(); |
| 267 | } |
| 268 | |
| 269 | void QSoundEffectPrivate::setPlaying(bool playing) |
| 270 | { |
| 271 | qCDebug(qLcSoundEffect) << this << "setPlaying(" << playing << ")" << m_playing; |
| 272 | if (m_audioSink) { |
| 273 | m_audioSink->reset(); |
| 274 | if (playing && !m_sampleReady) |
| 275 | return; |
| 276 | } |
| 277 | |
| 278 | if (m_playing == playing) |
| 279 | return; |
| 280 | m_playing = playing; |
| 281 | |
| 282 | if (m_audioSink && playing) |
| 283 | m_audioSink->start(device: this); |
| 284 | |
| 285 | emit q_ptr->playingChanged(); |
| 286 | } |
| 287 | |
| 288 | /*! |
| 289 | \class QSoundEffect |
| 290 | \brief The QSoundEffect class provides a way to play low latency sound effects. |
| 291 | |
| 292 | \ingroup multimedia |
| 293 | \ingroup multimedia_audio |
| 294 | \inmodule QtMultimedia |
| 295 | |
| 296 | This class allows you to play uncompressed audio files (typically WAV files) in |
| 297 | a generally lower latency way, and is suitable for "feedback" type sounds in |
| 298 | response to user actions (e.g. virtual keyboard sounds, positive or negative |
| 299 | feedback for popup dialogs, or game sounds). If low latency is not important, |
| 300 | consider using the QMediaPlayer class instead, since it supports a wider |
| 301 | variety of media formats and is less resource intensive. |
| 302 | |
| 303 | This example shows how a looping, somewhat quiet sound effect |
| 304 | can be played: |
| 305 | |
| 306 | \snippet multimedia-snippets/qsound.cpp 2 |
| 307 | |
| 308 | Typically the sound effect should be reused, which allows all the |
| 309 | parsing and preparation to be done ahead of time, and only triggered |
| 310 | when necessary. This assists with lower latency audio playback. |
| 311 | |
| 312 | \snippet multimedia-snippets/qsound.cpp 3 |
| 313 | |
| 314 | Since QSoundEffect requires slightly more resources to achieve lower |
| 315 | latency playback, the platform may limit the number of simultaneously playing |
| 316 | sound effects. |
| 317 | */ |
| 318 | |
| 319 | |
| 320 | /*! |
| 321 | \qmltype SoundEffect |
| 322 | \nativetype QSoundEffect |
| 323 | \brief The SoundEffect type provides a way to play sound effects in QML. |
| 324 | |
| 325 | \inmodule QtMultimedia |
| 326 | \ingroup multimedia_qml |
| 327 | \ingroup multimedia_audio_qml |
| 328 | \inqmlmodule QtMultimedia |
| 329 | |
| 330 | This type allows you to play uncompressed audio files (typically WAV files) in |
| 331 | a generally lower latency way, and is suitable for "feedback" type sounds in |
| 332 | response to user actions (e.g. virtual keyboard sounds, positive or negative |
| 333 | feedback for popup dialogs, or game sounds). If low latency is not important, |
| 334 | consider using the MediaPlayer type instead, since it support a wider |
| 335 | variety of media formats and is less resource intensive. |
| 336 | |
| 337 | Typically the sound effect should be reused, which allows all the |
| 338 | parsing and preparation to be done ahead of time, and only triggered |
| 339 | when necessary. This is easy to achieve with QML, since you can declare your |
| 340 | SoundEffect instance and refer to it elsewhere. |
| 341 | |
| 342 | The following example plays a WAV file on mouse click. |
| 343 | |
| 344 | \snippet multimedia-snippets/soundeffect.qml complete snippet |
| 345 | |
| 346 | Since SoundEffect requires slightly more resources to achieve lower |
| 347 | latency playback, the platform may limit the number of simultaneously playing |
| 348 | sound effects. |
| 349 | */ |
| 350 | |
| 351 | /*! |
| 352 | Creates a QSoundEffect with the given \a parent. |
| 353 | */ |
| 354 | QSoundEffect::QSoundEffect(QObject *parent) |
| 355 | : QSoundEffect(QAudioDevice(), parent) |
| 356 | { |
| 357 | } |
| 358 | |
| 359 | /*! |
| 360 | Creates a QSoundEffect with the given \a audioDevice and \a parent. |
| 361 | */ |
| 362 | QSoundEffect::QSoundEffect(const QAudioDevice &audioDevice, QObject *parent) |
| 363 | : QObject(parent) |
| 364 | , d(new QSoundEffectPrivate(this, audioDevice)) |
| 365 | { |
| 366 | } |
| 367 | |
| 368 | /*! |
| 369 | Destroys this sound effect. |
| 370 | */ |
| 371 | QSoundEffect::~QSoundEffect() |
| 372 | { |
| 373 | stop(); |
| 374 | d->m_audioSink.reset(); |
| 375 | d->m_sample.reset(); |
| 376 | delete d; |
| 377 | } |
| 378 | |
| 379 | /*! |
| 380 | \fn QSoundEffect::supportedMimeTypes() |
| 381 | |
| 382 | Returns a list of the supported mime types for this platform. |
| 383 | */ |
| 384 | QStringList QSoundEffect::supportedMimeTypes() |
| 385 | { |
| 386 | // Only return supported mime types if we have a audio device available |
| 387 | const QList<QAudioDevice> devices = QMediaDevices::audioOutputs(); |
| 388 | if (devices.isEmpty()) |
| 389 | return QStringList(); |
| 390 | |
| 391 | return QStringList() << QLatin1String("audio/x-wav" ) |
| 392 | << QLatin1String("audio/wav" ) |
| 393 | << QLatin1String("audio/wave" ) |
| 394 | << QLatin1String("audio/x-pn-wav" ); |
| 395 | } |
| 396 | |
| 397 | /*! |
| 398 | \qmlproperty url QtMultimedia::SoundEffect::source |
| 399 | |
| 400 | This property holds the url for the sound to play. For the SoundEffect |
| 401 | to attempt to load the source, the URL must exist and the application must have read permission |
| 402 | in the specified directory. If the desired source is a local file the URL may be specified |
| 403 | using either absolute or relative (to the file that declared the SoundEffect) pathing. |
| 404 | */ |
| 405 | /*! |
| 406 | \property QSoundEffect::source |
| 407 | |
| 408 | This property holds the url for the sound to play. For the SoundEffect |
| 409 | to attempt to load the source, the URL must exist and the application must have read permission |
| 410 | in the specified directory. |
| 411 | */ |
| 412 | |
| 413 | /*! Returns the URL of the current source to play */ |
| 414 | QUrl QSoundEffect::source() const |
| 415 | { |
| 416 | return d->m_url; |
| 417 | } |
| 418 | |
| 419 | /*! Set the current URL to play to \a url. */ |
| 420 | void QSoundEffect::setSource(const QUrl &url) |
| 421 | { |
| 422 | qCDebug(qLcSoundEffect) << this << "setSource current=" << d->m_url << ", to=" << url; |
| 423 | if (d->m_url == url) |
| 424 | return; |
| 425 | |
| 426 | Q_ASSERT(d->m_url != url); |
| 427 | |
| 428 | stop(); |
| 429 | |
| 430 | d->m_url = url; |
| 431 | d->m_sampleReady = false; |
| 432 | |
| 433 | if (url.isEmpty()) { |
| 434 | d->setStatus(QSoundEffect::Null); |
| 435 | return; |
| 436 | } |
| 437 | |
| 438 | if (!url.isValid()) { |
| 439 | d->setStatus(QSoundEffect::Error); |
| 440 | return; |
| 441 | } |
| 442 | |
| 443 | if (d->m_sample) { |
| 444 | if (!d->m_sampleReady) { |
| 445 | disconnect(sender: d->m_sample.get(), signal: &QSample::error, receiver: d, slot: &QSoundEffectPrivate::decoderError); |
| 446 | disconnect(sender: d->m_sample.get(), signal: &QSample::ready, receiver: d, slot: &QSoundEffectPrivate::sampleReady); |
| 447 | } |
| 448 | d->m_sample.reset(); |
| 449 | } |
| 450 | |
| 451 | if (d->m_audioSink) { |
| 452 | disconnect(sender: d->m_audioSink.get(), signal: &QAudioSink::stateChanged, receiver: d, slot: &QSoundEffectPrivate::stateChanged); |
| 453 | d->m_audioSink.reset(); |
| 454 | } |
| 455 | |
| 456 | d->setStatus(QSoundEffect::Loading); |
| 457 | d->m_sample.reset(p: sampleCache()->requestSample(url)); |
| 458 | connect(sender: d->m_sample.get(), signal: &QSample::error, context: d, slot: &QSoundEffectPrivate::decoderError); |
| 459 | connect(sender: d->m_sample.get(), signal: &QSample::ready, context: d, slot: &QSoundEffectPrivate::sampleReady); |
| 460 | |
| 461 | switch (d->m_sample->state()) { |
| 462 | case QSample::Ready: |
| 463 | d->sampleReady(sample: d->m_sample.get()); |
| 464 | break; |
| 465 | case QSample::Error: |
| 466 | d->decoderError(sample: d->m_sample.get()); |
| 467 | break; |
| 468 | default: |
| 469 | break; |
| 470 | } |
| 471 | |
| 472 | emit sourceChanged(); |
| 473 | } |
| 474 | |
| 475 | /*! |
| 476 | \qmlproperty int QtMultimedia::SoundEffect::loops |
| 477 | |
| 478 | This property holds the number of times the sound is played. A value of 0 or 1 means |
| 479 | the sound will be played only once; set to SoundEffect.Infinite to enable infinite looping. |
| 480 | |
| 481 | The value can be changed while the sound effect is playing, in which case it will update |
| 482 | the remaining loops to the new value. |
| 483 | */ |
| 484 | |
| 485 | /*! |
| 486 | \property QSoundEffect::loops |
| 487 | This property holds the number of times the sound is played. A value of 0 or 1 means |
| 488 | the sound will be played only once; set to SoundEffect.Infinite to enable infinite looping. |
| 489 | |
| 490 | The value can be changed while the sound effect is playing, in which case it will update |
| 491 | the remaining loops to the new value. |
| 492 | */ |
| 493 | |
| 494 | /*! |
| 495 | Returns the total number of times that this sound effect will be played before stopping. |
| 496 | |
| 497 | See the \l loopsRemaining() method for the number of loops currently remaining. |
| 498 | */ |
| 499 | int QSoundEffect::loopCount() const |
| 500 | { |
| 501 | return d->m_loopCount; |
| 502 | } |
| 503 | |
| 504 | /*! |
| 505 | \enum QSoundEffect::Loop |
| 506 | |
| 507 | \value Infinite Used as a parameter to \l setLoopCount() for infinite looping |
| 508 | */ |
| 509 | |
| 510 | /*! |
| 511 | Set the total number of times to play this sound effect to \a loopCount. |
| 512 | |
| 513 | Setting the loop count to 0 or 1 means the sound effect will be played only once; |
| 514 | pass \c QSoundEffect::Infinite to repeat indefinitely. The loop count can be changed while |
| 515 | the sound effect is playing, in which case it will update the remaining loops to |
| 516 | the new \a loopCount. |
| 517 | |
| 518 | \sa loopsRemaining() |
| 519 | */ |
| 520 | void QSoundEffect::setLoopCount(int loopCount) |
| 521 | { |
| 522 | if (loopCount < 0 && loopCount != Infinite) { |
| 523 | qWarning(msg: "SoundEffect: loops should be SoundEffect.Infinite, 0 or positive integer" ); |
| 524 | return; |
| 525 | } |
| 526 | if (loopCount == 0) |
| 527 | loopCount = 1; |
| 528 | if (d->m_loopCount == loopCount) |
| 529 | return; |
| 530 | |
| 531 | d->m_loopCount = loopCount; |
| 532 | if (d->m_playing) |
| 533 | d->setLoopsRemaining(loopCount); |
| 534 | emit loopCountChanged(); |
| 535 | } |
| 536 | |
| 537 | /*! |
| 538 | \property QSoundEffect::audioDevice |
| 539 | |
| 540 | Returns the QAudioDevice instance. |
| 541 | */ |
| 542 | QAudioDevice QSoundEffect::audioDevice() |
| 543 | { |
| 544 | return d->m_audioDevice; |
| 545 | } |
| 546 | |
| 547 | void QSoundEffect::setAudioDevice(const QAudioDevice &device) |
| 548 | { |
| 549 | qCDebug(qLcSoundEffect) << this << "setAudioDevice:" << device.description(); |
| 550 | |
| 551 | if (d->m_audioDevice == device) |
| 552 | return; |
| 553 | |
| 554 | d->m_audioDevice = device; |
| 555 | |
| 556 | if (!d->m_sampleReady) { |
| 557 | emit audioDeviceChanged(); |
| 558 | return; // The audio sink will be recreated later by QSoundEffect::sampleReady() |
| 559 | } |
| 560 | |
| 561 | bool playing = d->m_playing; |
| 562 | std::chrono::microseconds current_time{d->m_audioBuffer.format().durationForBytes(byteCount: d->m_offset)}; |
| 563 | |
| 564 | // Recreate the QAudioSink with the new audio device and current sample |
| 565 | if (d->updateAudioOutput() && playing) { |
| 566 | // Resume playback from current position |
| 567 | d->m_offset = d->m_audioBuffer.format().bytesForDuration(microseconds: current_time.count()); |
| 568 | d->setPlaying(true); |
| 569 | } |
| 570 | |
| 571 | emit audioDeviceChanged(); |
| 572 | } |
| 573 | |
| 574 | /*! |
| 575 | \qmlproperty int QtMultimedia::SoundEffect::loopsRemaining |
| 576 | |
| 577 | This property contains the number of loops remaining before the sound effect |
| 578 | stops by itself, or SoundEffect.Infinite if that's what has been set in \l loops. |
| 579 | */ |
| 580 | /*! |
| 581 | \property QSoundEffect::loopsRemaining |
| 582 | |
| 583 | This property contains the number of loops remaining before the sound effect |
| 584 | stops by itself, or QSoundEffect::Infinite if that's what has been set in \l loops. |
| 585 | */ |
| 586 | int QSoundEffect::loopsRemaining() const |
| 587 | { |
| 588 | return d->m_runningCount; |
| 589 | } |
| 590 | |
| 591 | |
| 592 | /*! |
| 593 | \qmlproperty real QtMultimedia::SoundEffect::volume |
| 594 | |
| 595 | This property holds the volume of the sound effect playback. |
| 596 | |
| 597 | The volume is scaled linearly from \c 0.0 (silence) to \c 1.0 (full volume). Values outside this |
| 598 | range will be clamped. |
| 599 | |
| 600 | The default volume is \c 1.0. |
| 601 | |
| 602 | UI volume controls should usually be scaled non-linearly. For example, using a logarithmic scale |
| 603 | will produce linear changes in perceived loudness, which is what a user would normally expect |
| 604 | from a volume control. See \l {QtAudio::convertVolume()}{convertVolume()} |
| 605 | for more details. |
| 606 | */ |
| 607 | /*! |
| 608 | \property QSoundEffect::volume |
| 609 | |
| 610 | This property holds the volume of the sound effect playback, from 0.0 (silence) to 1.0 (full volume). |
| 611 | */ |
| 612 | |
| 613 | /*! |
| 614 | Returns the current volume of this sound effect, from 0.0 (silent) to 1.0 (maximum volume). |
| 615 | */ |
| 616 | float QSoundEffect::volume() const |
| 617 | { |
| 618 | if (d->m_audioSink && !d->m_muted) |
| 619 | return d->m_audioSink->volume(); |
| 620 | |
| 621 | return d->m_volume; |
| 622 | } |
| 623 | |
| 624 | /*! |
| 625 | Sets the sound effect volume to \a volume. |
| 626 | |
| 627 | The volume is scaled linearly from \c 0.0 (silence) to \c 1.0 (full volume). Values outside this |
| 628 | range will be clamped. |
| 629 | |
| 630 | The default volume is \c 1.0. |
| 631 | |
| 632 | UI volume controls should usually be scaled non-linearly. For example, using a logarithmic scale |
| 633 | will produce linear changes in perceived loudness, which is what a user would normally expect |
| 634 | from a volume control. See QtAudio::convertVolume() for more details. |
| 635 | */ |
| 636 | void QSoundEffect::setVolume(float volume) |
| 637 | { |
| 638 | volume = qBound(min: 0.0f, val: volume, max: 1.0f); |
| 639 | if (d->m_volume == volume) |
| 640 | return; |
| 641 | |
| 642 | d->m_volume = volume; |
| 643 | |
| 644 | if (d->m_audioSink && !d->m_muted) |
| 645 | d->m_audioSink->setVolume(volume); |
| 646 | |
| 647 | emit volumeChanged(); |
| 648 | } |
| 649 | |
| 650 | /*! |
| 651 | \qmlproperty bool QtMultimedia::SoundEffect::muted |
| 652 | |
| 653 | This property provides a way to control muting. A value of \c true will mute this effect. |
| 654 | Otherwise, playback will occur with the currently specified \l volume. |
| 655 | */ |
| 656 | /*! |
| 657 | \property QSoundEffect::muted |
| 658 | |
| 659 | This property provides a way to control muting. A value of \c true will mute this effect. |
| 660 | */ |
| 661 | |
| 662 | /*! Returns whether this sound effect is muted */ |
| 663 | bool QSoundEffect::isMuted() const |
| 664 | { |
| 665 | return d->m_muted; |
| 666 | } |
| 667 | |
| 668 | /*! |
| 669 | Sets whether to mute this sound effect's playback. |
| 670 | |
| 671 | If \a muted is true, playback will be muted (silenced), |
| 672 | and otherwise playback will occur with the currently |
| 673 | specified volume(). |
| 674 | */ |
| 675 | void QSoundEffect::setMuted(bool muted) |
| 676 | { |
| 677 | if (d->m_muted == muted) |
| 678 | return; |
| 679 | |
| 680 | if (muted && d->m_audioSink) |
| 681 | d->m_audioSink->setVolume(0); |
| 682 | else if (!muted && d->m_audioSink && d->m_muted) |
| 683 | d->m_audioSink->setVolume(d->m_volume); |
| 684 | |
| 685 | d->m_muted = muted; |
| 686 | emit mutedChanged(); |
| 687 | } |
| 688 | |
| 689 | /*! |
| 690 | \fn QSoundEffect::isLoaded() const |
| 691 | |
| 692 | Returns whether the sound effect has finished loading the \l source(). |
| 693 | */ |
| 694 | /*! |
| 695 | \qmlmethod bool QtMultimedia::SoundEffect::isLoaded() |
| 696 | |
| 697 | Returns whether the sound effect has finished loading the \l source. |
| 698 | */ |
| 699 | bool QSoundEffect::isLoaded() const |
| 700 | { |
| 701 | return d->m_status == QSoundEffect::Ready; |
| 702 | } |
| 703 | |
| 704 | /*! |
| 705 | \qmlmethod QtMultimedia::SoundEffect::play() |
| 706 | |
| 707 | Start playback of the sound effect, looping the effect for the number of |
| 708 | times as specified in the loops property. |
| 709 | |
| 710 | This is the default method for SoundEffect. |
| 711 | |
| 712 | \snippet multimedia-snippets/soundeffect.qml play sound on click |
| 713 | */ |
| 714 | /*! |
| 715 | \fn QSoundEffect::play() |
| 716 | |
| 717 | Start playback of the sound effect, looping the effect for the number of |
| 718 | times as specified in the loops property. |
| 719 | */ |
| 720 | void QSoundEffect::play() |
| 721 | { |
| 722 | d->m_offset = 0; |
| 723 | d->setLoopsRemaining(d->m_loopCount); |
| 724 | qCDebug(qLcSoundEffect) << this << "play" << d->m_loopCount << d->m_runningCount; |
| 725 | if (d->m_status == QSoundEffect::Null || d->m_status == QSoundEffect::Error) { |
| 726 | d->setStatus(QSoundEffect::Null); |
| 727 | return; |
| 728 | } |
| 729 | d->setPlaying(true); |
| 730 | } |
| 731 | |
| 732 | /*! |
| 733 | \qmlproperty bool QtMultimedia::SoundEffect::playing |
| 734 | |
| 735 | This property indicates whether the sound effect is playing or not. |
| 736 | */ |
| 737 | /*! |
| 738 | \property QSoundEffect::playing |
| 739 | |
| 740 | This property indicates whether the sound effect is playing or not. |
| 741 | */ |
| 742 | |
| 743 | /*! Returns true if the sound effect is currently playing, or false otherwise */ |
| 744 | bool QSoundEffect::isPlaying() const |
| 745 | { |
| 746 | return d->m_playing; |
| 747 | } |
| 748 | |
| 749 | /*! |
| 750 | \enum QSoundEffect::Status |
| 751 | |
| 752 | \value Null No source has been set or the source is null. |
| 753 | \value Loading The SoundEffect is trying to load the source. |
| 754 | \value Ready The source is loaded and ready for play. |
| 755 | \value Error An error occurred during operation, such as failure of loading the source. |
| 756 | |
| 757 | */ |
| 758 | |
| 759 | /*! |
| 760 | \qmlproperty enumeration QtMultimedia::SoundEffect::status |
| 761 | |
| 762 | This property indicates the current status of the SoundEffect |
| 763 | as enumerated within SoundEffect. |
| 764 | Possible statuses are listed below. |
| 765 | |
| 766 | \table |
| 767 | \header \li Value \li Description |
| 768 | \row \li SoundEffect.Null \li No source has been set or the source is null. |
| 769 | \row \li SoundEffect.Loading \li The SoundEffect is trying to load the source. |
| 770 | \row \li SoundEffect.Ready \li The source is loaded and ready for play. |
| 771 | \row \li SoundEffect.Error \li An error occurred during operation, such as failure of loading the source. |
| 772 | \endtable |
| 773 | */ |
| 774 | /*! |
| 775 | \property QSoundEffect::status |
| 776 | |
| 777 | This property indicates the current status of the sound effect |
| 778 | from the \l QSoundEffect::Status enumeration. |
| 779 | */ |
| 780 | |
| 781 | /*! |
| 782 | Returns the current status of this sound effect. |
| 783 | */ |
| 784 | QSoundEffect::Status QSoundEffect::status() const |
| 785 | { |
| 786 | return d->m_status; |
| 787 | } |
| 788 | |
| 789 | /*! |
| 790 | \qmlmethod QtMultimedia::SoundEffect::stop() |
| 791 | |
| 792 | Stop current playback. |
| 793 | |
| 794 | */ |
| 795 | /*! |
| 796 | \fn QSoundEffect::stop() |
| 797 | |
| 798 | Stop current playback. |
| 799 | |
| 800 | */ |
| 801 | void QSoundEffect::stop() |
| 802 | { |
| 803 | if (!d->m_playing) |
| 804 | return; |
| 805 | qCDebug(qLcSoundEffect) << "stop()" ; |
| 806 | d->m_offset = 0; |
| 807 | |
| 808 | d->setPlaying(false); |
| 809 | } |
| 810 | |
| 811 | /* Signals */ |
| 812 | |
| 813 | /*! |
| 814 | \fn void QSoundEffect::sourceChanged() |
| 815 | |
| 816 | The \c sourceChanged signal is emitted when the source has been changed. |
| 817 | */ |
| 818 | /*! |
| 819 | \qmlsignal QtMultimedia::SoundEffect::sourceChanged() |
| 820 | |
| 821 | The \c sourceChanged signal is emitted when the source has been changed. |
| 822 | */ |
| 823 | /*! |
| 824 | \fn void QSoundEffect::loadedChanged() |
| 825 | |
| 826 | The \c loadedChanged signal is emitted when the loading state has changed. |
| 827 | */ |
| 828 | /*! |
| 829 | \qmlsignal QtMultimedia::SoundEffect::loadedChanged() |
| 830 | |
| 831 | The \c loadedChanged signal is emitted when the loading state has changed. |
| 832 | */ |
| 833 | |
| 834 | /*! |
| 835 | \fn void QSoundEffect::loopCountChanged() |
| 836 | |
| 837 | The \c loopCountChanged signal is emitted when the initial number of loops has changed. |
| 838 | */ |
| 839 | /*! |
| 840 | \qmlsignal QtMultimedia::SoundEffect::loopCountChanged() |
| 841 | |
| 842 | The \c loopCountChanged signal is emitted when the initial number of loops has changed. |
| 843 | */ |
| 844 | |
| 845 | /*! |
| 846 | \fn void QSoundEffect::loopsRemainingChanged() |
| 847 | |
| 848 | The \c loopsRemainingChanged signal is emitted when the remaining number of loops has changed. |
| 849 | */ |
| 850 | /*! |
| 851 | \qmlsignal QtMultimedia::SoundEffect::loopsRemainingChanged() |
| 852 | |
| 853 | The \c loopsRemainingChanged signal is emitted when the remaining number of loops has changed. |
| 854 | */ |
| 855 | |
| 856 | /*! |
| 857 | \fn void QSoundEffect::volumeChanged() |
| 858 | |
| 859 | The \c volumeChanged signal is emitted when the volume has changed. |
| 860 | */ |
| 861 | /*! |
| 862 | \qmlsignal QtMultimedia::SoundEffect::volumeChanged() |
| 863 | |
| 864 | The \c volumeChanged signal is emitted when the volume has changed. |
| 865 | */ |
| 866 | |
| 867 | /*! |
| 868 | \fn void QSoundEffect::mutedChanged() |
| 869 | |
| 870 | The \c mutedChanged signal is emitted when the mute state has changed. |
| 871 | */ |
| 872 | /*! |
| 873 | \qmlsignal QtMultimedia::SoundEffect::mutedChanged() |
| 874 | |
| 875 | The \c mutedChanged signal is emitted when the mute state has changed. |
| 876 | */ |
| 877 | |
| 878 | /*! |
| 879 | \fn void QSoundEffect::playingChanged() |
| 880 | |
| 881 | The \c playingChanged signal is emitted when the playing property has changed. |
| 882 | */ |
| 883 | /*! |
| 884 | \qmlsignal QtMultimedia::SoundEffect::playingChanged() |
| 885 | |
| 886 | The \c playingChanged signal is emitted when the playing property has changed. |
| 887 | */ |
| 888 | |
| 889 | /*! |
| 890 | \fn void QSoundEffect::statusChanged() |
| 891 | |
| 892 | The \c statusChanged signal is emitted when the status property has changed. |
| 893 | */ |
| 894 | /*! |
| 895 | \qmlsignal QtMultimedia::SoundEffect::statusChanged() |
| 896 | |
| 897 | The \c statusChanged signal is emitted when the status property has changed. |
| 898 | */ |
| 899 | |
| 900 | QT_END_NAMESPACE |
| 901 | |
| 902 | #include "moc_qsoundeffect.cpp" |
| 903 | |