diff --git a/.gitmodules b/.gitmodules index 1ae63625e..4e418cabf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -56,3 +56,9 @@ [submodule "externals/openvr"] path = externals/openvr url = https://github.com/ValveSoftware/openvr.git +[submodule "externals/openal-soft"] + path = externals/openal-soft + url = https://github.com/kcat/openal-soft.git +[submodule "externals/libsndfile"] + path = externals/libsndfile + url = https://github.com/libsndfile/libsndfile.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8820e6fa0..924ccb687 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,6 +143,18 @@ else() SET(VistaCoreLibs_DIR ${COSMOSCOUT_EXTERNALS_DIR}/share/VistaCoreLibs/cmake) endif() +if (DEFINED ENV{OPENAL_ROOT_DIR}) + SET(ENV{OPENALDIR} "$ENV{OPENAL_ROOT_DIR}") +else() + SET(ENV{OPENALDIR} ${COSMOSCOUT_EXTERNALS_DIR}) +endif() + +if (DEFINED ENV{LIBSNDFILE_ROOT_DIR}) + SET(ENV{LIBSNDFILE_DIR} "$ENV{LIBSNDFILE_ROOT_DIR}") +else() + SET(ENV{LIBSNDFILE_DIR} ${COSMOSCOUT_EXTERNALS_DIR}) +endif() + find_package(GLM REQUIRED) find_package(GLI REQUIRED) find_package(DOCTEST REQUIRED) @@ -161,6 +173,8 @@ find_package(CIVETWEB REQUIRED) find_package(VistaCoreLibs REQUIRED COMPONENTS "VistaBase" "VistaKernel" "VistaKernelOpenSGExt" "VistaOGLExt" ) +find_package(OpenAL REQUIRED) +find_package(SndFile REQUIRED) # X11 is used on Linux to set the application window's name and icon. if (UNIX) @@ -309,6 +323,7 @@ include_directories( ${CMAKE_BINARY_DIR}/src/cs-core ${CMAKE_BINARY_DIR}/src/cs-utils ${CMAKE_BINARY_DIR}/src/cs-graphics + ${CMAKE_BINARY_DIR}/src/cs-audio ${CMAKE_BINARY_DIR}/src/cs-gui ${CMAKE_BINARY_DIR}/src/cs-scene ) diff --git a/cmake/FindSndFile.cmake b/cmake/FindSndFile.cmake new file mode 100644 index 000000000..94239c89c --- /dev/null +++ b/cmake/FindSndFile.cmake @@ -0,0 +1,33 @@ +# ------------------------------------------------------------------------------------------------ # +# This file is part of CosmoScout VR # +# ------------------------------------------------------------------------------------------------ # + +# SPDX-FileCopyrightText: German Aerospace Center (DLR) +# SPDX-License-Identifier: MIT + +# Locate header. +find_path(SNDFILE_INCLUDE_DIR sndfile.h + HINTS $ENV{LIBSNDFILE_DIR}/include) + + +find_library(SNDFILE_LIBRARY + NAMES sndfile + HINTS $ENV{LIBSNDFILE_DIR}/lib) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SndFile DEFAULT_MSG SNDFILE_INCLUDE_DIR SNDFILE_LIBRARY) + +# Add imported target. +if(SndFile_FOUND) + set(SNDFILE_INCLUDE_DIRS "${SNDFILE_INCLUDE_DIR}") + + if(NOT SndFile_FIND_QUIETLY) + message(STATUS "SNDFILE_INCLUDE_DIRS .............. ${SNDFILE_INCLUDE_DIR}") + endif() + + if(NOT TARGET sndfile::sndfile) + add_library(sndfile::sndfile INTERFACE IMPORTED) + set_target_properties(sndfile::sndfile PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${SNDFILE_INCLUDE_DIRS}") + endif() +endif() diff --git a/externals/libsndfile b/externals/libsndfile new file mode 160000 index 000000000..e486f20fd --- /dev/null +++ b/externals/libsndfile @@ -0,0 +1 @@ +Subproject commit e486f20fd4b1c7490cde84f22635e1c267ae882b diff --git a/externals/openal-soft b/externals/openal-soft new file mode 160000 index 000000000..4527b8737 --- /dev/null +++ b/externals/openal-soft @@ -0,0 +1 @@ +Subproject commit 4527b873788373edb630046b0ab586255aa15e44 diff --git a/make_externals.bat b/make_externals.bat index 6fd353e02..ab1feed9e 100644 --- a/make_externals.bat +++ b/make_externals.bat @@ -423,7 +423,29 @@ if %USING_NINJA%==true ( cmake -E copy "%BUILD_DIR%/cef/libcef_dll_wrapper/%BUILD_TYPE%/libcef_dll_wrapper.lib" "%INSTALL_DIR%/lib" ) -rem ------------------------------------------------------------------------------------------------ +rem openal-soft -------------------------------------------------------------------------------------- +:openal-soft + +echo. +echo Building and installing openal-soft ... +echo. + +cmake -E make_directory "%BUILD_DIR%/openal-soft" && cd "%BUILD_DIR%/openal-soft" +cmake %CMAKE_FLAGS% -DCMAKE_BUILD_TYPE=%BUILD_TYPE% -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%"^ + -DALSOFT_INSTALL_EXAMPLES=Off -DALSOFT_EXAMPLES=Off "%EXTERNALS_DIR%/openal-soft" || goto :error +cmake --build . --config %BUILD_TYPE% --target install --parallel %NUMBER_OF_PROCESSORS% || goto :error + +rem libsndfile ---------------------------------------------------------------------------------------- +:libsndfile + +echo. +echo Building and installing libsndfile ... +echo. + +cmake -E make_directory "%BUILD_DIR%/libsndfile" && cd "%BUILD_DIR%/libsndfile" +cmake %CMAKE_FLAGS% -DCMAKE_BUILD_TYPE=%BUILD_TYPE% -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%"^ + -DBUILD_SHARED_LIBS=On "%EXTERNALS_DIR%/libsndfile" || goto :error +cmake --build . --config %BUILD_TYPE% --target install --parallel %NUMBER_OF_PROCESSORS% || goto :error :finish echo Finished successfully. diff --git a/make_externals.sh b/make_externals.sh index ce1fadf4b..7de79c6c7 100755 --- a/make_externals.sh +++ b/make_externals.sh @@ -318,6 +318,29 @@ cmake -E copy_directory "$BUILD_DIR/cef/extracted/$CEF_DIR/Resources" "$INSTALL cmake -E copy_directory "$BUILD_DIR/cef/extracted/$CEF_DIR/Release" "$INSTALL_DIR/lib" cmake -E copy "$BUILD_DIR/cef/libcef_dll_wrapper/libcef_dll_wrapper.a" "$INSTALL_DIR/lib" +# openal-soft -------------------------------------------------------------------------------------- + +echo "" +echo "Building and installing openal-soft ..." +echo "" + +cmake -E make_directory "$BUILD_DIR/openal-soft" && cd "$BUILD_DIR/openal-soft" +cmake "${CMAKE_FLAGS[@]}" -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ + -DALSOFT_INSTALL_EXAMPLES=Off -DALSOFT_EXAMPLES=Off "$EXTERNALS_DIR/openal-soft" +cmake --build . --config $BUILD_TYPE --target install --parallel $NUMBER_OF_PROCESSORS + +# libsndfile ---------------------------------------------------------------------------------------- + +echo "" +echo "Building and installing libsndfile ..." +echo "" + +cmake -E make_directory "$BUILD_DIR/libsndfile" && cd "$BUILD_DIR/libsndfile" +cmake "${CMAKE_FLAGS[@]}" -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ + -DBUILD_SHARED_LIBS=On -DENABLE_EXTERNAL_LIBS=Off "$EXTERNALS_DIR/libsndfile" +cmake --build . --config $BUILD_TYPE --target install --parallel $NUMBER_OF_PROCESSORS + + # -------------------------------------------------------------------------------------------------- if [ -e "$INSTALL_DIR/lib64" ]; then diff --git a/plugins/csp-lod-bodies/src/TileTextureArray.cpp b/plugins/csp-lod-bodies/src/TileTextureArray.cpp index 47dadedd1..e81afe3a4 100644 --- a/plugins/csp-lod-bodies/src/TileTextureArray.cpp +++ b/plugins/csp-lod-bodies/src/TileTextureArray.cpp @@ -95,7 +95,7 @@ TileDataType TileTextureArray::getDataType() const { //////////////////////////////////////////////////////////////////////////////////////////////////// void TileTextureArray::allocateGPU(std::shared_ptr data) { - assert(rdata->getTexLayer() < 0); + assert(data->getTexLayer() < 0); mUploadQueue.push_back(std::move(data)); } diff --git a/plugins/csp-lod-bodies/src/TreeManager.cpp b/plugins/csp-lod-bodies/src/TreeManager.cpp index 3bf21f0ae..3d7e9b100 100644 --- a/plugins/csp-lod-bodies/src/TreeManager.cpp +++ b/plugins/csp-lod-bodies/src/TreeManager.cpp @@ -296,7 +296,7 @@ void TreeManager::prune() { if (count > 0) { #if !defined(NDEBUG) && !defined(VISTAPLANET_NO_VERBOSE) - vstr::outi() << "[TreeManager::prune] nodes removed/kept " << count << " / " << mRdMap.size() + vstr::outi() << "[TreeManager::prune] nodes removed/kept " << count << " / " << mNodes.size() << std::endl; #endif } @@ -320,7 +320,7 @@ void TreeManager::merge() { for (auto& node : mergeNodes) { assert(node != nullptr); - assert(node->getTile() != nullptr); + // assert(node->getTile() != nullptr); if (insertNode(&mTree, node)) { onNodeInserted(node); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1a941d22a..f9a9f3299 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(cs-utils) add_subdirectory(cs-core) add_subdirectory(cs-graphics) +add_subdirectory(cs-audio) add_subdirectory(cs-gui) add_subdirectory(cs-scene) add_subdirectory(cosmoscout) diff --git a/src/cosmoscout/Application.cpp b/src/cosmoscout/Application.cpp index 073add45f..f5bcba6ba 100644 --- a/src/cosmoscout/Application.cpp +++ b/src/cosmoscout/Application.cpp @@ -115,6 +115,7 @@ bool Application::Init(VistaSystem* pVistaSystem) { mSolarSystem = std::make_shared(mSettings, mGraphicsEngine, mTimeControl); mDragNavigation = std::make_unique(mSolarSystem, mInputManager, mTimeControl); + mAudioEngine = std::make_shared(mSettings, mGuiManager); // The ObserverNavigationNode is used by several DFN networks to move the celestial observer. VdfnNodeFactory* pNodeFactory = VdfnNodeFactory::GetSingleton(); @@ -212,6 +213,9 @@ void Application::Quit() { assertCleanUp("mTimeControl", mTimeControl.use_count()); mTimeControl.reset(); + assertCleanUp("mAudioEngine", mAudioEngine.use_count()); + mAudioEngine.reset(); + assertCleanUp("mGuiManager", mGuiManager.use_count()); mGuiManager.reset(); @@ -558,6 +562,12 @@ void Application::FrameUpdate() { cs::utils::FrameStats::ScopedTimer timer("Update Graphics Engine"); mGraphicsEngine->update(glm::normalize(mSolarSystem->pSunPosition.get())); } + + // Update the AudioEngine + { + cs::utils::FrameStats::ScopedTimer timer("Update Audio Engine"); + mAudioEngine->update(); + } } // Update the user interface. @@ -755,7 +765,8 @@ void Application::initPlugin(std::string const& name) { // First provide the plugin with all required class instances. plugin->second.mPlugin->setAPI(mSettings, mSolarSystem, mGuiManager, mInputManager, - GetVistaSystem()->GetGraphicsManager()->GetSceneGraph(), mGraphicsEngine, mTimeControl); + GetVistaSystem()->GetGraphicsManager()->GetSceneGraph(), mGraphicsEngine, mAudioEngine, + mTimeControl); // Then do the actual initialization. This may actually take a while and the application will // become unresponsive in the meantime. diff --git a/src/cosmoscout/Application.hpp b/src/cosmoscout/Application.hpp index 4a9f0070d..5a44d0a93 100644 --- a/src/cosmoscout/Application.hpp +++ b/src/cosmoscout/Application.hpp @@ -8,6 +8,8 @@ #ifndef CS_APPLICATION_HPP #define CS_APPLICATION_HPP +#include "../cs-core/AudioEngine.hpp" + #include #include #include @@ -33,6 +35,7 @@ class GraphicsEngine; class TimeControl; class SolarSystem; class DragNavigation; +// class AudioEngine; } // namespace cs::core namespace cs::graphics { @@ -173,6 +176,7 @@ class Application : public VistaFrameLoop { std::shared_ptr mTimeControl; std::shared_ptr mSolarSystem; std::unique_ptr mDragNavigation; + std::shared_ptr mAudioEngine; std::map mPlugins; std::unique_ptr mDownloader; std::unique_ptr mSceneSync; diff --git a/src/cs-audio/AudioController.cpp b/src/cs-audio/AudioController.cpp new file mode 100644 index 000000000..4d66f01f0 --- /dev/null +++ b/src/cs-audio/AudioController.cpp @@ -0,0 +1,238 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "AudioController.hpp" +#include "../cs-utils/FrameStats.hpp" +#include "Source.hpp" +#include "SourceGroup.hpp" +#include "StreamingSource.hpp" +#include "internal/BufferManager.hpp" +#include "internal/ProcessingStepsManager.hpp" +#include "internal/SettingsMixer.hpp" +#include "internal/UpdateInstructor.hpp" + +namespace cs::audio { + +AudioController::AudioController(std::shared_ptr bufferManager, + std::shared_ptr processingStepsManager, + std::shared_ptr updateConstructor, int id) + : SourceSettings() + , std::enable_shared_from_this() + , mControllerId(id) + , mBufferManager(std::move(bufferManager)) + , mProcessingStepsManager(std::move(processingStepsManager)) + , mSources(std::vector>()) + , mStreams(std::vector>()) + , mGroups(std::vector>()) + , mUpdateInstructor(std::make_shared()) + , mUpdateConstructor(std::move(updateConstructor)) { + setUpdateInstructor(mUpdateInstructor); +} + +AudioController::AudioController() + : SourceSettings(false) + , std::enable_shared_from_this() + , mControllerId(-1) { +} + +AudioController::~AudioController() { + if (mIsLeader) { + mProcessingStepsManager->removeAudioController(mControllerId); + mSources.clear(); + mStreams.clear(); + mGroups.clear(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::shared_ptr AudioController::createSourceGroup() { + if (!mIsLeader) { + return std::make_shared(); + } + auto group = + std::make_shared(mUpdateInstructor, mUpdateConstructor, shared_from_this()); + mGroups.push_back(group); + return group; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::shared_ptr AudioController::createSource(std::string file) { + if (!mIsLeader) { + return std::make_shared(); + } + auto source = std::make_shared(mBufferManager, file, mUpdateInstructor); + mSources.push_back(source); + + // apply audioController settings to newly creates source + if (!mCurrentSettings->empty()) { + mUpdateConstructor->applyCurrentControllerSettings( + source, shared_from_this(), mCurrentSettings); + } + return source; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::shared_ptr AudioController::createStreamingSource( + std::string file, int bufferSize, int queueSize) { + + if (!mIsLeader) { + return std::make_shared(); + } + auto source = std::make_shared(file, bufferSize, queueSize, mUpdateInstructor); + mSources.push_back(source); + mStreams.push_back(source); + + // apply audioController settings to newly creates source + if (!mCurrentSettings->empty()) { + mUpdateConstructor->applyCurrentControllerSettings( + source, shared_from_this(), mCurrentSettings); + } + return source; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioController::setPipeline(std::vector processingSteps) { + if (!mIsLeader) { + return; + } + mProcessingStepsManager->createPipeline(processingSteps, mControllerId); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioController::update() { + if (!mIsLeader) { + return; + } + auto frameStats = cs::utils::FrameStats::ScopedTimer( + "AudioEngineController", cs::utils::FrameStats::TimerMode::eCPU); + + auto updateInstructions = mUpdateInstructor->createUpdateInstruction(); + + // update every source and group with plugin settings + if (updateInstructions.updateAll) { + mUpdateConstructor->updateAll( + std::make_shared>>(getSources()), + std::make_shared>>(getGroups()), + shared_from_this()); + return; + } + + // update changed groups with member sources + if (updateInstructions.updateWithGroup->size() > 0) { + mUpdateConstructor->updateGroups(updateInstructions.updateWithGroup, + std::make_shared>>(getGroups()), + shared_from_this()); + } + + // update leftover changed sources + if (updateInstructions.updateSourceOnly->size() > 0) { + mUpdateConstructor->updateSources(updateInstructions.updateSourceOnly, shared_from_this()); + } +} + +void AudioController::updateStreamingSources() { + if (!mIsLeader) { + return; + } + bool streamExpired = false; + for (auto stream : mStreams) { + if (stream.expired()) { + streamExpired = true; + continue; + } + if (stream.lock()->updateStream()) { + update(); + } + } + if (streamExpired) { + removeExpiredElements(mStreams); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector> AudioController::getSources() { + if (!mIsLeader) { + std::vector>(); + } + std::vector> sourcesShared; + bool sourceExpired = false; + + for (auto source : mSources) { + if (source.expired()) { + sourceExpired = true; + continue; + } + sourcesShared.push_back(source.lock()); + } + + if (sourceExpired) { + removeExpiredElements(mSources); + } + + return sourcesShared; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector> AudioController::getGroups() { + if (!mIsLeader) { + std::vector>(); + } + std::vector> groupsShared; + bool groupExpired = false; + + for (auto group : mGroups) { + if (group.expired()) { + groupExpired = true; + continue; + } + groupsShared.push_back(group.lock()); + } + + if (groupExpired) { + removeExpiredElements(mGroups); + } + return groupsShared; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +template +void AudioController::removeExpiredElements(std::vector> elements) { + elements.erase(std::remove_if(elements.begin(), elements.end(), + [](const std::weak_ptr& ptr) { return ptr.expired(); }), + elements.end()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const int AudioController::getControllerId() const { + if (!mIsLeader) { + return 0; + } + return mControllerId; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioController::addToUpdateList() { + mUpdateInstructor->update(shared_from_this()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioController::removeFromUpdateList() { + mUpdateInstructor->removeUpdate(shared_from_this()); +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/AudioController.hpp b/src/cs-audio/AudioController.hpp new file mode 100644 index 000000000..8e3724cd5 --- /dev/null +++ b/src/cs-audio/AudioController.hpp @@ -0,0 +1,116 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_AUDIO_CONTROLLER_HPP +#define CS_AUDIO_AUDIO_CONTROLLER_HPP + +#include "Source.hpp" +#include "SourceGroup.hpp" +#include "StreamingSource.hpp" +#include "cs_audio_export.hpp" +#include "internal/BufferManager.hpp" +#include "internal/SourceBase.hpp" +#include "internal/UpdateInstructor.hpp" + +#include +#include +#include +#include + +namespace cs::audio { + +// forward declarations +class ProcessingStepsManager; + +/// @brief This class is the gateway to create audio objects and to optionally define a processing +/// pipeline for these objects. It is recommended that each use case for audio should have +/// it's own AudioController, for example each plugin should have it's own and/or a separation of +/// different sources, like spatialized sources in space and ambient background music. This is +/// recommended because each use case will most probably require a different pipeline, which if +/// configured correctly, could benefit performance. +class CS_AUDIO_EXPORT AudioController : public SourceSettings, + public std::enable_shared_from_this { + + public: + /// @brief This is the standard constructor used for non-cluster mode and cluster mode leader + /// calls + AudioController(std::shared_ptr bufferManager, + std::shared_ptr processingStepsManager, + std::shared_ptr updateConstructor, int id); + /// @brief This Constructor will create a dummy controller which is used when a member of a + /// cluster tries to create an AudioController. Doing this will disable any functionality of this + /// class. + AudioController(); + ~AudioController(); + + /// @brief Creates a new audio source + /// @return Pointer to the new source + std::shared_ptr createSource(std::string file); + + /// @brief Creates a new streaming audio source + /// @param file audio file to stream + /// @param bufferLength time in milliseconds of each buffer + /// @param queueSize number of buffers used for the stream + /// @return Pointer to the new source + std::shared_ptr createStreamingSource( + std::string file, int bufferLength = 200, int queueSize = 4); + + /// @brief Creates a new audio source group + /// @return Pointer to the new source group + std::shared_ptr createSourceGroup(); + + /// @brief Defines a new pipeline for the audioController + /// @param processingSteps list of all processing steps that should be part of the pipeline + void setPipeline(std::vector processingSteps); + + /// @brief Calls the pipeline for all newly set settings for the audioController, Groups and + /// Sources since the last update call. + void update(); + + void updateStreamingSources(); + + /// @return A list of all sources which live on the audioController + std::vector> getSources(); + + /// @return A list of all groups which live on the audioController + std::vector> getGroups(); + + /// @return ID of the controller. Only useful for internal AudioEngine stuff. + const int getControllerId() const; + + private: + const int mControllerId; + /// Ptr to the single BufferManager of the audioEngine + std::shared_ptr mBufferManager; + /// Ptr to the single ProcessingStepsManager of the audioEngine + std::shared_ptr mProcessingStepsManager; + /// List of all Sources that live on the AudioController + std::vector> mSources; + /// List of Streaming Sources that live on the AudioController + std::vector> mStreams; + /// List of all Groups that live on the AudioController + std::vector> mGroups; + /// Ptr to the UpdateInstructor. Each AudioController has their own Instructor + std::shared_ptr mUpdateInstructor; + /// Ptr to the single UpdateConstructor of the audioEngine + std::shared_ptr mUpdateConstructor; + + /// @brief registers itself to the updateInstructor to be updated + void addToUpdateList() override; + /// @brief deregister itself from the updateInstructor + void removeFromUpdateList() override; + + /// @brief Removes expired weak_ptr from a vector. + /// @tparam T SourceBase, StreamingSource, SourceGroup + /// @param elements vector to remove from + template + void removeExpiredElements(std::vector> elements); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_AUDIO_CONTROLLER_HPP diff --git a/src/cs-audio/AudioUtil.cpp b/src/cs-audio/AudioUtil.cpp new file mode 100644 index 000000000..457a5948a --- /dev/null +++ b/src/cs-audio/AudioUtil.cpp @@ -0,0 +1,209 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "AudioUtil.hpp" +#include "../cs-scene/CelestialAnchor.hpp" +#include "../cs-scene/CelestialSurface.hpp" +#include "../cs-utils/convert.hpp" +#include "logger.hpp" +#include +#include + +namespace cs::audio { + +double AudioUtil::getObserverScaleAt( + glm::dvec3 position, double ObserverScale, std::shared_ptr settings) { + + // First we have to find the planet which is closest to the position. + std::shared_ptr closestObject; + double dClosestDistance = std::numeric_limits::max(); + + // Here we will store the position of the source relative to the closestObject. + glm::dvec3 vClosestPlanetPosition(0.0); + + for (auto const& [name, object] : settings->mObjects) { + + // Skip non-existent objects. + if (!object->getIsInExistence() || !object->getHasValidPosition() || + !object->getIsTrackable()) { + continue; + } + + // Skip objects with an unknown radius. + auto radii = object->getRadii() * object->getScale(); + if (radii.x <= 0.0 || radii.y <= 0.0 || radii.z <= 0.0) { + continue; + } + + // Finally check if the current body is closest to the source. We won't incorporate surface + // elevation in this check. + auto vObjectPosToObserver = object->getObserverRelativePosition(); + vObjectPosToObserver *= static_cast(ObserverScale); + + glm::dvec3 vSourcePosToObject(vObjectPosToObserver.x - position.x, + vObjectPosToObserver.y - position.y, vObjectPosToObserver.z - position.z); + double dDistance = glm::length(vSourcePosToObject) - radii[0]; + + if (dDistance < dClosestDistance) { + closestObject = object; + dClosestDistance = dDistance; + vClosestPlanetPosition = vSourcePosToObject; + } + } + + // Now that we found a closest body, we will scale the in such a way, that the closest + // body is rendered at a distance between settings->mSceneScale.mCloseVisualDistance and + // settings->mSceneScale.mFarVisualDistance (in meters). + if (closestObject) { + + // First we calculate the *real* world-space distance to the planet (incorporating surface + // elevation). + auto radii = closestObject->getRadii() * closestObject->getScale(); + auto lngLatHeight = cs::utils::convert::cartesianToLngLatHeight(vClosestPlanetPosition, radii); + double dRealDistance = lngLatHeight.z; + + if (closestObject->getSurface()) { + dRealDistance -= closestObject->getSurface()->getHeight(lngLatHeight.xy()) * + settings->mGraphics.pHeightScale.get(); + } + + if (std::isnan(dRealDistance)) { + return -1.0; + } + + // The render distance between settings->mSceneScale.mCloseVisualDistance and + // settings->mSceneScale.mFarVisualDistance is chosen based on the observer's world-space + // distance between settings->mSceneScale.mFarRealDistance and + // settings->mSceneScale.mCloseRealDistance (also in meters). + double interpolate = 1.0; + + if (settings->mSceneScale.mFarRealDistance != settings->mSceneScale.mCloseRealDistance) { + interpolate = glm::clamp( + (dRealDistance - settings->mSceneScale.mCloseRealDistance) / + (settings->mSceneScale.mFarRealDistance - settings->mSceneScale.mCloseRealDistance), + 0.0, 1.0); + } + + double dScale = dRealDistance / glm::mix(settings->mSceneScale.mCloseVisualDistance, + settings->mSceneScale.mFarVisualDistance, interpolate); + dScale = glm::clamp(dScale, settings->mSceneScale.mMinScale, settings->mSceneScale.mMaxScale); + + return dScale; + } + return -1.0; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioUtil::printAudioSettings(std::shared_ptr> map) { + for (auto [key, val] : (*map)) { + + std::cout << key << ": "; + + if (val.type() == typeid(int)) { + std::cout << std::any_cast(val) << std::endl; + continue; + } + + if (val.type() == typeid(bool)) { + std::cout << (std::any_cast(val) ? "true" : "false") << std::endl; + continue; + } + + if (val.type() == typeid(float)) { + std::cout << std::any_cast(val) << std::endl; + continue; + } + + if (val.type() == typeid(std::string)) { + std::cout << std::any_cast(val) << std::endl; + continue; + } + + if (val.type() == typeid(glm::dvec3)) { + auto v3 = std::any_cast(val); + std::cout << v3.x << ", " << v3.y << ", " << v3.z << std::endl; + continue; + } + + std::cout << "type not yet supported for printing in AudioUtil::printAudioSettings()" + << std::endl; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioUtil::printAudioSettings( + const std::shared_ptr> map) { + printAudioSettings(std::const_pointer_cast>(map)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +double inverseClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) { + return referenceDistance / (referenceDistance + rollOffFactor * (distance - referenceDistance)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +double linearClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) { + return (1 - rollOffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +double exponentClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) { + return std::pow((distance / referenceDistance), -1 * rollOffFactor); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +ALfloat AudioUtil::computeFallOffFactor(double distance, std::string model, ALfloat fallOffStart, ALfloat fallOffEnd) { + + if (distance < fallOffStart) { + logger().warn("AudioUtil::setFallOffFactor: distance cannot be smaller then the sources fallOffStart distance!"); + return -1.f; + } + + if (distance > fallOffEnd) { + logger().warn("AudioUtil::setFallOffFactor: distance cannot be larger then the sources fallOffEnd distance!"); + return -1.f; + } + + // Get function for distance calculation + double (*distanceFunction)(double, ALfloat, ALfloat, ALfloat); + + if (model == "inverse") { + distanceFunction = inverseClamped; + + } else if (model == "linear") { + distanceFunction = linearClamped; + + } else if (model == "exponent") { + distanceFunction = exponentClamped; + + } else { + logger().warn("AudioUtil::setFallOffFactor: Invalid distance model!"); + return -1.f; + } + + // Compute FallOffFactor + ALfloat fallOffFactor = 0.01f; + float stepSize = 0.1f; + float resultWindow = 0.001f; + + while (distanceFunction(distance, fallOffFactor, fallOffStart, fallOffEnd) > resultWindow) { + fallOffFactor += stepSize; + } + + return fallOffFactor; +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/AudioUtil.hpp b/src/cs-audio/AudioUtil.hpp new file mode 100644 index 000000000..fa05d8c81 --- /dev/null +++ b/src/cs-audio/AudioUtil.hpp @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_UTILS_HPP +#define CS_AUDIO_UTILS_HPP + +#include "../cs-core/Settings.hpp" +#include "cs_audio_export.hpp" +#include +#include +#include +#include +#include + +namespace cs::audio { + +class CS_AUDIO_EXPORT AudioUtil { + public: + /// @brief Computes the scale the observer would have if he would be at the provided position. + /// This can be useful as a starting point to scale a source, either as a spatialized sphere or + /// it's fallOffStart distance. If the source is far away from the next celestial object, meaning + /// the observer scale is very large, it can be very hard to navigate in such a way that the + /// source is audible because even the smallest change of postion can lead to a very large change + /// of the real world position. + /// @param position Position at which to compute the observer scale + /// @param ObserverScale Scale at the current Observer Position + /// @param settings settings + /// @return Observer scale at position + static double getObserverScaleAt( + glm::dvec3 position, double ObserverScale, std::shared_ptr settings); + + /// @brief Prints a settings map. Can, for example, be used on + /// SourceSettings::getCurrentSettings(), SourceSettings::getUpdateSettings() or + /// Source::getPlaybackSettings(). + /// @param map map to print + static void printAudioSettings(std::shared_ptr> map); + static void printAudioSettings(const std::shared_ptr> map); + + /// @brief Computes a FallOffFactor at which the gain is zero for the given distance + /// @param distance distance at which the gain of source should be zero + /// @param model attenuation shape of the distance model + /// @param fallOffStart fallOffStart of the distance model + /// @param fallOffEnd fallOffEnd of the distance model + /// @return fallOffFactor + static ALfloat computeFallOffFactor(double distance, std::string model="inverse", + ALfloat fallOffStart=1.f, ALfloat fallOffEnd=static_cast(std::numeric_limits::max())); + + private: +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_UTILS_HPP \ No newline at end of file diff --git a/src/cs-audio/CMakeLists.txt b/src/cs-audio/CMakeLists.txt new file mode 100644 index 000000000..38ecc8730 --- /dev/null +++ b/src/cs-audio/CMakeLists.txt @@ -0,0 +1,46 @@ +# ------------------------------------------------------------------------------------------------ # +# This file is part of CosmoScout VR # +# ------------------------------------------------------------------------------------------------ # + +# SPDX-FileCopyrightText: German Aerospace Center (DLR) +# SPDX-License-Identifier: MIT + +# build library ------------------------------------------------------------------------------------ + +file(GLOB SOURCE_FILES *.cpp */*.cpp) + +# Header files are only added in order to make them available in your IDE. +file(GLOB HEADER_FILES *.hpp */*.hpp) + +add_library(cs-audio SHARED + ${SOURCE_FILES} + ${HEADER_FILES} +) + +target_link_libraries(cs-audio + PUBLIC + ${SNDFILE_LIBRARY} + ${OPENAL_LIBRARY} + cs-utils + cs-scene +) + +if(COSMOSCOUT_USE_PRECOMPILED_HEADERS) + target_precompile_headers(cs-audio PRIVATE precompiled.pch) +endif() + +# Make directory structure available in your IDE. +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "src" FILES + ${SOURCE_FILES} ${HEADER_FILES} +) + +# install the library ------------------------------------------------------------------------------ + +install(TARGETS cs-audio DESTINATION lib) +install(DIRECTORY "gui" DESTINATION "share/resources") + +# export header ------------------------------------------------------------------------------------ + +generate_export_header(cs-audio + EXPORT_FILE_NAME cs_audio_export.hpp +) diff --git a/src/cs-audio/Source.cpp b/src/cs-audio/Source.cpp new file mode 100644 index 000000000..10c92c953 --- /dev/null +++ b/src/cs-audio/Source.cpp @@ -0,0 +1,109 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "Source.hpp" +#include "internal/AlErrorHandling.hpp" +#include "internal/BufferManager.hpp" +#include "internal/SettingsMixer.hpp" +#include "logger.hpp" + +#include +#include +#include +#include + +namespace cs::audio { + +Source::Source(std::shared_ptr bufferManager, std::string file, + std::shared_ptr UpdateInstructor) + : SourceBase(file, UpdateInstructor) + , mBufferManager(std::move(bufferManager)) { + + alGetError(); // clear error code + + // check if file exists + if (!std::filesystem::exists(mFile)) { + logger().warn("{} file does not exist! Unable to fill buffer!", mFile); + return; + } + // get buffer + std::pair buffer = mBufferManager->getBuffer(mFile); + if (!buffer.first) { + return; + } + // bind buffer to source + alSourcei(mOpenAlId, AL_BUFFER, buffer.second); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to bind buffer to source!"); + return; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +Source::Source() + : SourceBase() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +Source::~Source() { + if (mIsLeader) { + alSourceStop(mOpenAlId); + alSourcei(mOpenAlId, AL_BUFFER, 0); + mBufferManager->removeBuffer(mFile); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool Source::setFile(std::string file) { + if (!mIsLeader) { + return true; + } + alGetError(); // clear error code + + ALint state; + alGetSourcei(mOpenAlId, AL_SOURCE_STATE, &state); + if (state == AL_PLAYING) { + alSourceStop(mOpenAlId); + } + + // remove current buffer + alSourcei(mOpenAlId, AL_BUFFER, 0); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to remove buffer from source!"); + return false; + } + mBufferManager->removeBuffer(mFile); + + // check if file exists + if (!std::filesystem::exists(file)) { + logger().warn("{} file does not exist! Unable to fill buffer!", file); + return false; + } + mFile = file; + + // get buffer and bind buffer to source + std::pair buffer = mBufferManager->getBuffer(mFile); + if (!buffer.first) { + return false; + } + alSourcei(mOpenAlId, AL_BUFFER, buffer.second); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to bind buffer to source!"); + return false; + } + + if (state == AL_PLAYING) { + alSourcePlay(mOpenAlId); + } + + return true; +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/Source.hpp b/src/cs-audio/Source.hpp new file mode 100644 index 000000000..e179ba6d1 --- /dev/null +++ b/src/cs-audio/Source.hpp @@ -0,0 +1,48 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_SOURCE_HPP +#define CS_AUDIO_SOURCE_HPP + +#include "cs_audio_export.hpp" +#include "internal/BufferManager.hpp" +#include "internal/SourceBase.hpp" + +#include +#include +#include + +namespace cs::audio { + +// forward declaration +class SourceGroup; + +/// @brief This is the derived source class for non-streaming sources. This means that the whole +/// file is being read and written into the buffer. This has the benefit that buffers can be shared +/// among all non-streaming sources. This is done via the BufferManager. +class CS_AUDIO_EXPORT Source : public SourceBase { + public: + /// @brief This is the standard constructor used for non-cluster mode and cluster mode leader + /// calls + Source(std::shared_ptr bufferManager, std::string file, + std::shared_ptr UpdateInstructor); + /// @brief This Constructor will create a dummy source which is used when a member of a cluster + /// tries to create a Source. Doing this will disable any functionality of this class. + Source(); + ~Source(); + + /// @brief Sets a new file to be played by the source. + /// @return Whether it was successful + bool setFile(std::string file) override; + + private: + std::shared_ptr mBufferManager; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_SOURCE_HPP diff --git a/src/cs-audio/SourceGroup.cpp b/src/cs-audio/SourceGroup.cpp new file mode 100644 index 000000000..19686a10c --- /dev/null +++ b/src/cs-audio/SourceGroup.cpp @@ -0,0 +1,139 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "SourceGroup.hpp" +#include "internal/SettingsMixer.hpp" +#include "internal/SourceBase.hpp" +#include "internal/SourceSettings.hpp" +#include "internal/UpdateInstructor.hpp" +#include "logger.hpp" + +namespace cs::audio { + +SourceGroup::SourceGroup(std::shared_ptr UpdateInstructor, + std::shared_ptr updateConstructor, + std::shared_ptr audioController) + : SourceSettings(std::move(UpdateInstructor)) + , std::enable_shared_from_this() + , mMembers(std::set, WeakPtrComparatorSource>()) + , mUpdateConstructor(std::move(updateConstructor)) + , mAudioController(audioController) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SourceGroup::SourceGroup() + : SourceSettings(false) + , std::enable_shared_from_this() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SourceGroup::~SourceGroup() { + if (mIsLeader) { + reset(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceGroup::join(std::shared_ptr source) { + if (!mIsLeader) { + return; + } + if (mAudioController.expired()) { + logger().warn( + "Group warning: AudioController of group is expired! Unable to assign source to group!"); + return; + } + + auto currentGroup = source->getGroup(); + if (currentGroup != shared_from_this()) { + source->setGroup(shared_from_this()); + + mMembers.insert(source); + + // apply group settings to newly added source + if (!mCurrentSettings->empty()) { + mUpdateConstructor->applyCurrentGroupSettings( + source, mAudioController.lock(), mCurrentSettings); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceGroup::leave(std::shared_ptr sourceToRemove) { + if (!mIsLeader) { + return; + } + if (mMembers.erase(sourceToRemove) == 1) { + sourceToRemove->leaveGroup(); + + if (mAudioController.expired()) { + logger().warn("Group warning: AudioController of group is expired! Unable remove group " + "settings from source!"); + return; + } + mUpdateConstructor->removeCurrentGroupSettings(sourceToRemove, mAudioController.lock()); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceGroup::reset() { + if (!mIsLeader) { + return; + } + for (auto sourcePtr : mMembers) { + if (sourcePtr.expired()) { + continue; + } + sourcePtr.lock()->leaveGroup(); + + if (mAudioController.expired()) { + logger().warn("Group warning: AudioController of group is expired! Unable remove group " + "settings from source!"); + continue; + } + mUpdateConstructor->removeCurrentGroupSettings(sourcePtr.lock(), mAudioController.lock()); + } + mMembers.clear(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const std::vector> SourceGroup::getMembers() { + if (!mIsLeader) { + std::vector>(); + } + std::vector> membersShared(mMembers.size()); + for (auto member : mMembers) { + + if (member.expired()) { + mMembers.erase(member); + continue; + } + + membersShared.emplace_back(member.lock()); + } + return membersShared; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceGroup::addToUpdateList() { + mUpdateInstructor->update(shared_from_this()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceGroup::removeFromUpdateList() { + mUpdateInstructor->removeUpdate(shared_from_this()); +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/SourceGroup.hpp b/src/cs-audio/SourceGroup.hpp new file mode 100644 index 000000000..056133d22 --- /dev/null +++ b/src/cs-audio/SourceGroup.hpp @@ -0,0 +1,74 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_CORE_AUDIO_SOURCE_GROUP_HPP +#define CS_CORE_AUDIO_SOURCE_GROUP_HPP + +#include "cs_audio_export.hpp" +#include "internal/SourceBase.hpp" +#include "internal/SourceSettings.hpp" + +#include +#include +#include +#include +#include + +namespace cs::audio { + +// forward declarations +class UpdateInstructor; + +/// @brief A SourceGroup is a way to apply source settings to multiple sources at once. Each +/// source can only be part of one group at a time and both *MUST* be on the same audio controller. +/// Doing otherwise can lead to undefined behavior and is not intended. +class CS_AUDIO_EXPORT SourceGroup : public SourceSettings, + public std::enable_shared_from_this { + + public: + /// @brief This is the standard constructor used for non-cluster mode and cluster mode leader + /// calls + explicit SourceGroup(std::shared_ptr UpdateInstructor, + std::shared_ptr updateConstructor, + std::shared_ptr audioController); + /// @brief This Constructor will create a dummy Group which is used when a member of a cluster + /// tries to create a Group. Doing this will disable any functionality of this class. + explicit SourceGroup(); + ~SourceGroup(); + + /// @brief Adds a new source to the group + void join(std::shared_ptr source); + /// @brief Removes a source from the group + void leave(std::shared_ptr source); + /// @brief Removes all sources form the group + void reset(); + + /// @return List to all members of the group + const std::vector> getMembers(); + + private: + struct WeakPtrComparatorSource { + bool operator()( + const std::weak_ptr& left, const std::weak_ptr& right) const { + std::owner_less> sharedPtrLess; + return sharedPtrLess(left.lock(), right.lock()); + } + }; + + std::set, WeakPtrComparatorSource> mMembers; + std::shared_ptr mUpdateConstructor; + std::weak_ptr mAudioController; + + /// @brief registers itself to the updateInstructor to be updated + void addToUpdateList() override; + /// @brief deregister itself from the updateInstructor + void removeFromUpdateList() override; +}; + +} // namespace cs::audio + +#endif // CS_CORE_AUDIO_SOURCE_GROUP_HPP diff --git a/src/cs-audio/StreamingSource.cpp b/src/cs-audio/StreamingSource.cpp new file mode 100644 index 000000000..038854ee3 --- /dev/null +++ b/src/cs-audio/StreamingSource.cpp @@ -0,0 +1,246 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "StreamingSource.hpp" +#include "internal/AlErrorHandling.hpp" +#include "internal/BufferManager.hpp" +#include "internal/FileReader.hpp" +#include "internal/SettingsMixer.hpp" +#include "logger.hpp" + +#include +#include +#include +#include +#include + +namespace cs::audio { + +StreamingSource::StreamingSource(std::string file, int bufferLength, int queueSize, + std::shared_ptr UpdateInstructor) + : SourceBase(file, UpdateInstructor) + , mBuffers(std::vector(queueSize)) + , mAudioContainer(FileReader::AudioContainerStreaming()) + , mBufferLength(std::move(bufferLength)) + , mRefillBuffer(true) + , mNotPlaying(true) { + + mAudioContainer.bufferLength = mBufferLength; + + alGetError(); // clear error code + + // create buffers + alGenBuffers((ALsizei)mBuffers.size(), mBuffers.data()); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to generate buffers!"); + return; + } + + startStream(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +StreamingSource::StreamingSource() + : SourceBase() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +StreamingSource::~StreamingSource() { + if (mIsLeader) { + alSourceStop(mOpenAlId); + alSourceUnqueueBuffers(mOpenAlId, (ALsizei)mBuffers.size(), mBuffers.data()); + alDeleteBuffers((ALsizei)mBuffers.size(), mBuffers.data()); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool StreamingSource::updateStream() { + if (!mIsLeader) { + return true; + } + + // possible improvement: instead of checking for playback and looping + // in each frame, override the SourceSettings::set() function to also + // set a state within the StreamingSource describing the playback and looping state + + // update the stream only if the source is supposed to be playing + auto search = mPlaybackSettings->find("playback"); + if (search == mPlaybackSettings->end() || search->second.type() != typeid(std::string) || + std::any_cast(search->second) != "play") { + mNotPlaying = true; + return false; + } + + // get looping setting + auto searchLooping = mPlaybackSettings->find("looping"); + if (searchLooping != mPlaybackSettings->end() && searchLooping->second.type() == typeid(bool)) { + mAudioContainer.isLooping = std::any_cast(searchLooping->second); + } + + if (mNotPlaying) { + mRefillBuffer = true; + } // source was just set to playing + mNotPlaying = false; + bool updateRequired = false; + + ALint numBufferProcessed, state; + alGetSourcei(mOpenAlId, AL_BUFFERS_PROCESSED, &numBufferProcessed); + + while (numBufferProcessed > 0) { + + ALuint bufferId; + alSourceUnqueueBuffers(mOpenAlId, 1, &bufferId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to unqueue buffer!"); + return false; + ; + } + + if (mRefillBuffer) { + if (!FileReader::getNextStreamBlock(mAudioContainer)) { + mRefillBuffer = false; + updateRequired = true; + stop(); + numBufferProcessed--; + continue; + } + fillBuffer(bufferId); + + alSourceQueueBuffers(mOpenAlId, 1, &bufferId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to requeue buffer!"); + return false; + } + } + numBufferProcessed--; + } + + // restart source if underrun occurred + alGetSourcei(mOpenAlId, AL_SOURCE_STATE, &state); + if (state != AL_PLAYING) { + alSourcePlay(mOpenAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to restart playback of streaming source!"); + return false; + } + } + + return updateRequired; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool StreamingSource::setFile(std::string file) { + if (!mIsLeader) { + return true; + } + alGetError(); // clear error code + + // stop source if source is currently playing + bool isPlaying = false; + auto search = mPlaybackSettings->find("playback"); + if (search != mPlaybackSettings->end() && search->second.type() == typeid(std::string) && + std::any_cast(search->second) == "play") { + + isPlaying = true; + alSourceStop(mOpenAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop source!"); + return false; + } + } + + // remove current buffers + ALuint buffers; + alSourceUnqueueBuffers(mOpenAlId, (ALsizei)mBuffers.size(), &buffers); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to unqueue buffers!"); + } + + mFile = file; + + if (!startStream()) { + return false; + } + + if (isPlaying) { + alSourcePlay(mOpenAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to restart source!"); + return false; + } + } + return true; +} + +bool StreamingSource::startStream() { + // check if file exists + if (!std::filesystem::exists(mFile)) { + logger().warn("{} file does not exist! Unable to fill buffer!", mFile); + return false; + } + + if (!FileReader::openStream(mFile, mAudioContainer)) { + logger().warn("Failed to open stream for: {}!", mFile); + return false; + } + + // fill buffer + for (auto buffer : mBuffers) { + FileReader::getNextStreamBlock(mAudioContainer); + if (mAudioContainer.splblockalign > 1) { + alBufferi(buffer, AL_UNPACK_BLOCK_ALIGNMENT_SOFT, mAudioContainer.splblockalign); + } + fillBuffer(buffer); + } + + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed the inital stream buffering for: {}", mFile); + return false; + } + + // queue buffer + alSourceQueueBuffers(mOpenAlId, (ALsizei)mBuffers.size(), mBuffers.data()); + + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to queue the stream buffers for: {}", mFile); + return false; + } + + return true; +} + +void StreamingSource::fillBuffer(ALuint buffer) { + switch (mAudioContainer.formatType) { + case FileReader::FormatType::Int16: + alBufferData(buffer, mAudioContainer.format, + std::get>(mAudioContainer.audioData).data(), + (ALsizei)mAudioContainer.bufferSize, mAudioContainer.sfInfo.samplerate); + break; + + case FileReader::FormatType::Float: + alBufferData(buffer, mAudioContainer.format, + std::get>(mAudioContainer.audioData).data(), + (ALsizei)mAudioContainer.bufferSize, mAudioContainer.sfInfo.samplerate); + break; + + default: + alBufferData(buffer, mAudioContainer.format, + std::get>(mAudioContainer.audioData).data(), + (ALsizei)mAudioContainer.bufferSize, mAudioContainer.sfInfo.samplerate); + } + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to fill buffer for: {}...", mFile); + mAudioContainer.print(); + return; + } +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/StreamingSource.hpp b/src/cs-audio/StreamingSource.hpp new file mode 100644 index 000000000..2b6c29737 --- /dev/null +++ b/src/cs-audio/StreamingSource.hpp @@ -0,0 +1,76 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_STREAMING_SOURCE_HPP +#define CS_AUDIO_STREAMING_SOURCE_HPP + +#include "cs_audio_export.hpp" +#include "internal/BufferManager.hpp" +#include "internal/FileReader.hpp" +#include "internal/SourceBase.hpp" +#include "internal/UpdateInstructor.hpp" + +#include +#include +#include + +namespace cs::audio { + +// forward declaration +class SourceGroup; + +/// @brief This is the derived source class for streaming sources. This means that not the whole +/// file is being read and written into the buffer but only small chunks at a time. StreamingSource +/// B´buffers cannot be shared among sources. Each StreamingSource handles it's buffers on it's own. +/// One current disadvantage is that StreamingSources can only be played after the loading screen +/// because the CosmoScout update cycle is needed in order to update the changing buffers. +class CS_AUDIO_EXPORT StreamingSource : public SourceBase { + public: + /// @brief This is the standard constructor used for non-cluster mode and cluster mode leader + /// calls + StreamingSource(std::string file, int bufferLength, int queueSize, + std::shared_ptr UpdateInstructor); + /// @brief This Constructor will create a dummy StreamingSource which is used when a member of a + /// cluster tries to create a StreamingSource. Doing this will disable any functionality of this + /// class. + StreamingSource(); + ~StreamingSource(); + + /// @brief Sets a new file to be played by the source. + /// @return true if successful + bool setFile(std::string file) override; + + /// @brief Checks if any buffer finished playing and if so, requeues the buffer with new data. + /// @return True if a AudioController::update() is required. This is done to set the playback + /// state after the stream finished playing and looping is not enabled. + bool updateStream(); + + private: + /// @brief Fills an OpenAL buffer with already read data from a file + /// @param buffer buffer to write to + void fillBuffer(ALuint buffer); + + /// @brief Starts a new stream from a new file + /// @return True if successful + bool startStream(); + + /// List of all OpenAL buffer IDs being used by the source + std::vector mBuffers; + /// Contains all information regarding a file/buffer that is needed. + FileReader::AudioContainerStreaming mAudioContainer; + /// Length of each buffer in milliseconds + int mBufferLength; + /// Specifies whether buffers should still be filled in a stream update. + /// Is false if no new buffer is required to play the remaining content. + bool mRefillBuffer; + /// Specifies whether the source was playing in the last frame + bool mNotPlaying; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_STREAMING_SOURCE_HPP \ No newline at end of file diff --git a/src/cs-audio/gui/audio_settings.html b/src/cs-audio/gui/audio_settings.html new file mode 100644 index 000000000..4606c60cc --- /dev/null +++ b/src/cs-audio/gui/audio_settings.html @@ -0,0 +1,26 @@ + + + +
+
+ Master Volume +
+
+
+
+
+ +
+
+ Output Device +
+
+ +
+
\ No newline at end of file diff --git a/src/cs-audio/gui/js/audio_settings.js b/src/cs-audio/gui/js/audio_settings.js new file mode 100644 index 000000000..22a4f25db --- /dev/null +++ b/src/cs-audio/gui/js/audio_settings.js @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +(() => { + /** + * Audio Api + */ + class AudioApi extends IApi { + /** + * @inheritDoc + */ + name = 'audio'; + + /** + * @inheritDoc + */ + init() { + CosmoScout.gui.initSlider("audio.masterVolume", 0.0, 5, 0.05, [1]); + } + } + + CosmoScout.init(AudioApi); +})(); diff --git a/src/cs-audio/internal/AlErrorHandling.cpp b/src/cs-audio/internal/AlErrorHandling.cpp new file mode 100644 index 000000000..ee4b2d483 --- /dev/null +++ b/src/cs-audio/internal/AlErrorHandling.cpp @@ -0,0 +1,44 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "AlErrorHandling.hpp" +#include "../logger.hpp" + +namespace cs::audio { + +bool AlErrorHandling::errorOccurred() { + ALenum error; + if ((error = alGetError()) != AL_NO_ERROR) { + + std::string errorCode; + switch (error) { + case AL_INVALID_NAME: + errorCode = "Invalid name (ID) passed to an AL call"; + break; + case AL_INVALID_ENUM: + errorCode = "Invalid enumeration passed to AL call"; + break; + case AL_INVALID_VALUE: + errorCode = "Invalid value passed to AL call"; + break; + case AL_INVALID_OPERATION: + errorCode = "Illegal AL call"; + break; + case AL_OUT_OF_MEMORY: + errorCode = "Not enough memory to execute the AL call"; + break; + default: + errorCode = "Unkown error code"; + } + + logger().warn("OpenAL-Soft Error occured! Reason: {}...", errorCode); + return true; + } + return false; +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/AlErrorHandling.hpp b/src/cs-audio/internal/AlErrorHandling.hpp new file mode 100644 index 000000000..bce413ff5 --- /dev/null +++ b/src/cs-audio/internal/AlErrorHandling.hpp @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_ERROR_HANDLING_HPP +#define CS_AUDIO_ERROR_HANDLING_HPP + +#include "cs_audio_export.hpp" +#include + +namespace cs::audio { + +class CS_AUDIO_EXPORT AlErrorHandling { + public: + /// @brief Checks if an OpenAL Error occurred and if so prints a logger warning containing the + /// error. + /// @return True if error occurred + static bool errorOccurred(); + +}; // namespace cs::audio + +} // namespace cs::audio + +#endif // CS_AUDIO_ERROR_HANDLING_HPP diff --git a/src/cs-audio/internal/BufferManager.cpp b/src/cs-audio/internal/BufferManager.cpp new file mode 100644 index 000000000..b70d47b6a --- /dev/null +++ b/src/cs-audio/internal/BufferManager.cpp @@ -0,0 +1,128 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "BufferManager.hpp" +#include "../logger.hpp" +#include "AlErrorHandling.hpp" +#include "FileReader.hpp" +#include +#include +#include + +namespace cs::audio { + +BufferManager::BufferManager() + : mBufferList(std::vector>()) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +BufferManager::~BufferManager() { + alGetError(); // clear error code + // gather all buffer Ids to delete them in a single OpenAL call + std::vector bufferIds(mBufferList.size()); + for (std::shared_ptr buffer : mBufferList) { + bufferIds.push_back(buffer->mOpenAlId); + } + alDeleteBuffers((ALsizei)mBufferList.size(), bufferIds.data()); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to delete (all) buffers!"); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::pair BufferManager::getBuffer(std::string file) { + for (std::shared_ptr buffer : mBufferList) { + if (buffer->mFile == file) { + buffer->mUsageNumber++; + return std::make_pair(true, buffer->mOpenAlId); + } + } + return createBuffer(file); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::pair BufferManager::createBuffer(std::string file) { + alGetError(); // clear error code + + // create buffer + ALuint newBufferId; + alGenBuffers((ALsizei)1, &newBufferId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to generate buffer!"); + return std::make_pair(false, newBufferId); + } + + // read wave file + FileReader::AudioContainer audioContainer; + if (!FileReader::loadFile(file, audioContainer)) { + logger().warn("{} is not a valid file! Unable to create buffer!", file); + alDeleteBuffers((ALsizei)1, &newBufferId); + return std::make_pair(false, newBufferId); + } + + // load file into buffer + if (audioContainer.splblockalign > 1) + alBufferi(newBufferId, AL_UNPACK_BLOCK_ALIGNMENT_SOFT, audioContainer.splblockalign); + + if (audioContainer.formatType == FileReader::FormatType::Int16) { + alBufferData(newBufferId, audioContainer.format, + std::get>(audioContainer.audioData).data(), audioContainer.size, + audioContainer.sfInfo.samplerate); + + } else if (audioContainer.formatType == FileReader::FormatType::Float) { + alBufferData(newBufferId, audioContainer.format, + std::get>(audioContainer.audioData).data(), audioContainer.size, + audioContainer.sfInfo.samplerate); + + } else { + alBufferData(newBufferId, audioContainer.format, + std::get>(audioContainer.audioData).data(), audioContainer.size, + audioContainer.sfInfo.samplerate); + } + + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to fill buffer with data!"); + alDeleteBuffers((ALsizei)1, &newBufferId); + return std::make_pair(false, newBufferId); + } + + // add Buffer + mBufferList.push_back(std::make_shared(file, newBufferId)); + + return std::make_pair(true, newBufferId); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void BufferManager::removeBuffer(std::string file) { + for (auto it = mBufferList.begin(); it != mBufferList.end(); it++) { + if ((*it)->mFile == file) { + if (--(*it)->mUsageNumber == 0) { + deleteBuffer(it); + } + break; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void BufferManager::deleteBuffer(std::vector>::iterator bufferIt) { + alGetError(); // clear error code + + alDeleteBuffers((ALsizei)1, &(*bufferIt)->mOpenAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to delete single buffer!"); + } + + mBufferList.erase(bufferIt); +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/BufferManager.hpp b/src/cs-audio/internal/BufferManager.hpp new file mode 100644 index 000000000..f1a002827 --- /dev/null +++ b/src/cs-audio/internal/BufferManager.hpp @@ -0,0 +1,70 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_BUFFER_MANAGER_HPP +#define CS_AUDIO_BUFFER_MANAGER_HPP + +#include "cs_audio_export.hpp" +#include +#include +#include +#include +#include + +namespace cs::audio { + +struct Buffer { + std::string mFile; + int mUsageNumber; + ALuint mOpenAlId; + + Buffer(std::string file, ALuint openAlId) + : mFile(std::move(file)) + , mOpenAlId(std::move(openAlId)) { + mUsageNumber = 1; + } +}; + +/// @brief This class handles the creation and deletion of buffers for non-streaming sources. +/// This class should only be instantiated once. +class CS_AUDIO_EXPORT BufferManager { + public: + BufferManager(const BufferManager& obj) = delete; + BufferManager(BufferManager&&) = delete; + + BufferManager& operator=(const BufferManager&) = delete; + BufferManager& operator=(BufferManager&&) = delete; + + BufferManager(); + ~BufferManager(); + + /// @brief Returns a buffer id containing the data for the provided file path. + /// The BufferManager will check if a buffer for this file already exists and if so reuse the + /// the existing one. + /// @return Pair of bool and potential buffer id. Bool is false if an error occurred, which + /// means the buffer id is not valid. + std::pair getBuffer(std::string file); + + /// @brief Signals to the BufferManager that a Source is no longer using a buffer to the + /// provided file. If there are no more Sources using a buffer to a specific file, the + /// BufferManager will automatically delete the buffer. + void removeBuffer(std::string file); + + private: + /// @brief List of all current buffers + std::vector> mBufferList; + + /// @brief Creates a new Buffer if none already exists for the provided file path. + std::pair createBuffer(std::string file); + + /// @brief Deletes a buffer if it is no longer used by any Source. + void deleteBuffer(std::vector>::iterator bufferIt); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_BUFFER_MANAGER_HPP \ No newline at end of file diff --git a/src/cs-audio/internal/FileReader.cpp b/src/cs-audio/internal/FileReader.cpp new file mode 100644 index 000000000..c8a7b849b --- /dev/null +++ b/src/cs-audio/internal/FileReader.cpp @@ -0,0 +1,430 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "FileReader.hpp" +#include "../logger.hpp" +#include "BufferManager.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cs::audio { + +const char* FileReader::getFormatName(ALenum format) { + switch (format) { + case AL_FORMAT_MONO8: + return "Mono, U8"; + case AL_FORMAT_MONO16: + return "Mono, S16"; + case AL_FORMAT_MONO_FLOAT32: + return "Mono, Float32"; + case AL_FORMAT_MONO_MULAW: + return "Mono, muLaw"; + case AL_FORMAT_MONO_ALAW_EXT: + return "Mono, aLaw"; + case AL_FORMAT_MONO_IMA4: + return "Mono, IMA4 ADPCM"; + case AL_FORMAT_MONO_MSADPCM_SOFT: + return "Mono, MS ADPCM"; + case AL_FORMAT_STEREO8: + return "Stereo, U8"; + case AL_FORMAT_STEREO16: + return "Stereo, S16"; + case AL_FORMAT_STEREO_FLOAT32: + return "Stereo, Float32"; + case AL_FORMAT_STEREO_MULAW: + return "Stereo, muLaw"; + case AL_FORMAT_STEREO_ALAW_EXT: + return "Stereo, aLaw"; + case AL_FORMAT_STEREO_IMA4: + return "Stereo, IMA4 ADPCM"; + case AL_FORMAT_STEREO_MSADPCM_SOFT: + return "Stereo, MS ADPCM"; + case AL_FORMAT_QUAD8: + return "Quadraphonic, U8"; + case AL_FORMAT_QUAD16: + return "Quadraphonic, S16"; + case AL_FORMAT_QUAD32: + return "Quadraphonic, Float32"; + case AL_FORMAT_QUAD_MULAW: + return "Quadraphonic, muLaw"; + case AL_FORMAT_51CHN8: + return "5.1 Surround, U8"; + case AL_FORMAT_51CHN16: + return "5.1 Surround, S16"; + case AL_FORMAT_51CHN32: + return "5.1 Surround, Float32"; + case AL_FORMAT_51CHN_MULAW: + return "5.1 Surround, muLaw"; + case AL_FORMAT_61CHN8: + return "6.1 Surround, U8"; + case AL_FORMAT_61CHN16: + return "6.1 Surround, S16"; + case AL_FORMAT_61CHN32: + return "6.1 Surround, Float32"; + case AL_FORMAT_61CHN_MULAW: + return "6.1 Surround, muLaw"; + case AL_FORMAT_71CHN8: + return "7.1 Surround, U8"; + case AL_FORMAT_71CHN16: + return "7.1 Surround, S16"; + case AL_FORMAT_71CHN32: + return "7.1 Surround, Float32"; + case AL_FORMAT_71CHN_MULAW: + return "7.1 Surround, muLaw"; + case AL_FORMAT_BFORMAT2D_8: + return "B-Format 2D, U8"; + case AL_FORMAT_BFORMAT2D_16: + return "B-Format 2D, S16"; + case AL_FORMAT_BFORMAT2D_FLOAT32: + return "B-Format 2D, Float32"; + case AL_FORMAT_BFORMAT2D_MULAW: + return "B-Format 2D, muLaw"; + case AL_FORMAT_BFORMAT3D_8: + return "B-Format 3D, U8"; + case AL_FORMAT_BFORMAT3D_16: + return "B-Format 3D, S16"; + case AL_FORMAT_BFORMAT3D_FLOAT32: + return "B-Format 3D, Float32"; + case AL_FORMAT_BFORMAT3D_MULAW: + return "B-Format 3D, muLaw"; + case AL_FORMAT_UHJ2CHN8_SOFT: + return "UHJ 2-channel, U8"; + case AL_FORMAT_UHJ2CHN16_SOFT: + return "UHJ 2-channel, S16"; + case AL_FORMAT_UHJ2CHN_FLOAT32_SOFT: + return "UHJ 2-channel, Float32"; + case AL_FORMAT_UHJ3CHN8_SOFT: + return "UHJ 3-channel, U8"; + case AL_FORMAT_UHJ3CHN16_SOFT: + return "UHJ 3-channel, S16"; + case AL_FORMAT_UHJ3CHN_FLOAT32_SOFT: + return "UHJ 3-channel, Float32"; + case AL_FORMAT_UHJ4CHN8_SOFT: + return "UHJ 4-channel, U8"; + case AL_FORMAT_UHJ4CHN16_SOFT: + return "UHJ 4-channel, S16"; + case AL_FORMAT_UHJ4CHN_FLOAT32_SOFT: + return "UHJ 4-channel, Float32"; + } + return "Unknown Format"; +} + +bool FileReader::readMetaData(std::string fileName, AudioContainer& audioContainer) { + FormatType sample_format = Int16; + ALint byteblockalign = 0; + ALint splblockalign = 0; + ALenum format; + SNDFILE* sndfile; + SF_INFO sfinfo; + + /* Open the audio file and check that it's usable. */ + sndfile = sf_open(fileName.c_str(), SFM_READ, &sfinfo); + if (!sndfile) { + logger().warn("Could not open audio in {}: {}", fileName, sf_strerror(sndfile)); + return false; + } + if (sfinfo.frames < 1) { + logger().warn("Bad sample count in {}({})", fileName, sfinfo.frames); + sf_close(sndfile); + return false; + } + + /* Detect a suitable format to load. Formats like Vorbis and Opus use float + * natively, so load as float to avoid clipping when possible. Formats + * larger than 16-bit can also use float to preserve a bit more precision. + */ + switch ((sfinfo.format & SF_FORMAT_SUBMASK)) { + case SF_FORMAT_PCM_24: + case SF_FORMAT_PCM_32: + case SF_FORMAT_FLOAT: + case SF_FORMAT_DOUBLE: + case SF_FORMAT_VORBIS: + case SF_FORMAT_OPUS: + case SF_FORMAT_ALAC_20: + case SF_FORMAT_ALAC_24: + case SF_FORMAT_ALAC_32: + case 0x0080 /*SF_FORMAT_MPEG_LAYER_I*/: + case 0x0081 /*SF_FORMAT_MPEG_LAYER_II*/: + case 0x0082 /*SF_FORMAT_MPEG_LAYER_III*/: + if (alIsExtensionPresent("AL_EXT_FLOAT32")) + sample_format = Float; + break; + case SF_FORMAT_IMA_ADPCM: + /* ADPCM formats require setting a block alignment as specified in the + * file, which needs to be read from the wave 'fmt ' chunk manually + * since libsndfile doesn't provide it in a format-agnostic way. + */ + if (sfinfo.channels <= 2 && (sfinfo.format & SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV && + alIsExtensionPresent("AL_EXT_IMA4") && alIsExtensionPresent("AL_SOFT_block_alignment")) + sample_format = IMA4; + break; + case SF_FORMAT_MS_ADPCM: + if (sfinfo.channels <= 2 && (sfinfo.format & SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV && + alIsExtensionPresent("AL_SOFT_MSADPCM") && alIsExtensionPresent("AL_SOFT_block_alignment")) + sample_format = MSADPCM; + break; + } + + if (sample_format == IMA4 || sample_format == MSADPCM) { + /* For ADPCM, lookup the wave file's "fmt " chunk, which is a + * WAVEFORMATEX-based structure for the audio format. + */ + SF_CHUNK_INFO inf = {"fmt ", 4, 0, NULL}; + SF_CHUNK_ITERATOR* iter = sf_get_chunk_iterator(sndfile, &inf); + + /* If there's an issue getting the chunk or block alignment, load as + * 16-bit and have libsndfile do the conversion. + */ + if (!iter || sf_get_chunk_size(iter, &inf) != SF_ERR_NO_ERROR || inf.datalen < 14) + sample_format = Int16; + else { + ALubyte* fmtbuf = static_cast(calloc(inf.datalen, 1)); + inf.data = fmtbuf; + if (sf_get_chunk_data(iter, &inf) != SF_ERR_NO_ERROR) + sample_format = Int16; + else { + /* Read the nBlockAlign field, and convert from bytes- to + * samples-per-block (verifying it's valid by converting back + * and comparing to the original value). + */ + byteblockalign = fmtbuf[12] | (fmtbuf[13] << 8); + if (sample_format == IMA4) { + splblockalign = (byteblockalign / sfinfo.channels - 4) / 4 * 8 + 1; + if (splblockalign < 1 || + ((splblockalign - 1) / 2 + 4) * sfinfo.channels != byteblockalign) + sample_format = Int16; + } else { + splblockalign = (byteblockalign / sfinfo.channels - 7) * 2 + 2; + if (splblockalign < 2 || + ((splblockalign - 2) / 2 + 7) * sfinfo.channels != byteblockalign) + sample_format = Int16; + } + } + free(fmtbuf); + } + } + + if (sample_format == Int16) { + splblockalign = 1; + byteblockalign = sfinfo.channels * 2; + } else if (sample_format == Float) { + splblockalign = 1; + byteblockalign = sfinfo.channels * 4; + } + + /* Figure out the OpenAL format from the file and desired sample type. */ + format = AL_NONE; + if (sfinfo.channels == 1) { + if (sample_format == Int16) + format = AL_FORMAT_MONO16; + else if (sample_format == Float) + format = AL_FORMAT_MONO_FLOAT32; + else if (sample_format == IMA4) + format = AL_FORMAT_MONO_IMA4; + else if (sample_format == MSADPCM) + format = AL_FORMAT_MONO_MSADPCM_SOFT; + } else if (sfinfo.channels == 2) { + if (sample_format == Int16) + format = AL_FORMAT_STEREO16; + else if (sample_format == Float) + format = AL_FORMAT_STEREO_FLOAT32; + else if (sample_format == IMA4) + format = AL_FORMAT_STEREO_IMA4; + else if (sample_format == MSADPCM) + format = AL_FORMAT_STEREO_MSADPCM_SOFT; + } else if (sfinfo.channels == 3) { + if (sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT) { + if (sample_format == Int16) + format = AL_FORMAT_BFORMAT2D_16; + else if (sample_format == Float) + format = AL_FORMAT_BFORMAT2D_FLOAT32; + } + } else if (sfinfo.channels == 4) { + if (sf_command(sndfile, SFC_WAVEX_GET_AMBISONIC, NULL, 0) == SF_AMBISONIC_B_FORMAT) { + if (sample_format == Int16) + format = AL_FORMAT_BFORMAT3D_16; + else if (sample_format == Float) + format = AL_FORMAT_BFORMAT3D_FLOAT32; + } + } + if (!format) { + logger().warn("Unsupported channel count: {}", sfinfo.channels); + sf_close(sndfile); + return false; + } + + if (sfinfo.frames / splblockalign > (sf_count_t)(INT_MAX / byteblockalign)) { + logger().warn("Too many samples in {} ({})", fileName, sfinfo.frames); + sf_close(sndfile); + return false; + } + + audioContainer.format = format; + audioContainer.formatType = sample_format; + audioContainer.sfInfo = sfinfo; + audioContainer.splblockalign = splblockalign; + audioContainer.byteblockalign = byteblockalign; + audioContainer.sndFile = sndfile; + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool FileReader::loadFile(std::string fileName, AudioContainer& audioContainer) { + + if (!readMetaData(fileName, audioContainer)) { + return false; + } + + /* Decode the whole audio file to a buffer. */ + sf_count_t num_frames; + switch (audioContainer.formatType) { + case Int16: + audioContainer.audioData = + std::vector((size_t)(audioContainer.sfInfo.frames / audioContainer.splblockalign * + audioContainer.byteblockalign)); + num_frames = sf_readf_short(audioContainer.sndFile, + std::get>(audioContainer.audioData).data(), + audioContainer.sfInfo.frames); + break; + + case Float: + audioContainer.audioData = + std::vector((size_t)(audioContainer.sfInfo.frames / audioContainer.splblockalign * + audioContainer.byteblockalign)); + num_frames = sf_readf_float(audioContainer.sndFile, + std::get>(audioContainer.audioData).data(), + audioContainer.sfInfo.frames); + break; + + default: + audioContainer.audioData = + std::vector((size_t)(audioContainer.sfInfo.frames / audioContainer.splblockalign * + audioContainer.byteblockalign)); + sf_count_t count = + audioContainer.sfInfo.frames / audioContainer.splblockalign * audioContainer.byteblockalign; + num_frames = sf_read_raw( + audioContainer.sndFile, std::get>(audioContainer.audioData).data(), count); + if (num_frames > 0) { + num_frames = num_frames / audioContainer.byteblockalign * audioContainer.splblockalign; + } + } + + if (num_frames < 1) { + audioContainer.reset(); + logger().warn("Failed to read samples in {} ({})", fileName, num_frames); + return false; + } + + audioContainer.size = + (ALsizei)(num_frames / audioContainer.splblockalign * audioContainer.byteblockalign); + sf_close(audioContainer.sndFile); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool FileReader::openStream(std::string fileName, AudioContainerStreaming& audioContainer) { + + audioContainer.reset(); + if (!readMetaData(fileName, audioContainer)) { + logger().warn("readMetaData() failed"); + return false; + } + + audioContainer.blockCount = audioContainer.sfInfo.samplerate / audioContainer.splblockalign; + audioContainer.blockCount = audioContainer.blockCount * audioContainer.bufferLength / 1000; + + switch (audioContainer.formatType) { + case Int16: + audioContainer.audioData = + std::vector((size_t)(audioContainer.blockCount * audioContainer.byteblockalign)); + break; + case Float: + audioContainer.audioData = + std::vector((size_t)(audioContainer.blockCount * audioContainer.byteblockalign)); + break; + default: + audioContainer.audioData = + std::vector((size_t)(audioContainer.blockCount * audioContainer.byteblockalign)); + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool FileReader::getNextStreamBlock(AudioContainerStreaming& audioContainer) { + + sf_count_t slen; + switch (audioContainer.formatType) { + case Int16: + slen = sf_readf_short(audioContainer.sndFile, + std::get>(audioContainer.audioData).data(), + audioContainer.blockCount * audioContainer.splblockalign); + + if (slen < 1) { + sf_seek(audioContainer.sndFile, 0, SEEK_SET); + + if (audioContainer.isLooping) { + return getNextStreamBlock(audioContainer); + } else { + return false; + } + } + slen *= audioContainer.byteblockalign; + break; + + case Float: + slen = sf_readf_float(audioContainer.sndFile, + std::get>(audioContainer.audioData).data(), + audioContainer.blockCount * audioContainer.splblockalign); + if (slen < 1) { + sf_seek(audioContainer.sndFile, 0, SEEK_SET); + + if (audioContainer.isLooping) { + return getNextStreamBlock(audioContainer); + } else { + return false; + } + } + slen *= audioContainer.byteblockalign; + break; + + default: + slen = sf_read_raw(audioContainer.sndFile, + std::get>(audioContainer.audioData).data(), + audioContainer.blockCount * audioContainer.splblockalign); + if (slen > 0) { + slen -= slen % audioContainer.byteblockalign; + } + if (slen < 1) { + sf_seek(audioContainer.sndFile, 0, SEEK_SET); + } + + if (audioContainer.isLooping) { + return getNextStreamBlock(audioContainer); + } else { + return false; + } + } + audioContainer.bufferSize = slen; + return true; +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/FileReader.hpp b/src/cs-audio/internal/FileReader.hpp new file mode 100644 index 000000000..4bc3ffab9 --- /dev/null +++ b/src/cs-audio/internal/FileReader.hpp @@ -0,0 +1,122 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_FILE_READER_HPP +#define CS_AUDIO_FILE_READER_HPP + +#include "cs_audio_export.hpp" +#include +#include +#include +#include +#include + +namespace cs::audio { + +class CS_AUDIO_EXPORT FileReader { + public: + enum FormatType { Int16, Float, IMA4, MSADPCM }; + + struct AudioContainer { + unsigned int format; + int size; + int splblockalign; + int byteblockalign; + FormatType formatType; + SF_INFO sfInfo; + SNDFILE* sndFile; + std::variant, std::vector, std::vector> audioData; + + void print() { + std::cout << "----AudioContainer Info----" << std::endl; + std::cout << "format: " << FileReader::getFormatName(formatType) << std::endl; + std::cout << "sampleRate: " << sfInfo.samplerate << "hz" << std::endl; + std::cout << "size: " << size << std::endl; + std::cout << "splblockalign: " << splblockalign << std::endl; + std::cout << "byteblockalign: " << byteblockalign << std::endl; + std::cout << "formatType: " << formatType << std::endl; + } + + void reset() { + format = 0; + size = 0; + splblockalign = 0; + byteblockalign = 0; + formatType = FormatType::Int16; + sf_close(sndFile); + + if (std::holds_alternative>(audioData)) { + std::get>(audioData).clear(); + } else if (std::holds_alternative>(audioData)) { + std::get>(audioData).clear(); + } else { + std::get>(audioData).clear(); + } + } + }; + + struct AudioContainerStreaming : public AudioContainer { + int bufferCounter; + int bufferLength; // in milliseconds + int blockCount; + bool isLooping; + sf_count_t bufferSize; + + void print() { + AudioContainer::print(); + std::cout << "bufferCounter: " << bufferCounter << std::endl; + std::cout << "blockCount: " << blockCount << std::endl; + std::cout << "bufferSize: " << bufferSize << std::endl; + } + + ~AudioContainerStreaming() { + reset(); + } + + void reset() { + AudioContainer::reset(); + bufferCounter = 0; + bufferSize = 0; + blockCount = 0; + isLooping = false; + } + }; + FileReader(const FileReader& obj) = delete; + FileReader(FileReader&&) = delete; + + FileReader& operator=(const FileReader&) = delete; + FileReader& operator=(FileReader&&) = delete; + + /// @brief Reads the content of a .wav file and writes all the important information for OpenAL + /// into the wavContainer. + /// @param fileName path to file + /// @param audioContainer audioContainer to write into + /// @return Whether the provided file path is a valid .wav file + static bool loadFile(std::string fileName, AudioContainer& audioContainer); + + /// @return Name of audio format + static const char* getFormatName(ALenum format); + + /// @brief Retrieves all necessary data from a file to start a stream + /// @param fileName path to file + /// @param audioContainer audioContainer to write into + /// @return True if successful + static bool openStream(std::string fileName, AudioContainerStreaming& audioContainer); + + /// @brief Retrieves the next chunk of data from a file to write into an OpenAL buffer + /// @param audioContainer to write into + /// @return False if source is not looping and no more buffers need to be filled + /// to play the remaining content + static bool getNextStreamBlock(AudioContainerStreaming& audioContainer); + + private: + static bool readMetaData(std::string fileName, AudioContainer& audioContainer); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_FILE_READER_HPP \ No newline at end of file diff --git a/src/cs-audio/internal/Listener.cpp b/src/cs-audio/internal/Listener.cpp new file mode 100644 index 000000000..4a0c3f67d --- /dev/null +++ b/src/cs-audio/internal/Listener.cpp @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "Listener.hpp" +#include "../logger.hpp" +#include "AlErrorHandling.hpp" +#include + +namespace cs::audio { + +bool Listener::setPosition(float x, float y, float z) { + alGetError(); // clear error code + alListener3f(AL_POSITION, x, y, z); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set Listener Position!"); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool Listener::setVelocity(float x, float y, float z) { + alGetError(); // clear error code + alListener3f(AL_VELOCITY, x, y, z); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set Listener Veclocity!"); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool Listener::setOrientation(float atX, float atY, float atZ, float upX, float upY, float upZ) { + alGetError(); // clear error code + ALfloat vec[] = {atX, atY, atZ, upX, upY, upZ}; + alListenerfv(AL_ORIENTATION, vec); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set Listener Veclocity!"); + return false; + } + return true; +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/Listener.hpp b/src/cs-audio/internal/Listener.hpp new file mode 100644 index 000000000..0f41e70e5 --- /dev/null +++ b/src/cs-audio/internal/Listener.hpp @@ -0,0 +1,36 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_LISTENER_HPP +#define CS_AUDIO_LISTENER_HPP + +#include "cs_audio_export.hpp" + +namespace cs::audio { + +/// @brief This class offers controls to adjust the OpenAL Listeners transformation. These +/// transformation should only affect spatialized sources (all others are supposed +/// to have a relative position of (0,0,0) to the listener and are therefor not affected). +/// These transformations are, in the current version of December 2023, not used. +class CS_AUDIO_EXPORT Listener { + public: + /// @brief Sets the OpenAL Listener position + /// @return True if no error occurred + static bool setPosition(float x, float y, float z); + + /// @brief Sets the OpenAL Listener velocity + /// @return True if no error occurred + static bool setVelocity(float x, float y, float z); + + /// @brief Sets the OpenAL Listener orientation. + /// @return True if no error occurred + static bool setOrientation(float atX, float atY, float atZ, float upX, float upY, float upZ); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_LISTENER_HPP diff --git a/src/cs-audio/internal/OpenAlManager.cpp b/src/cs-audio/internal/OpenAlManager.cpp new file mode 100644 index 000000000..57b19471a --- /dev/null +++ b/src/cs-audio/internal/OpenAlManager.cpp @@ -0,0 +1,169 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "OpenAlManager.hpp" +#include "../../cs-core/Settings.hpp" +#include "../logger.hpp" + +#include +#include +#include + +namespace cs::audio { + +OpenAlManager::OpenAlManager() + : mDevice(nullptr) + , mContext(nullptr) + , mAttributeList(std::vector(12)) + , alcReopenDeviceSOFT(nullptr) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +OpenAlManager::~OpenAlManager() { + alcMakeContextCurrent(nullptr); + alcDestroyContext(mContext); + alcCloseDevice(mDevice); + if (contextErrorOccurd()) { + logger().warn("Error occurred during OpenAL deconstruction!"); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool OpenAlManager::initOpenAl(core::Settings::Audio settings) { + // create settings for context + mAttributeList[0] = ALC_FREQUENCY; + mAttributeList[1] = settings.pMixerFrequency.get(); + mAttributeList[2] = ALC_MONO_SOURCES; + mAttributeList[3] = settings.pNumberMonoSources.get(); + mAttributeList[4] = ALC_STEREO_SOURCES; + mAttributeList[5] = settings.pNumberStereoSources.get(); + mAttributeList[6] = ALC_REFRESH; + mAttributeList[7] = settings.pRefreshRate.get(); + mAttributeList[8] = ALC_SYNC; + mAttributeList[9] = settings.pContextSync.get(); + mAttributeList[10] = ALC_HRTF_SOFT; + mAttributeList[11] = settings.pEnableHRTF.get(); + + // open default device + mDevice = alcOpenDevice(nullptr); + if (!mDevice) { + logger().warn("Failed to open default device!"); + return false; + } + + // create context + mContext = alcCreateContext(mDevice, mAttributeList.data()); + if (contextErrorOccurd()) { + logger().warn("Failed to create context!"); + return false; + } + + // select context + ALCboolean contextSelected = alcMakeContextCurrent(mContext); + if (contextErrorOccurd() || !contextSelected) { + logger().warn("Faild to select current context!"); + return false; + } + + // enables the Option to set the distance model per source and not per context. This is needed for + // the DistanceModel_PS + alEnable(AL_SOURCE_DISTANCE_MODEL); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool OpenAlManager::setDevice(std::string outputDevice) { + if (alcIsExtensionPresent(NULL, "ALC_SOFT_reopen_device") == ALC_FALSE) { + logger().warn( + "OpenAL Extension 'ALC_SOFT_reopen_device' not found. Unable to change the output device!"); + return false; + } + + if (alcReopenDeviceSOFT == nullptr) { + alcReopenDeviceSOFT = (LPALCREOPENDEVICESOFT)alGetProcAddress("alcReopenDeviceSOFT"); + } + + if (alcReopenDeviceSOFT(mDevice, outputDevice.c_str(), mAttributeList.data()) == ALC_FALSE) { + contextErrorOccurd(); + logger().warn("Failed to set the new output device! Playback remains on the current device!"); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector OpenAlManager::getDevices() { + std::vector result; + int macro; + + if (alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT") == ALC_TRUE) { + macro = ALC_ALL_DEVICES_SPECIFIER; + + } else if (alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT") == ALC_TRUE) { + logger().warn("OpenAL Extensions 'ALC_ENUMERATE_ALL_EXT' not found. Not all available devices " + "might be found!"); + macro = ALC_DEVICE_SPECIFIER; + + } else { + logger().warn("OpenAL Extensions 'ALC_ENUMERATE_ALL_EXT' and 'ALC_ENUMERATION_EXT' not found. " + "Unable to find available devices!"); + return result; + } + + const ALCchar* device = alcGetString(nullptr, macro); + const ALCchar* next = alcGetString(nullptr, macro) + 1; + size_t len = 0; + + // Parsing device list. + // Devices are separated by NULL character and the list ends with two NULL characters. + while (device && *device != '\0' && next && *next != '\0') { + result.push_back(device); + len = strlen(device); + device += (len + 1); + next += (len + 2); + } + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool OpenAlManager::contextErrorOccurd() { + ALCenum error; + if ((error = alcGetError(mDevice)) != ALC_NO_ERROR) { + + std::string errorCode; + switch (error) { + case ALC_INVALID_DEVICE: + errorCode = "Invalid device handle"; + break; + case ALC_INVALID_CONTEXT: + errorCode = "Invalid context handle"; + break; + case ALC_INVALID_ENUM: + errorCode = "Invalid enumeration passed to an ALC call"; + break; + case ALC_INVALID_VALUE: + errorCode = "Invalid value passed to an ALC call"; + break; + case ALC_OUT_OF_MEMORY: + errorCode = "Not enough memory to execute the ALC call"; + break; + default: + errorCode = "Unkown error code"; + } + logger().warn("OpenAL-Soft Context Error occurred! Reason: {}...", errorCode); + return true; + } + return false; +} +} // namespace cs::audio diff --git a/src/cs-audio/internal/OpenAlManager.hpp b/src/cs-audio/internal/OpenAlManager.hpp new file mode 100644 index 000000000..59f9e224b --- /dev/null +++ b/src/cs-audio/internal/OpenAlManager.hpp @@ -0,0 +1,66 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_OPEN_AL_MANAGER_HPP +#define CS_AUDIO_OPEN_AL_MANAGER_HPP + +#include "../../cs-core/Settings.hpp" +#include "cs_audio_export.hpp" + +#include +#include + +namespace cs::audio { + +/// @brief This class handles the initialization (Device and Context) of OpenAL and the +/// functionality of getting and setting an audio output device. This class should only be +/// instantiated once. +class CS_AUDIO_EXPORT OpenAlManager { + public: + OpenAlManager(const OpenAlManager& obj) = delete; + OpenAlManager(OpenAlManager&&) = delete; + + OpenAlManager& operator=(const OpenAlManager&) = delete; + OpenAlManager& operator=(OpenAlManager&&) = delete; + + OpenAlManager(); + ~OpenAlManager(); + + /// @brief Initializes OpenAL by opening a device and creating a context. + /// @return True if successful + bool initOpenAl(core::Settings::Audio settings); + + /// @brief Checks for all available output devices. Either by the ALC_ENUMERATE_ALL_EXT extension + /// or, if not available, the ALC_ENUMERATE_EXT extension. + /// @return List of name of all available devices + std::vector getDevices(); + + /// @brief Try's to set the provided device name as the OpenAL output device via the + /// alcReopenDeviceSOFT extension. + /// @return True if successful + bool setDevice(std::string outputDevice); + + private: + /// Pointer to the current device + ALCdevice* mDevice; + /// Pointer to the current context + ALCcontext* mContext; + /// Specifies the current settings for OpenAL. The attributes are set via the config file. + std::vector mAttributeList; + + /// @brief Checks if an OpenAL Context Error occurred and if so prints a logger warning containing + /// the error. + /// @return True if an error occurred + bool contextErrorOccurd(); + + // OpenALSoft extensions function pointers: + LPALCREOPENDEVICESOFT alcReopenDeviceSOFT; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_OPEN_AL_MANAGER_HPP diff --git a/src/cs-audio/internal/ProcessingStepsManager.cpp b/src/cs-audio/internal/ProcessingStepsManager.cpp new file mode 100644 index 000000000..33020cbf9 --- /dev/null +++ b/src/cs-audio/internal/ProcessingStepsManager.cpp @@ -0,0 +1,146 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "ProcessingStepsManager.hpp" +#include "../AudioController.hpp" +#include "../logger.hpp" +#include + +// processingSteps: +#include "../processingSteps/SoundAttributes_PS.hpp" +#include "../processingSteps/DirectPlay_PS.hpp" +#include "../processingSteps/DistanceCulling_PS.hpp" +#include "../processingSteps/DistanceModel_PS.hpp" +#include "../processingSteps/PointSpatialization_PS.hpp" +#include "../processingSteps/SphereSpatialization_PS.hpp" +#include "../processingSteps/VolumeCulling_PS.hpp" + +namespace cs::audio { + +ProcessingStepsManager::~ProcessingStepsManager() { + mPipelines.clear(); + mUpdateProcessingSteps.clear(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +ProcessingStepsManager::ProcessingStepsManager(std::shared_ptr settings) + : mPipelines(std::map>>()) + , mSettings(std::move(settings)) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void ProcessingStepsManager::createPipeline( + std::vector processingSteps, int audioControllerId) { + + std::set> pipeline; + + for (std::string processingStep : processingSteps) { + auto ps = getProcessingStep(processingStep); + + if (ps != nullptr) { + pipeline.insert(ps); + + if (ps->requiresUpdate()) { + mUpdateProcessingSteps.insert(ps); + } + } + } + + mPipelines[audioControllerId] = pipeline; + removeObsoletePsFromUpdateList(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::shared_ptr ProcessingStepsManager::getProcessingStep( + std::string processingStep) { + + if (processingStep == "SoundAttributes") { + return SoundAttributes_PS::create(); + } + + if (processingStep == "PointSpatialization") { + return PointSpatialization_PS::create(mSettings->mAudio.pStationaryOutputDevice.get()); + } + + if (processingStep == "SphereSpatialization") { + return SphereSpatialization_PS::create(mSettings->mAudio.pStationaryOutputDevice.get()); + } + + if (processingStep == "DirectPlay") { + return DirectPlay_PS::create(); + } + + if (processingStep == "VolumeCulling") { + return VolumeCulling_PS::create(mSettings->mAudio.pVolumeCullingThreshold.get()); + } + + if (processingStep == "DistanceCulling") { + return DistanceCulling_PS::create(mSettings->mAudio.pDistanceCullingThreshold.get()); + } + + if (processingStep == "DistanceModel") { + return DistanceModel_PS::create(); + } + + // Add new processing steps here... + + logger().warn("Audio Processing Warning: Processing step '{}' is not defined!", processingStep); + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::shared_ptr> ProcessingStepsManager::process( + std::shared_ptr source, int audioControllerId, + std::shared_ptr> settings) { + + auto failedSettings = std::make_shared>(); + for (auto step : mPipelines[audioControllerId]) { + step->process(source, settings, failedSettings); + } + return failedSettings; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void ProcessingStepsManager::callPsUpdateFunctions() { + for (auto psPtr : mUpdateProcessingSteps) { + psPtr->update(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void ProcessingStepsManager::removeObsoletePsFromUpdateList() { + // get active PS + std::set> activePS; + for (auto const& [key, val] : mPipelines) { + activePS.insert(val.begin(), val.end()); + } + + // get all PS that are in mUpdateProcessingSteps but not in activePS + std::set> obsoletePS; + std::set_difference(mUpdateProcessingSteps.begin(), mUpdateProcessingSteps.end(), + activePS.begin(), activePS.end(), std::inserter(obsoletePS, obsoletePS.end())); + + // erase obsoletePS from mUpdateProcessingSteps + for (auto ps : obsoletePS) { + mUpdateProcessingSteps.erase(ps); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void ProcessingStepsManager::removeAudioController(int audioControllerId) { + mPipelines.erase(audioControllerId); + removeObsoletePsFromUpdateList(); +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/ProcessingStepsManager.hpp b/src/cs-audio/internal/ProcessingStepsManager.hpp new file mode 100644 index 000000000..4609b9ce9 --- /dev/null +++ b/src/cs-audio/internal/ProcessingStepsManager.hpp @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PROCESSING_STEPS_MANAGER_HPP +#define CS_AUDIO_PROCESSING_STEPS_MANAGER_HPP + +#include "../../cs-core/Settings.hpp" +#include "../AudioController.hpp" +#include "../processingSteps/ProcessingStep.hpp" +#include "cs_audio_export.hpp" +#include +#include + +namespace cs::audio { + +/// @brief This class manages the creation, deletion and calling of all processing steps. +/// This class should only be instantiated once. +class CS_AUDIO_EXPORT ProcessingStepsManager { + public: + ProcessingStepsManager(const ProcessingStepsManager& obj) = delete; + ProcessingStepsManager(ProcessingStepsManager&&) = delete; + + ProcessingStepsManager& operator=(const ProcessingStepsManager&) = delete; + ProcessingStepsManager& operator=(ProcessingStepsManager&&) = delete; + + ProcessingStepsManager(std::shared_ptr settings); + ~ProcessingStepsManager(); + + /// @brief Creates a new Pipeline. A pipeline is a just a list of processing steps that should + /// be active for all sources of an audio controller. + /// @param processingSteps List of processing step names, which should be part of the pipeline + /// @param audioControllerId ID of the audioController requesting the pipeline + void createPipeline(std::vector processingSteps, int audioControllerId); + + /// @brief Deletes a pipeline completely. Gets called during the deconstruction + /// of an audio controller. + void removeAudioController(int audioControllerId); + + /// @brief Calls all processing steps part of the audioControllers pipeline for a source and + /// tries to apply all provided settings. + /// @param source Source to process. + /// @param audioControllerId AudioController on which the source lives. Specifies the pipeline. + /// @param sourceSettings Settings to apply to the provided source + /// @return List of settings that failed when trying to apply the settings to the source. + std::shared_ptr> process(std::shared_ptr source, + int audioControllerId, std::shared_ptr> sourceSettings); + + /// @brief This functions will call all update functions of processing steps that are + /// active and require an update every frame. + void callPsUpdateFunctions(); + + private: + /// Holds all pipelines and their corresponding audioController + std::map>> mPipelines; + /// List of processing steps that require an update call every frame + std::set> mUpdateProcessingSteps; + std::shared_ptr mSettings; + + /// @brief Searches for and creates a processing step when defining a pipeline. If you want to add + /// a new processing step then you need to define the name and the corresponding create call here. + /// @param processingStep Name of the processing step to create + /// @return Pointer to the processing step instance. Nullptr if processing step was not found. + std::shared_ptr getProcessingStep(std::string processingStep); + + /// @brief Check if any processing step was removed during a redefinition of a pipeline that + /// is part of mUpdateProcessingSteps. If so, removes the given processing step from + /// mUpdateProcessingSteps. + void removeObsoletePsFromUpdateList(); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PROCESSING_STEPS_MANAGER_HPP diff --git a/src/cs-audio/internal/SettingsMixer.cpp b/src/cs-audio/internal/SettingsMixer.cpp new file mode 100644 index 000000000..7c7f1837f --- /dev/null +++ b/src/cs-audio/internal/SettingsMixer.cpp @@ -0,0 +1,65 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "SettingsMixer.hpp" +#include + +namespace cs::audio { + +void SettingsMixer::A_Without_B(std::shared_ptr> A, + std::shared_ptr> B) { + + for (auto const& [key, val] : *B) { + if (auto search = A->find(key); search != A->end()) { + A->erase(search); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SettingsMixer::A_Without_B(std::shared_ptr> A, + std::shared_ptr> B) { + + for (auto key : *B) { + if (auto search = A->find(key); search != A->end()) { + A->erase(search); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SettingsMixer::A_Without_B_Value( + std::shared_ptr> A, std::string B) { + + for (auto it = A->begin(); it != A->end(); ++it) { + if (it->second.type() == typeid(std::string) && std::any_cast(it->second) == B) { + A->erase(it); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SettingsMixer::OverrideAdd_A_with_B(std::shared_ptr> A, + std::shared_ptr> B) { + + for (auto const& [key, val] : *B) { + A->operator[](key) = val; + } +} + +void SettingsMixer::Add_A_with_B_if_not_defined(std::shared_ptr> A, + std::shared_ptr> B) { + + for (auto const& element : *B) { + A->insert(element); + } +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/SettingsMixer.hpp b/src/cs-audio/internal/SettingsMixer.hpp new file mode 100644 index 000000000..07f8b7131 --- /dev/null +++ b/src/cs-audio/internal/SettingsMixer.hpp @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_SETTINGS_MIXER_HPP +#define CS_AUDIO_SETTINGS_MIXER_HPP + +#include "cs_audio_export.hpp" + +#include +#include +#include +#include +#include + +namespace cs::audio { + +/// @brief This static class acts as the tool kit for the UpdateConstructor. It provides all +/// functions needed to mix source settings for the pipeline. +class CS_AUDIO_EXPORT SettingsMixer { + public: + /// @brief Modifies A. Deletes all keys in A that are also a key in B. + /// @param A source settings + /// @param B source settings + static void A_Without_B(std::shared_ptr> A, + std::shared_ptr> B); + + /// @brief Modifies A. Deletes all keys in A that are part of the list of B. + /// @param A source settings + /// @param B list of settings keys + static void A_Without_B(std::shared_ptr> A, + std::shared_ptr> B); + + /// @brief Modifies A. Deletes all elements in A which's value is the same as B. + /// @param A source settings + /// @param B value to remove + static void A_Without_B_Value(std::shared_ptr> A, std::string B); + + /// @brief Modifies A. Adds all elements of B to A or, if the key already exists, overrides them. + /// @param A source settings + /// @param B source settings + static void OverrideAdd_A_with_B(std::shared_ptr> A, + std::shared_ptr> B); + + /// @brief Modifies A. Adds all elements of B to A if A is not already defining the same key. + /// @param A source settings + /// @param B source settings + static void Add_A_with_B_if_not_defined(std::shared_ptr> A, + std::shared_ptr> B); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_SETTINGS_MIXER_HPP diff --git a/src/cs-audio/internal/SourceBase.cpp b/src/cs-audio/internal/SourceBase.cpp new file mode 100644 index 000000000..b4abe2097 --- /dev/null +++ b/src/cs-audio/internal/SourceBase.cpp @@ -0,0 +1,171 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "SourceBase.hpp" +#include "../SourceGroup.hpp" +#include "../logger.hpp" +#include "AlErrorHandling.hpp" +#include "BufferManager.hpp" +#include "SettingsMixer.hpp" + +#include +#include +#include + +namespace cs::audio { + +SourceBase::SourceBase(std::string file, std::shared_ptr UpdateInstructor) + : SourceSettings(UpdateInstructor) + , std::enable_shared_from_this() + , mFile(std::move(file)) + , mPlaybackSettings(std::make_shared>()) { + + alGetError(); // clear error code + + // generate new source + alGenSources((ALuint)1, &mOpenAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to generate OpenAL-Soft Source!"); + return; + } + + // positions needs to be set relative in case the listener moves: + // set position to listener relative + alSourcei(mOpenAlId, AL_SOURCE_RELATIVE, AL_TRUE); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source position specification to relative!"); + return; + } + + alSource3i(mOpenAlId, AL_POSITION, 0, 0, 0); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source position to (0, 0, 0)!"); + } +} +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SourceBase::SourceBase() + : SourceSettings(false) + , std::enable_shared_from_this() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SourceBase::~SourceBase() { + if (mIsLeader) { + alGetError(); // clear error code + alDeleteSources(1, &mOpenAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to delete source!"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceBase::play() { + if (!mIsLeader) { + return; + } + set("playback", std::string("play")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceBase::stop() { + if (!mIsLeader) { + return; + } + set("playback", std::string("stop")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceBase::pause() { + if (!mIsLeader) { + return; + } + set("playback", std::string("pause")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const std::string SourceBase::getFile() const { + if (!mIsLeader) { + return std::string(); + } + return mFile; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const ALuint SourceBase::getOpenAlId() const { + if (!mIsLeader) { + return 0; + } + return mOpenAlId; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const std::shared_ptr> +SourceBase::getPlaybackSettings() const { + if (!mIsLeader) { + std::shared_ptr>(); + } + return mPlaybackSettings; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceBase::addToUpdateList() { + if (!mIsLeader) { + return; + } + mUpdateInstructor->update(shared_from_this()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceBase::removeFromUpdateList() { + if (!mIsLeader) { + return; + } + mUpdateInstructor->removeUpdate(shared_from_this()); +} + +const std::shared_ptr SourceBase::getGroup() { + if (!mIsLeader) { + std::shared_ptr(); + } + if (mGroup.expired()) { + return nullptr; + } + return mGroup.lock(); +} + +void SourceBase::setGroup(std::shared_ptr newGroup) { + if (!mIsLeader) { + return; + } + leaveGroup(); + mGroup = newGroup; + newGroup->join(shared_from_this()); +} + +void SourceBase::leaveGroup() { + if (!mIsLeader) { + return; + } + if (!mGroup.expired()) { + auto sharedGroup = mGroup.lock(); + mGroup.reset(); + sharedGroup->leave(shared_from_this()); + } +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/SourceBase.hpp b/src/cs-audio/internal/SourceBase.hpp new file mode 100644 index 000000000..940e84509 --- /dev/null +++ b/src/cs-audio/internal/SourceBase.hpp @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_BASE_SOURCE_HPP +#define CS_AUDIO_BASE_SOURCE_HPP + +#include "SourceSettings.hpp" +#include "UpdateInstructor.hpp" +#include "cs_audio_export.hpp" +#include +#include +#include + +namespace cs::audio { + +// forward declaration +class SourceGroup; + +/// @brief This class implements the basic common functions of sources and is the parent for +/// both specific source types: streaming source and non-streaming sources. +class CS_AUDIO_EXPORT SourceBase : public SourceSettings, + public std::enable_shared_from_this { + + public: + /// @brief This is the standard constructor used for non-cluster mode and cluster mode leader + /// calls + SourceBase(std::string file, std::shared_ptr UpdateInstructor); + /// @brief This Constructor will create a dummy SourceBase which is used when a member of a + /// cluster tries to create a SourceBase. Doing this will disable any functionality of this class. + SourceBase(); + virtual ~SourceBase(); + + /// @brief Sets setting to start playback. This call does not change the playback immediately. + /// It still requires a call to AudioController::update(). + void play(); + + /// @brief Sets setting to stop playback. This call does not change the playback immediately. + /// It still requires a call to AudioController::update(). + void stop(); + + /// @brief Sets setting to pause playback. This call does not change the playback immediately. + /// It still requires a call to AudioController::update(). + void pause(); + + /// @brief Virtual function to handle the change of file that is being played by a source. + /// @param file Filepath to the new file. + /// @return True if successful + virtual bool setFile(std::string file) = 0; + + /// @return Returns the current file that is being played by the source. + const std::string getFile() const; + + /// @return Returns the OpenAL ID + const ALuint getOpenAlId() const; + + /// @brief Returns the current group + /// @return Assigned group or nullptr if not part of any group + const std::shared_ptr getGroup(); + + /// @brief Assigns the source to a new group + /// @param newGroup group to join + void setGroup(std::shared_ptr newGroup); + + /// @brief leaves the current group + void leaveGroup(); + + /// @return Returns all settings (Source + Group + Controller) currently set and playing. + const std::shared_ptr> getPlaybackSettings() const; + + // Is friend because the UpdateConstructor needs write permissions to the mPlaybackSettings. + friend class UpdateConstructor; + + protected: + /// OpenAL ID of source + ALuint mOpenAlId; + /// Currently set file to play + std::string mFile; + /// Ptr to the group that the source is part of + std::weak_ptr mGroup; + /// Contains all settings (Source + Group + Controller) currently set and playing. + std::shared_ptr> mPlaybackSettings; + + /// @brief Registers itself to the updateInstructor to be updated + void addToUpdateList() override; + /// @brief Deregisters itself from the updateInstructor + void removeFromUpdateList() override; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_BASE_SOURCE_HPP diff --git a/src/cs-audio/internal/SourceSettings.cpp b/src/cs-audio/internal/SourceSettings.cpp new file mode 100644 index 000000000..7a1b881c9 --- /dev/null +++ b/src/cs-audio/internal/SourceSettings.cpp @@ -0,0 +1,103 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "SourceSettings.hpp" +#include +#include + +namespace cs::audio { + +SourceSettings::SourceSettings(std::shared_ptr UpdateInstructor) + : mIsLeader(true) + , mUpdateSettings(std::make_shared>()) + , mCurrentSettings(std::make_shared>()) + , mUpdateInstructor(std::move(UpdateInstructor)) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SourceSettings::SourceSettings() + : mIsLeader(true) + , mUpdateSettings(std::make_shared>()) + , mCurrentSettings(std::make_shared>()) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SourceSettings::SourceSettings(bool isLeader) + : mIsLeader(false) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SourceSettings::~SourceSettings() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceSettings::setUpdateInstructor(std::shared_ptr UpdateInstructor) { + if (!mIsLeader) { + return; + } + mUpdateInstructor = UpdateInstructor; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceSettings::set(std::string key, std::any value) { + if (!mIsLeader) { + return; + } + mUpdateSettings->operator[](key) = value; + addToUpdateList(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const std::shared_ptr> +SourceSettings::getCurrentSettings() const { + if (!mIsLeader) { + std::shared_ptr>(); + } + return mCurrentSettings; +} + +const std::shared_ptr> +SourceSettings::getUpdateSettings() const { + if (!mIsLeader) { + std::shared_ptr>(); + } + return mUpdateSettings; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceSettings::remove(std::string key) { + if (!mIsLeader) { + return; + } + mUpdateSettings->erase(key); + if (mCurrentSettings->find(key) == mCurrentSettings->end()) { + return; + } + mUpdateSettings->operator[](key) = std::string("remove"); + addToUpdateList(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SourceSettings::removeUpdate(std::string key) { + if (!mIsLeader) { + return; + } + mUpdateSettings->erase(key); + if (mUpdateSettings->empty()) { + removeFromUpdateList(); + } +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/SourceSettings.hpp b/src/cs-audio/internal/SourceSettings.hpp new file mode 100644 index 000000000..9059ed8da --- /dev/null +++ b/src/cs-audio/internal/SourceSettings.hpp @@ -0,0 +1,91 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_SOURCE_SETTINGS_HPP +#define CS_AUDIO_SOURCE_SETTINGS_HPP + +#include "UpdateConstructor.hpp" +#include "cs_audio_export.hpp" +#include +#include +#include +#include + +namespace cs::audio { + +class UpdateInstructor; + +/// @brief This class implements everything that is needed to define some properties for a source. +/// This property defining function is not limited to the source itself but also for a sourceGroup +/// and an audioController, which can both define properties for their sources. +class CS_AUDIO_EXPORT SourceSettings { + public: + virtual ~SourceSettings(); + + /// @brief Sets a value in mUpdateSettings + /// @param key setting type + /// @param value setting value + void set(std::string key, std::any value); + + /// @brief Returns the currently set settings for the sourceSettings instance. + /// To get all settings currently playing on a source call Source::getPlaybackSettings(). + /// @return Pointer to the settings map + const std::shared_ptr> getCurrentSettings() const; + + /// @brief Returns the currently set update settings for the sourceSettings instance. + /// @return Pointer to the settings map + const std::shared_ptr> getUpdateSettings() const; + + /// @brief Remove a setting and reset it to the default value. + /// @param key Setting to remove. + void remove(std::string key); + + /// @brief Removes a key from the update settings. + /// @param key key to remove + void removeUpdate(std::string key); + + // Is friend, because the UpdateConstructor needs write permissions to + // mUpdateSettings and mCurrentSettings. + friend class UpdateConstructor; + + protected: + /// @brief This is a standard constructor used for non-cluster mode and cluster mode leader calls + explicit SourceSettings(std::shared_ptr UpdateInstructor); + /// @brief This is a standard constructor used for non-cluster mode and cluster mode leader calls + explicit SourceSettings(); + /// @brief This Constructor will create a dummy SourceSetting which is used when a member of a + /// cluster tries to create a SourceSetting. Doing this will disable any functionality of this + /// class. + explicit SourceSettings(bool isLeader); + + /// Later assignment of UpdateInstructor needed because the audioController, which initializes the + /// UpdateInstructor, needs to initialize SourceSettings first. + void setUpdateInstructor(std::shared_ptr UpdateInstructor); + + bool mIsLeader; + /// Contains all settings that are about to be set using the AudioController::update() function. + /// If update() is called these settings will be used to apply to a source. Not all settings might + /// be set as they can be be overwritten by other settings higher up in the hierarchy (take a look + /// at the UpdateConstructor for more details on this). After the update all set values will be + /// written into mCurrentSettings and mUpdateSettings gets reset. + std::shared_ptr> mUpdateSettings; + /// Contains all settings currently set by sourceSettings instance itself + std::shared_ptr> mCurrentSettings; + /// UpdateInstructor to call to add sourceSettings instance to updateList + std::shared_ptr mUpdateInstructor; + + /// @brief Add sourceSettings instance to the updateList. Each derived class needs to implement + /// this by calling UpdateInstructor::update(shared_from_this()) + virtual void addToUpdateList() = 0; + /// @brief Remove sourceSettings instance from the updateList. Each derived class needs to + /// implement this by calling UpdateInstructor::removeUpdate(shared_from_this()) + virtual void removeFromUpdateList() = 0; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_SOURCE_SETTINGS_HPP diff --git a/src/cs-audio/internal/UpdateConstructor.cpp b/src/cs-audio/internal/UpdateConstructor.cpp new file mode 100644 index 000000000..66f5a81ce --- /dev/null +++ b/src/cs-audio/internal/UpdateConstructor.cpp @@ -0,0 +1,323 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "UpdateConstructor.hpp" +#include "../AudioController.hpp" +#include "../SourceGroup.hpp" +#include "ProcessingStepsManager.hpp" +#include "SettingsMixer.hpp" +#include "SourceBase.hpp" +#include +#include + +namespace cs::audio { + +UpdateConstructor::UpdateConstructor(std::shared_ptr processingStepsManager) + : mProcessingStepsManager(std::move(processingStepsManager)) { +} + +UpdateConstructor::~UpdateConstructor() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateConstructor::updateAll(std::shared_ptr>> sources, + std::shared_ptr>> groups, + std::shared_ptr audioController) { + + // possible improvement: disable mixing with group settings if there are no group updates -> + // change in createUpdateList() required + + if (containsRemove(audioController->mUpdateSettings)) { + for (auto sourcePtr : *sources) { + rebuildPlaybackSettings(audioController, sourcePtr); + } + + } else { + for (auto sourcePtr : *sources) { + + if (containsRemove(sourcePtr->mUpdateSettings)) { + rebuildPlaybackSettings(audioController, sourcePtr); + continue; + } + + if (!sourcePtr->mGroup.expired() && + containsRemove(sourcePtr->mGroup.lock()->mUpdateSettings)) { + rebuildPlaybackSettings(audioController, sourcePtr); + continue; + } + + // take controller settings + auto finalSettings = + std::make_shared>(*(audioController->mUpdateSettings)); + + // remove controller settings that are already set by the source + SettingsMixer::A_Without_B(finalSettings, sourcePtr->mCurrentSettings); + + if (!sourcePtr->mGroup.expired()) { + // remove controller settings that are already set by the group + SettingsMixer::A_Without_B(finalSettings, sourcePtr->mGroup.lock()->mCurrentSettings); + + // take group settings + auto finalGroup = std::make_shared>( + *(sourcePtr->mGroup.lock()->mUpdateSettings)); + + // remove group settings that are already set by the source + SettingsMixer::A_Without_B(finalGroup, sourcePtr->mCurrentSettings); + + // Mix controller and group Settings + SettingsMixer::OverrideAdd_A_with_B(finalSettings, finalGroup); + } + + // add source update settings to finalSettings + SettingsMixer::OverrideAdd_A_with_B(finalSettings, sourcePtr->mUpdateSettings); + + // run finalSetting through pipeline + auto failedSettings = mProcessingStepsManager->process( + sourcePtr, audioController->getControllerId(), finalSettings); + + // update current source playback settings + SettingsMixer::A_Without_B(finalSettings, failedSettings); + SettingsMixer::OverrideAdd_A_with_B(sourcePtr->mPlaybackSettings, finalSettings); + + // Update current source settings + SettingsMixer::A_Without_B(sourcePtr->mUpdateSettings, failedSettings); + SettingsMixer::OverrideAdd_A_with_B(sourcePtr->mCurrentSettings, sourcePtr->mUpdateSettings); + sourcePtr->mUpdateSettings->clear(); + } + } + + // Update currently set settings for a group + for (std::shared_ptr groupPtr : *groups) { + if (!groupPtr->mUpdateSettings->empty()) { + SettingsMixer::OverrideAdd_A_with_B(groupPtr->mCurrentSettings, groupPtr->mUpdateSettings); + groupPtr->mUpdateSettings->clear(); + } + } + + // Update currently set settings for the plugin + SettingsMixer::OverrideAdd_A_with_B( + audioController->mCurrentSettings, audioController->mUpdateSettings); + audioController->mUpdateSettings->clear(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateConstructor::updateGroups( + std::shared_ptr>> sources, + std::shared_ptr>> groups, + std::shared_ptr audioController) { + + for (auto sourcePtr : *sources) { + + if (containsRemove(sourcePtr->mUpdateSettings) || + containsRemove(sourcePtr->mGroup.lock()->mUpdateSettings)) { + rebuildPlaybackSettings(audioController, sourcePtr); + continue; + } + + // take group settings + auto finalSettings = std::make_shared>( + *(sourcePtr->mGroup.lock()->mUpdateSettings)); + + // remove settings that are already set by the source + SettingsMixer::A_Without_B(finalSettings, sourcePtr->mCurrentSettings); + + // add source update settings to finalSettings + SettingsMixer::OverrideAdd_A_with_B(finalSettings, sourcePtr->mUpdateSettings); + + // run finalSetting through pipeline + auto failedSettings = mProcessingStepsManager->process( + sourcePtr, audioController->getControllerId(), finalSettings); + + // update current source playback settings + SettingsMixer::A_Without_B(finalSettings, failedSettings); + SettingsMixer::OverrideAdd_A_with_B(sourcePtr->mPlaybackSettings, finalSettings); + + // Update current source settings + SettingsMixer::A_Without_B(sourcePtr->mUpdateSettings, failedSettings); + SettingsMixer::OverrideAdd_A_with_B(sourcePtr->mCurrentSettings, sourcePtr->mUpdateSettings); + sourcePtr->mUpdateSettings->clear(); + } + + // Update currently set settings for a group + for (std::shared_ptr groupPtr : *groups) { + if (!groupPtr->mUpdateSettings->empty()) { + SettingsMixer::OverrideAdd_A_with_B(groupPtr->mCurrentSettings, groupPtr->mUpdateSettings); + groupPtr->mUpdateSettings->clear(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateConstructor::updateSources( + std::shared_ptr>> sources, + std::shared_ptr audioController) { + + for (auto sourcePtr : *sources) { + + if (containsRemove(sourcePtr->mUpdateSettings)) { + rebuildPlaybackSettings(audioController, sourcePtr); + continue; + } + + // run finalSetting through pipeline + auto failedSettings = mProcessingStepsManager->process( + sourcePtr, audioController->getControllerId(), sourcePtr->mUpdateSettings); + + // update current source playback settings + SettingsMixer::A_Without_B(sourcePtr->mUpdateSettings, failedSettings); + SettingsMixer::OverrideAdd_A_with_B(sourcePtr->mPlaybackSettings, sourcePtr->mUpdateSettings); + + // Update currently set settings for a source + SettingsMixer::OverrideAdd_A_with_B(sourcePtr->mCurrentSettings, sourcePtr->mUpdateSettings); + sourcePtr->mUpdateSettings->clear(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateConstructor::applyCurrentControllerSettings(std::shared_ptr source, + std::shared_ptr audioController, + std::shared_ptr> settings) { + + // There is no need to check for already set values here because this functions only gets called + // when creating a new source, at which point there cannot be any previous settings. + + // run finalSetting through pipeline + auto failedSettings = + mProcessingStepsManager->process(source, audioController->getControllerId(), settings); + + // Update currently set settings for a source + auto settingsCopy(settings); + SettingsMixer::A_Without_B(settingsCopy, failedSettings); + SettingsMixer::OverrideAdd_A_with_B(source->mPlaybackSettings, settingsCopy); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateConstructor::applyCurrentGroupSettings(std::shared_ptr source, + std::shared_ptr audioController, + std::shared_ptr> settings) { + + // take group settings + std::map x(*settings); + auto finalSettings = std::make_shared>(x); + + // remove settings that are already set + SettingsMixer::A_Without_B(finalSettings, source->mCurrentSettings); + + // run finalSetting through pipeline + auto failedSettings = + mProcessingStepsManager->process(source, audioController->getControllerId(), finalSettings); + + // Update currently set settings for a source + SettingsMixer::A_Without_B(finalSettings, failedSettings); + SettingsMixer::OverrideAdd_A_with_B(source->mPlaybackSettings, finalSettings); +} + +void UpdateConstructor::removeCurrentGroupSettings( + std::shared_ptr source, std::shared_ptr audioController) { + + rebuildPlaybackSettings(audioController, source); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool UpdateConstructor::containsRemove(std::shared_ptr> settings) { + for (auto it = settings->begin(); it != settings->end(); ++it) { + if (it->second.type() == typeid(std::string) && + std::any_cast(it->second) == "remove") { + return true; + } + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateConstructor::rebuildPlaybackSettings( + std::shared_ptr audioController, std::shared_ptr source) { + // take current controller settings + auto finalSettings = + std::make_shared>(*(audioController->mCurrentSettings)); + // Mix with controller update settings + SettingsMixer::OverrideAdd_A_with_B(finalSettings, audioController->mUpdateSettings); + + // update current controller settings + auto controllerSettings = std::make_shared>(*(finalSettings)); + + // removing "remove" because "remove" settings should not appear in the current settings + SettingsMixer::A_Without_B_Value(controllerSettings, "remove"); + audioController->mCurrentSettings = controllerSettings; + audioController->mUpdateSettings->clear(); + + if (!source->mGroup.expired()) { + // take current group settings + auto groupSettings = std::make_shared>( + *(source->mGroup.lock()->mCurrentSettings)); + + // Mix with group update Settings + SettingsMixer::OverrideAdd_A_with_B(groupSettings, source->mGroup.lock()->mUpdateSettings); + + // filter out remove settings + auto normalGroupSettings = std::make_shared>(*(groupSettings)); + SettingsMixer::A_Without_B_Value(normalGroupSettings, "remove"); + + // create remove settings + auto removeGroupSettings = std::make_shared>(*(groupSettings)); + SettingsMixer::A_Without_B(removeGroupSettings, normalGroupSettings); + + // add controller settings with normalSettings + SettingsMixer::OverrideAdd_A_with_B(finalSettings, normalGroupSettings); + + // add finalSettings with removeSettings + SettingsMixer::Add_A_with_B_if_not_defined(finalSettings, removeGroupSettings); + + // update current group settings + source->mGroup.lock()->mCurrentSettings = normalGroupSettings; + source->mGroup.lock()->mUpdateSettings->clear(); + } + + // take current source settings + auto sourceSettings = + std::make_shared>(*(source->mCurrentSettings)); + + // Mix with source update Settings + SettingsMixer::OverrideAdd_A_with_B(sourceSettings, source->mUpdateSettings); + + // filter out remove settings + auto normalSourceSettings = std::make_shared>(*(sourceSettings)); + SettingsMixer::A_Without_B_Value(normalSourceSettings, "remove"); + + // create remove settings + auto removeSourceSettings = std::make_shared>(*(sourceSettings)); + SettingsMixer::A_Without_B(removeSourceSettings, normalSourceSettings); + + // add finalSettings with normalSettings + SettingsMixer::OverrideAdd_A_with_B(finalSettings, normalSourceSettings); + + // add finalSettings with removeSettings + SettingsMixer::Add_A_with_B_if_not_defined(finalSettings, removeSourceSettings); + + // run finalSettings through pipeline + auto failedSettings = + mProcessingStepsManager->process(source, audioController->getControllerId(), finalSettings); + + // Update current playback settings for a source + SettingsMixer::A_Without_B(finalSettings, failedSettings); + SettingsMixer::A_Without_B_Value(finalSettings, "remove"); + SettingsMixer::OverrideAdd_A_with_B(source->mPlaybackSettings, finalSettings); + + // Update currently set settings by a source + SettingsMixer::A_Without_B(normalSourceSettings, failedSettings); + source->mCurrentSettings = normalSourceSettings; + source->mUpdateSettings->clear(); +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/UpdateConstructor.hpp b/src/cs-audio/internal/UpdateConstructor.hpp new file mode 100644 index 000000000..c52fb8afc --- /dev/null +++ b/src/cs-audio/internal/UpdateConstructor.hpp @@ -0,0 +1,107 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_UPDATE_CONSTRUCTOR_HPP +#define CS_AUDIO_UPDATE_CONSTRUCTOR_HPP + +#include "cs_audio_export.hpp" +#include +#include +#include +#include +#include + +namespace cs::audio { + +// forward declaration +class SourceBase; +class SourceGroup; +class AudioController; +class ProcessingStepsManager; + +/// @brief This class takes the controller, groups and sources, which need to be updated, and +/// builds the final settings taking the hierarchy of these classes into account. +/// The general rule is 'local overwrites global', meaning the hierarchy looks like this: +/// controller < group < source, where source has the highest level. The idea is that the final +/// settings only contain things that are changing. Once the settings are computed, the +/// UpdateConstructor will call the pipeline and write the currently set settings to the controller, +/// groups, sources and playbackSettings. This class should only be instantiated once. +class CS_AUDIO_EXPORT UpdateConstructor { + public: + UpdateConstructor(std::shared_ptr processingStepsManager); + ~UpdateConstructor(); + + /// @brief Updates the controller, all groups and all sources + /// @param sources sources to update + /// @param groups groups to update + /// @param audioController controller to update + void updateAll(std::shared_ptr>> sources, + std::shared_ptr>> groups, + std::shared_ptr audioController); + + /// @brief Updates the groups and all sources + /// @param sources sources to update + /// @param groups groups to update + /// @param audioController audio controller on which sources and groups live + void updateGroups(std::shared_ptr>> sources, + std::shared_ptr>> groups, + std::shared_ptr audioController); + + /// @brief Update all sources + /// @param sources sources to update + /// @param audioController audio controller on which the source lives + void updateSources(std::shared_ptr>> sources, + std::shared_ptr audioController); + + /// @brief Update source settings with the currently set settings of the audio Controller. + /// Is only ever called when a new source gets created. + /// @param source source to update + /// @param audioController audioController on which the source lives + /// @param settings audio controller settings to apply to source + void applyCurrentControllerSettings(std::shared_ptr source, + std::shared_ptr audioController, + std::shared_ptr> settings); + + /// @brief Update source settings with the currently set settings of a group. + /// Is only ever called when a source gets added to a group. + /// @param source source to update + /// @param audioController audioController on which the source lives + /// @param settings group settings to apply to source + void applyCurrentGroupSettings(std::shared_ptr source, + std::shared_ptr audioController, + std::shared_ptr> settings); + + /// @brief Update source settings and remove the currently set settings of a group. + /// Gets called when a source leaves a group or the group gets deleted. + /// @param source source to update + /// @param audioController audioController on which the source lives + void removeCurrentGroupSettings( + std::shared_ptr source, std::shared_ptr audioController); + + private: + /// @brief Checks whether a settings map contains a remove setting + /// @param settings settings to check + /// @return True if settings contains remove + bool containsRemove(std::shared_ptr> settings); + + /// @brief Completely rebuilds the settings of source by taking the current and update setting + /// of the source, group and controller into account. This is only done if there is at least + /// one settings that gets removed. This is needed because if a setting gets removed, the + /// hierarchy can reverse, meaning a lower level could overwrite a higher one. Completely + /// rebuilding it is easier instead of trying to figure out if and how a lower level one could + /// overwrite a higher one. + /// @param audioController audioController on which the source lives. + /// @param source source to update + void rebuildPlaybackSettings( + std::shared_ptr audioController, std::shared_ptr source); + + std::shared_ptr mProcessingStepsManager; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_UPDATE_CONSTRUCTOR_HPP diff --git a/src/cs-audio/internal/UpdateInstructor.cpp b/src/cs-audio/internal/UpdateInstructor.cpp new file mode 100644 index 000000000..b1688fa3c --- /dev/null +++ b/src/cs-audio/internal/UpdateInstructor.cpp @@ -0,0 +1,112 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "UpdateInstructor.hpp" +#include "../SourceGroup.hpp" +#include "../logger.hpp" +#include + +namespace cs::audio { + +UpdateInstructor::UpdateInstructor() + : mSourceUpdateList(std::set, WeakPtrComparatorSource>()) + , mGroupUpdateList(std::set, WeakPtrComparatorGroup>()) + , mAudioControllerUpdate(false) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +UpdateInstructor::~UpdateInstructor() { + mSourceUpdateList.clear(); + mGroupUpdateList.clear(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateInstructor::update(std::shared_ptr source) { + mSourceUpdateList.insert(source); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateInstructor::update(std::shared_ptr sourceGroup) { + mGroupUpdateList.insert(sourceGroup); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateInstructor::update(std::shared_ptr audioController) { + mAudioControllerUpdate = true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateInstructor::removeUpdate(std::shared_ptr source) { + mSourceUpdateList.erase(source); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateInstructor::removeUpdate(std::shared_ptr sourceGroup) { + mGroupUpdateList.erase(sourceGroup); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void UpdateInstructor::removeUpdate(std::shared_ptr audioController) { + mAudioControllerUpdate = false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +UpdateInstructor::UpdateInstruction UpdateInstructor::createUpdateInstruction() { + UpdateInstruction result; + + if (mAudioControllerUpdate) { + // update every source and group + result.updateAll = true; + result.updateWithGroup = nullptr; + result.updateSourceOnly = nullptr; + + } else { + result.updateAll = false; + result.updateWithGroup = std::make_shared>>(); + result.updateSourceOnly = std::make_shared>>(); + + // add group members to updateList + for (auto groupPtr : mGroupUpdateList) { + if (groupPtr.expired()) { + continue; + } + auto groupMembers = groupPtr.lock()->getMembers(); + result.updateWithGroup->insert( + std::end(*(result.updateWithGroup)), std::begin(groupMembers), std::end(groupMembers)); + } + + // Filter out all source that are already part of updateWithGroup and add the rest to + // updateSourceOnly. This is done to not run the same source twice through the pipeline. + for (auto sourcePtr : mSourceUpdateList) { + if (sourcePtr.expired()) { + continue; + } + auto sourceShared = sourcePtr.lock(); + if (std::find(result.updateWithGroup->begin(), result.updateWithGroup->end(), sourceShared) == + result.updateWithGroup->end()) { + result.updateSourceOnly->push_back(sourceShared); + } + } + } + + // reset update state + mSourceUpdateList.clear(); + mGroupUpdateList.clear(); + mAudioControllerUpdate = false; + + return result; +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/internal/UpdateInstructor.hpp b/src/cs-audio/internal/UpdateInstructor.hpp new file mode 100644 index 000000000..c60e52874 --- /dev/null +++ b/src/cs-audio/internal/UpdateInstructor.hpp @@ -0,0 +1,119 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_UPDATE_INSTRUCTOR_HPP +#define CS_AUDIO_UPDATE_INSTRUCTOR_HPP + +#include "cs_audio_export.hpp" +#include +#include +#include +#include + +namespace cs::audio { + +// forward declarations +class SourceBase; +class SourceGroup; +class AudioController; + +/** + * @brief This class acts as the manager telling who needs to be updated. Each audio controller + * has its own updateInstructor. When a SourceSettings instance gets updated, it will register + * itself to this class. When AudioController::update() gets called, the UpdateInstructor will + * creates the update instructions, containing all sourceSettings instances, which need to be + *updated, and their update scope. There are 3 update scopes: + * 1. updateAll: When updating the audioController settings. The audioController, all Groups and + *Source get processed + * 2. updateWithGroup: When updating a Group. All changed groups and all their members get + *processed + * 3. updateSourceOnly: When updating a Source. Only the changed source gets processed + * When updateAll is active updateWithGroup and updateSourceOnly get ignored because all sources and + *groups need to be processed anyways. Otherwise both will be used to determine the sources which + *need to be updated. There is a filtering step to ensure that no source is part of both update + *scopes. + **/ +class CS_AUDIO_EXPORT UpdateInstructor { + public: + UpdateInstructor(); + ~UpdateInstructor(); + + /// @brief Adds a Source to the updateList + /// @param source Source to add + void update(std::shared_ptr source); + + /// @brief Adds a Source Group, and therefor all Member Sources, to the updateList + /// @param sourceGroup Source Group to add + void update(std::shared_ptr sourceGroup); + + /// @brief Adds an AudioController, and therefor all Sources and Groups + /// which live on the controller, to the updateList. + /// @param audioController AudioController to add + void update(std::shared_ptr audioController); + + /// @brief Removes a Source from the updateList + /// @param source Source to remove + void removeUpdate(std::shared_ptr source); + + /// @brief Removes a Source Group, and therefor all Member Sources, from the updateList + /// @param sourceGroup Source Group to remove + void removeUpdate(std::shared_ptr sourceGroup); + + /// @brief Removes an AudioController, and therefor all Sources and Groups + /// which live on the controller, from the updateList. + /// @param audioController AudioController to remove + void removeUpdate(std::shared_ptr audioController); + + /// Struct to hold all update instructions + struct UpdateInstruction { + bool updateAll; + std::shared_ptr>> updateWithGroup; + std::shared_ptr>> updateSourceOnly; + + // for testing: + void print() { + std::cout << "-----Update Instructions-----" << std::endl; + std::cout << "updateAll: " << (updateAll ? "true" : "false") << std::endl; + std::cout << "size group update: " + << (updateWithGroup == nullptr ? 0 : updateWithGroup->size()) << std::endl; + std::cout << "size source update: " + << (updateSourceOnly == nullptr ? 0 : updateSourceOnly->size()) << std::endl; + std::cout << "-----------------------------" << std::endl; + } + }; + + /// @brief Creates the update instructions when calling AudioController::update(). + UpdateInstruction createUpdateInstruction(); + + private: + struct WeakPtrComparatorGroup { + bool operator()( + const std::weak_ptr& left, const std::weak_ptr& right) const { + std::owner_less> sharedPtrLess; + return sharedPtrLess(left.lock(), right.lock()); + } + }; + + struct WeakPtrComparatorSource { + bool operator()( + const std::weak_ptr& left, const std::weak_ptr& right) const { + std::owner_less> sharedPtrLess; + return sharedPtrLess(left.lock(), right.lock()); + } + }; + + /// List of all source to be updated. + std::set, WeakPtrComparatorSource> mSourceUpdateList; + /// List of all source groups to be updated. + std::set, WeakPtrComparatorGroup> mGroupUpdateList; + /// Indicates if the audioController settings changed. + bool mAudioControllerUpdate; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_UPDATE_INSTRUCTOR_HPP diff --git a/src/cs-audio/logger.cpp b/src/cs-audio/logger.cpp new file mode 100644 index 000000000..d1ff0bd4d --- /dev/null +++ b/src/cs-audio/logger.cpp @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "logger.hpp" + +#include "../cs-utils/logger.hpp" + +namespace cs::audio { + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +spdlog::logger& logger() { + static auto logger = utils::createLogger("cs-audio"); + return *logger; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace cs::audio diff --git a/src/cs-audio/logger.hpp b/src/cs-audio/logger.hpp new file mode 100644 index 000000000..604cccf43 --- /dev/null +++ b/src/cs-audio/logger.hpp @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_LOGGER_HPP +#define CS_AUDIO_LOGGER_HPP + +#include "cs_audio_export.hpp" + +#include + +namespace cs::audio { + +/// This creates the default singleton logger for "cs-audio" when called for the first time and +/// returns it. See cs-utils/logger.hpp for more logging details. +CS_AUDIO_EXPORT spdlog::logger& logger(); + +} // namespace cs::audio + +#endif // CS_AUDIO_LOGGER_HPP diff --git a/src/cs-audio/precompiled.pch b/src/cs-audio/precompiled.pch new file mode 100644 index 000000000..f11336bf5 --- /dev/null +++ b/src/cs-audio/precompiled.pch @@ -0,0 +1,10 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +// Headers which are included here are used at least 3 times. + +#include "logger.hpp" \ No newline at end of file diff --git a/src/cs-audio/processingSteps/DirectPlay_PS.cpp b/src/cs-audio/processingSteps/DirectPlay_PS.cpp new file mode 100644 index 000000000..d26df7508 --- /dev/null +++ b/src/cs-audio/processingSteps/DirectPlay_PS.cpp @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "DirectPlay_PS.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" + +#include +#include + +namespace cs::audio { + +std::shared_ptr DirectPlay_PS::create() { + static auto directPlay_PS = std::shared_ptr(new DirectPlay_PS()); + return directPlay_PS; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +DirectPlay_PS::DirectPlay_PS() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void DirectPlay_PS::process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) { + + ALuint openAlId = source->getOpenAlId(); + + if (auto search = settings->find("playback"); search != settings->end()) { + if (!processPlayback(openAlId, search->second)) { + failedSettings->push_back("playback"); + } + } +} + +bool DirectPlay_PS::processPlayback(ALuint openAlId, std::any value) { + // wrong type passed + if (value.type() != typeid(std::string)) { + logger().warn("Audio source settings error! Wrong type used for playback setting! Allowed " + "Type: std::string"); + return false; + } + + std::string stringValue = std::any_cast(value); + + if (stringValue == "play") { + alSourcePlay(openAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to start playback of source!"); + return false; + } + return true; + } + + else if (stringValue == "stop") { + alSourceStop(openAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop playback of source!"); + return false; + } + return true; + } + + else if (stringValue == "pause") { + alSourcePause(openAlId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to pause playback of source!"); + return false; + } + return true; + } + logger().warn( + "Unkown value passed to playback settings! Allowed values: 'play', 'pause', 'stop'"); + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool DirectPlay_PS::requiresUpdate() const { + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void DirectPlay_PS::update() { +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/DirectPlay_PS.hpp b/src/cs-audio/processingSteps/DirectPlay_PS.hpp new file mode 100644 index 000000000..cb71a971a --- /dev/null +++ b/src/cs-audio/processingSteps/DirectPlay_PS.hpp @@ -0,0 +1,59 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_DIRECT_PLAY_HPP +#define CS_AUDIO_PS_DIRECT_PLAY_HPP + +#include "../internal/SourceBase.hpp" +#include "ProcessingStep.hpp" +#include "cs_audio_export.hpp" + +#include + +namespace cs::audio { + +/* +DirectPlay_PS is the most basic playback control processing step. It will immediately apply +the specified playback setting. +As with all playback control processing steps the playback setting can be set via the play(), +pause() and stop() functions of a source. +-------------------------------------------- +Name Type Range Description +-------------------------------------------- +playback std::string "play" playback option + "stop" + "pause" +-------------------------------------------- +*/ +class CS_AUDIO_EXPORT DirectPlay_PS : public ProcessingStep { + public: + /// @brief Creates new access to the single DirectPlay_PS object + /// @return Pointer to the PS + static std::shared_ptr create(); + + /// @brief processes a source with the given settings + /// @param source Source to process + /// @param settings settings to apply + /// @param failedSettings Pointer to list which contains all failed settings + void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) override; + + /// @return Wether the processing requires an update call each frame + bool requiresUpdate() const override; + + /// @brief update function to call each frame + void update() override; + + private: + DirectPlay_PS(); + bool processPlayback(ALuint openAlId, std::any value); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_DIRECT_PLAY_HPP diff --git a/src/cs-audio/processingSteps/DistanceCulling_PS.cpp b/src/cs-audio/processingSteps/DistanceCulling_PS.cpp new file mode 100644 index 000000000..e77201ac2 --- /dev/null +++ b/src/cs-audio/processingSteps/DistanceCulling_PS.cpp @@ -0,0 +1,203 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "DistanceCulling_PS.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" + +#include +#include +#include +#include +#include + +namespace cs::audio { + +std::shared_ptr DistanceCulling_PS::create(double distanceThreshold) { + static auto distanceCulling_ps = + std::shared_ptr(new DistanceCulling_PS(distanceThreshold)); + return distanceCulling_ps; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +DistanceCulling_PS::DistanceCulling_PS(double distanceThreshold) + : mDistanceThreshold(std::move(distanceThreshold)) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void DistanceCulling_PS::process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) { + + if (auto searchPos = settings->find("position"); searchPos != settings->end()) { + + auto searchPlayback = settings->find("playback"); + std::any newPlayback = + (searchPlayback != settings->end() ? searchPlayback->second : std::any()); + + if (!processPosition(source, searchPos->second, newPlayback)) { + failedSettings->push_back("playback"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool DistanceCulling_PS::processPosition( + std::shared_ptr source, std::any position, std::any newPlayback) { + + ALuint openALId = source->getOpenAlId(); + + // validate position setting + if (position.type() != typeid(glm::dvec3)) { + // remove position + if (position.type() == typeid(std::string) && + std::any_cast(position) == "remove") { + + alSourceStop(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop source playback!"); + return false; + } + return true; + } + + // wrong type passed + logger().warn("Audio source settings error! Wrong type used for position setting! Allowed " + "Type: glm::dvec3"); + return false; + } + + // Search for the currently set playback state(play, stop, pause) + std::string supposedState; + if (newPlayback.has_value()) { + if (newPlayback.type() == typeid(std::string)) { + supposedState = std::any_cast(newPlayback); + } + + } else { + auto settings = source->getPlaybackSettings(); + if (auto search = settings->find("playback"); search != settings->end()) { + supposedState = std::any_cast(search->second); + } + } + + // Get currently set state in OpenAL + ALint isState; + alGetSourcei(openALId, AL_SOURCE_STATE, &isState); + + /* Evaluate what to do based on the supposedState and isState of a source. Possible combinations: + + supposedState isState do + ------------------------------------------- + play play compute culling + stop compute culling + pause compute culling + stop play stop playback + stop nothing + pause stop playback + pause play pause playback + stop pause playback + pause nothing + */ + + if (supposedState == "stop") { + switch (isState) { + + case AL_PLAYING: + alSourceStop(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop playback of source!"); + return false; + } + return true; + + case AL_PAUSED: + alSourceStop(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop playback of source!"); + return false; + } + return true; + + case AL_STOPPED: + default: + return true; + } + } + + if (supposedState == "pause") { + switch (isState) { + + case AL_PLAYING: + alSourcePause(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to pause playback of source!"); + return false; + } + return true; + + case AL_STOPPED: + alSourcePause(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to pause playback of source!"); + return false; + } + return true; + + case AL_PAUSED: + default: + return true; + } + } + + // compute culling: + if (supposedState == "play") { + + glm::dvec3 sourcePosToObserver = std::any_cast(position); + double distance = glm::length(sourcePosToObserver); + + // start/pause source based on the distance compared to the specified threshold + if (distance > mDistanceThreshold) { + if (isState != AL_PAUSED && isState != AL_INITIAL) { + alSourcePause(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to pause playback of source!"); + return false; + } + } + } else { + if (isState != AL_PLAYING) { + alSourcePlay(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to start playback of source!"); + return false; + } + } + } + return true; + } + + logger().warn( + "Unkown value passed to playback settings! Allowed values: 'play', 'pause', 'stop'"); + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool DistanceCulling_PS::requiresUpdate() const { + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void DistanceCulling_PS::update() { +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/DistanceCulling_PS.hpp b/src/cs-audio/processingSteps/DistanceCulling_PS.hpp new file mode 100644 index 000000000..a76a28d46 --- /dev/null +++ b/src/cs-audio/processingSteps/DistanceCulling_PS.hpp @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_DISTANCE_CULLING_HPP +#define CS_AUDIO_PS_DISTANCE_CULLING_HPP + +#include "../internal/SourceBase.hpp" +#include "ProcessingStep.hpp" +#include "cs_audio_export.hpp" + +#include + +namespace cs::audio { + +/* +DistanceCulling_PS is a playback control processing step. If the playback option is set to "play" it +will play a source if it's distance to the observer is smaller then the specified distance culling +threshold distance. Otherwise it will pause the source. This processing step will only get active if +a source has a postion. If this is not the case the source will never get played. As with all +playback control processing steps the playback setting can be set via the play(), pause() and stop() +functions of a source. +-------------------------------------------- +Name Type Range Description +-------------------------------------------- +playback std::string "play" playback option + "stop" + "pause" +-------------------------------------------- +*/ +class CS_AUDIO_EXPORT DistanceCulling_PS : public ProcessingStep { + public: + /// @brief Creates new access to the single DistanceCulling_PS object + /// @return Pointer to the PS + static std::shared_ptr create(double distanceThreshold); + + /// @brief processes a source with the given settings + /// @param source Source to process + /// @param settings settings to apply + /// @param failedSettings Pointer to list which contains all failed settings + void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) override; + + /// @return Wether the processing requires an update call each frame + bool requiresUpdate() const override; + + /// @brief update function to call each frame + void update() override; + + private: + double mDistanceThreshold; + + DistanceCulling_PS(double distanceThreshold); + bool processPosition(std::shared_ptr, std::any position, std::any newPlayback); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_DISTANCE_CULLING_HPP diff --git a/src/cs-audio/processingSteps/DistanceModel_PS.cpp b/src/cs-audio/processingSteps/DistanceModel_PS.cpp new file mode 100644 index 000000000..aba163154 --- /dev/null +++ b/src/cs-audio/processingSteps/DistanceModel_PS.cpp @@ -0,0 +1,217 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "DistanceModel_PS.hpp" +#include "../../cs-scene/CelestialAnchor.hpp" +#include "../../cs-scene/CelestialSurface.hpp" +#include "../../cs-utils/convert.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" +#include +#include + +namespace cs::audio { + +std::shared_ptr DistanceModel_PS::create() { + static auto distanceModel_PS = std::shared_ptr(new DistanceModel_PS()); + return distanceModel_PS; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +DistanceModel_PS::DistanceModel_PS() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void DistanceModel_PS::process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) { + + ALuint openALId = source->getOpenAlId(); + auto searchModel = settings->find("distanceModel"); + if (searchModel != settings->end()) { + if (!processModel(openALId, searchModel->second)) { + failedSettings->push_back("distanceModel"); + } + } + + auto searchFallOffStart = settings->find("fallOffStart"); + if (searchFallOffStart != settings->end()) { + if (!processFallOffStart(openALId, searchFallOffStart->second)) { + failedSettings->push_back("fallOffStart"); + } + } + + auto searchFallOffEnd = settings->find("fallOffEnd"); + if (searchFallOffEnd != settings->end()) { + if (!processFallOffEnd(openALId, searchFallOffEnd->second)) { + failedSettings->push_back("fallOffEnd"); + } + } + + auto searchFallOffFactor = settings->find("fallOffFactor"); + if (searchFallOffFactor != settings->end()) { + if (!processFallOffFactor(openALId, searchFallOffFactor->second)) { + failedSettings->push_back("fallOffFactor"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool DistanceModel_PS::processModel(ALuint openALId, std::any model) { + if (model.type() != typeid(std::string)) { + logger().warn("Audio source settings error! Wrong type used for distanceModel setting! Allowed " + "Type: std::string"); + return false; + } + auto modelValue = std::any_cast(model); + + if (modelValue == "remove") { + modelValue = "inverse"; + } + + if (modelValue == "inverse") { + alSourcei(openALId, AL_DISTANCE_MODEL, AL_INVERSE_DISTANCE_CLAMPED); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set the distance model of a source!"); + return false; + } + return true; + } + + if (modelValue == "exponent") { + alSourcei(openALId, AL_DISTANCE_MODEL, AL_EXPONENT_DISTANCE_CLAMPED); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set the distance model of a source!"); + return false; + } + return true; + } + + if (modelValue == "linear") { + alSourcei(openALId, AL_DISTANCE_MODEL, AL_LINEAR_DISTANCE_CLAMPED); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set the distance model of a source!"); + return false; + } + return true; + } + + if (modelValue == "none") { + alSourcei(openALId, AL_DISTANCE_MODEL, AL_NONE); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set the distance model of a source!"); + return false; + } + return true; + } + + logger().warn("Audio source settings error! Wrong value passed for distanceModel setting! " + "Allowed values: 'inverse', 'exponent', 'linear', 'none'"); + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool DistanceModel_PS::processFallOffStart(ALuint openALId, std::any fallOffStart) { + if (fallOffStart.type() != typeid(float)) { + + if (fallOffStart.type() == typeid(std::string) && + std::any_cast(fallOffStart) == "remove") { + alSourcei(openALId, AL_REFERENCE_DISTANCE, 1); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset the fallOffStart setting of a source!"); + return false; + } + return true; + } + + logger().warn("Audio source settings error! Wrong type used for fallOffStart setting! Allowed " + "Type: float"); + return false; + } + + auto fallOffStartValue = std::any_cast(fallOffStart); + + alSourcef(openALId, AL_REFERENCE_DISTANCE, fallOffStartValue); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set the fallOffStart setting of a source!"); + return false; + } + return true; +} + +bool DistanceModel_PS::processFallOffEnd(ALuint openALId, std::any fallOffEnd) { + if (fallOffEnd.type() != typeid(float)) { + + if (fallOffEnd.type() == typeid(std::string) && + std::any_cast(fallOffEnd) == "remove") { + alSourcef(openALId, AL_MAX_DISTANCE, static_cast(std::numeric_limits::max())); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset the fallOffEnd setting of a source!"); + return false; + } + return true; + } + + logger().warn( + "Audio source settings error! Wrong type used for fallOffEnd setting! Allowed Type: float"); + return false; + } + + auto fallOffEndValue = std::any_cast(fallOffEnd); + + alSourcef(openALId, AL_MAX_DISTANCE, fallOffEndValue); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set the fallOffEnd setting of a source!"); + return false; + } + return true; +} + +bool DistanceModel_PS::processFallOffFactor(ALuint openALId, std::any fallOffFactor) { + if (fallOffFactor.type() != typeid(float)) { + + if (fallOffFactor.type() == typeid(std::string) && + std::any_cast(fallOffFactor) == "remove") { + alSourcei(openALId, AL_ROLLOFF_FACTOR, 1); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset the fallOffFactor setting of a source!"); + return false; + } + return true; + } + + logger().warn( + "Audio source settings error! Wrong type used for fallOffFactor setting! Allowed Type: float"); + return false; + } + + auto fallOffFactorValue = std::any_cast(fallOffFactor); + + alSourcef(openALId, AL_ROLLOFF_FACTOR, fallOffFactorValue); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set the fallOffFactor setting of a source!"); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool DistanceModel_PS::requiresUpdate() const { + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void DistanceModel_PS::update() { +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/DistanceModel_PS.hpp b/src/cs-audio/processingSteps/DistanceModel_PS.hpp new file mode 100644 index 000000000..8d421c962 --- /dev/null +++ b/src/cs-audio/processingSteps/DistanceModel_PS.hpp @@ -0,0 +1,60 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_DISTANCE_MODEL_HPP +#define CS_AUDIO_PS_DISTANCE_MODEL_HPP + +#include "../../cs-core/Settings.hpp" +#include "../internal/SourceBase.hpp" +#include "ProcessingStep.hpp" +#include "cs_audio_export.hpp" +#include +#include +#include +#include + +namespace cs::audio { +/* +The DistanceModel_PS introduces distance attenuation controls for a source. +This processing step will only get active if a source has a postion. +--------------------------------------------------------- +Name Type Range Description +--------------------------------------------------------- +distanceModel std::string "inverse" Defines the fallOff Shape. + "linear" + "exponent" +fallOffStart float 0.0 - Distance at which the fallOff Starts. If the distance is +smaller you will hear the source at full volume but still spatialized. fallOffEnd float 0.0 - +Distance at which the fallOff clamps. The does not disable the source but stops a further fallOff, +meaning the attenuation stays the same beyond this distance. fallOffFactor float 0.0 - +Multiplier to the distance attenuation. If set to 0.0, no attenuation occurs. +--------------------------------------------------------- +*/ +class CS_AUDIO_EXPORT DistanceModel_PS : public ProcessingStep { + public: + static std::shared_ptr create(); + + void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) override; + + bool requiresUpdate() const override; + + void update() override; + + private: + DistanceModel_PS(); + + bool processModel(ALuint openALId, std::any model); + bool processFallOffStart(ALuint openALId, std::any fallOffStart); + bool processFallOffEnd(ALuint openALId, std::any fallOffEnd); + bool processFallOffFactor(ALuint openALId, std::any fallOffFactor); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_DISTANCE_MODEL_HPP \ No newline at end of file diff --git a/src/cs-audio/processingSteps/PointSpatialization_PS.cpp b/src/cs-audio/processingSteps/PointSpatialization_PS.cpp new file mode 100644 index 000000000..e85cea4dc --- /dev/null +++ b/src/cs-audio/processingSteps/PointSpatialization_PS.cpp @@ -0,0 +1,102 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "PointSpatialization_PS.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace cs::audio { + +std::shared_ptr PointSpatialization_PS::create(bool stationaryOutputDevice) { + static auto spatialization_ps = + std::shared_ptr(new PointSpatialization_PS(stationaryOutputDevice)); + return spatialization_ps; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +PointSpatialization_PS::PointSpatialization_PS(bool stationaryOutputDevice) + : SpatializationUtils(stationaryOutputDevice) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void PointSpatialization_PS::process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) { + + if (auto search = settings->find("position"); search != settings->end()) { + if (!processPosition(source, search->second)) { + failedSettings->push_back("position"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool PointSpatialization_PS::processPosition(std::shared_ptr source, std::any value) { + + if (value.type() != typeid(glm::dvec3)) { + + // remove position + if (value.type() == typeid(std::string) && std::any_cast(value) == "remove") { + return resetSpatialization(source->getOpenAlId()); + } + + logger().warn("Audio source settings error! Wrong type used for position setting! Allowed " + "Type: glm::dvec3"); + return false; + } + + ALuint openAlId = source->getOpenAlId(); + + alSourcei(openAlId, AL_SOURCE_RELATIVE, AL_FALSE); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source position specification to absolute!"); + return false; + } + + glm::dvec3 positionValue = std::any_cast(value); + if (!mStationaryOutputDevice) { + compensateSpeakerRotation(positionValue); + } + alSource3f(openAlId, AL_POSITION, (ALfloat)positionValue.x, (ALfloat)positionValue.y, + (ALfloat)positionValue.z); + + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source position!"); + return false; + } + + mSourcePositions[openAlId] = + SourceContainer{std::weak_ptr(source), positionValue, positionValue}; + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool PointSpatialization_PS::requiresUpdate() const { + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void PointSpatialization_PS::update() { + calculateVelocity(); +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/PointSpatialization_PS.hpp b/src/cs-audio/processingSteps/PointSpatialization_PS.hpp new file mode 100644 index 000000000..5d11d3057 --- /dev/null +++ b/src/cs-audio/processingSteps/PointSpatialization_PS.hpp @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_POINT_SPATIALIZATION_HPP +#define CS_AUDIO_PS_POINT_SPATIALIZATION_HPP + +#include "../internal/SourceBase.hpp" +#include "ProcessingStep.hpp" +#include "SpatializationUtils.hpp" +#include "cs_audio_export.hpp" +#include +#include +#include + +namespace cs::audio { +/* +The PointSpatialization_PS is a spatialization processing step with which lets you define a position +as a single point in space. This processing step will also automatically compute the velocity of a +source and the observer. The position must be specified relative to the observer. +--------------------------------------------------------- +Name Type Range Description +--------------------------------------------------------- +position glm::dvec3 Position of a source relative to the observer. +--------------------------------------------------------- +*/ +class CS_AUDIO_EXPORT PointSpatialization_PS : public ProcessingStep, public SpatializationUtils { + public: + /// @brief Creates new access to the single PointSpatialization_PS object + /// @return Pointer to the PS + static std::shared_ptr create(bool stationaryOutputDevice); + + /// @brief processes a source with the given settings + /// @param source Source to process + /// @param settings settings to apply + /// @param failedSettings Pointer to list which contains all failed settings + void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) override; + + /// @return Wether the processing requires an update call each frame + bool requiresUpdate() const override; + + /// @brief update function to call each frame + void update() override; + + private: + PointSpatialization_PS(bool stationaryOutputDevice); + bool processPosition(std::shared_ptr source, std::any position); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_POINT_SPATIALIZATION_HPP diff --git a/src/cs-audio/processingSteps/ProcessingStep.hpp b/src/cs-audio/processingSteps/ProcessingStep.hpp new file mode 100644 index 000000000..f9de8630a --- /dev/null +++ b/src/cs-audio/processingSteps/ProcessingStep.hpp @@ -0,0 +1,51 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PROCESSING_STEP_HPP +#define CS_AUDIO_PROCESSING_STEP_HPP + +#include "../internal/SourceBase.hpp" +#include "cs_audio_export.hpp" +#include +#include +#include + +namespace cs::audio { + +/// A processing step is an optional building block to add more features to the audio engine. +/// This feature enhancement is limited to the settings that define the properties of a source. +/// Features that change the audio engine itself are not possible or are at least very +/// limited via processing steps. +class CS_AUDIO_EXPORT ProcessingStep { + public: + /// Every derived class of ProcessingStep must implement a static create() function. + /// Defining it here is not possible as virtual static function are not possible in C++. + /// An alternative would be to use the Curiously Recurring Template Pattern (CRTP) but this + /// approach would require an additional abstract parent class because with CRTP the + /// ProcessingStep class would become a template class which prevents the storage of all derived + /// classes inside a single container. + // virtual static std::shared_ptr create() = 0; + virtual ~ProcessingStep(){}; + + /// @brief processes a source with the given settings + /// @param source Source to process + /// @param settings settings to apply + /// @param failedSettings Pointer to list which contains all failed settings + virtual void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) = 0; + + /// @return Wether the processing requires an update call each frame + virtual bool requiresUpdate() const = 0; + + /// @brief update function to call each frame + virtual void update() = 0; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PROCESSING_STEP_HPP diff --git a/src/cs-audio/processingSteps/SoundAttributes_PS.cpp b/src/cs-audio/processingSteps/SoundAttributes_PS.cpp new file mode 100644 index 000000000..2cbb403fe --- /dev/null +++ b/src/cs-audio/processingSteps/SoundAttributes_PS.cpp @@ -0,0 +1,182 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "SoundAttributes_PS.hpp" +#include "../StreamingSource.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" + +#include +#include +#include + +namespace cs::audio { + +std::shared_ptr SoundAttributes_PS::create() { + static auto attributes_ps = std::shared_ptr(new SoundAttributes_PS()); + return attributes_ps; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SoundAttributes_PS::SoundAttributes_PS() { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SoundAttributes_PS::process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) { + + ALuint openAlId = source->getOpenAlId(); + + if (auto search = settings->find("gain"); search != settings->end()) { + if (!processGain(openAlId, search->second)) { + failedSettings->push_back("gain"); + } + } + + if (auto search = settings->find("looping"); search != settings->end()) { + if (!processLooping(source, search->second)) { + failedSettings->push_back("looping"); + } + } + + if (auto search = settings->find("pitch"); search != settings->end()) { + if (!processPitch(openAlId, search->second)) { + failedSettings->push_back("pitch"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SoundAttributes_PS::processGain(ALuint openAlId, std::any value) { + if (value.type() != typeid(float)) { + + // remove gain + if (value.type() == typeid(std::string) && std::any_cast(value) == "remove") { + + alSourcef(openAlId, AL_GAIN, 1.f); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset source gain!"); + return false; + } + return true; + } + + // wrong type provided + logger().warn( + "Audio source settings error! Wrong type used for gain setting! Allowed Type: float"); + return false; + } + + float floatValue = std::any_cast(value); + + if (floatValue < 0.f) { + logger().warn("Audio source settings error! Unable to set a negative gain!"); + return false; + } + + alSourcef(openAlId, AL_GAIN, floatValue); + + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source gain!"); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SoundAttributes_PS::processLooping(std::shared_ptr source, std::any value) { + ALuint openAlId = source->getOpenAlId(); + if (value.type() != typeid(bool)) { + + // remove looping + if (value.type() == typeid(std::string) && std::any_cast(value) == "remove") { + + alSourcei(openAlId, AL_LOOPING, AL_FALSE); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset source looping!"); + return false; + } + return true; + } + + // wrong type provided + logger().warn( + "Audio source settings error! Wrong type used for looping setting! Allowed Type: bool"); + return false; + } + + // Looping via OpenAL is not set for streaming sources. Doing this would make it impossible to + // stream because a buffer would never reach the 'processed' state. Instead we will check for + // looping inside the StreamingSource::updateStream() function and implement looping there. + if (auto derivedPtr = std::dynamic_pointer_cast(source)) { + return true; + } + + alSourcei(openAlId, AL_LOOPING, std::any_cast(value)); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source looping!"); + return false; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SoundAttributes_PS::processPitch(ALuint openAlId, std::any value) { + if (value.type() != typeid(float)) { + + // remove pitch + if (value.type() == typeid(std::string) && std::any_cast(value) == "remove") { + + alSourcef(openAlId, AL_PITCH, 1.f); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset source pitch!"); + return false; + } + return true; + } + + // wrong type provided + logger().warn( + "Audio source settings error! Wrong type used for pitch setting! Allowed Type: float"); + return false; + } + + float floatValue = std::any_cast(value); + + if (floatValue < 0.f) { + logger().warn("Audio source error! Unable to set a negative pitch!"); + return false; + } + + alSourcef(openAlId, AL_PITCH, floatValue); + + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source pitch!"); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SoundAttributes_PS::requiresUpdate() const { + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SoundAttributes_PS::update() { +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/SoundAttributes_PS.hpp b/src/cs-audio/processingSteps/SoundAttributes_PS.hpp new file mode 100644 index 000000000..29b73a2ad --- /dev/null +++ b/src/cs-audio/processingSteps/SoundAttributes_PS.hpp @@ -0,0 +1,60 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_SOUND_ATTRIBUTES_HPP +#define CS_AUDIO_PS_SOUND_ATTRIBUTES_HPP + +#include "../internal/SourceBase.hpp" +#include "ProcessingStep.hpp" +#include "cs_audio_export.hpp" + +#include + +namespace cs::audio { + +/* +This Processing Step introduces basic settings for a source and is automatically enabled +in every pipeline. +-------------------------------------------- +Name Type Range Description +-------------------------------------------- +gain float 0.0 - Multiplier for the Volume of a source +pitch float 0.5 - 2.0 Multiplier for the sample rate of the source's buffer +looping bool Whether the source shall loop the playback or stop after + playing the buffer once. +-------------------------------------------- +*/ +class CS_AUDIO_EXPORT SoundAttributes_PS : public ProcessingStep { + public: + /// @brief Creates new access to the single SoundAttributes_PS object + /// @return Pointer to the PS + static std::shared_ptr create(); + + /// @brief processes a source with the given settings + /// @param source Source to process + /// @param settings settings to apply + /// @param failedSettings Pointer to list which contains all failed settings + void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) override; + + /// @return Wether the processing requires an update call each frame + bool requiresUpdate() const override; + + /// @brief update function to call each frame + void update() override; + + private: + SoundAttributes_PS(); + bool processGain(ALuint openAlId, std::any value); + bool processLooping(std::shared_ptr source, std::any value); + bool processPitch(ALuint openAlId, std::any value); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_SOUND_ATTRIBUTES_HPP diff --git a/src/cs-audio/processingSteps/SpatializationUtils.cpp b/src/cs-audio/processingSteps/SpatializationUtils.cpp new file mode 100644 index 000000000..8da19cff2 --- /dev/null +++ b/src/cs-audio/processingSteps/SpatializationUtils.cpp @@ -0,0 +1,101 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "SpatializationUtils.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" +#include +#include +#include +#include + +namespace cs::audio { + +SpatializationUtils::SpatializationUtils(bool stationaryOutputDevice) + : mSourcePositions(std::map()) + , mLastTime(std::chrono::system_clock::now()) + , mStationaryOutputDevice(stationaryOutputDevice) { +} + +void SpatializationUtils::calculateVelocity() { + std::chrono::system_clock::time_point currentTime = std::chrono::system_clock::now(); + std::chrono::duration elapsed_seconds = currentTime - mLastTime; + auto elapsed_secondsf = elapsed_seconds.count(); + + for (auto source : mSourcePositions) { + + if (source.second.sourcePtr.expired()) { + mSourcePositions.erase(source.first); + continue; + } + + glm::dvec3 velocity; + ALuint openAlId = source.second.sourcePtr.lock()->getOpenAlId(); + + if (source.second.currentPos != source.second.lastPos) { + glm::dvec3 posDelta = source.second.currentPos - source.second.lastPos; + velocity.x = posDelta.x / elapsed_secondsf; + velocity.y = posDelta.y / elapsed_secondsf; + velocity.z = posDelta.z / elapsed_secondsf; + mSourcePositions[openAlId].lastPos = source.second.currentPos; + + } else { + velocity.x = 0; + velocity.y = 0; + velocity.z = 0; + } + + alSource3f( + openAlId, AL_VELOCITY, (ALfloat)velocity.x, (ALfloat)velocity.y, (ALfloat)velocity.z); + + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source velocity!"); + } + } + + mLastTime = currentTime; +} + +void SpatializationUtils::compensateSpeakerRotation(glm::dvec3& position) { + auto viewerOrient = GetVistaSystem() + ->GetDisplayManager() + ->GetDisplaySystem() + ->GetDisplaySystemProperties() + ->GetViewerOrientation(); + viewerOrient.Invert(); + VistaVector3D vista3d((float)position.x, (float)position.y, (float)position.z); + VistaVector3D sourceRelPosToObsRot = viewerOrient.Rotate(vista3d); + position.x = sourceRelPosToObsRot[0]; + position.y = sourceRelPosToObsRot[1]; + position.z = sourceRelPosToObsRot[2]; +} + +bool SpatializationUtils::resetSpatialization(ALuint openAlId) { + // TODO: erase source from list + mSourcePositions.erase(openAlId); + + alSourcei(openAlId, AL_SOURCE_RELATIVE, AL_TRUE); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset source position specification to relative!"); + return false; + } + + alSource3f(openAlId, AL_POSITION, (ALfloat)0.f, (ALfloat)0.f, (ALfloat)0.f); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset source position!"); + return false; + } + + alSource3f(openAlId, AL_VELOCITY, (ALfloat)0.f, (ALfloat)0.f, (ALfloat)0.f); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to reset source velocity!"); + return false; + } + return true; +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/SpatializationUtils.hpp b/src/cs-audio/processingSteps/SpatializationUtils.hpp new file mode 100644 index 000000000..b2b21ea54 --- /dev/null +++ b/src/cs-audio/processingSteps/SpatializationUtils.hpp @@ -0,0 +1,60 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_SPATIALIZATION_UTILS_HPP +#define CS_AUDIO_PS_SPATIALIZATION_UTILS_HPP + +#include "../internal/SourceBase.hpp" +#include "AL/al.h" +#include "cs_audio_export.hpp" +#include +#include +#include + +namespace cs::audio { + +class CS_AUDIO_EXPORT SpatializationUtils { + public: + SpatializationUtils(bool stationaryOutputDevice); + + /// @brief Calculates and applies the velocity for each spatialized source via the change of + /// position + void calculateVelocity(); + + /// @brief Rotates the the position of source around the inverse of the vista viewer orientation. + /// This is needed to keep the relative position between the physical audio output device and the + /// audio source in Cosmoscout the same when the output device, for example when using headphones + /// on a head mounted display, rotate with the user. This is only gets called if the config value + /// stationarySpeaker is set to false. + /// @param position Relative position to observer + void compensateSpeakerRotation(glm::dvec3& position); + + /// @brief Sets the position and Velocity of a source to zero and removes said source from the + /// update list. + /// @param openAlId id of source to reset + /// @return True if successful. False otherwise. + bool resetSpatialization(ALuint openAlId); + + protected: + /// Struct to hold all necessary information regarding a spatialized source + struct SourceContainer { + std::weak_ptr sourcePtr; + glm::dvec3 currentPos; + glm::dvec3 lastPos; + }; + + /// List of all Source which have a position + std::map mSourcePositions; + /// Point in time since the last calculateVelocity() call + std::chrono::system_clock::time_point mLastTime; + /// Whether the audio output devices moves with the user or is stationary + bool mStationaryOutputDevice; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_SPATIALIZATION_UTILS_HPP diff --git a/src/cs-audio/processingSteps/SphereSpatialization_PS.cpp b/src/cs-audio/processingSteps/SphereSpatialization_PS.cpp new file mode 100644 index 000000000..88c2af85f --- /dev/null +++ b/src/cs-audio/processingSteps/SphereSpatialization_PS.cpp @@ -0,0 +1,176 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "SphereSpatialization_PS.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" +#include +#include +#include +#include + +namespace cs::audio { + +std::shared_ptr SphereSpatialization_PS::create(bool stationaryOutputDevice) { + static auto sphereSpatialization_PS = + std::shared_ptr(new SphereSpatialization_PS(stationaryOutputDevice)); + return sphereSpatialization_PS; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SphereSpatialization_PS::SphereSpatialization_PS(bool stationaryOutputDevice) + : SpatializationUtils(stationaryOutputDevice) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SphereSpatialization_PS::process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) { + + ALuint openAlId = source->getOpenAlId(); + bool processRequired = false; + std::any pos, radius; + if (auto searchPos = settings->find("position"); searchPos != settings->end()) { + if (processPosition(openAlId, searchPos->second)) { + processRequired = true; + pos = searchPos->second; + } else { + failedSettings->push_back("position"); + } + } + + if (auto searchRad = settings->find("sourceRadius"); searchRad != settings->end()) { + if (processRadius(openAlId, searchRad->second)) { + processRequired = true; + radius = searchRad->second; + } else { + failedSettings->push_back("sourceRadius"); + } + } + + if (processRequired) { + if (!pos.has_value()) { + auto currentSettings = source->getPlaybackSettings(); + if (auto searchPos = currentSettings->find("position"); searchPos != currentSettings->end()) { + pos = searchPos->second; + } else { + return; + } + } + + if (!radius.has_value()) { + auto currentSettings = source->getPlaybackSettings(); + if (auto searchRad = currentSettings->find("sourceRadius"); + searchRad != currentSettings->end()) { + radius = searchRad->second; + } else { + return; + } + } + + processSpatialization(source, pos, radius); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SphereSpatialization_PS::processPosition(ALuint openAlId, std::any position) { + if (position.type() != typeid(glm::dvec3)) { + + // remove position setting from source + if (position.type() == typeid(std::string) && + std::any_cast(position) == "remove") { + return resetSpatialization(openAlId); + } + + // wrong datatype used for position + logger().warn("Audio source settings error! Wrong type used for position setting! Allowed " + "Type: glm::dvec3"); + return false; + } + return true; +} + +bool SphereSpatialization_PS::processRadius(ALuint openAlId, std::any sourceRadius) { + if (sourceRadius.type() != typeid(float)) { + + // remove source radius setting from source + if (sourceRadius.type() == typeid(std::string) && + std::any_cast(sourceRadius) == "remove") { + return resetSpatialization(openAlId); + } + + // wrong datatype used for position + logger().warn("Audio source settings error! Wrong type used for sourceRadius setting! Allowed " + "Type: float"); + return false; + } + + if (std::any_cast(sourceRadius) < 0.f) { + logger().warn("Audio source settings error! Unable to set a negative source radius!"); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SphereSpatialization_PS::processSpatialization( + std::shared_ptr source, std::any position, std::any sourceRadius) { + + auto sourcePosToObserver = std::any_cast(position); + if (!mStationaryOutputDevice) { + compensateSpeakerRotation(sourcePosToObserver); + } + auto radius = std::any_cast(sourceRadius); + ALuint openAlId = source->getOpenAlId(); + + // Set source position to Observer Pos if the Observer is inside the source radius. + // Otherwise set to the real position. + alSourcei(openAlId, AL_SOURCE_RELATIVE, AL_FALSE); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source position specification to absolute!"); + return false; + } + + if (glm::length(sourcePosToObserver) < radius) { + alSource3f(openAlId, AL_POSITION, (ALfloat)0.f, (ALfloat)0.f, (ALfloat)0.f); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source position!"); + return false; + } + + } else { + alSource3f(openAlId, AL_POSITION, (ALfloat)sourcePosToObserver.x, + (ALfloat)sourcePosToObserver.y, (ALfloat)sourcePosToObserver.z); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set source position!"); + return false; + } + } + + mSourcePositions[openAlId] = + SourceContainer{std::weak_ptr(source), sourcePosToObserver, sourcePosToObserver}; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SphereSpatialization_PS::requiresUpdate() const { + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SphereSpatialization_PS::update() { + calculateVelocity(); +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/SphereSpatialization_PS.hpp b/src/cs-audio/processingSteps/SphereSpatialization_PS.hpp new file mode 100644 index 000000000..429267c31 --- /dev/null +++ b/src/cs-audio/processingSteps/SphereSpatialization_PS.hpp @@ -0,0 +1,61 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_SPHERE_SOURCE_HPP +#define CS_AUDIO_PS_SPHERE_SOURCE_HPP + +#include "../internal/SourceBase.hpp" +#include "ProcessingStep.hpp" +#include "SpatializationUtils.hpp" +#include "cs_audio_export.hpp" +#include +#include +#include +#include + +namespace cs::audio { +/* +The SphereSpatialization_PS is a spatialization processing step which lets you define a position +as a 3D sphere in space. If the observer is inside the sphere you will hear the source at full +volume without spatialization. If the sphere is large and the observer leaves the sphere you will +notice that the source will most probably cut off immediately. This is because once the observer is +outside the sphere the source gets positioned at the center of the sphere and due to the distance +attenuation the volume drops to zero. If this is not the behaviour you want, you can use the +DistanceModel processing step and set the fallOffStart to the sphere radius. This will enable the +distance attenuation only at the edge of the sphere. This processing step will also automatically +compute the velocity of a source and the observer. The position must be specified relative to the +observer. +--------------------------------------------------------- +Name Type Range Description +--------------------------------------------------------- +position glm::dvec3 Position of a source relative to the observer. +sourceRadius float 0.0 - Radius of the sphere. +--------------------------------------------------------- +*/ +class CS_AUDIO_EXPORT SphereSpatialization_PS : public ProcessingStep, public SpatializationUtils { + public: + static std::shared_ptr create(bool stationaryOutputDevice); + + void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) override; + + bool requiresUpdate() const override; + + void update() override; + + private: + SphereSpatialization_PS(bool stationaryOutputDevice); + bool processPosition(ALuint openAlId, std::any position); + bool processRadius(ALuint openAlId, std::any sourceRadius); + bool processSpatialization( + std::shared_ptr source, std::any position, std::any sourceRadius); +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_SPHERE_SOURCE_HPP diff --git a/src/cs-audio/processingSteps/VolumeCulling_PS.cpp b/src/cs-audio/processingSteps/VolumeCulling_PS.cpp new file mode 100644 index 000000000..392439eed --- /dev/null +++ b/src/cs-audio/processingSteps/VolumeCulling_PS.cpp @@ -0,0 +1,270 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "VolumeCulling_PS.hpp" +#include "../internal/AlErrorHandling.hpp" +#include "../logger.hpp" + +#include +#include +#include +#include +#include + +namespace cs::audio { + +std::shared_ptr VolumeCulling_PS::create(float gainThreshold) { + static auto volumeCulling_ps = + std::shared_ptr(new VolumeCulling_PS(gainThreshold)); + return volumeCulling_ps; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +VolumeCulling_PS::VolumeCulling_PS(float gainThreshold) + : mGainThreshold(std::move(gainThreshold)) { +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void VolumeCulling_PS::process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) { + + if (auto searchPos = settings->find("position"); searchPos != settings->end()) { + + auto searchGain = settings->find("gain"); + std::any newGain = (searchGain != settings->end() ? searchGain->second : std::any()); + + auto searchPlayback = settings->find("playback"); + std::any newPlayback = + (searchPlayback != settings->end() ? searchPlayback->second : std::any()); + + if (!processPosition(source, searchPos->second, newGain, newPlayback)) { + failedSettings->push_back("playback"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool VolumeCulling_PS::processPosition( + std::shared_ptr source, std::any position, std::any newGain, std::any newPlayback) { + + ALuint openALId = source->getOpenAlId(); + + // validate position setting + if (position.type() != typeid(glm::dvec3)) { + // remove position + if (position.type() == typeid(std::string) && + std::any_cast(position) == "remove") { + + alSourceStop(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop source playback!"); + return false; + } + return true; + } + + // wrong type passed + logger().warn("Audio source settings error! Wrong type used for position setting! Allowed " + "Type: glm::dvec3"); + return false; + } + + // Search for the currently set playback state(play, stop, pause) + std::string supposedState; + if (newPlayback.has_value()) { + if (newPlayback.type() == typeid(std::string)) { + supposedState = std::any_cast(newPlayback); + } + + } else { + auto settings = source->getPlaybackSettings(); + if (auto search = settings->find("playback"); search != settings->end()) { + supposedState = std::any_cast(search->second); + } + } + + // Get currently set state in OpenAL + ALint isState; + alGetSourcei(openALId, AL_SOURCE_STATE, &isState); + + /* Evaluate what to do based on the supposedState and isState of a source. Possible combinations: + + supposedState isState do + ------------------------------------------- + play play compute culling + stop compute culling + pause compute culling + stop play stop playback + stop nothing + pause stop playback + pause play pause playback + stop pause playback + pause nothing + */ + + if (supposedState == "stop") { + switch (isState) { + + case AL_PLAYING: + alSourceStop(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop playback of source!"); + return false; + } + return true; + + case AL_PAUSED: + alSourceStop(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to stop playback of source!"); + return false; + } + return true; + + case AL_STOPPED: + default: + return true; + } + } + + if (supposedState == "pause") { + switch (isState) { + + case AL_PLAYING: + alSourcePause(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to pause playback of source!"); + return false; + } + return true; + + case AL_STOPPED: + alSourcePause(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to pause playback of source!"); + return false; + } + return true; + + case AL_PAUSED: + default: + return true; + } + } + + // compute culling: + if (supposedState == "play") { + glm::dvec3 sourcePosToObserver = std::any_cast(position); + + ALint disModel; + ALfloat rollOffFac, refDis, maxDis; + alGetSourcei(openALId, AL_DISTANCE_MODEL, &disModel); + alGetSourcef(openALId, AL_ROLLOFF_FACTOR, &rollOffFac); + alGetSourcef(openALId, AL_REFERENCE_DISTANCE, &refDis); + alGetSourcef(openALId, AL_MAX_DISTANCE, &maxDis); + + double distance = glm::length(sourcePosToObserver); + distance = (distance > refDis ? distance : refDis); + distance = (distance < maxDis ? distance : maxDis); + + double supposedVolume; + switch (disModel) { + case AL_INVERSE_DISTANCE_CLAMPED: + supposedVolume = inverseClamped(distance, rollOffFac, refDis, maxDis); + break; + case AL_LINEAR_DISTANCE_CLAMPED: + supposedVolume = linearClamped(distance, rollOffFac, refDis, maxDis); + break; + case AL_EXPONENT_DISTANCE_CLAMPED: + supposedVolume = exponentClamped(distance, rollOffFac, refDis, maxDis); + break; + default: + logger().warn("Unsupported distance model used! Only clamped distance models are supported!"); + return false; + } + + // Multiply just calculated volume with the gain of the source: + float gain = -1.f; + // check if a new gain is being set during this update cycle + if (newGain.has_value()) { + if (newGain.type() == typeid(float)) { + gain = std::any_cast(newGain); + } + + // else check if a gain was set in a previous update cycle + } else { + auto settings = source->getPlaybackSettings(); + if (auto search = settings->find("gain"); search != settings->end()) { + gain = std::any_cast(search->second); + } + } + + if (gain != -1.f) { + supposedVolume *= gain; + } + + // start/pause source based on the volume compared to the specified threshold + if (supposedVolume < mGainThreshold) { + if (isState != AL_PAUSED && isState != AL_INITIAL) { + alSourcePause(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to pause playback of source!"); + return false; + } + } + } else { + if (isState != AL_PLAYING) { + alSourcePlay(openALId); + if (AlErrorHandling::errorOccurred()) { + logger().warn("Failed to start playback of source!"); + return false; + } + } + } + return true; + } + + logger().warn( + "Unkown value passed to playback settings! Allowed values: 'play', 'pause', 'stop'"); + return false; +} + +double VolumeCulling_PS::inverseClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) const { + return referenceDistance / (referenceDistance + rollOffFactor * (distance - referenceDistance)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +double VolumeCulling_PS::linearClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) const { + return (1 - rollOffFactor * (distance - referenceDistance) / (maxDistance - referenceDistance)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +double VolumeCulling_PS::exponentClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) const { + return std::pow((distance / referenceDistance), -1 * rollOffFactor); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool VolumeCulling_PS::requiresUpdate() const { + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void VolumeCulling_PS::update() { +} + +} // namespace cs::audio \ No newline at end of file diff --git a/src/cs-audio/processingSteps/VolumeCulling_PS.hpp b/src/cs-audio/processingSteps/VolumeCulling_PS.hpp new file mode 100644 index 000000000..d4d0f39a8 --- /dev/null +++ b/src/cs-audio/processingSteps/VolumeCulling_PS.hpp @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_AUDIO_PS_VOLUME_CULLING_HPP +#define CS_AUDIO_PS_VOLUME_CULLING_HPP + +#include "../internal/SourceBase.hpp" +#include "ProcessingStep.hpp" +#include "cs_audio_export.hpp" + +#include + +namespace cs::audio { + +/* +VolumeCulling_PS is a playback control processing step. If the playback option is set to "play" it +will play a source if it's theoretical volume is greater then the specified volume culling +threshold. This theoretical volume is calculated according to a sources distance model formula and +multiplied by the set gain via Default_PS. This volume does not necessarily reflect the actual +volume of a source because there many more factors that can have an influence. This processing step +will only get active if a source has a postion. If this is not the case the source will never get +played. As with all playback control processing steps the playback setting can be set via the +play(), pause() and stop() functions of a source. +-------------------------------------------- +Name Type Range Description +-------------------------------------------- +playback std::string "play" playback option + "stop" + "pause" +-------------------------------------------- +*/ +class CS_AUDIO_EXPORT VolumeCulling_PS : public ProcessingStep { + public: + /// @brief Creates new access to the single Default_PS object + /// @return Pointer to the PS + static std::shared_ptr create(float gainThreshold); + + /// @brief processes a source with the given settings + /// @param source Source to process + /// @param settings settings to apply + /// @param failedSettings Pointer to list which contains all failed settings + void process(std::shared_ptr source, + std::shared_ptr> settings, + std::shared_ptr> failedSettings) override; + + /// @return Wether the processing requires an update call each frame + bool requiresUpdate() const override; + + /// @brief update function to call each frame + void update() override; + + private: + float mGainThreshold; + + VolumeCulling_PS(float gainThreshold); + bool processPosition( + std::shared_ptr, std::any position, std::any newGain, std::any newPlayback); + double inverseClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) const; + double linearClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) const; + double exponentClamped( + double distance, ALfloat rollOffFactor, ALfloat referenceDistance, ALfloat maxDistance) const; +}; + +} // namespace cs::audio + +#endif // CS_AUDIO_PS_VOLUME_CULLING_HPP diff --git a/src/cs-core/AudioEngine.cpp b/src/cs-core/AudioEngine.cpp new file mode 100644 index 000000000..4b6a192ef --- /dev/null +++ b/src/cs-core/AudioEngine.cpp @@ -0,0 +1,194 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#include "AudioEngine.hpp" +#include "GuiManager.hpp" +#include "Settings.hpp" +#include "logger.hpp" + +#include "../cs-audio/Source.hpp" +#include "../cs-audio/SourceGroup.hpp" +#include "../cs-audio/internal/AlErrorHandling.hpp" +#include "../cs-audio/internal/BufferManager.hpp" +#include "../cs-audio/internal/FileReader.hpp" +#include "../cs-audio/internal/Listener.hpp" +#include "../cs-audio/internal/OpenAlManager.hpp" +#include "../cs-audio/internal/ProcessingStepsManager.hpp" +#include "../cs-audio/internal/UpdateConstructor.hpp" +#include "../cs-utils/FrameStats.hpp" +#include "../cs-utils/Property.hpp" +#include + +namespace cs::core { + +AudioEngine::AudioEngine(std::shared_ptr settings, std::shared_ptr guiManager) + : std::enable_shared_from_this() + , mIsLeader(GetVistaSystem()->GetIsClusterLeader()) { + logger().debug("mIsLeader: {}", mIsLeader); + if (!mIsLeader) { + return; + } + mSettings = std::move(settings); + mGuiManager = std::move(guiManager); + mOpenAlManager = std::make_shared(); + mBufferManager = std::make_shared(); + mProcessingStepsManager = std::make_shared(mSettings); + mUpdateConstructor = std::make_shared(mProcessingStepsManager); + mMasterVolume = utils::Property(1.f); + mAudioControllers = std::vector>(); + + // Tell the user what's going on. + logger().debug("Creating AudioEngine."); + + if (!mOpenAlManager->initOpenAl(mSettings->mAudio)) { + logger().error("Failed to (fully) initialize OpenAL!"); + return; + } + logger().info("OpenAL-Soft Vendor: {}", alGetString(AL_VENDOR)); + logger().info("OpenAL-Soft Version: {}", alGetString(AL_VERSION)); + + createGUI(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +AudioEngine::~AudioEngine() { + if (mIsLeader) { + try { + // Tell the user what's going on. + logger().debug("Deleting AudioEngine."); + + mAudioControllers.clear(); + } catch (...) {} + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector AudioEngine::getDevices() { + if (!mIsLeader) { + return std::vector(); + } + return mOpenAlManager->getDevices(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool AudioEngine::setDevice(std::string outputDevice) { + if (!mIsLeader) { + return true; + } + + if (mOpenAlManager->setDevice(outputDevice)) { + // update gui: + mGuiManager->getGui()->callJavascript( + "CosmoScout.gui.setDropdownValue", "audio.outputDevice", outputDevice); + return true; + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool AudioEngine::setMasterVolume(float gain) { + if (!mIsLeader) { + return true; + } + + if (gain < 0) { + logger().warn("Unable to set a negative gain!"); + return false; + } + alListenerf(AL_GAIN, (ALfloat)gain); + if (audio::AlErrorHandling::errorOccurred()) { + logger().warn("Failed to set master volume!"); + return false; + } + mMasterVolume = gain; + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioEngine::createGUI() { + // add settings to GUI + mGuiManager->addSettingsSectionToSideBarFromHTML( + "Audio", "volume_up", "../share/resources/gui/audio_settings.html"); + mGuiManager->executeJavascriptFile("../share/resources/gui/js/audio_settings.js"); + + // register callback for master volume slider + mGuiManager->getGui()->registerCallback("audio.masterVolume", + "Values sets the overall audio volume.", + std::function([this](double value) { setMasterVolume(static_cast(value)); })); + mMasterVolume.connectAndTouch( + [this](float value) { mGuiManager->setSliderValue("audio.masterVolume", value); }); + + // Fill the dropdowns with the available output devices + for (auto device : getDevices()) { + + std::string displayName; + if (device.find(std::string{"OpenAL Soft on"}) != std::string::npos) { + displayName = device.substr(14, device.length()); + } else { + displayName = device; + } + mGuiManager->getGui()->callJavascript( + "CosmoScout.gui.addDropdownValue", "audio.outputDevice", device, displayName, false); + } + + // register callback for dropdown output devices + mGuiManager->getGui()->registerCallback("audio.outputDevice", "Set the audio output device.", + std::function([this](std::string value) { setDevice(static_cast(value)); })); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void AudioEngine::update() { + if (!mIsLeader) { + return; + } + + auto frameStats = + cs::utils::FrameStats::ScopedTimer("AudioEngineMain", cs::utils::FrameStats::TimerMode::eCPU); + + // Call all update functions of active Processing steps + mProcessingStepsManager->callPsUpdateFunctions(); + + // Check if a stream finished a buffer. If so refill and requeue buffer to the stream. + bool controllerExpired = false; + + for (auto controller : mAudioControllers) { + if (controller.expired()) { + controllerExpired = true; + continue; + } + controller.lock()->updateStreamingSources(); + } + if (controllerExpired) { + mAudioControllers.erase( + std::remove_if(mAudioControllers.begin(), mAudioControllers.end(), + [](const std::weak_ptr& ptr) { return ptr.expired(); }), + mAudioControllers.end()); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::shared_ptr AudioEngine::createAudioController() { + if (!mIsLeader) { + return std::make_shared(); + } + + static int controllerId = 0; + auto controller = std::make_shared( + mBufferManager, mProcessingStepsManager, mUpdateConstructor, controllerId++); + controller->setPipeline(std::vector{"DirectPlay"}); + mAudioControllers.push_back(controller); + return controller; +} + +} // namespace cs::core \ No newline at end of file diff --git a/src/cs-core/AudioEngine.hpp b/src/cs-core/AudioEngine.hpp new file mode 100644 index 000000000..eed76a739 --- /dev/null +++ b/src/cs-core/AudioEngine.hpp @@ -0,0 +1,79 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: German Aerospace Center (DLR) +// SPDX-License-Identifier: MIT + +#ifndef CS_CORE_AUDIO_AUDIOENGINE_HPP +#define CS_CORE_AUDIO_AUDIOENGINE_HPP + +#include "GuiManager.hpp" +#include "Settings.hpp" +#include "cs_audio_export.hpp" + +#include "../cs-audio/AudioController.hpp" +#include "../cs-audio/Source.hpp" +#include "../cs-audio/SourceGroup.hpp" +#include "../cs-audio/StreamingSource.hpp" +#include "../cs-audio/internal/BufferManager.hpp" +#include "../cs-audio/internal/OpenAlManager.hpp" +#include "../cs-audio/internal/ProcessingStepsManager.hpp" +#include "../cs-audio/internal/UpdateConstructor.hpp" +#include "../cs-utils/Property.hpp" + +#include +#include + +namespace cs::core { + +/// @brief The AudioEngine is responsible for initializing all necessary audio classes. +/// It also provides access to create audio controllers. This class should only be instantiated +/// once. This instance will be passed to all plugins. +class CS_CORE_EXPORT AudioEngine : public std::enable_shared_from_this { + public: + AudioEngine(const AudioEngine& obj) = delete; + AudioEngine(AudioEngine&&) = delete; + + AudioEngine& operator=(const AudioEngine&) = delete; + AudioEngine& operator=(AudioEngine&&) = delete; + + AudioEngine(std::shared_ptr settings, std::shared_ptr guiManager); + ~AudioEngine(); + + /// @brief Returns a list of all possible Output Devices (wrapper to the OpenAlManager function) + std::vector getDevices(); + + /// @brief Sets the output device for the audioEngine (wrapper to the OpenAlManager function) + bool setDevice(std::string outputDevice); + + /// @brief Sets the master volume for the audioEngine + /// @return Whether it was successful + bool setMasterVolume(float gain); + + /// @brief Update function to call every frame. Currently calls only the update function of the + /// processing steps manager. + void update(); + + /// @brief Create a new AudioController + /// @return Ptr to the new controller + std::shared_ptr createAudioController(); + + private: + std::shared_ptr mSettings; + std::shared_ptr mOpenAlManager; + std::shared_ptr mBufferManager; + std::shared_ptr mProcessingStepsManager; + std::shared_ptr mGuiManager; + std::vector> mAudioControllers; + std::shared_ptr mUpdateConstructor; + utils::Property mMasterVolume; + bool mIsLeader; + + /// Creates the Audio GUI Settings + void createGUI(); +}; + +} // namespace cs::core + +#endif // CS_CORE_AUDIO_AUDIOENGINE_HPP diff --git a/src/cs-core/CMakeLists.txt b/src/cs-core/CMakeLists.txt index 1b479e64e..4dd95b28b 100644 --- a/src/cs-core/CMakeLists.txt +++ b/src/cs-core/CMakeLists.txt @@ -21,6 +21,7 @@ target_link_libraries(cs-core PUBLIC cs-scene cs-graphics + cs-audio cs-gui cs-utils ) diff --git a/src/cs-core/PluginBase.cpp b/src/cs-core/PluginBase.cpp index 44aa5f014..153e253f9 100644 --- a/src/cs-core/PluginBase.cpp +++ b/src/cs-core/PluginBase.cpp @@ -14,13 +14,15 @@ namespace cs::core { void PluginBase::setAPI(std::shared_ptr settings, std::shared_ptr solarSystem, std::shared_ptr guiManager, std::shared_ptr inputManager, VistaSceneGraph* sceneGraph, - std::shared_ptr graphicsEngine, std::shared_ptr timeControl) { + std::shared_ptr graphicsEngine, std::shared_ptr audioEngine, + std::shared_ptr timeControl) { mAllSettings = std::move(settings); mSolarSystem = std::move(solarSystem); mSceneGraph = sceneGraph; mGuiManager = std::move(guiManager); mGraphicsEngine = std::move(graphicsEngine); + mAudioEngine = std::move(audioEngine); mInputManager = std::move(inputManager); mTimeControl = std::move(timeControl); } diff --git a/src/cs-core/PluginBase.hpp b/src/cs-core/PluginBase.hpp index 493a36a2a..dd88b23f5 100644 --- a/src/cs-core/PluginBase.hpp +++ b/src/cs-core/PluginBase.hpp @@ -22,6 +22,7 @@ class VistaSceneGraph; namespace cs::core { class GraphicsEngine; +class AudioEngine; class GuiManager; class InputManager; class TimeControl; @@ -50,7 +51,7 @@ class CS_CORE_EXPORT PluginBase { void setAPI(std::shared_ptr settings, std::shared_ptr solarSystem, std::shared_ptr guiManager, std::shared_ptr inputManager, VistaSceneGraph* sceneGraph, std::shared_ptr graphicsEngine, - std::shared_ptr timeControl); + std::shared_ptr audioEngine, std::shared_ptr timeControl); /// Override this function to initialize your plugin. It will be called directly after /// application startup and before the update loop starts. @@ -70,6 +71,7 @@ class CS_CORE_EXPORT PluginBase { VistaSceneGraph* mSceneGraph{}; std::shared_ptr mGuiManager; std::shared_ptr mGraphicsEngine; + std::shared_ptr mAudioEngine; std::shared_ptr mInputManager; std::shared_ptr mTimeControl; }; diff --git a/src/cs-core/Settings.cpp b/src/cs-core/Settings.cpp index 5c16e1ccc..c880089d0 100644 --- a/src/cs-core/Settings.cpp +++ b/src/cs-core/Settings.cpp @@ -364,6 +364,31 @@ void to_json(nlohmann::json& j, Settings::Graphics const& o) { //////////////////////////////////////////////////////////////////////////////////////////////////// +void from_json(nlohmann::json const& j, Settings::Audio& o) { + Settings::deserialize(j, "enableHRTF", o.pEnableHRTF); + Settings::deserialize(j, "numberMonoSources", o.pNumberMonoSources); + Settings::deserialize(j, "numberStereoSources", o.pNumberStereoSources); + Settings::deserialize(j, "refreshRate", o.pRefreshRate); + Settings::deserialize(j, "contextSync", o.pContextSync); + Settings::deserialize(j, "mixerOutputFrequency", o.pMixerFrequency); + Settings::deserialize(j, "volumeCullingThreshold", o.pVolumeCullingThreshold); + Settings::deserialize(j, "distanceCullingThreshold", o.pDistanceCullingThreshold); + Settings::deserialize(j, "stationaryOutputDevice", o.pStationaryOutputDevice); +} + +void to_json(nlohmann::json& j, Settings::Audio const& o) { + Settings::serialize(j, "enableHRTF", o.pEnableHRTF); + Settings::serialize(j, "numberMonoSources", o.pNumberMonoSources); + Settings::serialize(j, "numberStereoSources", o.pNumberStereoSources); + Settings::serialize(j, "refreshRate", o.pRefreshRate); + Settings::serialize(j, "contextSync", o.pContextSync); + Settings::serialize(j, "volumeCullingThreshold", o.pVolumeCullingThreshold); + Settings::serialize(j, "distanceCullingThreshold", o.pDistanceCullingThreshold); + Settings::serialize(j, "stationaryOutputDevice", o.pStationaryOutputDevice); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + void from_json(nlohmann::json const& j, Settings& o) { Settings::deserialize(j, "startDate", o.mStartDate); Settings::deserialize(j, "resetDate", o.mResetDate); @@ -372,6 +397,7 @@ void from_json(nlohmann::json const& j, Settings& o) { Settings::deserialize(j, "sceneScale", o.mSceneScale); Settings::deserialize(j, "guiPosition", o.mGuiPosition); Settings::deserialize(j, "graphics", o.mGraphics); + Settings::deserialize(j, "audio", o.mAudio); Settings::deserialize(j, "enableUserInterface", o.pEnableUserInterface); Settings::deserialize(j, "enableMouseRay", o.pEnableMouseRay); Settings::deserialize(j, "enableSensorSizeControl", o.pEnableSensorSizeControl); diff --git a/src/cs-core/Settings.hpp b/src/cs-core/Settings.hpp index 39366d04e..8fa90b2e3 100644 --- a/src/cs-core/Settings.hpp +++ b/src/cs-core/Settings.hpp @@ -475,6 +475,40 @@ class CS_CORE_EXPORT Settings { // ----------------------------------------------------------------------------------------------- + struct Audio { + /// Enables or disables HRTF + utils::DefaultProperty pEnableHRTF{true}; + + /// Specifies the maximum number of mono sources + utils::DefaultProperty pNumberMonoSources{512}; + + /// Specifies the maximum number of stereo sources + utils::DefaultProperty pNumberStereoSources{5}; + + /// Specifies the Refresh rate of the context processing + utils::DefaultProperty pRefreshRate{30}; + + /// Specifies whether it should be a sychronous or asynchronous(default) context + utils::DefaultProperty pContextSync{false}; + + /// Frequency for mixing in the output buffer, measured in Hz + utils::DefaultProperty pMixerFrequency{48000}; + + /// Gain at which a source will stop playing if the volume culling PS is active + utils::DefaultProperty pVolumeCullingThreshold{0.01f}; + + /// *real* Distance at which a source will stop playing if the distance culling PS is active + utils::DefaultProperty pDistanceCullingThreshold{100.0}; + + /// Specifies wheter the audio output device is stationary or moves with the user (e.g. when + /// using a head mounted display with headphones) + utils::DefaultProperty pStationaryOutputDevice{true}; + }; + + Audio mAudio; + + // ----------------------------------------------------------------------------------------------- + /// A map with configuration options for each plugin. The JSON object is not parsed, this is done /// by the plugins themselves. std::map mPlugins; @@ -567,6 +601,8 @@ CS_CORE_EXPORT void from_json(nlohmann::json const& j, Settings::SceneScale& o); CS_CORE_EXPORT void to_json(nlohmann::json& j, Settings::SceneScale const& o); CS_CORE_EXPORT void from_json(nlohmann::json const& j, Settings::Graphics& o); CS_CORE_EXPORT void to_json(nlohmann::json& j, Settings::Graphics const& o); +CS_CORE_EXPORT void from_json(nlohmann::json const& j, Settings::Audio& o); +CS_CORE_EXPORT void to_json(nlohmann::json& j, Settings::Audio const& o); CS_CORE_EXPORT void from_json(nlohmann::json const& j, Settings& o); CS_CORE_EXPORT void to_json(nlohmann::json& j, Settings const& o);