diff --git a/examples/sound_effects/SoundEffects.cpp b/examples/sound_effects/SoundEffects.cpp index f0429f5fe..7c4f9635c 100644 --- a/examples/sound_effects/SoundEffects.cpp +++ b/examples/sound_effects/SoundEffects.cpp @@ -1089,14 +1089,25 @@ int main() // Create the description text sf::Text description(font, "Current effect: " + effects[current]->getName(), 20); - description.setPosition({10.f, 530.f}); + description.setPosition({10.f, 522.f}); description.setFillColor(sf::Color(80, 80, 80)); // Create the instructions text sf::Text instructions(font, "Press left and right arrows to change the current effect", 20); - instructions.setPosition({280.f, 555.f}); + instructions.setPosition({280.f, 544.f}); instructions.setFillColor(sf::Color(80, 80, 80)); + // Create the playback device text + auto playbackDeviceName = sf::PlaybackDevice::getDevice(); + sf::Text playbackDevice(font, "Current playback device: " + playbackDeviceName.value_or("None"), 20); + playbackDevice.setPosition({10.f, 566.f}); + playbackDevice.setFillColor(sf::Color(80, 80, 80)); + + // Create the playback device instructions text + sf::Text playbackDeviceInstructions(font, "Press F1 to change device", 20); + playbackDeviceInstructions.setPosition({565.f, 566.f}); + playbackDeviceInstructions.setFillColor(sf::Color(80, 80, 80)); + // Start the game loop const sf::Clock clock; while (window.isOpen()) @@ -1139,6 +1150,37 @@ int main() description.setString("Current effect: " + effects[current]->getName()); break; + // F1 key: change playback device + case sf::Keyboard::Key::F1: + { + // We need to query the list every time we want to change + // since new devices could have been added in the mean time + const auto devices = sf::PlaybackDevice::getAvailableDevices(); + const auto currentDevice = sf::PlaybackDevice::getDevice(); + auto next = currentDevice; + + for (auto iter = devices.begin(); iter != devices.end(); ++iter) + { + if (*iter == currentDevice) + { + const auto nextIter = std::next(iter); + next = (nextIter == devices.end()) ? devices.front() : *nextIter; + break; + } + } + + if (next) + { + if (!sf::PlaybackDevice::setDevice(*next)) + std::cerr << "Failed to set the playback device to: " << *next << std::endl; + + playbackDeviceName = sf::PlaybackDevice::getDevice(); + playbackDevice.setString("Current playback device: " + playbackDeviceName.value_or("None")); + } + + break; + } + default: effects[current]->handleKey(keyPressed->code); break; @@ -1160,6 +1202,8 @@ int main() window.draw(textBackground); window.draw(instructions); window.draw(description); + window.draw(playbackDevice); + window.draw(playbackDeviceInstructions); // Finally, display the rendered frame on screen window.display(); diff --git a/include/SFML/Audio.hpp b/include/SFML/Audio.hpp index 98ea110d4..f0d262ea7 100644 --- a/include/SFML/Audio.hpp +++ b/include/SFML/Audio.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include diff --git a/include/SFML/Audio/PlaybackDevice.hpp b/include/SFML/Audio/PlaybackDevice.hpp new file mode 100644 index 000000000..48bb00ba7 --- /dev/null +++ b/include/SFML/Audio/PlaybackDevice.hpp @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2024 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 + + +namespace sf::PlaybackDevice +{ +//////////////////////////////////////////////////////////// +/// \brief Get a list of the names of all available audio playback devices +/// +/// This function returns a vector of strings containing +/// 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. +/// +/// For example, if the operating system reports multiple +/// devices with the name "Sound Card", the entries returned +/// would be: +/// - Sound Card +/// - Sound Card 2 +/// - Sound Card 3 +/// - ... +/// +/// The default device, if one is marked as such, will be +/// placed at the beginning of the vector. +/// +/// If no devices are available, this function will return +/// an empty vector. +/// +/// \return A vector of strings containing the device names or an empty vector if no devices are available +/// +//////////////////////////////////////////////////////////// +[[nodiscard]] SFML_AUDIO_API std::vector getAvailableDevices(); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the default audio playback device +/// +/// This function returns the name of the default audio +/// playback device. If none is available, an empty string +/// is returned. +/// +/// \return The name of the default audio playback device +/// +//////////////////////////////////////////////////////////// +[[nodiscard]] SFML_AUDIO_API std::optional getDefaultDevice(); + +//////////////////////////////////////////////////////////// +/// \brief Set the audio playback device +/// +/// This function sets the audio playback device to the device +/// with the given \a 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 +/// +/// \see getAvailableDevices, getDefaultDevice +/// +//////////////////////////////////////////////////////////// +[[nodiscard]] SFML_AUDIO_API bool setDevice(const std::string& name); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the current audio playback device +/// +/// \return The name of the current audio playback device or std::nullopt if there is none +/// +//////////////////////////////////////////////////////////// +[[nodiscard]] SFML_AUDIO_API std::optional getDevice(); + +} // namespace sf::PlaybackDevice diff --git a/src/SFML/Audio/AudioDevice.cpp b/src/SFML/Audio/AudioDevice.cpp index dc8e45494..7657d55e0 100644 --- a/src/SFML/Audio/AudioDevice.cpp +++ b/src/SFML/Audio/AudioDevice.cpp @@ -26,16 +26,36 @@ // Headers //////////////////////////////////////////////////////////// #include +#include #include #include #include #include +#include + +#include namespace sf::priv { +namespace +{ +// Instead of a variable in an anonymous namespace, +// we use a function that returns a reference to a static +// variable to delay initialization of the variable as long +// as possible, i.e. until it is requested by someone. +// This also avoids static initialization order races in the +// event some other static object gets/sets the current device. +std::optional& getCurrentDevice() +{ + static std::optional currentDevice; + return currentDevice; +} +} // namespace + + //////////////////////////////////////////////////////////// AudioDevice::AudioDevice() { @@ -115,72 +135,8 @@ AudioDevice::AudioDevice() if (m_context->backend == ma_backend_null) err() << "Using NULL audio backend for playback" << std::endl; - // Create the playback device - m_playbackDevice.emplace(); - - auto playbackDeviceConfig = ma_device_config_init(ma_device_type_playback); - playbackDeviceConfig.dataCallback = [](ma_device* device, void* output, const void*, ma_uint32 frameCount) - { - auto& audioDevice = *static_cast(device->pUserData); - - if (audioDevice.m_engine) - { - if (const auto result = ma_engine_read_pcm_frames(&*audioDevice.m_engine, output, frameCount, nullptr); - result != MA_SUCCESS) - err() << "Failed to read PCM frames from audio engine: " << ma_result_description(result) << std::endl; - } - }; - playbackDeviceConfig.pUserData = this; - playbackDeviceConfig.playback.format = ma_format_f32; - - if (const auto result = ma_device_init(&*m_context, &playbackDeviceConfig, &*m_playbackDevice); result != MA_SUCCESS) - { - m_playbackDevice.reset(); - err() << "Failed to initialize the audio playback device: " << ma_result_description(result) << std::endl; - return; - } - - // Create the engine - auto engineConfig = ma_engine_config_init(); - engineConfig.pContext = &*m_context; - engineConfig.pDevice = &*m_playbackDevice; - engineConfig.listenerCount = 1; - - m_engine.emplace(); - - if (const auto result = ma_engine_init(&engineConfig, &*m_engine); result != MA_SUCCESS) - { - m_engine.reset(); - err() << "Failed to initialize the audio engine: " << ma_result_description(result) << std::endl; - return; - } - - // Set master volume, position, velocity, cone and world up vector - if (const auto result = ma_device_set_master_volume(ma_engine_get_device(&*m_engine), - getListenerProperties().volume * 0.01f); - result != MA_SUCCESS) - err() << "Failed to set audio device master volume: " << ma_result_description(result) << std::endl; - - ma_engine_listener_set_position(&*m_engine, - 0, - getListenerProperties().position.x, - getListenerProperties().position.y, - getListenerProperties().position.z); - ma_engine_listener_set_velocity(&*m_engine, - 0, - getListenerProperties().velocity.x, - getListenerProperties().velocity.y, - getListenerProperties().velocity.z); - ma_engine_listener_set_cone(&*m_engine, - 0, - getListenerProperties().cone.innerAngle.asRadians(), - getListenerProperties().cone.outerAngle.asRadians(), - getListenerProperties().cone.outerGain); - ma_engine_listener_set_world_up(&*m_engine, - 0, - getListenerProperties().upVector.x, - getListenerProperties().upVector.y, - getListenerProperties().upVector.z); + if (!initialize()) + err() << "Failed to initialize audio device or engine" << std::endl; } @@ -221,6 +177,143 @@ ma_engine* AudioDevice::getEngine() } +//////////////////////////////////////////////////////////// +bool AudioDevice::reinitialize() +{ + auto* instance = getInstance(); + + // We don't have to do anything if an instance doesn't exist yet + if (!instance) + return true; + + const std::lock_guard lock(instance->m_resourcesMutex); + + // Deinitialize all audio resources + for (const auto& entry : instance->m_resources) + entry.deinitializeFunc(entry.resource); + + // Destroy the old engine + if (instance->m_engine) + ma_engine_uninit(&*instance->m_engine); + + // Destroy the old playback device + if (instance->m_playbackDevice) + ma_device_uninit(&*instance->m_playbackDevice); + + // Create the new objects + const auto result = instance->initialize(); + + // Reinitialize all audio resources + for (const auto& entry : instance->m_resources) + entry.reinitializeFunc(entry.resource); + + return result; +} + + +//////////////////////////////////////////////////////////// +std::vector AudioDevice::getAvailableDevices() +{ + const auto getDevices = [](auto& context) + { + ma_device_info* deviceInfos{}; + ma_uint32 deviceCount{}; + + // Get the playback devices + if (const auto result = ma_context_get_devices(&context, &deviceInfos, &deviceCount, nullptr, nullptr); + result != MA_SUCCESS) + { + err() << "Failed to get audio playback devices: " << ma_result_description(result) << std::endl; + return std::vector{}; + } + + std::vector deviceList; + deviceList.reserve(deviceCount); + + // In order to report devices with identical names and still allow + // the user to differentiate between them when selecting, we append + // an index (number) to their name starting from the second entry + std::unordered_map deviceIndices; + deviceIndices.reserve(deviceCount); + + for (auto i = 0u; i < deviceCount; ++i) + { + auto name = std::string(deviceInfos[i].name); + auto& index = deviceIndices[name]; + + ++index; + + if (index > 1) + name += ' ' + std::to_string(index); + + // Make sure the default device is always placed at the front + deviceList.emplace(deviceInfos[i].isDefault ? deviceList.begin() : deviceList.end(), + DeviceEntry{name, deviceInfos[i].id, deviceInfos[i].isDefault == MA_TRUE}); + } + + return deviceList; + }; + + // Use an existing instance's context if one exists + auto* instance = getInstance(); + + if (instance && instance->m_context) + return getDevices(*instance->m_context); + + // Otherwise, construct a temporary context + ma_context context{}; + + if (const auto result = ma_context_init(nullptr, 0, nullptr, &context); result != MA_SUCCESS) + { + err() << "Failed to initialize the audio playback context: " << ma_result_description(result) << std::endl; + return {}; + } + + auto deviceList = getDevices(context); + ma_context_uninit(&context); + return deviceList; +} + + +//////////////////////////////////////////////////////////// +bool AudioDevice::setDevice(const std::string& name) +{ + getCurrentDevice() = name; + return reinitialize(); +} + + +//////////////////////////////////////////////////////////// +std::optional AudioDevice::getDevice() +{ + return getCurrentDevice(); +} + + +//////////////////////////////////////////////////////////// +AudioDevice::ResourceEntryIter AudioDevice::registerResource(void* resource, + ResourceEntry::Func deinitializeFunc, + ResourceEntry::Func reinitializeFunc) +{ + // There should always be an AudioDevice instance when registerResource is called + auto* instance = getInstance(); + assert(instance && "AudioDevice instance should exist when calling AudioDevice::registerResource"); + const std::lock_guard lock(instance->m_resourcesMutex); + return instance->m_resources.insert(instance->m_resources.end(), {resource, deinitializeFunc, reinitializeFunc}); +} + + +//////////////////////////////////////////////////////////// +void AudioDevice::unregisterResource(AudioDevice::ResourceEntryIter resourceEntry) +{ + // There should always be an AudioDevice instance when unregisterResource is called + auto* instance = getInstance(); + assert(instance && "AudioDevice instance should exist when calling AudioDevice::unregisterResource"); + const std::lock_guard lock(instance->m_resourcesMutex); + instance->m_resources.erase(resourceEntry); +} + + //////////////////////////////////////////////////////////// void AudioDevice::setGlobalVolume(float volume) { @@ -359,6 +452,126 @@ Vector3f AudioDevice::getUpVector() } +//////////////////////////////////////////////////////////// +std::optional AudioDevice::getSelectedDeviceId() const +{ + const auto devices = getAvailableDevices(); + auto deviceName = getDevice(); + + // If no device has been selected by the user yet, use the default device + if (!deviceName) + deviceName = PlaybackDevice::getDefaultDevice(); + + auto iter = std::find_if(devices.begin(), + devices.end(), + [&](const auto& device) { return device.name == deviceName; }); + + if (iter != devices.end()) + return iter->id; + + return std::nullopt; +} + + +//////////////////////////////////////////////////////////// +bool AudioDevice::initialize() +{ + const auto deviceId = getSelectedDeviceId(); + + // Create the playback device + m_playbackDevice.emplace(); + + auto playbackDeviceConfig = ma_device_config_init(ma_device_type_playback); + playbackDeviceConfig.dataCallback = [](ma_device* device, void* output, const void*, ma_uint32 frameCount) + { + auto& audioDevice = *static_cast(device->pUserData); + + if (audioDevice.m_engine) + { + if (const auto result = ma_engine_read_pcm_frames(&*audioDevice.m_engine, output, frameCount, nullptr); + result != MA_SUCCESS) + err() << "Failed to read PCM frames from audio engine: " << ma_result_description(result) << std::endl; + } + }; + playbackDeviceConfig.pUserData = this; + playbackDeviceConfig.playback.format = ma_format_f32; + playbackDeviceConfig.playback.pDeviceID = deviceId ? &*deviceId : nullptr; + + if (const auto result = ma_device_init(&*m_context, &playbackDeviceConfig, &*m_playbackDevice); result != MA_SUCCESS) + { + m_playbackDevice.reset(); + getCurrentDevice() = std::nullopt; + err() << "Failed to initialize the audio playback device: " << ma_result_description(result) << std::endl; + return false; + } + + // Update the current device string from the the device we just initialized + { + std::array deviceName{}; + size_t deviceNameLength{}; + + if (const auto result = ma_device_get_name(&*m_playbackDevice, + ma_device_type_playback, + deviceName.data(), + deviceName.size(), + &deviceNameLength); + result != MA_SUCCESS) + { + err() << "Failed to get name of audio playback device: " << ma_result_description(result) << std::endl; + getCurrentDevice() = std::nullopt; + } + else + { + getCurrentDevice() = std::string(deviceName.data(), deviceNameLength); + } + } + + // Create the engine + auto engineConfig = ma_engine_config_init(); + engineConfig.pContext = &*m_context; + engineConfig.pDevice = &*m_playbackDevice; + engineConfig.listenerCount = 1; + + m_engine.emplace(); + + if (const auto result = ma_engine_init(&engineConfig, &*m_engine); result != MA_SUCCESS) + { + m_engine.reset(); + err() << "Failed to initialize the audio engine: " << ma_result_description(result) << std::endl; + return false; + } + + // Set master volume, position, velocity, cone and world up vector + if (const auto result = ma_device_set_master_volume(ma_engine_get_device(&*m_engine), + getListenerProperties().volume * 0.01f); + result != MA_SUCCESS) + err() << "Failed to set audio device master volume: " << ma_result_description(result) << std::endl; + + ma_engine_listener_set_position(&*m_engine, + 0, + getListenerProperties().position.x, + getListenerProperties().position.y, + getListenerProperties().position.z); + ma_engine_listener_set_velocity(&*m_engine, + 0, + getListenerProperties().velocity.x, + getListenerProperties().velocity.y, + getListenerProperties().velocity.z); + ma_engine_listener_set_cone(&*m_engine, + 0, + getListenerProperties().cone.innerAngle.asRadians(), + getListenerProperties().cone.outerAngle.asRadians(), + getListenerProperties().cone.outerGain); + ma_engine_listener_set_world_up(&*m_engine, + 0, + getListenerProperties().upVector.x, + getListenerProperties().upVector.y, + getListenerProperties().upVector.z); + + return true; +} + + //////////////////////////////////////////////////////////// AudioDevice*& AudioDevice::getInstance() { diff --git a/src/SFML/Audio/AudioDevice.hpp b/src/SFML/Audio/AudioDevice.hpp index c2396ec62..d85b97845 100644 --- a/src/SFML/Audio/AudioDevice.hpp +++ b/src/SFML/Audio/AudioDevice.hpp @@ -33,7 +33,11 @@ #include +#include +#include #include +#include +#include namespace sf::priv @@ -71,6 +75,110 @@ public: //////////////////////////////////////////////////////////// static ma_engine* getEngine(); + //////////////////////////////////////////////////////////// + /// \brief Reinitialize the audio engine and device + /// + /// Calling this function will reinitialize the audio engine + /// and device using the currently selected device name as + /// returned by sf::PlaybackDevice::getDevice. + /// + /// \return True if reinitialization was successful, false otherwise + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] static bool reinitialize(); + + struct DeviceEntry + { + std::string name; + ma_device_id id{}; + bool isDefault{}; + }; + + //////////////////////////////////////////////////////////// + /// \brief Get a list of all available audio playback devices + /// + /// This function returns a vector of device entries, + /// containing the names and IDs of all available audio + /// playback devices. Additionally, if applicable, one entry + /// will be marked as the default device as reported by the + /// operating system. + /// + /// \return A vector of device entries containing the names and IDs of all available audio playback devices + /// + //////////////////////////////////////////////////////////// + static std::vector getAvailableDevices(); + + //////////////////////////////////////////////////////////// + /// \brief Set the audio playback device + /// + /// This function sets the audio playback device to the device + /// with the given \a 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 + /// + /// \see getAvailableDevices, getDefaultDevice + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] static bool setDevice(const std::string& name); + + //////////////////////////////////////////////////////////// + /// \brief Get the name of the current audio playback device + /// + /// \return The name of the current audio playback device or `std::nullopt` if there is none + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] static std::optional getDevice(); + + struct ResourceEntry + { + using Func = void (*)(void*); + void* resource{}; + Func deinitializeFunc{}; + Func reinitializeFunc{}; + }; + + using ResourceEntryList = std::list; + using ResourceEntryIter = ResourceEntryList::const_iterator; + + //////////////////////////////////////////////////////////// + /// \brief Register an audio resource + /// + /// In order to support switching audio devices during + /// runtime, all audio resources will have to be + /// deinitialized using the old engine and device and then + /// reinitialized using the new engine and device. In order + /// for the AudioDevice to know which resources have to be + /// notified, they need to register themselves with the + /// AudioDevice using this function + /// + /// \param resource A pointer uniquely identifying the object + /// \param deinitializeFunc The function to call to deinitialize the object + /// \param reinitializeFunc The function to call to reinitialize the object + /// + /// \see unregisterResource + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] static ResourceEntryIter registerResource(void* resource, + ResourceEntry::Func deinitializeFunc, + ResourceEntry::Func reinitializeFunc); + + //////////////////////////////////////////////////////////// + /// \brief Unregister an audio resource + /// + /// \param resourceEntry The iterator returned when registering the resource + /// + /// \see registerResource + /// + //////////////////////////////////////////////////////////// + static void unregisterResource(ResourceEntryIter resourceEntry); + //////////////////////////////////////////////////////////// /// \brief Change the global volume of all the sounds and musics /// @@ -217,6 +325,22 @@ public: static Vector3f getUpVector(); private: + //////////////////////////////////////////////////////////// + /// \brief Get the device ID of the currently selected device + /// + /// \return The device ID of the currently selected device or `std::nullopt` if none could be found + /// + //////////////////////////////////////////////////////////// + std::optional getSelectedDeviceId() const; + + //////////////////////////////////////////////////////////// + /// \brief Initialize the audio device and engine + /// + /// \return True if initialization was successful, false if it failed + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] bool initialize(); + //////////////////////////////////////////////////////////// /// \brief This function makes sure the instance pointer is initialized before using it /// @@ -250,6 +374,8 @@ private: std::optional m_context; //!< The miniaudio context std::optional m_playbackDevice; //!< The miniaudio playback device std::optional m_engine; //!< The miniaudio engine (used for effects and spatialisation) + ResourceEntryList m_resources; //!< Registered resources + std::mutex m_resourcesMutex; //!< The mutex guarding the registered resources }; } // namespace sf::priv diff --git a/src/SFML/Audio/CMakeLists.txt b/src/SFML/Audio/CMakeLists.txt index a4bd1ed36..5112aa2e8 100644 --- a/src/SFML/Audio/CMakeLists.txt +++ b/src/SFML/Audio/CMakeLists.txt @@ -15,6 +15,8 @@ set(SRC ${SRCROOT}/MiniaudioUtils.cpp ${SRCROOT}/Music.cpp ${INCROOT}/Music.hpp + ${SRCROOT}/PlaybackDevice.cpp + ${INCROOT}/PlaybackDevice.hpp ${SRCROOT}/Sound.cpp ${INCROOT}/Sound.hpp ${SRCROOT}/SoundBuffer.cpp diff --git a/src/SFML/Audio/MiniaudioUtils.cpp b/src/SFML/Audio/MiniaudioUtils.cpp index 44cd82e8a..c9c79a808 100644 --- a/src/SFML/Audio/MiniaudioUtils.cpp +++ b/src/SFML/Audio/MiniaudioUtils.cpp @@ -25,81 +25,57 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include -#include #include #include #include -#include -#include #include #include +#include -namespace sf::priv -{ namespace { //////////////////////////////////////////////////////////// -struct SavedSettings -{ - float pitch{1.f}; - float pan{0.f}; - float volume{1.f}; - ma_bool32 spatializationEnabled{MA_TRUE}; - ma_vec3f position{0.f, 0.f, 0.f}; - ma_vec3f direction{0.f, 0.f, -1.f}; - float directionalAttenuationFactor{1.f}; - ma_vec3f velocity{0.f, 0.f, 0.f}; - float dopplerFactor{1.f}; - ma_positioning positioning{ma_positioning_absolute}; - float minDistance{1.f}; - float maxDistance{std::numeric_limits::max()}; - float minGain{0.f}; - float maxGain{1.f}; - float rollOff{1.f}; - float innerAngle{degrees(360.f).asRadians()}; - float outerAngle{degrees(360.f).asRadians()}; - float outerGain{0.f}; -}; - - -//////////////////////////////////////////////////////////// -SavedSettings saveSettings(const ma_sound& sound) +sf::priv::MiniaudioUtils::SavedSettings saveSettings(const ma_sound& sound) { float innerAngle = 0; float outerAngle = 0; float outerGain = 0; ma_sound_get_cone(&sound, &innerAngle, &outerAngle, &outerGain); - return SavedSettings{ma_sound_get_pitch(&sound), - ma_sound_get_pan(&sound), - ma_sound_get_volume(&sound), - ma_sound_is_spatialization_enabled(&sound), - ma_sound_get_position(&sound), - ma_sound_get_direction(&sound), - ma_sound_get_directional_attenuation_factor(&sound), - ma_sound_get_velocity(&sound), - ma_sound_get_doppler_factor(&sound), - ma_sound_get_positioning(&sound), - ma_sound_get_min_distance(&sound), - ma_sound_get_max_distance(&sound), - ma_sound_get_min_gain(&sound), - ma_sound_get_max_gain(&sound), - ma_sound_get_rolloff(&sound), - innerAngle, - outerAngle, - outerGain}; + return sf::priv::MiniaudioUtils:: + SavedSettings{ma_sound_get_pitch(&sound), + ma_sound_get_pan(&sound), + ma_sound_get_volume(&sound), + ma_sound_is_spatialization_enabled(&sound), + ma_sound_get_position(&sound), + ma_sound_get_direction(&sound), + ma_sound_get_directional_attenuation_factor(&sound), + ma_sound_get_velocity(&sound), + ma_sound_get_doppler_factor(&sound), + ma_sound_get_positioning(&sound), + ma_sound_get_min_distance(&sound), + ma_sound_get_max_distance(&sound), + ma_sound_get_min_gain(&sound), + ma_sound_get_max_gain(&sound), + ma_sound_get_rolloff(&sound), + ma_sound_is_playing(&sound), + ma_sound_is_looping(&sound), + innerAngle, + outerAngle, + outerGain}; } //////////////////////////////////////////////////////////// -void applySettings(ma_sound& sound, const SavedSettings& savedSettings) +void applySettings(ma_sound& sound, const sf::priv::MiniaudioUtils::SavedSettings& savedSettings) { ma_sound_set_pitch(&sound, savedSettings.pitch); ma_sound_set_pan(&sound, savedSettings.pan); @@ -116,22 +92,188 @@ void applySettings(ma_sound& sound, const SavedSettings& savedSettings) ma_sound_set_min_gain(&sound, savedSettings.minGain); ma_sound_set_max_gain(&sound, savedSettings.maxGain); ma_sound_set_rolloff(&sound, savedSettings.rollOff); + ma_sound_set_looping(&sound, savedSettings.looping); ma_sound_set_cone(&sound, savedSettings.innerAngle, savedSettings.outerAngle, savedSettings.outerGain); + + if (savedSettings.playing) + { + ma_sound_start(&sound); + } + else + { + ma_sound_stop(&sound); + } +} +} // namespace + + +namespace sf::priv +{ +//////////////////////////////////////////////////////////// +MiniaudioUtils::SoundBase::SoundBase(const ma_data_source_vtable& dataSourceVTable, + AudioDevice::ResourceEntry::Func reinitializeFunc) +{ + // Set this object up as a miniaudio data source + ma_data_source_config config = ma_data_source_config_init(); + config.vtable = &dataSourceVTable; + + if (const ma_result result = ma_data_source_init(&config, &dataSourceBase); result != MA_SUCCESS) + err() << "Failed to initialize audio data source: " << ma_result_description(result) << std::endl; + + resourceEntryIter = priv::AudioDevice::registerResource( + this, + [](void* ptr) { static_cast(ptr)->deinitialize(); }, + reinitializeFunc); } //////////////////////////////////////////////////////////// -void initializeDataSource(ma_data_source_base& dataSourceBase, const ma_data_source_vtable& vtable) +MiniaudioUtils::SoundBase::~SoundBase() { - // Set this object up as a miniaudio data source - ma_data_source_config config = ma_data_source_config_init(); - config.vtable = &vtable; - - if (const ma_result result = ma_data_source_init(&config, &dataSourceBase); result != MA_SUCCESS) - err() << "Failed to initialize audio data source: " << ma_result_description(result) << std::endl; + priv::AudioDevice::unregisterResource(resourceEntryIter); + ma_sound_uninit(&sound); + ma_node_uninit(&effectNode, nullptr); + ma_data_source_uninit(&dataSourceBase); +} + + +//////////////////////////////////////////////////////////// +void MiniaudioUtils::SoundBase::initialize(ma_sound_end_proc endCallback) +{ + // Initialize the sound + auto* engine = priv::AudioDevice::getEngine(); + + if (engine == nullptr) + { + err() << "Failed to initialize sound: No engine available" << std::endl; + return; + } + + ma_sound_config soundConfig; + + soundConfig = ma_sound_config_init(); + soundConfig.pDataSource = this; + soundConfig.pEndCallbackUserData = this; + soundConfig.endCallback = endCallback; + + if (const ma_result result = ma_sound_init_ex(engine, &soundConfig, &sound); result != MA_SUCCESS) + { + err() << "Failed to initialize sound: " << ma_result_description(result) << std::endl; + return; + } + + // Initialize the custom effect node + effectNodeVTable.onProcess = + [](ma_node* node, const float** framesIn, ma_uint32* frameCountIn, float** framesOut, ma_uint32* frameCountOut) + { static_cast(node)->impl->processEffect(framesIn, *frameCountIn, framesOut, *frameCountOut); }; + effectNodeVTable.onGetRequiredInputFrameCount = nullptr; + effectNodeVTable.inputBusCount = 1; + effectNodeVTable.outputBusCount = 1; + effectNodeVTable.flags = MA_NODE_FLAG_CONTINUOUS_PROCESSING | MA_NODE_FLAG_ALLOW_NULL_INPUT; + + const auto nodeChannelCount = ma_engine_get_channels(engine); + ma_node_config nodeConfig = ma_node_config_init(); + nodeConfig.vtable = &effectNodeVTable; + nodeConfig.pInputChannels = &nodeChannelCount; + nodeConfig.pOutputChannels = &nodeChannelCount; + + if (const ma_result result = ma_node_init(ma_engine_get_node_graph(engine), &nodeConfig, nullptr, &effectNode); + result != MA_SUCCESS) + { + err() << "Failed to initialize effect node: " << ma_result_description(result) << std::endl; + return; + } + + effectNode.impl = this; + effectNode.channelCount = nodeChannelCount; + + // Route the sound through the effect node depending on whether an effect processor is set + connectEffect(bool{effectProcessor}); + + applySettings(sound, savedSettings); +} + + +//////////////////////////////////////////////////////////// +void MiniaudioUtils::SoundBase::deinitialize() +{ + savedSettings = saveSettings(sound); + ma_sound_uninit(&sound); + ma_node_uninit(&effectNode, nullptr); +} + + +//////////////////////////////////////////////////////////// +void MiniaudioUtils::SoundBase::processEffect(const float** framesIn, + ma_uint32& frameCountIn, + float** framesOut, + ma_uint32& frameCountOut) const +{ + // If a processor is set, call it + if (effectProcessor) + { + if (!framesIn) + frameCountIn = 0; + + effectProcessor(framesIn ? framesIn[0] : nullptr, frameCountIn, framesOut[0], frameCountOut, effectNode.channelCount); + return; + } + + // Otherwise just pass the data through 1:1 + if (framesIn == nullptr) + { + frameCountIn = 0; + frameCountOut = 0; + return; + } + + const auto toProcess = std::min(frameCountIn, frameCountOut); + std::memcpy(framesOut[0], framesIn[0], toProcess * effectNode.channelCount * sizeof(float)); + frameCountIn = toProcess; + frameCountOut = toProcess; +} + + +//////////////////////////////////////////////////////////// +void MiniaudioUtils::SoundBase::connectEffect(bool connect) +{ + auto* engine = AudioDevice::getEngine(); + + if (engine == nullptr) + { + err() << "Failed to connect effect: No engine available" << std::endl; + return; + } + + if (connect) + { + // Attach the custom effect node output to our engine endpoint + if (const ma_result result = ma_node_attach_output_bus(&effectNode, 0, ma_engine_get_endpoint(engine), 0); + result != MA_SUCCESS) + { + err() << "Failed to attach effect node output to endpoint: " << ma_result_description(result) << std::endl; + return; + } + } + else + { + // Detach the custom effect node output from our engine endpoint + if (const ma_result result = ma_node_detach_output_bus(&effectNode, 0); result != MA_SUCCESS) + { + err() << "Failed to detach effect node output from endpoint: " << ma_result_description(result) << std::endl; + return; + } + } + + // Attach the sound output to the custom effect node or the engine endpoint + if (const ma_result result = ma_node_attach_output_bus(&sound, 0, connect ? &effectNode : ma_engine_get_endpoint(engine), 0); + result != MA_SUCCESS) + { + err() << "Failed to attach sound node output to effect node: " << ma_result_description(result) << std::endl; + return; + } } -} // namespace //////////////////////////////////////////////////////////// @@ -266,30 +408,4 @@ ma_uint64 MiniaudioUtils::getFrameIndex(ma_sound& sound, Time timeOffset) return frameIndex; } - -//////////////////////////////////////////////////////////// -void MiniaudioUtils::reinitializeSound(ma_sound& sound, const std::function& initializeFn) -{ - const SavedSettings savedSettings = saveSettings(sound); - ma_sound_uninit(&sound); - - initializeFn(); - - applySettings(sound, savedSettings); -} - - -//////////////////////////////////////////////////////////// -void MiniaudioUtils::initializeSound(const ma_data_source_vtable& vtable, - ma_data_source_base& dataSourceBase, - ma_sound& sound, - const std::function& initializeFn) -{ - initializeDataSource(dataSourceBase, vtable); - - // Initialize sound structure and set default settings - initializeFn(); - applySettings(sound, SavedSettings{}); -} - } // namespace sf::priv diff --git a/src/SFML/Audio/MiniaudioUtils.hpp b/src/SFML/Audio/MiniaudioUtils.hpp index a9d86ce63..6d6faa76c 100644 --- a/src/SFML/Audio/MiniaudioUtils.hpp +++ b/src/SFML/Audio/MiniaudioUtils.hpp @@ -27,11 +27,15 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include +#include + +#include #include -#include +#include //////////////////////////////////////////////////////////// @@ -44,15 +48,64 @@ class Time; namespace priv::MiniaudioUtils { +struct SavedSettings +{ + float pitch{1.f}; + float pan{0.f}; + float volume{1.f}; + ma_bool32 spatializationEnabled{MA_TRUE}; + ma_vec3f position{0.f, 0.f, 0.f}; + ma_vec3f direction{0.f, 0.f, -1.f}; + float directionalAttenuationFactor{1.f}; + ma_vec3f velocity{0.f, 0.f, 0.f}; + float dopplerFactor{1.f}; + ma_positioning positioning{ma_positioning_absolute}; + float minDistance{1.f}; + float maxDistance{std::numeric_limits::max()}; + float minGain{0.f}; + float maxGain{1.f}; + float rollOff{1.f}; + ma_bool32 playing{MA_FALSE}; + ma_bool32 looping{MA_FALSE}; + float innerAngle{degrees(360.f).asRadians()}; + float outerAngle{degrees(360.f).asRadians()}; + float outerGain{0.f}; +}; + +struct SoundBase +{ + SoundBase(const ma_data_source_vtable& dataSourceVTable, AudioDevice::ResourceEntry::Func reinitializeFunc); + ~SoundBase(); + void initialize(ma_sound_end_proc endCallback); + void deinitialize(); + void processEffect(const float** framesIn, ma_uint32& frameCountIn, float** framesOut, ma_uint32& frameCountOut) const; + void connectEffect(bool connect); + + //////////////////////////////////////////////////////////// + // Member data + //////////////////////////////////////////////////////////// + struct EffectNode + { + ma_node_base base{}; + SoundBase* impl{}; + ma_uint32 channelCount{}; + }; + + ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member) + ma_node_vtable effectNodeVTable{}; //!< Vtable of the effect node + EffectNode effectNode; //!< The engine node that performs effect processing + std::vector soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels) + ma_sound sound{}; //!< The sound + SoundSource::Status status{SoundSource::Status::Stopped}; //!< The status + SoundSource::EffectProcessor effectProcessor; //!< The effect processor + priv::AudioDevice::ResourceEntryIter resourceEntryIter; //!< Iterator to the resource entry registered with the AudioDevice + priv::MiniaudioUtils::SavedSettings savedSettings; //!< Saved settings used to restore ma_sound state in case we need to recreate it +}; + [[nodiscard]] ma_channel soundChannelToMiniaudioChannel(SoundChannel soundChannel); [[nodiscard]] SoundChannel miniaudioChannelToSoundChannel(ma_channel soundChannel); [[nodiscard]] Time getPlayingOffset(ma_sound& sound); [[nodiscard]] ma_uint64 getFrameIndex(ma_sound& sound, Time timeOffset); -void reinitializeSound(ma_sound& sound, const std::function& initializeFn); -void initializeSound(const ma_data_source_vtable& vtable, - ma_data_source_base& dataSourceBase, - ma_sound& sound, - const std::function& initializeFn); } // namespace priv::MiniaudioUtils } // namespace sf diff --git a/src/SFML/Audio/PlaybackDevice.cpp b/src/SFML/Audio/PlaybackDevice.cpp new file mode 100644 index 000000000..38c7d36c4 --- /dev/null +++ b/src/SFML/Audio/PlaybackDevice.cpp @@ -0,0 +1,83 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2024 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 + + +namespace sf::PlaybackDevice +{ +//////////////////////////////////////////////////////////// +std::vector getAvailableDevices() +{ + const auto devices = priv::AudioDevice::getAvailableDevices(); + + std::vector deviceNameList; + deviceNameList.reserve(devices.size()); + + for (const auto& device : devices) + deviceNameList.emplace_back(device.name); + + return deviceNameList; +} + + +//////////////////////////////////////////////////////////// +std::optional getDefaultDevice() +{ + for (const auto& device : priv::AudioDevice::getAvailableDevices()) + { + if (device.isDefault) + return device.name; + } + + return std::nullopt; +} + + +//////////////////////////////////////////////////////////// +bool setDevice(const std::string& name) +{ + // Perform a sanity check to make sure the user isn't passing us a non-existant device name + const auto devices = priv::AudioDevice::getAvailableDevices(); + if (auto iter = std::find_if(devices.begin(), devices.end(), [&](const auto& device) { return device.name == name; }); + iter == devices.end()) + return false; + + return priv::AudioDevice::setDevice(name); +} + + +//////////////////////////////////////////////////////////// +std::optional getDevice() +{ + return priv::AudioDevice::getDevice(); +} + +} // namespace sf::PlaybackDevice diff --git a/src/SFML/Audio/Sound.cpp b/src/SFML/Audio/Sound.cpp index c61a71570..190d13339 100644 --- a/src/SFML/Audio/Sound.cpp +++ b/src/SFML/Audio/Sound.cpp @@ -25,7 +25,6 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// -#include #include #include #include @@ -44,80 +43,17 @@ namespace sf { -struct Sound::Impl +struct Sound::Impl : priv::MiniaudioUtils::SoundBase { - Impl() + Impl() : SoundBase(vtable, [](void* ptr) { static_cast(ptr)->initialize(); }) { - static constexpr ma_data_source_vtable vtable{read, seek, getFormat, getCursor, getLength, setLooping, 0}; - priv::MiniaudioUtils::initializeSound(vtable, dataSourceBase, sound, [this] { initialize(); }); - } - - ~Impl() - { - ma_sound_uninit(&sound); - ma_node_uninit(&effectNode, nullptr); - ma_data_source_uninit(&dataSourceBase); + // Initialize sound structure and set default settings + initialize(); } void initialize() { - // Initialize the sound - auto* engine = priv::AudioDevice::getEngine(); - - if (engine == nullptr) - { - err() << "Failed to initialize sound: No engine available" << std::endl; - return; - } - - ma_sound_config soundConfig; - - soundConfig = ma_sound_config_init(); - soundConfig.pDataSource = this; - soundConfig.pEndCallbackUserData = this; - soundConfig.endCallback = [](void* userData, ma_sound* soundPtr) - { - auto& impl = *static_cast(userData); - impl.status = Status::Stopped; - - // Seek back to the start of the sound when it finishes playing - if (const ma_result result = ma_sound_seek_to_pcm_frame(soundPtr, 0); result != MA_SUCCESS) - err() << "Failed to seek sound to frame 0: " << ma_result_description(result) << std::endl; - }; - - if (const ma_result result = ma_sound_init_ex(engine, &soundConfig, &sound); result != MA_SUCCESS) - { - err() << "Failed to initialize sound: " << ma_result_description(result) << std::endl; - return; - } - - // Initialize the custom effect node - effectNodeVTable.onProcess = - [](ma_node* node, const float** framesIn, ma_uint32* frameCountIn, float** framesOut, ma_uint32* frameCountOut) - { static_cast(node)->impl->processEffect(framesIn, *frameCountIn, framesOut, *frameCountOut); }; - effectNodeVTable.onGetRequiredInputFrameCount = nullptr; - effectNodeVTable.inputBusCount = 1; - effectNodeVTable.outputBusCount = 1; - effectNodeVTable.flags = MA_NODE_FLAG_CONTINUOUS_PROCESSING | MA_NODE_FLAG_ALLOW_NULL_INPUT; - - const auto nodeChannelCount = ma_engine_get_channels(engine); - ma_node_config nodeConfig = ma_node_config_init(); - nodeConfig.vtable = &effectNodeVTable; - nodeConfig.pInputChannels = &nodeChannelCount; - nodeConfig.pOutputChannels = &nodeChannelCount; - - if (const ma_result result = ma_node_init(ma_engine_get_node_graph(engine), &nodeConfig, nullptr, &effectNode); - result != MA_SUCCESS) - { - err() << "Failed to initialize effect node: " << ma_result_description(result) << std::endl; - return; - } - - effectNode.impl = this; - effectNode.channelCount = nodeChannelCount; - - // Route the sound through the effect node depending on whether an effect processor is set - connectEffect(bool{effectProcessor}); + SoundBase::initialize(onEnd); // Because we are providing a custom data source, we have to provide the channel map ourselves if (buffer && !buffer->getChannelMap().empty()) @@ -137,82 +73,14 @@ struct Sound::Impl } } - void reinitialize() + static void onEnd(void* userData, ma_sound* soundPtr) { - priv::MiniaudioUtils::reinitializeSound(sound, - [this] - { - ma_node_uninit(&effectNode, nullptr); - initialize(); - }); - } + auto& impl = *static_cast(userData); + impl.status = Status::Stopped; - void processEffect(const float** framesIn, ma_uint32& frameCountIn, float** framesOut, ma_uint32& frameCountOut) const - { - // If a processor is set, call it - if (effectProcessor) - { - if (!framesIn) - frameCountIn = 0; - - effectProcessor(framesIn ? framesIn[0] : nullptr, frameCountIn, framesOut[0], frameCountOut, effectNode.channelCount); - - return; - } - - // Otherwise just pass the data through 1:1 - if (framesIn == nullptr) - { - frameCountIn = 0; - frameCountOut = 0; - return; - } - - const auto toProcess = std::min(frameCountIn, frameCountOut); - std::memcpy(framesOut[0], framesIn[0], toProcess * effectNode.channelCount * sizeof(float)); - frameCountIn = toProcess; - frameCountOut = toProcess; - } - - void connectEffect(bool connect) - { - auto* engine = priv::AudioDevice::getEngine(); - - if (engine == nullptr) - { - err() << "Failed to connect effect: No engine available" << std::endl; - return; - } - - if (connect) - { - // Attach the custom effect node output to our engine endpoint - if (const ma_result result = ma_node_attach_output_bus(&effectNode, 0, ma_engine_get_endpoint(engine), 0); - result != MA_SUCCESS) - { - err() << "Failed to attach effect node output to endpoint: " << ma_result_description(result) << std::endl; - return; - } - } - else - { - // Detach the custom effect node output from our engine endpoint - if (const ma_result result = ma_node_detach_output_bus(&effectNode, 0); result != MA_SUCCESS) - { - err() << "Failed to detach effect node output from endpoint: " << ma_result_description(result) - << std::endl; - return; - } - } - - // Attach the sound output to the custom effect node or the engine endpoint - if (const ma_result - result = ma_node_attach_output_bus(&sound, 0, connect ? &effectNode : ma_engine_get_endpoint(engine), 0); - result != MA_SUCCESS) - { - err() << "Failed to attach sound node output to effect node: " << ma_result_description(result) << std::endl; - return; - } + // Seek back to the start of the sound when it finishes playing + if (const ma_result result = ma_sound_seek_to_pcm_frame(soundPtr, 0); result != MA_SUCCESS) + err() << "Failed to seek sound to frame 0: " << ma_result_description(result) << std::endl; } static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) @@ -310,23 +178,10 @@ struct Sound::Impl //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - struct EffectNode - { - ma_node_base base{}; - Impl* impl{}; - ma_uint32 channelCount{}; - }; - - ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member) - ma_node_vtable effectNodeVTable{}; //!< Vtable of the effect node - EffectNode effectNode; //!< The engine node that performs effect processing - std::vector soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels) - ma_sound sound{}; //!< The sound - std::size_t cursor{}; //!< The current playing position - bool looping{}; //!< True if we are looping the sound - const SoundBuffer* buffer{}; //!< Sound buffer bound to the source - Status status{Status::Stopped}; //!< The status - EffectProcessor effectProcessor; //!< The effect processor + static constexpr ma_data_source_vtable vtable{read, seek, getFormat, getCursor, getLength, setLooping, 0}; + std::size_t cursor{}; //!< The current playing position + bool looping{}; //!< True if we are looping the sound + const SoundBuffer* buffer{}; //!< Sound buffer bound to the source }; @@ -422,7 +277,8 @@ void Sound::setBuffer(const SoundBuffer& buffer) m_impl->buffer = &buffer; m_impl->buffer->attachSound(this); - m_impl->reinitialize(); + m_impl->deinitialize(); + m_impl->initialize(); } diff --git a/src/SFML/Audio/SoundStream.cpp b/src/SFML/Audio/SoundStream.cpp index 334cdac3a..af0b4535e 100644 --- a/src/SFML/Audio/SoundStream.cpp +++ b/src/SFML/Audio/SoundStream.cpp @@ -25,7 +25,6 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// -#include #include #include @@ -44,81 +43,19 @@ namespace sf { -struct SoundStream::Impl +struct SoundStream::Impl : priv::MiniaudioUtils::SoundBase { - Impl(SoundStream* ownerPtr) : owner(ownerPtr) + Impl(SoundStream* ownerPtr) : + SoundBase(vtable, [](void* ptr) { static_cast(ptr)->initialize(); }), + owner(ownerPtr) { - static constexpr ma_data_source_vtable vtable{read, seek, getFormat, getCursor, getLength, setLooping, /* flags */ 0}; - priv::MiniaudioUtils::initializeSound(vtable, dataSourceBase, sound, [this] { initialize(); }); - } - - ~Impl() - { - ma_sound_uninit(&sound); - ma_node_uninit(&effectNode, nullptr); - ma_data_source_uninit(&dataSourceBase); + // Initialize sound structure and set default settings + initialize(); } void initialize() { - // Initialize the sound - auto* engine = priv::AudioDevice::getEngine(); - - if (engine == nullptr) - { - err() << "Failed to initialize sound: No engine available" << std::endl; - return; - } - - ma_sound_config soundConfig; - - soundConfig = ma_sound_config_init(); - soundConfig.pDataSource = this; - soundConfig.pEndCallbackUserData = this; - soundConfig.endCallback = [](void* userData, ma_sound* soundPtr) - { - // Seek back to the start of the sound when it finishes playing - auto& impl = *static_cast(userData); - impl.streaming = true; - impl.status = Status::Stopped; - - if (const ma_result result = ma_sound_seek_to_pcm_frame(soundPtr, 0); result != MA_SUCCESS) - err() << "Failed to seek sound to frame 0: " << ma_result_description(result) << std::endl; - }; - - if (const ma_result result = ma_sound_init_ex(engine, &soundConfig, &sound); result != MA_SUCCESS) - { - err() << "Failed to initialize sound: " << ma_result_description(result) << std::endl; - return; - } - - // Initialize the custom effect node - effectNodeVTable.onProcess = - [](ma_node* node, const float** framesIn, ma_uint32* frameCountIn, float** framesOut, ma_uint32* frameCountOut) - { static_cast(node)->impl->processEffect(framesIn, *frameCountIn, framesOut, *frameCountOut); }; - effectNodeVTable.onGetRequiredInputFrameCount = nullptr; - effectNodeVTable.inputBusCount = 1; - effectNodeVTable.outputBusCount = 1; - effectNodeVTable.flags = MA_NODE_FLAG_CONTINUOUS_PROCESSING | MA_NODE_FLAG_ALLOW_NULL_INPUT; - - const auto nodeChannelCount = ma_engine_get_channels(engine); - ma_node_config nodeConfig = ma_node_config_init(); - nodeConfig.vtable = &effectNodeVTable; - nodeConfig.pInputChannels = &nodeChannelCount; - nodeConfig.pOutputChannels = &nodeChannelCount; - - if (const ma_result result = ma_node_init(ma_engine_get_node_graph(engine), &nodeConfig, nullptr, &effectNode); - result != MA_SUCCESS) - { - err() << "Failed to initialize effect node: " << ma_result_description(result) << std::endl; - return; - } - - effectNode.impl = this; - effectNode.channelCount = nodeChannelCount; - - // Route the sound through the effect node depending on whether an effect processor is set - connectEffect(bool{effectProcessor}); + SoundBase::initialize(onEnd); // Because we are providing a custom data source, we have to provide the channel map ourselves if (!channelMap.empty()) @@ -138,81 +75,15 @@ struct SoundStream::Impl } } - void reinitialize() + static void onEnd(void* userData, ma_sound* soundPtr) { - priv::MiniaudioUtils::reinitializeSound(sound, - [this] - { - ma_node_uninit(&effectNode, nullptr); - initialize(); - }); - } + // Seek back to the start of the sound when it finishes playing + auto& impl = *static_cast(userData); + impl.streaming = true; + impl.status = Status::Stopped; - void processEffect(const float** framesIn, ma_uint32& frameCountIn, float** framesOut, ma_uint32& frameCountOut) const - { - // If a processor is set, call it - if (effectProcessor) - { - if (!framesIn) - frameCountIn = 0; - - effectProcessor(framesIn ? framesIn[0] : nullptr, frameCountIn, framesOut[0], frameCountOut, effectNode.channelCount); - return; - } - - // Otherwise just pass the data through 1:1 - if (framesIn == nullptr) - { - frameCountIn = 0; - frameCountOut = 0; - return; - } - - const auto toProcess = std::min(frameCountIn, frameCountOut); - std::memcpy(framesOut[0], framesIn[0], toProcess * effectNode.channelCount * sizeof(float)); - frameCountIn = toProcess; - frameCountOut = toProcess; - } - - void connectEffect(bool connect) - { - auto* engine = priv::AudioDevice::getEngine(); - - if (engine == nullptr) - { - err() << "Failed to connect effect: No engine available" << std::endl; - return; - } - - if (connect) - { - // Attach the custom effect node output to our engine endpoint - if (const ma_result result = ma_node_attach_output_bus(&effectNode, 0, ma_engine_get_endpoint(engine), 0); - result != MA_SUCCESS) - { - err() << "Failed to attach effect node output to endpoint: " << ma_result_description(result) << std::endl; - return; - } - } - else - { - // Detach the custom effect node output from our engine endpoint - if (const ma_result result = ma_node_detach_output_bus(&effectNode, 0); result != MA_SUCCESS) - { - err() << "Failed to detach effect node output from endpoint: " << ma_result_description(result) - << std::endl; - return; - } - } - - // Attach the sound output to the custom effect node or the engine endpoint - if (const ma_result - result = ma_node_attach_output_bus(&sound, 0, connect ? &effectNode : ma_engine_get_endpoint(engine), 0); - result != MA_SUCCESS) - { - err() << "Failed to attach sound node output to effect node: " << ma_result_description(result) << std::endl; - return; - } + if (const ma_result result = ma_sound_seek_to_pcm_frame(soundPtr, 0); result != MA_SUCCESS) + err() << "Failed to seek sound to frame 0: " << ma_result_description(result) << std::endl; } static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) @@ -339,29 +210,16 @@ struct SoundStream::Impl //////////////////////////////////////////////////////////// // Member data //////////////////////////////////////////////////////////// - struct EffectNode - { - ma_node_base base{}; - Impl* impl{}; - ma_uint32 channelCount{}; - }; - - ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member) - SoundStream* const owner; //!< Owning SoundStream object - ma_node_vtable effectNodeVTable{}; //!< Vtable of the effect node - EffectNode effectNode; //!< The engine node that performs effect processing - std::vector soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels) - ma_sound sound{}; //!< The sound - std::vector sampleBuffer; //!< Our temporary sample buffer - std::size_t sampleBufferCursor{}; //!< The current read position in the temporary sample buffer - std::uint64_t samplesProcessed{}; //!< Number of samples processed since beginning of the stream - unsigned int channelCount{}; //!< Number of channels (1 = mono, 2 = stereo, ...) - unsigned int sampleRate{}; //!< Frequency (samples / second) - std::vector channelMap{}; //!< The map of position in sample frame to sound channel - bool loop{}; //!< Loop flag (true to loop, false to play once) - bool streaming{true}; //!< True if we are still streaming samples from the source - Status status{Status::Stopped}; //!< The status - EffectProcessor effectProcessor; //!< The effect processor + static constexpr ma_data_source_vtable vtable{read, seek, getFormat, getCursor, getLength, setLooping, /* flags */ 0}; + SoundStream* const owner; //!< Owning SoundStream object + std::vector sampleBuffer; //!< Our temporary sample buffer + std::size_t sampleBufferCursor{}; //!< The current read position in the temporary sample buffer + std::uint64_t samplesProcessed{}; //!< Number of samples processed since beginning of the stream + unsigned int channelCount{}; //!< Number of channels (1 = mono, 2 = stereo, ...) + unsigned int sampleRate{}; //!< Frequency (samples / second) + std::vector channelMap{}; //!< The map of position in sample frame to sound channel + bool loop{}; //!< Loop flag (true to loop, false to play once) + bool streaming{true}; //!< True if we are still streaming samples from the source }; @@ -383,7 +241,8 @@ void SoundStream::initialize(unsigned int channelCount, unsigned int sampleRate, m_impl->channelMap = channelMap; m_impl->samplesProcessed = 0; - m_impl->reinitialize(); + m_impl->deinitialize(); + m_impl->initialize(); }