Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
va.hpp
Go to the documentation of this file.
1#pragma once
2
101
102#include "blep.hpp"
103#include "phase.hpp"
104
105#include <cmath>
106#include <cstddef>
107#include <numbers>
108#include <span>
109
110namespace pulp::signal::osc {
111
119
145inline int threshold_crossings(double phase_before_advance, double increment,
146 double threshold, std::span<double> out) noexcept {
147 if (!std::isfinite(increment) || increment == 0.0) return 0;
148
149 // Shift so the threshold sits on an integer, then the crossings are the
150 // integers strictly between the endpoints.
151 const double q0 = phase_before_advance - threshold;
152 const double first = std::floor(q0);
153 const double last = std::floor(q0 + increment);
154 if (!std::isfinite(first) || !std::isfinite(last)) return 0;
155
156 const bool forward = increment > 0.0;
157 const double magnitude = forward ? (last - first) : (first - last);
158 if (!(magnitude >= 1.0)) return 0;
159
160 const int capacity = static_cast<int>(out.size());
161 const int count = magnitude > static_cast<double>(capacity)
162 ? capacity
163 : static_cast<int>(magnitude);
164
165 for (int k = 0; k < count; ++k) {
166 const double level =
167 forward ? first + 1.0 + static_cast<double>(k) : first - static_cast<double>(k);
168 out[static_cast<std::size_t>(k)] = (level - q0) / increment;
169 }
170 return count;
171}
172
175public:
176 void set_shape(VaShape shape) noexcept { shape_ = shape; }
177 VaShape shape() const noexcept { return shape_; }
178
188 void set_pulse_width(double width) noexcept { pulse_width_ = clamp_unit(width); }
189 double pulse_width() const noexcept { return pulse_width_; }
190
192 void reset(double phase = 0.0) noexcept {
193 phase_.reset(phase);
194 carry_ = 0.0;
195 }
196
198 double phase() const noexcept { return phase_.phase(); }
199
208 double next(double increment) noexcept {
209 const double entry_phase = phase_.phase();
210 double out = value(entry_phase) + carry_;
211 carry_ = 0.0;
212
213 phase_.advance(increment);
214 correct_events(increment, out);
215 // No sync, so the phase ran the whole sample at one rate from one place:
216 // the sample is a single segment.
217 scan_threshold(entry_phase, increment, increment, 0.0, 1.0, out);
218 return out;
219 }
220
233 double next_synced(double increment, double sync_frac, double sync_phase = 0.0) noexcept {
234 const double entry_phase = phase_.phase();
235 double out = value(entry_phase) + carry_;
236 carry_ = 0.0;
237
238 phase_.advance_synced(increment, sync_frac, sync_phase);
239 correct_events(increment, out);
240
241 // A sync SPLITS the sample in two. The phase runs from `entry_phase` over
242 // sub-sample span [0, f], is then replaced outright, and runs from the
243 // target over [f, 1]. An internal threshold must therefore be scanned
244 // once per segment, from that segment's own starting phase: a single scan
245 // of (entry_phase, increment) describes a trajectory the phase never
246 // took, and would place the apex where it never was while missing the one
247 // it actually crossed after the reset.
248 //
249 // This mirrors `advance_synced`'s own segmentation — the same split, the
250 // same sub-sample spans — because that split IS the semantics, not an
251 // implementation detail to approximate.
252 const double f = clamp_unit(sync_frac);
253 scan_threshold(entry_phase, increment * f, increment, 0.0, f, out);
254 scan_threshold(wrap_unit(sync_phase), increment * (1.0 - f), increment, f, 1.0 - f, out);
255 return out;
256 }
257
258private:
268 void correct_events(double increment, double& out) noexcept {
269 for (const PhaseEvent& e : phase_.events()) {
270 const double value_step = value(e.phase_after) - value(e.phase_before);
271 const double slope_step =
272 (slope_per_cycle(e.phase_after) - slope_per_cycle(e.phase_before)) * increment;
273 add_break(e.frac, value_step, slope_step, out);
274 }
275 }
276
286 void scan_threshold(double start_phase, double delta, double rate, double t0, double span,
287 double& out) noexcept {
288 Threshold t;
289 if (!internal_threshold(t)) return;
290
292 const int n = threshold_crossings(start_phase, delta, t.phase, std::span<double>(fracs));
293 if (n == 0) return;
294
295 // Time order decides which side is "before": going forward the phase
296 // arrives from below the threshold, going backward from above.
297 const bool forward = delta > 0.0;
298 const double value_step =
299 forward ? (t.value_above - t.value_below) : (t.value_below - t.value_above);
300 const double slope_step =
301 (forward ? (t.slope_above - t.slope_below) : (t.slope_below - t.slope_above)) * rate;
302
303 for (int k = 0; k < n; ++k)
304 add_break(t0 + fracs[k] * span, value_step, slope_step, out);
305 }
306
313 struct Threshold {
314 double phase = 0.0;
315 double value_below = 0.0;
316 double value_above = 0.0;
317 double slope_below = 0.0;
318 double slope_above = 0.0;
319 };
320
321 bool internal_threshold(Threshold& t) const noexcept {
322 switch (shape_) {
323 case VaShape::square:
324 // No edge at all when the pulse fills the period or none of it.
325 if (!(pulse_width_ > 0.0 && pulse_width_ < 1.0)) return false;
326 t = {pulse_width_, 1.0, -1.0, 0.0, 0.0};
327 return true;
329 // The apex: value is continuous, the slope reverses.
330 t = {0.5, 1.0, 1.0, 4.0, -4.0};
331 return true;
332 case VaShape::sine:
333 case VaShape::saw:
334 return false;
335 }
336 return false;
337 }
338
355 double value(double p) const noexcept {
356 switch (shape_) {
357 case VaShape::sine:
358 // sin is periodic, so its limit at the top of the period is
359 // sin(0) = 0 EXACTLY. `std::sin(2*pi)` returns -2.4e-16 instead,
360 // because 2*pi is not exactly representable — and handing that to
361 // the corrector applies a BLEP at every wrap of a shape that does
362 // not step there at all. Mapping the sentinel to its exact limit
363 // keeps a wrap's step exactly zero, which the corrector then
364 // skips outright, leaving a plain sine bit-identical to
365 // `std::sin`. That bit-exactness is the null proving the
366 // corrector is inert on a continuous shape, and it is worth more
367 // than the 2.4e-16 it costs to protect.
368 //
369 // A SYNC on a sine is a different matter and IS corrected: the
370 // reset jumps to an unrelated phase, so the value genuinely
371 // steps. It is the wrap that is continuous here, not the shape.
372 return p >= 1.0 ? 0.0 : std::sin(2.0 * std::numbers::pi * p);
373 case VaShape::saw:
374 // Limit at 1 is +1, which is what `2p - 1` gives at p = 1.
375 return 2.0 * p - 1.0;
376 case VaShape::square:
377 // A width of exactly 1 fills the period: the shape is constantly
378 // high, so its limit at the top is high too. Testing `p < width`
379 // alone would report LOW at the p = 1 sentinel and manufacture a
380 // +2 step at a wrap that has no edge on either side of it.
381 return (pulse_width_ >= 1.0 || p < pulse_width_) ? 1.0 : -1.0;
383 // Limit at 1 is -1, which is what the descending leg gives at 1.
384 return p < 0.5 ? (4.0 * p - 1.0) : (3.0 - 4.0 * p);
385 }
386 return 0.0;
387 }
388
397 double slope_per_cycle(double p) const noexcept {
398 switch (shape_) {
399 case VaShape::sine:
400 return 2.0 * std::numbers::pi * std::cos(2.0 * std::numbers::pi * p);
401 case VaShape::saw:
402 return 2.0;
404 return p < 0.5 ? 4.0 : -4.0;
405 case VaShape::square:
406 return 0.0;
407 }
408 return 0.0;
409 }
410
413 void add_break(double frac, double value_step, double slope_step, double& out) noexcept {
414 if (value_step != 0.0) {
415 const Correction c = poly_blep(frac, value_step);
416 out += c.before;
417 carry_ += c.after;
418 }
419 if (slope_step != 0.0) {
420 const Correction c = poly_blamp(frac, slope_step);
421 out += c.before;
422 carry_ += c.after;
423 }
424 }
425
426 static double clamp_unit(double x) noexcept {
427 if (!(x > 0.0)) return 0.0;
428 if (x > 1.0) return 1.0;
429 return x;
430 }
431
438 static double wrap_unit(double x) noexcept {
439 const double w = x - std::floor(x);
440 return (w >= 0.0 && w < 1.0) ? w : 0.0;
441 }
442
443 PhaseAccumulator phase_;
444 double carry_ = 0.0;
445 double pulse_width_ = 0.5;
446 VaShape shape_ = VaShape::saw;
447};
448
449} // namespace pulp::signal::osc
Bandlimited-step (BLEP) and bandlimited-ramp (BLAMP) correction kernels.
static constexpr int max_events_per_sample
Event capacity of a single advance. See the truncation note above.
Definition phase.hpp:132
std::span< const PhaseEvent > events() const
Events from the last advance, in chronological order.
Definition phase.hpp:179
Virtual-analog oscillator over the static shapes.
Definition va.hpp:174
void set_shape(VaShape shape) noexcept
Definition va.hpp:176
void set_pulse_width(double width) noexcept
Fraction of the period the square spends high, clamped to [0, 1].
Definition va.hpp:188
VaShape shape() const noexcept
Definition va.hpp:177
double next(double increment) noexcept
Generate one sample and advance by increment cycles.
Definition va.hpp:208
double pulse_width() const noexcept
Definition va.hpp:189
void reset(double phase=0.0) noexcept
Set the phase and drop any pending correction.
Definition va.hpp:192
double phase() const noexcept
Current phase, in [0, 1).
Definition va.hpp:198
double next_synced(double increment, double sync_frac, double sync_phase=0.0) noexcept
Generate one sample, advancing by increment but forcing the phase to sync_phase at sub-sample positio...
Definition va.hpp:233
Definition blep.hpp:72
Correction poly_blep(double d, double height) noexcept
polyBLEP correction for a step in the VALUE.
Definition blep.hpp:177
int threshold_crossings(double phase_before_advance, double increment, double threshold, std::span< double > out) noexcept
Sub-sample positions at which the phase crosses threshold (mod 1) during one sample,...
Definition va.hpp:145
Correction poly_blamp(double d, double slope_change) noexcept
polyBLAMP correction for a break in the SLOPE.
Definition blep.hpp:206
VaShape
The static shapes.
Definition va.hpp:113
@ square
Value steps of +2 at the wrap and -2 at the pulse edge.
Definition va.hpp:116
@ sine
Continuous across a wrap; a sync still steps it.
Definition va.hpp:114
@ saw
Value step of -2 at the wrap.
Definition va.hpp:115
@ triangle
Slope breaks at the wrap and the apex; value is continuous.
Definition va.hpp:117
@ first
Definition dither.hpp:23
@ forward
(s + 1) mod N.
Definition stage_sequencer.hpp:18
@ c
Definition spectrum_trace.hpp:35
@ e
Definition formant_filter_bank.hpp:20
@ p
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ t
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
A single phase discontinuity within one sample period.
Definition phase.hpp:28