Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
flanger.hpp
Go to the documentation of this file.
1#pragma once
2
119
126#include <pulp/signal/lfo.hpp>
128#include <pulp/signal/units.hpp>
129
130#include <algorithm>
131#include <array>
132#include <cmath>
133#include <cstddef>
134#include <cstdint>
135#include <vector>
136
137namespace pulp::signal {
138
140enum class FlangerMode : std::uint8_t {
144};
145
147enum class FlangerPolarity : std::uint8_t {
150};
151
153enum class FlangerDelayEngine : std::uint8_t {
156};
157
159template <typename SampleType = float>
160class FlangerT {
161public:
165 using Wave = LfoWave;
166
167 // ── Design parameters (the complete roster) ───────────────────────────
168
172 static constexpr double kFbClamp = 0.97;
173
185 static constexpr double kLoopElementGainBound = 1.012;
186
191 static constexpr double kDcBlockHz = 5.0;
192
194 static constexpr double kDepthMinMs = 0.02;
195 static constexpr double kDepthMaxMs = 4.0;
196
200 static constexpr double kCenterMinMs = 0.1;
201 static constexpr double kCenterMaxMs = 10.0;
202
205 static constexpr double kOffsetMinMs = 0.5;
206 static constexpr double kOffsetMaxMs = 10.0;
207
209 static constexpr double kRateMinHz = 0.02;
210 static constexpr double kRateMaxHz = 10.0;
211
217 static constexpr double kMinClassicDelayMs = 0.05;
218
222 static constexpr int kInterpGuardSamples = 2;
223
228 static constexpr double kControlSmoothMs = 3.0;
229
233 static constexpr double kModeSwitchMs = 5.0;
234
238 static constexpr double kBarberpoleShiftMaxHz = 20.0;
239
245 static constexpr double kBbdCharacter = 0.5;
246
247 // ── Lifecycle ─────────────────────────────────────────────────────────
248
251 void prepare(double sample_rate) {
252 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
253
254 // The modulated line has to hold the widest delay any mode can reach:
255 // through-zero's `2 · kOffsetMaxMs` beats classic's centre + depth.
256 const double widest_ms = std::max(2.0 * kOffsetMaxMs, kCenterMaxMs + kDepthMaxMs);
257 modulated_capacity_ =
258 static_cast<std::size_t>(std::ceil(units::ms_to_samples(widest_ms, sample_rate_))) +
259 static_cast<std::size_t>(kInterpGuardSamples) + 4u;
260 fixed_capacity_ =
261 static_cast<std::size_t>(std::ceil(units::ms_to_samples(kOffsetMaxMs, sample_rate_))) +
262 2u;
263
264 const auto pole = static_cast<double>(std::exp(-kTwoPi * kDcBlockHz / sample_rate_));
265 for (auto& ch : channels_) {
266 ch.line.prepare(modulated_capacity_);
267 ch.fixed.assign(fixed_capacity_, 0.0);
268 ch.fixed_valid = 0;
269 ch.blocker.set_pole(pole);
270 ch.bbd.prepare(sample_rate_);
271 // The shifter runs as a pure translator inside this loop: the
272 // flanger owns the feedback and the mix, so the shifter must own
273 // neither, or the spiral would be shaped twice.
274 ch.shifter.prepare(sample_rate_);
275 ch.shifter.set_feedback(0.0);
276 ch.shifter.set_mix(1.0);
277 ch.shifter.set_shift_hz(barberpole_hz_);
278 // One smoother per channel: the two rails read the modulated line
279 // at different delays, so a shared smoother would hand each
280 // channel the other's ramp.
281 ch.delay_smoother.set_ramp_time(kControlSmoothMs * 0.001, sample_rate_);
282 }
283
284 for (auto& lfo : lfos_) lfo.prepare(sample_rate_);
285 mode_switch_samples_ =
286 std::max(2, static_cast<int>(std::lround(units::ms_to_samples(kModeSwitchMs,
287 sample_rate_))));
288 apply_lfo_settings();
289 reset();
290 }
291
294 void reset() {
295 for (auto& ch : channels_) {
296 ch.line.reset();
297 std::fill(ch.fixed.begin(), ch.fixed.end(), 0.0);
298 ch.fixed_write = 0;
299 ch.fixed_valid = 0;
300 ch.blocker.reset();
301 ch.bbd.reset();
302 ch.shifter.reset();
303 ch.feedback = 0.0;
304 ch.delay_ms = 0.0;
305 ch.delay_smoother.set_immediate(delay_samples_for(mode_, 0.0));
306 }
307 for (auto& lfo : lfos_) lfo.reset();
308 mode_switch_remaining_ = 0;
309 previous_mode_ = mode_;
310 }
311
314 void discard_history() noexcept {
315 for (auto& ch : channels_) {
316 ch.line.discard_history();
317 ch.fixed_write = 0;
318 ch.fixed_valid = 0;
319 ch.blocker.reset();
320 ch.bbd.reset();
321 ch.shifter.discard_history();
322 ch.feedback = 0.0;
323 ch.delay_ms = 0.0;
324 ch.delay_smoother.set_immediate(delay_samples_for(mode_, 0.0));
325 }
326 for (auto& lfo : lfos_) lfo.reset();
327 mode_switch_remaining_ = 0;
328 previous_mode_ = mode_;
329 }
330
331 // ── Parameters (real units throughout) ────────────────────────────────
332
337 if (mode == mode_) return;
338 previous_mode_ = mode_;
339 mode_ = mode;
340 mode_switch_remaining_ = mode_switch_samples_;
341 }
342
343 Mode mode() const { return mode_; }
344
345 void set_polarity(Polarity polarity) { polarity_ = polarity; }
346 Polarity polarity() const { return polarity_; }
347
348 void set_delay_engine(Engine engine) { engine_ = engine; }
349 Engine delay_engine() const { return engine_; }
350
351 void set_rate_hz(double hz) {
352 if (!std::isfinite(hz)) return;
353 rate_hz_ = std::clamp(hz, kRateMinHz, kRateMaxHz);
354 apply_lfo_settings();
355 }
356
357 void set_waveform(Wave wave) {
358 wave_ = wave;
359 apply_lfo_settings();
360 }
361
369 void set_stereo_spread(double cycles01) {
370 if (!std::isfinite(cycles01)) return;
371 stereo_spread_ = std::clamp(cycles01, 0.0, 0.5);
372 apply_lfo_settings();
373 }
374
378 void set_depth_ms(double ms) {
379 if (!std::isfinite(ms)) return;
380 depth_ms_ = std::clamp(ms, 0.0, kDepthMaxMs);
381 }
382 double depth_ms() const { return depth_ms_; }
383
384 void set_center_delay_ms(double ms) {
385 if (!std::isfinite(ms)) return;
386 center_ms_ = std::clamp(ms, kCenterMinMs, kCenterMaxMs);
387 }
388 double center_delay_ms() const { return center_ms_; }
389
390 void set_offset_ms(double ms) {
391 if (!std::isfinite(ms)) return;
392 offset_ms_ = std::clamp(ms, kOffsetMinMs, kOffsetMaxMs);
393 }
394 double offset_ms() const { return offset_ms_; }
395
399 void set_feedback(double fb) {
400 if (!std::isfinite(fb)) return;
401 feedback_ = std::clamp(fb, -kFbClamp, kFbClamp);
402 }
403 double feedback() const { return feedback_; }
404
408 void set_mix(double wet01) {
409 if (!std::isfinite(wet01)) return;
410 mix_ = std::clamp(wet01, 0.0, 1.0);
411 }
412 double mix() const { return mix_; }
413
415 void set_barberpole_shift_hz(double hz) {
416 if (!std::isfinite(hz)) return;
417 barberpole_hz_ = std::clamp(hz, -kBarberpoleShiftMaxHz, kBarberpoleShiftMaxHz);
418 for (auto& ch : channels_) ch.shifter.set_shift_hz(barberpole_hz_);
419 }
420 double barberpole_shift_hz() const { return barberpole_hz_; }
421
422 // ── Processing ────────────────────────────────────────────────────────
423
424 void process(const SampleType* in, SampleType* out, int n) {
425 for (int i = 0; i < n; ++i) {
426 if (!std::isfinite(static_cast<double>(in[i]))) {
428 out[i] = SampleType{0};
429 continue;
430 }
431 const Sweep sweep = advance_sweep();
432 out[i] = static_cast<SampleType>(
433 run_channel(channels_[0], static_cast<double>(in[i]), sweep));
434 }
435 }
436
437 void process_stereo(const SampleType* in_left, const SampleType* in_right,
438 SampleType* out_left, SampleType* out_right, int n) {
439 for (int i = 0; i < n; ++i) {
440 if (!std::isfinite(static_cast<double>(in_left[i])) ||
441 !std::isfinite(static_cast<double>(in_right[i]))) {
443 out_left[i] = out_right[i] = SampleType{0};
444 continue;
445 }
446 const Sweep sweep = advance_sweep();
447 out_left[i] = static_cast<SampleType>(
448 run_channel(channels_[0], static_cast<double>(in_left[i]), sweep));
449 out_right[i] = static_cast<SampleType>(
450 run_channel(channels_[1], static_cast<double>(in_right[i]), sweep, 1));
451 }
452 }
453
467 int latency_samples() const noexcept {
468 return mode_ == Mode::through_zero ? fixed_delay_samples() : 0;
469 }
470
471 // ── Derived quantities (pure functions of the shipped constants) ───────
472
484 double effective_depth_ms() const { return effective_depth_for(mode_); }
485
490 double instantaneous_delay_ms(int channel = 0) const {
491 return channels_[static_cast<std::size_t>(channel)].delay_ms;
492 }
493
496 int fixed_delay_samples() const noexcept {
497 return static_cast<int>(std::lround(units::ms_to_samples(offset_ms_, sample_rate_)));
498 }
499
503 static double notch_hz(int k, double delay_ms, Polarity polarity) {
504 const double d = delay_ms * 0.001;
505 return polarity == Polarity::positive ? (2.0 * k + 1.0) / (2.0 * d)
506 : static_cast<double>(k) / d;
507 }
508
511 static double notch_spacing_hz(double delay_ms) { return 1.0 / (delay_ms * 0.001); }
512
517 static constexpr double worst_case_gain() {
518 return 1.0 / (1.0 - kFbClamp * kLoopElementGainBound);
519 }
520
524 static void mix_gains(double wet01, double& dry_gain, double& wet_gain) {
525 const double theta = std::clamp(wet01, 0.0, 1.0) * 0.5 * kPi;
526 dry_gain = std::cos(theta);
527 wet_gain = std::sin(theta);
528 }
529
530private:
531 static constexpr double kPi = 3.14159265358979323846;
532 static constexpr double kTwoPi = 6.283185307179586476925286766559;
533
536 struct Sweep {
537 double lfo[2] = {0.0, 0.0};
538 double wet_scale = 1.0;
539 double old_dry = 0.0;
540 double new_dry = 1.0;
541 bool switching = false;
542 bool past_midpoint = true;
543 bool snap_delay = false;
544 };
545
546 struct Channel {
547 chardelay::FractionalDelayLine line{};
548 std::vector<double> fixed{};
549 std::size_t fixed_write = 0;
550 std::size_t fixed_valid = 0;
551 DcBlocker<double> blocker{};
552 chardelay::BbdChannel bbd{};
553 SsbFrequencyShifterT<double> shifter{};
554 SmoothedValue<double> delay_smoother{};
555 double feedback = 0.0;
556 double delay_ms = 0.0;
557 };
558
559 Sweep advance_sweep() {
560 Sweep s;
561 // Both modulators advance every frame, in mono as in stereo. They are
562 // two `LfoT` instances rather than one read at two offsets, and they
563 // cannot drift apart: identical increments accumulated in `double`
564 // from an identical reset produce identical phase forever, so the
565 // spread is a fixed angular relationship rather than two clocks that
566 // might disagree over a long render.
567 s.lfo[0] = static_cast<double>(lfos_[0].next());
568 s.lfo[1] = static_cast<double>(lfos_[1].next());
569
570 const bool was_past = past_midpoint();
571 if (mode_switch_remaining_ > 0) --mode_switch_remaining_;
572 s.switching = mode_switch_remaining_ > 0;
573 s.past_midpoint = past_midpoint();
574 s.snap_delay = !was_past && s.past_midpoint;
575
576 if (s.switching) {
577 const double progress =
578 1.0 - static_cast<double>(mode_switch_remaining_) /
579 static_cast<double>(mode_switch_samples_);
581 s.old_dry, s.new_dry);
582 // A raised-cosine notch: 1 at both ends, exactly 0 at the midpoint
583 // where the delay law changes hands.
584 s.wet_scale = std::abs(std::cos(kPi * progress));
585 }
586 return s;
587 }
588
589 bool past_midpoint() const {
590 return mode_switch_remaining_ <= 0 || mode_switch_remaining_ * 2 <= mode_switch_samples_;
591 }
592
594 double delay_samples_for(Mode mode, double lfo) const {
595 double ms = 0.0;
596 switch (mode) {
597 case Mode::classic:
598 ms = center_ms_ + effective_depth_for(mode) * lfo;
599 break;
600 case Mode::through_zero:
601 // Anchored to the dry path's REALISED integer delay rather
602 // than to `offset_ms` in the abstract. The spec's law is
603 // written in milliseconds, but the dry line is an integer
604 // buffer, so an offset that does not land on a whole sample
605 // would put the crossing between two samples and the null
606 // would never be exact. Sweeping about the integer the dry
607 // path actually uses makes the crossing exact at every offset.
608 return static_cast<double>(fixed_delay_samples()) +
609 units::ms_to_samples(effective_depth_for(mode), sample_rate_) * lfo;
610 case Mode::barberpole:
611 ms = center_ms_; // fixed; the shift, not the delay, moves
612 break;
613 }
614 return units::ms_to_samples(ms, sample_rate_);
615 }
616
617 double effective_depth_for(Mode mode) const {
618 switch (mode) {
619 case Mode::classic:
620 return std::clamp(depth_ms_, 0.0, center_ms_ - kMinClassicDelayMs);
621 case Mode::through_zero:
622 return std::min(depth_ms_, offset_ms_);
623 case Mode::barberpole:
624 return 0.0;
625 }
626 return 0.0;
627 }
628
640 double run_channel(Channel& ch, double input, const Sweep& sweep, int index = 0) {
641 const Mode active = sweep.past_midpoint ? mode_ : previous_mode_;
642
643 // Delay-time target. Smoothed so a host automating the centre does not
644 // step the read pointer.
645 const double target = delay_samples_for(active, sweep.lfo[index]);
646 if (sweep.snap_delay) ch.delay_smoother.set_immediate(target);
647 ch.delay_smoother.set_target(target);
648 const double delay = std::clamp(ch.delay_smoother.next(), 1.0, ch.line.max_delay());
649 ch.delay_ms = units::samples_to_ms(delay, sample_rate_);
650
651 // The dry reference. Both forms are maintained at all times — the
652 // fixed line is written every sample regardless of mode — so a mode
653 // change crossfades between two dry paths that are both already
654 // correct, with no buffer to fill.
655 const double dry_direct = input;
656 const double dry_fixed = read_fixed(ch);
657 push_fixed(ch, input);
658 const double dry =
659 sweep.switching
660 ? sweep.old_dry * dry_for(previous_mode_, dry_direct, dry_fixed) +
661 sweep.new_dry * dry_for(mode_, dry_direct, dry_fixed)
662 : dry_for(active, dry_direct, dry_fixed);
663
664 // The transition window gates the wet path AT ITS SOURCE, not just on
665 // the way to the mix, and that placement is the whole point. Silencing
666 // only the mix contribution still writes a discontinuous feedback value
667 // into the delay line at the instant the delay law changes hands — and
668 // that step re-emerges one loop delay later, at full gain, long after
669 // the crossfade has finished. Gating the tap instead means the line,
670 // the DC blocker and the shifter all see a signal that goes smoothly to
671 // zero and back, so there is nothing discontinuous anywhere to come
672 // back around.
673 const double sign = polarity_ == Polarity::negative ? -1.0 : 1.0;
674 double wet = 0.0;
675
676 if (engine_ == Engine::bbd) {
677 // The bucket line owns its own delay, so its tap cannot be read
678 // ahead of the write the way the clean line's can. Its loop
679 // therefore carries one extra sample, which is inaudible against a
680 // clocked line whose own delay quantises to its bucket count
681 // anyway — but it is a real difference from the clean engine and
682 // is stated rather than left to be discovered.
683 const double recirculated = snap_to_zero(ch.blocker.process(ch.feedback));
684 ch.bbd.update(kBbdCharacter, delay / sample_rate_);
685 wet = ch.bbd.process(input + feedback_ * sign * recirculated) * sweep.wet_scale;
686 } else {
687 // The tap is read at `delay − 1` BEFORE the write, which is
688 // bit-for-bit the same sample a read at `delay` after the write
689 // would return — the same four taps at the same fractional offset,
690 // with the buffer shifted by one instead of the request. Doing it
691 // in this order is what makes the loop exactly `delay` samples
692 // long rather than `delay + 1`.
693 //
694 // That one sample is not a rounding detail. The resonance sits
695 // where the round-trip phase is zero, so a loop of `delay + 1`
696 // puts the peaks at `1/(D+1)` while the feedforward comb's teeth
697 // stay at `1/D` — 0.7 % apart at the default 3 ms delay, and 42 %
698 // apart at the 0.05 ms floor, where the comb and its own resonance
699 // would be audibly detuned from each other.
700 wet = ch.line.read(std::max(1.0, delay - 1.0)) * sweep.wet_scale;
701 // Barberpole puts the shifter INSIDE the loop, which is what makes
702 // the pattern climb without end: every recirculation adds another
703 // Δf, so the comb never lands back where it started the way an LFO
704 // sweep does. The shifter is a pure translator here — its own
705 // feedback and mix are off, because this module owns both.
706 if (active == Mode::barberpole) wet = ch.shifter.process(wet);
707 // The recirculating tap is DC-blocked so rounding cannot
708 // accumulate a drift the (deliberately absent) saturator would
709 // otherwise bound, then denormal-snapped because a decaying loop
710 // tail approaches zero without reaching it. The FORWARD wet tap is
711 // neither, which is what keeps the through-zero crossing an exact
712 // cancellation rather than one filtered path against another.
713 const double recirculated = snap_to_zero(ch.blocker.process(wet));
714 ch.line.push(input + feedback_ * sign * recirculated);
715 }
716 ch.feedback = wet;
717
718 double dry_gain = 0.0, wet_gain = 0.0;
719 mix_gains(mix_, dry_gain, wet_gain);
720 // `wet` already carries the transition gate — it was applied at the tap.
721 return snap_to_zero(dry_gain * dry + wet_gain * sign * wet);
722 }
723
724 static double dry_for(Mode mode, double direct, double fixed) {
725 return mode == Mode::through_zero ? fixed : direct;
726 }
727
728 double read_fixed(const Channel& ch) const {
729 const auto delay = static_cast<std::size_t>(std::max(0, fixed_delay_samples()));
730 const std::size_t capacity = ch.fixed.size();
731 if (capacity == 0) return 0.0;
732 if (ch.fixed_valid < capacity && delay >= ch.fixed_valid) return 0.0;
733 const std::size_t index = (ch.fixed_write + capacity - (delay % capacity)) % capacity;
734 return ch.fixed[index];
735 }
736
737 void push_fixed(Channel& ch, double x) {
738 if (ch.fixed.empty()) return;
739 ch.fixed[ch.fixed_write] = x;
740 ch.fixed_write = (ch.fixed_write + 1u) % ch.fixed.size();
741 ch.fixed_valid = std::min(ch.fixed_valid + 1u, ch.fixed.size());
742 }
743
744 void apply_lfo_settings() {
745 for (std::size_t i = 0; i < lfos_.size(); ++i) {
746 lfos_[i].set_rate_hz(rate_hz_);
747 lfos_[i].set_wave(wave_);
748 lfos_[i].set_phase_offset(i == 0 ? 0.0 : stereo_spread_);
749 }
750 }
751
752 double sample_rate_ = 44100.0;
753 std::size_t modulated_capacity_ = 8;
754 std::size_t fixed_capacity_ = 8;
755
756 Mode mode_ = Mode::classic;
757 Mode previous_mode_ = Mode::classic;
758 Polarity polarity_ = Polarity::positive;
759 Engine engine_ = Engine::clean;
760 Wave wave_ = LfoWave::sine;
761
762 double rate_hz_ = 0.5;
763 double depth_ms_ = 1.5;
764 double center_ms_ = 3.0;
765 double offset_ms_ = 4.0;
766 double feedback_ = 0.6;
767 double mix_ = 0.5;
768 double stereo_spread_ = 0.25;
769 double barberpole_hz_ = 3.0;
770
771 int mode_switch_samples_ = 2;
772 int mode_switch_remaining_ = 0;
773
774 std::array<Channel, 2> channels_{};
775 std::array<EffectLfoT<double>, 2> lfos_{};
776};
777
780
781} // namespace pulp::signal
The flanger.
Definition flanger.hpp:160
void set_mode(Mode mode)
Takes effect through a kModeSwitchMs transition rather than instantly.
Definition flanger.hpp:336
double offset_ms() const
Definition flanger.hpp:394
static constexpr double kControlSmoothMs
Definition flanger.hpp:228
Polarity polarity() const
Definition flanger.hpp:346
void prepare(double sample_rate)
Sizes both delay buffers for the widest sweep any mode can ask for and prepares the composed engines.
Definition flanger.hpp:251
static constexpr double kCenterMaxMs
Definition flanger.hpp:201
int latency_samples() const noexcept
Zero in classic and barberpole modes — the dry path is a direct passthrough and the mix is a gain,...
Definition flanger.hpp:467
void set_offset_ms(double ms)
Definition flanger.hpp:390
static constexpr double kFbClamp
Definition flanger.hpp:172
static constexpr int kInterpGuardSamples
Definition flanger.hpp:222
static constexpr double kMinClassicDelayMs
Definition flanger.hpp:217
void process_stereo(const SampleType *in_left, const SampleType *in_right, SampleType *out_left, SampleType *out_right, int n)
Definition flanger.hpp:437
void set_waveform(Wave wave)
Definition flanger.hpp:357
static constexpr double kBbdCharacter
Definition flanger.hpp:245
void set_delay_engine(Engine engine)
Definition flanger.hpp:348
Mode mode() const
Definition flanger.hpp:343
void set_mix(double wet01)
Equal-power dry/wet, in the range [0, 1].
Definition flanger.hpp:408
double feedback() const
Definition flanger.hpp:403
Engine delay_engine() const
Definition flanger.hpp:349
void reset()
Rewinds every buffer, filter, oscillator and generator.
Definition flanger.hpp:294
static double notch_spacing_hz(double delay_ms)
Spacing between adjacent notches — the reciprocal of the delay, for either polarity.
Definition flanger.hpp:511
void set_feedback(double fb)
Resonance, in the range [-kFbClamp, +kFbClamp].
Definition flanger.hpp:399
void set_stereo_spread(double cycles01)
Stereo spread in CYCLES of the sweep, in the range [0, 0.5].
Definition flanger.hpp:369
static constexpr double kCenterMinMs
Definition flanger.hpp:200
static constexpr double kModeSwitchMs
Definition flanger.hpp:233
void set_rate_hz(double hz)
Definition flanger.hpp:351
static constexpr double kOffsetMaxMs
Definition flanger.hpp:206
void set_barberpole_shift_hz(double hz)
Barberpole shift in Hz. Positive climbs, negative descends.
Definition flanger.hpp:415
LfoWave Wave
Definition flanger.hpp:165
double instantaneous_delay_ms(int channel=0) const
The delay the modulated line was last read at, in ms.
Definition flanger.hpp:490
void set_depth_ms(double ms)
Modulation excursion.
Definition flanger.hpp:378
double center_delay_ms() const
Definition flanger.hpp:388
void process(const SampleType *in, SampleType *out, int n)
Definition flanger.hpp:424
double mix() const
Definition flanger.hpp:412
void set_center_delay_ms(double ms)
Definition flanger.hpp:384
int fixed_delay_samples() const noexcept
The fixed dry delay in samples, exact and integer.
Definition flanger.hpp:496
void discard_history() noexcept
Constant-time audio fault recovery.
Definition flanger.hpp:314
double effective_depth_ms() const
The excursion actually in force, after the mode's clamp.
Definition flanger.hpp:484
static constexpr double kRateMaxHz
Definition flanger.hpp:210
double barberpole_shift_hz() const
Definition flanger.hpp:420
double depth_ms() const
Definition flanger.hpp:382
static constexpr double kRateMinHz
Definition flanger.hpp:209
static constexpr double kDepthMinMs
Definition flanger.hpp:194
static double notch_hz(int k, double delay_ms, Polarity polarity)
Notch frequencies of the comb at a given delay, from Δf = 1/D.
Definition flanger.hpp:503
void set_polarity(Polarity polarity)
Definition flanger.hpp:345
static constexpr double kOffsetMinMs
Definition flanger.hpp:205
FlangerMode Mode
Definition flanger.hpp:162
static void mix_gains(double wet01, double &dry_gain, double &wet_gain)
The dry and wet weights the shipped equal-power law produces at a given mix.
Definition flanger.hpp:524
static constexpr double kBarberpoleShiftMaxHz
Definition flanger.hpp:238
FlangerPolarity Polarity
Definition flanger.hpp:163
FlangerDelayEngine Engine
Definition flanger.hpp:164
static constexpr double worst_case_gain()
The bound the Forge registry cites (series law 8): the steady-state envelope of a recursive comb at t...
Definition flanger.hpp:517
static constexpr double kDepthMaxMs
Definition flanger.hpp:195
static constexpr double kLoopElementGainBound
Definition flanger.hpp:185
static constexpr double kDcBlockHz
Definition flanger.hpp:191
The one shared old→new crossfade gain law for the SIGNAL side.
The single-sideband (Bode/Moog) frequency shifter: a Hilbert phase-difference network,...
The house low-frequency oscillator: seven waveforms plus a continuous shape morph,...
@ index
Definition gpu_pass.hpp:140
T samples_to_ms(T samples, T sample_rate)
Samples → milliseconds at sample_rate Hz.
Definition units.hpp:153
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
Definition additive_bank.hpp:218
FlangerMode
Which of the three sweep topologies the engine runs.
Definition flanger.hpp:140
@ through_zero
Swept wet crossing a fixed-delay dry. Latency = offset.
Definition flanger.hpp:142
@ barberpole
Fixed delay, frequency-shifted wet. Latency 0.
Definition flanger.hpp:143
@ classic
One modulated delay against an undelayed dry. Latency 0.
Definition flanger.hpp:141
@ delay
Definition envelope.hpp:57
FlangerDelayEngine
Which delay engine fills the modulated slot.
Definition flanger.hpp:153
@ clean
Lagrange-interpolated line. Linear, transparent, the default.
Definition flanger.hpp:154
@ bbd
The bucket-brigade character model: dark, companded, alive.
Definition flanger.hpp:155
FlangerT< double > Flanger64
Definition flanger.hpp:779
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ EqualPower
(cos(u·π/2), sin(u·π/2)) — power sum == 1.
Definition crossfade.hpp:43
constexpr T crossfade_smoothstep(T t) noexcept
Smoothstep progress shaping: cubic 3u²−2u³ with zero slope at both ends, clamped to [0,...
Definition crossfade.hpp:50
@ i
Definition formant_filter_bank.hpp:20
LfoWave
The effect-wave vocabulary, shared by these functions and by EffectLfoT (lfo.hpp),...
Definition lfo_sampling.hpp:53
@ sine
Definition lfo_sampling.hpp:54
@ direct
set_shift_semitones. Detents and the pedal law are bypassed.
Definition pitch_shifter.hpp:194
void crossfade_gains(T u, CrossfadeGainLaw law, T &old_gain, T &new_gain) noexcept
Old/new blend gains for an ALREADY-SHAPED progress u (the caller applies any smoothstep/clamp first).
Definition crossfade.hpp:61
FlangerPolarity
The sign of the wet contribution, in the mix and in the feedback tap alike.
Definition flanger.hpp:147
@ negative
y = dry − wet. Notches at k/D, including one at DC.
Definition flanger.hpp:149
@ positive
y = dry + wet. Notches at (2k+1)/(2D).
Definition flanger.hpp:148
FlangerT< float > Flanger
Definition flanger.hpp:778
@ progress
Definition finite_stretch_builder.hpp:30
@ s
Definition input_events.hpp:41
@ x
Definition input_events.hpp:41
@ fixed
Definition design_ir.hpp:61
The real-unit vocabulary shared by every DSP block in the catalog.