Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
modal_bank.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <cmath>
8#include <span>
9
10namespace pulp::signal {
11
16struct ModalMode {
17 float freq_hz = 440.0f;
18 float t60_s = 1.0f;
19 float gain = 1.0f;
20};
21
27template <typename SampleType = float>
28inline void fill_strike_pulse(std::span<SampleType> dst, SampleType velocity) {
29 const auto n = dst.size();
30 if (n == 0) return;
31 if (n == 1) { dst[0] = velocity; return; }
32 constexpr SampleType two_pi = SampleType(6.28318530717958647692);
33 const SampleType phase_step = two_pi / static_cast<SampleType>(n);
34 for (std::size_t i = 0; i < n; ++i) {
35 dst[i] = velocity * SampleType(0.5)
36 * (SampleType(1) - std::cos(phase_step * static_cast<SampleType>(i)));
37 }
38}
39
56struct ModalShape {
57 float half_waves = 1.0f;
58 float end_phase = 0.0f;
59};
60
67inline float mode_shape_at(const ModalShape& shape, float position) {
68 constexpr double pi = 3.14159265358979323846;
69 return static_cast<float>(std::sin(pi * static_cast<double>(shape.half_waves)
70 * static_cast<double>(position)
71 + static_cast<double>(shape.end_phase)));
72}
73
78inline void fill_ideal_string_shapes(std::span<ModalShape> dst) {
79 for (std::size_t i = 0; i < dst.size(); ++i)
80 dst[i] = ModalShape{static_cast<float>(i + 1), 0.0f};
81}
82
88template <typename SampleType = float>
89inline void fill_mode_weights(std::span<SampleType> dst,
90 std::span<const ModalShape> shapes, float position) {
91 const std::size_t n = std::min(dst.size(), shapes.size());
92 for (std::size_t i = 0; i < n; ++i)
93 dst[i] = static_cast<SampleType>(mode_shape_at(shapes[i], position));
94 for (std::size_t i = n; i < dst.size(); ++i) dst[i] = SampleType{0};
95}
96
153template <typename SampleType = float>
155public:
158 static constexpr int kLanes = 16;
159
164 static constexpr int kMaxPickups = 8;
165
170 void prepare(double sample_rate, int max_modes, int max_pickups = 1) {
171 sample_rate_ = sample_rate > 0.0 ? sample_rate : 48000.0;
172 max_modes_ = max_modes > 0 ? max_modes : 0;
173 max_pickups_ = std::clamp(max_pickups, 1, kMaxPickups);
174 padded_ = static_cast<std::size_t>((max_modes_ + kLanes - 1) / kLanes) * kLanes;
175 cu_.resize(padded_);
176 cv_.resize(padded_);
177 rc_.resize(padded_);
178 rs_.resize(padded_);
179 u_.resize(padded_);
180 v_.resize(padded_);
181 pickup_.resize(padded_ * static_cast<std::size_t>(max_pickups_));
182 std::fill(pickup_.begin(), pickup_.end(), SampleType{1});
183 mode_count_ = 0;
184 }
185
193 void set_modes(std::span<const ModalMode> modes) {
194 const int count = static_cast<int>(
195 std::min<std::size_t>(modes.size(), static_cast<std::size_t>(max_modes_)));
196 const double fs = sample_rate_;
197 for (int m = 0; m < count; ++m) {
198 const auto i = static_cast<std::size_t>(m);
199 const double freq = std::clamp(static_cast<double>(modes[i].freq_hz),
200 1.0, 0.49 * fs);
201 const double t60 = std::max(static_cast<double>(modes[i].t60_s), 1.0e-3);
202 const double r = std::pow(10.0, -3.0 / (t60 * fs));
203 const double w = 2.0 * 3.14159265358979323846 * freq / fs;
204 const double cos_w = std::cos(w);
205 const double sin_w = std::sin(w);
206 const double gain = static_cast<double>(modes[i].gain);
207 rc_[i] = static_cast<SampleType>(r * cos_w);
208 rs_[i] = static_cast<SampleType>(r * sin_w);
209 cu_[i] = static_cast<SampleType>(gain * cos_w);
210 cv_[i] = static_cast<SampleType>(gain * sin_w);
211 }
212 // Zero the coefficients of unused padding lanes so they contribute
213 // exact silence regardless of stale state.
214 for (std::size_t m = static_cast<std::size_t>(count); m < padded_; ++m) {
215 cu_[m] = SampleType{0};
216 cv_[m] = SampleType{0};
217 rc_[m] = SampleType{0};
218 rs_[m] = SampleType{0};
219 }
220 mode_count_ = count;
221 }
222
226 void set_pickup_gains(int channel, std::span<const SampleType> gains) {
227 if (channel < 0 || channel >= max_pickups_) return;
228 const std::size_t base = static_cast<std::size_t>(channel) * padded_;
229 const std::size_t n = std::min(gains.size(), padded_);
230 for (std::size_t m = 0; m < n; ++m) pickup_[base + m] = gains[m];
231 }
232
237 void process_add(const SampleType* input, SampleType* output, int num_samples,
238 int channel = 0) {
239 SampleType* outs[1] = {output};
240 const int c = std::clamp(channel, 0, max_pickups_ - 1);
241 run(input, outs, &c, 1, num_samples);
242 }
243
249 void process_add(const SampleType* input, SampleType* const* outputs,
250 int num_channels, int num_samples) {
251 int channels[kMaxPickups];
252 const int n = std::clamp(num_channels, 0, max_pickups_);
253 for (int c = 0; c < n; ++c) channels[c] = c;
254 run(input, outputs, channels, n, num_samples);
255 }
256
259 void reset() {
260 u_.clear();
261 v_.clear();
262 }
263
264 int mode_count() const { return mode_count_; }
265 int max_modes() const { return max_modes_; }
266 int max_pickups() const { return max_pickups_; }
267 double sample_rate() const { return sample_rate_; }
268
269private:
270 void run(const SampleType* input, SampleType* const* outputs,
271 const int* channels, int num_channels, int num_samples) {
272 if (num_channels <= 0) return;
273 const int groups = (mode_count_ + kLanes - 1) / kLanes;
274 for (int g = 0; g < groups; ++g) {
275 const std::size_t base = static_cast<std::size_t>(g) * kLanes;
276 SampleType cu[kLanes], cv[kLanes], rc[kLanes], rs[kLanes];
277 SampleType u[kLanes], v[kLanes];
278 SampleType pick[kMaxPickups][kLanes];
279 for (int l = 0; l < kLanes; ++l) {
280 const std::size_t i = base + static_cast<std::size_t>(l);
281 cu[l] = cu_[i];
282 cv[l] = cv_[i];
283 rc[l] = rc_[i];
284 rs[l] = rs_[i];
285 u[l] = u_[i];
286 v[l] = v_[i];
287 }
288 for (int c = 0; c < num_channels; ++c) {
289 const std::size_t pbase =
290 static_cast<std::size_t>(channels[c]) * padded_ + base;
291 for (int l = 0; l < kLanes; ++l) pick[c][l] = pickup_[pbase + l];
292 }
293 for (int i = 0; i < num_samples; ++i) {
294 const SampleType x = input[i];
295 SampleType y[kLanes];
296 for (int l = 0; l < kLanes; ++l) {
297 const SampleType nu = rc[l] * u[l] - rs[l] * v[l] + cu[l] * x;
298 const SampleType nv = rs[l] * u[l] + rc[l] * v[l] + cv[l] * x;
299 u[l] = nu;
300 v[l] = nv;
301 y[l] = nv;
302 }
303 for (int c = 0; c < num_channels; ++c) {
304 SampleType t[kLanes];
305 for (int l = 0; l < kLanes; ++l) t[l] = pick[c][l] * y[l];
306 // Fixed pairwise reduction order: deterministic output and
307 // vector-width friendly (elementwise adds, then one scalar).
308 for (int step = kLanes / 2; step > 0; step /= 2)
309 for (int l = 0; l < step; ++l) t[l] += t[l + step];
310 outputs[c][i] += t[0];
311 }
312 }
313 for (int l = 0; l < kLanes; ++l) {
314 const std::size_t i = base + static_cast<std::size_t>(l);
315 u_[i] = snap_to_zero(u[l]);
316 v_[i] = snap_to_zero(v[l]);
317 }
318 }
319 }
320
321 AlignedBufferT<SampleType> cu_, cv_, rc_, rs_, u_, v_, pickup_;
322 double sample_rate_ = 48000.0;
323 std::size_t padded_ = 0;
324 int max_modes_ = 0;
325 int max_pickups_ = 1;
326 int mode_count_ = 0;
327};
328
331
332} // namespace pulp::signal
Bank of two-pole modal resonators, stored structure-of-arrays in lane groups so the per-sample state ...
Definition modal_bank.hpp:154
void set_modes(std::span< const ModalMode > modes)
Load a mode set (up to the prepared max; excess modes are ignored).
Definition modal_bank.hpp:193
void set_pickup_gains(int channel, std::span< const SampleType > gains)
Set how loudly pickup channel hears each mode.
Definition modal_bank.hpp:226
void process_add(const SampleType *input, SampleType *const *outputs, int num_channels, int num_samples)
Excite and run the bank into num_channels outputs, one per pickup: pickup c accumulates into outputs[...
Definition modal_bank.hpp:249
void reset()
Zero all resonator state (call on discontinuities / re-strike from silence).
Definition modal_bank.hpp:259
static constexpr int kMaxPickups
Definition modal_bank.hpp:164
int max_modes() const
Definition modal_bank.hpp:265
static constexpr int kLanes
Definition modal_bank.hpp:158
double sample_rate() const
Definition modal_bank.hpp:267
void process_add(const SampleType *input, SampleType *output, int num_samples, int channel=0)
Excite and run the bank into one output: adds pickup channel's view of the summed mode outputs for nu...
Definition modal_bank.hpp:237
int max_pickups() const
Definition modal_bank.hpp:266
int mode_count() const
Definition modal_bank.hpp:264
void prepare(double sample_rate, int max_modes, int max_pickups=1)
Allocate storage for up to max_modes modes and max_pickups listening positions at sample_rate.
Definition modal_bank.hpp:170
Definition additive_bank.hpp:218
void fill_ideal_string_shapes(std::span< ModalShape > dst)
Fill dst with the mode shapes of an ideal string fixed at both ends: dst[i] describes the (i+1)-th tr...
Definition modal_bank.hpp:78
@ c
Definition spectrum_trace.hpp:35
void fill_mode_weights(std::span< SampleType > dst, std::span< const ModalShape > shapes, float position)
Sample every shape in shapes at position, writing phi_m(position) to dst.
Definition modal_bank.hpp:89
float mode_shape_at(const ModalShape &shape, float position)
Evaluate a mode's shape at a normalized position along the body.
Definition modal_bank.hpp:67
ModalBankT< double > ModalBank64
Definition modal_bank.hpp:330
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
void fill_strike_pulse(std::span< SampleType > dst, SampleType velocity)
Fill dst with a raised-cosine contact pulse, peak-normalized to velocity.
Definition modal_bank.hpp:28
ModalBankT< float > ModalBank
Definition modal_bank.hpp:329
@ l
Definition input_events.hpp:40
@ t
Definition input_events.hpp:41
One resonant mode of a vibrating object: frequency, decay time, and impulse-response amplitude.
Definition modal_bank.hpp:16
float t60_s
time for this mode to decay by 60 dB
Definition modal_bank.hpp:18
float gain
amplitude of this mode in the impulse response
Definition modal_bank.hpp:19
float freq_hz
Definition modal_bank.hpp:17
The spatial eigenfunction of one mode along a body's normalized length coordinate x in [0,...
Definition modal_bank.hpp:56
float half_waves
number of half-wavelengths across the body
Definition modal_bank.hpp:57
float end_phase
0 puts a node at x = 0; pi/2 an antinode
Definition modal_bank.hpp:58