diff --git a/CMakeLists.txt b/CMakeLists.txt index e84b1434..3e381824 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ Source/Effects/chorus.cpp Source/Effects/decimator.cpp Source/Effects/flanger.cpp Source/Effects/fold.cpp +Source/Effects/Freeverb.cpp Source/Effects/overdrive.cpp Source/Effects/reverbsc.cpp Source/Effects/sampleratereducer.cpp @@ -79,6 +80,9 @@ Source/Effects/bitcrush.h Source/Effects/chorus.h Source/Effects/decimator.h Source/Effects/flanger.h +Source/Effects/Freeverb.h +Source/Effects/FvAllpass.h +Source/Effects/FvComb.h Source/Effects/fold.h Source/Effects/overdrive.h Source/Effects/reverbsc.h diff --git a/Makefile b/Makefile index 20df5cb9..e3122ab3 100644 --- a/Makefile +++ b/Makefile @@ -35,11 +35,14 @@ bitcrush \ chorus \ decimator \ flanger \ +Freeverb \ fold \ overdrive \ reverbsc \ sampleratereducer \ tremolo +#FvComb +#FvAllpass #pitchshifter FILTER_MOD_DIR = Filters diff --git a/Source/Effects/Freeverb.cpp b/Source/Effects/Freeverb.cpp new file mode 100644 index 00000000..53ba4ccb --- /dev/null +++ b/Source/Effects/Freeverb.cpp @@ -0,0 +1,186 @@ +#include "Freeverb.h" + + +using namespace daisysp; + +void Freeverb::Init() +{ + /** Init all the buffs */ + combL[0].Init(bufcombL1, kCombTuningL1); + combR[0].Init(bufcombR1, kCombTuningR1); + combL[1].Init(bufcombL2, kCombTuningL2); + combR[1].Init(bufcombR2, kCombTuningR2); + combL[2].Init(bufcombL3, kCombTuningL3); + combR[2].Init(bufcombR3, kCombTuningR3); + combL[3].Init(bufcombL4, kCombTuningL4); + combR[3].Init(bufcombR4, kCombTuningR4); + combL[4].Init(bufcombL5, kCombTuningL5); + combR[4].Init(bufcombR5, kCombTuningR5); + combL[5].Init(bufcombL6, kCombTuningL6); + combR[5].Init(bufcombR6, kCombTuningR6); + combL[6].Init(bufcombL7, kCombTuningL7); + combR[6].Init(bufcombR7, kCombTuningR7); + combL[7].Init(bufcombL8, kCombTuningL8); + combR[7].Init(bufcombR8, kCombTuningR8); + allpassL[0].Init(bufallpassL1, kAllpassTuningL1); + allpassR[0].Init(bufallpassR1, kAllpassTuningR1); + allpassL[1].Init(bufallpassL2, kAllpassTuningL2); + allpassR[1].Init(bufallpassR2, kAllpassTuningR2); + allpassL[2].Init(bufallpassL3, kAllpassTuningL3); + allpassR[2].Init(bufallpassR3, kAllpassTuningR3); + allpassL[3].Init(bufallpassL4, kAllpassTuningL4); + allpassR[3].Init(bufallpassR4, kAllpassTuningR4); + + // Set default values + allpassL[0].SetFeedback(0.5f); + allpassR[0].SetFeedback(0.5f); + allpassL[1].SetFeedback(0.5f); + allpassR[1].SetFeedback(0.5f); + allpassL[2].SetFeedback(0.5f); + allpassR[2].SetFeedback(0.5f); + allpassL[3].SetFeedback(0.5f); + allpassR[3].SetFeedback(0.5f); + SetWet(kInitialwet); + SetRoomSize(kInitialroom); + SetDry(kInitialdry); + SetDamp(kInitialdamp); + SetWidth(kInitialwidth); + SetMode(kInitialmode); + // Clear the buffers + Mute(); +} +void Freeverb::Mute() +{ + if(GetMode() >= kFreezeMode) + return; + for(int i = 0; i < kNumCombs; i++) + { + combL[i].Mute(); + combR[i].Mute(); + } + for(int i = 0; i < kNumAllpass; i++) + { + allpassL[i].Mute(); + allpassR[i].Mute(); + } +} +void Freeverb::Process(float *inl, float *inr, float *outl, float *outr) +{ + float left, right, input; + + left = right = 0.f; + input = (*inl + *inr) * gain_; + + // Parallel comb accumulation + for(int i = 0; i < kNumCombs; i++) + { + left += combL[i].Process(input); + right += combR[i].Process(input); + } + // Series allpass + for(int i = 0; i < kNumAllpass; i++) + { + left = allpassL[i].Process(left); + right = allpassR[i].Process(right); + } + // Calc replacement + *outl = left * wet1_ + right * wet2_ + *inl * dry_; + *outr = right * wet1_ + left * wet2_ + *inr * dry_; +} +void Freeverb::ProcessBlockReplace(float *inl, + float *inr, + float *outl, + float *outr, + int size, + int skip) +{ + float left, right, input; + while(size--) + { + left = right = 0.f; + input = (*inl + *inr) * gain_; + + // Parallel comb accumulation + for(int i = 0; i < kNumCombs; i++) + { + left += combL[i].Process(input); + right += combR[i].Process(input); + } + // Series allpass + for(int i = 0; i < kNumAllpass; i++) + { + left = allpassL[i].Process(left); + right = allpassR[i].Process(right); + } + // Calc replacement + *outl = left * wet1_ + right * wet2_ + *inl * dry_; + *outr = right * wet1_ + left * wet2_ + *inr * dry_; + // Increment buffers + *inl += skip; + *inr += skip; + *outl += skip; + *outr += skip; + } +} +void Freeverb::ProcessBlockMix(float *inl, + float *inr, + float *outl, + float *outr, + int size, + int skip) +{ + float left, right, input; + while(size--) + { + left = right = 0.f; + input = (*inl + *inr) * gain_; + + // Parallel comb accumulation + for(int i = 0; i < kNumCombs; i++) + { + left += combL[i].Process(input); + right += combR[i].Process(input); + } + // Series allpass + for(int i = 0; i < kNumAllpass; i++) + { + left = allpassL[i].Process(left); + right = allpassR[i].Process(right); + } + // Calcmixin + *outl = left * wet1_ + right * wet2_ + *inl * dry_; + *outr = right * wet1_ + left * wet2_ + *inr * dry_; + // Increment buffers + *inl += skip; + *inr += skip; + *outl += skip; + *outr += skip; + } +} + +void Freeverb::Update() +{ + wet1_ = wet_ * (width_ / 2.f + 0.5f); + if(mode_ > kFreezeMode) + { + roomsize1_ = 1.f; + damp1_ = 0; + gain_ = kMuted; + } + else + { + roomsize1_ = roomsize_; + damp1_ = damp_; + gain_ = kFixedGain; + } + for(int i = 0; i < kNumCombs; i++) + { + combL[i].SetFeedback(roomsize1_); + combR[i].SetFeedback(roomsize1_); + } + for(int i = 0; i < kNumCombs; i++) + { + combL[i].SetDamp(damp1_); + combR[i].SetDamp(damp1_); + } +} diff --git a/Source/Effects/Freeverb.h b/Source/Effects/Freeverb.h new file mode 100644 index 00000000..03c653ee --- /dev/null +++ b/Source/Effects/Freeverb.h @@ -0,0 +1,198 @@ +#pragma once +#ifndef DSY_FREEVERB_H +#define DSY_FREEVERB_H + +#include "FvAllpass.h" +#include "FvComb.h" + +namespace daisysp +{ +/** Tuning Specific settings -- should be configurable */ +/** Considering either making this a separate header or just a config struct that could be used as a template parameter */ + +const int kNumCombs = 8; +const int kNumAllpass = 4; +const float kMuted = 0; +const float kFixedGain = 0.015f; +//const float kScaleWet = 3; +const float kScaleWet = 4; +const float kScaleDry = 2; +const float kScaledamp = 0.4f; +const float kScaleroom = 0.28f; +const float kOffsetroom = 0.7f; +const float kInitialroom = 0.5f; +const float kInitialdamp = 0.5f; +const float kInitialwet = 1 / kScaleWet; +const float kInitialdry = 0; +const float kInitialwidth = 1; +const float kInitialmode = 0; +const float kFreezeMode = 0.5f; +const int kStereoSpread = 23; + +constexpr float kTargetSr = 96000.f; +constexpr float kOriginalSr = 44100.f; +constexpr float kRetuneCoeff = (kTargetSr / kOriginalSr); + +// These values assume 44.1KHz sample rate +// they will probably be OK for 48KHz sample rate +// but would need scaling for 96KHz (or other) sample rates. +// The values were obtained by listening tests. + +/** Freeverb Implementation + * based on original C++ sources from Jezar at Dreampoint, June 2000 + * + */ +class Freeverb +{ + public: + Freeverb() {} + ~Freeverb() {} + /** Initialize the internal bits */ + void Init(); + + /** Clear all of the filters and stop the reverb. */ + void Mute(); + + /** Process a single sample replacing the output */ + void Process(float *inl, float *inr, float *outl, float *outr); + + /** Processes a block of audio, skip can be used if necessary. + The contents of *outN are replaced by the new signal */ + void ProcessBlockReplace(float *inl, + float *inr, + float *outl, + float *outr, + int size, + int skip = 1); + + /** Processes a block of audio, skip can be used if necessary. + The contents of *outN are mixed with by the new signal */ + void ProcessBlockMix(float *inl, + float *inr, + float *outl, + float *outr, + int size, + int skip = 1); + + void SetRoomSize(float val) + { + roomsize_ = (val * kScaleroom) + kOffsetroom; + Update(); + } + inline float GetRoomSize() + { + return (roomsize_ - kOffsetroom) / kScaleroom; + } + + void SetDamp(float val) + { + damp_ = val * kScaledamp; + Update(); + } + const inline float GetDamp() const { return damp_ / kScaledamp; } + + void SetWet(float val) + { + wet_ = val * kScaleWet; + Update(); + } + const inline float GetWet() const { return wet_ / kScaleWet; } + + void SetDry(float val) + { + dry_ = val * kScaleDry; + Update(); + } + const inline float GetDry() const { return dry_ / kScaleDry; } + + void SetWidth(float val) + { + width_ = val; + Update(); + } + + const inline float GetWidth() const { return width_; } + + void SetMode(float val) + { + mode_ = val; + Update(); + } + const inline float GetMode() const + { + return mode_ > kFreezeMode ? 1.f : 0.f; + } + + + private: + static constexpr int kCombTuningL1 = 1116 * kRetuneCoeff; + static constexpr int kCombTuningR1 = (1116 + kStereoSpread) * kRetuneCoeff; + static constexpr int kCombTuningL2 = 1188 * kRetuneCoeff; + static constexpr int kCombTuningR2 = (1188 + kStereoSpread) * kRetuneCoeff; + static constexpr int kCombTuningL3 = 1277 * kRetuneCoeff; + static constexpr int kCombTuningR3 = (1277 + kStereoSpread) * kRetuneCoeff; + static constexpr int kCombTuningL4 = 1356 * kRetuneCoeff; + static constexpr int kCombTuningR4 = (1356 + kStereoSpread) * kRetuneCoeff; + static constexpr int kCombTuningL5 = 1422 * kRetuneCoeff; + static constexpr int kCombTuningR5 = (1422 + kStereoSpread) * kRetuneCoeff; + static constexpr int kCombTuningL6 = 1491 * kRetuneCoeff; + static constexpr int kCombTuningR6 = (1491 + kStereoSpread) * kRetuneCoeff; + static constexpr int kCombTuningL7 = 1557 * kRetuneCoeff; + static constexpr int kCombTuningR7 = (1557 + kStereoSpread) * kRetuneCoeff; + static constexpr int kCombTuningL8 = 1617 * kRetuneCoeff; + static constexpr int kCombTuningR8 = (1617 + kStereoSpread) * kRetuneCoeff; + static constexpr int kAllpassTuningL1 = 556 * kRetuneCoeff; + static constexpr int kAllpassTuningR1 + = (556 + kStereoSpread) * kRetuneCoeff; + static constexpr int kAllpassTuningL2 = 441 * kRetuneCoeff; + static constexpr int kAllpassTuningR2 + = (441 + kStereoSpread) * kRetuneCoeff; + static constexpr int kAllpassTuningL3 = 341 * kRetuneCoeff; + static constexpr int kAllpassTuningR3 + = (341 + kStereoSpread) * kRetuneCoeff; + static constexpr int kAllpassTuningL4 = 225 * kRetuneCoeff; + static constexpr int kAllpassTuningR4 + = (225 + kStereoSpread) * kRetuneCoeff; + + void Update(); + float gain_, roomsize_, roomsize1_; + float damp_, damp1_; + float wet_, wet1_, wet2_; + float dry_; + float width_, mode_; + + FvComb combL[kNumCombs], combR[kNumCombs]; + FvAllpass allpassL[kNumAllpass], allpassR[kNumAllpass]; + + /** Buffers for combs */ + float bufcombL1[kCombTuningL1]; + float bufcombR1[kCombTuningR1]; + float bufcombL2[kCombTuningL2]; + float bufcombR2[kCombTuningR2]; + float bufcombL3[kCombTuningL3]; + float bufcombR3[kCombTuningR3]; + float bufcombL4[kCombTuningL4]; + float bufcombR4[kCombTuningR4]; + float bufcombL5[kCombTuningL5]; + float bufcombR5[kCombTuningR5]; + float bufcombL6[kCombTuningL6]; + float bufcombR6[kCombTuningR6]; + float bufcombL7[kCombTuningL7]; + float bufcombR7[kCombTuningR7]; + float bufcombL8[kCombTuningL8]; + float bufcombR8[kCombTuningR8]; + + // Buffers for the allpasses + float bufallpassL1[kAllpassTuningL1]; + float bufallpassR1[kAllpassTuningR1]; + float bufallpassL2[kAllpassTuningL2]; + float bufallpassR2[kAllpassTuningR2]; + float bufallpassL3[kAllpassTuningL3]; + float bufallpassR3[kAllpassTuningR3]; + float bufallpassL4[kAllpassTuningL4]; + float bufallpassR4[kAllpassTuningR4]; +}; + +} // namespace daisysp + +#endif \ No newline at end of file diff --git a/Source/Effects/FvAllpass.h b/Source/Effects/FvAllpass.h new file mode 100644 index 00000000..30a35b0a --- /dev/null +++ b/Source/Effects/FvAllpass.h @@ -0,0 +1,66 @@ +#pragma once +#ifndef DSY_FV_ALLPASS_H +#define DSY_FV_ALLPASS_H +#include "Utility/dsp.h" + + +namespace daisysp +{ +/** Allpass utility block for Freeverb. + Consider merging with previous allpass or replace if functionally similar + */ +class FvAllpass +{ + public: + FvAllpass() {} + ~FvAllpass() {} + + void Init(float *buf, size_t size) + { + buffer_ = buf; + size_ = size; + idx_ = 0; + Mute(); + fb_ = 0.7f; + } + + inline float Process(float input) + { + float output, bufout; + output = 0.f; + bufout = buffer_[idx_]; + denormalize(bufout); + output = -input + bufout; + buffer_[idx_] = input + (bufout * fb_); + idx_ += 1; + if(idx_ >= size_) + idx_ = 0; + return output; + } + + /** Clears internal buffer */ + void Mute() + { + for(int i = 0; i < size_; i++) + { + buffer_[i] = 0.f; + } + } + + /*** Sets the amount of feedback through the allpass */ + void SetFeedback(float val) { fb_ = val; } + + /** Returns the current feedback value. */ + const inline float GetFeedback() const { return fb_; } + + + private: + float fb_; + float *buffer_; + int size_, idx_; +}; + +} // namespace daisysp + + +#endif diff --git a/Source/Effects/FvComb.h b/Source/Effects/FvComb.h new file mode 100644 index 00000000..d6df9023 --- /dev/null +++ b/Source/Effects/FvComb.h @@ -0,0 +1,78 @@ +#pragma once +#ifndef DSY_FV_COMB_H +#define DSY_FV_COMB_H +#include "Utility/dsp.h" + + +namespace daisysp +{ +/** Comb utility block for Freeverb. + Consider merging with previous comb or replace if functionally similar + Lowpass Feedback Comb Filter + */ +class FvComb +{ + public: + FvComb() {} + ~FvComb() {} + + void Init(float *buf, int size) + { + buffer_ = buf; + size_ = size; + Mute(); + idx_ = 0; + fb_ = 0.5f; + prev_ = 0.f; + damp1_ = 0.3f; + damp2_ = 0.7f; + } + + void Mute() + { + for(int i = 0; i < size_; i++) + { + buffer_[i] = 0.f; + } + } + + inline float Process(float input) + { + float output; + output = buffer_[idx_]; + denormalize(output); + prev_ = (output * damp2_) + (prev_ * damp1_); + denormalize(prev_); + buffer_[idx_] = input + (prev_ * fb_); + idx_ += 1; + if(idx_ >= size_) + idx_ = 0; + return output; + } + + /*** Sets the amount of feedback through the allpass */ + void SetFeedback(float val) { fb_ = val; } + + /** Returns the current feedback value. */ + const inline float GetFeedback() const { return fb_; } + + /*** Sets the amount of feedback through the allpass */ + void SetDamp(float val) + { + damp1_ = val; + damp2_ = 1.f - val; + } + + /** Returns the current feedback value. */ + const inline float GetDamp() const { return damp1_; } + + private: + float fb_, prev_, damp1_, damp2_; + float *buffer_; + int size_, idx_; +}; + +} // namespace daisysp + + +#endif diff --git a/Source/Utility/dsp.h b/Source/Utility/dsp.h index fb5b9dcd..47213628 100644 --- a/Source/Utility/dsp.h +++ b/Source/Utility/dsp.h @@ -121,6 +121,14 @@ inline float mtof(float m) return powf(2, (m - 69.0f) / 12.0f) * 440.0f; } +/** Denormalize small floats in place + * Based on undernormalize macro by Jezar based on IS_DENORMAL macro by Jon Watte + */ +inline void denormalize(float &sample) +{ + if(((*(unsigned int *)&sample) & 0x7f800000) == 0) + sample = 0.f; +} /** one pole lpf out is passed by reference, and must be retained between diff --git a/Source/daisysp.h b/Source/daisysp.h index 7dec0e1d..61ef80eb 100644 --- a/Source/daisysp.h +++ b/Source/daisysp.h @@ -40,6 +40,7 @@ #include "Effects/chorus.h" #include "Effects/decimator.h" #include "Effects/flanger.h" +#include "Effects/Freeverb.h" #include "Effects/fold.h" #include "Effects/overdrive.h" #include "Effects/reverbsc.h"