Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
dco.hpp
Go to the documentation of this file.
1#pragma once
2
86
87#include "phase.hpp"
88#include "va.hpp"
89
90#include <cmath>
91#include <cstddef>
92#include <cstdint>
93#include <span>
94
95namespace pulp::signal::osc {
96
102
129
138public:
139 void prepare(double sample_rate) noexcept {
140 sample_rate_ = sample_rate > 0.0 ? sample_rate : sample_rate_;
141 recompute();
142 }
143
144 void set_profile(const DcoProfile& profile) noexcept {
145 profile_ = profile;
146 recompute();
147 }
148 const DcoProfile& profile() const noexcept { return profile_; }
149
152 void set_note_hz(double note_hz) noexcept {
153 note_hz_ = note_hz;
154 recompute();
155 }
156 double note_hz() const noexcept { return note_hz_; }
157
158 // ── Shared shape stage (delegated; OSC-DCO shapes nothing) ───────────────
159 void set_shape(VaShape shape) noexcept { core_.set_shape(shape); }
160 VaShape shape() const noexcept { return core_.shape(); }
161 void set_pulse_width(double width) noexcept { core_.set_pulse_width(width); }
162 double pulse_width() const noexcept { return core_.pulse_width(); }
163
164 // ── Derived timing facts (the exact equality-test targets) ───────────────
167 long long divider_n() const noexcept {
168 return profile_.divider_scheme == DcoDivider::integer_n ? divider_ : 0;
169 }
170
172 long long tuning_word() const noexcept {
173 return profile_.divider_scheme == DcoDivider::fractional_n
174 ? static_cast<long long>(delta_)
175 : 0;
176 }
177
179 double realized_hz() const noexcept { return realized_hz_; }
182 double effective_note_hz() const noexcept { return effective_note_hz_; }
190 double detune_cents() const noexcept {
191 return (realized_hz_ > 0.0 && effective_note_hz_ > 0.0)
192 ? 1200.0 * std::log2(realized_hz_ / effective_note_hz_)
193 : 0.0;
194 }
195
212 double quantization_bound_cents() const noexcept {
213 if (!(profile_.master_clock_hz > 0.0) || !(effective_note_hz_ > 0.0)) return 0.0;
214 const double ideal_divider =
215 profile_.divider_scheme == DcoDivider::integer_n
216 ? profile_.master_clock_hz / effective_note_hz_
217 : static_cast<double>(two_b_) * effective_note_hz_ / profile_.master_clock_hz;
218 // A note at/above 2·f_clk drives the ideal divider below ½ LSB, where the
219 // divider clamp — not rounding — governs and the ½-LSB envelope no longer
220 // applies. Report no bound there rather than a false one (unreachable for
221 // any audio note below the master clock).
222 if (!(ideal_divider > 0.5)) return 0.0;
223 // Fractional-N also clamps the tuning word at the TOP (`Δ ≤ 2^B − 1`): as
224 // the note approaches the clock the ideal `Δ` exceeds that and the clamp,
225 // not ½-LSB rounding, governs the realized pitch — so the envelope does not
226 // apply there either. Report no bound rather than a false tiny one.
227 if (profile_.divider_scheme == DcoDivider::fractional_n &&
228 !(ideal_divider < static_cast<double>(two_b_) - 0.5))
229 return 0.0;
230 return 1200.0 * std::log2(ideal_divider / (ideal_divider - 0.5));
231 }
232
238 int reset_intervals(std::span<long long> out) const noexcept {
239 const int n = static_cast<int>(out.size());
240 if (profile_.divider_scheme == DcoDivider::integer_n) {
241 for (int i = 0; i < n; ++i) out[static_cast<std::size_t>(i)] = divider_;
242 return n;
243 }
244 std::uint64_t residue = 0;
245 for (int i = 0; i < n; ++i)
246 out[static_cast<std::size_t>(i)] = advance_divider(residue);
247 return n;
248 }
249
252 void reset(double phase = 0.0) noexcept {
253 core_.reset(phase);
254 residue_ = 0;
255 if (profile_.divider_scheme == DcoDivider::fractional_n) {
256 current_interval_ = advance_divider(residue_);
257 clocks_to_reset_ = static_cast<double>(current_interval_);
258 } else {
259 current_interval_ = divider_;
260 clocks_to_reset_ = 0.0;
261 }
262 }
263
264 double phase() const noexcept { return core_.phase(); }
265
267 double next() noexcept {
268 if (profile_.divider_scheme == DcoDivider::integer_n) {
269 // The natural wrap IS the reset; a constant increment keeps the period
270 // exactly N clocks, so integer-N carries no jitter.
271 return core_.next(increment_integer_);
272 }
273
274 // Fractional-N: the reset lands on the master-clock grid. The per-cycle
275 // increment carries the phase to exactly 1.0 at the grid edge, so a single
276 // clean step is emitted there; re-anchoring to 0 each cycle keeps the
277 // ±1-clock jitter exact rather than accumulating slope error.
278 const double increment = clocks_per_sample_ / static_cast<double>(current_interval_);
279 double out;
280 if (clocks_to_reset_ < clocks_per_sample_) {
281 const double frac = clocks_to_reset_ > 0.0 ? clocks_to_reset_ / clocks_per_sample_ : 0.0;
282 out = core_.next_synced(increment, frac, 0.0);
283 const long long next_interval = advance_divider(residue_);
284 clocks_to_reset_ += static_cast<double>(next_interval);
285 current_interval_ = next_interval;
286 } else {
287 out = core_.next(increment);
288 }
289 clocks_to_reset_ -= clocks_per_sample_;
290 return out;
291 }
292
293private:
299 long long advance_divider(std::uint64_t& residue) const noexcept {
300 // Smallest m with residue + m·Δ >= 2^B (ceil division), which alternates
301 // between floor(2^B/Δ) and ceil(2^B/Δ) as the residue walks.
302 const std::uint64_t m = (two_b_ - residue + delta_ - 1) / delta_;
303 residue = residue + m * delta_ - two_b_;
304 return static_cast<long long>(m);
305 }
306
307 void recompute() noexcept {
308 clocks_per_sample_ = sample_rate_ > 0.0 ? profile_.master_clock_hz / sample_rate_ : 0.0;
309
310 const int bits = profile_.accumulator_bits < 1 ? 1
311 : profile_.accumulator_bits > 32 ? 32
312 : profile_.accumulator_bits;
313 two_b_ = std::uint64_t{1} << bits;
314
315 effective_note_hz_ =
316 note_hz_ * std::exp2(static_cast<double>(profile_.octave_shift) +
317 profile_.fine_tune_cents / 1200.0);
318
319 const double f_clk = profile_.master_clock_hz;
320 if (profile_.divider_scheme == DcoDivider::integer_n) {
321 const double ideal = effective_note_hz_ > 0.0 ? f_clk / effective_note_hz_ : 1.0;
322 divider_ = static_cast<long long>(std::llround(ideal));
323 if (divider_ < 1) divider_ = 1;
324 realized_hz_ = divider_ > 0 ? f_clk / static_cast<double>(divider_) : 0.0;
325 increment_integer_ = sample_rate_ > 0.0 ? realized_hz_ / sample_rate_ : 0.0;
326 delta_ = 1;
327 } else {
328 const double ideal =
329 f_clk > 0.0 ? effective_note_hz_ * static_cast<double>(two_b_) / f_clk : 1.0;
330 long long d = static_cast<long long>(std::llround(ideal));
331 if (d < 1) d = 1;
332 if (static_cast<std::uint64_t>(d) >= two_b_) d = static_cast<long long>(two_b_ - 1);
333 delta_ = static_cast<std::uint64_t>(d);
334 realized_hz_ = f_clk * static_cast<double>(delta_) / static_cast<double>(two_b_);
335 divider_ = 0;
336 increment_integer_ = 0.0;
337 }
338
339 reset(phase());
340 }
341
342 VaOscillator core_;
343
344 DcoProfile profile_{};
345 double sample_rate_ = 48000.0;
346 double note_hz_ = 261.625565; // C4.
347
348 // Derived.
349 double clocks_per_sample_ = 0.0;
350 double effective_note_hz_ = 261.625565;
351 double realized_hz_ = 0.0;
352 long long divider_ = 1; // integer-N.
353 std::uint64_t delta_ = 1; // fractional-N tuning word Δ.
354 std::uint64_t two_b_ = 1u << 24;
355 double increment_integer_ = 0.0;
356
357 // Fractional-N running schedule.
358 std::uint64_t residue_ = 0;
359 long long current_interval_ = 1;
360 double clocks_to_reset_ = 0.0;
361};
362
363} // namespace pulp::signal::osc
A divider-clocked oscillator front-end composing the shared VaOscillator.
Definition dco.hpp:137
void reset(double phase=0.0) noexcept
Reset the phase and the divider schedule to a known start.
Definition dco.hpp:252
VaShape shape() const noexcept
Definition dco.hpp:160
double note_hz() const noexcept
Definition dco.hpp:156
void prepare(double sample_rate) noexcept
Definition dco.hpp:139
double phase() const noexcept
Definition dco.hpp:264
double effective_note_hz() const noexcept
The effective note after footage and fine-tune — the target the divider approximates.
Definition dco.hpp:182
void set_shape(VaShape shape) noexcept
Definition dco.hpp:159
double detune_cents() const noexcept
Pitch-quantization error in cents: 1200 · log2(realized / f_note_eff).
Definition dco.hpp:190
const DcoProfile & profile() const noexcept
Definition dco.hpp:148
long long tuning_word() const noexcept
Fractional tuning word Δ = round(f_note_eff · 2^B / f_clk).
Definition dco.hpp:172
void set_pulse_width(double width) noexcept
Definition dco.hpp:161
void set_profile(const DcoProfile &profile) noexcept
Definition dco.hpp:144
double pulse_width() const noexcept
Definition dco.hpp:162
long long divider_n() const noexcept
Integer divider N = round(f_clk / f_note_eff).
Definition dco.hpp:167
double next() noexcept
Generate one sample and advance the divider by one output sample.
Definition dco.hpp:267
int reset_intervals(std::span< long long > out) const noexcept
Fill out with the next divider reset intervals, in master clocks, from a fresh divider state — a pure...
Definition dco.hpp:238
double quantization_bound_cents() const noexcept
The worst-case quantization bound in cents for the active scheme — an EXACT upper bound on |detune_ce...
Definition dco.hpp:212
double realized_hz() const noexcept
The realised frequency the divider actually produces: f_clk / N for integer-N, f_clk · Δ / 2^B (the a...
Definition dco.hpp:179
void set_note_hz(double note_hz) noexcept
Command the pitch.
Definition dco.hpp:152
Definition blep.hpp:72
DcoDivider
The divider scheme a profile selects.
Definition dco.hpp:98
@ integer_n
Fixed integer divider N; perfectly periodic, quantized pitch.
Definition dco.hpp:99
@ fractional_n
B-bit fractional accumulator; accurate average pitch, ±1-clock jitter.
Definition dco.hpp:100
VaShape
The static shapes.
Definition va.hpp:113
@ i
Definition formant_filter_bank.hpp:20
OSC-DCO profile — data-driven timing parameters, no waveform or hardware capture.
Definition dco.hpp:111
DcoDivider divider_scheme
Selects the integer-N (perfectly periodic) or fractional-N (accurate average, jittered) path.
Definition dco.hpp:117
double master_clock_hz
f_clk in Hz.
Definition dco.hpp:114
int octave_shift
Footage / octave selection, in octaves relative to the commanded note.
Definition dco.hpp:125
double fine_tune_cents
Global detune folded into the note before N / Δ derivation, in cents.
Definition dco.hpp:127
int accumulator_bits
B — accumulator width, fractional-N only.
Definition dco.hpp:120
Virtual-analog oscillator: the classic static shapes, bandlimited by composing PhaseAccumulator (the ...