Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
tom.hpp
Go to the documentation of this file.
1#pragma once
2
10
11#include <algorithm>
12#include <array>
13#include <cmath>
14#include <cstdint>
15#include <string_view>
16
17namespace pulp::signal::drum {
18
49class TomVoice : public Voice {
50 public:
56 enum class LadderMathProfile : std::uint8_t {
59 };
60
65 enum class Wave { triangle, sine };
66
77
78 struct PresetData {
80 std::string_view name;
81 double tune_hz;
83 double bend_ms;
84 double decay_ms;
90 };
91
95 static constexpr std::array<PresetData, 8> presets{{
96 {Preset::generic_tom, "Generic Tom", 130.0, 0.35, 30.0, 450.0,
97 Wave::triangle, 0.25, 2200.0, 0.25, 0.35},
98 {Preset::low_tom, "LowTom", 75.0, 0.80, 45.0, 700.0,
99 Wave::triangle, 0.08, 1100.0, 0.25, 0.30},
100 {Preset::mid_tom, "MidTom", 110.0, 0.90, 35.0, 550.0,
101 Wave::triangle, 0.10, 1600.0, 0.28, 0.32},
102 {Preset::hi_tom, "HiTom", 170.0, 1.00, 28.0, 400.0,
103 Wave::triangle, 0.12, 2400.0, 0.30, 0.35},
104 {Preset::bass, "Bass", 55.0, 0.55, 55.0, 1100.0,
105 Wave::sine, 0.04, 900.0, 0.18, 0.22},
106 {Preset::snary, "Snary", 155.0, 0.25, 18.0, 320.0,
107 Wave::triangle, 0.70, 2500.0, 0.82, 0.50},
108 {Preset::zap, "Zap", 210.0, 3.20, 55.0, 500.0,
109 Wave::triangle, 0.02, 3200.0, 0.20, 0.15},
110 {Preset::cosmic, "Cosmic", 280.0, 2.40, 180.0, 1800.0,
111 Wave::sine, 0.20, 6000.0, 0.70, 0.25},
112 }};
113
116 r.level_db = 15.0f;
117 // The signature coupling: velocity deepens the dive.
118 r.bend_octaves = 1.5f;
119 r.brightness_octaves = 1.0f;
121
122 click_.set_level(0.35);
123 click_.set_cutoff_hz(5000.0);
124 click_.set_decay_ms(2.0);
125 }
126
128 void set_tune_hz(double hz) { tune_hz_ = std::clamp(hz, 30.0, 1200.0); }
129
133 void set_bend_octaves(double octaves) { bend_octaves_ = std::clamp(octaves, 0.0, 6.0); }
134 void set_bend_ms(double ms) { bend_ms_ = std::clamp(ms, 0.5, 500.0); }
135
137 void set_decay_ms(double ms) { decay_ms_ = std::clamp(ms, 10.0, 4000.0); }
138
139 void set_wave(Wave wave) { wave_ = wave; }
140
142 void set_noise_balance(double amount) { noise_balance_ = std::clamp(amount, 0.0, 1.0); }
143
146 void set_noise_cutoff_hz(double hz) { noise_cutoff_hz_ = std::clamp(hz, 50.0, 16000.0); }
147 void set_noise_resonance(double amount) {
148 noise_resonance_ = std::clamp(amount, 0.0, 1.0);
149 }
150
151 void set_noise_color(NoiseColor color) { noise_.set_color(color); }
152
156 if (profile != LadderMathProfile::reference &&
158 return false;
159 if (is_active()) return false;
160 if (profile != ladder_math_profile_) noise_filter_.reset();
161 ladder_math_profile_ = profile;
162 return true;
163 }
164
166 return ladder_math_profile_;
167 }
168
171 void set_click_level(double level) { click_.set_level(level); }
172 void set_click_cutoff_hz(double hz) { click_.set_cutoff_hz(hz); }
173 void set_click_decay_ms(double ms) { click_.set_decay_ms(ms); }
174
175 void apply_preset(Preset preset) {
176 const auto it = std::find_if(
177 presets.begin(), presets.end(),
178 [preset](const PresetData& data) { return data.id == preset; });
179 if (it == presets.end()) return;
180 set_tune_hz(it->tune_hz);
181 set_bend_octaves(it->bend_octaves);
182 set_bend_ms(it->bend_ms);
183 set_decay_ms(it->decay_ms);
184 set_wave(it->wave);
185 set_noise_balance(it->noise_balance);
186 set_noise_cutoff_hz(it->noise_cutoff_hz);
187 set_noise_resonance(it->noise_resonance);
188 set_click_level(it->click_level);
189 }
190
197 LofiChain& body_lofi() { return body_lofi_; }
198 const LofiChain& body_lofi() const { return body_lofi_; }
199 LofiChain& noise_lofi() { return noise_lofi_; }
200 const LofiChain& noise_lofi() const { return noise_lofi_; }
201
202 OutputStage& output() { return output_; }
203 const OutputStage& output() const { return output_; }
204 OutputStage* output_stage() noexcept override { return &output_; }
205 int latency_samples() const noexcept override {
206 return output_.latency_samples();
207 }
208 OutputOversampling output_oversampling() const noexcept override {
209 return output_.oversampling();
210 }
212 output_.set_oversampling(factor);
213 }
214
215protected:
216 void on_prepare(double sample_rate) override {
217 noise_.prepare(sample_rate);
218 amp_env_.set_sample_rate(sample_rate);
219 bend_env_.set_sample_rate(sample_rate);
220 click_.prepare(sample_rate);
221 noise_filter_.set_sample_rate(static_cast<float>(sample_rate));
222 click_hp_.prepare(static_cast<float>(sample_rate));
223 body_lofi_.set_sample_rate(sample_rate);
224 noise_lofi_.set_sample_rate(sample_rate);
225 output_.prepare(sample_rate);
226 }
227
228 void on_reset() override {
229 amp_env_.reset();
230 bend_env_.reset();
231 click_.reset();
232 click_hp_.reset();
233 // The skin's 4-pole ladder holds four stages of state and a saturated
234 // feedback sample. Leaving them behind made reset() a partial clear: a
235 // voice reset to silence still carried the previous hit inside its
236 // filter, so the same hit rendered twice from a reset voice did not
237 // produce the same samples.
238 noise_filter_.reset();
239 body_lofi_.reset();
240 noise_lofi_.reset();
241 output_.reset();
242 noise_.reset();
243 phase_ = 0.0;
244 }
245
246 void on_note_on(float velocity) override {
247 output_.reset_nonlinear_state();
248 output_.trigger();
249 const auto& response = velocity_response();
250 velocity_gain_ = response.gain(velocity);
251 // The bend deepens with velocity; the decay deliberately does not, so a
252 // soft hit is a shallower swoop of the same length rather than a
253 // shorter note.
254 applied_bend_ = bend_octaves_ + response.bend(velocity);
255 brightness_ = response.brightness_scale(velocity);
256
257 noise_.reset();
258 phase_ = 0.0;
259 click_hp_.reset();
260
261 bend_env_.set_attack_ms(0.0);
262 bend_env_.set_decay_time_constant_ms(bend_ms_);
263 bend_env_.trigger();
264
265 amp_env_.set_attack_ms(0.5);
266 amp_env_.set_decay_t60_ms(decay_ms_);
267 amp_env_.trigger();
268
269 click_.trigger(brightness_);
270
271 noise_filter_.set_frequency(static_cast<float>(
272 std::min(noise_cutoff_hz_ * brightness_, 0.45 * sample_rate())));
273 noise_filter_.set_resonance(static_cast<float>(noise_resonance_));
274 }
275
276 bool on_is_active() const override {
277 return amp_env_.is_active() || click_.is_active() || body_lofi_.has_tail() ||
278 noise_lofi_.has_tail() || output_.has_tail();
279 }
280
281 void render_add(float* out, int num_samples) override {
282 if (ladder_math_profile_ == LadderMathProfile::realtime_efficient)
283 render_add_profiled<true>(out, num_samples);
284 else
285 render_add_profiled<false>(out, num_samples);
286 }
287
288private:
289 template <bool RealtimeEfficient>
290 void render_add_profiled(float* out, int num_samples) {
291 const double oscillator_mix = 1.0 - noise_balance_;
292
293 for (int i = 0; i < num_samples; ++i) {
294 // Two envelopes, read independently. The bend has usually finished
295 // long before the amplitude has.
296 const double bend = bend_env_.process();
297 const double amplitude = amp_env_.process();
298
299 double body = 0.0;
300 if (oscillator_mix > 0.0) {
301 const double f = std::min(tune_hz_ * std::exp2(applied_bend_ * bend),
302 0.49 * sample_rate());
303 phase_ += f / sample_rate();
304 if (phase_ >= 1.0) phase_ -= std::floor(phase_);
305 body = oscillator_mix * (wave_ == Wave::triangle
306 ? 2.0 * std::fabs(2.0 * phase_ - 1.0) - 1.0
307 : std::sin(2.0 * 3.14159265358979323846 * phase_));
308 }
309
310 // Each path is degraded before the mix, which is the whole point
311 // of carrying two chains: crushing the sum could not tell the body
312 // from the skin.
313 body = static_cast<double>(body_lofi_.process(static_cast<float>(body)));
314
315 double hiss = 0.0;
316 if (noise_balance_ > 0.0) {
317 hiss = noise_balance_ *
318 static_cast<double>(RealtimeEfficient
319 ? noise_filter_.process_realtime_efficient(noise_.process())
320 : noise_filter_.process_reference(noise_.process()));
321 }
322 hiss = static_cast<double>(noise_lofi_.process(static_cast<float>(hiss)));
323
324 const double beater =
325 click_hp_.process_highpass(static_cast<float>(
326 click_.process(static_cast<double>(noise_.white()))));
327
328 const double mixed = (body + hiss) * amplitude + beater;
329 out[i] += static_cast<float>(
330 output_.process(static_cast<float>(mixed)) * velocity_gain_);
331 }
332 }
333
334 double tune_hz_ = 120.0;
335 double bend_octaves_ = 1.0;
336 double bend_ms_ = 30.0;
337 double decay_ms_ = 500.0;
338 Wave wave_ = Wave::triangle;
339 double noise_balance_ = 0.15;
340 double noise_cutoff_hz_ = 1500.0;
341 double noise_resonance_ = 0.3;
342 LadderMathProfile ladder_math_profile_ = LadderMathProfile::reference;
343
344 NoiseSource noise_;
345 DecayEnvelope64 amp_env_;
346 DecayEnvelope64 bend_env_;
347 ClickLayer click_;
348 LadderFilter noise_filter_;
349 LofiChain body_lofi_;
350 LofiChain noise_lofi_;
351 TptFilter click_hp_;
352 OutputStage output_;
353
354 double velocity_gain_ = 1.0;
355 double applied_bend_ = 1.0;
356 double brightness_ = 1.0;
357 double phase_ = 0.0;
358};
359
360} // namespace pulp::signal::drum
SampleType process()
Next envelope sample in [0, peak].
Definition decay_envelope.hpp:125
SampleType process_reference(SampleType input)
Process one sample with exact std::tanh stages.
Definition ladder_filter.hpp:40
SampleType process_realtime_efficient(SampleType input)
Process one sample with the Padé FastMath::tanh stages.
Definition ladder_filter.hpp:45
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
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
SampleType process(SampleType input)
Definition output_stage.hpp:204
void set_noise_resonance(double amount)
Definition tom.hpp:147
LadderMathProfile ladder_math_profile() const noexcept
Definition tom.hpp:165
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition tom.hpp:216
const LofiChain & body_lofi() const
Definition tom.hpp:198
const LofiChain & noise_lofi() const
Definition tom.hpp:200
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition tom.hpp:208
void set_noise_cutoff_hz(double hz)
Corner and resonance of the four-pole lowpass the noise runs through.
Definition tom.hpp:146
void set_tune_hz(double hz)
Pitch the body settles at, in Hz.
Definition tom.hpp:128
void set_click_level(double level)
The beater.
Definition tom.hpp:171
void set_noise_balance(double amount)
Balance between the oscillator (0) and the filtered noise (1).
Definition tom.hpp:142
bool set_ladder_math_profile(LadderMathProfile profile) noexcept
Selects the noise ladder's saturation curve between hits.
Definition tom.hpp:155
void set_click_cutoff_hz(double hz)
Definition tom.hpp:172
void set_bend_ms(double ms)
Definition tom.hpp:134
OutputStage & output()
Definition tom.hpp:202
Wave
Body waveform.
Definition tom.hpp:65
@ sine
Definition tom.hpp:65
@ triangle
Definition tom.hpp:65
TomVoice()
Definition tom.hpp:114
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition tom.hpp:205
bool on_is_active() const override
Definition tom.hpp:276
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition tom.hpp:228
void set_decay_ms(double ms)
How long the note rings, as a T60. Unrelated to the bend.
Definition tom.hpp:137
static constexpr std::array< PresetData, 8 > presets
Original Pulp-authored voicings of this one SDS-V-family topology.
Definition tom.hpp:95
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition tom.hpp:281
void set_wave(Wave wave)
Definition tom.hpp:139
void set_bend_octaves(double octaves)
Depth of the downward dive in octaves, and how quickly it lands.
Definition tom.hpp:133
LofiChain & noise_lofi()
Definition tom.hpp:199
void apply_preset(Preset preset)
Definition tom.hpp:175
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition tom.hpp:204
void set_output_oversampling(OutputOversampling factor) override
Definition tom.hpp:211
void set_noise_color(NoiseColor color)
Definition tom.hpp:151
void set_click_decay_ms(double ms)
Definition tom.hpp:173
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition tom.hpp:246
LofiChain & body_lofi()
Independent degradation for the two halves of the drum.
Definition tom.hpp:197
const OutputStage & output() const
Definition tom.hpp:203
Preset
Definition tom.hpp:67
@ mid_tom
Definition tom.hpp:70
@ generic_tom
Definition tom.hpp:68
@ low_tom
Definition tom.hpp:69
LadderMathProfile
The noise ladder's saturator, chosen per voice.
Definition tom.hpp:56
bool is_active() const
Whether the voice still has something to render.
Definition voice.hpp:97
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
TptFilterT< float > TptFilter
Definition tpt_filter.hpp:172
LadderFilterT< float > LadderFilter
Definition ladder_filter.hpp:127
@ 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
double noise_resonance
Definition tom.hpp:88
double bend_octaves
Definition tom.hpp:82
double decay_ms
Definition tom.hpp:84
double tune_hz
Definition tom.hpp:81
double click_level
Definition tom.hpp:89
double bend_ms
Definition tom.hpp:83
double noise_cutoff_hz
Definition tom.hpp:87
std::string_view name
Definition tom.hpp:80
Preset id
Definition tom.hpp:79
double noise_balance
Definition tom.hpp:86
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33
First-order topology-preserving transform (TPT) filter.