Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
clap.hpp
Go to the documentation of this file.
1#pragma once
2
6#include <pulp/signal/svf.hpp>
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <cstdint>
12
14
49class ClapVoice : public Voice {
50public:
53 static constexpr int max_bursts = 8;
54
57 r.level_db = 15.0f;
58 r.brightness_octaves = 0.7f;
60 }
61
63 void set_burst_count(int count) { burst_count_ = std::clamp(count, 1, max_bursts); }
64
66 void set_burst_spacing_ms(double ms) { spacing_ms_ = std::clamp(ms, 1.0, 60.0); }
67
69 void set_burst_decay_ms(double ms) { burst_decay_ms_ = std::clamp(ms, 0.5, 60.0); }
70
74 void set_burst_falloff(double ratio) { falloff_ = std::clamp(ratio, 0.2, 1.5); }
75
80 void set_gap_jitter(double amount) { gap_jitter_ = std::clamp(amount, 0.0, 1.0); }
81
85 void set_alternate_polarity(bool alternate) { alternate_polarity_ = alternate; }
86
89 void set_stereo_width(double width) {
90 stereo_width_ = std::clamp(width, 0.0, 1.0);
91 }
92
94 void set_tail_level(double level) { tail_level_ = std::max(level, 0.0); }
95 void set_tail_decay_ms(double ms) { tail_decay_ms_ = std::clamp(ms, 10.0, 3000.0); }
96
100 void set_cutoff_hz(double hz) { cutoff_hz_ = std::clamp(hz, 200.0, 16000.0); }
101 void set_resonance(double q) { resonance_ = std::clamp(q, 0.5, 12.0); }
102
103 void set_noise_color(NoiseColor color) { noise_.set_color(color); }
104
108 void set_body_level(double level) { body_level_ = std::max(level, 0.0); }
109 void set_body_hz(double hz) { body_hz_ = std::clamp(hz, 40.0, 2000.0); }
110
111 OutputStage& output() { return output_; }
112 const OutputStage& output() const { return output_; }
113 OutputStage* output_stage() noexcept override { return &output_; }
114 int latency_samples() const noexcept override {
115 return output_.latency_samples();
116 }
117 OutputOversampling output_oversampling() const noexcept override {
118 return output_.oversampling();
119 }
121 output_.set_oversampling(factor);
122 }
123
124protected:
125 void on_prepare(double sample_rate) override {
126 noise_.prepare(sample_rate);
127 burst_env_.set_sample_rate(sample_rate);
128 tail_env_.set_sample_rate(sample_rate);
129 filter_.set_sample_rate(static_cast<float>(sample_rate));
130 filter_.set_mode(Svf::Mode::bandpass);
131 output_.prepare(sample_rate);
132 burst_output_.prepare(sample_rate);
133 }
134
135 void on_reset() override {
136 burst_env_.reset();
137 tail_env_.reset();
138 filter_.reset();
139 output_.reset();
140 burst_output_.reset();
141 stereo_path_active_ = false;
142 noise_.reset();
143 body_phase_ = 0.0;
144 next_burst_ = 0;
145 samples_until_burst_ = 0;
146 burst_gain_ = 1.0;
147 burst_sign_ = 1.0;
148 burst_pan_ = -1.0;
149 reset_pan_delay();
150 jitter_state_ = kJitterSeed;
151 }
152
153 void on_note_on(float velocity) override {
154 output_.reset_nonlinear_state();
155 output_.trigger();
156 if (stereo_path_active_) {
157 burst_output_.sync_configuration_from(output_);
158 burst_output_.reset_nonlinear_state();
159 burst_output_.trigger();
160 }
161 const auto& response = velocity_response();
162 velocity_gain_ = response.gain(velocity);
163 brightness_ = response.brightness_scale(velocity);
164
165 noise_.reset();
166 body_phase_ = 0.0;
167 // The band filter shapes the strike rather than modelling a body, so
168 // it starts clean on every hit. Voices whose filter *is* a body -- the
169 // kick's resonator and circuit network, the snare's shell, the hat's
170 // oscillator bank -- deliberately carry their state across hits
171 // instead, because a struck body is not re-struck from rest.
172 filter_.reset();
173
174 filter_.set_frequency(static_cast<float>(
175 std::min(cutoff_hz_ * brightness_, 0.49 * sample_rate())));
176 filter_.set_resonance(static_cast<float>(resonance_));
177
178 spacing_samples_ = std::max(1, static_cast<int>(0.001 * spacing_ms_ * sample_rate()));
179
180 burst_env_.set_attack_ms(0.0);
181 burst_env_.set_decay_time_constant_ms(burst_decay_ms_);
182 tail_env_.set_attack_ms(0.5);
183 tail_env_.set_decay_t60_ms(tail_decay_ms_);
184
185 // Fire the first burst immediately and schedule the rest; the tail runs
186 // from the same instant so it is already present under burst one.
187 burst_gain_ = 1.0;
188 burst_sign_ = 1.0;
189 burst_pan_ = -1.0;
190 jitter_state_ = kJitterSeed;
191 burst_env_.trigger();
192 tail_env_.trigger();
193 next_burst_ = 1;
194 samples_until_burst_ = next_gap();
195 }
196
197 bool on_is_active() const override {
198 return burst_env_.is_active() || tail_env_.is_active() ||
199 next_burst_ < burst_count_ || output_.has_tail();
200 }
201
202 void render_add(float* out, int num_samples) override {
203 // A later switch back to stereo must start from fresh side-path state;
204 // the optional processor is deliberately not advanced by mono blocks.
205 stereo_path_active_ = false;
206 for (int i = 0; i < num_samples; ++i) {
207 out[i] += render_frame(false).mono;
208 }
209 }
210
211 void render_add_stereo(float* left, float* right,
212 int num_samples) override {
213 const bool quality_changed =
214 burst_output_.oversampling() != output_.oversampling();
215 burst_output_.sync_configuration_from(output_);
216 if (!stereo_path_active_ || quality_changed) {
217 burst_output_.reset();
218 burst_output_.trigger();
219 reset_pan_delay();
220 stereo_path_active_ = true;
221 }
222 for (int i = 0; i < num_samples; ++i) {
223 const RenderFrame frame = render_frame(true);
224 const double pan = frame.burst_pan * stereo_width_;
225 left[i] += static_cast<float>(
226 0.5 * frame.mono - 0.5 * pan * frame.burst);
227 right[i] += static_cast<float>(
228 0.5 * frame.mono + 0.5 * pan * frame.burst);
229 }
230 }
231
232private:
233 struct RenderFrame {
234 float mono = 0.0f;
235 double burst = 0.0;
236 double burst_pan = 0.0;
237 };
238
239 RenderFrame render_frame(bool render_burst_side) {
240 if (next_burst_ < burst_count_ && --samples_until_burst_ <= 0) {
241 burst_gain_ *= falloff_;
242 if (alternate_polarity_) burst_sign_ = -burst_sign_;
243 burst_pan_ = (next_burst_ & 1) != 0 ? 1.0 : -1.0;
244 burst_env_.trigger();
245 ++next_burst_;
246 samples_until_burst_ = next_gap();
247 }
248
249 const double filtered = filter_.process(noise_.process());
250 const double train =
251 filtered * burst_env_.process() * burst_gain_ * burst_sign_;
252 const double tail_env = tail_env_.process();
253 const double room = filtered * tail_env * tail_level_;
254
255 double body = 0.0;
256 if (body_level_ > 0.0) {
257 body_phase_ += body_hz_ / sample_rate();
258 if (body_phase_ >= 1.0) body_phase_ -= std::floor(body_phase_);
259 body = std::sin(2.0 * 3.14159265358979323846 * body_phase_) *
260 tail_env * body_level_;
261 }
262
263 const double centre = room + body;
264
265 RenderFrame frame;
266 frame.mono = static_cast<float>(
267 output_.process(static_cast<float>(train + centre)) * velocity_gain_);
268 // The side channel carries only the burst train. Any nonlinear
269 // interaction between the train and centred layers remains in the
270 // mid channel, which preserves the canonical mono sum exactly without
271 // allowing room or body energy to leak into stereo width.
272 if (render_burst_side) {
273 frame.burst =
274 burst_output_.process(static_cast<float>(train)) * velocity_gain_;
275 }
276 frame.burst_pan = delayed_burst_pan();
277 return frame;
278 }
279
280 void reset_pan_delay() {
281 burst_pan_delay_.fill(burst_pan_);
282 burst_pan_write_ = 0;
283 }
284
285 double delayed_burst_pan() {
286 burst_pan_delay_[static_cast<std::size_t>(burst_pan_write_)] =
287 burst_pan_;
288 const int size = static_cast<int>(burst_pan_delay_.size());
289 const int delay = burst_output_.latency_samples();
290 const int read = (burst_pan_write_ - delay + size) % size;
291 const double delayed =
292 burst_pan_delay_[static_cast<std::size_t>(read)];
293 burst_pan_write_ = (burst_pan_write_ + 1) % size;
294 return delayed;
295 }
296
297 // A fixed seed for the gap sequence, kept separate from the voice's audio
298 // noise so that changing the jitter does not also change the noise the
299 // bursts are made of.
300 static constexpr std::uint32_t kJitterSeed = 0x2545F491u;
301
302 // Next gap in samples, perturbed by up to +/- half the jitter amount.
303 int next_gap() {
304 if (gap_jitter_ <= 0.0) return spacing_samples_;
305 jitter_state_ ^= jitter_state_ << 13;
306 jitter_state_ ^= jitter_state_ >> 17;
307 jitter_state_ ^= jitter_state_ << 5;
308 const double bipolar =
309 static_cast<double>(jitter_state_) * (1.0 / 2147483648.0) - 1.0;
310 const double scaled =
311 static_cast<double>(spacing_samples_) * (1.0 + 0.5 * gap_jitter_ * bipolar);
312 return std::max(1, static_cast<int>(scaled));
313 }
314
315 int burst_count_ = 4;
316 double spacing_ms_ = 11.0;
317 double burst_decay_ms_ = 6.0;
318 double falloff_ = 0.82;
319 // On by default: an unjittered train combs audibly, and a clap that needs
320 // a control turned on to stop sounding wrong is a clap with a bad default.
321 double gap_jitter_ = 0.35;
322 bool alternate_polarity_ = false;
323 double stereo_width_ = 1.0;
324 double tail_level_ = 0.35;
325 double tail_decay_ms_ = 180.0;
326 double cutoff_hz_ = 1400.0;
327 double resonance_ = 1.4;
328 double body_level_ = 0.0;
329 double body_hz_ = 300.0;
330
331 NoiseSource noise_;
332 DecayEnvelope64 burst_env_;
333 DecayEnvelope64 tail_env_;
334 Svf filter_;
335 OutputStage output_;
336 OutputStage burst_output_;
337 bool stereo_path_active_ = false;
338 static constexpr int kMaxOutputLatency =
340 std::array<double, kMaxOutputLatency + 1> burst_pan_delay_{};
341 int burst_pan_write_ = 0;
342
343 double velocity_gain_ = 1.0;
344 double brightness_ = 1.0;
345 double body_phase_ = 0.0;
346
347 int spacing_samples_ = 1;
348 int next_burst_ = 0;
349 int samples_until_burst_ = 0;
350 double burst_gain_ = 1.0;
351 double burst_sign_ = 1.0;
352 double burst_pan_ = -1.0;
353 std::uint32_t jitter_state_ = kJitterSeed;
354};
355
356} // namespace pulp::signal::drum
SampleType process()
Next envelope sample in [0, peak].
Definition decay_envelope.hpp:125
void trigger(SampleType peak=SampleType{1})
Starts the envelope from silence.
Definition decay_envelope.hpp:101
SampleType process()
Next sample, normalised so every colour has approximately the same RMS.
Definition noise_source.hpp:111
SampleType process(SampleType input)
Definition svf.hpp:54
void set_noise_color(NoiseColor color)
Definition clap.hpp:103
void set_cutoff_hz(double hz)
Centre and resonance of the bandpass the noise is heard through.
Definition clap.hpp:100
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition clap.hpp:153
static constexpr int max_bursts
Upper bound on the burst train.
Definition clap.hpp:53
void set_gap_jitter(double amount)
How much each gap varies from the nominal spacing, 0 to 1.
Definition clap.hpp:80
const OutputStage & output() const
Definition clap.hpp:112
void set_burst_decay_ms(double ms)
How quickly each burst decays.
Definition clap.hpp:69
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition clap.hpp:135
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition clap.hpp:113
void set_output_oversampling(OutputOversampling factor) override
Definition clap.hpp:120
void set_burst_count(int count)
How many bursts the train contains.
Definition clap.hpp:63
void set_burst_spacing_ms(double ms)
Milliseconds between bursts.
Definition clap.hpp:66
void set_burst_falloff(double ratio)
How much quieter each burst is than the one before it, as a ratio.
Definition clap.hpp:74
void set_body_hz(double hz)
Definition clap.hpp:109
void set_alternate_polarity(bool alternate)
Invert every other burst.
Definition clap.hpp:85
void set_tail_decay_ms(double ms)
Definition clap.hpp:95
void set_tail_level(double level)
Level and decay of the tail that fuses the train.
Definition clap.hpp:94
void set_resonance(double q)
Definition clap.hpp:101
OutputStage & output()
Definition clap.hpp:111
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition clap.hpp:125
void set_body_level(double level)
Level of an optional sine under the train, at set_body_hz.
Definition clap.hpp:108
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition clap.hpp:202
ClapVoice()
Definition clap.hpp:55
void set_stereo_width(double width)
Alternating burst width, 0 for mono through 1 for hard left/right.
Definition clap.hpp:89
void render_add_stereo(float *left, float *right, int num_samples) override
Optional stereo realization.
Definition clap.hpp:211
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition clap.hpp:114
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition clap.hpp:117
bool on_is_active() const override
Definition clap.hpp:197
static constexpr int latency_samples_for(OutputOversampling factor) noexcept
Definition output_stage.hpp:97
SampleType process(SampleType input)
Definition output_stage.hpp:204
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
@ x4
Definition output_stage.hpp:23
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
@ size
Definition config.hpp:216
std::uint64_t read(std::span< const std::uint8_t > b, std::size_t p, unsigned n) noexcept
Definition spectral_frame_codec.hpp:111
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
SvfT< float > Svf
Definition svf.hpp:148
@ delay
Definition envelope.hpp:57
@ 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
@ bipolar
Definition nonlinear_shaping.hpp:436
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33