51 static_assert(std::is_floating_point_v<SampleType>);
52 static_assert(MaxFormants >= 1 && MaxFormants <= 8);
58 static constexpr double min_q = 0.5;
59 static constexpr double max_q = 50.0;
73 std::array<FormantSpec, MaxFormants>
formants{};
79 const SampleType processing_rate =
static_cast<SampleType
>(
sample_rate);
80 if (!std::isfinite(
sample_rate) || !std::isfinite(processing_rate) ||
84 sample_rate_ =
static_cast<double>(processing_rate);
89 requested_coefficients_ = {};
90 requested_weights_ = {};
91 requested_tail_seconds_ = 0.0;
93 transition_total_ = 0;
94 transition_remaining_ = 0;
95 queued_transition_samples_ = 0;
96 queued_transition_ =
false;
112 transition_samples_ = samples;
115 return transition_samples_;
118 return transition_remaining_ != 0;
122 double headroom_db = 3.0) noexcept {
125 const auto status = validate(formants, headroom_db);
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_))
136 requested_ = candidate;
137 capture_requested(transition_);
139 queued_transition_samples_ = 0;
140 queued_transition_ =
false;
144 if (!design(candidate, designed))
147 requested_ = candidate;
148 capture_requested(designed);
150 if (transition_samples_ == 0) {
151 load_coefficients(designed, active_,
true);
153 transition_ = active_;
154 load_coefficients(designed, transition_,
true);
155 transition_total_ = transition_samples_;
156 transition_remaining_ = transition_samples_;
158 }
else if (transition_remaining_ == transition_total_) {
159 load_coefficients(designed, transition_,
true);
162 queued_transition_samples_ =
163 transition_samples_ != 0 ? transition_samples_ : transition_total_;
164 queued_transition_ =
true;
170 if (recipe.count > MaxFormants)
173 return configure(std::span<const FormantSpec>(recipe.formants.data(), recipe.count),
178 double headroom_db = 3.0) noexcept {
179 if (!std::isfinite(amount))
181 if (amount < 0.0 || amount > 1.0)
184 recipe.headroom_db = headroom_db;
192 SampleType
process(SampleType input)
noexcept {
195 if (!std::isfinite(
static_cast<double>(input))) {
202 output = process_bank(active_,
static_cast<double>(input));
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;
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;
222 if (!std::isfinite(output)) {
226 const double limit =
static_cast<double>(std::numeric_limits<SampleType>::max());
227 return static_cast<SampleType
>(std::clamp(output, -limit, limit));
230 bool process_block(
const SampleType* input, SampleType* output, std::size_t frames)
noexcept {
231 if ((input ==
nullptr || output ==
nullptr) && frames != 0)
233 for (std::size_t
i = 0;
i < frames; ++
i)
244 reset_bank(transition_);
253 double result = requested_tail_seconds_;
255 result = std::max(result, tail_of(active_));
256 result = std::max(result, tail_of(transition_));
262 return index < requested_.count ? requested_coefficients_[index]
268 if (!prepared_ || requested_.count == 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();
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))
287 sum += requested_weights_[
i] * numerator / denominator;
289 return std::abs(sum);
297 std::span<SampleType> output)
const noexcept {
301 std::fill(output.begin(), output.end(), SampleType{});
304 const double nyquist = sample_rate_ * 0.5;
305 const double lo = std::clamp(std::isfinite(min_hz) ? min_hz :
min_frequency_hz,
307 const double clamped_hi =
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
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)));
322 return make_vowel({800, 1150, 2900, 3900, 4950}, {80, 90, 120, 130, 140},
323 {0, -4, -9, -12, -15});
325 return make_vowel({400, 1600, 2700, 3300, 4950}, {70, 100, 120, 150, 200},
326 {0, -4, -8, -12, -15});
328 return make_vowel({350, 1700, 2700, 3700, 4950}, {60, 100, 120, 150, 200},
329 {0, -3, -9, -12, -15});
331 return make_vowel({450, 800, 2830, 3800, 4950}, {70, 80, 100, 130, 150},
332 {0, -5, -10, -13, -16});
334 return make_vowel({325, 700, 2530, 3500, 4950}, {50, 60, 170, 180, 200},
335 {0, -6, -12, -15, -18});
340 static Recipe
interpolate(
const Recipe& from,
const Recipe& to,
double amount)
noexcept {
342 if (!std::isfinite(amount))
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;
362 std::array<BiquadT<SampleType>, MaxFormants> filters{};
363 std::array<double, MaxFormants> weights{};
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 {
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]};
377 static bool recipes_equal(
const Recipe& lhs,
const Recipe& rhs)
noexcept {
378 if (lhs.count != rhs.count || lhs.headroom_db != rhs.headroom_db)
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)
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;
415 bool design(
const Recipe& recipe, Bank& bank)
noexcept {
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];
427 static_cast<SampleType
>(formant.frequency_hz / formant.bandwidth_hz),
428 static_cast<SampleType
>(sample_rate_));
429 const auto coefficients = designer.coefficients();
432 bank.filters[
i].set_coefficients(coefficients);
433 bank.weights[
i] = headroom_gain * std::pow(10.0, formant.gain_db / 20.0) / weight_sum;
439 return std::isfinite(coefficients.b0) && std::isfinite(coefficients.b1) &&
440 std::isfinite(coefficients.b2) && std::isfinite(coefficients.a1) &&
441 std::isfinite(coefficients.a2);
444 void load_coefficients(
const Bank& source, Bank& destination,
bool preserve_state)
noexcept {
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();
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];
464 requested_tail_seconds_ = tail_of(source);
467 static double process_bank(Bank& bank,
double input)
noexcept {
468 if (bank.recipe.count == 0)
471 for (std::size_t
i = 0;
i < bank.recipe.count; ++
i)
472 sum += bank.weights[
i] * bank.filters[
i].process(input);
476 static void reset_bank(Bank& bank)
noexcept {
477 for (
auto& filter : bank.filters)
481 double tail_of(
const Bank& bank)
const noexcept {
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),
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_));
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;
510 std::array<BiquadCoefficientsT<SampleType>, MaxFormants> requested_coefficients_{};
511 std::array<double, MaxFormants> requested_weights_{};
512 double requested_tail_seconds_ = 0.0;
@ bandpass
Definition biquad.hpp:58
void set_coefficients(const BiquadCoefficientsT< U > &c)
Definition biquad.hpp:69
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