Deluge Firmware
1.3.0
Build date: 2026.10.14
Toggle main menu visibility
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
12
namespace
deluge::dsp {
13
constexpr
float
OnePole(
float
& out,
float
in,
float
coefficient) {
14
out += (coefficient) * ((in)-out);
15
return
out;
16
}
17
18
template
<
typename
T =
float
>
19
constexpr
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
24
namespace
deluge::dsp::reverb {
25
constexpr
static
int32_t TAIL = -1;
26
27
#if DELUGE_DSP_BOUNDS_CHECK
28
namespace
debug {
31
inline
bool
out_of_bounds_access =
false
;
32
}
// namespace debug
33
#endif
34
35
enum
LFOIndex { LFO_1, LFO_2 };
36
37
class
FxEngine {
38
public
:
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
83
private
:
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_;
93
DualCosineOscillator
lfo_;
94
95
size_t
mask;
96
97
public
:
/******************** 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
DualCosineOscillator
Definition
cosine_oscillator.hpp:11
deluge::dsp::reverb::FxEngine::Context
Definition
fx_engine.hpp:98
deluge::dsp::reverb::FxEngine::LFO
float LFO(LFOIndex lfo)
Definition
fx_engine.hpp:73
deluge::dsp::reverb::FxEngine::kLFOStride
static constexpr float kLFOStride
Definition
fx_engine.hpp:44
deluge::dsp::reverb::FxEngine::AllPass::Interpolate
float Interpolate(Context &c, float offset, float scale)
Can be used in place of any AllPass::Read calls.
Definition
fx_engine.hpp:200
deluge::dsp::reverb::FxEngine::DelayLine::Write
void Write(int32_t offset, float value)
writes to the delayline
Definition
fx_engine.hpp:160