Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
snare.hpp
Go to the documentation of this file.
1#pragma once
2
7#include <pulp/signal/svf.hpp>
9
10#include <algorithm>
11#include <cmath>
12
13namespace pulp::signal::drum {
14
41class SnareVoice : public Voice {
42public:
45 r.level_db = 16.0f;
46 // Tension, not portamento: a struck head tightens slightly under a
47 // harder strike, and that is all this is meant to convey.
48 r.bend_octaves = 0.08f;
49 r.brightness_octaves = 1.2f;
50 // A harder snare hit puts proportionally more into the wires than into
51 // the shell -- which is why a rimshot is nearly all snap.
52 r.noise_balance = 0.25f;
54
55 snap_.set_level(0.4);
56 snap_.set_cutoff_hz(6000.0);
57 snap_.set_decay_ms(4.0);
58 }
59
60 // -- Tone (the drum) -----------------------------------------------------
61
63 void set_tune_hz(double hz) { tune_hz_ = std::clamp(hz, 60.0, 800.0); }
64
68 void set_tone_ratio(double ratio) { tone_ratio_ = std::clamp(ratio, 1.0, 4.0); }
69
70 void set_tone_level(double level) { tone_level_ = std::max(level, 0.0); }
71 void set_tone_decay_ms(double ms) { tone_decay_ms_ = std::clamp(ms, 5.0, 2000.0); }
72
74 void set_pitch_sweep_octaves(double octaves) {
75 pitch_sweep_oct_ = std::clamp(octaves, 0.0, 4.0);
76 }
77 void set_pitch_sweep_ms(double ms) { pitch_sweep_ms_ = std::clamp(ms, 0.5, 300.0); }
78
80 void set_fm_amount(double amount) { fm_amount_ = std::clamp(amount, 0.0, 8.0); }
81
85 void set_ring(double amount) { ring_ = std::clamp(amount, 0.0, 1.0); }
86
87 // -- Snares (the wires) --------------------------------------------------
88
89 void set_noise_level(double level) { noise_level_ = std::max(level, 0.0); }
90 void set_noise_decay_ms(double ms) { noise_decay_ms_ = std::clamp(ms, 5.0, 3000.0); }
91 void set_noise_color(NoiseColor color) { noise_.set_color(color); }
92
94 void set_noise_cutoff_hz(double hz) { noise_cutoff_hz_ = std::clamp(hz, 100.0, 18000.0); }
95 void set_noise_resonance(double q) { noise_q_ = std::clamp(q, 0.5, 12.0); }
96
99 void set_noise_sweep_octaves(double octaves) {
100 noise_sweep_oct_ = std::clamp(octaves, -4.0, 4.0);
101 }
102
104 void set_rattle(double amount) { rattle_ = std::clamp(amount, 0.0, 1.0); }
105 void set_rattle_hz(double hz) { rattle_hz_ = std::clamp(hz, 5.0, 400.0); }
106
108 void set_snap_level(double level) { snap_.set_level(level); }
109 void set_snap_cutoff_hz(double hz) { snap_.set_cutoff_hz(hz); }
110 void set_snap_decay_ms(double ms) { snap_.set_decay_ms(ms); }
111
113 void set_shell_level(double level) { shell_.set_level(level); }
114 void set_shell_resonance(double q) { shell_.set_resonance(q); }
115
116 OutputStage& output() { return output_; }
117 const OutputStage& output() const { return output_; }
118 OutputStage* output_stage() noexcept override { return &output_; }
119 int latency_samples() const noexcept override {
120 return output_.latency_samples();
121 }
122 OutputOversampling output_oversampling() const noexcept override {
123 return output_.oversampling();
124 }
126 output_.set_oversampling(factor);
127 }
128
132 LofiChain& tone_lofi() { return tone_lofi_; }
133
137 LofiChain& noise_lofi() { return noise_lofi_; }
138
139protected:
140 void on_prepare(double sample_rate) override {
141 noise_.prepare(sample_rate);
142 tone_env_.set_sample_rate(sample_rate);
143 pitch_env_.set_sample_rate(sample_rate);
144 noise_env_.set_sample_rate(sample_rate);
145 snap_.prepare(sample_rate);
146 noise_filter_.set_sample_rate(static_cast<float>(sample_rate));
147 shell_.prepare(sample_rate);
148 snap_hp_.prepare(static_cast<float>(sample_rate));
149 tone_lofi_.set_sample_rate(sample_rate);
150 noise_lofi_.set_sample_rate(sample_rate);
151 output_.prepare(sample_rate);
152 }
153
154 void on_reset() override {
155 tone_env_.reset();
156 pitch_env_.reset();
157 noise_env_.reset();
158 snap_.reset();
159 noise_filter_.reset();
160 shell_.reset();
161 snap_hp_.reset();
162 tone_lofi_.reset();
163 noise_lofi_.reset();
164 output_.reset();
165 noise_.reset();
166 phase_low_ = 0.0;
167 phase_high_ = 0.0;
168 rattle_phase_ = 0.0;
169 high_previous_ = 0.0;
170 }
171
172 void on_note_on(float velocity) override {
173 // The shell keeps ringing across hits; preserve the shared output
174 // history with it so a retrigger cannot truncate the pending FIR.
175 output_.trigger();
176 const auto& response = velocity_response();
177 velocity_gain_ = response.gain(velocity);
178 bend_octaves_ = pitch_sweep_oct_ + response.bend(velocity);
179 brightness_ = response.brightness_scale(velocity);
180 // Velocity tilts the mix toward the wires without touching the levels
181 // the caller set, so a preset's balance is preserved and only the
182 // performance moves it.
183 noise_tilt_ = response.noise_shift(velocity);
184
185 noise_.reset();
186 phase_low_ = 0.0;
187 phase_high_ = 0.0;
188 rattle_phase_ = 0.0;
189 high_previous_ = 0.0;
190 snap_hp_.reset();
191 // The wire filter shapes the strike and starts clean; `shell_` is the
192 // drum's body and deliberately keeps ringing across hits.
193 noise_filter_.reset();
194
195 pitch_env_.set_attack_ms(0.0);
196 pitch_env_.set_decay_time_constant_ms(pitch_sweep_ms_);
197 pitch_env_.trigger();
198
199 // A layer at zero level is not started, and is not counted as active
200 // below. Both halves are needed: its render path returns early, so a
201 // started envelope would never advance and the voice would report
202 // itself active forever.
203 if (tone_level_ > 0.0) {
204 tone_env_.set_attack_ms(0.3);
205 tone_env_.set_decay_t60_ms(tone_decay_ms_);
206 tone_env_.trigger();
207 }
208 if (noise_level_ > 0.0) {
209 noise_env_.set_attack_ms(0.2);
210 noise_env_.set_decay_t60_ms(noise_decay_ms_);
211 noise_env_.trigger();
212 }
213
214 snap_.trigger(brightness_);
215 shell_.set_frequency_hz(tune_hz_);
216 }
217
218 bool on_is_active() const override {
219 return (tone_level_ > 0.0 && tone_env_.is_active()) ||
220 (noise_level_ > 0.0 && noise_env_.is_active()) || snap_.is_active() ||
221 shell_.is_ringing() || tone_lofi_.has_tail() ||
222 noise_lofi_.has_tail() || output_.has_tail();
223 }
224
225 void render_add(float* out, int num_samples) override {
226 const double tone_gain = tone_level_ * (1.0 - noise_tilt_);
227 const double wire_gain = noise_level_ * (1.0 + noise_tilt_);
228
229 for (int i = 0; i < num_samples; ++i) {
230 const auto tone = tone_lofi_.process(
231 static_cast<float>(render_tone(tone_gain)));
232 out[i] += static_cast<float>(
233 output_.process(static_cast<float>(
234 static_cast<double>(tone) + render_wires(wire_gain))) *
235 velocity_gain_);
236 }
237 }
238
239private:
240 double render_tone(double gain) {
241 if (!tone_env_.is_active() || gain <= 0.0) return 0.0;
242
243 const double pitch_env = pitch_env_.process();
244 const double sweep = std::exp2(bend_octaves_ * pitch_env);
245 const double f_low = tune_hz_ * sweep;
246 const double f_high = f_low * tone_ratio_;
247
248 phase_high_ += f_high / sample_rate();
249 if (phase_high_ >= 1.0) phase_high_ -= std::floor(phase_high_);
250 const double high = std::sin(2.0 * 3.14159265358979323846 * phase_high_);
251
252 phase_low_ += f_low / sample_rate();
253 if (phase_low_ >= 1.0) phase_low_ -= std::floor(phase_low_);
254 // The modulator is read one sample late so the pair cannot form a
255 // delay-free loop when a caller cross-modulates them.
256 const double low = std::sin(2.0 * 3.14159265358979323846 * phase_low_ +
257 fm_amount_ * high_previous_);
258 high_previous_ = high;
259
260 const double summed = 0.5 * (low + high);
261 const double ringed = low * high;
262 return (summed + ring_ * (ringed - summed)) * tone_env_.process() * gain;
263 }
264
265 double render_wires(double gain) {
266 const double snap = snap_hp_.process_highpass(
267 static_cast<float>(snap_.process(static_cast<double>(noise_.white()))));
268
269 double wires = 0.0;
270 if (noise_env_.is_active() && gain > 0.0) {
271 const double envelope = noise_env_.process();
272
273 // The filter sweeps over the note, so the wires change character as
274 // they die rather than simply fading.
275 const double corner = noise_cutoff_hz_ * brightness_ *
276 std::exp2(noise_sweep_oct_ * envelope);
277 noise_filter_.set_frequency(
278 static_cast<float>(std::clamp(corner, 20.0, 0.49 * sample_rate())));
279 noise_filter_.set_resonance(static_cast<float>(noise_q_));
280
281 wires = noise_filter_.process(noise_.process()) * envelope * gain;
282
283 if (rattle_ > 0.0) {
284 rattle_phase_ += rattle_hz_ / sample_rate();
285 if (rattle_phase_ >= 1.0) rattle_phase_ -= std::floor(rattle_phase_);
286 wires *= 1.0 + rattle_ * std::sin(2.0 * 3.14159265358979323846 * rattle_phase_);
287 }
288 }
289
290 const double path = shell_.process(wires + snap);
291 return noise_lofi_.process(static_cast<float>(path));
292 }
293
294 double tune_hz_ = 180.0;
295 double tone_ratio_ = 1.6;
296 double tone_level_ = 0.5;
297 double tone_decay_ms_ = 120.0;
298 double pitch_sweep_oct_ = 0.5;
299 double pitch_sweep_ms_ = 25.0;
300 double fm_amount_ = 0.0;
301 double ring_ = 0.0;
302
303 double noise_level_ = 0.6;
304 double noise_decay_ms_ = 180.0;
305 double noise_cutoff_hz_ = 3000.0;
306 double noise_q_ = 1.0;
307 double noise_sweep_oct_ = 0.0;
308 double rattle_ = 0.0;
309 double rattle_hz_ = 45.0;
310
311 NoiseSource noise_;
312 DecayEnvelope64 tone_env_;
313 DecayEnvelope64 pitch_env_;
314 DecayEnvelope64 noise_env_;
315 ClickLayer snap_;
316 Svf noise_filter_;
317 ShellLayer shell_;
318 TptFilter snap_hp_;
319 LofiChain tone_lofi_;
320 LofiChain noise_lofi_;
321 OutputStage output_;
322
323 double velocity_gain_ = 1.0;
324 double bend_octaves_ = 0.5;
325 double brightness_ = 1.0;
326 double noise_tilt_ = 0.0;
327
328 double phase_low_ = 0.0;
329 double phase_high_ = 0.0;
330 double rattle_phase_ = 0.0;
331 double high_previous_ = 0.0;
332};
333
334} // namespace pulp::signal::drum
bool is_active() const
Definition decay_envelope.hpp:122
SampleType process()
Next envelope sample in [0, peak].
Definition decay_envelope.hpp:125
SampleType process(SampleType input)
Definition lofi_chain.hpp:300
SampleType process()
Next sample, normalised so every colour has approximately the same RMS.
Definition noise_source.hpp:111
SampleType white()
Raw uniform white sample in [-1, 1), bypassing the colour filter and the normalisation.
Definition noise_source.hpp:123
void set_frequency(SampleType hz)
Definition svf.hpp:21
void set_resonance(SampleType q)
Definition svf.hpp:22
SampleType process(SampleType input)
Definition svf.hpp:54
SampleType process_highpass(SampleType input)
Process and return the highpass output.
Definition tpt_filter.hpp:78
double process(double noise)
Next sample. noise is a raw white sample from the voice's generator.
Definition layers.hpp:85
double process(double excitation)
Definition layers.hpp:200
void set_pitch_sweep_ms(double ms)
Definition snare.hpp:77
LofiChain & tone_lofi()
Degradation applied to the pitched shell before it meets the wires.
Definition snare.hpp:132
void set_noise_cutoff_hz(double hz)
Corner and resonance of the filter the wires are heard through.
Definition snare.hpp:94
bool on_is_active() const override
Definition snare.hpp:218
OutputStage & output()
Definition snare.hpp:116
void set_pitch_sweep_octaves(double octaves)
Depth and time constant of the shared downward pitch sweep, in octaves.
Definition snare.hpp:74
void set_tune_hz(double hz)
Fundamental of the lower tone oscillator, in Hz.
Definition snare.hpp:63
void set_fm_amount(double amount)
Phase-modulation depth of the upper oscillator into the lower.
Definition snare.hpp:80
const OutputStage & output() const
Definition snare.hpp:117
void set_output_oversampling(OutputOversampling factor) override
Definition snare.hpp:125
SnareVoice()
Definition snare.hpp:43
void set_snap_decay_ms(double ms)
Definition snare.hpp:110
void set_rattle_hz(double hz)
Definition snare.hpp:105
void set_tone_ratio(double ratio)
Frequency ratio of the upper tone oscillator to the lower.
Definition snare.hpp:68
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition snare.hpp:140
void set_ring(double amount)
Crossfade from the two oscillators summed (0) to ring-modulated (1).
Definition snare.hpp:85
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition snare.hpp:172
void set_noise_sweep_octaves(double octaves)
How far that corner sweeps over the note, in octaves.
Definition snare.hpp:99
void set_tone_decay_ms(double ms)
Definition snare.hpp:71
void set_snap_cutoff_hz(double hz)
Definition snare.hpp:109
void set_noise_color(NoiseColor color)
Definition snare.hpp:91
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition snare.hpp:122
void set_rattle(double amount)
Depth and rate of the wires' buzz against the head.
Definition snare.hpp:104
void set_noise_resonance(double q)
Definition snare.hpp:95
void set_tone_level(double level)
Definition snare.hpp:70
void set_noise_level(double level)
Definition snare.hpp:89
LofiChain & noise_lofi()
A second degradation chain on the noise path alone.
Definition snare.hpp:137
void set_snap_level(double level)
The strike itself: a short highpassed crack, distinct from the wires.
Definition snare.hpp:108
void set_noise_decay_ms(double ms)
Definition snare.hpp:90
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition snare.hpp:118
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition snare.hpp:225
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition snare.hpp:119
void set_shell_level(double level)
Level of the shell resonance applied to the noise path, and its Q.
Definition snare.hpp:113
void set_shell_resonance(double q)
Definition snare.hpp:114
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition snare.hpp:154
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
OutputOversampling
Realtime quality choices for the shared drum output nonlinearity.
Definition output_stage.hpp:20
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
LofiChainT< float > LofiChain
Definition lofi_chain.hpp:328
SvfT< float > Svf
Definition svf.hpp:148
TptFilterT< float > TptFilter
Definition tpt_filter.hpp:172
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ i
Definition formant_filter_bank.hpp:20
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
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33
First-order topology-preserving transform (TPT) filter.