blob: 96046fa3145ec4e77c950aa9597031e503fb9a5b [file] [log] [blame]
/****************************************************************************
**
** 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