Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
kick.hpp
Go to the documentation of this file.
1#pragma once
2
11
12#include <algorithm>
13#include <cmath>
14
15namespace pulp::signal::drum {
16
20enum class KickBody {
25
30
38};
39
56class KickVoice : public Voice {
57public:
60 r.level_db = 14.0f;
61 // Deliberately small. An 808-family kick barely moves in pitch with how
62 // hard it is hit -- its velocity story is level and click tone, and a
63 // bend deep enough to hear reads as portamento rather than as tension.
64 r.bend_octaves = 0.15f;
65 r.brightness_octaves = 1.5f;
67
68 // A kick without a beater click reads as a sine with an envelope, so
69 // the click is on by default; the noise and sub layers are additions a
70 // caller opts into.
71 click_.set_level(0.3);
72 click_.set_cutoff_hz(4000.0);
73 click_.set_decay_ms(2.0);
74 noise_layer_.set_decay_ms(60.0);
75 }
76
77 // -- Shared controls -----------------------------------------------------
78
80 body_mode_ = body;
81 apply_circuit_tuning();
82 }
83 KickBody body() const { return body_mode_; }
84
86 void set_tune_hz(double hz) {
87 tune_hz_ = std::clamp(hz, 20.0, 400.0);
88 apply_circuit_tuning();
89 }
90
92 void set_body_decay_ms(double ms) { body_decay_ms_ = std::clamp(ms, 10.0, 4000.0); }
93
104 void set_pitch_sweep_octaves(double octaves) {
105 pitch_sweep_oct_ = std::clamp(octaves, 0.0, 6.0);
106 }
107 void set_pitch_sweep_ms(double ms) { pitch_sweep_ms_ = std::clamp(ms, 0.5, 500.0); }
108
112 void set_click_level(double level) { click_.set_level(level); }
113 void set_click_tone_hz(double hz) { click_.set_cutoff_hz(hz); }
114 void set_click_decay_ms(double ms) { click_.set_decay_ms(ms); }
115
117 void set_noise_level(double level) { noise_layer_.set_level(level); }
118 void set_noise_decay_ms(double ms) { noise_layer_.set_decay_ms(ms); }
119 void set_noise_color(NoiseColor color) { noise_.set_color(color); }
120
123 void set_sub_level(double level) { sub_.set_level(level); }
124
126 OutputStage& output() { return output_; }
127 const OutputStage& output() const { return output_; }
128 OutputStage* output_stage() noexcept override { return &output_; }
129 int latency_samples() const noexcept override {
130 return output_.latency_samples();
131 }
132 OutputOversampling output_oversampling() const noexcept override {
133 return output_.oversampling();
134 }
136 output_.set_oversampling(factor);
137 }
138
139 // -- Oscillator-body controls -------------------------------------------
140
143 void set_oscillator_triangle(bool triangle) { triangle_ = triangle; }
144
147 void set_fm_amount(double amount) { fm_amount_ = std::clamp(amount, 0.0, 8.0); }
148 void set_fm_ratio(double ratio) { fm_ratio_ = std::clamp(ratio, 0.25, 16.0); }
149
150 // -- Circuit-body controls ----------------------------------------------
151
155 authored_components_ = c;
156 apply_circuit_tuning();
157 }
158
169 void set_circuit_feedback(double gain) { circuit_feedback_ = std::clamp(gain, 0.0, 1.0); }
170
174 void set_circuit_attack_ms(double ms) { circuit_attack_ms_ = std::clamp(ms, 0.0, 50.0); }
175
177 void set_circuit_pulse_ms(double ms) { circuit_pulse_ms_ = std::clamp(ms, 0.2, 20.0); }
178
193 void set_circuit_drive(double amount) {
194 circuit_drive_ = std::clamp(amount, 0.0, 1.0);
195 }
196
199 void set_circuit_sigh(bool enabled) { circuit_.set_sigh_enabled(enabled); }
200
204 circuit_life_.set_mode(mode);
205 }
206 HitLifeMode circuit_hit_life() const { return circuit_life_.mode(); }
207
208protected:
209 void on_prepare(double sample_rate) override {
210 noise_.prepare(sample_rate);
211 body_env_.set_sample_rate(sample_rate);
212 pitch_env_.set_sample_rate(sample_rate);
213 click_.prepare(sample_rate);
214 noise_layer_.prepare(sample_rate);
215 sub_.prepare(sample_rate);
216 resonator_.set_sample_rate(sample_rate);
217 circuit_.prepare(sample_rate);
218 output_.prepare(sample_rate);
219 shaper_.prepare(static_cast<float>(sample_rate));
220 shaper_.set_cutoff(static_cast<float>(kShaperHz));
221 tone_.prepare(static_cast<float>(sample_rate));
222 tone_.set_cutoff(static_cast<float>(kToneHz));
223 }
224
225 void on_reset() override {
226 body_env_.reset();
227 pitch_env_.reset();
228 click_.reset();
229 noise_layer_.reset();
230 sub_.reset();
231 resonator_.reset();
232 circuit_.reset();
233 circuit_.set_attack_shunt(false);
234 output_.reset();
235 noise_.reset();
236 circuit_life_.reset();
237 shaper_.reset();
238 tone_.reset();
239 phase_ = 0.0;
240 mod_phase_ = 0.0;
241 feedback_z_ = 0.0;
242 ring_level_ = 0.0;
243 pulse_remaining_ = 0;
244 shunt_remaining_ = 0;
245 excite_remaining_ = 0;
246 }
247
248 void on_note_on(float velocity) override {
249 HitLifeDecision circuit_life;
250 if (body_mode_ == KickBody::circuit)
251 circuit_life =
252 circuit_life_.trigger(NoiseSource::default_seed);
253
254 if (body_mode_ == KickBody::oscillator ||
255 (body_mode_ == KickBody::circuit &&
256 circuit_life.reset_dsp_state))
257 output_.reset_nonlinear_state();
258 output_.trigger();
259 const auto& response = velocity_response();
260 velocity_gain_ = response.gain(velocity);
261 bend_octaves_ = pitch_sweep_oct_ + response.bend(velocity);
262 brightness_ = response.brightness_scale(velocity);
263
264 if (body_mode_ != KickBody::circuit) {
265 // Non-circuit bodies intentionally restart their procedural
266 // excitation on every hit.
267 noise_.reset();
268 }
269
270 pitch_env_.set_decay_time_constant_ms(pitch_sweep_ms_);
271 pitch_env_.set_attack_ms(0.0);
272 pitch_env_.trigger();
273
274 body_env_.set_attack_ms(0.4);
275 body_env_.set_decay_t60_ms(body_decay_ms_);
276 body_env_.trigger();
277
278 // Velocity brightens the click by moving its cutoff, not by scaling it.
279 click_.trigger(brightness_);
280 noise_layer_.trigger();
281 sub_.trigger();
282
283 switch (body_mode_) {
285 phase_ = 0.0;
286 mod_phase_ = 0.0;
287 break;
288
290 resonator_.set_frequency(tune_hz_);
291 resonator_.set_t60_ms(body_decay_ms_);
292 // A short broadband burst rings the body. Its length is what
293 // the strike sounds like, so it is deliberately sub-millisecond
294 // rather than an envelope.
295 excite_remaining_ = std::max(
296 1, static_cast<int>(0.0004 * sample_rate()));
297 break;
298
300 // The authentic default does not reset the network: a trigger
301 // adds energy to a body that may still be ringing. Fixed and
302 // advancing modes explicitly restart it.
303 if (circuit_life.reset_dsp_state) {
304 reset_circuit_memory();
305 if (circuit_life.reseed_excitation) {
306 noise_.set_seed(circuit_life.seed);
307 noise_.reset();
308 }
309 }
310 circuit_excitation_gain_ =
311 1.0 + 0.03 * static_cast<double>(noise_.white());
312 pulse_remaining_ = std::max(
313 1, static_cast<int>(0.001 * circuit_pulse_ms_ * sample_rate()));
314 shunt_remaining_ =
315 static_cast<int>(0.001 * circuit_attack_ms_ * sample_rate());
316 circuit_.set_attack_shunt(shunt_remaining_ > 0);
317 break;
318 }
319 }
320
321 bool on_is_active() const override {
322 if (body_mode_ == KickBody::circuit) {
323 return body_env_.is_active() || layers_active() ||
324 pulse_remaining_ > 0 || ring_level_ > kRingSilenceLevel ||
325 output_.has_tail();
326 }
327 if (body_mode_ == KickBody::resonant) {
328 return resonator_.is_ringing() || layers_active() || excite_remaining_ > 0 ||
329 output_.has_tail();
330 }
331 return body_env_.is_active() || layers_active() || output_.has_tail();
332 }
333
334 void render_add(float* out, int num_samples) override {
335 for (int i = 0; i < num_samples; ++i) {
336 const double body_env = body_env_.process();
337
338 double body = 0.0;
339 switch (body_mode_) {
340 case KickBody::oscillator: body = render_oscillator_body(body_env); break;
341 case KickBody::resonant: body = render_resonant_body(); break;
342 case KickBody::circuit: body = render_circuit_body(); break;
343 }
344
345 const double click = click_.process(static_cast<double>(noise_.white()));
346 const double noise = noise_layer_.process(static_cast<double>(noise_.process()));
347 const double sub = sub_.process(tune_hz_, body_env);
348
349 const double mixed = body + click + noise + sub;
350 out[i] += static_cast<float>(
351 output_.process(static_cast<float>(mixed)) * velocity_gain_);
352 }
353
354 circuit_.snap_denormals();
355 }
356
357private:
358 // The circuit body has no frequency control of its own -- its pitch is a
359 // property of the network. Rather than refuse a tune control on this body,
360 // spend it the way the instrument is actually retuned: substitute
361 // capacitors. Scaling both arms moves the centre frequency and leaves Q
362 // untouched, so the drum keeps its character and changes its pitch.
363 //
364 // Only the circuit body needs this; the other two read tune_hz_ directly.
365 void apply_circuit_tuning() {
366 circuit_.set_components(body_mode_ == KickBody::circuit
367 ? bridged_t_retuned_to(authored_components_, tune_hz_)
368 : authored_components_);
369 }
370
371 void reset_circuit_memory() {
372 circuit_.reset();
373 circuit_.set_attack_shunt(false);
374 shaper_.reset();
375 tone_.reset();
376 feedback_z_ = 0.0;
377 ring_level_ = 0.0;
378 pulse_remaining_ = 0;
379 shunt_remaining_ = 0;
380 }
381
382 double render_oscillator_body(double body_env) {
383 const double pitch_env = pitch_env_.process();
384 const double f = tune_hz_ * std::exp2(bend_octaves_ * pitch_env);
385
386 double modulation = 0.0;
387 if (fm_amount_ > 0.0) {
388 mod_phase_ += f * fm_ratio_ / sample_rate();
389 if (mod_phase_ >= 1.0) mod_phase_ -= std::floor(mod_phase_);
390 modulation = fm_amount_ * std::sin(2.0 * 3.14159265358979323846 * mod_phase_);
391 }
392
393 phase_ += f / sample_rate();
394 if (phase_ >= 1.0) phase_ -= std::floor(phase_);
395
396 const double angle = 2.0 * 3.14159265358979323846 * phase_ + modulation;
397 const double shape = triangle_ ? triangle_from_phase(angle) : std::sin(angle);
398 return shape * body_env;
399 }
400
401 double render_resonant_body() {
402 double excitation = 0.0;
403 if (excite_remaining_ > 0) {
404 excitation = static_cast<double>(noise_.white());
405 --excite_remaining_;
406 }
407 return static_cast<double>(resonator_.process(static_cast<float>(excitation)));
408 }
409
410 double render_circuit_body() {
411 // Rectangular trigger pulse into an RC shaper, then a diode that passes
412 // the positive edge and softly clamps the negative one. The shaper and
413 // the diode together are what turn a square trigger into something the
414 // network rings on rather than buzzes on.
415 double pulse = 0.0;
416 if (pulse_remaining_ > 0) {
417 pulse = 1.0;
418 --pulse_remaining_;
419 }
420 const double shaped = shaper_.process_lowpass(static_cast<float>(pulse));
421 const double diode = shaped >= 0.0
422 ? shaped
423 : kDiodeVt * (std::exp(shaped / kDiodeVt) - 1.0);
424
425 if (shunt_remaining_ > 0) {
426 --shunt_remaining_;
427 if (shunt_remaining_ == 0) circuit_.set_attack_shunt(false);
428 }
429
430 const double drive_volts =
431 kPulseVoltsMin + circuit_drive_ * (kPulseVoltsMax - kPulseVoltsMin);
432 const auto ring = circuit_.process(
433 diode * drive_volts * circuit_excitation_gain_, feedback_z_, 0.0);
434
435 // One sample of delay breaks the delay-free loop. The sign is negative
436 // because the network already subtracts its feedback injection, so a
437 // positively-signed buffer would damp the ring instead of sustaining
438 // it. The saturator's knee is a large fraction of the supply rail, so
439 // the buffer is close to linear for an ordinary hit and only softens on
440 // the loudest ones; a knee near unity would turn the loop into a
441 // bang-bang switch and the drum into a square wave.
442 feedback_z_ = -circuit_feedback_ * kMaxLoopGain * kFeedbackKnee *
443 std::tanh(kSwellGain * ring.vbt / kFeedbackKnee);
444
445 const double out =
446 tone_.process_lowpass(static_cast<float>(ring.vbt)) * kCircuitOutputScale;
447
448 // The loop can hold a ring at almost constant amplitude, so "has the
449 // voice finished" cannot be read from a single sample -- it crosses
450 // zero twice a cycle. A peak follower answers it honestly.
451 ring_level_ = std::max(std::fabs(out), ring_level_ * kRingFollowerDecay);
452 return out;
453 }
454
455 static double triangle_from_phase(double angle) {
456 const double normalised =
457 angle / (2.0 * 3.14159265358979323846) - std::floor(angle / (2.0 * 3.14159265358979323846));
458 return 2.0 * std::fabs(2.0 * normalised - 1.0) - 1.0;
459 }
460
461 bool layers_active() const { return click_.is_active() || noise_layer_.is_active(); }
462
463 // Circuit-body constants. The pulse amplitude and output scale convert
464 // between the network's volts and the +/-1 the rest of the voice works in;
465 // the shaper and tone corners are below the band the drum occupies, so
466 // they shape the trigger and the output without colouring the ring.
467 // The drive level is not a taste control. Below about a volt into the
468 // shaper the ring never pushes Vcomm past the -0.556 V knee where the
469 // transistor starts to leak, so the pitch sigh -- the drum's signature --
470 // silently does not happen; above about two volts the op-amp output sits on
471 // its 15 V rail and the ring becomes a square wave. This value keeps the
472 // network inside the range the model was fitted over, and the output scale
473 // brings its volts back to the range the rest of the voice works in.
474 // Measured bounds of the window the network behaves in. Below the low end
475 // the ring never reaches the -0.556 V conduction knee and the pitch drop
476 // silently does not happen; above the high end the op-amp sits on its 15 V
477 // rail. `set_circuit_drive` maps its 0..1 across exactly this span.
478 static constexpr double kPulseVoltsMin = 1.0;
479 static constexpr double kPulseVoltsMax = 1.7;
480 static constexpr double kCircuitOutputScale = 0.1;
481 static constexpr double kShaperHz = 120.0;
482 static constexpr double kToneHz = 4000.0;
483 static constexpr double kDiodeVt = 0.03;
484 static constexpr double kSwellGain = 1.1;
485 static constexpr double kFeedbackKnee = 8.0;
486 // Measured ceiling: above this the loop replaces more than the network
487 // dissipates and the ring never decays.
488 static constexpr double kMaxLoopGain = 0.86;
489 // ~40 ms follower, long enough to bridge a cycle of the lowest tuning.
490 static constexpr double kRingFollowerDecay = 0.9995;
491 static constexpr double kRingSilenceLevel = 1e-5;
492
493 KickBody body_mode_ = KickBody::oscillator;
494
495 double tune_hz_ = 55.0;
496 // What the caller asked for, before the tune control retunes the arms, so
497 // a later tune_hz does not compound onto an already-scaled set.
498 BridgedTComponents authored_components_{};
499 double body_decay_ms_ = 400.0;
500 double pitch_sweep_oct_ = 2.0;
501 double pitch_sweep_ms_ = 30.0;
502 bool triangle_ = false;
503 double fm_amount_ = 0.0;
504 double fm_ratio_ = 1.0;
505
506 double circuit_feedback_ = 0.85;
507 double circuit_drive_ = 0.3;
508 double circuit_attack_ms_ = 4.0;
509 double circuit_pulse_ms_ = 2.0;
510
511 NoiseSource noise_;
512 DecayEnvelope64 body_env_;
513 DecayEnvelope64 pitch_env_;
514 ClickLayer click_;
515 NoiseLayer noise_layer_;
516 SubLayer sub_;
517 TwoPoleResonator resonator_;
518 BridgedTResonator circuit_;
519 HitLife circuit_life_{HitLifeMode::preserved_state};
520 OutputStage output_;
521
522 double velocity_gain_ = 1.0;
523 double bend_octaves_ = 2.0;
524 double brightness_ = 1.0;
525
526 TptFilter shaper_;
527 TptFilter tone_;
528
529 double phase_ = 0.0;
530 double mod_phase_ = 0.0;
531 double feedback_z_ = 0.0;
532 double ring_level_ = 0.0;
533 double circuit_excitation_gain_ = 1.0;
534 int pulse_remaining_ = 0;
535 int shunt_remaining_ = 0;
536 int excite_remaining_ = 0;
537};
538
539} // namespace pulp::signal::drum
void set_components(const BridgedTComponents &c) noexcept
Definition bridged_t_resonator.hpp:204
void set_attack_shunt(bool active) noexcept
Ground Q43's collector, shorting out R165.
Definition bridged_t_resonator.hpp:242
void reset() noexcept
Clear the ring.
Definition bridged_t_resonator.hpp:231
SampleType process()
Next envelope sample in [0, peak].
Definition decay_envelope.hpp:125
static constexpr std::uint32_t default_seed
Definition noise_source.hpp:55
void reset()
Definition tpt_filter.hpp:127
void set_oscillator_triangle(bool triangle)
Triangle instead of sine.
Definition kick.hpp:143
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition kick.hpp:132
void set_click_decay_ms(double ms)
Definition kick.hpp:114
void set_fm_ratio(double ratio)
Definition kick.hpp:148
void set_pitch_sweep_octaves(double octaves)
Depth of the downward pitch sweep, in octaves, and how quickly it settles.
Definition kick.hpp:104
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition kick.hpp:128
void set_circuit_attack_ms(double ms)
How long the attack shunt grounds the transistor's collector after a trigger.
Definition kick.hpp:174
void set_tune_hz(double hz)
Fundamental of the body, in Hz.
Definition kick.hpp:86
HitLifeMode circuit_hit_life() const
Definition kick.hpp:206
void set_sub_level(double level)
Level of the sub layer, a sine an octave below the body gated by the body's own envelope.
Definition kick.hpp:123
void set_circuit_drive(double amount)
How hard the trigger drives the network, 0 to 1.
Definition kick.hpp:193
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition kick.hpp:225
KickVoice()
Definition kick.hpp:58
void set_body_decay_ms(double ms)
How long the body rings, as a T60 in milliseconds.
Definition kick.hpp:92
void set_noise_color(NoiseColor color)
Definition kick.hpp:119
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition kick.hpp:248
void set_click_tone_hz(double hz)
Definition kick.hpp:113
bool on_is_active() const override
Definition kick.hpp:321
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition kick.hpp:209
void set_circuit_pulse_ms(double ms)
Width of the trigger pulse into the pulse shaper, in milliseconds.
Definition kick.hpp:177
OutputStage & output()
The saturation and degradation stage the voice ends with.
Definition kick.hpp:126
void set_noise_level(double level)
Level and decay of the noise layer.
Definition kick.hpp:117
void set_noise_decay_ms(double ms)
Definition kick.hpp:118
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition kick.hpp:129
void set_circuit_sigh(bool enabled)
Disconnect the leakage path that produces the pitch sigh.
Definition kick.hpp:199
void set_circuit_hit_life(HitLifeMode mode)
Select whether circuit retriggers restart, vary their excitation, or preserve the network memory.
Definition kick.hpp:203
void set_body(KickBody body)
Definition kick.hpp:79
void set_circuit_feedback(double gain)
Regeneration around the network, 0 to 1.
Definition kick.hpp:169
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition kick.hpp:334
const OutputStage & output() const
Definition kick.hpp:127
KickBody body() const
Definition kick.hpp:83
void set_circuit_components(const BridgedTComponents &c)
Component values of the bridged-T network.
Definition kick.hpp:154
void set_fm_amount(double amount)
Phase-modulation depth and the modulator's frequency ratio.
Definition kick.hpp:147
void set_click_level(double level)
Level of the attack click, and the corner of the one-pole that tones it.
Definition kick.hpp:112
void set_output_oversampling(OutputOversampling factor) override
Definition kick.hpp:135
void set_pitch_sweep_ms(double ms)
Definition kick.hpp:107
double sample_rate() const
Definition voice.hpp:105
void set_velocity_response(const VelocityResponse &r)
Definition voice.hpp:129
float velocity() const
Definition voice.hpp:104
const VelocityResponse & velocity_response() const
Definition voice.hpp:130
Definition clap.hpp:13
@ ring
Definition string.hpp:18
HitLifeMode
Selects the intentional source of variation between otherwise identical hits.
Definition hit_life.hpp:26
@ preserved_state
Definition hit_life.hpp:29
OutputOversampling
Realtime quality choices for the shared drum output nonlinearity.
Definition output_stage.hpp:20
KickBody
How a kick's body is produced.
Definition kick.hpp:20
@ resonant
A struck resonance.
Definition kick.hpp:29
@ oscillator
A swept oscillator.
Definition kick.hpp:24
@ circuit
The TR-808's bridged-T network in its op-amp feedback loop.
Definition kick.hpp:37
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
@ c
Definition spectrum_trace.hpp:35
TwoPoleResonatorT< float > TwoPoleResonator
Definition two_pole_resonator.hpp:118
TptFilterT< float > TptFilter
Definition tpt_filter.hpp:172
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
BridgedTComponents bridged_t_retuned_to(const BridgedTComponents &c, double target_hz) noexcept
Quality factor of the bridged-T network alone, with its three inputs grounded.
Definition bridged_t_resonator.hpp:143
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ triangle
Definition lfo_sampling.hpp:55
@ pulse
a ramp then a hold: sine becomes pulse-like
Definition phase_distortion.hpp:11
NoiseColor
Spectral tilt of a NoiseSourceT, named by the usual colour convention.
Definition noise_source.hpp:17
DecayEnvelopeT< double > DecayEnvelope64
Definition decay_envelope.hpp:201
NoiseSourceT< float > NoiseSource
Definition noise_source.hpp:279
STL namespace.
Component values of the TR-808 bass drum's bridged-T network, in SI units (ohms, farads).
Definition bridged_t_resonator.hpp:21
Definition hit_life.hpp:34
bool reseed_excitation
Definition hit_life.hpp:36
std::uint32_t seed
Definition hit_life.hpp:35
bool reset_dsp_state
Definition hit_life.hpp:37
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33
First-order topology-preserving transform (TPT) filter.