Commit 3b4e9c74 authored by Matěj Toul's avatar Matěj Toul
Browse files

audio: added a reverb/echo effect (implementation of Freeverb)

parent 8925de89
Loading
Loading
Loading
Loading
+82 −0
Original line number Diff line number Diff line
#ifndef AUDIO_AUDIO_REVERB_EFFECT_HPP_INCLUDED
#   define AUDIO_AUDIO_REVERB_EFFECT_HPP_INCLUDED

#include "audio_node.hpp"
#include "dsp/comb_filter.hpp"
#include "dsp/allpass_delay.hpp"
#include <array>

namespace audio {

// Freeverb is a public domain reverb by Jezar at Dreampoint: https://ccrma.stanford.edu/~jos/pasp/Freeverb.html
// AudioReverbEffect is an implementation of the Freeverb algorithm consisting of:
// - 8 parallel comb filters per channel
// - 4 series allpass filters per channel
struct AudioReverbEffect : public audio::AudioNode
{
public:
    AudioReverbEffect(std::string const& name);
    ~AudioReverbEffect() override;

    // Room size (0.0 to 1.0) - affects feedback amount
    void set_room_size(float room_size);
    
    // Damping (0.0 to 1.0) - high frequency damping
    void set_damping(float damping);
    
    // Wet level (0.0 to 1.0) - how much reverb is in the mix
    void set_wet(float wet);
    
    // Dry level (0.0 to 1.0) - how much original signal is in the mix
    void set_dry(float dry);
    
    // Width (0.0 to 1.0) - stereo width (0=mono, 1=full stereo)
    void set_width(float width);
    
    // Freeze mode - when true, sets feedback to maximum for infinite reverb
    void set_freeze(bool freeze);

    void process(std::vector<float>& output_buffer, uint32_t frame_count, uint32_t sample_rate) override;

private:
    static constexpr int NUM_COMBS = 8;
    static constexpr int NUM_ALLPASSES = 4;
    static constexpr float REFERENCE_SAMPLE_RATE = 44100.0f;
    
    // Freeverb tuning constants (delay times in samples at 44.1kHz)
    // Delay times are recalculated during initialization based on actual sample rate to maintain consistent reverb characteristics
    static constexpr int STEREO_SPREAD = 23;
    static constexpr int COMB_TUNING_L[NUM_COMBS] = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
    static constexpr int COMB_TUNING_R[NUM_COMBS] = {1116+STEREO_SPREAD, 1188+STEREO_SPREAD, 1277+STEREO_SPREAD, 1356+STEREO_SPREAD, 1422+STEREO_SPREAD, 1491+STEREO_SPREAD, 1557+STEREO_SPREAD, 1617+STEREO_SPREAD};
    static constexpr int ALLPASS_TUNING_L[NUM_ALLPASSES] = {556, 441, 341, 225};
    static constexpr int ALLPASS_TUNING_R[NUM_ALLPASSES] = {556+STEREO_SPREAD, 441+STEREO_SPREAD, 341+STEREO_SPREAD, 225+STEREO_SPREAD};
    
    static constexpr float SCALE_WET = 3.0f; // necessary to compensate for comb filter gain reduction
    
    // Constants for calculating feedback from room size (empirically proved to be the most natural-sounding by original author)
    static constexpr float SCALE_DAMPING = 0.4f;
    static constexpr float SCALE_ROOM = 0.28f;
    static constexpr float OFFSET_ROOM = 0.7f;
    
    std::array<dsp::CombFilter, NUM_COMBS> m_combs_l;
    std::array<dsp::CombFilter, NUM_COMBS> m_combs_r;
    std::array<dsp::AllpassDelay, NUM_ALLPASSES> m_allpasses_l;
    std::array<dsp::AllpassDelay, NUM_ALLPASSES> m_allpasses_r;
    
    float m_room_size;
    float m_damping;
    float m_wet;
    float m_dry;
    float m_width;
    bool m_freeze;
    
    uint32_t m_current_sample_rate;
    
    void update_parameters();
    void initialize_filters(uint32_t sample_rate);
    int scale_delay(int reference_delay, uint32_t sample_rate) const;
};

}

#endif
+45 −0
Original line number Diff line number Diff line
#ifndef AUDIO_DSP_ALLPASS_DELAY_HPP_INCLUDED
#   define AUDIO_DSP_ALLPASS_DELAY_HPP_INCLUDED

#include <vector>

namespace audio::dsp {

// Simple allpass delay filter used in reverb algorithms
// y(n) = -x(n) + x(n-delay) + feedback * y(n-delay)
// Note that this is very different from a biquad allpass filter - it has a single delay line and feedback
// While the biquad modifies phase across the freq. spectrum, this allpass delay creates an actual echo effect (in time domain) with a specific delay time
struct AllpassDelay
{
	float feedback;
	int delay_samples;
	
