diff --git a/.gitmodules b/.gitmodules index d98a1a3adf..48dc166939 100644 --- a/.gitmodules +++ b/.gitmodules @@ -41,3 +41,6 @@ [submodule "support/doxygen/css"] path = support/doxygen/css url = https://github.com/jothepro/doxygen-awesome-css.git +[submodule "modules/audio/ext/soloud"] + path = modules/audio/ext/soloud + url = https://github.com/jarikomppa/soloud diff --git a/apps/OpenSpace/main.cpp b/apps/OpenSpace/main.cpp index f999623859..7bcbbcbaa9 100644 --- a/apps/OpenSpace/main.cpp +++ b/apps/OpenSpace/main.cpp @@ -1085,6 +1085,8 @@ std::string selectedSgctProfileFromLauncher(LauncherWindow& lw, bool hasCliSGCTC int main(int argc, char* argv[]) { + ZoneScoped; + #ifdef OPENSPACE_BREAK_ON_FLOATING_POINT_EXCEPTION _clearfp(); _controlfp(_controlfp(0, 0) & ~(_EM_ZERODIVIDE | _EM_OVERFLOW), _MCW_EM); diff --git a/data/assets/examples/audio/audio_playback.asset b/data/assets/examples/audio/audio_playback.asset new file mode 100644 index 0000000000..b236dd744d --- /dev/null +++ b/data/assets/examples/audio/audio_playback.asset @@ -0,0 +1,21 @@ +-- Lets first download a file that we can use for our examples. If you have files +-- locally, you can skip this part. +-- The song in the example comes from https://incompetech.com/ which is a fantastic +-- webpage for very high quality royalty-free music +openspace.downloadFile( + "https://incompetech.com/music/royalty-free/mp3-royaltyfree/Arcadia.mp3", + openspace.absPath("${TEMPORARY}/Arcadia.mp3"), + true +) + +local soundName = "Example_Sound_1" + +asset.onInitialize(function() + -- Start playing the song immediately + openspace.audio.playAudio(openspace.absPath("${TEMPORARY}/Arcadia.mp3"), soundName) +end) + +asset.onDeinitialize(function() + -- When we remove this asset, we want to audio to stop playing no matter what + openspace.audio.stopAudio(soundName) +end) diff --git a/data/assets/examples/audio/audio_playback_advanced.asset b/data/assets/examples/audio/audio_playback_advanced.asset new file mode 100644 index 0000000000..7ba43bdb01 --- /dev/null +++ b/data/assets/examples/audio/audio_playback_advanced.asset @@ -0,0 +1,71 @@ +-- Lets first download a file that we can use for our examples. If you have files +-- locally, you can skip this part. +-- The song in the example comes from https://incompetech.com/ which is a fantastic +-- webpage for very high quality royalty-free music +openspace.downloadFile( + "https://incompetech.com/music/royalty-free/mp3-royaltyfree/Sneaky%20Adventure.mp3", + openspace.absPath("${TEMPORARY}/SneakyAdventure.mp3"), + true +) +local soundName = "Advanced_Example_Sound_1" + +local panleft = { + Identifier = "os.examples.PanLeft", + Name = "Example Audio Pan Left", + Command = [[openspace.audio.set3dSourcePosition("Advanced_Example_Sound_1", { -1, 0, 0 })]], + GuiPath = "/Example/Audio" +} + +local pancenter = { + Identifier = "os.examples.PanCenter", + Name = "Example Audio Pan Center", + Command = [[openspace.audio.set3dSourcePosition("Advanced_Example_Sound_1", { 0, 0, 0 })]], + GuiPath = "/Example/Audio" +} + +local panright = { + Identifier = "os.examples.PanRight", + Name = "Example Audio Pan Right", + Command = [[openspace.audio.set3dSourcePosition("Advanced_Example_Sound_1", { 1, 0, 0 })]], + GuiPath = "/Example/Audio" +} + +local lowvolume = { + Identifier = "os.examples.LowVolume", + Name = "Example Audio Volume Low", + Command = [[openspace.audio.setVolume("Advanced_Example_Sound_1", 0.15)]], + GuiPath = "/Example/Audio" +} + +local fullvolume = { + Identifier = "os.examples.FullVolume", + Name = "Example Audio Volume Full", + Command = [[openspace.audio.setVolume("Advanced_Example_Sound_1", 1.0)]], + GuiPath = "/Example/Audio" +} + +asset.onInitialize(function() + -- Start playing the song immediately in 3D. Place the audio straight in front of us + openspace.audio.playAudio3d( + openspace.absPath("${TEMPORARY}/SneakyAdventure.mp3"), + soundName, + { 0.0, 0.0, 0.0 } + ) + + openspace.action.registerAction(panleft) + openspace.action.registerAction(pancenter) + openspace.action.registerAction(panright) + openspace.action.registerAction(lowvolume) + openspace.action.registerAction(fullvolume) +end) + +asset.onDeinitialize(function() + openspace.action.removeAction(fullvolume) + openspace.action.removeAction(lowvolume) + openspace.action.removeAction(panright) + openspace.action.removeAction(pancenter) + openspace.action.removeAction(panleft) + + -- When we remove this asset, we want to audio to stop playing no matter what + openspace.audio.stopAudio(soundName) +end) diff --git a/modules/audio/CMakeLists.txt b/modules/audio/CMakeLists.txt new file mode 100644 index 0000000000..76129785b6 --- /dev/null +++ b/modules/audio/CMakeLists.txt @@ -0,0 +1,58 @@ +########################################################################################## +# # +# OpenSpace # +# # +# Copyright (c) 2014-2023 # +# # +# 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(${PROJECT_SOURCE_DIR}/support/cmake/module_definition.cmake) + +set(HEADER_FILES + audiomodule.h +) +source_group("Header Files" FILES ${HEADER_FILES}) + +set(SOURCE_FILES + audiomodule.cpp + audiomodule_lua.inl +) +source_group("Source Files" FILES ${SOURCE_FILES}) + +create_new_module( + "Audio" + audio_module + STATIC + ${HEADER_FILES} ${SOURCE_FILES} ${SHADER_FILES} +) + +begin_dependency("SoLoud") +set(SOLOUD_BACKEND_SDL2 OFF CACHE BOOL "") +if (WIN32) + set(SOLOUD_BACKEND_WINMM ON CACHE BOOL "") +elseif (UNIX) + set(SOLOUD_BACKEND_ALSA ON CACHE BOOL "") +endif () +add_subdirectory(ext/soloud/contrib) + +# Unfortunately, the soloud cmake tarket doesn't set the include directories correctly +target_include_directories(openspace-module-audio SYSTEM PRIVATE ext/soloud/include) +target_link_libraries(openspace-module-audio PRIVATE soloud) +set_property(TARGET soloud PROPERTY FOLDER "External") +end_dependency("SoLoud") diff --git a/modules/audio/audiomodule.cpp b/modules/audio/audiomodule.cpp new file mode 100644 index 0000000000..7af25633e8 --- /dev/null +++ b/modules/audio/audiomodule.cpp @@ -0,0 +1,409 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * 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 "audiomodule_lua.inl" + +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "AudioModule"; + + struct [[codegen::Dictionary(AudioModule)]] Parameters { + // Sets the maximum number of simultaneous channels that can be played back by the + // audio subsystem. If this value is not specified, it defaults to 128. + std::optional maxNumberOfChannels [[codegen::greater(0)]]; + }; + +#include "audiomodule_codegen.cpp" +} // namespace + +namespace openspace { + +AudioModule::AudioModule() + : OpenSpaceModule(Name) + , _engine(std::make_unique()) +{} + +AudioModule::~AudioModule() {} + +void AudioModule::internalInitialize(const ghoul::Dictionary& dictionary) { + const Parameters p = codegen::bake(dictionary); + + LDEBUG(fmt::format("Initializing SoLoud version: {}", SOLOUD_VERSION)); + + _engine->init(); + _engine->setGlobalVolume(0.5f); + const int nChannels = p.maxNumberOfChannels.value_or(16); + _engine->setMaxActiveVoiceCount(static_cast(nChannels)); + + global::callback::postDraw->emplace_back([this]() { + if (!_sounds.empty()) { + _engine->update3dAudio(); + } + }); + + LDEBUG(fmt::format("Audio backend: {}", _engine->getBackendString())); + LDEBUG(fmt::format("Number of channels: {}", _engine->getBackendChannels())); +} + +void AudioModule::internalDeinitializeGL() { + ghoul_assert(_engine, "No audio engine loaded"); + + _sounds.clear(); + _engine->deinit(); + _engine = nullptr; +} + +std::unique_ptr AudioModule::loadSound(const std::filesystem::path& path) { + ghoul_assert(_engine, "No audio engine loaded"); + + std::unique_ptr sound = std::make_unique(); + const std::string p = path.string(); + SoLoud::result res = sound->load(p.c_str()); + if (res != 0) { + throw ghoul::RuntimeError(fmt::format( + "Error loading sound from {}. {}: {}", + path, static_cast(res), _engine->getErrorString(res) + )); + } + + // While we are loading a sound, we also want to do a little garbage collection on our + // internal data structure to remove the songs that someone has loaded at some point + // and that have since organically stopped. In general, this should only happen if the + // song was started without looping and has ended + for (auto it = _sounds.begin(); it != _sounds.end();) { + if (!isPlaying(it->first)) { + // We have found one of the candidates + LDEBUG(fmt::format("Removing song {} as it has ended", it->first)); + _sounds.erase(it); + // It is easier to just reset the iterator to the beginning than deal with the + // off-by-one error when deleting the last element in the list and the + // subsequent crash + it = _sounds.begin(); + } + else { + it++; + } + } + + return sound; +} + +void AudioModule::playAudio(const std::filesystem::path& path, std::string identifier, + ShouldLoop loop) +{ + ghoul_assert(_engine, "No audio engine loaded"); + if (_sounds.find(identifier) != _sounds.end()) { + LERROR(fmt::format("Sound with name '{}' already played", identifier)); + return; + } + + std::unique_ptr sound = loadSound(path); + sound->setLooping(loop); + SoLoud::handle handle = _engine->playBackground(*sound); + + ghoul_assert(_sounds.find(identifier) == _sounds.end(), "Handle already used"); + _sounds[identifier] = { + .sound = std::move(sound), + .handle = handle + }; +} + +void AudioModule::playAudio3d(const std::filesystem::path& path, std::string identifier, + const glm::vec3& position, ShouldLoop loop) +{ + ghoul_assert(_engine, "No audio engine loaded"); + if (_sounds.find(identifier) != _sounds.end()) { + LERROR(fmt::format("Sound with name '{}' already played", identifier)); + return; + } + + std::unique_ptr sound = loadSound(path); + sound->setLooping(loop); + SoLoud::handle handle = _engine->play3d(*sound, position.x, position.y, position.z); + + _sounds[identifier] = { + .sound = std::move(sound), + .handle = handle + }; +} + +void AudioModule::stopAudio(const std::string& identifier) { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + _engine->stop(it->second.handle); + _sounds.erase(it); + } +} + +void AudioModule::stopAll() { + ghoul_assert(_engine, "No audio engine loaded"); + + _engine->stopAll(); + _sounds.clear(); +} + +void AudioModule::pauseAll() const { + for (const std::pair& sound : _sounds) { + pauseAudio(sound.first); + } +} + +void AudioModule::resumeAll() const { + for (const std::pair& sound : _sounds) { + resumeAudio(sound.first); + } +} + +void AudioModule::playAllFromStart() const { + for (const std::pair& sound : _sounds) { + _engine->seek(sound.second.handle, 0.f); + } + resumeAll(); +} + +bool AudioModule::isPlaying(const std::string& identifier) const { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + return _engine->isValidVoiceHandle(it->second.handle); + } + else { + return false; + } +} + +void AudioModule::pauseAudio(const std::string& identifier) const { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + _engine->setPause(it->second.handle, true); + } +} + +void AudioModule::resumeAudio(const std::string& identifier) const { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + _engine->setPause(it->second.handle, false); + } +} + +bool AudioModule::isPaused(const std::string& identifier) const { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + const bool isPaused = _engine->getPause(it->second.handle); + return isPaused; + } + else { + return true; + } +} + +void AudioModule::setLooping(const std::string& identifier, ShouldLoop loop) const { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + _engine->setLooping(it->second.handle, loop); + } +} + +AudioModule::ShouldLoop AudioModule::isLooping(const std::string& identifier) const { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + return _engine->getLooping(it->second.handle) ? ShouldLoop::Yes : ShouldLoop::No; + } + else { + return ShouldLoop::No; + } +} + +void AudioModule::setVolume(const std::string& identifier, float volume, float fade) const +{ + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it == _sounds.end()) { + return; + } + + // We clamp the volume level between [0, 1] to not accidentally blow any speakers + volume = glm::clamp(volume, 0.f, 1.f); + if (fade == 0.f) { + _engine->setVolume(it->second.handle, volume); + } + else { + _engine->fadeVolume(it->second.handle, volume, fade); + } +} + +float AudioModule::volume(const std::string& identifier) const { + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + const float volume = _engine->getVolume(it->second.handle); + return volume; + } + else { + return 0.f; + } +} + +void AudioModule::set3dSourcePosition(const std::string& identifier, + const glm::vec3& position) const +{ + ghoul_assert(_engine, "No audio engine loaded"); + + auto it = _sounds.find(identifier); + if (it != _sounds.end()) { + _engine->set3dSourcePosition( + it->second.handle, + position.x, position.y, position.z + ); + } +} + +std::vector AudioModule::currentlyPlaying() const { + // This function is *technically* not the ones that are playing, but that ones that we + // are keeping track of. So we still have songs in our internal data structure that + // were started as not-looping and that have ended playing. We need to filter them out + // here. + // The alternative would be to have a periodic garbage collection running, but that + // feels worse. We are doing the garbage collection in the two playAudio functions + // instead, since we have to do some work their either way + + std::vector res; + res.reserve(_sounds.size()); + for (const std::pair& sound : _sounds) { + if (isPlaying(sound.first)) { + res.push_back(sound.first); + } + } + return res; +} + +void AudioModule::setGlobalVolume(float volume, float fade) const { + ghoul_assert(_engine, "No audio engine loaded"); + + // We clamp the volume level between [0, 1] to not accidentally blow any speakers + volume = glm::clamp(volume, 0.f, 1.f); + if (fade == 0.f) { + _engine->setGlobalVolume(volume); + } + else { + _engine->fadeGlobalVolume(volume, fade); + } +} + +float AudioModule::globalVolume() const { + ghoul_assert(_engine, "No audio engine loaded"); + return _engine->getGlobalVolume(); +} + +void AudioModule::set3dListenerParameters(const std::optional& position, + const std::optional& lookAt, + const std::optional& up) const +{ + ghoul_assert(_engine, "No audio engine loaded"); + + if (position.has_value()) { + _engine->set3dListenerPosition(position->x, position->y, position->z); + } + if (lookAt.has_value()) { + _engine->set3dListenerAt(lookAt->x, lookAt->y, lookAt->z); + } + if (up.has_value()) { + _engine->set3dListenerUp(up->x, up->y, up->z); + } +} + +void AudioModule::setSpeakerPosition(int channel, const glm::vec3& position) const { + ghoul_assert(_engine, "No audio engine loaded"); + _engine->setSpeakerPosition(channel, position.x, position.y, position.z); +} + +glm::vec3 AudioModule::speakerPosition(int channel) const { + ghoul_assert(_engine, "No audio engine loaded"); + + float x = 0.f; + float y = 0.f; + float z = 0.f; + _engine->getSpeakerPosition(channel, x, y, z); + return glm::vec3(x, y, z); +} + +std::vector AudioModule::documentations() const { + return { + }; +} + +scripting::LuaLibrary AudioModule::luaLibrary() const { + return { + "audio", + { + codegen::lua::PlayAudio, + codegen::lua::PlayAudio3d, + codegen::lua::StopAudio, + codegen::lua::StopAll, + codegen::lua::PauseAll, + codegen::lua::ResumeAll, + codegen::lua::PlayAllFromStart, + codegen::lua::IsPlaying, + codegen::lua::PauseAudio, + codegen::lua::ResumeAudio, + codegen::lua::IsPaused, + codegen::lua::SetLooping, + codegen::lua::IsLooping, + codegen::lua::SetVolume, + codegen::lua::Volume, + codegen::lua::Set3dSourcePosition, + codegen::lua::CurrentlyPlaying, + codegen::lua::SetGlobalVolume, + codegen::lua::GlobalVolume, + codegen::lua::Set3dListenerPosition, + codegen::lua::SetSpeakerPosition, + codegen::lua::SpeakerPosition + } + }; +} + +} // namespace openspace diff --git a/modules/audio/audiomodule.h b/modules/audio/audiomodule.h new file mode 100644 index 0000000000..18f5deecee --- /dev/null +++ b/modules/audio/audiomodule.h @@ -0,0 +1,315 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_SPACE___AUDIOMODULE___H__ +#define __OPENSPACE_MODULE_SPACE___AUDIOMODULE___H__ + +#include + +#include +#include +#include + +namespace SoLoud { + class Soloud; + class Wav; +} // namespace SoLoud + +namespace openspace { + +class AudioModule : public OpenSpaceModule { +public: + constexpr static const char* Name = "Audio"; + + AudioModule(); + ~AudioModule() override; + std::vector documentations() const override; + + scripting::LuaLibrary luaLibrary() const override; + + BooleanType(ShouldLoop); + + /** + * Starts playing the audio file located and the provided \p path. The \p loop + * parameter determines whether the file is only played once, or on a loop. The sound + * is later referred to by the \p identifier name. The audio file will be played in + * "background" mode, which means that each channel will be played at full volume. To + * play a video using spatial audio, use the #playAudio3d function instead. + * + * \param path The audio file that should be played + * \param identifier The name for the sound that is used to refer to the sound + * \param loop If `Yes` then the song will be played in a loop until the program is + * closed or the playing is stopped through the #stopAudio function + */ + void playAudio(const std::filesystem::path& path, std::string identifier, + ShouldLoop loop); + + /** + * Starts playing the audio file located and the provided \p path. The \p loop + * parameter determines whether the file is only played once, or on a loop. The sound + * is later referred to by the \p identifier name. The \p position parameter + * determines the spatial location of the sound in a meter-based coordinate system. + * The position of the listener is (0,0,0) with the forward direction along the +y + * axis. This means that the "left" channel in a stereo setting is towards -x and the + * "right" channel towards x. This default value can be customized through the + * #set3dListenerParameters function. If you want to play a video without spatial + * audio, use the #playAudio function instead. + * + * \param path The audio file that should be played + * \param identifier The name for the sound that is used to refer to the sound + * \param position The position of the audio file in the 3D environment + * \param loop If `Yes` then the song will be played in a loop until the program is + * closed or the playing is stopped through the #stopAudio function + */ + void playAudio3d(const std::filesystem::path& path, std::string identifier, + const glm::vec3& position, ShouldLoop loop); + + /** + * Stops the audio referenced by the \p identifier. The \p identifier must be a name + * for a sound that was started through the #playAudio or #playAudio3d functions. + * After this function, the \p identifier can not be used for any other function + * anymore except for #playAudio or #playAudio3d to start a new sound. + * + * \param identifier The identifier to the track that should be stopped + */ + void stopAudio(const std::string& identifier); + + /** + * Stops all currently playing tracks. After this function, none of the identifiers + * used to previously play a sound a valid any longer, but can still be used by the + * #playAudio or #playAudio3d functions to start a new sound. + * This function behaves the same way as if manually calling #stopAudio on all of the + * sounds that have been started. + */ + void stopAll(); + + /** + * Pauses the playback for all sounds, while keeping them valid. This function behaves + * the same as if calling #pauseAudio on all of the sounds that are currently playing. + */ + void pauseAll() const; + + /** + * Resumes the playback for all sounds that have been paused. Please note that this + * will also resume the playback for the sounds that have been manually paused, not + * just those that were paused through the #pauseAll function. + */ + void resumeAll() const; + + /** + * Takes all of the sounds that are currently registers, unpauses them and plays them + * from their starting points + */ + void playAllFromStart() const; + + /** + * Returns whether the track referred to by the \p identifier is currently playing. A + * volume of 0 is still considered to be playing. The \p identifier must be a name for + * a sound that was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \return `true` if the track is currently playing, `false` otherwise + */ + bool isPlaying(const std::string& identifier) const; + + /** + * Pauses the playback of the track referred to by the \p identifier. The playback can + * later be resumed through the #resumeAudio function. Trying to pause an already + * paused track will not do anything, but is valid. The \p identifier must be a name + * for a sound that was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + */ + void pauseAudio(const std::string& identifier) const; + + /** + * Resumes the playback of a track that was previously paused through the #pauseAudio + * function. Trying to resume an already playing track will not do anything, but is + * valid. The \p identifier must be a name for a sound that was started through the + * #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + */ + void resumeAudio(const std::string& identifier) const; + + /** + * Returns whether the track refered to by the \p identifier is currently playing or + * paused. If it was be paused through a previous call to #pauseAudio, this function + * will return `true`. If it has just been created or resumed through a call to + * #resumeAudio, it will return `false`. The \p identifier must be a name for a sound + * that was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \return `true` if the track is currently paused, `false` if it is playing + */ + bool isPaused(const std::string& identifier) const; + + /** + * Controls whether the track referred to by the \p identifier should be looping or + * just be played once. If a track is converted to not looping, it will finish playing + * until the end of the file. The \p identifier must be a name for a sound that was + * started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \param loop If `Yes` then the song will be played in a loop until the program is + * closed or the playing is stopped through the #stopAudio function + */ + void setLooping(const std::string& identifier, ShouldLoop loop) const; + + /** + * Returns whether the track referred to by the \p identifier is set to be looping or + * whether it should played only once. The \p identifier must be a name for a sound + * that was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \return `Yes` if the track is looping, `No` otherwise + */ + ShouldLoop isLooping(const std::string& identifier) const; + + /** + * Sets the volume of the track referred to by \p identifier to the new \p volume. The + * volume should be a number bigger than 0, where 1 is the maximum volume level. + * The \p fade controls whether the volume change should be immediately (if + * it is 0) or over how many seconds it should change. The default is for it to change + * over 500 ms. The \p identifier must be a name for a sound that was started through + * the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \param volume The new volume level. Must be greater or equal to 0 + * \param fade How much time the fade from the current volume to the new volume should + * take + */ + void setVolume(const std::string& identifier, float volume, float fade = 0.5f) const; + + /** + * Returns the volume for the track referred to by the \p handle. The number returned + * will be greater or equal to 0. The \p identifier must be a name for a sound that + * was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \return The volume for the track referred to by the \p handle, which will be + * greater or equal to 0 + */ + float volume(const std::string& identifier) const; + + /** + * Updates the 3D position of a track started through the #playAudio3d function. See + * that function and the #set3dListenerParameters function for a complete description. + * The \p identifier must be a name for a sound that was started through the + * #playAudio3d function. + * + * \param handle A valid handle for a track started through the #playAudio3d function + * \param position The new position from which the track originates + */ + void set3dSourcePosition(const std::string& identifier, + const glm::vec3& position) const; + + /** + * Returns the list of all tracks that are currently playing. + * + * \return The list of all tracks that are currently playing + */ + std::vector currentlyPlaying() const; + + /** + * Sets the global volume for all track referred to the new \p volume. The total + * for each track is the global volume set by this function multiplied with the volume + * for the specific track set through the #setVolume function. The default value for + * the global volume is 0.5. The volume should be a number bigger than 0, where 1 is + * the maximum volume level. The \p fade controls whether the volume change should be + * immediately (if it is 0) or over how many seconds it should change. The default is + * for it to change over 500 ms. + * + * \param volume The new volume level. Must be greater or equal to 0 + * \param fade How much time the fade from the current volume to the new volume should + * take + */ + void setGlobalVolume(float volume, float fade = 0.5f) const; + + /** + * Returns the global volume for all track. The number returned will be greater or + * equal to 0. + * + * \return The global volume + */ + float globalVolume() const; + + /** + * Sets the position and orientation of the listener. This new position is + * automatically used to adjust the relative position of all 3D tracks. Each parameter + * to this function call is optional and if a value is omitted, the currently set + * value continues to be used instead. The coordinate system for the tracks and the + * listener is a meter-based coordinate system. + * + * \param position The position of the listener. + * \param lookAt The direction vector of the forward direction + * \param up The up-vector of the coordinate system + */ + void set3dListenerParameters(const std::optional& position, + const std::optional& lookAt = std::nullopt, + const std::optional& up = std::nullopt) const; + + /** + * Sets the position of the speaker for the provided \p channel to the provided + * \position. In general, this is considered an advanced feature to accommodate + * non-standard audio environments. + * + * \param channel The channel whose speaker's position should be changed + * \param position The new position for the speaker + */ + void setSpeakerPosition(int channel, const glm::vec3& position) const; + + /** + * Returns the position for the speaker of the provided \p channel. + * \return The position for the speaker of the provided \p channel + */ + glm::vec3 speakerPosition(int channel) const; + +private: + struct Info { + std::unique_ptr sound; + unsigned int handle = 0; + }; + + void internalInitialize(const ghoul::Dictionary&) override; + void internalDeinitializeGL() override; + + /** + * Loads the sound at the provided \p path as an audio source and returns the pointer + * to it. The sound has only been loaded and no other attributes have changed. + * + * \param path The path to the audio file on disk that should be loaded + * \return The SoLoud::Wav object of the loaded file + * \throw ghoul::RuntimeError If the \p path is not a loadable audio file + */ + std::unique_ptr loadSound(const std::filesystem::path& path); + + std::unique_ptr _engine = nullptr; + + std::map _sounds; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_SPACE___AUDIOMODULE___H__ diff --git a/modules/audio/audiomodule_lua.inl b/modules/audio/audiomodule_lua.inl new file mode 100644 index 0000000000..79a1b524f3 --- /dev/null +++ b/modules/audio/audiomodule_lua.inl @@ -0,0 +1,354 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * 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 + +namespace { + +/** + * Starts playing the audio file located and the provided \p path. The \p loop parameter + * determines whether the file is only played once, or on a loop. The sound is later + * referred to by the \p identifier name. The audio file will be played in "background" + * mode, which means that each channel will be played at full volume. To play a video + * using spatial audio, use the #playAudio3d function instead. + * + * \param path The audio file that should be played + * \param identifier The name for the sound that is used to refer to the sound + * \param loop If `Yes` then the song will be played in a loop until the program is closed + * or the playing is stopped through the #stopAudio function + */ +[[codegen::luawrap]] void playAudio(std::filesystem::path path, std::string identifier, + bool shouldLoop = true) +{ + using namespace openspace; + + global::moduleEngine->module()->playAudio( + path, + std::move(identifier), + AudioModule::ShouldLoop(shouldLoop) + ); +} + +/** + * Starts playing the audio file located and the provided \p path. The \p loop parameter + * determines whether the file is only played once, or on a loop. The sound is later + * referred to by the \p identifier name. The \p position parameter determines the spatial + * location of the sound in a meter-based coordinate system. The position of the listener + * is (0,0,0) with the forward direction along the +y axis. This means that the "left" + * channel in a stereo setting is towards -x and the "right" channel towards x. This + * default value can be customized through the #set3dListenerParameters function. If you + * want to play a video without spatial audio, use the #playAudio function instead. + * + * \param path The audio file that should be played + * \param identifier The name for the sound that is used to refer to the sound + * \param position The position of the audio file in the 3D environment + * \param loop If `Yes` then the song will be played in a loop until the program is closed + * or the playing is stopped through the #stopAudio function + */ +[[codegen::luawrap]] void playAudio3d(std::filesystem::path path, std::string identifier, + glm::vec3 position, bool shouldLoop = true) +{ + using namespace openspace; + + global::moduleEngine->module()->playAudio3d( + path, + std::move(identifier), + position, + AudioModule::ShouldLoop(shouldLoop) + ); +} + +/** + * Stops the audio referenced by the \p identifier. The \p identifier must be a name for a + * sound that was started through the #playAudio or #playAudio3d functions. After this + * function, the \p identifier can not be used for any other function anymore except for + * #playAudio or #playAudio3d to start a new sound. + * + * \param identifier The identifier to the track that should be stopped + */ +[[codegen::luawrap]] void stopAudio(std::string identifier) { + using namespace openspace; + global::moduleEngine->module()->stopAudio(identifier); +} + +/** + * Stops all currently playing tracks. After this function, none of the identifiers used + * to previously play a sound a valid any longer, but can still be used by the #playAudio + * or #playAudio3d functions to start a new sound. This function behaves the same way as + * if manually calling #stopAudio on all of the sounds that have been started. + */ +[[codegen::luawrap]] void stopAll() { + using namespace openspace; + global::moduleEngine->module()->stopAll(); +} + +/** + * Pauses the playback for all sounds, while keeping them valid. This function behaves the + * same as if calling #pauseAudio on all of the sounds that are currently playing. + */ +[[codegen::luawrap]] void pauseAll() { + using namespace openspace; + global::moduleEngine->module()->pauseAll(); +} + +/** + * Resumes the playback for all sounds that have been paused. Please note that this will + * also resume the playback for the sounds that have been manually paused, not just those + * that were paused through the #pauseAll function. + */ +[[codegen::luawrap]] void resumeAll() { + using namespace openspace; + global::moduleEngine->module()->resumeAll(); +} + +/** + * Takes all of the sounds that are currently registers, unpauses them and plays them + * from their starting points + */ +[[codegen::luawrap]] void playAllFromStart() { + using namespace openspace; + global::moduleEngine->module()->playAllFromStart(); +} + +/** + * Returns whether the track referred to by the \p identifier is currently playing. A + * volume of 0 is still considered to be playing. The \p identifier must be a name for a + * sound that was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \return `true` if the track is currently playing, `false` otherwise + */ +[[codegen::luawrap]] bool isPlaying(std::string identifier) { + using namespace openspace; + return global::moduleEngine->module()->isPlaying(identifier); +} + +/** + * Pauses the playback of the track referred to by the \p identifier. The playback can + * later be resumed through the #resumeAudio function. Trying to pause an already paused + * track will not do anything, but is valid. The \p identifier must be a name for a sound + * that was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + */ +[[codegen::luawrap]] void pauseAudio(std::string identifier) { + using namespace openspace; + global::moduleEngine->module()->pauseAudio(identifier); +} + +/** + * Returns whether the track refered to by the \p identifier is currently playing or + * paused. If it was be paused through a previous call to #pauseAudio, this function will + * return `true`. If it has just been created or resumed through a call to #resumeAudio, + * it will return `false`. The \p identifier must be a name for a sound that was started + * through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \return `true` if the track is currently paused, `false` if it is playing + */ +[[codegen::luawrap]] bool isPaused(std::string identifier) { + using namespace openspace; + return global::moduleEngine->module()->isPaused(identifier); +} + +/** + * Resumes the playback of a track that was previously paused through the #pauseAudio + * function. Trying to resume an already playing track will not do anything, but is valid. + * The \p identifier must be a name for a sound that was started through the #playAudio or + * #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + */ +[[codegen::luawrap]] void resumeAudio(std::string identifier) { + using namespace openspace; + global::moduleEngine->module()->resumeAudio(identifier); +} + +/** + * Controls whether the track referred to by the \p identifier should be looping or just + * be played once. If a track is converted to not looping, it will finish playing until + * the end of the file. The \p identifier must be a name for a sound that was started + * through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \param loop If `Yes` then the song will be played in a loop until the program is closed + * or the playing is stopped through the #stopAudio function + */ +[[codegen::luawrap]] void setLooping(std::string identifier, bool shouldLoop) { + using namespace openspace; + global::moduleEngine->module()->setLooping( + identifier, + AudioModule::ShouldLoop(shouldLoop) + ); +} + +/** + * Returns whether the track referred to by the \p identifier is set to be looping or + * whether it should played only once. The \p identifier must be a name for a sound that + * was started through the #playAudio or #playAudio3d functions. + * + * \param identifier The identifier to the track that should be stopped + * \return `Yes` if the track is looping, `No` otherwise + */ +[[codegen::luawrap]] bool isLooping(std::string identifier) { + using namespace openspace; + return global::moduleEngine->module()->isLooping(identifier); +} + +/** + * Sets the volume of the track referred to by \p handle to the new \p volume. The volume + * should be a number bigger than 0, where 1 is the maximum volume level. The \p fade + * controls whether the volume change should be immediately (if it is 0) or over how many + * seconds it should change. The default is for it to change over 500 ms. + * + * \param handle The handle to the track whose volume should be changed + * \param volume The new volume level. Must be greater or equal to 0 + * \param fade How much time the fade from the current volume to the new volume should + * take + */ +[[codegen::luawrap]] void setVolume(std::string identifier, float volume, + float fade = 0.5f) +{ + using namespace openspace; + global::moduleEngine->module()->setVolume(identifier, volume, fade); +} + +/** + * Returns the volume for the track referred to by the \p handle. The number returned will + * be greater or equal to 0. + * + * \return The volume for the track referred to by the \p handle, which will be + * greater or equal to 0 + */ +[[codegen::luawrap]] float volume(std::string identifier) { + using namespace openspace; + return global::moduleEngine->module()->volume(identifier); +} + +/** + * Updates the 3D position of a track started through the #playAudio3d function. See that + * function and the #set3dListenerParameters function for a complete description. The + * \p identifier must be a name for a sound that was started through the #playAudio3d + * function. + * + * \param handle A valid handle for a track started through the #playAudio3d function + * \param position The new position from which the track originates + */ +[[codegen::luawrap]] void set3dSourcePosition(std::string identifier, + glm::vec3 position) +{ + using namespace openspace; + global::moduleEngine->module()->set3dSourcePosition( + identifier, + position + ); +} + +/** + * Returns the list of all tracks that are currently playing. + * + * \return The list of all tracks that are currently playing + */ +[[codegen::luawrap]] std::vector currentlyPlaying() { + using namespace openspace; + return global::moduleEngine->module()->currentlyPlaying(); +} + +/** + * Sets the global volume for all track referred to the new \p volume. The total for each + * track is the global volume set by this function multiplied with the volume for the + * specific track set through the #setVolume function. The default value for the global + * volume is 0.5. The volume should be a number bigger than 0, where 1 is the maximum + * volume level. The \p fade controls whether the volume change should be immediately (if + * it is 0) or over how many seconds it should change. The default is for it to change + * over 500 ms. + * + * \param volume The new volume level. Must be greater or equal to 0 + * \param fade How much time the fade from the current volume to the new volume should + * take + */ +[[codegen::luawrap]] void setGlobalVolume(float volume, float fade = 0.5f) { + using namespace openspace; + global::moduleEngine->module()->setGlobalVolume(volume, fade); +} + +/** + * Returns the global volume for all track. The number returned will be greater or equal + * to 0. + * + * \return The global volume + */ +[[codegen::luawrap]] float globalVolume() { + using namespace openspace; + return global::moduleEngine->module()->globalVolume(); +} + +/** + * Sets the position and orientation of the listener. This new position is automatically + * used to adjust the relative position of all 3D tracks. Each parameter to this function + * call is optional and if a value is omitted, the currently set value continues to be + * used instead. The coordinate system for the tracks and the listener is a meter-based + * coordinate system. + * + * \param position The position of the listener. + * \param lookAt The direction vector of the forward direction + * \param up The up-vector of the coordinate system + */ +[[codegen::luawrap]] void set3dListenerPosition(glm::vec3 position, + std::optional lookAt, + std::optional up) +{ + using namespace openspace; + global::moduleEngine->module()->set3dListenerParameters( + position, + lookAt, + up + ); +} + +/** + * Sets the position of the speaker for the provided \p channel to the provided + * \p position. In general, this is considered an advanced feature to accommodate + * non-standard audio environments. + * + * \param channel The channel whose speaker's position should be changed + * \param position The new position for the speaker + */ +[[codegen::luawrap]] void setSpeakerPosition(int handle, glm::vec3 position) { + using namespace openspace; + global::moduleEngine->module()->setSpeakerPosition(handle, position); +} + +/** + * Returns the position for the speaker of the provided \p channel. + * \return The position for the speaker of the provided \p channel + */ +[[codegen::luawrap]] glm::vec3 speakerPosition(int handle) { + using namespace openspace; + return global::moduleEngine->module()->speakerPosition(handle); +} + +#include "audiomodule_lua_codegen.cpp" + +} // namespace diff --git a/modules/audio/ext/soloud b/modules/audio/ext/soloud new file mode 160000 index 0000000000..1157475881 --- /dev/null +++ b/modules/audio/ext/soloud @@ -0,0 +1 @@ +Subproject commit 1157475881da0d7f76102578255b937c7d4e8f57 diff --git a/modules/globebrowsing/globebrowsingmodule.cpp b/modules/globebrowsing/globebrowsingmodule.cpp index 1a63f355b2..934d855f1b 100644 --- a/modules/globebrowsing/globebrowsingmodule.cpp +++ b/modules/globebrowsing/globebrowsingmodule.cpp @@ -181,7 +181,6 @@ namespace { } struct [[codegen::Dictionary(GlobeBrowsingModule)]] Parameters { - // [[codegen::verbatim(TileCacheSizeInfo.description)]] std::optional tileCacheSize; diff --git a/modules/sound/ext/soloud b/modules/sound/ext/soloud new file mode 160000 index 0000000000..1157475881 --- /dev/null +++ b/modules/sound/ext/soloud @@ -0,0 +1 @@ +Subproject commit 1157475881da0d7f76102578255b937c7d4e8f57