Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
frequency_shifter_ssb.hpp
Go to the documentation of this file.
1#pragma once
2
150
156#include <pulp/signal/units.hpp>
157
158#include <algorithm>
159#include <array>
160#include <cmath>
161#include <cstddef>
162#include <cstdint>
163
164namespace pulp::signal {
165
209template <typename SampleType = double>
211public:
217 static constexpr std::size_t kSections = 4;
218
224 static constexpr std::array<double, kSections> kInPhasePoles = {
225 0.4021921162426, 0.8561710882420, 0.9722909545651, 0.9952884791278};
226
229 static constexpr std::array<double, kSections> kQuadraturePoles = {
230 0.6923878, 0.9360654322959, 0.9882295226860, 0.9987488452737};
231
240 static constexpr double kBandEdgeNormalized = 4.6e-4;
241
246 static constexpr double kImageRejectDb = 40.0;
247
248 struct Outputs {
249 SampleType in_phase = 0;
250 SampleType quadrature = 0;
251 };
252
255 void reset() {
256 in_phase_state_ = {};
257 quadrature_state_ = {};
258 delayed_ = 0;
259 }
260
261 Outputs process(SampleType x) {
262 const SampleType in_phase = cascade(x, kInPhasePoles, in_phase_state_);
263 const SampleType quadrature = cascade(x, kQuadraturePoles, quadrature_state_);
264 const Outputs out{in_phase, delayed_};
265 delayed_ = quadrature;
266 return out;
267 }
268
271 static constexpr double band_low_hz(double sample_rate) {
272 return kBandEdgeNormalized * sample_rate;
273 }
274
276 static constexpr double band_high_hz(double sample_rate) {
277 return (0.5 - kBandEdgeNormalized) * sample_rate;
278 }
279
280private:
285 struct SectionState {
286 SampleType s1 = 0;
287 SampleType s2 = 0;
288 };
289
290 using BranchState = std::array<SectionState, kSections>;
291
292 static SampleType cascade(SampleType x, const std::array<double, kSections>& poles,
293 BranchState& state) {
294 SampleType v = x;
295 for (std::size_t k = 0; k < kSections; ++k) {
296 const auto c = static_cast<SampleType>(poles[k] * poles[k]);
297 const SampleType y = c * v + state[k].s1;
298 state[k].s1 = state[k].s2;
299 // Snap the recursive state: the top sections' poles sit at
300 // |z| = 0.9994, so a decaying tail stalls in denormals for a long
301 // time with no flush-to-zero guard. No-op above 1e-15.
302 state[k].s2 = snap_to_zero(c * y - v);
303 v = y;
304 }
305 return v;
306 }
307
308 BranchState in_phase_state_{};
309 BranchState quadrature_state_{};
310 SampleType delayed_ = 0;
311};
312
320
331template <typename SampleType = float>
333public:
336
337 // ── Design parameters (the complete roster; see §11 of the spec) ───────
338
342 static constexpr double kMaxShiftHz = 5000.0;
343
347 static constexpr double kLinZoneHz = 20.0;
348
351 static constexpr double kSplit = 0.5;
352
355 static constexpr double kMaxFeedback = 0.90;
356
362 static constexpr double kMinDelayMs = 0.1;
363
367 static constexpr double kMaxLoopMs = 50.0;
368
374 static constexpr double kGshiftBudget = 1.02;
375
379 static constexpr double kDcCornerHz = 5.0;
380
384 static constexpr double kShiftSmoothingMs = 20.0;
385
390 static constexpr double kGainSmoothingMs = 20.0;
391
396 static constexpr double kDelaySmoothingMs = 50.0;
397
398 // ── Lifecycle ─────────────────────────────────────────────────────────
399
403 void prepare(double sample_rate) {
404 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
405
406 const int max_delay = std::max(
407 2, static_cast<int>(std::ceil(units::ms_to_samples(kMaxLoopMs, sample_rate_))) + 2);
408 for (auto& line : delay_) line.prepare(max_delay);
409 max_delay_samples_ = static_cast<double>(max_delay);
410
411 // H(z) = (1 − z⁻¹)/(1 − p·z⁻¹) has its corner where the pole sits;
412 // `exp(−2π·fc/fs)` places it at `kDcCornerHz` at any sample rate,
413 // which the fixed 0.995 default cannot.
414 const auto pole = static_cast<double>(std::exp(-2.0 * kPi * kDcCornerHz / sample_rate_));
415 for (auto& dc : dc_blocker_) dc.set_pole(pole);
416
417 const auto sr = static_cast<double>(sample_rate_);
418 shift_smoother_.set_ramp_time(kShiftSmoothingMs * 0.001, sr);
419 feedback_smoother_.set_ramp_time(kGainSmoothingMs * 0.001, sr);
420 mix_smoother_.set_ramp_time(kGainSmoothingMs * 0.001, sr);
421 spread_smoother_.set_ramp_time(kGainSmoothingMs * 0.001, sr);
422 delay_smoother_.set_ramp_time(kDelaySmoothingMs * 0.001, sr);
423
424 update_delay_target();
425 reset();
426 }
427
431 void reset() {
432 for (auto& n : network_) n.reset();
433 for (auto& dc : dc_blocker_) dc.reset();
434 for (auto& line : delay_) line.reset();
435 carrier_.reset(0.0);
436 shift_smoother_.set_immediate(shift_hz_);
437 feedback_smoother_.set_immediate(feedback_);
438 mix_smoother_.set_immediate(mix_);
439 spread_smoother_.set_immediate(spread_);
440 delay_smoother_.set_immediate(delay_target_samples_);
441 }
442
445 void discard_history() noexcept {
446 for (auto& n : network_) n.reset();
447 for (auto& dc : dc_blocker_) dc.reset();
448 for (auto& line : delay_) line.discard_history();
449 carrier_.reset(0.0);
450 shift_smoother_.set_immediate(shift_hz_);
451 feedback_smoother_.set_immediate(feedback_);
452 mix_smoother_.set_immediate(mix_);
453 spread_smoother_.set_immediate(spread_);
454 delay_smoother_.set_immediate(delay_target_samples_);
455 }
456
457 // ── Parameters (real units throughout, series law 3) ──────────────────
458
469 void set_shift_hz(double hz) {
470 if (!std::isfinite(hz)) return;
471 shift_hz_ = std::clamp(hz, -kMaxShiftHz, kMaxShiftHz);
472 shift_smoother_.set_target(shift_hz_);
473 }
474
475 double shift_hz() const { return shift_hz_; }
476
478 void set_feedback(double g) {
479 if (!std::isfinite(g)) return;
480 feedback_ = std::clamp(g, 0.0, kMaxFeedback);
481 feedback_smoother_.set_target(feedback_);
482 }
483
484 double feedback() const { return feedback_; }
485
489 void set_feedback_delay_ms(double ms) {
490 if (!std::isfinite(ms)) return;
491 feedback_delay_ms_ = std::clamp(ms, kMinDelayMs, kMaxLoopMs);
492 update_delay_target();
493 delay_smoother_.set_target(delay_target_samples_);
494 }
495
496 double feedback_delay_ms() const { return feedback_delay_ms_; }
497
498 void set_mode(Mode mode) { mode_ = mode; }
499 Mode mode() const { return mode_; }
500
502 void set_mix(double wet01) {
503 if (!std::isfinite(wet01)) return;
504 mix_ = std::clamp(wet01, 0.0, 1.0);
505 mix_smoother_.set_target(mix_);
506 }
507
522 void set_stereo_spread(double spread01) {
523 if (!std::isfinite(spread01)) return;
524 spread_ = std::clamp(spread01, 0.0, 1.0);
525 spread_smoother_.set_target(spread_);
526 }
527
528 // ── Processing ────────────────────────────────────────────────────────
529
533 SampleType process(SampleType x) {
534 if (!std::isfinite(static_cast<double>(x))) {
536 return SampleType{0};
537 }
538 const Controls c = advance_controls();
539 const double dry = static_cast<double>(x);
540 const double wet = shift_channel(0, dry, c, /*left=*/true);
541 return static_cast<SampleType>(snap_to_zero(dry + c.mix * (wet - dry)));
542 }
543
547 void process_stereo(SampleType& left, SampleType& right) {
548 if (!std::isfinite(static_cast<double>(left)) ||
549 !std::isfinite(static_cast<double>(right))) {
551 left = right = SampleType{0};
552 return;
553 }
554 const Controls c = advance_controls();
555 const double dry_l = static_cast<double>(left);
556 const double dry_r = static_cast<double>(right);
557 const double wet_l = shift_channel(0, dry_l, c, /*left=*/true);
558 const double wet_r = shift_channel(1, dry_r, c, /*left=*/false);
559 left = static_cast<SampleType>(snap_to_zero(dry_l + c.mix * (wet_l - dry_l)));
560 right = static_cast<SampleType>(snap_to_zero(dry_r + c.mix * (wet_r - dry_r)));
561 }
562
571 int latency_samples() const noexcept { return 0; }
572
573 // ── Registry and taper helpers (pure functions of shipped constants) ───
574
585 static constexpr double worst_case_gain() {
586 return 1.0 / (1.0 - kMaxFeedback * kGshiftBudget);
587 }
588
594 static double shift_hz_from_knob(double knob) {
595 const double clamped = std::clamp(knob, -1.0, 1.0);
596 const double m = std::abs(clamped);
597 const double magnitude =
598 m < kSplit ? (m / kSplit) * kLinZoneHz
600 return clamped < 0.0 ? -magnitude : magnitude;
601 }
602
604 static double knob_from_shift_hz(double hz) {
605 const double clamped = std::clamp(hz, -kMaxShiftHz, kMaxShiftHz);
606 const double m = std::abs(clamped);
607 const double position =
608 m < kLinZoneHz
609 ? (m / kLinZoneHz) * kSplit
611 return clamped < 0.0 ? -position : position;
612 }
613
617 static double feedback_delay_ms_from_knob(double u) {
618 return units::taper_log(std::clamp(u, 0.0, 1.0), kMinDelayMs, kMaxLoopMs);
619 }
620
621private:
622 static constexpr double kPi = 3.14159265358979323846;
623 static constexpr double kTwoPi = 6.283185307179586476925286766559;
624
627 struct Controls {
628 double cosine = 1.0;
629 double sine = 0.0;
630 double feedback = 0.0;
631 double delay_samples = 0.0;
632 double mix = 1.0;
633 double spread = 1.0;
634 };
635
636 Controls advance_controls() {
637 Controls c;
638 c.feedback = feedback_smoother_.next();
639 c.delay_samples = delay_smoother_.next();
640 c.mix = mix_smoother_.next();
641 c.spread = spread_smoother_.next();
642
643 // In `stereo_split` the spread scales the carrier itself, so the two
644 // legs land at `f ± spread·Δf`. Both legs read the SAME accumulator:
645 // the right channel's carrier is the left's conjugate, which is the
646 // same cosine with the sine negated, so one phase serves both.
647 const double shift = shift_smoother_.next() *
648 (mode_ == Mode::stereo_split ? c.spread : 1.0);
649
650 // Advance FIRST, then evaluate both functions at the advanced phase —
651 // `LfoT::next_quadrature`'s order, and the reason an impulse produces a
652 // non-zero sample 0: at `n = 0` the carrier is already off zero, so the
653 // quadrature term contributes immediately.
654 carrier_.advance(shift / sample_rate_);
655 const double radians = kTwoPi * carrier_.phase();
656 c.cosine = std::cos(radians);
657 c.sine = std::sin(radians);
658 return c;
659 }
660
663 double shift_channel(std::size_t channel, double input, const Controls& c, bool left) {
664 // Read BEFORE the write, so the shortest realisable round trip is one
665 // sample and the loop is never algebraic. `read()` looks back
666 // `delay + 1` samples from the write cursor, so asking for
667 // `delay_samples − 1` makes the total round trip exactly
668 // `delay_samples` — with a floor of one sample.
669 const double read_position = std::clamp(c.delay_samples - 1.0, 0.0, max_delay_samples_);
670 const double recirculated = delay_[channel].read(read_position);
671
672 const double driven = dc_blocker_[channel].process(input + c.feedback * recirculated);
673 const auto pair = network_[channel].process(driven);
674
675 // `up` keeps `f + Δf`, `down` keeps `f − Δf`. In `stereo_split` the
676 // left leg is the up combine and the right the down one — exactly the
677 // up/down pair run in parallel, over a carrier the spread has already
678 // scaled.
679 double quadrature_gain = 0.0;
680 switch (mode_) {
681 case Mode::up:
682 case Mode::dual_mono:
683 quadrature_gain = -1.0;
684 break;
685 case Mode::down:
686 quadrature_gain = 1.0;
687 break;
688 case Mode::stereo_split:
689 quadrature_gain = left ? -1.0 : 1.0;
690 break;
691 }
692
693 const double wet = snap_to_zero(pair.in_phase * c.cosine +
694 quadrature_gain * pair.quadrature * c.sine);
695 delay_[channel].push(wet);
696 return wet;
697 }
698
699 void update_delay_target() {
700 const double samples = units::ms_to_samples(feedback_delay_ms_, sample_rate_);
701 delay_target_samples_ = std::clamp(samples, 1.0, max_delay_samples_);
702 }
703
704 double sample_rate_ = 44100.0;
705 double max_delay_samples_ = 1.0;
706
707 double shift_hz_ = 0.0;
708 double feedback_ = 0.0;
709 double feedback_delay_ms_ = 8.0;
710 double mix_ = 1.0;
711 double spread_ = 1.0;
712 double delay_target_samples_ = 1.0;
713 Mode mode_ = Mode::up;
714
715 std::array<Network, 2> network_{};
716 std::array<DcBlocker<double>, 2> dc_blocker_{};
717 std::array<DelayLineT<double>, 2> delay_{};
718
719 osc::PhaseAccumulator carrier_{};
720
721 SmoothedValue<double> shift_smoother_{};
722 SmoothedValue<double> feedback_smoother_{};
723 SmoothedValue<double> mix_smoother_{1.0};
724 SmoothedValue<double> spread_smoother_{1.0};
725 SmoothedValue<double> delay_smoother_{1.0};
726};
727
731
732} // namespace pulp::signal
A pair of allpass cascades whose outputs stand a quarter cycle apart across nearly the whole band — t...
Definition frequency_shifter_ssb.hpp:210
static constexpr std::size_t kSections
Definition frequency_shifter_ssb.hpp:217
static constexpr std::array< double, kSections > kQuadraturePoles
Definition frequency_shifter_ssb.hpp:229
static constexpr double band_low_hz(double sample_rate)
Definition frequency_shifter_ssb.hpp:271
void reset()
Definition frequency_shifter_ssb.hpp:255
static constexpr double kImageRejectDb
Definition frequency_shifter_ssb.hpp:246
static constexpr double kBandEdgeNormalized
Definition frequency_shifter_ssb.hpp:240
static constexpr std::array< double, kSections > kInPhasePoles
Definition frequency_shifter_ssb.hpp:224
Outputs process(SampleType x)
Definition frequency_shifter_ssb.hpp:261
static constexpr double band_high_hz(double sample_rate)
Definition frequency_shifter_ssb.hpp:276
T next()
Definition smoothed_value.hpp:79
The single-sideband frequency shifter.
Definition frequency_shifter_ssb.hpp:332
void set_mix(double wet01)
Dry/wet, 0 .. 1.
Definition frequency_shifter_ssb.hpp:502
double feedback_delay_ms() const
Definition frequency_shifter_ssb.hpp:496
static constexpr double kLinZoneHz
Definition frequency_shifter_ssb.hpp:347
double feedback() const
Definition frequency_shifter_ssb.hpp:484
static double shift_hz_from_knob(double knob)
The signed piecewise taper: a linear fine zone out to kLinZoneHz, geometric beyond it to kMaxShiftHz,...
Definition frequency_shifter_ssb.hpp:594
static constexpr double kShiftSmoothingMs
Definition frequency_shifter_ssb.hpp:384
int latency_samples() const noexcept
Zero, and reported exactly rather than estimated (series law 5).
Definition frequency_shifter_ssb.hpp:571
static constexpr double kMaxFeedback
Definition frequency_shifter_ssb.hpp:355
static constexpr double kMaxShiftHz
Definition frequency_shifter_ssb.hpp:342
static constexpr double kMinDelayMs
Definition frequency_shifter_ssb.hpp:362
double shift_hz() const
Definition frequency_shifter_ssb.hpp:475
void set_feedback(double g)
Barberpole feedback depth, in the range [0, kMaxFeedback].
Definition frequency_shifter_ssb.hpp:478
static double knob_from_shift_hz(double hz)
Inverse of shift_hz_from_knob.
Definition frequency_shifter_ssb.hpp:604
SampleType process(SampleType x)
One mono sample.
Definition frequency_shifter_ssb.hpp:533
void prepare(double sample_rate)
Sizes the two feedback delay lines for kMaxLoopMs at this rate and recomputes the DC-blocker pole and...
Definition frequency_shifter_ssb.hpp:403
static double feedback_delay_ms_from_knob(double u)
The loop-delay taper.
Definition frequency_shifter_ssb.hpp:617
void process_stereo(SampleType &left, SampleType &right)
One stereo frame.
Definition frequency_shifter_ssb.hpp:547
static constexpr double kDcCornerHz
Definition frequency_shifter_ssb.hpp:379
void set_mode(Mode mode)
Definition frequency_shifter_ssb.hpp:498
static constexpr double kGshiftBudget
Definition frequency_shifter_ssb.hpp:374
static constexpr double kMaxLoopMs
Definition frequency_shifter_ssb.hpp:367
static constexpr double kSplit
Definition frequency_shifter_ssb.hpp:351
void set_stereo_spread(double spread01)
stereo_split only: scales how far apart the two channels are driven.
Definition frequency_shifter_ssb.hpp:522
static constexpr double kDelaySmoothingMs
Definition frequency_shifter_ssb.hpp:396
static constexpr double worst_case_gain()
The bound Forge's registry cites (series law 8): the steady-state envelope of a comb-like loop at the...
Definition frequency_shifter_ssb.hpp:585
void set_feedback_delay_ms(double ms)
Feedback-loop delay in ms, in the range [kMinDelayMs, kMaxLoopMs].
Definition frequency_shifter_ssb.hpp:489
void set_shift_hz(double hz)
Signed shift in Hz: positive shifts the retained sideband up, negative down.
Definition frequency_shifter_ssb.hpp:469
void discard_history() noexcept
Audio-thread fault recovery: identical running state to reset() but logically invalidates the delay h...
Definition frequency_shifter_ssb.hpp:445
void reset()
Clears every filter, line, accumulator and smoother, and snaps each smoother onto its target so a ren...
Definition frequency_shifter_ssb.hpp:431
static constexpr double kGainSmoothingMs
Definition frequency_shifter_ssb.hpp:390
HilbertQuadratureNetworkT< double > Network
Definition frequency_shifter_ssb.hpp:335
FrequencyShiftMode Mode
Definition frequency_shifter_ssb.hpp:334
Mode mode() const
Definition frequency_shifter_ssb.hpp:499
double phase() const
Current phase, always in [0, 1).
Definition phase.hpp:176
int advance(double increment)
Advance one sample by increment cycles. Returns the number of events.
Definition phase.hpp:142
T taper_log(T u, T lo, T hi)
Geometric ("log") taper: maps a normalised knob position u ∈ [0, 1] onto [lo, hi] so equal knob trave...
Definition units.hpp:233
T untaper_log(T value, T lo, T hi)
Inverse of taper_log: a value in [lo, hi] → its knob position in [0, 1].
Definition units.hpp:241
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
Definition additive_bank.hpp:218
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ c
Definition spectrum_trace.hpp:35
FrequencyShiftMode
Which sideband a SsbFrequencyShifterT keeps, and how it spreads it.
Definition frequency_shifter_ssb.hpp:314
@ stereo_split
Up on the left, down on the right, scaled by the spread.
Definition frequency_shifter_ssb.hpp:318
@ dual_mono
Two independent identical mono shifters. See the note below.
Definition frequency_shifter_ssb.hpp:317
@ up
Upper sideband on both channels: f → f + Δf.
Definition frequency_shifter_ssb.hpp:315
@ down
Lower sideband on both channels: f → f − Δf.
Definition frequency_shifter_ssb.hpp:316
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
HilbertQuadratureNetworkT< double > HilbertQuadratureNetwork
Definition frequency_shifter_ssb.hpp:728
SsbFrequencyShifterT< double > SsbFrequencyShifter64
Definition frequency_shifter_ssb.hpp:730
SsbFrequencyShifterT< float > SsbFrequencyShifter
Definition frequency_shifter_ssb.hpp:729
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
Definition frequency_shifter_ssb.hpp:248
SampleType in_phase
the reference branch, no bulk delay
Definition frequency_shifter_ssb.hpp:249
SampleType quadrature
a quarter cycle behind it
Definition frequency_shifter_ssb.hpp:250
The real-unit vocabulary shared by every DSP block in the catalog.