diff --git a/src/Magnum/Audio/Audio.h b/src/Magnum/Audio/Audio.h index fadb678e8..f6b8b8e68 100644 --- a/src/Magnum/Audio/Audio.h +++ b/src/Magnum/Audio/Audio.h @@ -49,6 +49,10 @@ template class PlayableGroup; using PlayableGroup2D = PlayableGroup<2>; using PlayableGroup3D = PlayableGroup<3>; +template class Listener; +using Listener2D = Listener<2>; +using Listener3D = Listener<3>; + }} #endif diff --git a/src/Magnum/Audio/CMakeLists.txt b/src/Magnum/Audio/CMakeLists.txt index 87b25fb09..af4034e2d 100644 --- a/src/Magnum/Audio/CMakeLists.txt +++ b/src/Magnum/Audio/CMakeLists.txt @@ -47,8 +47,12 @@ set(MagnumAudio_HEADERS if(WITH_SCENEGRAPH) list(APPEND MagnumAudio_HEADERS + Listener.h Playable.h PlayableGroup.h) + + list(APPEND MagnumAudio_SRCS + Listener.cpp) endif() # Audio library diff --git a/src/Magnum/Audio/Listener.cpp b/src/Magnum/Audio/Listener.cpp new file mode 100644 index 000000000..0f996f303 --- /dev/null +++ b/src/Magnum/Audio/Listener.cpp @@ -0,0 +1,113 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015 + Vladimír Vondruš + Copyright © 2015 Jonathan Hale + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "Magnum/Audio/Listener.h" +#include "Magnum/Audio/Playable.h" +#include "Magnum/Audio/PlayableGroup.h" + + +namespace Magnum { namespace Audio { + +using namespace SceneGraph; + +namespace Implementation { + void* activeListener = nullptr; +} + +namespace { + +/* Create a Matrix4 from a Matrix3/Matrix4 */ +inline Matrix4 padMatrix4(const Matrix3& m) { + return Matrix4{Vector4::pad(m[0]), Vector4::pad(m[1]), Vector4::pad(m[2]), Vector4{}}; +} + +inline Matrix4 padMatrix4(const Matrix4& m) { + return m; +} + +} + +template void Listener::clean(const MatrixTypeFor& absoluteTransformationMatrix) { + if(!isActive()) { + /* Only clean if this Listener is active */ + return; + } + + Renderer::setListenerPosition(soundTransformation().transformVector(Vector3::pad(absoluteTransformationMatrix.translation()))); + + const Vector3 fwd = soundTransformation().transformVector(-padMatrix4(absoluteTransformationMatrix).backward()); + const Vector3 up = soundTransformation().transformVector(padMatrix4(absoluteTransformationMatrix).up()); + + Renderer::setListenerOrientation(fwd, up); + + Renderer::setListenerGain(_gain); + + // TODO: velocity +} + +template void Listener::update(std::initializer_list>> groups) { + + /* Check if active listener just changed to this */ + if(this != Implementation::activeListener) { + /* Ensure that clean() is called also when switching between (clean) + listeners */ + Implementation::activeListener = this; + this->object().setDirty(); + + /* Listener gain needs to be updated only when active listener changed + and in setGain() */ + Renderer::setListenerGain(_gain); + } + + /* Add all objects of the Playables in the PlayableGroups to a vector to + later setClean() */ + std::vector>> objects; + + objects.push_back(this->object()); + for(PlayableGroup& group : groups) { + for(UnsignedInt i = 0; i < groups.size(); ++i) { + objects.push_back(group[i].object()); + } + } + + /* Use the more performant way to set multiple objects clean */ + AbstractObject::setClean(objects); +} + +/* On non-MinGW Windows the instantiations are already marked with extern + template */ +#if !defined(CORRADE_TARGET_WINDOWS) || defined(__MINGW32__) +#define MAGNUM_AUDIO_EXPORT_HPP MAGNUM_AUDIO_EXPORT +#else +#define MAGNUM_AUDIO_EXPORT_HPP +#endif + +#ifndef DOXYGEN_GENERATING_OUTPUT +template class MAGNUM_AUDIO_EXPORT_HPP Listener<2>; +template class MAGNUM_AUDIO_EXPORT_HPP Listener<3>; +#endif + +}} diff --git a/src/Magnum/Audio/Listener.h b/src/Magnum/Audio/Listener.h new file mode 100644 index 000000000..565aa623c --- /dev/null +++ b/src/Magnum/Audio/Listener.h @@ -0,0 +1,224 @@ +#ifndef Magnum_Audio_Listener_h +#define Magnum_Audio_Listener_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015 + Vladimír Vondruš + Copyright © 2015 Jonathan Hale + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::Audio::Listener, @ref Magnum::Audio::Listener2D, @ref Magnum::Audio::Listener3D + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include "Magnum/Audio/Renderer.h" +#include "Magnum/Audio/PlayableGroup.h" + +namespace Magnum { namespace Audio { + +namespace Implementation { + /* Pointer to the currently active listener. Should never be dereferenced, + since may be null at any given time. */ + extern MAGNUM_AUDIO_EXPORT void* activeListener; +} + +/** +@brief Listener + +Feature which manages the position, orientation and gain of the OpenAL listener +for an @ref SceneGraph::Object. + +## Usage + +Minimal scene setup for a @ref Listener3D will require the following code: + +@code +Scene3D scene; +Object3D object{&scene}; +Listener3D listener{object}; + +// ... every frame, update the listener to changes in object transformation: +listener.update({}); + +@endcode + +For two dimensional scenes simply replace all `3D` with `2D`. + +### Using Listener with PlayableGroup + +When using @ref PlayableGroup, you can update the listener and groups as +follows: + +@code +// ... +Listener3D listener{object}; +PlayableGroup3D group1, group2; + +// ... and every frame: +listener.update({group1, group2}); +@endcode + +## Active Listener + +There can only be at the most *one* active listener at a given time, i.e. the +one on which @ref Listener::update() was called last. This is because OpenAL +only supports notion of one listener. Having multiple @ref Listener2D or +@ref Listener3D instances can still be useful for conveniently switching +between them for cinematics for example. + +@ref AbstractObject::setClean() will not affect inactive listeners. + +## Sound Transformation + +@ref Listener::setSoundTransformation() enables you to set a transformation +matrix which is applied to the listeners orientation and position before passed +onto OpenAL. This can be used for rotating two dimensional scenes as planes +into the three dimensional audio space or even scaling the audio scene to match +a certain world scale. In the later case you might want to instead consider +@ref Renderer::setSpeedOfSound(). + +- @ref Listener2D +- @ref Listener3D + +@see @ref Audio::Renderer, @ref Playable, @ref PlayableGroup +*/ +template class Listener: public SceneGraph::AbstractFeature { + friend class ActiveListenerHolder; + + public: + + /** + * @brief Constructor + * @param object Object this listener belongs to + * + * Creates a listener with a forward vector of `{0.0f, 0.0f, -1.0f}` and + * up vector of `{0.0f, 1.0f, 0.0f}`. These vectors cannot be changed, + * the listeners orientation and translation can be instead affected by + * `object` or via @ref Listener::setSoundTransformation(). + * @see @ref setGain() + */ + explicit Listener(SceneGraph::AbstractObject& object): + SceneGraph::AbstractFeature(object), + _gain{1.0f} + { + SceneGraph::AbstractFeature::setCachedTransformations(SceneGraph::CachedTransformation::Absolute); + } + + /** + * @brief Sound transformation + * @see @ref Listener::setSoundTransformation() + */ + const Matrix4& soundTransformation() const { + return _soundTransformation; + } + + /** + * @brief Set sound transformation + * @param soundTransformation Transformation for transforming from + * world to listener space + * @return Reference to self (for method chaining) + * @see @ref Listener::soundTransformation() + */ + Listener& setSoundTransformation(const Matrix4& soundTransformation) { + _soundTransformation = soundTransformation; + this->object().setDirty(); + return *this; + } + + /** + * @brief Update the listener + * @param groups Groups to update + * + * Makes this Listener the active listener and calls + * @ref SceneGraph::AbstractObject::setClean() on its parent object and + * all objects of the @ref Playable s in the group. Updates listene + * related configuration for @ref Renderer (position, orientation, gain). + */ + void update(std::initializer_list>> groups); + + /** + * @brief Listener gain + * @see @ref Listener::setGain() + */ + Float gain() const { + return _gain; + } + + /** + * @brief Set listener gain + * @param gain Gain + * @return Reference to self (for method chaining) + * @see @ref Listener::gain() + */ + Listener& setGain(Float gain) { + _gain = gain; + if(isActive()) { + Renderer::setListenerGain(_gain); + } + return *this; + } + + /** @brief Whether this listener is the active listener */ + bool isActive() const { + return this == Implementation::activeListener; + } + + private: + + virtual void clean(const MatrixTypeFor& absoluteTransformationMatrix) override; + + Matrix4 _soundTransformation; + Float _gain; +}; + + +/** + * @brief Listener for two dimensional float scenes + * + * @see @ref Listener3D + */ +using Listener2D = Listener<2>; + +/** + * @brief Listener for three dimensional float scenes + * + * @see @ref Listener2D + */ +using Listener3D = Listener<3>; + +#if defined(CORRADE_TARGET_WINDOWS) && !defined(__MINGW32__) +extern template class MAGNUM_AUDIO_EXPORT Listener<2>; +extern template class MAGNUM_AUDIO_EXPORT Listener<3>; +#endif + +}} + +#endif diff --git a/src/Magnum/Audio/Test/CMakeLists.txt b/src/Magnum/Audio/Test/CMakeLists.txt index 9b2ca6463..d4c693a1a 100644 --- a/src/Magnum/Audio/Test/CMakeLists.txt +++ b/src/Magnum/Audio/Test/CMakeLists.txt @@ -36,4 +36,5 @@ corrade_add_test(AudioSourceTest SourceTest.cpp LIBRARIES MagnumAudio) if(WITH_SCENEGRAPH) corrade_add_test(AudioListenerTest ListenerTest.cpp LIBRARIES MagnumSceneGraph MagnumAudio) + corrade_add_test(AudioPlayableTest PlayableTest.cpp LIBRARIES MagnumSceneGraph MagnumAudio) endif() diff --git a/src/Magnum/Audio/Test/ListenerTest.cpp b/src/Magnum/Audio/Test/ListenerTest.cpp new file mode 100644 index 000000000..2127e15ef --- /dev/null +++ b/src/Magnum/Audio/Test/ListenerTest.cpp @@ -0,0 +1,110 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015 + Vladimír Vondruš + Copyright © 2015 Jonathan Hale + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +#include +#include +#include +#include + +#include "Magnum/Audio/Playable.h" +#include "Magnum/Audio/Context.h" +#include "Magnum/Audio/Listener.h" +#include "Magnum/Audio/PlayableGroup.h" + +namespace Magnum { namespace Audio { namespace Test { + +typedef SceneGraph::Scene Scene2D; +typedef SceneGraph::Object Object2D; + +typedef SceneGraph::Scene Scene3D; +typedef SceneGraph::Object Object3D; + +struct ListenerTest: TestSuite::Tester { + explicit ListenerTest(); + + void testFeature2D(); + void testFeature3D(); + void testUpdateGroups(); + + Context _context; +}; + +ListenerTest::ListenerTest(): _context() { + addTests({&ListenerTest::testFeature2D, + &ListenerTest::testFeature3D, + &ListenerTest::testUpdateGroups}); +} + +void ListenerTest::testFeature2D() { + Scene2D scene; + Object2D object{&scene}; + Listener2D listener{object}; + + constexpr Vector3 offset{1.0f, 2.0f, 0.0f}; + object.translate(offset.xy()); + listener.update({}); + + CORRADE_COMPARE(Renderer::listenerPosition(), offset); +} + +void ListenerTest::testFeature3D() { + Scene3D scene; + Object3D object{&scene}; + Listener3D listener{object}; + + constexpr Vector3 offset{2.0f, 4.0f, 7.0f}; + object.translate(offset); + listener.update({}); + + CORRADE_COMPARE(Renderer::listenerPosition(), offset); +} + +void ListenerTest::testUpdateGroups() { + Scene3D scene; + Object3D sourceObject{&scene}; + Object3D object{&scene}; + PlayableGroup3D group; + Playable3D playable{sourceObject, &group}; + Listener3D listener{object}; + + constexpr Vector3 offset{6.0f, 2.0f, -2.0f}; + object.rotateY(Deg(90.0f)); + object.translate(offset); + sourceObject.translate(offset*13.0f); + + listener.update({group}); + + CORRADE_COMPARE(Renderer::listenerPosition(), offset); + constexpr Vector3 rotatedFwd{-1.0f, 0.0f, 0.0f}; + CORRADE_COMPARE(Renderer::listenerOrientation()[0], rotatedFwd); + CORRADE_COMPARE(playable.source().position(), offset*13.0f); +} + +}}} + +CORRADE_TEST_MAIN(Magnum::Audio::Test::ListenerTest)