Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
lpg.hpp
Go to the documentation of this file.
1#pragma once
2
67
70
71#include <algorithm>
72#include <cmath>
73
74namespace pulp::signal {
75
77template <typename SampleType = float> class LpgT {
78 public:
80 static constexpr double kRiseMs = 1.5;
81
88 static constexpr double kStrikePulseMs = 3.0;
89
91 static constexpr SampleType kVcaExponent = SampleType{1.5};
92
93 static constexpr SampleType kDefaultFcMinHz = SampleType{40};
94 static constexpr SampleType kDefaultFcMaxHz = SampleType{12000};
95 static constexpr SampleType kMinFcHz = SampleType{10};
96 static constexpr SampleType kMaxFcHz = SampleType{20000};
97 static constexpr double kDefaultDecayMs = 150.0;
98
103 static constexpr SampleType kDefaultDroop = SampleType{0.5};
104 static constexpr SampleType kMaxDroop = SampleType{0.95};
105
107 static constexpr SampleType kVelocityCurve = SampleType{0.7};
108
111 static SampleType velocity_to_strike(SampleType velocity) {
112 const SampleType v = std::clamp(velocity, SampleType{0}, SampleType{1});
113 return std::pow(v, kVelocityCurve);
114 }
115
116 void prepare(SampleType sample_rate) {
117 filter_.prepare(sample_rate);
118 sample_rate_ = filter_.sample_rate();
119 update_coefficients_();
120 reset();
121 }
122
125 void set_decay_ms(double ms) {
126 decay_ms_ = std::clamp(ms, 1.0, 5000.0);
127 update_coefficients_();
128 }
129
130 void set_rise_ms(double ms) {
131 rise_ms_ = std::clamp(ms, 0.05, 100.0);
132 update_coefficients_();
133 }
134
137 void set_colour(SampleType c) {
138 if (std::isnan(c))
139 c = colour_;
140 colour_ = std::clamp(c, SampleType{0}, SampleType{1});
141 command_cutoff_();
142 }
143
146 void set_range_hz(SampleType min_hz, SampleType max_hz) {
147 min_hz = normalize_cutoff_endpoint_(min_hz, kDefaultFcMinHz);
148 max_hz = normalize_cutoff_endpoint_(max_hz, kDefaultFcMaxHz);
149 if (min_hz > max_hz)
150 std::swap(min_hz, max_hz);
151 fc_min_ = min_hz;
152 fc_max_ = max_hz;
153 log_range_ = std::log(fc_max_ / fc_min_);
154 command_cutoff_();
155 }
156
157 void set_droop(SampleType d) {
158 droop_ = std::clamp(d, SampleType{0}, kMaxDroop);
159 }
160
163 void strike(SampleType level) {
164 pulse_level_ = std::clamp(level, SampleType{0}, SampleType{1});
165 pulse_remaining_ = pulse_samples_;
166 }
167
170 void set_gate(SampleType level) {
171 gate_ = std::clamp(level, SampleType{0}, SampleType{1});
172 }
173
174 void reset() {
175 control_ = SampleType{0};
176 gate_ = SampleType{0};
177 pulse_level_ = SampleType{0};
178 pulse_remaining_ = 0;
179 filter_.reset();
180 command_cutoff_();
181 }
182
183 SampleType process(SampleType input) {
184 advance_control_();
185
186 // Amplitude and brightness are the same control through two different
187 // laws — that coupling is the entire effect.
188 const SampleType amplitude = control_ * std::sqrt(control_); // control^1.5
189 // The commanded cutoff is capped at sample_rate / 4. Above that the
190 // trapezoidal one-pole's step response exceeds one: near-Nyquist
191 // content pumps the integrator state past the input bound, and the
192 // next low-frequency sample reads it back out as a peak above the
193 // input's — up to ~1.5x at 18 kHz / 44.1 kHz. At or below the cap
194 // every state update is a convex combination, so peak out <= peak in
195 // holds unconditionally, which is the no-boost contract the LPG's
196 // consumers (and its own "can only attenuate" gain law) rely on.
197 command_cutoff_();
198
199 const SampleType filtered = filter_.process_lowpass(input);
200 const SampleType gain = amplitude + colour_ * (SampleType{1} - amplitude);
201 return filtered * gain;
202 }
203
206 SampleType control() const {
207 return control_;
208 }
209
213 SampleType commanded_cutoff_hz() const {
214 return commanded_cutoff_hz_;
215 }
216
218 SampleType cutoff_hz() const {
219 return commanded_cutoff_hz();
220 }
221
222 private:
223 static SampleType normalize_cutoff_endpoint_(SampleType hz, SampleType nan_fallback) {
224 if (std::isnan(hz))
225 hz = nan_fallback;
226 return std::clamp(hz, kMinFcHz, kMaxFcHz);
227 }
228
229 void command_cutoff_() {
230 const SampleType filter_control = SampleType{1} - colour_ * (SampleType{1} - control_);
231 const SampleType cutoff = fc_min_ * std::exp(log_range_ * filter_control);
232 filter_.set_cutoff(std::min(cutoff, SampleType{0.25} * sample_rate_));
233 commanded_cutoff_hz_ = filter_.cutoff();
234 }
235
236 void update_coefficients_() {
237 rise_coeff_ = coeff_for_(rise_ms_);
238 fall_coeff_ = coeff_for_(decay_ms_);
239 pulse_samples_ = static_cast<long long>(
240 std::lround(kStrikePulseMs * 0.001 * static_cast<double>(sample_rate_)));
241 if (pulse_samples_ < 1)
242 pulse_samples_ = 1;
243 }
244
245 SampleType coeff_for_(double ms) const {
246 const double samples = std::max(1.0e-6, ms * 0.001 * static_cast<double>(sample_rate_));
247 return static_cast<SampleType>(1.0 - std::exp(-1.0 / samples));
248 }
249
250 void advance_control_() {
251 SampleType target = gate_;
252 if (pulse_remaining_ > 0) {
253 target = std::max(target, pulse_level_);
254 --pulse_remaining_;
255 }
256 if (target > control_) {
257 control_ += rise_coeff_ * (target - control_);
258 } else {
259 // The droop is what makes the close non-exponential: as the cell
260 // darkens the recovery slows, which is the decay contour of struck
261 // wood and skin. droop = 0 recovers the plain first-order model.
262 const SampleType coeff =
263 fall_coeff_ * (SampleType{1} - droop_ * (SampleType{1} - control_));
264 control_ += coeff * (target - control_);
265 }
266 control_ = std::clamp(snap_to_zero(control_), SampleType{0}, SampleType{1});
267 }
268
269 TptFilterT<SampleType> filter_{};
270 SampleType sample_rate_ = SampleType{48000};
271 SampleType control_ = SampleType{0};
272 SampleType commanded_cutoff_hz_ = kDefaultFcMinHz;
273 SampleType gate_ = SampleType{0};
274 SampleType pulse_level_ = SampleType{0};
275 SampleType rise_coeff_ = SampleType{0};
276 SampleType fall_coeff_ = SampleType{0};
277 long long pulse_remaining_ = 0;
278 long long pulse_samples_ = 144;
279 double rise_ms_ = kRiseMs;
280 double decay_ms_ = kDefaultDecayMs;
281 SampleType fc_min_ = kDefaultFcMinHz;
282 SampleType fc_max_ = kDefaultFcMaxHz;
283 SampleType log_range_ = static_cast<SampleType>(5.703782474656201); // log(12000 / 40)
284 SampleType colour_ = SampleType{0.5};
285 SampleType droop_ = kDefaultDroop;
286};
287
290
291} // namespace pulp::signal
Vactrol-modelled low-pass gate.
Definition lpg.hpp:77
static constexpr float kVcaExponent
Definition lpg.hpp:91
SampleType cutoff_hz() const
Compatibility spelling for the cutoff telemetry.
Definition lpg.hpp:218
static constexpr float kMaxDroop
Definition lpg.hpp:104
void set_range_hz(SampleType min_hz, SampleType max_hz)
The effective ceiling is additionally capped at sample_rate / 4 when the cutoff command is applied — ...
Definition lpg.hpp:146
SampleType commanded_cutoff_hz() const
Last cutoff command applied to the owned filter.
Definition lpg.hpp:213
static constexpr float kVelocityCurve
Definition lpg.hpp:107
void reset()
Definition lpg.hpp:174
SampleType control() const
Conditioned cell state, 0..1 — the value both the amplitude and the cutoff are derived from.
Definition lpg.hpp:206
static constexpr float kDefaultFcMinHz
Definition lpg.hpp:93
SampleType process(SampleType input)
Definition lpg.hpp:183
static constexpr float kDefaultFcMaxHz
Definition lpg.hpp:94
static constexpr double kRiseMs
Definition lpg.hpp:80
static SampleType velocity_to_strike(SampleType velocity)
Map a 0..1 velocity to a strike level whose loudness and brightness rise together the way a struck ob...
Definition lpg.hpp:111
static constexpr float kMinFcHz
Definition lpg.hpp:95
static constexpr float kDefaultDroop
Definition lpg.hpp:103
void set_droop(SampleType d)
Definition lpg.hpp:157
void strike(SampleType level)
Ping the cell.
Definition lpg.hpp:163
static constexpr float kMaxFcHz
Definition lpg.hpp:96
static constexpr double kDefaultDecayMs
Definition lpg.hpp:97
static constexpr double kStrikePulseMs
Definition lpg.hpp:88
void set_rise_ms(double ms)
Definition lpg.hpp:130
void set_decay_ms(double ms)
Dark-recovery time.
Definition lpg.hpp:125
void prepare(SampleType sample_rate)
Definition lpg.hpp:116
void set_colour(SampleType c)
0 = pure VCA (the filter stays wide open), 1 = pure filter (the gain stays at unity),...
Definition lpg.hpp:137
void set_gate(SampleType level)
Sustained control level, for idiom 4.
Definition lpg.hpp:170
Definition additive_bank.hpp:218
@ c
Definition spectrum_trace.hpp:35
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
LpgT< float > Lpg
Definition lpg.hpp:288
LpgT< double > Lpg64
Definition lpg.hpp:289
First-order topology-preserving transform (TPT) filter.