DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Oscillator.h
Go to the documentation of this file.
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
26#include "DspMath.h"
27#include "AudioSpec.h"
28#include "AudioBuffer.h"
29#include "MinBlepTable.h"
30
31#include <cmath>
32#include <algorithm>
33#include <array>
34#include <cassert>
35#include <cstddef>
36#include <type_traits>
37
38namespace dspark {
39
62template <typename T>
64{
65 static_assert(std::is_floating_point_v<T>, "Oscillator requires float or double");
66
67public:
68 enum class Waveform { Sine, Saw, Square, Triangle };
69
79 void prepare(double sampleRate) noexcept
80 {
81 assert(sampleRate > 0.0);
82 sampleRate_ = sampleRate;
83 // Touch the shared minBLEP table here so its one-time FFT build runs
84 // on the control thread, never inside the audio callback.
86 setFrequency(frequency_);
87 }
88
93 void prepare(const AudioSpec& spec) noexcept
94 {
95 prepare(spec.sampleRate);
96 }
97
102 void setFrequency(T freq) noexcept
103 {
104 // Reject NaN before clamp: std::clamp(NaN, ...) returns NaN (all
105 // comparisons false), which would poison phaseInc_ and the phase
106 // accumulator forever. Matches Phasor/WavetableOscillator guards.
107 if (freq != freq) return;
108 // Clamp frequency to valid bounds [0, Nyquist] to prevent aliasing breakdown
109 T nyquist = static_cast<T>(sampleRate_ * 0.5);
110 frequency_ = std::clamp(freq, T(0), nyquist);
111 updatePhaseInc();
112 }
113
118 void setWaveform(Waveform w) noexcept { waveform_ = w; }
119
141 void setSyncRatio(T ratio) noexcept
142 {
143 syncRatio_ = std::max(T(0), ratio);
144 updatePhaseInc();
145 }
146
159 void setPhase(T phase) noexcept
160 {
161 // Wrap (not clamp): a phase of exactly 1.0 must land on 0.0 so the
162 // first sample is not a one-off discontinuity.
163 phase_ = phase - std::floor(phase);
164 if (phase_ >= T(1)) phase_ -= T(1);
165
166 if (syncOn_)
167 reseedSlave();
168
169 // Steady-state triangle value at this phase (piecewise-linear ideal;
170 // the leaky integrator's true cycle deviates by O(increment), so any
171 // remaining transient is negligible). Phase 0 drives the underlying
172 // square positive, so the triangle rises from -peak over [0, 0.5).
173 const T ph = syncOn_ ? slavePhase_ : phase_;
174 const T ideal = (ph < T(0.5)) ? (T(4) * ph - T(1)) : (T(3) - T(4) * ph);
175 triState_ = static_cast<double>(ideal * triExpectedPeak_);
176 }
177
184 void reset() noexcept
185 {
186 phase_ = T(0);
187 triState_ = -static_cast<double>(triExpectedPeak_);
188 slavePhase_ = T(0);
189 corr_.fill(T(0));
190 corrHead_ = 0;
191 }
192
198 [[nodiscard]] inline T getNextSample() noexcept
199 {
200 if (syncOn_)
201 return nextSyncSample();
202
203 T out = T(0);
204
205 switch (waveform_)
206 {
207 case Waveform::Sine:
208 // fastSin: error < ~4e-6 in float (over 100 dB down), 3-6x
209 // faster than std::sin -- inaudible even for direct synthesis.
210 out = fastSin(phase_ * twoPi<T>);
211 break;
212
213 case Waveform::Saw:
214 out = T(2) * phase_ - T(1);
215 out -= polyBlep(phase_, phaseInc_);
216 break;
217
218 case Waveform::Square:
219 {
220 T raw = (phase_ < T(0.5)) ? T(1) : T(-1);
221 raw += polyBlep(phase_, phaseInc_);
222
223 // Optimized phase shift without std::fmod
224 T halfPhase = phase_ + T(0.5);
225 if (halfPhase >= T(1)) halfPhase -= T(1);
226
227 raw -= polyBlep(halfPhase, phaseInc_);
228 out = raw;
229 break;
230 }
231
233 {
234 T raw = (phase_ < T(0.5)) ? T(1) : T(-1);
235 raw += polyBlep(phase_, phaseInc_);
236
237 T halfPhase = phase_ + T(0.5);
238 if (halfPhase >= T(1)) halfPhase -= T(1);
239 raw -= polyBlep(halfPhase, phaseInc_);
240
241 // Leaky integration for analog-style triangle. The recursion
242 // runs in double (framework rule for recursive state): with a
243 // float accumulator, slow LFO rates quantise near the peaks.
244 const double inc = static_cast<double>(phaseInc_);
245 triState_ = inc * static_cast<double>(raw) + (1.0 - inc) * triState_;
246 out = static_cast<T>(triState_) * triNorm_;
247 break;
248 }
249 }
250
251 // Fast phase wrap assuming positive frequencies only (enforced in setFrequency)
252 phase_ += phaseInc_;
253 if (phase_ >= T(1)) phase_ -= T(1);
254
255 return out;
256 }
257
268 void processBlock(T* buffer, size_t numSamples) noexcept
269 {
270 for (size_t i = 0; i < numSamples; ++i)
271 {
272 buffer[i] = getNextSample();
273 }
274 }
275
277 [[nodiscard]] inline T getSample() noexcept { return getNextSample(); }
278
284 void generateBlock(AudioBufferView<T> buffer) noexcept
285 {
286 const int nCh = buffer.getNumChannels();
287 const int nS = buffer.getNumSamples();
288 if (nCh <= 0)
289 {
290 for (int i = 0; i < nS; ++i)
291 (void) getNextSample(); // keep the phase advancing
292 return;
293 }
294 T* ch0 = buffer.getChannel(0);
295 for (int i = 0; i < nS; ++i)
296 ch0[i] = getNextSample();
297 for (int ch = 1; ch < nCh; ++ch)
298 std::copy_n(ch0, static_cast<size_t>(nS), buffer.getChannel(ch));
299 }
300
301 [[nodiscard]] T getPhase() const noexcept { return phase_; }
302 [[nodiscard]] T getFrequency() const noexcept { return frequency_; }
303 [[nodiscard]] Waveform getWaveform() const noexcept { return waveform_; }
304 [[nodiscard]] T getSyncRatio() const noexcept { return syncRatio_; }
305
306private:
308 [[nodiscard]] T rawSlaveValue(T ph) const noexcept
309 {
310 switch (waveform_)
311 {
312 case Waveform::Sine: return fastSin(ph * twoPi<T>);
313 case Waveform::Saw: return T(2) * ph - T(1);
314 case Waveform::Square:
315 case Waveform::Triangle: return (ph < T(0.5)) ? T(1) : T(-1);
316 }
317 return T(0);
318 }
319
332 [[nodiscard]] T nextSyncSample() noexcept
333 {
334 const bool squareLike =
335 waveform_ == Waveform::Square || waveform_ == Waveform::Triangle;
336
337 // --- emit: raw value plus the pending band-limiting correction ------
338 T raw = rawSlaveValue(slavePhase_) + corr_[static_cast<size_t>(corrHead_)];
339 corr_[static_cast<size_t>(corrHead_)] = T(0);
340 corrHead_ = (corrHead_ + 1) & kCorrMask; // now the NEXT sample's slot
341
342 T out = raw;
343 if (waveform_ == Waveform::Triangle)
344 {
345 // Feed the integrator with the duty-compensated square: the synced
346 // square has inherent DC (see updatePhaseInc) and the integrator's
347 // unity DC gain times triNorm_ would turn it into a large offset
348 // (+0.45 measured at ratio 2.7 without compensation).
349 const double inc = static_cast<double>(slaveInc_);
350 triState_ = inc * (static_cast<double>(raw) - static_cast<double>(syncSquareDc_))
351 + (1.0 - inc) * triState_;
352 out = static_cast<T>(triState_) * triNorm_;
353 }
354
355 // --- advance both phases; schedule corrections in time order --------
356 const T masterOld = phase_;
357 const T slaveOld = slavePhase_;
358 phase_ += phaseInc_;
359 slavePhase_ += slaveInc_;
360
361 const bool masterWrap = phase_ >= T(1);
362 const T alphaSync = masterWrap ? (T(1) - masterOld) / phaseInc_ : T(2);
363
364 // Natural slave edge inside this interval (at most one: slaveInc_ is
365 // clamped to 0.5). It only happens if the reset does not pre-empt it;
366 // on an exact tie the edge wins and the reset jump measures zero.
367 if (waveform_ != Waveform::Sine)
368 {
369 T alphaEdge = T(2), edgeJump = T(0);
370 if (slavePhase_ >= T(1))
371 {
372 alphaEdge = (T(1) - slaveOld) / slaveInc_;
373 edgeJump = (waveform_ == Waveform::Saw) ? T(-2) : T(2);
374 }
375 else if (squareLike && slaveOld < T(0.5) && slavePhase_ >= T(0.5))
376 {
377 alphaEdge = (T(0.5) - slaveOld) / slaveInc_;
378 edgeJump = T(-2);
379 }
380 if (alphaEdge <= alphaSync && alphaEdge <= T(1))
381 scheduleMinBlep(edgeJump, T(1) - alphaEdge);
382 }
383 if (slavePhase_ >= T(1)) slavePhase_ -= T(1);
384
385 if (masterWrap)
386 {
387 // Slave value the instant before the reset (wrap folded in); the
388 // jump lands on the freshly seeded phase-0 value.
389 T atJump = slaveOld + alphaSync * slaveInc_;
390 atJump -= std::floor(atJump);
391 const T jump = rawSlaveValue(T(0)) - rawSlaveValue(atJump);
392 if (jump != T(0))
393 scheduleMinBlep(jump, T(1) - alphaSync);
394
395 phase_ -= T(1);
396 slavePhase_ = (phase_ / std::max(phaseInc_, T(1e-12))) * slaveInc_;
397 slavePhase_ -= std::floor(slavePhase_);
398 }
399 return out;
400 }
401
409 void scheduleMinBlep(T jump, T frac) noexcept
410 {
411 const auto& table = MinBlepTable<T>::instance();
412 for (int j = 0; j < kCorrLen; ++j)
413 corr_[static_cast<size_t>((corrHead_ + j) & kCorrMask)] +=
414 jump * table.residual(static_cast<T>(j) + frac);
415 }
416
426 void reseedSlave() noexcept
427 {
428 const T r = slaveInc_ / std::max(phaseInc_, T(1e-12));
429 slavePhase_ = phase_ * r;
430 slavePhase_ -= std::floor(slavePhase_);
431 corr_.fill(T(0));
432 corrHead_ = 0;
433 }
434
442 static inline T polyBlep(T phase, T inc) noexcept
443 {
444 if (inc < T(1e-10)) return T(0);
445
446 if (phase < inc)
447 {
448 T t = phase / inc;
449 return t + t - t * t - T(1);
450 }
451 else if (phase > T(1) - inc)
452 {
453 T t = (phase - T(1)) / inc;
454 return t * t + t + t + T(1);
455 }
456 return T(0);
457 }
458
460 void updatePhaseInc() noexcept
461 {
462 // Defensive Nyquist clamp. setFrequency() already clamps, but prepare()
463 // can lower the sample rate afterwards (it re-clamps too), and the
464 // T-cast Nyquist bound may round up by an ulp; polyBlep() and the
465 // sync event logic both assume inc <= 0.5.
466 phaseInc_ = std::min(frequency_ / static_cast<T>(sampleRate_), T(0.5));
467 const bool wasOn = syncOn_;
468 syncOn_ = syncRatio_ > T(1.001) && phaseInc_ > T(0);
469 // Clamp the slave below Nyquist like any oscillator frequency.
470 slaveInc_ = std::min(phaseInc_ * syncRatio_, T(0.5));
471
472 // DC of the hard-synced square (feeds the triangle integrator). The
473 // slave always restarts in its +1 half, so its duty cycle is
474 // asymmetric by construction: for an effective ratio r with
475 // fractional part f, the +1 time per master cycle exceeds the -1
476 // time by min(f, 0.5) - max(f - 0.5, 0) slave cycles.
477 syncSquareDc_ = T(0);
478 if (syncOn_ && phaseInc_ > T(0))
479 {
480 const T r = slaveInc_ / phaseInc_; // effective (clamped) ratio
481 const T f = r - std::floor(r);
482 syncSquareDc_ = (std::min(f, T(0.5)) - std::max(f - T(0.5), T(0))) / r;
483 }
484
485 // Sync just (re-)engaged: the ring may hold corrections from a
486 // previous sync run and the slave phase is stale -- re-seed both.
487 if (syncOn_ && !wasOn)
488 reseedSlave();
489
490 updateTriNorm();
491 }
492
496 void updateTriNorm() noexcept
497 {
498 // Under hard sync the integrator runs at the slave rate.
499 const T inc = syncOn_ ? slaveInc_ : phaseInc_;
500 if (inc == triNormInc_)
501 return; // unchanged increment: skip the std::pow below. Chorus
502 // and Phaser call setFrequency() every block, and FM
503 // callers may call it every sample.
504 triNormInc_ = inc;
505 if (inc > T(0) && inc < T(1))
506 {
507 // Steady-state peak of the leaky integrator driven by a +-1 square:
508 // over a half period of n samples starting at -p, the state reaches
509 // p = q*(-p) + (1 - q) with q = leak^n, so p = (1 - q) / (1 + q).
510 // (Using just 1 - q under-normalised the triangle by ~4 dB.)
511 T leakCoeff = T(1) - inc;
512 T halfPeriodSamples = T(0.5) / inc;
513 T q = std::pow(leakCoeff, halfPeriodSamples);
514 T expectedPeak = (T(1) - q) / (T(1) + q);
515 triExpectedPeak_ = expectedPeak;
516 triNorm_ = (expectedPeak > T(0.001)) ? T(1) / expectedPeak : T(4);
517 }
518 else
519 {
520 triExpectedPeak_ = T(0.25);
521 triNorm_ = T(4);
522 }
523 }
524
525 double sampleRate_ = 48000.0;
526 T frequency_ = T(440);
527 T phase_ = T(0);
528 T phaseInc_ = T(0);
529 double triState_ = 0.0;
530 T triNorm_ = T(4);
531 T triExpectedPeak_ = T(0.25);
532 T triNormInc_ = T(-1);
533 Waveform waveform_ = Waveform::Sine;
534
535 // Hard sync (slave) state.
536 static constexpr int kCorrLen = MinBlepTable<T>::kTaps;
537 static constexpr int kCorrMask = kCorrLen - 1;
538 static_assert((kCorrLen & kCorrMask) == 0, "minBLEP ring needs a power-of-two span");
539
540 bool syncOn_ = false;
541 T syncRatio_ = T(0);
542 T slavePhase_ = T(0);
543 T slaveInc_ = T(0);
544 T syncSquareDc_ = T(0);
545 std::array<T, kCorrLen> corr_{};
546 int corrHead_ = 0;
547};
548
549} // namespace dspark
Owning audio buffer and non-owning view for real-time DSP processing.
Describes the audio processing environment (sample rate, block size, channels).
Core mathematical utilities for digital signal processing.
Shared minimum-phase band-limited step (minBLEP) residual table.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Shared minimum-phase band-limited step (minBLEP) residual table.
static const MinBlepTable & instance() noexcept
Returns the process-wide shared table, building it on first call.
static constexpr int kTaps
Correction span in base-rate samples (power of two, ring-buffer friendly).
Band-limited oscillator featuring PolyBLEP anti-aliasing and analog-modeled integration.
Definition Oscillator.h:64
void setSyncRatio(T ratio) noexcept
Enables band-limited hard sync.
Definition Oscillator.h:141
Waveform getWaveform() const noexcept
Definition Oscillator.h:303
T getSyncRatio() const noexcept
Definition Oscillator.h:304
void generateBlock(AudioBufferView< T > buffer) noexcept
Fills every channel of the view with the generated waveform. Satisfies the GeneratorProcessor concept...
Definition Oscillator.h:284
void processBlock(T *buffer, size_t numSamples) noexcept
Fills a buffer with generated samples.
Definition Oscillator.h:268
void reset() noexcept
Hard-resets the oscillator phase and integrator state.
Definition Oscillator.h:184
void setFrequency(T freq) noexcept
Sets the oscillator's fundamental frequency.
Definition Oscillator.h:102
void prepare(double sampleRate) noexcept
Prepares the oscillator with the system sample rate.
Definition Oscillator.h:79
T getPhase() const noexcept
Definition Oscillator.h:301
void setPhase(T phase) noexcept
Forces the oscillator phase to a specific value.
Definition Oscillator.h:159
void prepare(const AudioSpec &spec) noexcept
Prepares the oscillator from an AudioSpec configuration.
Definition Oscillator.h:93
T getNextSample() noexcept
Computes and returns the next single audio sample.
Definition Oscillator.h:198
T getSample() noexcept
Generator contract alias for getNextSample() (GeneratorProcessor).
Definition Oscillator.h:277
void setWaveform(Waveform w) noexcept
Changes the active waveform.
Definition Oscillator.h:118
T getFrequency() const noexcept
Definition Oscillator.h:302
Main namespace for the DSPark framework.
T fastSin(T x) noexcept
Fast sine approximation (degree-9 odd minimax polynomial).
Definition DspMath.h:213
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35