| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the plugins of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qdeclarative_audioengine_p.h" |
| #include "qdeclarative_audiolistener_p.h" |
| #include "qdeclarative_audiocategory_p.h" |
| #include "qdeclarative_audiosample_p.h" |
| #include "qdeclarative_sound_p.h" |
| #include "qdeclarative_playvariation_p.h" |
| #include "qdeclarative_attenuationmodel_p.h" |
| #include "qdeclarative_soundinstance_p.h" |
| #include "qsoundinstance_p.h" |
| #include <QtQml/qqmlengine.h> |
| #include "qdebug.h" |
| |
| #define DEBUG_AUDIOENGINE |
| |
| QT_BEGIN_NAMESPACE |
| |
| /*! |
| \qmltype AudioEngine |
| \instantiates QDeclarativeAudioEngine |
| \since 5.0 |
| \brief Organize all your 3d audio content in one place. |
| \inqmlmodule QtAudioEngine |
| \ingroup multimedia_audioengine |
| \inherits Item |
| \preliminary |
| \deprecated |
| |
| \qml |
| Rectangle { |
| color:"white" |
| width: 300 |
| height: 500 |
| |
| AudioEngine { |
| id:audioengine |
| |
| AudioSample { |
| name:"explosion" |
| source: "explosion-02.wav" |
| } |
| |
| Sound { |
| name:"explosion" |
| PlayVariation { |
| sample:"explosion" |
| } |
| } |
| |
| dopplerFactor: 1 |
| speedOfSound: 343.33 // Approximate speed of sound in air at 20 degrees Celsius |
| |
| listener.up:"0,0,1" |
| listener.position:"0,0,0" |
| listener.velocity:"0,0,0" |
| listener.direction:"0,1,0" |
| } |
| |
| MouseArea { |
| anchors.fill: parent |
| onPressed: { |
| audioengine.sounds["explosion"].play(); |
| } |
| } |
| } |
| \endqml |
| |
| \c AudioEngine acts as a central library for configuring all 3d audio content in an |
| app, so you should define only one in your app. |
| |
| It is mostly used as a container to access other types such as AudioCategory, AudioSample and |
| Sound. |
| |
| \sa AudioCategory, AudioSample, Sound, SoundInstance, AttenuationModelLinear, AttenuationModelInverse |
| */ |
| |
| QDeclarativeAudioEngine::QDeclarativeAudioEngine(QObject *parent) |
| : QObject(parent) |
| , m_complete(false) |
| , m_defaultCategory(0) |
| , m_defaultAttenuationModel(0) |
| , m_audioEngine(0) |
| { |
| m_audioEngine = QAudioEngine::create(this); |
| connect(m_audioEngine, SIGNAL(isLoadingChanged()), this, SIGNAL(isLoadingChanged())); |
| connect(m_audioEngine, SIGNAL(isLoadingChanged()), this, SLOT(handleLoadingChanged())); |
| m_listener = new QDeclarativeAudioListener(this); |
| m_updateTimer.setInterval(100); |
| connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updateSoundInstances())); |
| } |
| |
| QDeclarativeAudioEngine::~QDeclarativeAudioEngine() |
| { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "QDeclarativeAudioEngine::dtor" |
| << "active = " << m_activeSoundInstances.count() |
| << "pool = " << m_soundInstancePool.count(); |
| #endif |
| qDeleteAll(m_activeSoundInstances); |
| m_activeSoundInstances.clear(); |
| |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "for pool"; |
| #endif |
| qDeleteAll(m_soundInstancePool); |
| m_soundInstancePool.clear(); |
| } |
| |
| void QDeclarativeAudioEngine::classBegin() |
| { |
| } |
| |
| bool QDeclarativeAudioEngine::isReady() const |
| { |
| return m_complete; |
| } |
| |
| QAudioEngine* QDeclarativeAudioEngine::engine() const |
| { |
| return m_audioEngine; |
| } |
| |
| QDeclarativeSoundInstance* QDeclarativeAudioEngine::newDeclarativeSoundInstance(bool managed) |
| { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "QDeclarativeAudioEngine::newDeclarativeSoundInstance(" << managed << ")"; |
| #endif |
| QDeclarativeSoundInstance *instance = 0; |
| if (managed) { |
| if (m_managedDeclSndInstancePool.count() > 0) { |
| instance = m_managedDeclSndInstancePool.last(); |
| m_managedDeclSndInstancePool.pop_back(); |
| } else { |
| instance = new QDeclarativeSoundInstance(this); |
| qmlEngine(instance)->setObjectOwnership(instance, QQmlEngine::CppOwnership); |
| instance->setEngine(this); |
| } |
| m_managedDeclSoundInstances.push_back(instance); |
| } else { |
| instance = new QDeclarativeSoundInstance(); |
| instance->setEngine(this); |
| qmlEngine(instance)->setObjectOwnership(instance, QQmlEngine::JavaScriptOwnership); |
| } |
| return instance; |
| } |
| |
| void QDeclarativeAudioEngine::releaseManagedDeclarativeSoundInstance(QDeclarativeSoundInstance* declSndInstance) |
| { |
| declSndInstance->setSound(QString()); |
| m_managedDeclSndInstancePool.push_back(declSndInstance); |
| } |
| |
| /*! |
| \qmlproperty int QtAudioEngine::AudioEngine::liveInstances |
| |
| This property indicates how many live sound instances there are at the moment. |
| */ |
| int QDeclarativeAudioEngine::liveInstanceCount() const |
| { |
| return m_activeSoundInstances.count(); |
| } |
| |
| QSoundInstance* QDeclarativeAudioEngine::newSoundInstance(const QString &name) |
| { |
| QSoundInstance *instance = 0; |
| if (m_soundInstancePool.count() > 0) { |
| instance = m_soundInstancePool.last(); |
| m_soundInstancePool.pop_back(); |
| } else { |
| instance = new QSoundInstance(this); |
| } |
| instance->bindSoundDescription(qobject_cast<QDeclarativeSound*>(qvariant_cast<QObject*>(m_sounds.value(name)))); |
| m_activeSoundInstances.push_back(instance); |
| if (!m_updateTimer.isActive()) |
| m_updateTimer.start(); |
| emit liveInstanceCountChanged(); |
| return instance; |
| } |
| |
| void QDeclarativeAudioEngine::releaseSoundInstance(QSoundInstance* instance) |
| { |
| instance->bindSoundDescription(0); |
| m_activeSoundInstances.removeOne(instance); |
| m_soundInstancePool.push_back(instance); |
| emit liveInstanceCountChanged(); |
| } |
| |
| void QDeclarativeAudioEngine::initAudioSample(QDeclarativeAudioSample *sample) |
| { |
| sample->init(); |
| } |
| |
| void QDeclarativeAudioEngine::initSound(QDeclarativeSound *sound) |
| { |
| QDeclarativeAudioCategory *category = m_defaultCategory; |
| if (m_categories.contains(sound->category())) { |
| category = qobject_cast<QDeclarativeAudioCategory*>( |
| qvariant_cast<QObject*>(m_categories[sound->category()])); |
| } |
| sound->setCategoryObject(category); |
| |
| QDeclarativeAttenuationModel *attenuationModel = 0; |
| if (sound->attenuationModel().isEmpty()) { |
| if (m_defaultAttenuationModel) |
| attenuationModel = m_defaultAttenuationModel; |
| } else if (m_attenuationModels.contains(sound->attenuationModel())){ |
| attenuationModel = m_attenuationModels[sound->attenuationModel()]; |
| } else { |
| qWarning() << "Sound[" << sound->name() << "] contains invalid attenuationModel[" |
| << sound->attenuationModel() << "]"; |
| } |
| sound->setAttenuationModelObject(attenuationModel); |
| |
| const auto playList = sound->playlist(); |
| for (QDeclarativePlayVariation *playVariation : playList) { |
| if (m_samples.contains(playVariation->sample())) { |
| playVariation->setSampleObject( |
| qobject_cast<QDeclarativeAudioSample*>( |
| qvariant_cast<QObject*>(m_samples[playVariation->sample()]))); |
| } else { |
| qWarning() << "Sound[" << sound->name() << "] contains invalid sample[" |
| << playVariation->sample() << "] for its playVarations"; |
| } |
| } |
| } |
| |
| /*! |
| \qmlmethod QtAudioEngine::AudioEngine::addAudioSample(AudioSample sample) |
| |
| Adds the given \a sample to the engine. |
| This can be used when the AudioSample is created dynamically: |
| |
| \qml \QtMinorVersion |
| import QtAudioEngine 1.\1 |
| |
| AudioEngine { |
| id: engine |
| |
| Component.onCompleted: { |
| var sample = Qt.createQmlObject('import QtAudioEngine 1.1; AudioSample {}', engine); |
| sample.name = "example"; |
| sample.source = "example.wav"; |
| engine.addAudioSample(sample); |
| } |
| } |
| \endqml |
| */ |
| void QDeclarativeAudioEngine::addAudioSample(QDeclarativeAudioSample *sample) |
| { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "add QDeclarativeAudioSample[" << sample->name() << "]"; |
| #endif |
| if (sample->name().isEmpty()) { |
| qWarning("AudioSample must have a name!"); |
| return; |
| } |
| |
| if (m_samples.contains(sample->name())) { |
| qWarning() << "Failed to add AudioSample[" << sample->name() << "], already exists!"; |
| return; |
| } |
| m_samples.insert(sample->name(), QVariant::fromValue(sample)); |
| sample->setEngine(this); |
| |
| if (m_complete) { initAudioSample(sample); } |
| } |
| |
| /*! |
| \qmlmethod QtAudioEngine::AudioEngine::addSound(Sound sound) |
| |
| Adds the given \a sound to the engine. |
| This can be used when the Sound is created dynamically: |
| |
| \qml |
| import QtAudioEngine 1.1 |
| |
| AudioEngine { |
| id: engine |
| |
| Component.onCompleted: { |
| var sound = Qt.createQmlObject('import QtAudioEngine 1.1; Sound {}', engine); |
| sound.name = "example"; |
| engine.addSound(sound); |
| } |
| } |
| \endqml |
| */ |
| void QDeclarativeAudioEngine::addSound(QDeclarativeSound *sound) |
| { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "add QDeclarativeSound[" << sound->name() << "]"; |
| #endif |
| if (sound->name().isEmpty()) { |
| qWarning("Sound must have a name!"); |
| return; |
| } |
| |
| if (m_sounds.contains(sound->name())) { |
| qWarning() << "Failed to add Sound[" << sound->name() << "], already exists!"; |
| return; |
| } |
| m_sounds.insert(sound->name(), QVariant::fromValue(sound)); |
| sound->setEngine(this); |
| |
| if (m_complete) { initSound(sound); } |
| |
| } |
| |
| /*! |
| \qmlmethod QtAudioEngine::AudioEngine::addAudioCategory(AudioCategory category) |
| |
| Adds the given \a category to the engine. |
| This can be used when the AudioCategory is created dynamically: |
| |
| \qml |
| import QtAudioEngine 1.1 |
| |
| AudioEngine { |
| id: engine |
| |
| Component.onCompleted: { |
| var category = Qt.createQmlObject('import QtAudioEngine 1.1; AudioCategory {}', engine); |
| category.name = "sample"; |
| category.volume = 0.9; |
| engine.addAudioCategory(category); |
| } |
| } |
| \endqml |
| */ |
| void QDeclarativeAudioEngine::addAudioCategory(QDeclarativeAudioCategory *category) |
| { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "add QDeclarativeAudioCategory[" << category->name() << "]"; |
| #endif |
| if (category->name().isEmpty()) { |
| qWarning("AudioCategory must have a name!"); |
| return; |
| } |
| |
| if (m_categories.contains(category->name())) { |
| qWarning() << "Failed to add AudioCategory[" << category->name() << "], already exists!"; |
| return; |
| } |
| m_categories.insert(category->name(), QVariant::fromValue(category)); |
| if (category->name() == QLatin1String("default")) { |
| if (!m_complete) { |
| m_defaultCategory = category; |
| } else { |
| qWarning() << "Can not change default category after initializing engine"; |
| } |
| } |
| |
| category->setEngine(this); |
| } |
| |
| /*! |
| \qmlmethod QtAudioEngine::AudioEngine::addAttenuationModel(AttenuationModel attenuationModel) |
| |
| Adds the given \a attenuationModel to the engine. |
| This can be used when the AttenuationModelLinear / AttenuationModelInverse is created dynamically: |
| |
| \qml |
| import QtAudioEngine 1.1 |
| |
| AudioEngine { |
| id: engine |
| |
| Component.onCompleted: { |
| var attenuationModel = Qt.createQmlObject('import QtAudioEngine 1.1; AttenuationModelLinear {}', engine); |
| attenuationModel.name ="linear" |
| attenuationModel.start = 20 |
| attenuationModel.end = 180 |
| engine.addAttenuationModel(attenuationModel); |
| } |
| } |
| \endqml |
| */ |
| void QDeclarativeAudioEngine::addAttenuationModel(QDeclarativeAttenuationModel *attenModel) |
| { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "add AttenuationModel[" << attenModel->name() << "]"; |
| #endif |
| if (attenModel->name().isEmpty()) { |
| qWarning("AttenuationModel must have a name!"); |
| return; |
| } |
| |
| if (m_attenuationModels.contains(attenModel->name())) { |
| qWarning() << "Failed to add AttenuationModel[" << attenModel->name() << "], already exists!"; |
| return; |
| } |
| m_attenuationModels.insert(attenModel->name(), attenModel); |
| |
| if (attenModel->name() == QLatin1String("default")) { |
| if (!m_complete) { |
| m_defaultAttenuationModel = attenModel; |
| } else { |
| qWarning() << "Can not change default attenuation model after initializing engine"; |
| } |
| } |
| |
| attenModel->setEngine(this); |
| } |
| |
| void QDeclarativeAudioEngine::componentComplete() |
| { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "AudioEngine begin initialization"; |
| #endif |
| if (!m_defaultCategory) { |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "creating default category"; |
| #endif |
| m_defaultCategory = new QDeclarativeAudioCategory(this); |
| m_defaultCategory->setName(QString::fromLatin1("default")); |
| m_defaultCategory->setVolume(1); |
| m_defaultCategory->setEngine(this); |
| } |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "init samples" << m_samples.keys().count(); |
| #endif |
| const auto samplesKeys = m_samples.keys(); |
| for (const QString& key : samplesKeys) { |
| QDeclarativeAudioSample *sample = qobject_cast<QDeclarativeAudioSample*>( |
| qvariant_cast<QObject*>(m_samples[key])); |
| if (!sample) { |
| qWarning() << "accessing invalid sample[" << key << "]"; |
| continue; |
| } |
| |
| initAudioSample(sample); |
| } |
| |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "init sounds" << m_sounds.keys().count(); |
| #endif |
| const auto soundsKeys = m_sounds.keys(); |
| for (const QString& key : soundsKeys) { |
| QDeclarativeSound *sound = qobject_cast<QDeclarativeSound*>( |
| qvariant_cast<QObject*>(m_sounds[key])); |
| |
| if (!sound) { |
| qWarning() << "accessing invalid sound[" << key << "]"; |
| continue; |
| } |
| |
| initSound(sound); |
| } |
| m_complete = true; |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "AudioEngine ready."; |
| #endif |
| emit ready(); |
| } |
| |
| void QDeclarativeAudioEngine::updateSoundInstances() |
| { |
| for (QList<QDeclarativeSoundInstance*>::Iterator it = m_managedDeclSoundInstances.begin(); |
| it != m_managedDeclSoundInstances.end();) { |
| QDeclarativeSoundInstance *declSndInstance = *it; |
| if (declSndInstance->state() == QDeclarativeSoundInstance::StoppedState) { |
| it = m_managedDeclSoundInstances.erase(it); |
| releaseManagedDeclarativeSoundInstance(declSndInstance); |
| #ifdef DEBUG_AUDIOENGINE |
| qDebug() << "AudioEngine removed managed sounce instance"; |
| #endif |
| } else { |
| declSndInstance->updatePosition(qreal(0.1)); |
| ++it; |
| } |
| } |
| |
| QVector3D listenerPosition = this->listener()->position(); |
| for (QSoundInstance *instance : qAsConst(m_activeSoundInstances)) { |
| if (instance->state() == QSoundInstance::PlayingState |
| && instance->attenuationEnabled()) { |
| instance->update3DVolume(listenerPosition); |
| } |
| } |
| |
| if (m_activeSoundInstances.count() == 0) |
| m_updateTimer.stop(); |
| } |
| |
| void QDeclarativeAudioEngine::appendFunction(QQmlListProperty<QObject> *property, QObject *value) |
| { |
| QDeclarativeAudioEngine* engine = static_cast<QDeclarativeAudioEngine*>(property->object); |
| if (engine->m_complete) { |
| return; |
| } |
| |
| QDeclarativeSound *sound = qobject_cast<QDeclarativeSound*>(value); |
| if (sound) { |
| engine->addSound(sound); |
| return; |
| } |
| |
| QDeclarativeAudioSample *sample = qobject_cast<QDeclarativeAudioSample*>(value); |
| if (sample) { |
| engine->addAudioSample(sample); |
| return; |
| } |
| |
| QDeclarativeAudioCategory *category = qobject_cast<QDeclarativeAudioCategory*>(value); |
| if (category) { |
| engine->addAudioCategory(category); |
| return; |
| } |
| |
| QDeclarativeAttenuationModel *attenModel = qobject_cast<QDeclarativeAttenuationModel*>(value); |
| if (attenModel) { |
| engine->addAttenuationModel(attenModel); |
| return; |
| } |
| |
| qWarning("Unknown child type for AudioEngine!"); |
| } |
| |
| QQmlListProperty<QObject> QDeclarativeAudioEngine::bank() |
| { |
| return QQmlListProperty<QObject>(this, 0, appendFunction, 0, 0, 0); |
| } |
| |
| /*! |
| \qmlproperty map QtAudioEngine::AudioEngine::categories |
| |
| Container of all AudioCategory instances. |
| */ |
| QObject* QDeclarativeAudioEngine::categories() |
| { |
| return &m_categories; |
| } |
| |
| /*! |
| \qmlproperty map QtAudioEngine::AudioEngine::samples |
| |
| Container of all AudioSample instances. |
| */ |
| QObject* QDeclarativeAudioEngine::samples() |
| { |
| return &m_samples; |
| } |
| |
| /*! |
| \qmlproperty map QtAudioEngine::AudioEngine::sounds |
| |
| Container of all Sound instances. |
| */ |
| QObject* QDeclarativeAudioEngine::sounds() |
| { |
| return &m_sounds; |
| } |
| |
| /*! |
| \qmlproperty QtAudioEngine::AudioListener QtAudioEngine::AudioEngine::listener |
| |
| This property holds the listener object. You can change various |
| properties to affect the 3D positioning of sounds. |
| |
| \sa AudioListener |
| */ |
| QDeclarativeAudioListener* QDeclarativeAudioEngine::listener() const |
| { |
| return m_listener; |
| } |
| |
| /*! |
| \qmlproperty real QtAudioEngine::AudioEngine::dopplerFactor |
| |
| This property holds a simple scaling for the effect of doppler shift. |
| */ |
| qreal QDeclarativeAudioEngine::dopplerFactor() const |
| { |
| return m_audioEngine->dopplerFactor(); |
| } |
| |
| void QDeclarativeAudioEngine::setDopplerFactor(qreal dopplerFactor) |
| { |
| m_audioEngine->setDopplerFactor(dopplerFactor); |
| } |
| |
| /*! |
| \qmlproperty real QtAudioEngine::AudioEngine::speedOfSound |
| |
| This property holds the reference value of the sound speed (in meters per second) |
| which will be used in doppler shift calculation. The doppler shift calculation is |
| used to emulate the change in frequency in sound that is perceived by an observer when |
| the sound source is travelling towards or away from the observer. The speed of sound |
| depends on the medium the sound is propagating through. |
| */ |
| qreal QDeclarativeAudioEngine::speedOfSound() const |
| { |
| return m_audioEngine->speedOfSound(); |
| } |
| |
| void QDeclarativeAudioEngine::setSpeedOfSound(qreal speedOfSound) |
| { |
| m_audioEngine->setSpeedOfSound(speedOfSound); |
| } |
| |
| /*! |
| \qmlproperty bool QtAudioEngine::AudioEngine::loading |
| |
| This property indicates if the audio engine is loading any audio sample at the moment. This may |
| be useful if you specified the preloaded property in AudioSample and would like to show a loading screen |
| to the user before all audio samples are loaded. |
| |
| /sa finishedLoading, AudioSample::preloaded |
| */ |
| bool QDeclarativeAudioEngine::isLoading() const |
| { |
| return m_audioEngine->isLoading(); |
| } |
| |
| void QDeclarativeAudioEngine::handleLoadingChanged() |
| { |
| if (!isLoading()) |
| emit finishedLoading(); |
| } |
| |
| /*! |
| \qmlsignal QtAudioEngine::AudioEngine::finishedLoading() |
| |
| This signal is emitted when \l loading has completed. |
| |
| The corresponding handler is \c onFinishedLoading. |
| */ |
| |
| /*! |
| \qmlsignal QtAudioEngine::AudioEngine::ready() |
| |
| This signal is emitted when the AudioEngine is ready to use. |
| |
| The corresponding handler is \c onReady. |
| */ |
| |
| /*! |
| \qmlsignal QtAudioEngine::AudioEngine::liveInstanceCountChanged() |
| |
| This signal is emitted when the number of live instances managed by the |
| AudioEngine is changed. |
| |
| The corresponding handler is \c onLiveInstanceCountChanged. |
| */ |
| |
| /*! |
| \qmlsignal QtAudioEngine::AudioEngine::isLoadingChanged() |
| |
| This signal is emitted when the \l loading property changes. |
| |
| The corresponding handler is \c onIsLoadingChanged. |
| */ |
| |
| QT_END_NAMESPACE |