Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
shimmer.hpp
Go to the documentation of this file.
1#pragma once
2
3// Granular octave-up shifter, one per tank line, injected into the FEEDBACK
4// path.
5//
6// Injecting into the feedback rather than the output is what makes shimmer
7// bloom: each pass shifts what the previous pass already shifted, so the octave
8// stack builds instead of sitting as a single fixed layer above the tail. It is
9// also exactly why the injection weight must be energy-normalized and counted
10// in the stability bound — an unnormalized recirculating shifter is a runaway.
11//
12// The shift ratio is fixed at 2x. One good interval, no ratio parameter: a
13// variable ratio buys artifacts at every non-octave setting and buys nothing
14// musical here.
15//
16// Four Hann grains staggered by a quarter period sum to a constant envelope
17// (75% overlap), so the output has no amplitude ripple. Grain SIZE varies per
18// channel across a 40-60 ms span, which is the cheap trick that makes sixteen
19// mediocre shifters sound like one good one: their grain-boundary artifacts
20// land at sixteen different periods instead of stacking into one buzz.
21//
22// The grains' READ offsets are deliberately NOT the same quarter-period stagger
23// as their window phases. With both equal, the four taps are coherent copies of
24// the source at fixed relative phases, and at the frequencies where those four
25// phasors sum to zero the shifter outputs nothing at all — a measured total
26// null, not a dip. Decoupling the read offset (pseudo-random, fixed per
27// channel) from the window phase (which must stay at a quarter period for the
28// windows to sum flat) breaks the structure that makes those nulls line up.
29//
30// RT contract: prepare() allocates; process/reset allocate nothing.
31
37
38#include <algorithm>
39#include <array>
40#include <cmath>
41#include <cstdint>
42
43namespace pulp::signal::fdn {
44
45template <typename SampleType = float>
47public:
48 // Sized for the longest grain at the highest tank rate; configure() only
49 // re-derives grain lengths.
50 void prepare(double max_sample_rate) {
51 const int cap =
52 static_cast<int>(kShimmerGrainMaxMs * 0.001 * max_sample_rate) + kHermiteGuard;
53 for (auto& line : lines_) line.prepare(cap * 2);
54 }
55
56 void configure(double tank_rate) {
57 for (int i = 0; i < kNumChannels; ++i) {
58 const double t = (kNumChannels > 1)
59 ? static_cast<double>(i) /
60 static_cast<double>(kNumChannels - 1)
61 : 0.0;
62 const double ms =
64 grain_[i] = std::max(16.0, ms * 0.001 * tank_rate);
65 XorShift32 rng(kOffsetSeed + static_cast<std::uint32_t>(i) * kOffsetStride);
66 for (int g = 0; g < kNumShimmerGrains; ++g)
67 read_offset_[static_cast<std::size_t>(i)][static_cast<std::size_t>(g)] =
68 rng.next_unit() * grain_[i];
69 }
70 // One-pole lowpass after the shift. The shifter's artifacts live in the
71 // top octave it creates; rolling them off there keeps the octave stack
72 // from turning into hiss after a dozen passes.
73 const double w = 6.283185307179586 * kShimmerToneHz / tank_rate;
74 tone_coeff_ = std::exp(-w);
75 reset_phases();
76 }
77
78 void reset() {
79 for (auto& line : lines_) line.reset();
80 for (auto& z : tone_state_) z = 0.0;
81 reset_phases();
82 }
83
84 // Push one sample per channel and read back its octave-up image.
85 SampleType process(int channel, SampleType x) {
86 FracDelayT<SampleType>& line = lines_[static_cast<std::size_t>(channel)];
87 line.push(x);
88
89 const double period = grain_[static_cast<std::size_t>(channel)];
90 if (period < 16.0) return SampleType{0}; // not configured yet
91 double age = phase_[static_cast<std::size_t>(channel)];
92 double sum = 0.0;
93 for (int g = 0; g < kNumShimmerGrains; ++g) {
94 // Grains are staggered by a quarter period; each one's read delay
95 // falls from `period` to 0 over its life, which advances the read
96 // pointer at 2x while the write pointer advances at 1x.
97 double a = age + static_cast<double>(g) * period / kNumShimmerGrains;
98 if (a >= period) a -= period;
99 const double delay =
100 period - a +
101 read_offset_[static_cast<std::size_t>(channel)][static_cast<std::size_t>(g)];
102 const double window =
103 0.5 - 0.5 * std::cos(6.283185307179586 * a / period);
104 sum += window * static_cast<double>(line.read(delay + 2.0));
105 }
106 // Four Hann grains at 75% overlap sum to 2.0.
107 sum *= 0.5;
108
109 age += 1.0;
110 if (age >= period) age -= period;
111 phase_[static_cast<std::size_t>(channel)] = age;
112
113 double& z = tone_state_[static_cast<std::size_t>(channel)];
114 z = snap_to_zero(sum * (1.0 - tone_coeff_) + z * tone_coeff_);
115 z = finite_or_zero(z);
116 return static_cast<SampleType>(z);
117 }
118
119 // The injection weight that keeps worst-case loop energy bounded: the
120 // per-line contribution is divided across the active lines so sixteen
121 // shifters inject the same total energy one would.
122 static double injection_weight(double shimmer, int active_n) {
123 const double n = static_cast<double>(std::max(active_n, 1));
124 return std::clamp(shimmer, 0.0, 1.0) * kShimmerMaxWeight / std::sqrt(n);
125 }
126
127private:
128 void reset_phases() {
129 for (int i = 0; i < kNumChannels; ++i) {
130 // Stagger the channels' grain clocks as well as the grains within a
131 // channel, so no two lines ever cross a grain boundary together.
132 phase_[static_cast<std::size_t>(i)] =
133 grain_[static_cast<std::size_t>(i)] * static_cast<double>(i) /
134 static_cast<double>(kNumChannels);
135 }
136 }
137
138 static constexpr std::uint32_t kOffsetSeed = 0xA5A5F00Du;
139 static constexpr std::uint32_t kOffsetStride = 0x27D4EB2Fu;
140
141 std::array<FracDelayT<SampleType>, kNumChannels> lines_{};
142 std::array<std::array<double, kNumShimmerGrains>, kNumChannels> read_offset_{};
143 std::array<double, kNumChannels> grain_{};
144 std::array<double, kNumChannels> phase_{};
145 std::array<double, kNumChannels> tone_state_{};
146 double tone_coeff_ = 0.0;
147};
148
149} // namespace pulp::signal::fdn
Definition frac_delay.hpp:36
Definition shimmer.hpp:46
SampleType process(int channel, SampleType x)
Definition shimmer.hpp:85
void configure(double tank_rate)
Definition shimmer.hpp:56
static double injection_weight(double shimmer, int active_n)
Definition shimmer.hpp:122
void prepare(double max_sample_rate)
Definition shimmer.hpp:50
void reset()
Definition shimmer.hpp:78
Definition modulation.hpp:23
double next_unit()
Definition modulation.hpp:40
Definition config.hpp:15
T finite_or_zero(T value)
Definition sanitize.hpp:12
constexpr int kNumChannels
Definition config.hpp:18
@ shimmer
Definition config.hpp:222
@ tank_rate
Definition config.hpp:226
constexpr int kNumShimmerGrains
Definition config.hpp:21
constexpr double kShimmerGrainMaxMs
Definition config.hpp:135
constexpr double kShimmerMaxWeight
Definition config.hpp:138
constexpr double kShimmerGrainMinMs
Definition config.hpp:134
constexpr int kHermiteGuard
Definition config.hpp:171
constexpr double kShimmerToneHz
Definition config.hpp:136
@ delay
Definition envelope.hpp:57
T snap_to_zero(T x)
Returns 0 if |x| < snap_threshold, else x.
Definition denormal.hpp:45
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20