From 523c48cf988e66ef43255dc1c9203b9e9f0a49e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:30:44 +0200 Subject: [PATCH] Add sfPlaybackDevice to manage the audio playback device --- .github/workflows/ci.yml | 4 + include/CSFML/Audio.h | 1 + include/CSFML/Audio/PlaybackDevice.h | 205 +++++++++++++++++++++++++++ src/CSFML/Audio/CMakeLists.txt | 2 + src/CSFML/Audio/PlaybackDevice.cpp | 135 ++++++++++++++++++ test/Audio/PlaybackDevice.test.cpp | 26 ++++ test/CMakeLists.txt | 1 + 7 files changed, 374 insertions(+) create mode 100644 include/CSFML/Audio/PlaybackDevice.h create mode 100644 src/CSFML/Audio/PlaybackDevice.cpp create mode 100644 test/Audio/PlaybackDevice.test.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03ffc97..f752e38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,9 +62,13 @@ jobs: BUILD_TYPE: ${{ matrix.type.name }} CMAKE_FLAGS: ${{ matrix.platform.flags }} ${{ matrix.config.flags }} run: | + # Let the installed SFML libraries find each other, same as tools/nuget/build.linux.sh + RPATH_FLAGS=() + if [ "$RUNNER_OS" = "Linux" ]; then RPATH_FLAGS=('-DCMAKE_INSTALL_RPATH=$ORIGIN'); fi cmake -S SFML -B SFML/build \ -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ -DCMAKE_INSTALL_PREFIX=SFML/install \ + "${RPATH_FLAGS[@]}" \ $CMAKE_FLAGS - name: Build SFML diff --git a/include/CSFML/Audio.h b/include/CSFML/Audio.h index 11a1771..b7f8a07 100644 --- a/include/CSFML/Audio.h +++ b/include/CSFML/Audio.h @@ -30,6 +30,7 @@ #include #include +#include #include #include #include diff --git a/include/CSFML/Audio/PlaybackDevice.h b/include/CSFML/Audio/PlaybackDevice.h new file mode 100644 index 0000000..58b4ce2 --- /dev/null +++ b/include/CSFML/Audio/PlaybackDevice.h @@ -0,0 +1,205 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include +#include + + +//////////////////////////////////////////////////////////// +/// \brief Enumeration of the playback device notifications +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfPlaybackDeviceStarted, ///< Playback device has been started + sfPlaybackDeviceStopped, ///< Playback device has been stopped + sfPlaybackDeviceRerouted, ///< Playback device has been rerouted (Generated on platforms that support automatic stream routing) + sfPlaybackDeviceInterruptionBegan, ///< Playback device interruption has begun (Generated on Apple mobile platforms) + sfPlaybackDeviceInterruptionEnded, ///< Playback device interruption has ended (Generated on Apple mobile platforms) + sfPlaybackDeviceUnlocked ///< Playback device has been unlocked (Generated by Emscripten/WebAudio) +} sfPlaybackDeviceNotification; + +//////////////////////////////////////////////////////////// +/// \brief Callback that is called to notify of changes to the playback device state +/// +/// \param notification The notification +/// \param userData User data passed to sfPlaybackDevice_setNotificationCallback +/// +//////////////////////////////////////////////////////////// +typedef void (*sfPlaybackDeviceNotificationCallback)(sfPlaybackDeviceNotification notification, void* userData); + + +//////////////////////////////////////////////////////////// +/// \brief Get a list of the names of all available audio playback devices +/// +/// If the operating system reports multiple devices with +/// the same name, a number will be appended to the name +/// of all subsequent devices to distinguish them from each +/// other. This guarantees that every entry returned by this +/// function will represent a unique device. +/// +/// The default device, if one is marked as such, will be +/// placed at the beginning of the list. +/// +/// The returned array and strings stay valid until the next +/// call to this function. +/// +/// \param count Pointer to a variable that will be filled with the number of devices +/// +/// \return An array of strings containing the device names or NULL if no devices are available +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API const char* const* sfPlaybackDevice_getAvailableDevices(size_t* count); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the default audio playback device +/// +/// Note that depending on when this function is called, the +/// default device reported by the operating system might +/// change e.g. when a USB audio device is plugged into or +/// unplugged from the system. +/// +/// The returned string stays valid until the next call to +/// this function. +/// +/// \return The name of the default audio playback device or NULL if there is none +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API const char* sfPlaybackDevice_getDefaultDevice(void); + +//////////////////////////////////////////////////////////// +/// \brief Set the audio playback device +/// +/// This function sets the audio playback device to the device +/// with the given name. It can be called on the fly (i.e: +/// while sounds are playing). +/// +/// If there are sounds playing when the audio playback +/// device is switched, the sounds will continue playing +/// uninterrupted on the new audio playback device. +/// +/// \param name The name of the audio playback device +/// +/// \return True, if it was able to set the requested device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_setDevice(const char* name); + +//////////////////////////////////////////////////////////// +/// \brief Set the audio playback device to the default +/// +/// This function sets the audio playback device to the +/// default device. It can be called on the fly (i.e: +/// while sounds are playing). +/// +/// When certain backends are used, using the default device +/// will enable automatic stream routing. When automatic +/// stream routing is enabled, audio data is automatically +/// sent to whichever physical audio device is currently +/// marked as the default on the system. +/// +/// Automatic stream routing is currently supported when using +/// the WASAPI or DirectSound backend on Windows or the +/// Core Audio backend on macOS and iOS. +/// +/// \return True, if it was able to set the audio playback device to the default device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_setDeviceToDefault(void); + +//////////////////////////////////////////////////////////// +/// \brief Set the audio playback device to the null device +/// +/// This function sets the audio playback device to the +/// null device. It can be called on the fly (i.e: +/// while sounds are playing). +/// +/// Audio data routed to the null device will be discarded +/// by the backend. This can be used to keep sounds playing +/// without having them actually output on a physical +/// audio playback device. +/// +/// \return True, if it was able to set the audio playback device to the null device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_setDeviceToNull(void); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the current audio playback device +/// +/// The returned string stays valid until the next call to +/// this function. +/// +/// \return The name of the current audio playback device or NULL if there is none +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API const char* sfPlaybackDevice_getDevice(void); + +//////////////////////////////////////////////////////////// +/// \brief Get the sample rate of the current audio playback device +/// +/// \return The sample rate of the current audio playback device or 0 if there is none +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API uint32_t sfPlaybackDevice_getDeviceSampleRate(void); + +//////////////////////////////////////////////////////////// +/// \brief Check if the current playback device is the default device +/// +/// This function will return false if there is no +/// current playback device. +/// +/// \return True, if the current playback device is the default device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_isDefaultDevice(void); + +//////////////////////////////////////////////////////////// +/// \brief Set a callback that should be called to notify of changes to the playback device state +/// +/// Warning: Do not attempt to alter the device state from +/// within this callback. This includes changing the device +/// and even creating/destroying sound objects since that +/// could indirectly cause the playback device to be +/// created/destroyed. Also do not attempt to set the +/// notification callback from within this callback. Doing +/// so will result in a deadlock. +/// +/// When receiving a notification via this callback, store +/// the information somewhere and react on it from another +/// thread e.g. the main thread within the application. +/// +/// \param callback The callback to be called, pass NULL to remove the current callback +/// \param userData User data that will be passed to the callback +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API void sfPlaybackDevice_setNotificationCallback(sfPlaybackDeviceNotificationCallback callback, void* userData); diff --git a/src/CSFML/Audio/CMakeLists.txt b/src/CSFML/Audio/CMakeLists.txt index eb7e439..d7a6cf1 100644 --- a/src/CSFML/Audio/CMakeLists.txt +++ b/src/CSFML/Audio/CMakeLists.txt @@ -11,6 +11,8 @@ set(SRC ${SRCROOT}/Music.cpp ${SRCROOT}/MusicStruct.hpp ${INCROOT}/Music.h + ${SRCROOT}/PlaybackDevice.cpp + ${INCROOT}/PlaybackDevice.h ${SRCROOT}/Sound.cpp ${SRCROOT}/SoundStruct.hpp ${INCROOT}/Sound.h diff --git a/src/CSFML/Audio/PlaybackDevice.cpp b/src/CSFML/Audio/PlaybackDevice.cpp new file mode 100644 index 0000000..307218c --- /dev/null +++ b/src/CSFML/Audio/PlaybackDevice.cpp @@ -0,0 +1,135 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include + +#include +#include +#include +#include + + +namespace +{ +// Helper function for returning optional strings +[[nodiscard]] const char* toCString(std::string& storage, const std::optional& string) +{ + if (!string) + return nullptr; + + storage = *string; + return storage.c_str(); +} +} // namespace + + +//////////////////////////////////////////////////////////// +const char* const* sfPlaybackDevice_getAvailableDevices(size_t* count) +{ + static std::vector stringDevices; + static std::vector devices; + + stringDevices = sf::PlaybackDevice::getAvailableDevices(); + devices.clear(); + devices.reserve(stringDevices.size()); + for (const auto& stringDevice : stringDevices) + devices.push_back(stringDevice.c_str()); + + if (count) + *count = devices.size(); + + return !devices.empty() ? devices.data() : nullptr; +} + + +//////////////////////////////////////////////////////////// +const char* sfPlaybackDevice_getDefaultDevice() +{ + static std::string defaultDevice; + return toCString(defaultDevice, sf::PlaybackDevice::getDefaultDevice()); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_setDevice(const char* name) +{ + assert(name); + return sf::PlaybackDevice::setDevice(name); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_setDeviceToDefault() +{ + return sf::PlaybackDevice::setDeviceToDefault(); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_setDeviceToNull() +{ + return sf::PlaybackDevice::setDeviceToNull(); +} + + +//////////////////////////////////////////////////////////// +const char* sfPlaybackDevice_getDevice() +{ + static std::string device; + return toCString(device, sf::PlaybackDevice::getDevice()); +} + + +//////////////////////////////////////////////////////////// +uint32_t sfPlaybackDevice_getDeviceSampleRate() +{ + return sf::PlaybackDevice::getDeviceSampleRate().value_or(0); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_isDefaultDevice() +{ + return sf::PlaybackDevice::isDefaultDevice(); +} + + +//////////////////////////////////////////////////////////// +void sfPlaybackDevice_setNotificationCallback(sfPlaybackDeviceNotificationCallback callback, void* userData) +{ + if (!callback) + { + sf::PlaybackDevice::setNotificationCallback({}); + return; + } + + sf::PlaybackDevice::setNotificationCallback( + [callback, userData](sf::PlaybackDevice::Notification notification) + { callback(static_cast(notification), userData); }); +} diff --git a/test/Audio/PlaybackDevice.test.cpp b/test/Audio/PlaybackDevice.test.cpp new file mode 100644 index 0000000..15350c1 --- /dev/null +++ b/test/Audio/PlaybackDevice.test.cpp @@ -0,0 +1,26 @@ +#include + +#include + +#include + +TEST_CASE("[Audio] sfPlaybackDevice") +{ + SECTION("sfPlaybackDeviceNotification") + { + STATIC_CHECK(sfPlaybackDeviceStarted == static_cast(sf::PlaybackDevice::Notification::DeviceStarted)); + STATIC_CHECK(sfPlaybackDeviceStopped == static_cast(sf::PlaybackDevice::Notification::DeviceStopped)); + STATIC_CHECK(sfPlaybackDeviceRerouted == static_cast(sf::PlaybackDevice::Notification::DeviceRerouted)); + STATIC_CHECK(sfPlaybackDeviceInterruptionBegan == + static_cast(sf::PlaybackDevice::Notification::DeviceInterruptionBegan)); + STATIC_CHECK(sfPlaybackDeviceInterruptionEnded == + static_cast(sf::PlaybackDevice::Notification::DeviceInterruptionEnded)); + STATIC_CHECK(sfPlaybackDeviceUnlocked == static_cast(sf::PlaybackDevice::Notification::DeviceUnlocked)); + } + + SECTION("sfPlaybackDevice_setNotificationCallback") + { + sfPlaybackDevice_setNotificationCallback([](sfPlaybackDeviceNotification, void*) {}, nullptr); + sfPlaybackDevice_setNotificationCallback(nullptr, nullptr); + } +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e613c17..332962e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -76,6 +76,7 @@ set_target_warnings(test-csfml-network) catch_discover_tests(test-csfml-network) add_executable(test-csfml-audio + Audio/PlaybackDevice.test.cpp Audio/SoundChannel.test.cpp ) target_link_libraries(test-csfml-audio PRIVATE csfml-audio Catch2::Catch2WithMain SFML::Audio)