A struck membrane, modelled as its modes rather than as an oscillator. More...
#include <membrane.hpp>
Public Member Functions | |
| MembraneVoice () | |
| void | set_tune_hz (double hz) |
| Fundamental of the body, in Hz. | |
| void | set_structure (double amount) |
| Blend from a harmonic series (0) to ideal-membrane ratios (1). | |
| void | set_stretch (double amount) |
| Extra progressive sharpening of the upper modes, as a stiff plate has. | |
| void | set_decay_ms (double ms) |
| How long the fundamental rings, as a T60 in milliseconds. | |
| void | set_damping (double amount) |
| How much faster the upper modes die than the fundamental. | |
| void | set_brightness (double amount) |
| Relative level of the upper modes in the strike. | |
| void | set_position (double position) |
| Where the head is struck, 0 (edge) to 0.5 (midpoint). | |
| void | set_spread (double amount) |
| Random detuning of the modes, drawn once per hit. | |
| void | set_exciter_ms (double ms) |
| Length of the exciter burst and the corner it is filtered at. | |
| void | set_exciter_cutoff_hz (double hz) |
| void | set_exciter (MembraneExciter exciter) |
| void | set_sub_level (double level) |
| Optional layers around the modal body. | |
| void | set_air_level (double level) |
| void | set_air_decay_ms (double ms) |
| void | set_click_level (double level) |
| void | set_click_decay_ms (double ms) |
| double | mode_gain_energy () const |
| Sum of squared mode gains after the constant-energy normalization. | |
| LowpassGate & | gate () |
| The lowpass gate that couples the tail's level to its brightness. | |
| OutputStage & | output () |
| const OutputStage & | output () const |
| OutputStage * | output_stage () noexcept override |
| The terminal saturation and degradation stage, at the generic voice boundary. | |
| int | latency_samples () const noexcept override |
| Fixed host-rate delay introduced by this voice's selected output quality. | |
| OutputOversampling | output_oversampling () const noexcept override |
| Output quality at the generic voice boundary. | |
| void | set_output_oversampling (OutputOversampling factor) override |
| void | prepare (double sample_rate) |
| Fixes the sample rate and clears all state. | |
| void | reset () |
| Silences the voice immediately, without a fade. | |
| void | note_on (float velocity) |
| Starts a hit. velocity is in [0, 1]; values outside are clamped. | |
| void | choke (float fade_ms=4.0f) |
| Fades the voice out over fade_ms and then silences it. | |
| bool | is_active () const |
| Whether the voice still has something to render. | |
| VoiceState | state () const |
| float | velocity () const |
| double | sample_rate () const |
| const OutputStage * | output_stage () const noexcept |
| void | set_velocity_response (const VelocityResponse &r) |
| const VelocityResponse & | velocity_response () const |
| void | process (float *out, int num_samples) |
| Adds num_samples of this voice's output into out. | |
| void | process_stereo (float *left, float *right, int num_samples) |
| Adds a stereo rendering to left and right. | |
Static Public Attributes | |
| static constexpr int | mode_count = 6 |
| Number of modes the body carries. | |
| static constexpr double | membrane_ratios [mode_count] |
| Frequency ratios of an ideal circular membrane's first six modes, relative to its fundamental. | |
Protected Member Functions | |
| void | on_prepare (double sample_rate) override |
| Sizes buffers and fixes coefficients. | |
| void | on_reset () override |
| Clears every piece of state the voice carries, so is_active() becomes false and the next render starts from silence. | |
| void | on_note_on (float velocity) override |
| Starts a hit at the given velocity, already clamped to [0, 1]. | |
| bool | on_is_active () const override |
| void | render_add (float *out, int num_samples) override |
| Adds the voice's output into out. Must add, never assign. | |
| virtual void | render_add_stereo (float *left, float *right, int num_samples) |
| Optional stereo realization. | |
A struck membrane, modelled as its modes rather than as an oscillator.
Everything the voice does follows from one idea: a drumhead has a set of resonances, striking it excites all of them at once, and they die away at different rates. Nothing here has a waveform. The character comes from which frequencies the modes sit at and how their decays differ, so the controls are physical (how big, how damped, where it was struck) rather than synthesis controls (which oscillator, which filter).
structure is the important one. At 0 the modes are a harmonic series, which is what a string does and what almost every synthesised drum implicitly assumes. At 1 they are the ratios of an ideal circular membrane — 1, 1.593, 2.135, 2.295, 2.653, 2.917 — which are not integer multiples of anything. That is why a real drum has no clear pitch while a harmonic bank plays a note, and moving between them is the difference between a tom and a tuned percussion instrument. The membrane ratios are published physics (they are the zeros of a Bessel function; see Fletcher & Rossing) rather than anything fitted.
position is a comb, not a level. A mode with a node at the strike point cannot be excited by a strike there, so the position silently removes whole partials rather than changing the level: at the midpoint every even-numbered mode disappears entirely. That is why moving the strike point changes which drum it sounds like and not how loud it is.
The weighting is the one-dimensional |sin(pi * n * position)| — the same comb a struck string has. A circular head's true weighting is a Bessel function of the strike radius, which nulls a different set of modes; the sine comb is the standard approximation and is what the mode ratios above are paired with in practice.
The lowpass gate at the end is what stops the result sounding like a bank of filters: it couples level to brightness, so the tail darkens as it dies the way a physical body does.
RT contract: prepare() sizes the mode bank and allocates. Everything else, including note_on and process, allocates nothing.
|
inline |
|
inlineinherited |
Fades the voice out over fade_ms and then silences it.
Used by choke groups; a shorter fade is more abrupt but never clicks the way an instant cut does.
|
inline |
The lowpass gate that couples the tail's level to its brightness.
|
inlineinherited |
Whether the voice still has something to render.
A kit uses this to skip voices entirely rather than summing silence.
|
inlineoverridevirtualnoexcept |
Fixed host-rate delay introduced by this voice's selected output quality.
Generic registry consumers can report it without recovering the concrete voice type.
Reimplemented from pulp::signal::drum::Voice.
|
inline |
Sum of squared mode gains after the constant-energy normalization.
Exposed for renderers and matchers that need to distinguish a changed strike position from an unintended level change.
|
inlineinherited |
Starts a hit. velocity is in [0, 1]; values outside are clamped.
|
inlineoverrideprotectedvirtual |
Implements pulp::signal::drum::Voice.
|
inlineoverrideprotectedvirtual |
Starts a hit at the given velocity, already clamped to [0, 1].
Implements pulp::signal::drum::Voice.
|
inlineoverrideprotectedvirtual |
Sizes buffers and fixes coefficients.
The only hook allowed to allocate.
Implements pulp::signal::drum::Voice.
|
inlineoverrideprotectedvirtual |
Clears every piece of state the voice carries, so is_active() becomes false and the next render starts from silence.
Implements pulp::signal::drum::Voice.
|
inline |
|
inline |
|
inlineoverridevirtualnoexcept |
Output quality at the generic voice boundary.
Kits use this surface to keep every registered path on one latency contract.
Reimplemented from pulp::signal::drum::Voice.
|
inlineoverridevirtualnoexcept |
The terminal saturation and degradation stage, at the generic voice boundary.
A registry consumer reaches it without recovering the concrete voice type, so adding an engine cannot desynchronise a downcast from the engine it was selected for. A voice that ends without one returns nullptr.
Reimplemented from pulp::signal::drum::Voice.
|
inlinenoexceptinherited |
|
inlineinherited |
Fixes the sample rate and clears all state.
The only method a derived voice may allocate in.
|
inlineinherited |
Adds num_samples of this voice's output into out.
|
inlineinherited |
Adds a stereo rendering to left and right.
Voices that do not provide a stereo realization are placed in the centre with half their mono signal in each channel, so summing the two channels reproduces process() exactly. A stereo-capable voice may override render_add_stereo, but the shared choke fade remains here.
|
inlineoverrideprotectedvirtual |
Adds the voice's output into out. Must add, never assign.
Implements pulp::signal::drum::Voice.
|
inlineprotectedvirtualinherited |
Optional stereo realization.
The default is centre-panned and preserves exact mono-sum equivalence.
Reimplemented in pulp::signal::drum::ClapVoice.
|
inlineinherited |
Silences the voice immediately, without a fade.
For transport stops and buffer-size changes, where a click cannot be heard because nothing is playing either side of it.
|
inlineinherited |
|
inline |
|
inline |
|
inline |
Relative level of the upper modes in the strike.
|
inline |
|
inline |
|
inline |
How much faster the upper modes die than the fundamental.
At 0 every mode rings equally long, which sounds like a bell rather than a drum.
|
inline |
How long the fundamental rings, as a T60 in milliseconds.
|
inline |
|
inline |
|
inline |
Length of the exciter burst and the corner it is filtered at.
A short bright burst is a stick; a longer darker one is a hand.
|
inlineoverridevirtual |
Reimplemented from pulp::signal::drum::Voice.
|
inline |
Where the head is struck, 0 (edge) to 0.5 (midpoint).
At the midpoint every even-numbered mode is nulled and the fundamental is at its strongest, so the strike point selects which partials exist rather than how loud the hit is.
|
inline |
Random detuning of the modes, drawn once per hit.
A real head is never perfectly uniform, and a bank at exact ratios sounds synthetic.
|
inline |
Extra progressive sharpening of the upper modes, as a stiff plate has.
|
inline |
Blend from a harmonic series (0) to ideal-membrane ratios (1).
|
inline |
Optional layers around the modal body.
The sub follows the body's decay at half the fundamental; air is a short high-passed noise wash; click is a still-shorter filtered-noise strike.
|
inline |
Fundamental of the body, in Hz.
|
inlineinherited |
|
inlineinherited |
|
inlineinherited |
|
inlineinherited |
|
staticconstexpr |
Frequency ratios of an ideal circular membrane's first six modes, relative to its fundamental.
Published physics, not fitted values.
|
staticconstexpr |
Number of modes the body carries.