Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
clip_launch.hpp
Go to the documentation of this file.
1#pragma once
2
8
9#include <cstdint>
10#include <limits>
11#include <optional>
12
13// Real-time clip-launch engine: it resolves an armed, quantized launch to a
14// sample-accurate offset within the current audio block and drives a per-slot
15// launch state machine. All resolution happens on the transport's MONOTONIC
16// clock (see pulp/timebase/tick.hpp), which never wraps, so "launch at the next
17// bar" stays well-defined even when the rendered block straddles a loop wrap.
18// Every function here is allocation-free, lock-free, and total; process() is
19// audio-callback safe.
20namespace pulp::playback {
21
22using timeline::LaunchQuantize;
23
24// The first monotonic boundary at or after `from` for the given quantization.
25// Immediate quantization returns `from` unchanged. The boundary set is
26// { phase + k*grid : k in Z }; this returns the boundary for the smallest k with
27// phase + k*grid >= from (ceil). Overflow saturates into the int64 tick domain,
28// per operation: the product rails first, then the sum. A wide intermediate
29// would instead clamp the exact value once, which differs only when a railed
30// product is pulled back into range by phase — a regime that needs ~1.3e13
31// quarter notes to reach, so both readings agree everywhere reachable. Stated
32// because it is a behavioural choice, not an accident of the arithmetic.
34 LaunchQuantize quantize) noexcept {
35 const std::int64_t grid = quantize.grid.value;
36 if (grid <= 0)
37 return from; // immediate: no quantization
38 const std::int64_t phase = quantize.phase.value;
39 const std::int64_t rel = (from.position - quantize.phase).value; // saturating subtract
40 // Ceil division toward +infinity for a positive grid.
41 std::int64_t k = rel / grid;
42 if (rel % grid != 0 && rel > 0)
43 ++k;
46}
47
48// Resolves the block-relative sample offset at which the monotonic clock reaches
49// `target`, scanning the transport ranges (which may split at a loop wrap).
50//
51// - target lands inside a range [monotonic_start, monotonic_end): returns the
52// exact offset. The mapping monotonic->timeline within a range is affine
53// (equal tick deltas by construction), and timeline->sample uses the exact
54// tempo map, so the result is sample-accurate even in the post-wrap range.
55// - target is already behind the block's first range: returns 0 (fire now,
56// late) — e.g. a seek jumped the clock past the boundary.
57// - target is still ahead of every range: returns nullopt (stay armed).
58//
59// Range monotonic spans are contiguous across a wrap (range[i].monotonic_start ==
60// range[i-1].monotonic_end), so a boundary exactly on a wrap belongs to the later
61// range under the half-open convention — the wrap-safe answer.
62inline std::optional<std::uint32_t>
64 timebase::MonotonicBeat target) noexcept {
65 const std::int64_t target_pos = target.position.value;
66 for (std::uint8_t index = 0; index < snapshot.range_count; ++index) {
67 const auto& range = snapshot.ranges[index];
68 const std::int64_t start = range.monotonic_start.position.value;
69 const std::int64_t end = range.monotonic_end.position.value;
70 if (target_pos < start)
71 return range.sample_offset; // boundary already behind us: fire now
72 if (target_pos < end) {
73 const timebase::TickDuration delta{target.position.value - start};
74 const timebase::TickPosition target_tick = range.timeline_tick_start + delta;
75 const timebase::SamplePosition target_sample = tempo_map.ticks_to_samples(target_tick);
76 const std::int64_t offset_in_range =
77 target_sample.value - range.timeline_sample_start.value;
78 const std::int64_t block_offset =
79 static_cast<std::int64_t>(range.sample_offset) + offset_in_range;
80 // Tick->sample rounding can push a boundary in the final range's last
81 // tick onto the block's end sample (== the next block's first sample).
82 // Defer it so the offset stays in [0, frame_count); the next block's
83 // range starts at/after this target and fires it at offset 0.
84 if (block_offset >= static_cast<std::int64_t>(snapshot.frame_count))
85 return std::nullopt;
86 return static_cast<std::uint32_t>(block_offset);
87 }
88 }
89 return std::nullopt; // boundary is beyond this block: stay armed
90}
91
92enum class LaunchState : std::uint8_t {
93 Stopped, // idle, not sounding
94 Armed, // triggered, waiting for the quantized launch boundary
95 Playing, // sounding
96 Stopping, // sounding, waiting for the quantized stop boundary
97};
98
99enum class LaunchEventKind : std::uint8_t { None, Start, Stop };
100
101// The outcome of processing one block. When `kind` is Start/Stop the transition
102// fires at `sample_offset` within the block (0 <= offset < frame_count).
107
108// Per-slot launch state machine. Control-thread callers arm()/stop(); the audio
109// thread calls process() once per block. The launch target is resolved lazily on
110// the first block after a trigger, anchored to that block's starting monotonic
111// position, so the resolution is deterministic given the block sequence.
113 public:
116
117 constexpr LaunchState state() const noexcept {
118 return state_;
119 }
120 constexpr bool has_target() const noexcept {
121 return has_target_;
122 }
123 constexpr timebase::MonotonicBeat target() const noexcept {
124 return target_;
125 }
126
127 // Arm a (re)launch quantized by `quantize`. Legal from any state — an armed
128 // slot re-arms, a playing slot retriggers at the next boundary. The target
129 // resolves on the next process() block.
130 constexpr void arm(LaunchQuantize quantize) noexcept {
131 quantize_ = quantize;
132 state_ = LaunchState::Armed;
133 has_target_ = false;
134 }
135
136 // Arm a stop quantized by `quantize`. Only meaningful while sounding; a no-op
137 // when already stopped/armed-from-stop.
138 constexpr void stop(LaunchQuantize quantize) noexcept {
139 if (state_ == LaunchState::Playing || state_ == LaunchState::Stopping) {
140 quantize_ = quantize;
141 state_ = LaunchState::Stopping;
142 has_target_ = false;
143 }
144 }
145
146 // Cancel any pending arm/stop and hard-stop immediately (no boundary).
147 constexpr void reset() noexcept {
148 state_ = LaunchState::Stopped;
149 has_target_ = false;
150 has_last_start_ = false;
151 }
152
153 // Advance one block. Emits at most one Start/Stop transition per block.
155 const timebase::CompiledTempoMap& tempo_map) noexcept {
156 if (state_ != LaunchState::Armed && state_ != LaunchState::Stopping)
157 return {};
158 if (snapshot.range_count == 0)
159 return {};
160
161 if (!has_target_) {
162 target_ = next_launch_boundary(snapshot.ranges[0].monotonic_start, quantize_);
163 has_target_ = true;
164 }
165
166 const auto offset = resolve_launch_sample(snapshot, tempo_map, target_);
167 if (!offset)
168 return {}; // boundary still ahead: stay armed
169
170 const bool starting = state_ == LaunchState::Armed;
171 state_ = starting ? LaunchState::Playing : LaunchState::Stopped;
172 has_target_ = false;
173 if (starting) {
174 last_start_ = target_;
175 has_last_start_ = true;
176 }
177 return {starting ? LaunchEventKind::Start : LaunchEventKind::Stop, *offset};
178 }
179
180 // The monotonic beat the most recent Start resolved to — the exact tick the
181 // clip began on, not an approximation recovered from a sample offset. A
182 // follow-action grid anchors here so it inherits the launch's accuracy.
183 constexpr bool has_last_start() const noexcept {
184 return has_last_start_;
185 }
186 constexpr timebase::MonotonicBeat last_start() const noexcept {
187 return last_start_;
188 }
189
190 private:
191 LaunchQuantize quantize_{};
192 timebase::MonotonicBeat target_{};
193 timebase::MonotonicBeat last_start_{};
195 bool has_target_ = false;
196 bool has_last_start_ = false;
197};
198
199// Counts off a slot's follow-action grid from the beat its launch resolved to.
200// The grid is anchored to that beat (phase == launch beat), so the boundaries
201// are exactly { launch + k*grid } and each one resolves through the same
202// resolve_launch_sample() path a launch does — a follow action is therefore
203// sample-accurate under the same block splits and loop wraps a launch is.
204// `repetitions` grid periods elapse before the action fires, so a two-bar grid
205// with three repetitions fires six bars after the clip started.
206//
207// The timer copies the grid and repetition count out of the set it is armed
208// with and holds no pointer into it, so the authored model may change under a
209// running timer without a dangling read on the audio thread.
211 bool fired = false;
212 std::uint32_t sample_offset = 0;
213};
214
216 public:
219
220 // Start counting from `launch_beat`. A set that cannot fire (no candidates,
221 // an immediate grid, zero repetitions) leaves the timer disarmed.
222 constexpr void arm(timebase::MonotonicBeat launch_beat,
223 const timeline::FollowActionSet& set) noexcept {
224 if (!set.enabled()) {
225 disarm();
226 return;
227 }
228 grid_ = set.grid;
229 remaining_ = set.repetitions;
230 target_ = advance(launch_beat);
231 armed_ = true;
232 }
233
234 constexpr void disarm() noexcept {
235 armed_ = false;
236 remaining_ = 0;
237 }
238
239 constexpr bool armed() const noexcept {
240 return armed_;
241 }
242 constexpr timebase::MonotonicBeat target() const noexcept {
243 return target_;
244 }
245 // How many times this timer has fired. Used as the follow-action draw index,
246 // which is what makes a repeated random choice reproducible.
247 constexpr std::uint64_t fire_count() const noexcept {
248 return fire_count_;
249 }
250
251 // Advance one block. Emits at most one fire per block; intermediate
252 // repetition boundaries falling in the same block are consumed silently.
254 const timebase::CompiledTempoMap& tempo_map) noexcept {
255 if (!armed_ || snapshot.range_count == 0)
256 return {};
257 // Bounded by remaining_: every iteration consumes one repetition.
258 while (true) {
259 const auto offset = resolve_launch_sample(snapshot, tempo_map, target_);
260 if (!offset)
261 return {}; // boundary still ahead: keep counting
262 --remaining_;
263 if (remaining_ == 0) {
264 armed_ = false;
265 ++fire_count_;
266 return {true, *offset};
267 }
268 target_ = advance(target_);
269 }
270 }
271
272 private:
273 // The next boundary strictly after `from` on a grid anchored at `from`.
274 constexpr timebase::MonotonicBeat advance(timebase::MonotonicBeat from) const noexcept {
275 const LaunchQuantize anchored{grid_, from.position};
276 return next_launch_boundary({from.position + timebase::TickDuration{1}}, anchored);
277 }
278
279 timebase::TickDuration grid_{0};
280 timebase::MonotonicBeat target_{};
281 std::uint32_t remaining_ = 0;
282 std::uint64_t fire_count_ = 0;
283 bool armed_ = false;
284};
285
286} // namespace pulp::playback
Definition clip_launch.hpp:215
static constexpr audio::RtSafetyClass process_rt_safety_class
Definition clip_launch.hpp:217
FollowActionEvent process(const TransportSnapshot &snapshot, const timebase::CompiledTempoMap &tempo_map) noexcept
Definition clip_launch.hpp:253
constexpr std::uint64_t fire_count() const noexcept
Definition clip_launch.hpp:247
constexpr void disarm() noexcept
Definition clip_launch.hpp:234
constexpr bool armed() const noexcept
Definition clip_launch.hpp:239
constexpr timebase::MonotonicBeat target() const noexcept
Definition clip_launch.hpp:242
constexpr void arm(timebase::MonotonicBeat launch_beat, const timeline::FollowActionSet &set) noexcept
Definition clip_launch.hpp:222
Definition clip_launch.hpp:112
LaunchEvent process(const TransportSnapshot &snapshot, const timebase::CompiledTempoMap &tempo_map) noexcept
Definition clip_launch.hpp:154
constexpr timebase::MonotonicBeat last_start() const noexcept
Definition clip_launch.hpp:186
constexpr void arm(LaunchQuantize quantize) noexcept
Definition clip_launch.hpp:130
constexpr LaunchState state() const noexcept
Definition clip_launch.hpp:117
constexpr timebase::MonotonicBeat target() const noexcept
Definition clip_launch.hpp:123
static constexpr audio::RtSafetyClass process_rt_safety_class
Definition clip_launch.hpp:114
constexpr void stop(LaunchQuantize quantize) noexcept
Definition clip_launch.hpp:138
constexpr void reset() noexcept
Definition clip_launch.hpp:147
constexpr bool has_target() const noexcept
Definition clip_launch.hpp:120
constexpr bool has_last_start() const noexcept
Definition clip_launch.hpp:183
Definition compiled_tempo_map.hpp:91
RtSafetyClass
Definition rt_safety_contract.hpp:11
@ AudioCallbackSafeAfterPrepare
Definition rt_safety_contract.hpp:13
Definition audio_renderer.hpp:20
@ Stop
Definition capture_engine.hpp:69
@ Start
Definition capture_engine.hpp:68
@ Stopped
Definition capture_engine.hpp:84
constexpr timebase::MonotonicBeat next_launch_boundary(timebase::MonotonicBeat from, LaunchQuantize quantize) noexcept
Definition clip_launch.hpp:33
@ None
Definition automation_recording.hpp:29
LaunchEventKind
Definition clip_launch.hpp:99
@ Stop
Definition clip_launch.hpp:99
@ None
Definition clip_launch.hpp:99
@ Start
Definition clip_launch.hpp:99
LaunchState
Definition clip_launch.hpp:92
@ Stopping
Definition clip_launch.hpp:96
@ Stopped
Definition clip_launch.hpp:93
@ Playing
Definition clip_launch.hpp:95
@ Armed
Definition clip_launch.hpp:94
std::optional< std::uint32_t > resolve_launch_sample(const TransportSnapshot &snapshot, const timebase::CompiledTempoMap &tempo_map, timebase::MonotonicBeat target) noexcept
Definition clip_launch.hpp:63
constexpr std::int64_t saturating_multiply(std::int64_t lhs, std::int64_t rhs) noexcept
Definition tick.hpp:27
constexpr std::int64_t saturating_add(std::int64_t lhs, std::int64_t rhs) noexcept
Definition tick.hpp:11
Definition clip_launch.hpp:210
bool fired
Definition clip_launch.hpp:211
std::uint32_t sample_offset
Definition clip_launch.hpp:212
Definition clip_launch.hpp:103
LaunchEventKind kind
Definition clip_launch.hpp:104
std::uint32_t sample_offset
Definition clip_launch.hpp:105
Definition transport.hpp:71
Definition tick.hpp:71
Definition rational_time.hpp:9
std::int64_t value
Definition rational_time.hpp:10
Definition tick.hpp:56
Definition tick.hpp:61
Bounded weighted follow behavior attached to a slot.
Definition clip_launch.hpp:105
Musical-boundary quantization for one launch.
Definition clip_launch.hpp:40
Immutable authored scene, slot, quantization, and follow-action model.