Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
formant_filter_bank.hpp
Go to the documentation of this file.
1#pragma once
2
5
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <complex>
13#include <cstddef>
14#include <limits>
15#include <span>
16#include <type_traits>
17
18namespace pulp::signal {
19
20enum class FormantVowel { a, e, i, o, u };
21
31
50template <typename SampleType = float, std::size_t MaxFormants = 5> class FormantFilterBankT {
51 static_assert(std::is_floating_point_v<SampleType>);
52 static_assert(MaxFormants >= 1 && MaxFormants <= 8);
53
54 public:
55 static constexpr std::size_t storage_capacity = MaxFormants;
56 static constexpr double min_frequency_hz = 40.0;
57 static constexpr double min_bandwidth_hz = 20.0;
58 static constexpr double min_q = 0.5;
59 static constexpr double max_q = 50.0;
60 static constexpr double min_gain_db = -60.0;
61 static constexpr double max_gain_db = 24.0;
62 static constexpr double min_headroom_db = 0.0;
63 static constexpr double max_headroom_db = 24.0;
64 static constexpr std::size_t default_transition_samples = 64;
65
66 struct FormantSpec {
67 double frequency_hz = 1000.0;
68 double bandwidth_hz = 100.0;
69 double gain_db = 0.0;
70 };
71
72 struct Recipe {
73 std::array<FormantSpec, MaxFormants> formants{};
74 std::size_t count = 0;
75 double headroom_db = 3.0;
76 };
77
78 bool prepare(double sample_rate, std::size_t capacity = MaxFormants) noexcept {
79 const SampleType processing_rate = static_cast<SampleType>(sample_rate);
80 if (!std::isfinite(sample_rate) || !std::isfinite(processing_rate) ||
81 processing_rate < static_cast<SampleType>(min_frequency_hz / 0.45) || capacity == 0 ||
82 capacity > MaxFormants)
83 return false;
84 sample_rate_ = static_cast<double>(processing_rate);
85 capacity_ = capacity;
86 active_ = {};
87 transition_ = {};
88 requested_ = {};
89 requested_coefficients_ = {};
90 requested_weights_ = {};
91 requested_tail_seconds_ = 0.0;
92 queued_ = {};
93 transition_total_ = 0;
94 transition_remaining_ = 0;
95 queued_transition_samples_ = 0;
96 queued_transition_ = false;
97 prepared_ = true;
98 return true;
99 }
100
101 bool prepared() const noexcept {
102 return prepared_;
103 }
104 std::size_t capacity() const noexcept {
105 return capacity_;
106 }
107 double sample_rate() const noexcept {
108 return sample_rate_;
109 }
110
111 void set_transition_samples(std::size_t samples) noexcept {
112 transition_samples_ = samples;
113 }
114 std::size_t transition_samples() const noexcept {
115 return transition_samples_;
116 }
117 bool transition_active() const noexcept {
118 return transition_remaining_ != 0;
119 }
120
121 FormantConfigureStatus configure(std::span<const FormantSpec> formants,
122 double headroom_db = 3.0) noexcept {
123 if (!prepared_)
125 const auto status = validate(formants, headroom_db);
127 return status;
128
129 Recipe candidate;
130 candidate.count = formants.size();
131 candidate.headroom_db = headroom_db;
132 std::copy(formants.begin(), formants.end(), candidate.formants.begin());
133 if (recipes_equal(candidate, requested_))
135 if (transition_active() && recipes_equal(candidate, transition_.recipe)) {
136 requested_ = candidate;
137 capture_requested(transition_);
138 queued_ = {};
139 queued_transition_samples_ = 0;
140 queued_transition_ = false;
142 }
143 Bank designed;
144 if (!design(candidate, designed))
146
147 requested_ = candidate;
148 capture_requested(designed);
149 if (!transition_active()) {
150 if (transition_samples_ == 0) {
151 load_coefficients(designed, active_, true);
152 } else {
153 transition_ = active_;
154 load_coefficients(designed, transition_, true);
155 transition_total_ = transition_samples_;
156 transition_remaining_ = transition_samples_;
157 }
158 } else if (transition_remaining_ == transition_total_) {
159 load_coefficients(designed, transition_, true);
160 } else {
161 queued_ = designed;
162 queued_transition_samples_ =
163 transition_samples_ != 0 ? transition_samples_ : transition_total_;
164 queued_transition_ = true;
165 }
167 }
168
169 FormantConfigureStatus configure(const Recipe& recipe) noexcept {
170 if (recipe.count > MaxFormants)
173 return configure(std::span<const FormantSpec>(recipe.formants.data(), recipe.count),
174 recipe.headroom_db);
175 }
176
178 double headroom_db = 3.0) noexcept {
179 if (!std::isfinite(amount))
181 if (amount < 0.0 || amount > 1.0)
183 Recipe recipe = interpolate(vowel_recipe(from), vowel_recipe(to), amount);
184 recipe.headroom_db = headroom_db;
185 return configure(recipe);
186 }
187
188 const Recipe& requested_recipe() const noexcept {
189 return requested_;
190 }
191
192 SampleType process(SampleType input) noexcept {
193 if (!prepared_)
194 return input;
195 if (!std::isfinite(static_cast<double>(input))) {
196 reset();
197 return SampleType{};
198 }
199
200 double output = 0.0;
201 if (!transition_active()) {
202 output = process_bank(active_, static_cast<double>(input));
203 } else {
204 const double from = process_bank(active_, static_cast<double>(input));
205 const double to = process_bank(transition_, static_cast<double>(input));
206 const std::size_t completed = transition_total_ - transition_remaining_ + 1;
207 const double mix =
208 static_cast<double>(completed) / static_cast<double>(transition_total_);
209 output = transition_remaining_ == 1 ? to : std::lerp(from, to, mix);
210 if (--transition_remaining_ == 0) {
211 active_ = transition_;
212 if (queued_transition_) {
213 transition_ = active_;
214 load_coefficients(queued_, transition_, true);
215 queued_transition_ = false;
216 transition_total_ = queued_transition_samples_;
217 transition_remaining_ = transition_total_;
218 queued_transition_samples_ = 0;
219 }
220 }
221 }
222 if (!std::isfinite(output)) {
223 reset();
224 return SampleType{};
225 }
226 const double limit = static_cast<double>(std::numeric_limits<SampleType>::max());
227 return static_cast<SampleType>(std::clamp(output, -limit, limit));
228 }
229
230 bool process_block(const SampleType* input, SampleType* output, std::size_t frames) noexcept {
231 if ((input == nullptr || output == nullptr) && frames != 0)
232 return false;
233 for (std::size_t i = 0; i < frames; ++i)
234 output[i] = process(input[i]);
235 return true;
236 }
237
238 bool process_block(SampleType* samples, std::size_t frames) noexcept {
239 return process_block(samples, samples, frames);
240 }
241
242 void reset() noexcept {
243 reset_bank(active_);
244 reset_bank(transition_);
245 reset_bank(queued_);
246 }
247
248 static constexpr int latency_samples() noexcept {
249 return 0;
250 }
251
252 double tail_seconds() const noexcept {
253 double result = requested_tail_seconds_;
254 if (transition_active()) {
255 result = std::max(result, tail_of(active_));
256 result = std::max(result, tail_of(transition_));
257 }
258 return result;
259 }
260
261 BiquadCoefficientsT<SampleType> coefficients(std::size_t index) const noexcept {
262 return index < requested_.count ? requested_coefficients_[index]
264 }
265
267 double magnitude(double frequency_hz) const noexcept {
268 if (!prepared_ || requested_.count == 0)
269 return 1.0;
270 if (!std::isfinite(frequency_hz) || frequency_hz < 0.0 ||
271 frequency_hz > sample_rate_ * 0.5)
272 return std::numeric_limits<double>::quiet_NaN();
273 const double omega = angular_frequency(frequency_hz, sample_rate_);
274 const std::complex<double> z1 = std::polar(1.0, -omega);
275 const std::complex<double> z2 = z1 * z1;
276 std::complex<double> sum{};
277 for (std::size_t i = 0; i < requested_.count; ++i) {
278 const auto& c = requested_coefficients_[i];
279 const std::complex<double> numerator = static_cast<double>(c.b0) +
280 static_cast<double>(c.b1) * z1 +
281 static_cast<double>(c.b2) * z2;
282 const std::complex<double> denominator =
283 1.0 + static_cast<double>(c.a1) * z1 + static_cast<double>(c.a2) * z2;
284 const double denominator_magnitude = std::abs(denominator);
285 if (!(denominator_magnitude > 0.0))
286 return 0.0;
287 sum += requested_weights_[i] * numerator / denominator;
288 }
289 return std::abs(sum);
290 }
291
292 double magnitude_db(double frequency_hz) const noexcept {
293 return static_cast<double>(magnitude_to_db(magnitude(frequency_hz)));
294 }
295
296 void response_curve_db(double min_hz, double max_hz,
297 std::span<SampleType> output) const noexcept {
298 if (output.empty())
299 return;
300 if (!prepared_) {
301 std::fill(output.begin(), output.end(), SampleType{});
302 return;
303 }
304 const double nyquist = sample_rate_ * 0.5;
305 const double lo = std::clamp(std::isfinite(min_hz) ? min_hz : min_frequency_hz,
306 min_frequency_hz, nyquist);
307 const double clamped_hi =
308 std::clamp(std::isfinite(max_hz) ? max_hz : lo, min_frequency_hz, nyquist);
309 const double hi = std::max(lo, clamped_hi);
310 for (std::size_t i = 0; i < output.size(); ++i) {
311 const double t = output.size() == 1
312 ? 0.0
313 : static_cast<double>(i) / static_cast<double>(output.size() - 1);
314 const double hz = std::exp(std::log(lo) + t * (std::log(hi) - std::log(lo)));
315 output[i] = static_cast<SampleType>(magnitude_db(hz));
316 }
317 }
318
319 static constexpr Recipe vowel_recipe(FormantVowel vowel) noexcept {
320 switch (vowel) {
321 case FormantVowel::a:
322 return make_vowel({800, 1150, 2900, 3900, 4950}, {80, 90, 120, 130, 140},
323 {0, -4, -9, -12, -15});
324 case FormantVowel::e:
325 return make_vowel({400, 1600, 2700, 3300, 4950}, {70, 100, 120, 150, 200},
326 {0, -4, -8, -12, -15});
327 case FormantVowel::i:
328 return make_vowel({350, 1700, 2700, 3700, 4950}, {60, 100, 120, 150, 200},
329 {0, -3, -9, -12, -15});
330 case FormantVowel::o:
331 return make_vowel({450, 800, 2830, 3800, 4950}, {70, 80, 100, 130, 150},
332 {0, -5, -10, -13, -16});
333 case FormantVowel::u:
334 return make_vowel({325, 700, 2530, 3500, 4950}, {50, 60, 170, 180, 200},
335 {0, -6, -12, -15, -18});
336 }
337 return {};
338 }
339
340 static Recipe interpolate(const Recipe& from, const Recipe& to, double amount) noexcept {
341 Recipe result;
342 if (!std::isfinite(amount))
343 return result;
344 amount = std::clamp(amount, 0.0, 1.0);
345 result.count = std::min({from.count, to.count, MaxFormants});
346 result.headroom_db = from.headroom_db + (to.headroom_db - from.headroom_db) * amount;
347 for (std::size_t i = 0; i < result.count; ++i) {
348 const auto& a = from.formants[i];
349 const auto& b = to.formants[i];
350 result.formants[i].frequency_hz =
351 std::exp(std::log(a.frequency_hz) +
352 (std::log(b.frequency_hz) - std::log(a.frequency_hz)) * amount);
353 result.formants[i].bandwidth_hz =
354 a.bandwidth_hz + (b.bandwidth_hz - a.bandwidth_hz) * amount;
355 result.formants[i].gain_db = a.gain_db + (b.gain_db - a.gain_db) * amount;
356 }
357 return result;
358 }
359
360 private:
361 struct Bank {
362 std::array<BiquadT<SampleType>, MaxFormants> filters{};
363 std::array<double, MaxFormants> weights{};
364 Recipe recipe{};
365 };
366
367 static constexpr Recipe make_vowel(const std::array<double, 5>& frequencies,
368 const std::array<double, 5>& bandwidths,
369 const std::array<double, 5>& gains) noexcept {
370 Recipe result;
371 result.count = std::min<std::size_t>(5, MaxFormants);
372 for (std::size_t i = 0; i < result.count; ++i)
373 result.formants[i] = {frequencies[i], bandwidths[i], gains[i]};
374 return result;
375 }
376
377 static bool recipes_equal(const Recipe& lhs, const Recipe& rhs) noexcept {
378 if (lhs.count != rhs.count || lhs.headroom_db != rhs.headroom_db)
379 return false;
380 for (std::size_t i = 0; i < lhs.count; ++i) {
381 if (lhs.formants[i].frequency_hz != rhs.formants[i].frequency_hz ||
382 lhs.formants[i].bandwidth_hz != rhs.formants[i].bandwidth_hz ||
383 lhs.formants[i].gain_db != rhs.formants[i].gain_db)
384 return false;
385 }
386 return true;
387 }
388
389 FormantConfigureStatus validate(std::span<const FormantSpec> formants,
390 double headroom_db) const noexcept {
391 if (formants.empty() || formants.size() > capacity_)
393 if (!std::isfinite(headroom_db))
395 if (headroom_db < min_headroom_db || headroom_db > max_headroom_db)
397 double previous = 0.0;
398 for (const auto& formant : formants) {
399 if (!std::isfinite(formant.frequency_hz) || !std::isfinite(formant.bandwidth_hz) ||
400 !std::isfinite(formant.gain_db))
402 const double q = formant.frequency_hz / formant.bandwidth_hz;
403 if (formant.frequency_hz < min_frequency_hz ||
404 formant.frequency_hz > 0.45 * sample_rate_ ||
405 formant.bandwidth_hz < min_bandwidth_hz || q < min_q || q > max_q ||
406 formant.gain_db < min_gain_db || formant.gain_db > max_gain_db)
408 if (formant.frequency_hz <= previous)
410 previous = formant.frequency_hz;
411 }
413 }
414
415 bool design(const Recipe& recipe, Bank& bank) noexcept {
416 bank = {};
417 bank.recipe = recipe;
418 double weight_sum = 0.0;
419 for (std::size_t i = 0; i < recipe.count; ++i)
420 weight_sum += std::pow(10.0, recipe.formants[i].gain_db / 20.0);
421 const double headroom_gain = std::pow(10.0, -recipe.headroom_db / 20.0);
422 for (std::size_t i = 0; i < recipe.count; ++i) {
423 const auto& formant = recipe.formants[i];
424 BiquadT<SampleType> designer;
425 designer.set_coefficients(
426 BiquadT<SampleType>::Type::bandpass, static_cast<SampleType>(formant.frequency_hz),
427 static_cast<SampleType>(formant.frequency_hz / formant.bandwidth_hz),
428 static_cast<SampleType>(sample_rate_));
429 const auto coefficients = designer.coefficients();
430 if (!finite(coefficients) || !biquad_is_stable(coefficients))
431 return false;
432 bank.filters[i].set_coefficients(coefficients);
433 bank.weights[i] = headroom_gain * std::pow(10.0, formant.gain_db / 20.0) / weight_sum;
434 }
435 return true;
436 }
437
438 static bool finite(const BiquadCoefficientsT<SampleType>& coefficients) noexcept {
439 return std::isfinite(coefficients.b0) && std::isfinite(coefficients.b1) &&
440 std::isfinite(coefficients.b2) && std::isfinite(coefficients.a1) &&
441 std::isfinite(coefficients.a2);
442 }
443
444 void load_coefficients(const Bank& source, Bank& destination, bool preserve_state) noexcept {
445 if (!preserve_state)
446 reset_bank(destination);
447 destination.recipe = source.recipe;
448 destination.weights = source.weights;
449 for (std::size_t i = 0; i < MaxFormants; ++i) {
450 destination.filters[i].set_coefficients(source.filters[i].coefficients());
451 if (i >= source.recipe.count)
452 destination.filters[i].reset();
453 }
454 }
455
456 void capture_requested(const Bank& source) noexcept {
457 requested_coefficients_ = {};
458 requested_weights_ = {};
459 for (std::size_t i = 0; i < source.recipe.count; ++i) {
460 const auto c = source.filters[i].coefficients();
461 requested_coefficients_[i] = c;
462 requested_weights_[i] = source.weights[i];
463 }
464 requested_tail_seconds_ = tail_of(source);
465 }
466
467 static double process_bank(Bank& bank, double input) noexcept {
468 if (bank.recipe.count == 0)
469 return input;
470 double sum = 0.0;
471 for (std::size_t i = 0; i < bank.recipe.count; ++i)
472 sum += bank.weights[i] * bank.filters[i].process(input);
473 return sum;
474 }
475
476 static void reset_bank(Bank& bank) noexcept {
477 for (auto& filter : bank.filters)
478 filter.reset();
479 }
480
481 double tail_of(const Bank& bank) const noexcept {
482 double result = 0.0;
483 for (std::size_t i = 0; i < bank.recipe.count; ++i) {
484 const auto c = bank.filters[i].coefficients();
485 const std::complex<double> discriminant = std::sqrt(
486 std::complex<double>{static_cast<double>(c.a1) * static_cast<double>(c.a1) -
487 4.0 * static_cast<double>(c.a2),
488 0.0});
489 const double radius =
490 std::max(std::abs((-static_cast<double>(c.a1) + discriminant) * 0.5),
491 std::abs((-static_cast<double>(c.a1) - discriminant) * 0.5));
492 if (radius > 0.0 && radius < 1.0)
493 result = std::max(result, -std::log(0.001) / (-std::log(radius) * sample_rate_));
494 }
495 return result;
496 }
497
498 double sample_rate_ = 0.0;
499 std::size_t capacity_ = 0;
500 std::size_t transition_samples_ = default_transition_samples;
501 std::size_t transition_total_ = 0;
502 std::size_t transition_remaining_ = 0;
503 std::size_t queued_transition_samples_ = 0;
504 bool prepared_ = false;
505 bool queued_transition_ = false;
506 Bank active_{};
507 Bank transition_{};
508 Bank queued_{};
509 Recipe requested_{};
510 std::array<BiquadCoefficientsT<SampleType>, MaxFormants> requested_coefficients_{};
511 std::array<double, MaxFormants> requested_weights_{};
512 double requested_tail_seconds_ = 0.0;
513};
514
517
518} // namespace pulp::signal
Definition biquad.hpp:53
@ bandpass
Definition biquad.hpp:58
void set_coefficients(const BiquadCoefficientsT< U > &c)
Definition biquad.hpp:69
A parallel bank of resonant band-pass sections for fixed source-filter colour.
Definition formant_filter_bank.hpp:50
static constexpr double max_headroom_db
Definition formant_filter_bank.hpp:63
FormantConfigureStatus configure(std::span< const FormantSpec > formants, double headroom_db=3.0) noexcept
Definition formant_filter_bank.hpp:121
static constexpr double min_q
Definition formant_filter_bank.hpp:58
static constexpr double min_bandwidth_hz
Definition formant_filter_bank.hpp:57
bool process_block(SampleType *samples, std::size_t frames) noexcept
Definition formant_filter_bank.hpp:238
std::size_t transition_samples() const noexcept
Definition formant_filter_bank.hpp:114
FormantConfigureStatus configure_vowel_morph(FormantVowel from, FormantVowel to, double amount, double headroom_db=3.0) noexcept
Definition formant_filter_bank.hpp:177
bool prepared() const noexcept
Definition formant_filter_bank.hpp:101
double magnitude(double frequency_hz) const noexcept
Exact complex-sum magnitude of the requested endpoint.
Definition formant_filter_bank.hpp:267
std::size_t capacity() const noexcept
Definition formant_filter_bank.hpp:104
static constexpr double min_gain_db
Definition formant_filter_bank.hpp:60
SampleType process(SampleType input) noexcept
Definition formant_filter_bank.hpp:192
void set_transition_samples(std::size_t samples) noexcept
Definition formant_filter_bank.hpp:111
static constexpr double max_gain_db
Definition formant_filter_bank.hpp:61
double tail_seconds() const noexcept
Definition formant_filter_bank.hpp:252
void response_curve_db(double min_hz, double max_hz, std::span< SampleType > output) const noexcept
Definition formant_filter_bank.hpp:296
bool prepare(double sample_rate, std::size_t capacity=MaxFormants) noexcept
Definition formant_filter_bank.hpp:78
static constexpr std::size_t default_transition_samples
Definition formant_filter_bank.hpp:64
void reset() noexcept
Definition formant_filter_bank.hpp:242
static constexpr double min_frequency_hz
Definition formant_filter_bank.hpp:56
static constexpr Recipe vowel_recipe(FormantVowel vowel) noexcept
Definition formant_filter_bank.hpp:319
BiquadCoefficientsT< SampleType > coefficients(std::size_t index) const noexcept
Definition formant_filter_bank.hpp:261
static constexpr double min_headroom_db
Definition formant_filter_bank.hpp:62
static Recipe interpolate(const Recipe &from, const Recipe &to, double amount) noexcept
Definition formant_filter_bank.hpp:340
static constexpr double max_q
Definition formant_filter_bank.hpp:59
FormantConfigureStatus configure(const Recipe &recipe) noexcept
Definition formant_filter_bank.hpp:169
const Recipe & requested_recipe() const noexcept
Definition formant_filter_bank.hpp:188
bool process_block(const SampleType *input, SampleType *output, std::size_t frames) noexcept
Definition formant_filter_bank.hpp:230
double sample_rate() const noexcept
Definition formant_filter_bank.hpp:107
static constexpr std::size_t storage_capacity
Definition formant_filter_bank.hpp:55
static constexpr int latency_samples() noexcept
Definition formant_filter_bank.hpp:248
double magnitude_db(double frequency_hz) const noexcept
Definition formant_filter_bank.hpp:292
bool transition_active() const noexcept
Definition formant_filter_bank.hpp:117
Evaluate what a biquad (or a cascade of them) does to the spectrum.
std::vector< Finding > validate(const Descriptor &descriptor, const std::vector< BakedParameter > &baked, std::string_view realization_mode={})
Definition catalog_descriptor.hpp:36
@ Recipe
Definition node_pack.hpp:43
Definition additive_bank.hpp:218
float magnitude_to_db(double magnitude)
Convert a linear magnitude to dB, floored at min_response_db.
Definition frequency_response.hpp:35
@ preserve_state
Definition sos_cascade.hpp:28
@ c
Definition spectrum_trace.hpp:35
@ not_prepared
Definition comb_filter.hpp:33
bool biquad_is_stable(const BiquadCoefficientsT< SampleType > &c) noexcept
Return whether a normalized real biquad's poles are strictly inside the unit circle.
Definition biquad.hpp:34
FormantVowel
Definition formant_filter_bank.hpp:20
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ o
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
FormantFilterBankT< float > FormantFilterBank
Definition formant_filter_bank.hpp:515
double angular_frequency(double freq_hz, double sample_rate)
Angular frequency (radians/sample) of freq_hz at sample_rate, clamped to [0, π].
Definition frequency_response.hpp:97
FormantFilterBankT< double > FormantFilterBank64
Definition formant_filter_bank.hpp:516
FormantConfigureStatus
Definition formant_filter_bank.hpp:22
@ unordered
Definition formant_filter_bank.hpp:28
@ non_finite
Definition formant_filter_bank.hpp:26
@ configured
Definition formant_filter_bank.hpp:23
@ out_of_range
Definition formant_filter_bank.hpp:27
@ invalid_count
Definition formant_filter_bank.hpp:25
@ unstable
Definition formant_filter_bank.hpp:29
@ not_prepared
Definition formant_filter_bank.hpp:24
float sum(const float *data, std::size_t count) noexcept
@ q
Definition input_events.hpp:41
Definition biquad.hpp:24
Definition formant_filter_bank.hpp:66
double gain_db
Definition formant_filter_bank.hpp:69
double bandwidth_hz
Definition formant_filter_bank.hpp:68
double frequency_hz
Definition formant_filter_bank.hpp:67
Definition formant_filter_bank.hpp:72
double headroom_db
Definition formant_filter_bank.hpp:75
std::array< FormantSpec, MaxFormants > formants
Definition formant_filter_bank.hpp:73
std::size_t count
Definition formant_filter_bank.hpp:74