Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
output_stage.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstddef>
10
11namespace pulp::signal::drum {
12
21 bypass = 1,
22 x2 = 2,
23 x4 = 4,
24};
25
41template <typename SampleType = float>
43public:
44 void prepare(double sample_rate) {
45 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
46 // This is the established house half-band configuration: 65 taps,
47 // Kaiser beta ~= 8 (81.3 dB design), with the upper stage keeping the
48 // same absolute passband when it runs at twice stage one's rate.
49 stage_a_.configure(kHouseHalfBandStageOnePassband,
52 stage_b_.configure(kHouseHalfBandStageTwoPassband,
55 update_internal_rate();
56 reset();
57 }
58
59 void reset() {
60 lofi_.reset();
61 stage_a_.reset();
62 stage_b_.reset();
63 tail_samples_remaining_ = 0;
64 nonlinear_activity_this_sample_ = false;
65 ahd_triggers_.fill(false);
66 ahd_trigger_cursor_ = 0;
67 ahd_phase_ = AhdPhase::inactive;
68 ahd_gain_ = ahd_enabled_ ? 0.0 : 1.0;
69 }
70
73 void reset_nonlinear_state() { lofi_.reset(); }
74
79 if (factor == oversampling_) return;
80 oversampling_ = factor;
81 update_internal_rate();
82 reset();
83 }
84
85 OutputOversampling oversampling() const noexcept { return oversampling_; }
86
96
97 static constexpr int latency_samples_for(
98 OutputOversampling factor) noexcept {
99 switch (factor) {
100 case OutputOversampling::bypass: return 0;
101 case OutputOversampling::x2: return halfband_latency_samples();
103 return halfband_latency_samples() +
104 halfband_latency_samples() / 2;
105 }
106 return 0;
107 }
108
111 int latency_samples() const noexcept {
112 return latency_samples_for(oversampling_);
113 }
114
118 bool has_tail() const noexcept {
119 return tail_samples_remaining_ > 0 || lofi_.has_tail();
120 }
121
125 void set_drive(double amount) { drive_ = std::clamp(amount, 0.0, 1.0); }
126
128 void set_fold(double amount) { fold_ = std::clamp(amount, 0.0, 1.0); }
129
131 void set_level(double level) { level_ = std::max(level, 0.0); }
132
136 void set_ahd_ms(double attack_ms, double hold_ms, double decay_ms) {
137 ahd_attack_ms_ = std::max(attack_ms, 0.0);
138 ahd_hold_ms_ = std::max(hold_ms, 0.0);
139 ahd_decay_ms_ = std::max(decay_ms, 0.01);
140 ahd_enabled_ = true;
141 update_ahd_times();
142 }
143
144 void set_ahd_enabled(bool enabled) {
145 ahd_enabled_ = enabled;
146 if (!enabled) {
147 ahd_triggers_.fill(false);
148 ahd_phase_ = AhdPhase::inactive;
149 ahd_gain_ = 1.0;
150 }
151 }
152
153 bool ahd_enabled() const noexcept { return ahd_enabled_; }
154 double ahd_gain() const noexcept { return ahd_gain_; }
155
160 if (oversampling_ != other.oversampling_) {
161 oversampling_ = other.oversampling_;
162 update_internal_rate();
163 reset();
164 }
165 drive_ = other.drive_;
166 fold_ = other.fold_;
167 level_ = other.level_;
168 lofi_.sync_configuration_from(other.lofi_);
169 if (ahd_attack_ms_ != other.ahd_attack_ms_ ||
170 ahd_hold_ms_ != other.ahd_hold_ms_ ||
171 ahd_decay_ms_ != other.ahd_decay_ms_) {
172 ahd_attack_ms_ = other.ahd_attack_ms_;
173 ahd_hold_ms_ = other.ahd_hold_ms_;
174 ahd_decay_ms_ = other.ahd_decay_ms_;
175 update_ahd_times();
176 }
177 if (ahd_enabled_ != other.ahd_enabled_)
178 set_ahd_enabled(other.ahd_enabled_);
179 }
180
182 void trigger() {
183 if (!ahd_enabled_) {
184 ahd_triggers_.fill(false);
185 ahd_phase_ = AhdPhase::inactive;
186 ahd_gain_ = 1.0;
187 return;
188 }
189 const int delay = latency_samples();
190 if (delay > 0) {
191 const int size = static_cast<int>(ahd_triggers_.size());
192 const int arrival = (ahd_trigger_cursor_ + delay) % size;
193 ahd_triggers_[static_cast<std::size_t>(arrival)] = true;
194 return;
195 }
196 start_ahd();
197 }
198
201 LofiChainT<SampleType>& lofi() { return lofi_; }
202 const LofiChainT<SampleType>& lofi() const { return lofi_; }
203
204 SampleType process(SampleType input) {
205 nonlinear_activity_this_sample_ = false;
206 SampleType processed = input;
207 switch (oversampling_) {
209 processed = process_nonlinear(input);
210 break;
212 SampleType even{};
213 SampleType odd{};
214 stage_a_.upsample(input, even, odd);
215 processed =
216 stage_a_.downsample(process_nonlinear(even), process_nonlinear(odd));
217 break;
218 }
220 SampleType even{};
221 SampleType odd{};
222 stage_a_.upsample(input, even, odd);
223 processed = stage_a_.downsample(process_inner_x2(even),
224 process_inner_x2(odd));
225 break;
226 }
227 }
228
229 if (nonlinear_activity_this_sample_ && latency_samples() > 0) {
230 tail_samples_remaining_ =
231 static_cast<std::size_t>(2 * latency_samples() + 1);
232 } else if (tail_samples_remaining_ > 0) {
233 --tail_samples_remaining_;
234 }
235 const double envelope = process_ahd();
236 return static_cast<SampleType>(
237 static_cast<double>(processed) * level_ * envelope);
238 }
239
240private:
241 static constexpr int halfband_latency_samples() noexcept {
242 return static_cast<int>((kHouseHalfBandTaps - 1u) / 2u);
243 }
244
245 int factor_value() const noexcept {
246 return static_cast<int>(oversampling_);
247 }
248
249 void update_internal_rate() {
250 lofi_.set_sample_rate(sample_rate_ * static_cast<double>(factor_value()));
251 update_ahd_times();
252 }
253
254 SampleType process_inner_x2(SampleType input) {
255 SampleType even{};
256 SampleType odd{};
257 stage_b_.upsample(input, even, odd);
258 return stage_b_.downsample(process_nonlinear(even), process_nonlinear(odd));
259 }
260
261 SampleType process_nonlinear(SampleType input) {
262 double x = static_cast<double>(input);
263 if (fold_ > 0.0) {
264 const double k = 1.0 + fold_ * 4.0;
265 x = std::sin(0.5 * 3.14159265358979323846 * k * x);
266 }
267 if (drive_ > 0.0) {
268 x = std::tanh((1.0 + drive_ * 9.0) * x);
269 }
270 x = static_cast<double>(lofi_.process(static_cast<SampleType>(x)));
271 nonlinear_activity_this_sample_ |= std::abs(x) > 1e-20;
272 return static_cast<SampleType>(x);
273 }
274
275 enum class AhdPhase {
276 inactive,
277 attack,
278 hold,
279 decay,
280 };
281
282 void update_ahd_times() {
283 ahd_attack_samples_ =
284 static_cast<int>(0.001 * ahd_attack_ms_ * sample_rate_);
285 ahd_hold_samples_ =
286 static_cast<int>(0.001 * ahd_hold_ms_ * sample_rate_);
287 ahd_decay_samples_ =
288 std::max(1, static_cast<int>(0.001 * ahd_decay_ms_ * sample_rate_));
289 ahd_attack_step_ =
290 ahd_attack_samples_ > 0 ? 1.0 / ahd_attack_samples_ : 1.0;
291 ahd_decay_multiplier_ =
292 std::pow(1e-6, 1.0 / static_cast<double>(ahd_decay_samples_));
293 }
294
295 void begin_ahd_decay() {
296 ahd_phase_ = AhdPhase::decay;
297 ahd_remaining_ = ahd_decay_samples_;
298 ahd_gain_ = 1.0;
299 }
300
301 void start_ahd() {
302 if (ahd_attack_samples_ > 0) {
303 ahd_phase_ = AhdPhase::attack;
304 ahd_remaining_ = ahd_attack_samples_;
305 ahd_gain_ = 0.0;
306 } else if (ahd_hold_samples_ > 0) {
307 ahd_phase_ = AhdPhase::hold;
308 ahd_remaining_ = ahd_hold_samples_;
309 ahd_gain_ = 1.0;
310 } else {
311 begin_ahd_decay();
312 }
313 }
314
315 double process_ahd() {
316 if (!ahd_enabled_) return 1.0;
317 if (ahd_triggers_[static_cast<std::size_t>(ahd_trigger_cursor_)]) {
318 ahd_triggers_[static_cast<std::size_t>(ahd_trigger_cursor_)] =
319 false;
320 start_ahd();
321 }
322 const double current =
323 ahd_phase_ == AhdPhase::inactive ? 0.0 : ahd_gain_;
324 switch (ahd_phase_) {
325 case AhdPhase::inactive:
326 break;
327 case AhdPhase::attack:
328 ahd_gain_ = std::min(ahd_gain_ + ahd_attack_step_, 1.0);
329 if (--ahd_remaining_ <= 0) {
330 ahd_gain_ = 1.0;
331 if (ahd_hold_samples_ > 0) {
332 ahd_phase_ = AhdPhase::hold;
333 ahd_remaining_ = ahd_hold_samples_;
334 } else {
335 begin_ahd_decay();
336 }
337 }
338 break;
339 case AhdPhase::hold:
340 if (--ahd_remaining_ <= 0) begin_ahd_decay();
341 break;
342 case AhdPhase::decay:
343 ahd_gain_ *= ahd_decay_multiplier_;
344 if (--ahd_remaining_ <= 0) {
345 ahd_gain_ = 0.0;
346 ahd_phase_ = AhdPhase::inactive;
347 }
348 break;
349 }
350 ahd_trigger_cursor_ =
351 (ahd_trigger_cursor_ + 1) %
352 static_cast<int>(ahd_triggers_.size());
353 return current;
354 }
355
356 LofiChainT<SampleType> lofi_;
357 detail::LinearPhaseOversamplingStage2x<SampleType> stage_a_;
358 detail::LinearPhaseOversamplingStage2x<SampleType> stage_b_;
359 double sample_rate_ = 44100.0;
360 double drive_ = 0.0;
361 double fold_ = 0.0;
362 double level_ = 1.0;
363 double ahd_attack_ms_ = 0.0;
364 double ahd_hold_ms_ = 0.0;
365 double ahd_decay_ms_ = 1000.0;
366 double ahd_gain_ = 1.0;
367 double ahd_attack_step_ = 1.0;
368 double ahd_decay_multiplier_ = 1.0;
369 int ahd_attack_samples_ = 0;
370 int ahd_hold_samples_ = 0;
371 int ahd_decay_samples_ = 1;
372 int ahd_remaining_ = 0;
373 bool ahd_enabled_ = false;
374 static constexpr int kMaxLatency =
376 std::array<bool, kMaxLatency + 1> ahd_triggers_{};
377 int ahd_trigger_cursor_ = 0;
378 AhdPhase ahd_phase_ = AhdPhase::inactive;
380 std::size_t tail_samples_remaining_ = 0;
381 bool nonlinear_activity_this_sample_ = false;
382};
383
386
387} // namespace pulp::signal::drum
The three lo-fi stages in the order the hardware they model applies them: quantise the word,...
Definition lofi_chain.hpp:251
The output stage every percussion voice ends with: fold, saturate, degrade, set the level,...
Definition output_stage.hpp:42
void set_drive(double amount)
Saturation amount, 0 (clean) to 1.
Definition output_stage.hpp:125
bool ahd_enabled() const noexcept
Definition output_stage.hpp:153
void trigger()
Start the AHD for a new hit. Every drum voice calls this from note-on.
Definition output_stage.hpp:182
void set_level(double level)
Output gain, linear.
Definition output_stage.hpp:131
void reset()
Definition output_stage.hpp:59
void prepare(double sample_rate)
Definition output_stage.hpp:44
LofiChainT< SampleType > & lofi()
The degradation stages, exposed so a voice can configure bit depth, hold rate, and dead zone without ...
Definition output_stage.hpp:201
void set_fold(double amount)
Wavefolding amount, 0 to 1.
Definition output_stage.hpp:128
void sync_configuration_from(const OutputStageT &other)
Mirror another stage's controls without copying its FIR, lo-fi, tail, or envelope history.
Definition output_stage.hpp:159
bool has_tail() const noexcept
True while the linear-phase pair still owns delayed output after the source layers have finished.
Definition output_stage.hpp:118
int latency_samples() const noexcept
Constant group delay at the host rate.
Definition output_stage.hpp:111
static constexpr OutputOversampling kDefaultOversampling
Definition output_stage.hpp:95
void set_oversampling(OutputOversampling factor)
Changes the output quality outside the audio callback.
Definition output_stage.hpp:78
OutputOversampling oversampling() const noexcept
Definition output_stage.hpp:85
double ahd_gain() const noexcept
Definition output_stage.hpp:154
void set_ahd_enabled(bool enabled)
Definition output_stage.hpp:144
static constexpr int latency_samples_for(OutputOversampling factor) noexcept
Definition output_stage.hpp:97
SampleType process(SampleType input)
Definition output_stage.hpp:204
void reset_nonlinear_state()
Restarts hit-scoped degradation without discarding samples already travelling through the linear-phas...
Definition output_stage.hpp:73
void set_ahd_ms(double attack_ms, double hold_ms, double decay_ms)
Configure and enable the dedicated post-saturation attack/hold/decay VCA.
Definition output_stage.hpp:136
const LofiChainT< SampleType > & lofi() const
Definition output_stage.hpp:202
Definition clap.hpp:13
OutputOversampling
Realtime quality choices for the shared drum output nonlinearity.
Definition output_stage.hpp:20
@ x2
Definition output_stage.hpp:22
@ x4
Definition output_stage.hpp:23
@ bypass
Definition output_stage.hpp:21
OutputStageT< double > OutputStage64
Definition output_stage.hpp:385
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
constexpr double kHouseHalfBandStopbandDb
Definition oversampling_fir.hpp:22
@ delay
Definition envelope.hpp:57
constexpr std::size_t kHouseHalfBandTaps
Pulp's low-latency linear-phase half-band design.
Definition oversampling_fir.hpp:21
constexpr double kHouseHalfBandStageTwoPassband
Definition oversampling_fir.hpp:24
@ e
Definition formant_filter_bank.hpp:20
constexpr double kHouseHalfBandStageOnePassband
Definition oversampling_fir.hpp:23
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
Linear-phase 2x FIR stage used by the realtime oversampler.