Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
phase.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <array>
4#include <cmath>
5#include <cstddef>
6#include <span>
7
8namespace pulp::signal::osc {
9
16
28struct PhaseEvent {
31 double frac = 0.0;
32 double phase_before = 0.0;
33 double phase_after = 0.0;
35};
36
130public:
132 static constexpr int max_events_per_sample = 8;
133
135 void reset(double phase = 0.0) {
136 phase_ = wrap_unit(phase);
137 count_ = 0;
138 truncated_ = false;
139 }
140
142 int advance(double increment) {
143 count_ = 0;
144 truncated_ = false;
145 scan(increment, 0.0, 1.0);
146 return count_;
147 }
148
160 int advance_synced(double increment, double sync_frac, double sync_phase = 0.0) {
161 count_ = 0;
162 truncated_ = false;
163
164 const double f = clamp_unit(sync_frac);
165 scan(increment * f, 0.0, f);
166
167 const double target = wrap_unit(sync_phase);
168 emit(f, phase_, target, PhaseEventKind::sync);
169 phase_ = target;
170
171 scan(increment * (1.0 - f), f, 1.0 - f);
172 return count_;
173 }
174
176 double phase() const { return phase_; }
177
179 std::span<const PhaseEvent> events() const {
180 return {events_.data(), static_cast<std::size_t>(count_)};
181 }
182
184 bool truncated() const { return truncated_; }
185
186private:
195 void scan(double delta, double t0, double span) {
196 // Absorb a non-finite increment here, where the phase is owned, so both
197 // advance() and advance_synced() get the same contract. Letting NaN fall
198 // through to the comparisons below works — every one fails, so no events
199 // are emitted — but an infinity does NOT: floor(inf) is inf, so the
200 // magnitude test below takes the truncation path and emits a full budget
201 // of wraps at a position no crossing occurred at. A caller that trusts
202 // the event list then corrects steps that never happened, and (0 * inf)
203 // in its own slope term hands it a NaN besides.
204 if (!std::isfinite(delta)) {
205 phase_ = 0.0;
206 return;
207 }
208
209 const double p0 = phase_;
210 const double raw = p0 + delta;
211 const double n = std::floor(raw);
212
213 // The `>= 1.0` case is not defensive: for a raw of -1e-20,
214 // floor is -1 and `raw - n` rounds to exactly 1.0, which is outside the
215 // half-open domain. Snapping to 0 is exact on the circle.
216 double wrapped = raw - n;
217 if (!(wrapped >= 0.0 && wrapped < 1.0))
218 wrapped = 0.0;
219
220 int events = 0;
221 const double magnitude = std::fabs(n);
222 if (magnitude > static_cast<double>(max_events_per_sample)) {
224 truncated_ = true;
225 } else if (magnitude >= 1.0) {
226 events = static_cast<int>(magnitude);
227 }
228
229 // p0 is in [0, 1), so raw > 0 whenever delta > 0 and raw < 1 whenever
230 // delta < 0. The sign of n therefore names the direction outright, and
231 // `delta` is non-zero whenever the loop runs.
232 const bool forward = n > 0.0;
233 for (int k = 0; k < events; ++k) {
234 const double level = forward ? static_cast<double>(k + 1)
235 : -static_cast<double>(k);
236 const double t = (level - p0) / delta;
237 emit(t0 + t * span,
238 forward ? 1.0 : 0.0,
239 forward ? 0.0 : 1.0,
242 }
243
244 phase_ = wrapped;
245 }
246
247 void emit(double frac, double before, double after, PhaseEventKind kind) {
248 if (count_ >= max_events_per_sample) {
249 truncated_ = true;
250 return;
251 }
252 events_[static_cast<std::size_t>(count_++)] = PhaseEvent{frac, before, after, kind};
253 }
254
256 static double wrap_unit(double x) {
257 double w = x - std::floor(x);
258 if (!(w >= 0.0 && w < 1.0))
259 w = 0.0;
260 return w;
261 }
262
264 static double clamp_unit(double x) {
265 if (!(x > 0.0)) return 0.0;
266 if (x > 1.0) return 1.0;
267 return x;
268 }
269
270 double phase_ = 0.0;
271 std::array<PhaseEvent, max_events_per_sample> events_{};
272 int count_ = 0;
273 bool truncated_ = false;
274};
275
276} // namespace pulp::signal::osc
Phase accumulator over the unit circle [0, 1), suitable as the clock for through-zero FM,...
Definition phase.hpp:129
int advance_synced(double increment, double sync_frac, double sync_phase=0.0)
Advance one sample by increment cycles, forcing the phase to sync_phase at sub-sample position sync_f...
Definition phase.hpp:160
double phase() const
Current phase, always in [0, 1).
Definition phase.hpp:176
static constexpr int max_events_per_sample
Event capacity of a single advance. See the truncation note above.
Definition phase.hpp:132
int advance(double increment)
Advance one sample by increment cycles. Returns the number of events.
Definition phase.hpp:142
bool truncated() const
Whether the last advance dropped events past max_events_per_sample.
Definition phase.hpp:184
std::span< const PhaseEvent > events() const
Events from the last advance, in chronological order.
Definition phase.hpp:179
void reset(double phase=0.0)
Set the phase, wrapped into [0, 1). Clears any reported events.
Definition phase.hpp:135
Definition child_process_manager.hpp:18
Definition blep.hpp:72
PhaseEventKind
What produced a phase discontinuity.
Definition phase.hpp:11
@ wrap_backward
Phase left 0 downward and wrapped to 1 (increment < 0).
Definition phase.hpp:13
@ wrap_forward
Phase reached 1 and wrapped to 0 (increment > 0).
Definition phase.hpp:12
@ sync
Phase was forced by advance_synced().
Definition phase.hpp:14
@ forward
(s + 1) mod N.
Definition stage_sequencer.hpp:18
@ x
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
A single phase discontinuity within one sample period.
Definition phase.hpp:28
PhaseEventKind kind
Definition phase.hpp:34
double phase_after
Definition phase.hpp:33
double phase_before
Definition phase.hpp:32
double frac
Sub-sample position of the discontinuity, relative to the start of the sample being advanced.
Definition phase.hpp:31