Added support for retrieving a list of audio playback devices, getting the default audio playback device and changing the audio playback device during runtime.

This commit is contained in:
binary1248 2024-05-21 03:08:05 +02:00 committed by Chris Thrasher
parent 4b1751321a
commit 123270f7ad
11 changed files with 948 additions and 485 deletions

View File

@ -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();

View File

@ -32,6 +32,7 @@
#include <SFML/Audio/Listener.hpp>
#include <SFML/Audio/Music.hpp>
#include <SFML/Audio/OutputSoundFile.hpp>
#include <SFML/Audio/PlaybackDevice.hpp>
#include <SFML/Audio/Sound.hpp>
#include <SFML/Audio/SoundBuffer.hpp>
#include <SFML/Audio/SoundBufferRecorder.hpp>

View File

@ -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 <SFML/Audio/Export.hpp>
#include <optional>
#include <string>
#include <vector>
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<std::string> 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<std::string> 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<std::string> getDevice();
} // namespace sf::PlaybackDevice

View File

@ -26,16 +26,36 @@
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Audio/AudioDevice.hpp>
#include <SFML/Audio/PlaybackDevice.hpp>
#include <SFML/System/Err.hpp>
#include <algorithm>
#include <array>
#include <ostream>
#include <unordered_map>
#include <cassert>
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<std::string>& getCurrentDevice()
{
static std::optional<std::string> 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<AudioDevice*>(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::DeviceEntry> 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<DeviceEntry>{};
}
std::vector<DeviceEntry> 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<std::string, int> 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<std::string> 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<ma_device_id> 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<AudioDevice*>(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<char, MA_MAX_DEVICE_NAME_LENGTH + 1> 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()
{

View File

@ -33,7 +33,11 @@
#include <miniaudio.h>
#include <list>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
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<DeviceEntry> 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<std::string> getDevice();
struct ResourceEntry
{
using Func = void (*)(void*);
void* resource{};
Func deinitializeFunc{};
Func reinitializeFunc{};
};
using ResourceEntryList = std::list<ResourceEntry>;
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<ma_device_id> 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<ma_context> m_context; //!< The miniaudio context
std::optional<ma_device> m_playbackDevice; //!< The miniaudio playback device
std::optional<ma_engine> 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

View File

@ -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

View File

@ -25,59 +25,33 @@
////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Audio/AudioDevice.hpp>
#include <SFML/Audio/MiniaudioUtils.hpp>
#include <SFML/Audio/SoundChannel.hpp>
#include <SFML/System/Angle.hpp>
#include <SFML/System/Err.hpp>
#include <SFML/System/Time.hpp>
#include <miniaudio.h>
#include <functional>
#include <limits>
#include <ostream>
#include <cassert>
#include <cstring>
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<float>::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),
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),
@ -92,6 +66,8 @@ SavedSettings saveSettings(const ma_sound& 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};
@ -99,7 +75,7 @@ SavedSettings saveSettings(const ma_sound& sound)
////////////////////////////////////////////////////////////
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<SoundBase*>(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<EffectNode*>(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<void()>& 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<void()>& initializeFn)
{
initializeDataSource(dataSourceBase, vtable);
// Initialize sound structure and set default settings
initializeFn();
applySettings(sound, SavedSettings{});
}
} // namespace sf::priv

View File

@ -27,11 +27,15 @@
////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Audio/AudioDevice.hpp>
#include <SFML/Audio/SoundChannel.hpp>
#include <SFML/Audio/SoundSource.hpp>
#include <SFML/System/Angle.hpp>
#include <miniaudio.h>
#include <functional>
#include <limits>
////////////////////////////////////////////////////////////
@ -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<float>::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<ma_channel> 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<void()>& initializeFn);
void initializeSound(const ma_data_source_vtable& vtable,
ma_data_source_base& dataSourceBase,
ma_sound& sound,
const std::function<void()>& initializeFn);
} // namespace priv::MiniaudioUtils
} // namespace sf

View File

@ -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 <SFML/Audio/AudioDevice.hpp>
#include <SFML/Audio/PlaybackDevice.hpp>
#include <algorithm>
namespace sf::PlaybackDevice
{
////////////////////////////////////////////////////////////
std::vector<std::string> getAvailableDevices()
{
const auto devices = priv::AudioDevice::getAvailableDevices();
std::vector<std::string> deviceNameList;
deviceNameList.reserve(devices.size());
for (const auto& device : devices)
deviceNameList.emplace_back(device.name);
return deviceNameList;
}
////////////////////////////////////////////////////////////
std::optional<std::string> 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<std::string> getDevice()
{
return priv::AudioDevice::getDevice();
}
} // namespace sf::PlaybackDevice

View File

@ -25,7 +25,6 @@
////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Audio/AudioDevice.hpp>
#include <SFML/Audio/MiniaudioUtils.hpp>
#include <SFML/Audio/Sound.hpp>
#include <SFML/Audio/SoundBuffer.hpp>
@ -44,80 +43,17 @@
namespace sf
{
struct Sound::Impl
struct Sound::Impl : priv::MiniaudioUtils::SoundBase
{
Impl()
Impl() : SoundBase(vtable, [](void* ptr) { static_cast<Impl*>(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<Impl*>(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<EffectNode*>(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<Impl*>(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<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels)
ma_sound sound{}; //!< The sound
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
Status status{Status::Stopped}; //!< The status
EffectProcessor effectProcessor; //!< The effect processor
};
@ -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();
}

View File

@ -25,7 +25,6 @@
////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Audio/AudioDevice.hpp>
#include <SFML/Audio/MiniaudioUtils.hpp>
#include <SFML/Audio/SoundStream.hpp>
@ -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<Impl*>(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<Impl*>(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<EffectNode*>(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<Impl*>(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,19 +210,8 @@ 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)
static constexpr ma_data_source_vtable vtable{read, seek, getFormat, getCursor, getLength, setLooping, /* flags */ 0};
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<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels)
ma_sound sound{}; //!< The sound
std::vector<std::int16_t> 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
@ -360,8 +220,6 @@ struct SoundStream::Impl
std::vector<SoundChannel> 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
};
@ -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();
}