Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
string.hpp
Go to the documentation of this file.
1#pragma once
2
9
10#include <algorithm>
11#include <cmath>
12
13namespace pulp::signal::drum {
14
21
45class StringVoice : public Voice {
46public:
49 r.level_db = 16.0f;
50 // A struck string tightens very little in pitch, so the bend stays
51 // small; the brightness term is where its velocity lives.
52 r.bend_octaves = 0.05f;
53 r.brightness_octaves = 2.0f;
55 }
56
58 void set_tune_hz(double hz) { tune_hz_ = std::clamp(hz, 30.0, 4000.0); }
59
62 void set_decay_seconds(double seconds) { decay_s_ = std::clamp(seconds, 0.05, 20.0); }
63
65 void set_damping(double amount) { damping_ = std::clamp(amount, 0.0, 1.0); }
66
69 void set_stiffness(double amount) { stiffness_ = std::clamp(amount, 0.0, 1.0); }
70
73 void set_pluck_position(double position) {
74 pluck_position_ = std::clamp(position, 0.0, 0.5);
75 }
76
79 void set_exciter_ms(double ms) { exciter_ms_ = std::clamp(ms, 0.1, 50.0); }
80
89 void set_brightness_hz(double hz) { brightness_hz_ = std::clamp(hz, 200.0, 18000.0); }
90
92 void set_pick_direction(double amount) { pick_direction_ = std::clamp(amount, 0.0, 0.99); }
93
97 void set_restart_on_hit(bool restart) { restart_on_hit_ = restart; }
98
105 if (modulation_ == StringModulation::fm &&
106 mode != StringModulation::fm) {
107 fm_string_.reset();
108 fm_path_active_ = false;
109 }
110 modulation_ = mode;
111 }
112 void set_modulation_mix(double mix) {
113 modulation_mix_ = std::clamp(mix, 0.0, 1.0);
114 }
116 modulation_ratio_ = std::clamp(ratio, 0.125, 16.0);
117 }
118 void set_fm_depth_octaves(double octaves) {
119 fm_depth_octaves_ = std::clamp(octaves, 0.0, 2.0);
120 if (fm_depth_octaves_ == 0.0)
121 fm_string_.set_frequency(base_frequency_);
122 }
123
126 void set_lpg_amount(double amount) { lpg_amount_ = std::clamp(amount, 0.0, 1.0); }
127 LowpassGate& gate() { return gate_; }
128
129 void set_noise_color(NoiseColor color) { noise_.set_color(color); }
130
131 OutputStage& output() { return output_; }
132 const OutputStage& output() const { return output_; }
133 OutputStage* output_stage() noexcept override { return &output_; }
134 int latency_samples() const noexcept override {
135 return output_.latency_samples();
136 }
137 OutputOversampling output_oversampling() const noexcept override {
138 return output_.oversampling();
139 }
141 output_.set_oversampling(factor);
142 }
143
144protected:
145 void on_prepare(double sample_rate) override {
146 noise_.prepare(sample_rate);
147 // FM can bend the 30 Hz public tuning floor down to the Karplus
148 // primitive's 20 Hz limit, so both paths need storage for that delay.
149 string_.prepare(sample_rate, 20.0);
150 fm_string_.prepare(sample_rate, 20.0);
151 exciter_env_.set_sample_rate(sample_rate);
152 gate_env_.set_sample_rate(sample_rate);
153 sync_env_.set_sample_rate(sample_rate);
154 gate_.set_sample_rate(sample_rate);
155 output_.prepare(sample_rate);
156 }
157
158 void on_reset() override {
159 string_.reset();
160 fm_string_.reset();
161 fm_path_active_ = false;
162 exciter_env_.reset();
163 gate_env_.reset();
164 sync_env_.reset();
165 gate_.reset();
166 output_.reset();
167 noise_.reset();
168 modulation_phase_ = 0.0;
169 master_phase_ = 0.0;
170 sync_phase_ = 0.0;
171 }
172
173 void on_note_on(float velocity) override {
174 if (restart_on_hit_) output_.reset_nonlinear_state();
175 output_.trigger();
176 const auto& response = velocity_response();
177 velocity_gain_ = response.gain(velocity);
178
179 noise_.reset();
180 base_frequency_ = tune_hz_ * std::exp2(response.bend(velocity));
181 string_.set_frequency(base_frequency_);
182 string_.set_decay_seconds(decay_s_);
183 string_.set_damping(damping_);
184 string_.set_stiffness(stiffness_);
185 string_.set_pluck_position(pluck_position_);
186 string_.set_pick_direction(pick_direction_);
187 // Velocity opens the excitation's bandwidth: harder is brighter.
188 string_.set_dynamic_bandwidth_hz(
189 brightness_hz_ * static_cast<double>(response.brightness_scale(velocity)));
190 string_.pluck(restart_on_hit_);
191 const bool was_fm_path_active = fm_path_active_;
192 fm_path_active_ = modulation_ == StringModulation::fm;
193 if (fm_path_active_) {
194 if (!was_fm_path_active) fm_string_.reset();
195 fm_string_.set_frequency(base_frequency_);
196 fm_string_.set_decay_seconds(decay_s_);
197 fm_string_.set_damping(damping_);
198 fm_string_.set_stiffness(stiffness_);
199 fm_string_.set_pluck_position(pluck_position_);
200 fm_string_.set_pick_direction(pick_direction_);
201 fm_string_.set_dynamic_bandwidth_hz(
202 brightness_hz_ *
203 static_cast<double>(response.brightness_scale(velocity)));
204 fm_string_.pluck(restart_on_hit_);
205 } else if (was_fm_path_active) {
206 fm_string_.reset();
207 }
208
209 exciter_env_.set_attack_ms(0.0);
210 exciter_env_.set_decay_time_constant_ms(exciter_ms_);
211 exciter_env_.trigger();
212
213 gate_env_.reset();
214 gate_env_.set_attack_ms(0.5);
215 gate_env_.set_decay_t60_ms(decay_s_ * 1000.0);
216 gate_env_.trigger();
217 sync_env_.reset();
218 sync_env_.set_attack_ms(0.0);
219 sync_env_.set_decay_t60_ms(decay_s_ * 1000.0);
220 sync_env_.trigger();
221 modulation_phase_ = 0.0;
222 master_phase_ = 0.0;
223 sync_phase_ = 0.0;
224 }
225
226 bool on_is_active() const override {
227 return string_.is_active() ||
228 (fm_path_active_ && fm_string_.is_active()) ||
229 output_.has_tail();
230 }
231
232 void render_add(float* out, int num_samples) override {
233 for (int i = 0; i < num_samples; ++i) {
234 modulation_phase_ +=
235 base_frequency_ * modulation_ratio_ / sample_rate();
236 if (modulation_phase_ >= 1.0) {
237 modulation_phase_ -= std::floor(modulation_phase_);
238 }
239 const double modulator =
240 std::sin(2.0 * 3.14159265358979323846 * modulation_phase_);
241 if (fm_path_active_ && modulation_ == StringModulation::fm &&
242 fm_depth_octaves_ > 0.0) {
243 fm_string_.set_modulated_frequency(
244 base_frequency_ *
245 std::exp2(fm_depth_octaves_ * modulator));
246 }
247
248 // process() rather than white(): the pluck is this voice's only
249 // noise consumer, so routing it through the colour filter is what
250 // makes set_noise_color() audible at all. A darker excitation
251 // reaches the string with less energy above its own brightness
252 // corner, which is the difference between a pick and a thumb.
253 const double burst = exciter_env_.is_active()
254 ? static_cast<double>(noise_.process()) *
255 exciter_env_.process()
256 : 0.0;
257 const double dry = static_cast<double>(
258 string_.process(static_cast<float>(burst)));
259 const double fm =
260 fm_path_active_
261 ? static_cast<double>(
262 fm_string_.process(static_cast<float>(burst)))
263 : dry;
264 const double sync_level =
265 sync_env_.is_active() ? sync_env_.process() : 0.0;
266
267 double effected = dry;
268 if (modulation_ == StringModulation::fm) {
269 effected = fm;
270 } else if (modulation_ == StringModulation::ring) {
271 effected = dry * modulator;
272 } else if (modulation_ == StringModulation::sync) {
273 master_phase_ += base_frequency_ / sample_rate();
274 const bool wrapped = master_phase_ >= 1.0;
275 if (wrapped) {
276 master_phase_ -= std::floor(master_phase_);
277 sync_phase_ = 0.0;
278 } else {
279 sync_phase_ +=
280 base_frequency_ * modulation_ratio_ / sample_rate();
281 if (sync_phase_ >= 1.0) {
282 sync_phase_ -= std::floor(sync_phase_);
283 }
284 }
285 effected =
286 (2.0 * sync_phase_ - 1.0) * sync_level;
287 }
288 const double modulated =
289 dry + modulation_mix_ * (effected - dry);
290 const double gate_target =
291 gate_env_.is_active() ? gate_env_.process() : 0.0;
292 const double gated =
293 static_cast<double>(gate_.process(static_cast<float>(modulated),
294 gate_target));
295 const double lpg =
296 modulated + lpg_amount_ * (gated - modulated);
297 out[i] += static_cast<float>(
298 output_.process(static_cast<float>(lpg)) * velocity_gain_);
299 }
300 }
301
302private:
303 double tune_hz_ = 220.0;
304 double decay_s_ = 2.0;
305 double damping_ = 0.3;
306 double stiffness_ = 0.0;
307 double pluck_position_ = 0.25;
308 double exciter_ms_ = 1.0;
309 double brightness_hz_ = 1800.0;
310 double pick_direction_ = 0.0;
311 bool restart_on_hit_ = false;
312 bool fm_path_active_ = false;
314 double modulation_mix_ = 0.0;
315 double modulation_ratio_ = 2.0;
316 double fm_depth_octaves_ = 0.25;
317 double lpg_amount_ = 0.0;
318
319 NoiseSource noise_;
320 KarplusStrong string_;
321 KarplusStrong fm_string_;
322 DecayEnvelope64 exciter_env_;
323 DecayEnvelope64 gate_env_;
324 DecayEnvelope64 sync_env_;
325 LowpassGate gate_;
326 OutputStage output_;
327
328 double velocity_gain_ = 1.0;
329 double base_frequency_ = 220.0;
330 double modulation_phase_ = 0.0;
331 double master_phase_ = 0.0;
332 double sync_phase_ = 0.0;
333};
334
335} // namespace pulp::signal::drum
bool on_is_active() const override
Definition string.hpp:226
void set_tune_hz(double hz)
Pitch of the string, in Hz.
Definition string.hpp:58
LowpassGate & gate()
Definition string.hpp:127
const OutputStage & output() const
Definition string.hpp:132
void set_modulation(StringModulation mode)
Optional post-body modulation.
Definition string.hpp:104
void set_decay_seconds(double seconds)
Time for the note to fall by 60 dB, in seconds.
Definition string.hpp:62
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition string.hpp:133
void set_output_oversampling(OutputOversampling factor) override
Definition string.hpp:140
void set_pick_direction(double amount)
Darkens the attack without retuning the string.
Definition string.hpp:92
void set_exciter_ms(double ms)
Length of the excitation burst in milliseconds.
Definition string.hpp:79
void set_pluck_position(double position)
Where along the string it is excited, 0 (at the end) to 0.5 (the middle).
Definition string.hpp:73
void set_modulation_mix(double mix)
Definition string.hpp:112
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition string.hpp:173
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition string.hpp:134
void set_stiffness(double amount)
Inharmonicity from string stiffness, 0 (a true harmonic series) to 1.
Definition string.hpp:69
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition string.hpp:232
void set_restart_on_hit(bool restart)
Whether a new hit restarts the string or adds to what is still ringing.
Definition string.hpp:97
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition string.hpp:137
void set_lpg_amount(double amount)
Amount of the lowpass-gate stage after modulation.
Definition string.hpp:126
void set_fm_depth_octaves(double octaves)
Definition string.hpp:118
OutputStage & output()
Definition string.hpp:131
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition string.hpp:145
StringVoice()
Definition string.hpp:47
void set_modulation_ratio(double ratio)
Definition string.hpp:115
void set_brightness_hz(double hz)
Bandwidth of the excitation in Hz at zero velocity, before the velocity response opens it.
Definition string.hpp:89
void set_damping(double amount)
How much faster the upper partials die than the fundamental.
Definition string.hpp:65
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition string.hpp:158
void set_noise_color(NoiseColor color)
Definition string.hpp:129
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
StringModulation
Definition string.hpp:15
@ fm
Definition string.hpp:17
@ ring
Definition string.hpp:18
@ none
Definition string.hpp:16
@ sync
Definition string.hpp:19
OutputOversampling
Realtime quality choices for the shared drum output nonlinearity.
Definition output_stage.hpp:20
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
KarplusStrongT< float > KarplusStrong
Definition karplus_strong.hpp:314
@ i
Definition formant_filter_bank.hpp:20
@ gated
Flat body, hard cut.
Definition nonlin_ambience_design.hpp:245
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
LowpassGateT< float > LowpassGate
Definition lowpass_gate.hpp:138
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33