	std::vector<float> delay_buffer;
	int write_index = 0;

	AllpassDelay(int delay_samples, float feedback);

	float process_sample(float input_sample)
	{
		int buffer_size = static_cast<int>(delay_buffer.size());
		int read_index = (write_index - delay_samples + buffer_size) % buffer_size;
		float delayed_sample = delay_buffer[read_index];
		
		// Allpass: y(n) = -x(n) + x(n-delay) + feedback * y(n-delay)
		float output_sample = -input_sample + delayed_sample;
		delay_buffer[write_index] = input_sample + feedback * delayed_sample;
		
		write_index = (write_index + 1) % buffer_size;
		return output_sample;
	}

	void clear()
	{
		std::fill(delay_buffer.begin(), delay_buffer.end(), 0.0f);
		write_index = 0;
	}
};

} // namespace audio::dsp

#endif // AUDIO_DSP_ALLPASS_DELAY_HPP_INCLUDED
+52 −0
Original line number Diff line number Diff line
#ifndef AUDIO_DSP_COMB_FILTER_HPP_INCLUDED
#   define AUDIO_DSP_COMB_FILTER_HPP_INCLUDED

#include <vector>

namespace audio::dsp {

// IIR comb filter with optional damping (primarily used for reverb effect)
struct CombFilter
{
	float feedback;  // feedback/wetness amount (0.0 to 1.0)
	float damping;   // damping factor for one-pole lowpass in feedback (0.0 to 1.0)
	int delay_samples;
	
	std::vector<float> delay_buffer;
	int write_index = 0;
	float filter_state = 0.0f;  // one-pole lowpass state for damping

    CombFilter(int delay_samples, float feedback, float damping = 0.0f);
	CombFilter(float delay_seconds, float sample_rate, float feedback, float damping = 0.0f);

	float process_sample(float input_sample)
	{
		int buffer_size = static_cast<int>(delay_buffer.size());
		int read_index = (write_index - delay_samples + buffer_size) % buffer_size;
		float delayed_sample = delay_buffer[read_index];
		
		// One-pole lowpass filter in feedback path (if damping > 0)
		filter_state = delayed_sample * (1.0f - damping) + filter_state * damping;
		
		// Feedback: y(n) = x(n) + feedback * filtered_y(n - delay)
		float output_sample = input_sample + feedback * filter_state;
		delay_buffer[write_index] = output_sample;

		write_index = (write_index + 1) % buffer_size;
		return output_sample;
	}

	void clear()
	{
		std::fill(delay_buffer.begin(), delay_buffer.end(), 0.0f);
		write_index = 0;
		filter_state = 0.0f;
	}
	
	void set_feedback(float new_feedback) { feedback = new_feedback; }
	void set_damping(float new_damping) { damping = new_damping; }
};

} // namespace audio::dsp

#endif // AUDIO_DSP_COMB_FILTER_HPP_INCLUDED
+1 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@
#include <audio/audio_source.hpp>
#include <audio/audio_delay_effect.hpp>
#include <audio/audio_filter_effect.hpp>
#include <audio/audio_reverb_effect.hpp>
#include <audio/audio_pan_effect.hpp>
#include <audio/audio_volume_change_effect.hpp>

+186 −0
Original line number Diff line number Diff line
#include <audio/audio_reverb_effect.hpp>
#include <audio/dsp/comb_filter.hpp>
#include <audio/dsp/allpass_delay.hpp>
#include <cmath>

namespace audio {

AudioReverbEffect::AudioReverbEffect(std::string const& name)
    : audio::AudioNode(name)
    , m_combs_l{
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f)
    }
    , m_combs_r{
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f),
        dsp::CombFilter(1, 0.84f, 0.2f)
    }
    , m_allpasses_l{
        dsp::AllpassDelay(1, 0.5f),
        dsp::AllpassDelay(1, 0.5f),
        dsp::AllpassDelay(1, 0.5f),
        dsp::AllpassDelay(1, 0.5f)
    }
    , m_allpasses_r{
        dsp::AllpassDelay(1, 0.5f),
        dsp::AllpassDelay(1, 0.5f),
        dsp::AllpassDelay(1, 0.5f),
        dsp::AllpassDelay(1, 0.5f)
    }
    , m_room_size(0.5f)
    , m_damping(0.5f)
    , m_wet(0.33f)
    , m_dry(0.4f)
    , m_width(1.0f)
    , m_freeze(false)
    , m_current_sample_rate(0)
{
    // Filters will be initialized on first process() call with actual sample rate
}

AudioReverbEffect::~AudioReverbEffect()
{
}

