Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
voice.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <cstddef>
8
9namespace pulp::signal::drum {
10
21
52class Voice {
53public:
54 virtual ~Voice() = default;
55
56 Voice() = default;
57 Voice(const Voice&) = delete;
58 Voice& operator=(const Voice&) = delete;
59
62 void prepare(double sample_rate) {
63 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
64 on_prepare(sample_rate_);
65 reset();
66 }
67
71 void reset() {
72 choking_ = false;
73 choke_gain_ = 1.0f;
74 on_reset();
75 }
76
78 void note_on(float velocity) {
79 choking_ = false;
80 choke_gain_ = 1.0f;
81 velocity_ = std::clamp(velocity, 0.0f, 1.0f);
82 on_note_on(velocity_);
83 }
84
88 void choke(float fade_ms = 4.0f) {
89 if (!is_active()) return;
90 const double samples = std::max(0.001 * static_cast<double>(fade_ms) * sample_rate_, 1.0);
91 choke_step_ = static_cast<float>(1.0 / samples);
92 choking_ = true;
93 }
94
97 bool is_active() const { return on_is_active(); }
98
99 VoiceState state() const {
100 if (!on_is_active()) return VoiceState::idle;
101 return choking_ ? VoiceState::choking : VoiceState::ringing;
102 }
103
104 float velocity() const { return velocity_; }
105 double sample_rate() const { return sample_rate_; }
106
110 virtual int latency_samples() const noexcept { return 0; }
111
114 virtual OutputOversampling output_oversampling() const noexcept {
116 }
118
124 virtual OutputStage* output_stage() noexcept { return nullptr; }
125 const OutputStage* output_stage() const noexcept {
126 return const_cast<Voice*>(this)->output_stage();
127 }
128
129 void set_velocity_response(const VelocityResponse& r) { velocity_response_ = r; }
130 const VelocityResponse& velocity_response() const { return velocity_response_; }
131
133 void process(float* out, int num_samples) {
134 if (out == nullptr || num_samples <= 0 || !is_active()) return;
135
136 if (!choking_) {
137 render_add(out, num_samples);
138 return;
139 }
140
141 // A fade cannot be applied to an additive render in place, because the
142 // buffer may already hold another voice. Render into a fixed-size
143 // scratch in chunks, scale, and accumulate -- which keeps the fade
144 // exact without allocating.
145 int done = 0;
146 while (done < num_samples) {
147 const int n = std::min(kScratchSamples, num_samples - done);
148 std::fill(scratch_, scratch_ + n, 0.0f);
149 render_add(scratch_, n);
150 for (int i = 0; i < n; ++i) {
151 out[done + i] += scratch_[i] * choke_gain_;
152 choke_gain_ = std::max(choke_gain_ - choke_step_, 0.0f);
153 }
154 done += n;
155 if (choke_gain_ <= 0.0f) break;
156 }
157
158 if (choke_gain_ <= 0.0f) reset();
159 }
160
167 void process_stereo(float* left, float* right, int num_samples) {
168 if (left == nullptr || right == nullptr || num_samples <= 0 ||
169 !is_active())
170 return;
171
172 if (!choking_) {
173 render_add_stereo(left, right, num_samples);
174 return;
175 }
176
177 int done = 0;
178 while (done < num_samples) {
179 const int n = std::min(kScratchSamples, num_samples - done);
180 std::fill(stereo_left_scratch_, stereo_left_scratch_ + n, 0.0f);
181 std::fill(stereo_right_scratch_, stereo_right_scratch_ + n, 0.0f);
182 render_add_stereo(stereo_left_scratch_, stereo_right_scratch_, n);
183 for (int i = 0; i < n; ++i) {
184 left[done + i] += stereo_left_scratch_[i] * choke_gain_;
185 right[done + i] += stereo_right_scratch_[i] * choke_gain_;
186 choke_gain_ = std::max(choke_gain_ - choke_step_, 0.0f);
187 }
188 done += n;
189 if (choke_gain_ <= 0.0f) break;
190 }
191
192 if (choke_gain_ <= 0.0f) reset();
193 }
194
195protected:
198 virtual void on_prepare(double sample_rate) = 0;
199
202 virtual void on_reset() = 0;
203
205 virtual void on_note_on(float velocity) = 0;
206
207 virtual bool on_is_active() const = 0;
208
210 virtual void render_add(float* out, int num_samples) = 0;
211
214 virtual void render_add_stereo(float* left, float* right,
215 int num_samples) {
216 int done = 0;
217 while (done < num_samples) {
218 const int n = std::min(kScratchSamples, num_samples - done);
219 std::fill(mono_stereo_scratch_, mono_stereo_scratch_ + n, 0.0f);
220 render_add(mono_stereo_scratch_, n);
221 for (int i = 0; i < n; ++i) {
222 const float centred = 0.5f * mono_stereo_scratch_[i];
223 left[done + i] += centred;
224 right[done + i] += centred;
225 }
226 done += n;
227 }
228 }
229
230private:
231 // 256 samples is longer than any block a plugin host is likely to ask for
232 // in the low-latency configurations where choking matters, and small
233 // enough that a whole kit's scratch stays inside a cache level.
234 static constexpr int kScratchSamples = 256;
235
236 double sample_rate_ = 44100.0;
237 float velocity_ = 1.0f;
238 VelocityResponse velocity_response_{};
239
240 bool choking_ = false;
241 float choke_gain_ = 1.0f;
242 float choke_step_ = 1.0f;
243 float scratch_[kScratchSamples]{};
244 float mono_stereo_scratch_[kScratchSamples]{};
245 float stereo_left_scratch_[kScratchSamples]{};
246 float stereo_right_scratch_[kScratchSamples]{};
247};
248
249} // namespace pulp::signal::drum
bool is_active() const
Whether the voice still has something to render.
Definition voice.hpp:97
virtual OutputStage * output_stage() noexcept
The terminal saturation and degradation stage, at the generic voice boundary.
Definition voice.hpp:124
double sample_rate() const
Definition voice.hpp:105
void reset()
Silences the voice immediately, without a fade.
Definition voice.hpp:71
const OutputStage * output_stage() const noexcept
Definition voice.hpp:125
void set_velocity_response(const VelocityResponse &r)
Definition voice.hpp:129
Voice(const Voice &)=delete
void choke(float fade_ms=4.0f)
Fades the voice out over fade_ms and then silences it.
Definition voice.hpp:88
VoiceState state() const
Definition voice.hpp:99
virtual void on_reset()=0
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Voice & operator=(const Voice &)=delete
void process(float *out, int num_samples)
Adds num_samples of this voice's output into out.
Definition voice.hpp:133
virtual int latency_samples() const noexcept
Fixed host-rate delay introduced by this voice's selected output quality.
Definition voice.hpp:110
void prepare(double sample_rate)
Fixes the sample rate and clears all state.
Definition voice.hpp:62
virtual void set_output_oversampling(OutputOversampling)
Definition voice.hpp:117
virtual void on_note_on(float velocity)=0
Starts a hit at the given velocity, already clamped to [0, 1].
virtual void render_add(float *out, int num_samples)=0
Adds the voice's output into out. Must add, never assign.
virtual OutputOversampling output_oversampling() const noexcept
Output quality at the generic voice boundary.
Definition voice.hpp:114
virtual bool on_is_active() const =0
void process_stereo(float *left, float *right, int num_samples)
Adds a stereo rendering to left and right.
Definition voice.hpp:167
virtual ~Voice()=default
virtual void render_add_stereo(float *left, float *right, int num_samples)
Optional stereo realization.
Definition voice.hpp:214
virtual void on_prepare(double sample_rate)=0
Sizes buffers and fixes coefficients.
float velocity() const
Definition voice.hpp:104
void note_on(float velocity)
Starts a hit. velocity is in [0, 1]; values outside are clamped.
Definition voice.hpp:78
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
@ bypass
Definition output_stage.hpp:21
VoiceState
Public lifecycle inspection for a drum voice.
Definition voice.hpp:16
@ ringing
Definition voice.hpp:18
@ choking
Definition voice.hpp:19
@ idle
Definition voice.hpp:17
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ i
Definition formant_filter_bank.hpp:20
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33