Deluge Firmware 1.3.0
Build date: 2026.10.14
Loading...
Searching...
No Matches
fx_engine.hpp
1// Copyright 2023 Katherine Whitlock
2// Heavily based on Mutable Instrument's code, copyright 2014 Emilie Gillet
3// Base class for building reverb.
4
5#pragma once
6#include "cosine_oscillator.hpp"
7#include <algorithm>
8#include <array>
9#include <cstdint>
10#include <span>
11
12namespace deluge::dsp {
13constexpr float OnePole(float& out, float in, float coefficient) {
14 out += (coefficient) * ((in)-out);
15 return out;
16}
17
18template <typename T = float>
19constexpr T Interpolate(const T x0, const T x1, float fractional) {
20 return static_cast<T>(x0 + (x1 - x0) * fractional);
21}
22} // namespace deluge::dsp
23
24namespace deluge::dsp::reverb {
25constexpr static int32_t TAIL = -1;
26
27#if DELUGE_DSP_BOUNDS_CHECK
28namespace debug {
31inline bool out_of_bounds_access = false;
32} // namespace debug
33#endif
34
35enum LFOIndex { LFO_1, LFO_2 };
36
37class FxEngine {
38public:
44 constexpr static float kLFOStride = 32.0f;
45
46 FxEngine(std::span<float> signal, std::array<float, 2> lfo_freqs)
47 : buffer_(signal), lfo_{{lfo_freqs[0] * kLFOStride, lfo_freqs[1] * kLFOStride}}, mask(buffer_.size() - 1) {};
48 ~FxEngine() = default;
49
50 void Clear() {
51 std::fill(buffer_.begin(), buffer_.end(), 0);
52 write_ptr_ = 0;
53 }
54
55 //[gnu::always_inline]
56 void SetLFOFrequency(LFOIndex index, float frequency) { lfo_.SetFrequency(index, frequency * kLFOStride); }
57
58 //[gnu::always_inline]
59 void Advance() {
60 --write_ptr_;
61 if (write_ptr_ < 0) {
62 write_ptr_ += buffer_.size();
63 }
64 StepLFO();
65 }
66
67 //[gnu::always_inline]
68 float& at(size_t index) { return buffer_[(write_ptr_ + index) & mask]; }
69
72 //[gnu::always_inline]
73 float LFO(LFOIndex lfo) {
74 switch (lfo) {
75 case LFO_1:
76 return lfo_.values()[0];
77 case LFO_2:
78 return lfo_.values()[1];
79 }
80 __builtin_unreachable();
81 }
82
83private:
84 //[gnu::always_inline]
85 void StepLFO() {
86 if ((write_ptr_ & 31) == 0) {
87 lfo_.Next();
88 }
89 }
90
91 int32_t write_ptr_ = 0;
92 std::span<float> buffer_;
94
95 size_t mask;
96
97public: /******************** INNER CLASSES ****************/
98 class Context {
99 public:
100 [[nodiscard]] constexpr float Get() const { return accumulator_; }
101 constexpr void Set(float value) { accumulator_ = value; }
102 constexpr void Add(float value) { accumulator_ += value; }
103 constexpr void Multiply(float value) { accumulator_ *= value; }
104 constexpr void Reset() { Set(0); }
105
106 constexpr void Lp(float& state, float coefficient) { accumulator_ = OnePole(state, accumulator_, coefficient); }
107
108 constexpr void Hp(float& state, float coefficient) {
109 accumulator_ -= OnePole(state, accumulator_, coefficient);
110 }
111
112 private:
113 float accumulator_ = 0.f;
114 };
115
116 struct DelayLine {
117 DelayLine(size_t length) : length(length) {};
118
119 // Store and Fetch
120 //[gnu::always_inline]
121 void Process(Context& c, size_t offset = 0) {
122 this->at(offset) = c.Get();
123 c.Set(this->at(length - offset));
124 }
125
126 //[gnu::always_inline]
127 float& at(int32_t index) {
128 if (index == TAIL) {
129 index = length - 1;
130 }
131#if DELUGE_DSP_BOUNDS_CHECK
132 // ConstructTopology reserves length + 1 slots per line, so indices 0..length are ours:
133 // index `length` is the slot written `length` frames ago. Anything past that is the
134 // next line's memory. Interpolated reads are the usual culprit, since they also
135 // touch offset + 1.
136 if (index < 0 || static_cast<size_t>(index) > length) {
137 debug::out_of_bounds_access = true;
138 }
139#endif
140 return engine_->at(this->base + index);
141 }
142
143 //[gnu::always_inline]
144 float Read(int32_t offset) {
145 // STATIC_ASSERT(d.base + d.length <= size, delay_memory_full);
146 return this->at(offset);
147 }
148
149 //[gnu::always_inline]
150 float Interpolate(float offset) {
151 auto offset_integral = static_cast<int32_t>(offset);
152 float offset_fractional = offset - static_cast<float>(offset_integral);
153 const float a = this->at(offset_integral);
154 const float b = this->at(offset_integral + 1);
155 return dsp::Interpolate(a, b, offset_fractional);
156 }
157
159 //[gnu::always_inline]
160 void Write(int32_t offset, float value) { this->at(offset) = value; }
161
162 public:
163 const size_t length = 0;
164 size_t base = 0;
165 FxEngine* engine_ = nullptr;
166 };
167
168 struct AllPass : public DelayLine {
169 AllPass(size_t length) : DelayLine(length) {};
170
171 //[gnu::always_inline]
172 float Read(Context& c, int32_t offset, float scale) {
173 const float r = DelayLine::Read(offset);
174 c.Add(r * scale);
175 return r;
176 }
177
178 // inline float Read(Context& c, float scale) { return Read(c, scale); }
179
180 //[gnu::always_inline]
181 void Write(Context& c, int32_t offset, float scale) {
182 DelayLine::Write(offset, c.Get());
183 c.Multiply(scale);
184 }
185
186 //[gnu::always_inline]
187 void Write(Context& c, float scale) { Write(c, (int32_t)0, scale); }
188
189 //[gnu::always_inline]
190 void Write(Context& c, int32_t offset, float scale, float input) {
191 Write(c, offset, scale);
192 c.Add(input);
193 }
194
195 //[gnu::always_inline]
196 void Write(Context& c, float scale, float input) { Write(c, 0, scale, input); }
197
199 //[gnu::always_inline]
200 float Interpolate(Context& c, float offset, float scale) {
201 // STATIC_ASSERT(d.base + d.length <= size, delay_memory_full);
202 const float r = DelayLine::Interpolate(offset);
203 c.Add(r * scale);
204 return r;
205 }
206
207 //[gnu::always_inline]
208 float Interpolate(Context& c, float offset, LFOIndex index, float amplitude, float scale) {
209 offset += amplitude * this->engine_->LFO(index);
210 return Interpolate(c, offset, scale);
211 }
212
213 //[gnu::always_inline]
214 void ProcessInterpolate(Context& c, float offset, LFOIndex index, float amplitude, float scale) {
215 const float read = this->Interpolate(c, offset, index, amplitude, scale);
216 this->Write(c, 0, -scale, read);
217 }
218
219 // Simple Schroeder allpass section
220 //
221 // ------[*-scale]-----,
222 // | ----------- ∨
223 // ---+-'-->| Delayline |--,-+--->
224 // ∧ ----------- |
225 // '------[*scale]------
226 //
227 //[gnu::always_inline]
228 void Process(Context& c, float scale) {
229 const float head = c.Get();
230 const float tail = this->at(TAIL);
231
232 const float feedback = head + (tail * scale);
233 this->at(0) = feedback; // feedback into delayline
234
235 const float feedforward = (feedback * -scale) + tail;
236 c.Set(feedforward); // output into pipeline
237
238 // c.Add(tail * scale);
239 // this->at(0) = c.Get();
240
241 // c.Multiply(-scale);
242 // c.Add(tail);
243 }
244 };
245
246 static void ConstructTopology(FxEngine& e, std::initializer_list<DelayLine*> delays) {
247 size_t base = 0;
248 for (DelayLine* d : delays) {
249 d->engine_ = &e;
250 d->base = base;
251 base += d->length + 1;
252 }
253 }
254};
255
256} // namespace deluge::dsp::reverb
Definition cosine_oscillator.hpp:11
Definition fx_engine.hpp:98
float LFO(LFOIndex lfo)
Definition fx_engine.hpp:73
static constexpr float kLFOStride
Definition fx_engine.hpp:44
float Interpolate(Context &c, float offset, float scale)
Can be used in place of any AllPass::Read calls.
Definition fx_engine.hpp:200
void Write(int32_t offset, float value)
writes to the delayline
Definition fx_engine.hpp:160