mirror of
https://github.com/SFML/SFML.git
synced 2024-11-25 04:41:05 +08:00
Added support for user defined sound effect implementations.
This commit is contained in:
parent
c89c32d7ba
commit
002b8953fa
@ -19,6 +19,7 @@ namespace
|
|||||||
constexpr auto windowWidth = 800u;
|
constexpr auto windowWidth = 800u;
|
||||||
constexpr auto windowHeight = 600u;
|
constexpr auto windowHeight = 600u;
|
||||||
constexpr auto pi = 3.14159265359f;
|
constexpr auto pi = 3.14159265359f;
|
||||||
|
constexpr auto sqrt2 = 2.0f * 0.707106781186547524401f;
|
||||||
|
|
||||||
std::filesystem::path resourcesDir()
|
std::filesystem::path resourcesDir()
|
||||||
{
|
{
|
||||||
@ -86,7 +87,7 @@ protected:
|
|||||||
private:
|
private:
|
||||||
// Virtual functions to be implemented in derived effects
|
// Virtual functions to be implemented in derived effects
|
||||||
virtual void onUpdate(float time, float x, float y) = 0;
|
virtual void onUpdate(float time, float x, float y) = 0;
|
||||||
virtual void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const = 0;
|
virtual void onDraw(sf::RenderTarget& target, sf::RenderStates states) const = 0;
|
||||||
virtual void onStart() = 0;
|
virtual void onStart() = 0;
|
||||||
virtual void onStop() = 0;
|
virtual void onStop() = 0;
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ public:
|
|||||||
m_music.setPosition({m_position.x, m_position.y, 0.f});
|
m_music.setPosition({m_position.x, m_position.y, 0.f});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override
|
void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
|
||||||
{
|
{
|
||||||
auto statesCopy(states);
|
auto statesCopy(states);
|
||||||
statesCopy.transform = sf::Transform::Identity;
|
statesCopy.transform = sf::Transform::Identity;
|
||||||
@ -203,7 +204,7 @@ public:
|
|||||||
m_volumeText.setString("Volume: " + std::to_string(m_volume));
|
m_volumeText.setString("Volume: " + std::to_string(m_volume));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override
|
void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
|
||||||
{
|
{
|
||||||
target.draw(m_pitchText, states);
|
target.draw(m_pitchText, states);
|
||||||
target.draw(m_volumeText, states);
|
target.draw(m_volumeText, states);
|
||||||
@ -306,7 +307,7 @@ public:
|
|||||||
m_music.setPosition({m_position.x, m_position.y, 0.f});
|
m_music.setPosition({m_position.x, m_position.y, 0.f});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override
|
void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
|
||||||
{
|
{
|
||||||
auto statesCopy(states);
|
auto statesCopy(states);
|
||||||
|
|
||||||
@ -376,7 +377,7 @@ public:
|
|||||||
m_currentFrequency.setString("Frequency: " + std::to_string(m_frequency) + " Hz");
|
m_currentFrequency.setString("Frequency: " + std::to_string(m_frequency) + " Hz");
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override
|
void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
|
||||||
{
|
{
|
||||||
target.draw(m_instruction, states);
|
target.draw(m_instruction, states);
|
||||||
target.draw(m_currentType, states);
|
target.draw(m_currentType, states);
|
||||||
@ -548,7 +549,7 @@ public:
|
|||||||
setDopplerFactor(m_factor);
|
setDopplerFactor(m_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override
|
void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
|
||||||
{
|
{
|
||||||
auto statesCopy(states);
|
auto statesCopy(states);
|
||||||
statesCopy.transform = sf::Transform::Identity;
|
statesCopy.transform = sf::Transform::Identity;
|
||||||
@ -616,6 +617,425 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// Processing base class
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
class Processing : public Effect
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void onUpdate([[maybe_unused]] 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, 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_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"))
|
||||||
|
std::cerr << "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<bool>& 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<bool> m_enabled{std::make_shared<bool>(true)};
|
||||||
|
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<State>(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<float>(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<float>(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<unsigned int>(static_cast<float>(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<float>(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<ReverbFilter<float>> 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 <typename T>
|
||||||
|
class AllPassFilter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AllPassFilter(std::size_t delay, float theGain) : m_buffer(delay, {}), m_gain(theGain)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
T operator()(T input)
|
||||||
|
{
|
||||||
|
const auto output = m_buffer[m_cursor];
|
||||||
|
input = static_cast<T>(input + m_gain * output);
|
||||||
|
m_buffer[m_cursor] = input;
|
||||||
|
m_cursor = (m_cursor + 1) % m_buffer.size();
|
||||||
|
return static_cast<T>(-m_gain * input + output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<T> m_buffer;
|
||||||
|
std::size_t m_cursor{};
|
||||||
|
const float m_gain{};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class FIRFilter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FIRFilter(std::vector<float> taps) : m_taps(std::move(taps))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
T operator()(T input)
|
||||||
|
{
|
||||||
|
m_buffer[m_cursor] = input;
|
||||||
|
m_cursor = (m_cursor + 1) % m_buffer.size();
|
||||||
|
|
||||||
|
T output{};
|
||||||
|
|
||||||
|
for (auto i = 0u; i < m_taps.size(); ++i)
|
||||||
|
output += static_cast<T>(m_taps[i] * m_buffer[(m_cursor + i) % m_buffer.size()]);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::vector<float> m_taps;
|
||||||
|
std::vector<T> m_buffer = std::vector<T>(m_taps.size(), {});
|
||||||
|
std::size_t m_cursor{};
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class ReverbFilter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ReverbFilter(unsigned int sampleRate, float feedbackGain) :
|
||||||
|
m_allPass{{sampleRate / 10, 0.6f}, {sampleRate / 30, -0.6f}, {sampleRate / 90, 0.6f}, {sampleRate / 270, -0.6f}},
|
||||||
|
m_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}),
|
||||||
|
m_buffer(sampleRate / 5, {}), // sample rate / 5 = 200ms buffer size
|
||||||
|
m_feedbackGain(feedbackGain)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
T operator()(T input)
|
||||||
|
{
|
||||||
|
auto output = static_cast<T>(0.7f * input + m_feedbackGain * m_buffer[m_cursor]);
|
||||||
|
|
||||||
|
for (auto& f : m_allPass)
|
||||||
|
output = f(output);
|
||||||
|
|
||||||
|
output = m_fir(output);
|
||||||
|
|
||||||
|
m_buffer[m_cursor] = output;
|
||||||
|
m_cursor = (m_cursor + 1) % m_buffer.size();
|
||||||
|
|
||||||
|
output += 0.5f * m_buffer[(m_cursor + 1 * m_interval - 1) % m_buffer.size()];
|
||||||
|
output += 0.25f * m_buffer[(m_cursor + 2 * m_interval - 1) % m_buffer.size()];
|
||||||
|
output += 0.125f * m_buffer[(m_cursor + 3 * m_interval - 1) % m_buffer.size()];
|
||||||
|
|
||||||
|
return 0.6f * output + input;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
AllPassFilter<T> m_allPass[4];
|
||||||
|
FIRFilter<T> m_fir;
|
||||||
|
std::vector<T> m_buffer;
|
||||||
|
std::size_t m_cursor{};
|
||||||
|
const std::size_t m_interval{m_buffer.size() / 3};
|
||||||
|
const float m_feedbackGain{};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
/// Entry point of application
|
/// Entry point of application
|
||||||
///
|
///
|
||||||
@ -642,14 +1062,20 @@ int main()
|
|||||||
Attenuation attenuationEffect;
|
Attenuation attenuationEffect;
|
||||||
Tone toneEffect;
|
Tone toneEffect;
|
||||||
Doppler dopplerEffect;
|
Doppler dopplerEffect;
|
||||||
|
HighPassFilter highPassFilterEffect;
|
||||||
|
LowPassFilter lowPassFilterEffect;
|
||||||
|
Echo echoEffect;
|
||||||
|
Reverb reverbEffect;
|
||||||
|
|
||||||
const std::array<Effect*, 5> effects{
|
const std::array<Effect*, 9> effects{&surroundEffect,
|
||||||
&surroundEffect,
|
|
||||||
&pitchVolumeEffect,
|
&pitchVolumeEffect,
|
||||||
&attenuationEffect,
|
&attenuationEffect,
|
||||||
&toneEffect,
|
&toneEffect,
|
||||||
&dopplerEffect,
|
&dopplerEffect,
|
||||||
};
|
&highPassFilterEffect,
|
||||||
|
&lowPassFilterEffect,
|
||||||
|
&echoEffect,
|
||||||
|
&reverbEffect};
|
||||||
|
|
||||||
std::size_t current = 0;
|
std::size_t current = 0;
|
||||||
|
|
||||||
|
@ -162,6 +162,17 @@ public:
|
|||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
void setPlayingOffset(Time timeOffset);
|
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
|
/// \brief Get the audio buffer attached to the sound
|
||||||
///
|
///
|
||||||
|
@ -34,6 +34,8 @@
|
|||||||
#include <SFML/System/Angle.hpp>
|
#include <SFML/System/Angle.hpp>
|
||||||
#include <SFML/System/Vector3.hpp>
|
#include <SFML/System/Vector3.hpp>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
|
||||||
namespace sf
|
namespace sf
|
||||||
{
|
{
|
||||||
@ -74,6 +76,76 @@ public:
|
|||||||
float outerGain{}; //!< Outer gain
|
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<
|
||||||
|
void(const float* inputFrames, unsigned int& inputFrameCount, float* outputFrames, unsigned int& outputFrameCount, unsigned int frameChannelCount)>;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
/// \brief Copy constructor
|
/// \brief Copy constructor
|
||||||
///
|
///
|
||||||
@ -331,6 +403,17 @@ public:
|
|||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
void setAttenuation(float attenuation);
|
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
|
/// \brief Get the pitch of the sound
|
||||||
///
|
///
|
||||||
|
@ -194,6 +194,17 @@ public:
|
|||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
bool getLoop() const;
|
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:
|
protected:
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
/// \brief Default constructor
|
/// \brief Default constructor
|
||||||
|
@ -55,6 +55,7 @@ struct Sound::Impl
|
|||||||
~Impl()
|
~Impl()
|
||||||
{
|
{
|
||||||
ma_sound_uninit(&sound);
|
ma_sound_uninit(&sound);
|
||||||
|
ma_node_uninit(&effectNode, nullptr);
|
||||||
ma_data_source_uninit(&dataSourceBase);
|
ma_data_source_uninit(&dataSourceBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +91,34 @@ struct Sound::Impl
|
|||||||
return;
|
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});
|
||||||
|
|
||||||
// Because we are providing a custom data source, we have to provide the channel map ourselves
|
// Because we are providing a custom data source, we have to provide the channel map ourselves
|
||||||
if (buffer && !buffer->getChannelMap().empty())
|
if (buffer && !buffer->getChannelMap().empty())
|
||||||
{
|
{
|
||||||
@ -110,7 +139,80 @@ struct Sound::Impl
|
|||||||
|
|
||||||
void reinitialize()
|
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)
|
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
|
// 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_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)
|
std::vector<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels)
|
||||||
ma_sound sound{}; //!< The sound
|
ma_sound sound{}; //!< The sound
|
||||||
std::size_t cursor{}; //!< The current playing position
|
std::size_t cursor{}; //!< The current playing position
|
||||||
bool looping{}; //!< True if we are looping the sound
|
bool looping{}; //!< True if we are looping the sound
|
||||||
const SoundBuffer* buffer{}; //!< Sound buffer bound to the source
|
const SoundBuffer* buffer{}; //!< Sound buffer bound to the source
|
||||||
Status status{Status::Stopped}; //!< The status
|
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(bool{m_impl->effectProcessor});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
const SoundBuffer& Sound::getBuffer() const
|
const SoundBuffer& Sound::getBuffer() const
|
||||||
{
|
{
|
||||||
|
@ -166,6 +166,13 @@ void SoundSource::setAttenuation(float attenuation)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// NOLINTNEXTLINE(performance-unnecessary-value-param)
|
||||||
|
void SoundSource::setEffectProcessor(EffectProcessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
float SoundSource::getPitch() const
|
float SoundSource::getPitch() const
|
||||||
{
|
{
|
||||||
|
@ -55,6 +55,7 @@ struct SoundStream::Impl
|
|||||||
~Impl()
|
~Impl()
|
||||||
{
|
{
|
||||||
ma_sound_uninit(&sound);
|
ma_sound_uninit(&sound);
|
||||||
|
ma_node_uninit(&effectNode, nullptr);
|
||||||
ma_data_source_uninit(&dataSourceBase);
|
ma_data_source_uninit(&dataSourceBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +92,34 @@ struct SoundStream::Impl
|
|||||||
return;
|
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});
|
||||||
|
|
||||||
// Because we are providing a custom data source, we have to provide the channel map ourselves
|
// Because we are providing a custom data source, we have to provide the channel map ourselves
|
||||||
if (!channelMap.empty())
|
if (!channelMap.empty())
|
||||||
{
|
{
|
||||||
@ -111,7 +140,79 @@ struct SoundStream::Impl
|
|||||||
|
|
||||||
void reinitialize()
|
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)
|
static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead)
|
||||||
@ -238,8 +339,17 @@ struct SoundStream::Impl
|
|||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
// Member data
|
// 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_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member)
|
||||||
SoundStream* const owner; //!< Owning SoundStream object
|
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)
|
std::vector<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels)
|
||||||
ma_sound sound{}; //!< The sound
|
ma_sound sound{}; //!< The sound
|
||||||
std::vector<std::int16_t> sampleBuffer; //!< Our temporary sample buffer
|
std::vector<std::int16_t> sampleBuffer; //!< Our temporary sample buffer
|
||||||
@ -251,6 +361,7 @@ struct SoundStream::Impl
|
|||||||
bool loop{}; //!< Loop flag (true to loop, false to play once)
|
bool loop{}; //!< Loop flag (true to loop, false to play once)
|
||||||
bool streaming{true}; //!< True if we are still streaming samples from the source
|
bool streaming{true}; //!< True if we are still streaming samples from the source
|
||||||
Status status{Status::Stopped}; //!< The status
|
Status status{Status::Stopped}; //!< The status
|
||||||
|
EffectProcessor effectProcessor; //!< The effect processor
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -395,6 +506,14 @@ bool SoundStream::getLoop() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
void SoundStream::setEffectProcessor(EffectProcessor effectProcessor)
|
||||||
|
{
|
||||||
|
m_impl->effectProcessor = std::move(effectProcessor);
|
||||||
|
m_impl->connectEffect(bool{m_impl->effectProcessor});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
std::optional<std::uint64_t> SoundStream::onLoop()
|
std::optional<std::uint64_t> SoundStream::onLoop()
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user