Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
cymbal.hpp
Go to the documentation of this file.
1#pragma once
2
10
11#include <algorithm>
12#include <array>
13#include <cmath>
14
15namespace pulp::signal::drum {
16
46class CymbalVoice : public Voice {
47public:
49 static constexpr int comb_count = 8;
50
54 static constexpr double comb_ratios[comb_count] = {1.00, 1.47, 1.83, 2.21,
55 2.59, 3.16, 3.71, 4.31};
56
59 r.level_db = 15.0f;
60 r.brightness_octaves = 1.2f;
62 }
63
65 void set_tune_hz(double hz) { tune_hz_ = std::clamp(hz, 40.0, 2000.0); }
66
68 void set_decay_ms(double ms) { decay_ms_ = std::clamp(ms, 50.0, 8000.0); }
69
72 void set_decay_tilt(double tilt) { decay_tilt_ = std::clamp(tilt, 0.5, 1.0); }
73
77 void set_high_mode_emphasis_db(double db) {
78 high_mode_emphasis_db_ = std::clamp(db, -18.0, 18.0);
79 }
80
85 void set_velocity_feedback(double amount) {
86 velocity_feedback_ = std::clamp(amount, 0.0, 1.0);
87 }
88 void set_velocity_high_mode_db(double db) {
89 velocity_high_mode_db_ = std::clamp(db, 0.0, 18.0);
90 }
91
94 void set_upper_highpass_hz(double hz) {
95 upper_highpass_hz_ = std::clamp(hz, 0.0, 8000.0);
96 }
97
100 void set_spread(double amount) { spread_ = std::clamp(amount, 0.0, 1.0); }
101
103 void set_inharmonicity(double amount) {
104 inharmonicity_ = std::clamp(amount, 0.0, 1.0);
105 }
106
109 void set_shift_hz(double hz) { shift_hz_ = std::clamp(hz, -400.0, 400.0); }
110
112 void set_noise_level(double level) { noise_level_ = std::max(level, 0.0); }
113 void set_strike_level(double level) { strike_level_ = std::max(level, 0.0); }
114 void set_strike_ms(double ms) { strike_ms_ = std::clamp(ms, 0.5, 200.0); }
115
118 void set_tone_hz(double hz) { tone_hz_ = std::clamp(hz, 500.0, 20000.0); }
119 void set_low_cut_hz(double hz) { low_cut_hz_ = std::clamp(hz, 20.0, 2000.0); }
120
126 void set_variation(double amount) {
127 variation_ = std::clamp(amount, 0.0, 1.0);
128 set_hit_life(variation_ > 0.0
131 }
132
133 void set_hit_life(HitLifeMode mode) { hit_life_.set_mode(mode); }
134 HitLifeMode hit_life() const { return hit_life_.mode(); }
135
136 void set_noise_color(NoiseColor color) { noise_.set_color(color); }
137
138 OutputStage& output() { return output_; }
139 const OutputStage& output() const { return output_; }
140 OutputStage* output_stage() noexcept override { return &output_; }
141 int latency_samples() const noexcept override {
142 return output_.latency_samples();
143 }
144 OutputOversampling output_oversampling() const noexcept override {
145 return output_.oversampling();
146 }
148 output_.set_oversampling(factor);
149 }
150
151protected:
152 void on_prepare(double sample_rate) override {
153 noise_.prepare(sample_rate);
154 strike_env_.set_sample_rate(sample_rate);
155 // The longest delay the lowest tuning can ask for, plus headroom.
156 const int longest = static_cast<int>(sample_rate / 40.0) + 4;
157 for (auto& comb : combs_) comb.prepare(longest);
158 for (auto& damper : dampers_) damper.prepare(static_cast<float>(sample_rate));
159 for (auto& highpass : upper_highpasses_) {
160 highpass.prepare(static_cast<float>(sample_rate));
161 }
162 shifter_.set_sample_rate(sample_rate);
163 tone_.prepare(static_cast<float>(sample_rate));
164 low_cut_.prepare(static_cast<float>(sample_rate));
165 output_.prepare(sample_rate);
166 }
167
168 void on_reset() override {
169 reset_dsp_memory();
170 strike_env_.reset();
171 output_.reset();
172 noise_.reset();
173 hit_life_.reset();
174 level_ = 0.0;
175 strike_phase_ = 0.0;
176 }
177
178 void on_note_on(float velocity) override {
179 const auto& response = velocity_response();
180 velocity_gain_ = response.gain(velocity);
181 const double brightness = response.brightness_scale(velocity);
182 velocity_ = velocity;
183
184 const auto life = hit_life_.trigger(NoiseSource::default_seed);
185 if (life.reset_dsp_state) {
186 reset_dsp_memory();
187 output_.reset_nonlinear_state();
188 }
189 if (life.reseed_excitation) {
190 noise_.set_seed(life.seed);
191 noise_.reset();
192 }
193 output_.trigger();
194
195 update_combs();
196 shifter_.set_shift_hz(shift_hz_);
197 tone_.set_cutoff(static_cast<float>(
198 std::min(tone_hz_ * brightness, 0.49 * sample_rate())));
199 low_cut_.set_cutoff(static_cast<float>(low_cut_hz_));
200
201 strike_env_.set_attack_ms(0.0);
202 strike_env_.set_decay_time_constant_ms(strike_ms_);
203 strike_env_.trigger();
204 strike_phase_ = 0.0;
205 level_ = 1.0;
206 }
207
208 bool on_is_active() const override {
209 return strike_env_.is_active() || level_ > kSilenceLevel ||
210 output_.has_tail();
211 }
212
213 void render_add(float* out, int num_samples) override {
214 for (int i = 0; i < num_samples; ++i) {
215 const double strike = strike_env_.is_active() ? strike_env_.process() : 0.0;
216
217 double excitation = 0.0;
218 if (strike > 0.0) {
219 excitation = static_cast<double>(noise_.process()) * noise_level_;
220 if (strike_level_ > 0.0) {
221 // A short downward chirp under the noise: the sound of the
222 // stick itself rather than of the metal.
223 strike_phase_ += tune_hz_ * (1.0 + 3.0 * strike) / sample_rate();
224 if (strike_phase_ >= 1.0) strike_phase_ -= std::floor(strike_phase_);
225 excitation += std::sin(2.0 * 3.14159265358979323846 * strike_phase_) *
226 strike_level_;
227 }
228 excitation *= strike;
229 }
230
231 double summed = 0.0;
232 for (int c = 0; c < comb_count; ++c) {
233 const auto index = static_cast<std::size_t>(c);
234 const float delayed =
235 combs_[index].read(static_cast<float>(delay_samples_[index]));
236 float damped = dampers_[index].process_lowpass(delayed);
237 if (c >= kUpperCombFirst) {
238 const float highpassed =
239 upper_highpasses_[index].process_highpass(damped);
240 if (upper_highpass_hz_ > 0.0) damped = highpassed;
241 }
242 combs_[index].push(static_cast<float>(
243 excitation + static_cast<double>(damped) * feedback_[index]));
244 summed += static_cast<double>(delayed) * mode_gain_[index];
245 }
246 summed /= static_cast<double>(comb_count);
247
248 // The de-harmoniser. Everything above it is a chord; everything
249 // after it is a cymbal.
250 const double shifted =
251 shift_hz_ != 0.0
252 ? static_cast<double>(shifter_.process(static_cast<float>(summed)))
253 : summed;
254
255 const double cleared = low_cut_.process_highpass(static_cast<float>(shifted));
256 const double toned = tone_.process_lowpass(static_cast<float>(cleared));
257 const double shaped =
258 output_.process(static_cast<float>(toned)) * velocity_gain_;
259
260 out[i] += static_cast<float>(shaped);
261 level_ = std::max(std::fabs(shaped), level_ * kLevelDecay);
262 }
263 }
264
265private:
266 static constexpr double kSilenceLevel = 1e-5;
267 static constexpr double kLevelDecay = 0.99995;
268
269 void reset_dsp_memory() {
270 for (auto& comb : combs_) comb.reset();
271 for (auto& damper : dampers_) damper.reset();
272 for (auto& highpass : upper_highpasses_) highpass.reset();
273 shifter_.reset();
274 tone_.reset();
275 low_cut_.reset();
276 }
277
278 void update_combs() {
279 for (int c = 0; c < comb_count; ++c) {
280 const auto index = static_cast<std::size_t>(c);
281 // Spread interpolates each comb from unison to its plate ratio;
282 // inharmonicity then pushes the upper ones further out still.
283 const double base = 1.0 + spread_ * (comb_ratios[index] - 1.0);
284 const double pushed =
285 base * (1.0 + inharmonicity_ * 0.01 * static_cast<double>(c * c));
286 const double f = std::min(tune_hz_ * pushed, 0.45 * sample_rate());
287 delay_samples_[index] = std::max(sample_rate() / f, 2.0);
288
289 // A comb's feedback reaching -60 dB after `decay_ms` means losing
290 // that much per round trip, and the trip rate is the comb's own
291 // frequency -- so this is derived rather than dialled, and stays
292 // meaningful when the tuning moves. (Jot's law, applied per comb: a
293 // raw coefficient would mean very different decay lengths at
294 // different comb frequencies.) The modal tilt sits on top of it.
295 const double trips = std::max(0.001 * decay_ms_ * f, 1.0);
296 const double tilt = std::pow(decay_tilt_, static_cast<double>(c));
297 const double upper_position =
298 static_cast<double>(c) / static_cast<double>(comb_count - 1);
299 const double velocity_ring =
300 1.0 + velocity_feedback_ * velocity_ * upper_position;
301 feedback_[index] =
302 std::min(std::pow(10.0, -3.0 / (trips * tilt * velocity_ring)),
303 0.9995);
304
305 mode_gain_[index] = std::pow(
306 10.0, (high_mode_emphasis_db_ +
307 velocity_high_mode_db_ * velocity_) *
308 upper_position / 20.0);
309
310 // Higher combs are damped harder in the loop, so the wash darkens
311 // as it decays instead of staying uniformly bright.
312 const double corner =
313 std::min(0.45 * sample_rate(), 16000.0 * std::pow(0.72, static_cast<double>(c)));
314 dampers_[index].set_cutoff(static_cast<float>(corner));
315 if (c >= kUpperCombFirst) {
316 const double highpass_corner =
317 std::min(0.45 * sample_rate(),
318 std::max(upper_highpass_hz_, 1.0) *
319 std::pow(1.35, static_cast<double>(c - kUpperCombFirst)));
320 upper_highpasses_[index].set_cutoff(
321 static_cast<float>(highpass_corner));
322 }
323 }
324 }
325
326 static constexpr int kUpperCombFirst = comb_count / 2;
327
328 double tune_hz_ = 320.0;
329 double decay_ms_ = 1800.0;
330 double decay_tilt_ = 0.93;
331 double high_mode_emphasis_db_ = 0.0;
332 double velocity_feedback_ = 0.35;
333 double velocity_high_mode_db_ = 4.0;
334 double upper_highpass_hz_ = 500.0;
335 double spread_ = 1.0;
336 double inharmonicity_ = 0.4;
337 double shift_hz_ = 45.0;
338 double noise_level_ = 0.7;
339 double strike_level_ = 0.3;
340 double strike_ms_ = 6.0;
341 double tone_hz_ = 12000.0;
342 double low_cut_hz_ = 300.0;
343 double variation_ = 0.5;
344
345 NoiseSource noise_;
346 // Matches the default `variation_` above: a struck cymbal keeps ringing
347 // through the next hit, and successive excitations differ.
349 std::array<DelayLine, comb_count> combs_;
350 std::array<TptFilter, comb_count> dampers_;
351 std::array<TptFilter, comb_count> upper_highpasses_;
352 DecayEnvelope64 strike_env_;
353 FrequencyShifter shifter_;
354 TptFilter tone_;
355 TptFilter low_cut_;
356 OutputStage output_;
357
358 std::array<double, comb_count> delay_samples_{};
359 std::array<double, comb_count> feedback_{};
360 std::array<double, comb_count> mode_gain_{};
361
362 double velocity_gain_ = 1.0;
363 double velocity_ = 1.0;
364 double level_ = 0.0;
365 double strike_phase_ = 0.0;
366};
367
368} // namespace pulp::signal::drum
void reset()
Definition frequency_shifter.hpp:69
static constexpr std::uint32_t default_seed
Definition noise_source.hpp:55
void reset()
Definition tpt_filter.hpp:127
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition cymbal.hpp:152
void set_upper_highpass_hz(double hz)
Base cutoff of the extra high-pass stages in the upper half of the comb bank.
Definition cymbal.hpp:94
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition cymbal.hpp:168
void set_spread(double amount)
How far the combs are spread from unison (0) to their full plate ratios (1).
Definition cymbal.hpp:100
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition cymbal.hpp:141
void set_decay_tilt(double tilt)
How much faster each successive comb dies than the one below it.
Definition cymbal.hpp:72
void set_noise_color(NoiseColor color)
Definition cymbal.hpp:136
void set_low_cut_hz(double hz)
Definition cymbal.hpp:119
void set_noise_level(double level)
Level of the noise part of the strike, and of the swept tone under it.
Definition cymbal.hpp:112
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition cymbal.hpp:144
HitLifeMode hit_life() const
Definition cymbal.hpp:134
void set_output_oversampling(OutputOversampling factor) override
Definition cymbal.hpp:147
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition cymbal.hpp:140
void set_strike_level(double level)
Definition cymbal.hpp:113
void set_high_mode_emphasis_db(double db)
Extra high-mode level at the top comb, interpolated from zero dB at the bottom comb.
Definition cymbal.hpp:77
void set_decay_ms(double ms)
How long the bank rings, as a T60 in milliseconds.
Definition cymbal.hpp:68
CymbalVoice()
Definition cymbal.hpp:57
OutputStage & output()
Definition cymbal.hpp:138
void set_tune_hz(double hz)
Frequency the lowest comb rings at, in Hz.
Definition cymbal.hpp:65
void set_tone_hz(double hz)
Output tone controls: a lowpass on top and a highpass to clear the mud the shifter leaves at the bott...
Definition cymbal.hpp:118
void set_hit_life(HitLifeMode mode)
Definition cymbal.hpp:133
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition cymbal.hpp:213
const OutputStage & output() const
Definition cymbal.hpp:139
static constexpr int comb_count
Number of parallel combs.
Definition cymbal.hpp:49
void set_shift_hz(double hz)
How many hertz every partial is shifted by.
Definition cymbal.hpp:109
void set_inharmonicity(double amount)
How far the ratios are pushed further apart still, progressively.
Definition cymbal.hpp:103
bool on_is_active() const override
Definition cymbal.hpp:208
static constexpr double comb_ratios[comb_count]
Plate-like ratios for the comb delays.
Definition cymbal.hpp:54
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition cymbal.hpp:178
void set_variation(double amount)
How much successive excitations differ, 0 (bit-identical) to 1.
Definition cymbal.hpp:126
void set_strike_ms(double ms)
Definition cymbal.hpp:114
void set_velocity_high_mode_db(double db)
Definition cymbal.hpp:88
void set_velocity_feedback(double amount)
Velocity-dependent feedback and high-mode emphasis.
Definition cymbal.hpp:85
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
The minimal in-graph frequency shifter: shift in Hz, one knob, no feedback.
Definition clap.hpp:13
HitLifeMode
Selects the intentional source of variation between otherwise identical hits.
Definition hit_life.hpp:26
@ advancing_seed_preserved_body
Definition hit_life.hpp:31
@ fixed_seed_preserved_body
Definition hit_life.hpp:30
OutputOversampling
Realtime quality choices for the shared drum output nonlinearity.
Definition output_stage.hpp:20
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
@ c
Definition spectrum_trace.hpp:35
TptFilterT< float > TptFilter
Definition tpt_filter.hpp:172
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
@ highpass
Definition filter_morph.hpp:19
FrequencyShifterT< float > FrequencyShifter
Definition frequency_shifter.hpp:96
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.