Authored sequence in the signal domain: blocks that remember a pattern, walk it under a clock, and hand back pitch CV and gates. More...
#include <pulp/signal/transport_edge.hpp>#include <pulp/signal/stage_sequencer.hpp>#include <pulp/signal/cartesian_walk.hpp>#include <pulp/signal/rungler.hpp>#include <pulp/signal/scale_quantizer.hpp>#include <pulp/signal/gate_logic.hpp>#include <pulp/signal/probability_gate.hpp>Go to the source code of this file.
Authored sequence in the signal domain: blocks that remember a pattern, walk it under a clock, and hand back pitch CV and gates.
trigger_kit.hpp ships the event plumbing — what an edge is, how a gate is made, how a clock is divided and multiplied. What it does not ship is a pattern. This header is that layer, and it composes the kit rather than re-deriving it: every edge in here is a TriggerDetectT edge, every slide is a SlewLimiterT in constant-time mode, every ratchet is an upstream ClockMultT, and every random draw is a seeded Xorshift32.
Three sequencer topologies, chosen because none of them is a linear step sequencer and none of them can be spelled by a MIDI arpeggiator:
Plus the two support blocks every patch built from those three needs: QuantizeScaleT (snap any CV to a scale on the 1 V/oct standard) and GateLogicT / ProbGateT (Boolean and probabilistic gate combinators).
── Pitch is volts, everywhere ────────────────────────────────────────────
Pitch is carried as volts on the 1 V/octave standard (12 semitones per volt, units::volts_to_semitones), not as note numbers, so any block's pitch output feeds any other's pitch input, the quantizer, a slew limiter, or a filter cutoff with no unit negotiation. That interchangeability is the whole reason these are signal blocks rather than note generators.
── The two reset verbs (the ambiguity this header closes) ────────────────
A "reset" input on one hardware module zeroes an LFO's phase, on another zeroes a counter, on another jumps a pattern to step 0, and a user can never predict which. There are exactly two verbs here and no third:
RunglerT is the single documented exception to the "continuous output holds" half of verb 2: its reset edge restores the seed pattern and the DAC output immediately, because re-pinning a wandering rungler line live is the entire point of giving it a reset jack, and its output is a stepped hold that steps by design (§ RunglerT) rather than a pitch CV that would click.
── Run / stop / reset transport ──────────────────────────────────────────
The three sequencers take process(run_high, reset_edge, clock_edge, …) and obey identical rules:
TransportEdgeT is the only place reset-edge detection lives, so every sequencer agrees on what an edge is.
── Oversampling policy ───────────────────────────────────────────────────
None, deliberately. Every output here is a stepped hold — a DAC level, a quantized semitone, a gate — and the steps are the sound, not an artefact of undersampling a continuous nonlinearity. There is no gain-carrying nonlinearity in this header to alias, so band-limiting would only remove the intended edge. A caller who wants the steps softened puts a SlewLimiterT downstream, which is a musical choice and not ours to make.
── Determinism ───────────────────────────────────────────────────────────
Series law 2: every draw is a seeded Xorshift32 rewound by reset(); seeds are compile-time/config values and never automatable parameters. Every block is per-sample and carries no block-length state, so a render is bit-identical across buffer sizes as well as across resets.
RT contract: prepare() recomputes sample counts and allocates nothing. set_*, process(), apply_reset_edge() and reset() allocate nothing, take no locks, and perform no I/O. All state is POD and fixed-capacity; a default-constructed instance is already in the state reset() would put it in. Every block reports latency_samples() == 0: a slide is a continuous smoother, not a delay.