diff --git a/examples/sound_effects/SoundEffects.cpp b/examples/sound_effects/SoundEffects.cpp index 77404e4c0b..4f229831f8 100644 --- a/examples/sound_effects/SoundEffects.cpp +++ b/examples/sound_effects/SoundEffects.cpp @@ -18,6 +18,7 @@ namespace constexpr auto windowWidth = 800u; constexpr auto windowHeight = 600u; constexpr auto pi = 3.14159265359f; +constexpr auto sqrt2 = 2.0f * 0.707106781186547524401f; std::filesystem::path resourcesDir() { @@ -615,6 +616,421 @@ class Doppler : public sf::SoundStream, public Effect }; +//////////////////////////////////////////////////////////// +// Processing base class +//////////////////////////////////////////////////////////// +class Processing : public Effect +{ +public: + void onUpdate(float /*time*/, float x, float y) override + { + m_position = {windowWidth * x - 10.f, windowHeight * y - 10.f}; + m_music.setPosition({m_position.x, m_position.y, 0.f}); + } + + void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override + { + auto statesCopy(states); + statesCopy.transform = sf::Transform::Identity; + statesCopy.transform.translate(m_position); + + target.draw(m_listener, states); + target.draw(m_soundShape, statesCopy); + target.draw(m_enabledText); + target.draw(m_instructions); + } + + void onStart() override + { + // Synchronize listener audio position with graphical position + sf::Listener::setPosition({m_listener.getPosition().x, m_listener.getPosition().y, 0.f}); + + m_music.play(); + } + + void onStop() override + { + m_music.stop(); + } + +protected: + Processing(std::string name) : + Effect(std::move(name)), + m_enabled(std::make_shared(true)), + m_enabledText(getFont(), "Processing: Enabled"), + m_instructions(getFont(), "Press Space to enable/disable processing") + { + m_listener.setPosition({(windowWidth - 20.f) / 2.f, (windowHeight - 20.f) / 2.f}); + m_listener.setFillColor(sf::Color::Red); + + m_enabledText.setPosition({windowWidth / 2.f - 120.f, windowHeight * 3.f / 4.f - 50.f}); + m_instructions.setPosition({windowWidth / 2.f - 250.f, windowHeight * 3.f / 4.f}); + + // Load the music file + if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) + sf::err() << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; + + // Set the music to loop + m_music.setLoop(true); + + // Set attenuation to a nice value + m_music.setAttenuation(0.0f); + } + + sf::Music& getMusic() + { + return m_music; + } + + const std::shared_ptr& getEnabled() const + { + return m_enabled; + } + +private: + void onKey(sf::Keyboard::Key key) override + { + if (key == sf::Keyboard::Key::Space) + *m_enabled = !*m_enabled; + + m_enabledText.setString(*m_enabled ? "Processing: Enabled" : "Processing: Disabled"); + } + + sf::CircleShape m_listener{20.f}; + sf::CircleShape m_soundShape{20.f}; + sf::Vector2f m_position; + sf::Music m_music; + std::shared_ptr m_enabled; + sf::Text m_enabledText; + sf::Text m_instructions; +}; + + +//////////////////////////////////////////////////////////// +// Biquad Filter (https://github.com/dimtass/DSP-Cpp-filters) +//////////////////////////////////////////////////////////// +class BiquadFilter : public Processing +{ +protected: + struct Coefficients + { + float a0{}; + float a1{}; + float a2{}; + float b1{}; + float b2{}; + float c0{}; + float d0{}; + }; + + using Processing::Processing; + + void setCoefficients(const Coefficients& coefficients) + { + auto& music = getMusic(); + + struct State + { + float xnz1{}; + float xnz2{}; + float ynz1{}; + float ynz2{}; + }; + + // We use a mutable lambda to tie the lifetime of the state and coefficients to the lambda itself + // This is necessary since the Echo object will be destroyed before the Music object + // While the Music object exists, it is possible that the audio engine will try to call + // this lambda hence we need to always have usable coefficients and state until the Music and the + // associated lambda are destroyed + music.setEffectProcessor( + [coefficients, + enabled = getEnabled(), + state = std::vector(music.getChannelCount())](const float* inputFrames, + unsigned int& inputFrameCount, + float* outputFrames, + unsigned int& outputFrameCount, + unsigned int frameChannelCount) mutable + { + for (auto frame = 0u; frame < outputFrameCount; ++frame) + { + for (auto channel = 0u; channel < frameChannelCount; ++channel) + { + auto& channelState = state[channel]; + + const auto xn = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available + const auto yn = coefficients.a0 * xn + coefficients.a1 * channelState.xnz1 + + coefficients.a2 * channelState.xnz2 - coefficients.b1 * channelState.ynz1 - + coefficients.b2 * channelState.ynz2; + + channelState.xnz2 = channelState.xnz1; + channelState.xnz1 = xn; + channelState.ynz2 = channelState.ynz1; + channelState.ynz1 = yn; + + outputFrames[channel] = *enabled ? yn : xn; + } + + inputFrames += (inputFrames ? frameChannelCount : 0u); + outputFrames += frameChannelCount; + } + + // We processed data 1:1 + inputFrameCount = outputFrameCount; + }); + } +}; + + +//////////////////////////////////////////////////////////// +// High-pass Filter (https://github.com/dimtass/DSP-Cpp-filters) +//////////////////////////////////////////////////////////// +struct HighPassFilter : BiquadFilter +{ + HighPassFilter() : BiquadFilter("High-pass Filter") + { + static constexpr auto cutoffFrequency = 2000.f; + + const auto c = std::tan(pi * cutoffFrequency / static_cast(getMusic().getSampleRate())); + + Coefficients coefficients; + + coefficients.a0 = 1.f / (1.f + sqrt2 * c + std::pow(c, 2.f)); + coefficients.a1 = -2.f * coefficients.a0; + coefficients.a2 = coefficients.a0; + coefficients.b1 = 2.f * coefficients.a0 * (std::pow(c, 2.f) - 1.f); + coefficients.b2 = coefficients.a0 * (1.f - sqrt2 * c + std::pow(c, 2.f)); + + setCoefficients(coefficients); + } +}; + + +//////////////////////////////////////////////////////////// +// Low-pass Filter (https://github.com/dimtass/DSP-Cpp-filters) +//////////////////////////////////////////////////////////// +struct LowPassFilter : BiquadFilter +{ + LowPassFilter() : BiquadFilter("Low-pass Filter") + { + static constexpr auto cutoffFrequency = 500.f; + + const auto c = 1.f / std::tan(pi * cutoffFrequency / static_cast(getMusic().getSampleRate())); + + Coefficients coefficients; + + coefficients.a0 = 1.f / (1.f + sqrt2 * c + std::pow(c, 2.f)); + coefficients.a1 = 2.f * coefficients.a0; + coefficients.a2 = coefficients.a0; + coefficients.b1 = 2.f * coefficients.a0 * (1.f - std::pow(c, 2.f)); + coefficients.b2 = coefficients.a0 * (1.f - sqrt2 * c + std::pow(c, 2.f)); + + setCoefficients(coefficients); + } +}; + + +//////////////////////////////////////////////////////////// +// Echo (miniaudio implementation) +//////////////////////////////////////////////////////////// +struct Echo : Processing +{ + Echo() : Processing("Echo") + { + auto& music = getMusic(); + + static constexpr auto delay = 0.2f; + static constexpr auto decay = 0.75f; + static constexpr auto wet = 0.8f; + static constexpr auto dry = 1.f; + + const auto channelCount = music.getChannelCount(); + const auto sampleRate = music.getSampleRate(); + const auto delayInFrames = static_cast(static_cast(sampleRate) * delay); + + // We use a mutable lambda to tie the lifetime of the state to the lambda itself + // This is necessary since the Echo object will be destroyed before the Music object + // While the Music object exists, it is possible that the audio engine will try to call + // this lambda hence we need to always have a usable state until the Music and the + // associated lambda are destroyed + music.setEffectProcessor( + [delayInFrames, + enabled = getEnabled(), + buffer = std::vector(delayInFrames * channelCount, 0.f), + cursor = 0u](const float* inputFrames, + unsigned int& inputFrameCount, + float* outputFrames, + unsigned int& outputFrameCount, + unsigned int frameChannelCount) mutable + { + for (auto frame = 0u; frame < outputFrameCount; ++frame) + { + for (auto channel = 0u; channel < frameChannelCount; ++channel) + { + const auto input = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available + const auto bufferIndex = (cursor * frameChannelCount) + channel; + buffer[bufferIndex] = (buffer[bufferIndex] * decay) + (input * dry); + outputFrames[channel] = *enabled ? buffer[bufferIndex] * wet : input; + } + + cursor = (cursor + 1) % delayInFrames; + + inputFrames += (inputFrames ? frameChannelCount : 0u); + outputFrames += frameChannelCount; + } + + // We processed data 1:1 + inputFrameCount = outputFrameCount; + }); + } +}; + + +//////////////////////////////////////////////////////////// +// Reverb (https://github.com/sellicott/DSP-FFMpeg-Reverb) +//////////////////////////////////////////////////////////// +class Reverb : public Processing +{ +public: + Reverb() : Processing("Reverb") + { + auto& music = getMusic(); + + static constexpr auto sustain = 0.7f; // [0.f; 1.f] + + const auto channelCount = music.getChannelCount(); + const auto sampleRate = music.getSampleRate(); + + std::vector> filters; + filters.reserve(channelCount); + + for (auto i = 0u; i < channelCount; ++i) + filters.emplace_back(sampleRate, sustain); + + // We use a mutable lambda to tie the lifetime of the state to the lambda itself + // This is necessary since the Echo object will be destroyed before the Music object + // While the Music object exists, it is possible that the audio engine will try to call + // this lambda hence we need to always have a usable state until the Music and the + // associated lambda are destroyed + music.setEffectProcessor( + [filters, enabled = getEnabled()](const float* inputFrames, + unsigned int& inputFrameCount, + float* outputFrames, + unsigned int& outputFrameCount, + unsigned int frameChannelCount) mutable + { + for (auto frame = 0u; frame < outputFrameCount; ++frame) + { + for (auto channel = 0u; channel < frameChannelCount; ++channel) + { + const auto input = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available + outputFrames[channel] = *enabled ? filters[channel](input) : input; + } + + inputFrames += (inputFrames ? frameChannelCount : 0u); + outputFrames += frameChannelCount; + } + + // We processed data 1:1 + inputFrameCount = outputFrameCount; + }); + } + +private: + template + struct AllPassFilter + { + AllPassFilter(std::size_t delay, float theGain) : buffer(delay, {}), gain(theGain) + { + } + + T operator()(T input) + { + const auto output = buffer[cursor]; + input = static_cast(input + gain * output); + buffer[cursor] = input; + cursor = (cursor + 1) % buffer.size(); + return static_cast(-gain * input + output); + } + + std::vector buffer; + std::size_t cursor{}; + const float gain{}; + }; + + + template + struct FIRFilter + { + FIRFilter(std::vector theTaps) : taps(std::move(theTaps)), buffer(taps.size(), {}) + { + } + + T operator()(T input) + { + buffer[cursor] = input; + cursor = (cursor + 1) % buffer.size(); + + T output{}; + + for (auto i = 0u; i < taps.size(); ++i) + output += static_cast(taps[i] * buffer[(cursor + i) % buffer.size()]); + + return output; + } + + const std::vector taps; + std::vector buffer; + std::size_t cursor{}; + }; + + template + struct ReverbFilter + { + ReverbFilter(unsigned int sampleRate, float theFeedbackGain) : + allPass{{sampleRate / 10, 0.6f}, {sampleRate / 30, -0.6f}, {sampleRate / 90, 0.6f}, {sampleRate / 270, -0.6f}}, + fir({0.003369f, 0.002810f, 0.001758f, 0.000340f, -0.001255f, -0.002793f, -0.004014f, -0.004659f, + -0.004516f, -0.003464f, -0.001514f, 0.001148f, 0.004157f, 0.006986f, 0.009003f, 0.009571f, + 0.008173f, 0.004560f, -0.001120f, -0.008222f, -0.015581f, -0.021579f, -0.024323f, -0.021933f, + -0.012904f, 0.003500f, 0.026890f, 0.055537f, 0.086377f, 0.115331f, 0.137960f, 0.150407f, + 0.150407f, 0.137960f, 0.115331f, 0.086377f, 0.055537f, 0.026890f, 0.003500f, -0.012904f, + -0.021933f, -0.024323f, -0.021579f, -0.015581f, -0.008222f, -0.001120f, 0.004560f, 0.008173f, + 0.009571f, 0.009003f, 0.006986f, 0.004157f, 0.001148f, -0.001514f, -0.003464f, -0.004516f, + -0.004659f, -0.004014f, -0.002793f, -0.001255f, 0.000340f, 0.001758f, 0.002810f, 0.003369f}), + buffer(sampleRate / 5, {}), // sample rate / 5 = 200ms buffer size + interval(buffer.size() / 3), + feedbackGain(theFeedbackGain) + { + } + + T operator()(T input) + { + auto output = static_cast(0.7f * input + feedbackGain * buffer[cursor]); + + for (auto& f : allPass) + output = f(output); + + output = fir(output); + + buffer[cursor] = output; + cursor = (cursor + 1) % buffer.size(); + + output += 0.5f * buffer[(cursor + 1 * interval - 1) % buffer.size()]; + output += 0.25f * buffer[(cursor + 2 * interval - 1) % buffer.size()]; + output += 0.125f * buffer[(cursor + 3 * interval - 1) % buffer.size()]; + + return 0.6f * output + input; + } + + AllPassFilter allPass[4]; + FIRFilter fir; + std::vector buffer; + std::size_t cursor{}; + const std::size_t interval{}; + const float feedbackGain{}; + }; +}; + + //////////////////////////////////////////////////////////// /// Entry point of application /// @@ -636,19 +1052,25 @@ int main() Effect::setFont(font); // Create the effects - Surround surroundEffect; - PitchVolume pitchVolumeEffect; - Attenuation attenuationEffect; - Tone toneEffect; - Doppler dopplerEffect; - - const std::array effects{ - &surroundEffect, - &pitchVolumeEffect, - &attenuationEffect, - &toneEffect, - &dopplerEffect, - }; + Surround surroundEffect; + PitchVolume pitchVolumeEffect; + Attenuation attenuationEffect; + Tone toneEffect; + Doppler dopplerEffect; + HighPassFilter highPassFilterEffect; + LowPassFilter lowPassFilterEffect; + Echo echoEffect; + Reverb reverbEffect; + + const std::array effects{&surroundEffect, + &pitchVolumeEffect, + &attenuationEffect, + &toneEffect, + &dopplerEffect, + &highPassFilterEffect, + &lowPassFilterEffect, + &echoEffect, + &reverbEffect}; std::size_t current = 0; diff --git a/include/SFML/Audio/Sound.hpp b/include/SFML/Audio/Sound.hpp index 6720a56f9b..ae9e3591fb 100644 --- a/include/SFML/Audio/Sound.hpp +++ b/include/SFML/Audio/Sound.hpp @@ -162,6 +162,17 @@ class SFML_AUDIO_API Sound : public SoundSource //////////////////////////////////////////////////////////// void setPlayingOffset(Time timeOffset); + //////////////////////////////////////////////////////////// + /// \brief Set the effect processor to be applied to the sound + /// + /// The effect processor is a callable that will be called + /// with sound data to be processed. + /// + /// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing + /// + //////////////////////////////////////////////////////////// + void setEffectProcessor(EffectProcessor effectProcessor) override; + //////////////////////////////////////////////////////////// /// \brief Get the audio buffer attached to the sound /// diff --git a/include/SFML/Audio/SoundSource.hpp b/include/SFML/Audio/SoundSource.hpp index c67da7084e..edcd95da94 100644 --- a/include/SFML/Audio/SoundSource.hpp +++ b/include/SFML/Audio/SoundSource.hpp @@ -34,6 +34,8 @@ #include #include +#include + namespace sf { @@ -74,6 +76,75 @@ class SFML_AUDIO_API SoundSource : protected AudioResource float outerGain{}; //!< Outer gain }; + //////////////////////////////////////////////////////////// + /// \brief Callable that is provided with sound data for processing + /// + /// When the audio engine sources sound data from sound + /// sources it will pass the data through an effects + /// processor if one is set. The sound data will already be + /// converted to the internal floating point format. + /// + /// Sound data that is processed this way is provided in + /// frames. Each frame contains 1 floating point sample per + /// channel. If e.g. the data source provides stereo data, + /// each frame will contain 2 floats. + /// + /// The effects processor function takes 4 parameters: + /// - The input data frames, channels interleaved + /// - The number of input data frames available + /// - The buffer to write output data frames to, channels interleaved + /// - The number of output data frames that the output buffer can hold + /// - The channel count + /// + /// The input and output frame counts are in/out parameters. + /// + /// When this function is called, the input count will + /// contain the number of frames available in the input + /// buffer. The output count will contain the size of the + /// output buffer i.e. the maximum number of frames that + /// can be written to the output buffer. + /// + /// Attempting to read more frames than the input frame + /// count or write more frames than the output frame count + /// will result in undefined behaviour. + /// + /// When done processing the frames, the input and output + /// frame counts must be updated to reflect the actual + /// number of frames that were read from the input and + /// written to the output. + /// + /// The processing function should always try to process as + /// much sound data as possible i.e. always try to fill the + /// output buffer to the maximum. In certain situations for + /// specific effects it can be possible that the input frame + /// count and output frame count aren't equal. As long as + /// the frame counts are updated accordingly this is + /// perfectly valid. + /// + /// If the audio engine determines that no audio data is + /// available from the data source, the input data frames + /// pointer is set to nullptr and the input frame count is + /// set to 0. In this case it is up to the function to + /// decide how to handle the situation. For specific effects + /// e.g. Echo/Delay buffered data might still be able to be + /// written to the output buffer even if there is no longer + /// any input data. + /// + /// An important thing to remember is that this function is + /// directly called by the audio engine. Because the audio + /// engine runs on an internal thread of its own, make sure + /// access to shared data is synchronized appropriately. + /// + /// Because this function is stored by the SoundSource + /// object it will be able to be called as long as the + /// SoundSource object hasn't yet been destroyed. Make sure + /// that any data this function references outlives the + /// SoundSource object otherwise use-after-free errors will + /// occur. + /// + //////////////////////////////////////////////////////////// + using EffectProcessor = std::function; + //////////////////////////////////////////////////////////// /// \brief Copy constructor /// @@ -331,6 +402,17 @@ class SFML_AUDIO_API SoundSource : protected AudioResource //////////////////////////////////////////////////////////// void setAttenuation(float attenuation); + //////////////////////////////////////////////////////////// + /// \brief Set the effect processor to be applied to the sound + /// + /// The effect processor is a callable that will be called + /// with sound data to be processed. + /// + /// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing + /// + //////////////////////////////////////////////////////////// + virtual void setEffectProcessor(EffectProcessor effectProcessor); + //////////////////////////////////////////////////////////// /// \brief Get the pitch of the sound /// diff --git a/include/SFML/Audio/SoundStream.hpp b/include/SFML/Audio/SoundStream.hpp index 12c29353b6..87ff8240c5 100644 --- a/include/SFML/Audio/SoundStream.hpp +++ b/include/SFML/Audio/SoundStream.hpp @@ -194,6 +194,17 @@ class SFML_AUDIO_API SoundStream : public SoundSource //////////////////////////////////////////////////////////// bool getLoop() const; + //////////////////////////////////////////////////////////// + /// \brief Set the effect processor to be applied to the sound + /// + /// The effect processor is a callable that will be called + /// with sound data to be processed. + /// + /// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing + /// + //////////////////////////////////////////////////////////// + void setEffectProcessor(EffectProcessor effectProcessor) override; + protected: //////////////////////////////////////////////////////////// /// \brief Default constructor diff --git a/src/SFML/Audio/Sound.cpp b/src/SFML/Audio/Sound.cpp index b683fdf596..d98b208135 100644 --- a/src/SFML/Audio/Sound.cpp +++ b/src/SFML/Audio/Sound.cpp @@ -55,6 +55,7 @@ struct Sound::Impl ~Impl() { ma_sound_uninit(&sound); + ma_node_uninit(&effectNode, nullptr); ma_data_source_uninit(&dataSourceBase); } @@ -90,6 +91,34 @@ struct Sound::Impl 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; + + ma_uint32 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(!!effectProcessor); + // Because we are providing a custom data source, we have to provide the channel map ourselves if (buffer && !buffer->getChannelMap().empty()) { @@ -110,7 +139,80 @@ struct Sound::Impl void reinitialize() { - priv::MiniaudioUtils::reinitializeSound(sound, [this] { initialize(); }); + priv::MiniaudioUtils::reinitializeSound(sound, + [this] + { + ma_node_uninit(&effectNode, nullptr); + initialize(); + }); + } + + 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; + } } static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) @@ -208,13 +310,23 @@ 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 }; @@ -334,6 +446,14 @@ void Sound::setPlayingOffset(Time timeOffset) } +//////////////////////////////////////////////////////////// +void Sound::setEffectProcessor(EffectProcessor effectProcessor) +{ + m_impl->effectProcessor = std::move(effectProcessor); + m_impl->connectEffect(!!m_impl->effectProcessor); +} + + //////////////////////////////////////////////////////////// const SoundBuffer& Sound::getBuffer() const { diff --git a/src/SFML/Audio/SoundSource.cpp b/src/SFML/Audio/SoundSource.cpp index 7b7d674778..433cefa098 100644 --- a/src/SFML/Audio/SoundSource.cpp +++ b/src/SFML/Audio/SoundSource.cpp @@ -166,6 +166,13 @@ void SoundSource::setAttenuation(float attenuation) } +//////////////////////////////////////////////////////////// +void SoundSource::setEffectProcessor(EffectProcessor effectProcessor) +{ + (void)std::move(effectProcessor); +} + + //////////////////////////////////////////////////////////// float SoundSource::getPitch() const { diff --git a/src/SFML/Audio/SoundStream.cpp b/src/SFML/Audio/SoundStream.cpp index 7eeeef813e..6653b8becf 100644 --- a/src/SFML/Audio/SoundStream.cpp +++ b/src/SFML/Audio/SoundStream.cpp @@ -55,6 +55,7 @@ struct SoundStream::Impl ~Impl() { ma_sound_uninit(&sound); + ma_node_uninit(&effectNode, nullptr); ma_data_source_uninit(&dataSourceBase); } @@ -91,6 +92,34 @@ struct SoundStream::Impl 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; + + ma_uint32 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(!!effectProcessor); + // Because we are providing a custom data source, we have to provide the channel map ourselves if (!channelMap.empty()) { @@ -111,7 +140,80 @@ struct SoundStream::Impl void reinitialize() { - priv::MiniaudioUtils::reinitializeSound(sound, [this] { initialize(); }); + priv::MiniaudioUtils::reinitializeSound(sound, + [this] + { + ma_node_uninit(&effectNode, nullptr); + initialize(); + }); + } + + 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; + } } static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) @@ -238,8 +340,17 @@ 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 @@ -251,6 +362,7 @@ struct SoundStream::Impl 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 }; @@ -395,6 +507,14 @@ bool SoundStream::getLoop() const } +//////////////////////////////////////////////////////////// +void SoundStream::setEffectProcessor(EffectProcessor effectProcessor) +{ + m_impl->effectProcessor = std::move(effectProcessor); + m_impl->connectEffect(!!m_impl->effectProcessor); +} + + //////////////////////////////////////////////////////////// std::optional SoundStream::onLoop() {