Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
stage_sequencer.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pulp/signal/detail/modular_sequencing_common.hpp>
4#include <pulp/signal/rng.hpp>
7
8#include <algorithm>
9#include <cmath>
10#include <cstdint>
11
12namespace pulp::signal {
13
14// ── Stage sequencer ───────────────────────────────────────────────────────
15
23
31
65template <typename SampleType = float>
66class StageSeqT {
67public:
70 static constexpr int kMaxStages = 16;
71 static constexpr int kDefaultStages = 8;
72
74 static constexpr int kMinPulseCount = 1;
75 static constexpr int kMaxPulseCount = 8;
76
79 static constexpr double kRepeatDuty = 0.5;
80
84 static constexpr double kSlideMs = 30.0;
85
89 static constexpr std::uint32_t kRandomSeed = 0x2A3Bu;
90
97 static constexpr int kRandomDrawAttempts = 64;
98
103 static constexpr double kMaxPeriodMs = 4000.0;
104
106 struct Frame {
107 SampleType pitch_v = SampleType{0};
108 bool gate = false;
109 };
110
112 slew_.set_mode(SlewMode::linear);
113 slew_.set_time_ms(0.0);
114 for (int i = 0; i < kMaxStages; ++i) pulse_count_[i] = kMinPulseCount;
115 }
116
117 void prepare(double sample_rate) {
118 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
119 max_period_samples_ = units::ms_to_samples(kMaxPeriodMs, sample_rate_);
120 slew_.prepare(sample_rate_);
121 }
122
123 // ── Pattern configuration (survives `reset()`) ─────────────────────────
124
125 void set_num_stages(int n) {
126 num_stages_ = std::clamp(n, 1, kMaxStages);
127 if (stage_ >= num_stages_) stage_ = 0;
128 }
129 int num_stages() const { return num_stages_; }
130
131 void set_direction(SeqDirection d) { direction_ = d; }
132 SeqDirection direction() const { return direction_; }
133
134 void set_stage_pitch(int s, SampleType volts) {
135 if (in_range(s) && std::isfinite(static_cast<double>(volts))) pitch_v_[s] = volts;
136 }
137 SampleType stage_pitch(int s) const { return in_range(s) ? pitch_v_[s] : SampleType{0}; }
138
139 void set_stage_pulse_count(int s, int count) {
140 if (in_range(s)) pulse_count_[s] = std::clamp(count, kMinPulseCount, kMaxPulseCount);
141 }
142 int stage_pulse_count(int s) const { return in_range(s) ? pulse_count_[s] : kMinPulseCount; }
143
145 if (in_range(s)) gate_mode_[s] = m;
146 }
148 return in_range(s) ? gate_mode_[s] : StageGateMode::hold;
149 }
150
151 void set_stage_slide(int s, bool on) {
152 if (in_range(s)) slide_[s] = on;
153 }
154 bool stage_slide(int s) const { return in_range(s) && slide_[s]; }
155
156 void set_stage_skip(int s, bool on) {
157 if (in_range(s)) skip_[s] = on;
158 }
159 bool stage_skip(int s) const { return in_range(s) && skip_[s]; }
160
165 static constexpr double kMaxSlideMs = 60000.0;
166
167 void set_slide_ms(double ms) { slide_ms_ = seq_detail::clamp_finite(ms, 0.0, kMaxSlideMs); }
168 double slide_ms() const { return slide_ms_; }
169
170 void set_repeat_duty(double duty) { repeat_duty_ = seq_detail::clamp_finite(duty, 0.0, 1.0); }
171 double repeat_duty() const { return repeat_duty_; }
172
173 void set_seed(std::uint32_t seed) { rng_.set_seed(seed); }
174
175 // ── Transport ──────────────────────────────────────────────────────────
176
186 stage_ = first_playable_from(0);
187 pulse_ = 0;
188 ascending_ = true;
189 started_ = false;
190 gate_ = false;
191 }
192
196 void reset() {
198 slew_.reset();
199 target_pitch_ = SampleType{0};
200 since_clock_ = 0;
201 period_ = 0.0;
202 have_clock_ = false;
203 rng_.reset();
204 }
205
206 static constexpr int latency_samples() { return 0; }
207
208 bool gate() const { return gate_; }
209 SampleType pitch_v() const { return slew_.value(); }
210 int stage() const { return stage_; }
212 int pulse() const { return pulse_; }
214 bool started() const { return started_; }
215
217 Frame process(bool run_high, bool reset_edge, bool clock_edge) {
218 if (reset_edge) apply_reset_edge();
219
220 if (!run_high) {
221 gate_ = false;
222 return Frame{slew_.value(), false};
223 }
224
225 // Counted only while running, so a stop does not inflate the measured
226 // period and hand `repeat` mode a duty window longer than the clock it
227 // is supposed to divide. The period this tracks is musical time, not
228 // wall time.
229 ++since_clock_;
230
231 if (clock_edge) {
232 if (have_clock_) {
233 const double measured = static_cast<double>(since_clock_);
234 period_ = (measured > 0.0 && measured <= max_period_samples_) ? measured : 0.0;
235 }
236 have_clock_ = true;
237 since_clock_ = 0;
238 advance();
239 }
240
241 gate_ = compute_gate();
242 const SampleType pitch = slew_.process(target_pitch_);
243 return Frame{pitch, gate_};
244 }
245
246private:
247 static bool in_range(int s) { return s >= 0 && s < kMaxStages; }
248
251 int first_playable_from(int from) const {
252 const int n = num_stages_;
253 for (int i = 0; i < n; ++i) {
254 const int s = (from + i) % n;
255 if (!skip_[s]) return s;
256 }
257 return from % n;
258 }
259
260 bool has_playable() const {
261 for (int i = 0; i < num_stages_; ++i)
262 if (!skip_[i]) return true;
263 return false;
264 }
265
269 void enter_stage() {
270 slew_.set_time_ms(slide_[stage_] ? slide_ms_ : 0.0);
271 target_pitch_ = pitch_v_[stage_];
272 }
273
274 void advance() {
275 // A pattern in which every stage is skipped has nowhere to go. Advancing
276 // would either spin forever looking for a playable stage or land on one
277 // the user asked to skip; the guard makes it a no-op with the gate low.
278 if (!has_playable()) return;
279
280 if (!started_) {
281 // Re-validate the landing before entering it. `apply_reset_edge()`
282 // already places the playhead on a playable stage, but the skip
283 // flags can change between the reset and the clock — and a
284 // freshly-constructed sequencer has never been through a reset at
285 // all. Without this, a pattern whose stage 0 is skipped would play
286 // stage 0 exactly once, on its downbeat.
287 started_ = true;
288 stage_ = first_playable_from(stage_);
289 pulse_ = 0;
290 enter_stage();
291 return;
292 }
293
294 ++pulse_;
295 if (pulse_ < pulse_count_[stage_]) return;
296
297 pulse_ = 0;
298 step_stage();
299 enter_stage();
300 }
301
302 void step_stage() {
303 const int n = num_stages_;
304 if (n <= 1) return;
305
306 switch (direction_) {
308 for (int i = 0; i < n; ++i) {
309 stage_ = (stage_ + 1) % n;
310 if (!skip_[stage_]) return;
311 }
312 return;
314 for (int i = 0; i < n; ++i) {
315 stage_ = (stage_ - 1 + n) % n;
316 if (!skip_[stage_]) return;
317 }
318 return;
320 // At most 2n − 2 positions in the cycle, so 2n reflections
321 // visit every one of them and the loop is bounded.
322 for (int i = 0; i < 2 * n; ++i) {
323 pingpong_step();
324 if (!skip_[stage_]) return;
325 }
326 return;
328 random_step();
329 return;
330 }
331 }
332
333 void pingpong_step() {
334 const int n = num_stages_;
335 if (ascending_) {
336 if (stage_ >= n - 1) {
337 ascending_ = false;
338 stage_ = n - 2;
339 } else {
340 ++stage_;
341 }
342 } else {
343 if (stage_ <= 0) {
344 ascending_ = true;
345 stage_ = 1;
346 } else {
347 --stage_;
348 }
349 }
350 }
351
352 void random_step() {
353 const int n = num_stages_;
354 // `next_uint() % n` carries the usual modulo bias for an `n` that does
355 // not divide 2^32. At n ≤ 16 the bias is below one part in 2^28 — far
356 // under any audible or measurable asymmetry — and the alternative
357 // (rejection-sampling the draw itself as well as the skip) would make
358 // the stream position depend on the pattern length, which is worse for
359 // reproducibility than a bias nobody can measure.
360 for (int attempt = 0; attempt < kRandomDrawAttempts; ++attempt) {
361 const int candidate =
362 static_cast<int>(rng_.next_uint() % static_cast<std::uint32_t>(n));
363 if (!skip_[candidate]) {
364 stage_ = candidate;
365 return;
366 }
367 }
368 stage_ = first_playable_from((stage_ + 1) % n);
369 }
370
371 bool compute_gate() const {
372 if (!started_) return false;
373 switch (gate_mode_[stage_]) {
375 return true;
377 return pulse_ == 0;
379 return false;
381 // The duty needs a period, and a multiplier cannot know the
382 // length of a clock pulse that has not finished yet — the same
383 // constraint `ClockMultT` documents. Until one period has been
384 // measured the gate stays high for the whole pulse, which is the
385 // only defensible answer: any fixed fallback length would be a
386 // tempo this block has no business inventing.
387 if (period_ <= 0.0) return true;
388 return static_cast<double>(since_clock_) < repeat_duty_ * period_;
389 }
390 return false;
391 }
392
393 // Configuration.
394 double sample_rate_ = 44100.0;
395 double max_period_samples_ = 44100.0 * 4.0;
396 int num_stages_ = kDefaultStages;
398 double slide_ms_ = kSlideMs;
399 double repeat_duty_ = kRepeatDuty;
400 SampleType pitch_v_[kMaxStages]{};
401 int pulse_count_[kMaxStages]{};
402 StageGateMode gate_mode_[kMaxStages]{};
403 bool slide_[kMaxStages]{};
404 bool skip_[kMaxStages]{};
405
406 // Running state.
407 int stage_ = 0;
408 int pulse_ = 0;
409 bool ascending_ = true;
410 bool started_ = false;
411 bool gate_ = false;
412 std::int64_t since_clock_ = 0;
413 double period_ = 0.0;
414 bool have_clock_ = false;
415 SampleType target_pitch_ = SampleType{0};
416 ConstantTimeSlewLimiterT<SampleType> slew_{};
417 Xorshift32 rng_{kRandomSeed};
418};
419
422
423} // namespace pulp::signal
The pulse-count stage sequencer.
Definition stage_sequencer.hpp:66
static constexpr int latency_samples()
Definition stage_sequencer.hpp:206
Frame process(bool run_high, bool reset_edge, bool clock_edge)
Advances one sample. See the header's order-of-operations rule.
Definition stage_sequencer.hpp:217
static constexpr int kMaxPulseCount
Definition stage_sequencer.hpp:75
void set_stage_pulse_count(int s, int count)
Definition stage_sequencer.hpp:139
bool gate() const
Definition stage_sequencer.hpp:208
static constexpr double kSlideMs
Definition stage_sequencer.hpp:84
static constexpr double kMaxSlideMs
Definition stage_sequencer.hpp:165
static constexpr double kMaxPeriodMs
Definition stage_sequencer.hpp:103
SampleType pitch_v() const
Definition stage_sequencer.hpp:209
SampleType stage_pitch(int s) const
Definition stage_sequencer.hpp:137
static constexpr int kMaxStages
Definition stage_sequencer.hpp:70
int stage_pulse_count(int s) const
Definition stage_sequencer.hpp:142
bool stage_skip(int s) const
Definition stage_sequencer.hpp:159
static constexpr int kRandomDrawAttempts
Definition stage_sequencer.hpp:97
StageGateMode stage_gate_mode(int s) const
Definition stage_sequencer.hpp:147
void set_stage_slide(int s, bool on)
Definition stage_sequencer.hpp:151
static constexpr int kDefaultStages
Definition stage_sequencer.hpp:71
double repeat_duty() const
Definition stage_sequencer.hpp:171
void set_stage_gate_mode(int s, StageGateMode m)
Definition stage_sequencer.hpp:144
void reset()
The lifecycle verb (verb 1): everything apply_reset_edge() does, plus held outputs to zero,...
Definition stage_sequencer.hpp:196
void set_slide_ms(double ms)
Definition stage_sequencer.hpp:167
static constexpr double kRepeatDuty
Definition stage_sequencer.hpp:79
bool stage_slide(int s) const
Definition stage_sequencer.hpp:154
void prepare(double sample_rate)
Definition stage_sequencer.hpp:117
StageSeqT()
Definition stage_sequencer.hpp:111
SeqDirection direction() const
Definition stage_sequencer.hpp:132
void set_direction(SeqDirection d)
Definition stage_sequencer.hpp:131
double slide_ms() const
Definition stage_sequencer.hpp:168
bool started() const
True once the first clock after a reset has landed.
Definition stage_sequencer.hpp:214
void set_num_stages(int n)
Definition stage_sequencer.hpp:125
void apply_reset_edge()
The modular reset jack (verb 2): position to the top of the pattern, gate low, pitch CV held,...
Definition stage_sequencer.hpp:185
static constexpr int kMinPulseCount
Definition stage_sequencer.hpp:74
static constexpr std::uint32_t kRandomSeed
Definition stage_sequencer.hpp:89
void set_seed(std::uint32_t seed)
Definition stage_sequencer.hpp:173
void set_repeat_duty(double duty)
Definition stage_sequencer.hpp:170
int num_stages() const
Definition stage_sequencer.hpp:129
void set_stage_pitch(int s, SampleType volts)
Definition stage_sequencer.hpp:134
void set_stage_skip(int s, bool on)
Definition stage_sequencer.hpp:156
int stage() const
Definition stage_sequencer.hpp:210
int pulse() const
0-based pulse index inside the current stage.
Definition stage_sequencer.hpp:212
T ms_to_samples(T ms, T sample_rate)
Milliseconds → samples at sample_rate Hz.
Definition units.hpp:147
Definition additive_bank.hpp:218
SeqDirection
How a StageSeqT walks from one stage to the next.
Definition stage_sequencer.hpp:17
@ reverse
(s − 1 + N) mod N.
Definition stage_sequencer.hpp:19
@ random
Seeded draw, rejection-sampled past skipped stages.
Definition stage_sequencer.hpp:21
@ forward
(s + 1) mod N.
Definition stage_sequencer.hpp:18
@ pingpong
Reflects at both ends without repeating them; period 2N − 2.
Definition stage_sequencer.hpp:20
@ linear
Definition mod_tools.hpp:106
StageSeqT< float > StageSeq
Definition stage_sequencer.hpp:420
@ hold
Definition envelope.hpp:59
StageGateMode
What the gate does across the clock pulses a stage occupies.
Definition stage_sequencer.hpp:25
@ repeat
A separate gate per pulse, at repeat_duty.
Definition stage_sequencer.hpp:27
@ rest
Low for the whole stage — silence with duration.
Definition stage_sequencer.hpp:29
@ hold
High for the whole stage.
Definition stage_sequencer.hpp:26
@ single
High on the first pulse only.
Definition stage_sequencer.hpp:28
StageSeqT< double > StageSeq64
Definition stage_sequencer.hpp:421
@ i
Definition formant_filter_bank.hpp:20
@ reverse
Rise to a peak, then cut.
Definition nonlin_ambience_design.hpp:246
@ n
Definition input_events.hpp:40
@ candidate
Definition waveform_editor_primitives.hpp:193
Deterministic randomness for modulation: a counter-free PRNG, a Gaussian draw, a stateless purpose-ke...
Compatibility include for the canonical modulation-toolkit slew limiter and sample-and-hold implement...
One sample of sequencer output.
Definition stage_sequencer.hpp:106
bool gate
Definition stage_sequencer.hpp:108
SampleType pitch_v
Definition stage_sequencer.hpp:107
The real-unit vocabulary shared by every DSP block in the catalog.