Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
membrane.hpp
Go to the documentation of this file.
1#pragma once
2
9
10#include <algorithm>
11#include <array>
12#include <cmath>
13#include <vector>
14
15namespace pulp::signal::drum {
16
25
63class MembraneVoice : public Voice {
64public:
66 static constexpr int mode_count = 6;
67
70 static constexpr double membrane_ratios[mode_count] = {1.0, 1.5933, 2.1355,
71 2.2954, 2.6531, 2.9173};
72
75 r.level_db = 16.0f;
76 // A harder strike tightens the head slightly and excites more of the
77 // high modes -- both are brightness rather than level.
78 r.bend_octaves = 0.15f;
79 r.brightness_octaves = 1.6f;
81 }
82
84 void set_tune_hz(double hz) { tune_hz_ = std::clamp(hz, 20.0, 2000.0); }
85
87 void set_structure(double amount) { structure_ = std::clamp(amount, 0.0, 1.0); }
88
90 void set_stretch(double amount) { stretch_ = std::clamp(amount, 0.0, 1.0); }
91
93 void set_decay_ms(double ms) { decay_ms_ = std::clamp(ms, 20.0, 8000.0); }
94
97 void set_damping(double amount) { damping_ = std::clamp(amount, 0.0, 1.0); }
98
100 void set_brightness(double amount) { brightness_ = std::clamp(amount, 0.0, 1.0); }
101
106 void set_position(double position) { position_ = std::clamp(position, 0.02, 0.5); }
107
110 void set_spread(double amount) { spread_ = std::clamp(amount, 0.0, 1.0); }
111
114 void set_exciter_ms(double ms) { exciter_ms_ = std::clamp(ms, 0.1, 50.0); }
115 void set_exciter_cutoff_hz(double hz) {
116 exciter_cutoff_hz_ = std::clamp(hz, 100.0, 18000.0);
117 }
118 void set_exciter(MembraneExciter exciter) { exciter_ = exciter; }
119
123 void set_sub_level(double level) { sub_level_ = std::max(level, 0.0); }
124 void set_air_level(double level) { air_level_ = std::max(level, 0.0); }
125 void set_air_decay_ms(double ms) { air_decay_ms_ = std::clamp(ms, 1.0, 500.0); }
126 void set_click_level(double level) { click_level_ = std::max(level, 0.0); }
127 void set_click_decay_ms(double ms) {
128 click_decay_ms_ = std::clamp(ms, 0.1, 50.0);
129 }
130
134 double mode_gain_energy() const { return mode_gain_energy_; }
135
137 LowpassGate& gate() { return gate_; }
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 bank_.prepare(sample_rate, mode_count, 1);
155 exciter_env_.set_sample_rate(sample_rate);
156 gate_env_.set_sample_rate(sample_rate);
157 sub_env_.set_sample_rate(sample_rate);
158 air_env_.set_sample_rate(sample_rate);
159 click_env_.set_sample_rate(sample_rate);
160 exciter_filter_.prepare(static_cast<float>(sample_rate));
161 air_filter_.prepare(static_cast<float>(sample_rate));
162 click_filter_.prepare(static_cast<float>(sample_rate));
163 gate_.set_sample_rate(sample_rate);
164 output_.prepare(sample_rate);
165 scratch_.assign(kBlock, 0.0f);
166 modes_.fill(ModalMode{});
167 }
168
169 void on_reset() override {
170 bank_.reset();
171 exciter_env_.reset();
172 gate_env_.reset();
173 sub_env_.reset();
174 air_env_.reset();
175 click_env_.reset();
176 exciter_filter_.reset();
177 air_filter_.reset();
178 click_filter_.reset();
179 gate_.reset();
180 output_.reset();
181 noise_.reset();
182 air_noise_.set_seed(NoiseSource::default_seed ^ 0xA17F32D1u);
183 click_noise_.set_seed(NoiseSource::default_seed ^ 0xC11C74E5u);
184 air_noise_.reset();
185 click_noise_.reset();
186 pluck_pending_ = false;
187 sub_phase_ = 0.0;
188 applied_fundamental_hz_ = tune_hz_;
189 level_ = 0.0;
190 }
191
192 void on_note_on(float velocity) override {
193 // The modal bank carries its resonator state across strikes, so the
194 // shared output history belongs to the same continuing body.
195 output_.trigger();
196 const auto& response = velocity_response();
197 velocity_gain_ = response.gain(velocity);
198 const double tension = std::exp2(response.bend(velocity));
199 applied_fundamental_hz_ = tune_hz_ * tension;
200 const double exciter_scale = response.brightness_scale(velocity);
201
202 noise_.reset();
203 air_noise_.reset();
204 click_noise_.reset();
205 build_modes(tension);
206 bank_.set_modes(std::span<const ModalMode>(modes_.data(), mode_count));
207
208 exciter_filter_.reset();
209 exciter_filter_.set_cutoff(static_cast<float>(
210 std::min(exciter_cutoff_hz_ * exciter_scale, 0.49 * sample_rate())));
211
212 pluck_pending_ = exciter_ == MembraneExciter::pluck;
213 if (exciter_ == MembraneExciter::noise_burst) {
214 exciter_env_.set_attack_ms(0.0);
215 exciter_env_.set_decay_time_constant_ms(exciter_ms_);
216 exciter_env_.trigger();
217 } else {
218 exciter_env_.reset();
219 }
220
221 sub_phase_ = 0.0;
222 sub_env_.reset();
223 sub_env_.set_attack_ms(0.0);
224 sub_env_.set_decay_t60_ms(decay_ms_);
225 if (sub_level_ > 0.0) sub_env_.trigger();
226
227 air_filter_.reset();
228 air_filter_.set_cutoff(static_cast<float>(
229 std::min(4000.0 * exciter_scale, 0.49 * sample_rate())));
230 air_env_.reset();
231 air_env_.set_attack_ms(0.0);
232 air_env_.set_decay_time_constant_ms(air_decay_ms_);
233 if (air_level_ > 0.0) air_env_.trigger();
234
235 click_filter_.reset();
236 click_filter_.set_cutoff(static_cast<float>(
237 std::min(8000.0 * exciter_scale, 0.49 * sample_rate())));
238 click_env_.reset();
239 click_env_.set_attack_ms(0.0);
240 click_env_.set_decay_time_constant_ms(click_decay_ms_);
241 if (click_level_ > 0.0) click_env_.trigger();
242
243 // The gate's control is a fast one-shot: it opens on the strike and the
244 // vactrol's own slow fall does the rest, which is the point of using a
245 // gate rather than an envelope.
246 gate_env_.set_attack_ms(0.5);
247 gate_env_.set_decay_t60_ms(decay_ms_ * 0.5);
248 gate_env_.trigger();
249 level_ = 1.0;
250 }
251
252 bool on_is_active() const override {
253 return pluck_pending_ || exciter_env_.is_active() || gate_env_.is_active() ||
254 sub_env_.is_active() || air_env_.is_active() ||
255 click_env_.is_active() ||
256 level_ > kSilenceLevel || output_.has_tail();
257 }
258
259 void render_add(float* out, int num_samples) override {
260 int done = 0;
261 while (done < num_samples) {
262 const int n = std::min<int>(kBlock, num_samples - done);
263
264 // The bank renders a block at a time, so the exciter is built into
265 // a scratch first rather than sample-interleaved.
266 for (int i = 0; i < n; ++i) {
267 double burst = 0.0;
268 if (pluck_pending_) {
269 burst = 1.0;
270 pluck_pending_ = false;
271 } else if (exciter_env_.is_active()) {
272 burst = static_cast<double>(noise_.white()) *
273 exciter_env_.process();
274 }
275 scratch_[static_cast<std::size_t>(i)] =
276 exciter_filter_.process_lowpass(static_cast<float>(burst));
277 }
278
279 std::fill(body_.begin(), body_.begin() + n, 0.0f);
280 bank_.process_add(scratch_.data(), body_.data(), n);
281
282 for (int i = 0; i < n; ++i) {
283 const double gated = gate_.process(body_[static_cast<std::size_t>(i)],
284 gate_env_.process());
285 double layers = 0.0;
286 if (sub_env_.is_active()) {
287 sub_phase_ +=
288 0.5 * applied_fundamental_hz_ / sample_rate();
289 if (sub_phase_ >= 1.0) sub_phase_ -= std::floor(sub_phase_);
290 layers += std::sin(2.0 * 3.14159265358979323846 * sub_phase_) *
291 sub_env_.process() * sub_level_;
292 }
293 if (air_env_.is_active()) {
294 layers += static_cast<double>(air_filter_.process_highpass(
295 air_noise_.white())) *
296 air_env_.process() * air_level_;
297 }
298 if (click_env_.is_active()) {
299 layers += static_cast<double>(click_filter_.process_lowpass(
300 click_noise_.white())) *
301 click_env_.process() * click_level_;
302 }
303 const double shaped =
304 output_.process(static_cast<float>(gated + layers)) * velocity_gain_;
305 out[done + i] += static_cast<float>(shaped);
306 level_ = std::max(std::fabs(shaped), level_ * kLevelDecay);
307 }
308 done += n;
309 }
310 }
311
312private:
313 static constexpr int kBlock = 128;
314 static constexpr double kSilenceLevel = 1e-5;
315 static constexpr double kLevelDecay = 0.9995;
316
317 void build_modes(double tension) {
318 // A deterministic detune per mode, drawn from the voice's own generator
319 // so a hit stays reproducible.
320 double raw_gain_energy = 0.0;
321 for (int m = 0; m < mode_count; ++m) {
322 const double harmonic = static_cast<double>(m + 1);
323 const double ratio = harmonic + structure_ * (membrane_ratios[m] - harmonic);
324 // Stiffness sharpens the upper modes progressively, which is what
325 // separates a plate from an ideal membrane.
326 const double stiffened =
327 ratio * (1.0 + stretch_ * 0.02 * static_cast<double>(m * m));
328 const double detune = 1.0 + spread_ * 0.03 * static_cast<double>(noise_.white());
329 const double f = tune_hz_ * tension * stiffened * detune;
330
331 // Upper modes die faster. At damping 0 they all ring equally, which
332 // is a bell; a drum needs the spread.
333 const double decay_scale =
334 std::pow(0.55, damping_ * static_cast<double>(m));
335
336 // Strike position is a comb: a mode with a node at the strike point
337 // simply is not excited.
338 const double comb = std::fabs(
339 std::sin(3.14159265358979323846 * static_cast<double>(m + 1) * position_));
340 const double rolloff =
341 std::pow(0.6 + 0.4 * brightness_, static_cast<double>(m));
342
343 const double raw_gain = comb * rolloff;
344 raw_gain_energy += raw_gain * raw_gain;
345 modes_[static_cast<std::size_t>(m)] = ModalMode{
346 static_cast<float>(std::min(f, 0.49 * sample_rate())),
347 static_cast<float>(0.001 * decay_ms_ * decay_scale),
348 static_cast<float>(raw_gain),
349 };
350 }
351
352 // Changing strike position selects modes; it must not also act as a
353 // hidden master level. Preserve the energy of six equal 1/N gains.
354 const double target_energy = 1.0 / static_cast<double>(mode_count);
355 const double scale =
356 raw_gain_energy > 0.0 ? std::sqrt(target_energy / raw_gain_energy) : 0.0;
357 mode_gain_energy_ = 0.0;
358 for (auto& mode : modes_) {
359 mode.gain = static_cast<float>(static_cast<double>(mode.gain) * scale);
360 mode_gain_energy_ +=
361 static_cast<double>(mode.gain) * static_cast<double>(mode.gain);
362 }
363 }
364
365 double tune_hz_ = 90.0;
366 double structure_ = 1.0;
367 double stretch_ = 0.0;
368 double decay_ms_ = 700.0;
369 double damping_ = 0.5;
370 double brightness_ = 0.5;
371 double position_ = 0.28;
372 double spread_ = 0.1;
373 double exciter_ms_ = 1.5;
374 double exciter_cutoff_hz_ = 6000.0;
376 double sub_level_ = 0.0;
377 double air_level_ = 0.0;
378 double air_decay_ms_ = 20.0;
379 double click_level_ = 0.0;
380 double click_decay_ms_ = 2.0;
381
382 NoiseSource noise_;
383 NoiseSource air_noise_;
384 NoiseSource click_noise_;
385 ModalBank bank_;
386 DecayEnvelope64 exciter_env_;
387 DecayEnvelope64 gate_env_;
388 DecayEnvelope64 sub_env_;
389 DecayEnvelope64 air_env_;
390 DecayEnvelope64 click_env_;
391 TptFilter exciter_filter_;
392 TptFilter air_filter_;
393 TptFilter click_filter_;
394 LowpassGate gate_;
395 OutputStage output_;
396
397 std::array<ModalMode, mode_count> modes_{};
398 std::vector<float> scratch_;
399 std::array<float, kBlock> body_{};
400
401 double velocity_gain_ = 1.0;
402 double mode_gain_energy_ = 0.0;
403 double sub_phase_ = 0.0;
404 double applied_fundamental_hz_ = 100.0;
405 bool pluck_pending_ = false;
406 double level_ = 0.0;
407};
408
409} // namespace pulp::signal::drum
SampleType white()
Raw uniform white sample in [-1, 1), bypassing the colour filter and the normalisation.
Definition noise_source.hpp:123
static constexpr std::uint32_t default_seed
Definition noise_source.hpp:55
const OutputStage & output() const
Definition membrane.hpp:139
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition membrane.hpp:152
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition membrane.hpp:140
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition membrane.hpp:259
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition membrane.hpp:141
void set_stretch(double amount)
Extra progressive sharpening of the upper modes, as a stiff plate has.
Definition membrane.hpp:90
void set_spread(double amount)
Random detuning of the modes, drawn once per hit.
Definition membrane.hpp:110
void set_click_decay_ms(double ms)
Definition membrane.hpp:127
MembraneVoice()
Definition membrane.hpp:73
void set_tune_hz(double hz)
Fundamental of the body, in Hz.
Definition membrane.hpp:84
void set_sub_level(double level)
Optional layers around the modal body.
Definition membrane.hpp:123
void set_exciter_cutoff_hz(double hz)
Definition membrane.hpp:115
static constexpr int mode_count
Number of modes the body carries.
Definition membrane.hpp:66
OutputStage & output()
Definition membrane.hpp:138
void set_damping(double amount)
How much faster the upper modes die than the fundamental.
Definition membrane.hpp:97
void set_exciter(MembraneExciter exciter)
Definition membrane.hpp:118
void set_decay_ms(double ms)
How long the fundamental rings, as a T60 in milliseconds.
Definition membrane.hpp:93
void set_exciter_ms(double ms)
Length of the exciter burst and the corner it is filtered at.
Definition membrane.hpp:114
void set_output_oversampling(OutputOversampling factor) override
Definition membrane.hpp:147
void set_structure(double amount)
Blend from a harmonic series (0) to ideal-membrane ratios (1).
Definition membrane.hpp:87
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition membrane.hpp:169
void set_position(double position)
Where the head is struck, 0 (edge) to 0.5 (midpoint).
Definition membrane.hpp:106
void set_air_level(double level)
Definition membrane.hpp:124
void set_brightness(double amount)
Relative level of the upper modes in the strike.
Definition membrane.hpp:100
LowpassGate & gate()
The lowpass gate that couples the tail's level to its brightness.
Definition membrane.hpp:137
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition membrane.hpp:144
static constexpr double membrane_ratios[mode_count]
Frequency ratios of an ideal circular membrane's first six modes, relative to its fundamental.
Definition membrane.hpp:70
double mode_gain_energy() const
Sum of squared mode gains after the constant-energy normalization.
Definition membrane.hpp:134
void set_air_decay_ms(double ms)
Definition membrane.hpp:125
void set_click_level(double level)
Definition membrane.hpp:126
bool on_is_active() const override
Definition membrane.hpp:252
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition membrane.hpp:192
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
MembraneExciter
What strikes the modal membrane.
Definition membrane.hpp:18
@ pluck
One filtered sample: a true impulse that rings the body without a continuing noise texture.
Definition membrane.hpp:23
@ noise_burst
A short filtered-noise burst: stick or hand depending on its length.
Definition membrane.hpp:20
OutputOversampling
Realtime quality choices for the shared drum output nonlinearity.
Definition output_stage.hpp:20
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
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
@ e
Definition formant_filter_bank.hpp:20
@ gated
Flat body, hard cut.
Definition nonlin_ambience_design.hpp:245
DecayEnvelopeT< double > DecayEnvelope64
Definition decay_envelope.hpp:201
ModalBankT< float > ModalBank
Definition modal_bank.hpp:329
NoiseSourceT< float > NoiseSource
Definition noise_source.hpp:279
@ detune
Sub-semitone offset scaled by the pedal. Dry on.
Definition pitch_shifter.hpp:171
LowpassGateT< float > LowpassGate
Definition lowpass_gate.hpp:138
One resonant mode of a vibrating object: frequency, decay time, and impulse-response amplitude.
Definition modal_bank.hpp:16
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33
First-order topology-preserving transform (TPT) filter.