void AudioReverbEffect::set_room_size(float room_size)
{
    m_room_size = std::clamp(room_size, 0.0f, 1.0f);
    update_parameters();
}

void AudioReverbEffect::set_damping(float damping)
{
    m_damping = std::clamp(damping, 0.0f, 1.0f);
    update_parameters();
}

void AudioReverbEffect::set_wet(float wet)
{
    m_wet = std::clamp(wet, 0.0f, 1.0f);
}

void AudioReverbEffect::set_dry(float dry)
{
    m_dry = std::clamp(dry, 0.0f, 1.0f);
}

void AudioReverbEffect::set_width(float width)
{
    m_width = std::clamp(width, 0.0f, 1.0f);
}

void AudioReverbEffect::set_freeze(bool freeze)
{
    m_freeze = freeze;
    update_parameters();
}

void AudioReverbEffect::update_parameters()
{
    float feedback = m_freeze ? 1.0f : (SCALE_ROOM * m_room_size + OFFSET_ROOM);
    float damping_scaled = SCALE_DAMPING * m_damping;
    
    // Update all comb filters
    for (int i = 0; i < NUM_COMBS; ++i)
    {
        m_combs_l[i].set_feedback(feedback);
        m_combs_l[i].set_damping(damping_scaled);
        m_combs_r[i].set_feedback(feedback);
        m_combs_r[i].set_damping(damping_scaled);
    }
}

int AudioReverbEffect::scale_delay(int reference_delay, uint32_t sample_rate) const
{
    float scale = static_cast<float>(sample_rate) / REFERENCE_SAMPLE_RATE;
    return static_cast<int>(std::round(static_cast<float>(reference_delay) * scale));
}

void AudioReverbEffect::initialize_filters(uint32_t sample_rate)
{
    // Only initialize if sample rate changed
    if (m_current_sample_rate == sample_rate)
        return;
    
    m_current_sample_rate = sample_rate;
    
    // Initialize comb filters with scaled delays
    for (int i = 0; i < NUM_COMBS; ++i)
    {
        int delay_l = scale_delay(COMB_TUNING_L[i], sample_rate);
        int delay_r = scale_delay(COMB_TUNING_R[i], sample_rate);
        
        m_combs_l[i] = dsp::CombFilter(delay_l, 0.84f, 0.2f);
        m_combs_r[i] = dsp::CombFilter(delay_r, 0.84f, 0.2f);
    }
    
    // Initialize allpass filters with scaled delays
    for (int i = 0; i < NUM_ALLPASSES; ++i)
    {
        int delay_l = scale_delay(ALLPASS_TUNING_L[i], sample_rate);
        int delay_r = scale_delay(ALLPASS_TUNING_R[i], sample_rate);
        
        m_allpasses_l[i] = dsp::AllpassDelay(delay_l, 0.5f);
        m_allpasses_r[i] = dsp::AllpassDelay(delay_r, 0.5f);
    }
    
    // Update parameters to apply room size, damping, etc.
    update_parameters();
}

void AudioReverbEffect::process(std::vector<float>& output_buffer, uint32_t frame_count, uint32_t sample_rate)
{
    // Initialize filters with correct sample rate on first call or if sample rate changed
    initialize_filters(sample_rate);
    
    for (uint32_t i = 0; i < frame_count; ++i)
    {
        float input_l = output_buffer[i * 2];
        float input_r = output_buffer[i * 2 + 1];
        
        // Mix input to mono for reverb processing
        float input_mono = (input_l + input_r) * 0.5f;
        
        // Process through parallel (hence the sum) comb filters
        float comb_out_l = 0.0f;
        float comb_out_r = 0.0f;
        
        for (int c = 0; c < NUM_COMBS; ++c)
        {
            comb_out_l += m_combs_l[c].process_sample(input_mono);
            comb_out_r += m_combs_r[c].process_sample(input_mono);
        }
        
        // Process through series allpass filters
        float allpass_out_l = comb_out_l;
        float allpass_out_r = comb_out_r;
        
        for (int a = 0; a < NUM_ALLPASSES; ++a)
        {
            allpass_out_l = m_allpasses_l[a].process_sample(allpass_out_l);
            allpass_out_r = m_allpasses_r[a].process_sample(allpass_out_r);
        }
        
        // Apply stereo width
        float wet_l = allpass_out_l * (1.0f - m_width * 0.5f) + allpass_out_r * (m_width * 0.5f);
        float wet_r = allpass_out_r * (1.0f - m_width * 0.5f) + allpass_out_l * (m_width * 0.5f);
        
        // Mix wet and dry signals
        output_buffer[i * 2] = input_l * m_dry + wet_l * m_wet;
        output_buffer[i * 2 + 1] = input_r * m_dry + wet_r * m_wet;
    }
}

}
Loading