Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
vco.hpp File Reference

OSC-VCO: the deterministic signal path of a circuit-flavored analog voltage- controlled oscillator, built by composing the bandlimited core (VaOscillator — itself PhaseAccumulator + the BLEP/BLAMP kernels) with the memoryless and LTI stages that give an analog VCO its character. More...

#include "blep.hpp"
#include "phase.hpp"
#include "va.hpp"
#include <pulp/signal/dc_blocker.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <numbers>

Go to the source code of this file.

Classes

struct  pulp::signal::osc::VcoTuning
 The analog exponential pitch converter, as a pure control-voltage → frequency map. More...
struct  pulp::signal::osc::WaveshaperParams
 A per-shape lumped memoryless nonlinearity — the "analog character" the output stage imprints. More...
class  pulp::signal::osc::VcoOscillator
 Virtual-analog VCO over the static shapes, with a circuit-flavored deterministic path. More...

Namespaces

namespace  pulp
namespace  pulp::signal
namespace  pulp::signal::osc

Detailed Description

OSC-VCO: the deterministic signal path of a circuit-flavored analog voltage- controlled oscillator, built by composing the bandlimited core (VaOscillator — itself PhaseAccumulator + the BLEP/BLAMP kernels) with the memoryless and LTI stages that give an analog VCO its character.

── What this is, and what it is NOT ──────────────────────────────────────

This is the deterministic front-to-back path only: a pitch-control front end, the bandlimited core shape, an integrator-leak "bow" on the ramp, a per-shape lumped waveshaper, a level-vs-pitch curve, and an output DC-blocking (AC-coupling) stage. Every one of those is a hand-set parameter with a physically-motivated meaning, and every one DEFAULTS TO ITS NEUTRAL VALUE, so a default-constructed OSC-VCO is bit-for-bit a VaOscillator (see the null in test_osc_vco.cpp).

It also carries the VCO's two stochastic pitch sources — drift and jitter — as a seeded frequency modulation of the per-sample increment before it reaches the bandlimited core. They are two distinct processes:

  • Drift is a slow, bandlimited random walk of the pitch: white noise colored by a one-pole (leaky-integrator / Ornstein–Uhlenbeck) filter whose corner (set_drift_rate_hz, default kDefaultDriftRateHz) sets a wander that evolves over hundreds of milliseconds, not per sample. Its stationary output has unit variance, so drift_depth reads directly as the RMS pitch excursion in cents.
  • Jitter is fast, near-white cycle-to-cycle frequency noise: independent per sample, so jitter_depth is the RMS per-sample frequency deviation in cents. In the time domain the two separate by their autocorrelation (drift is coherent frame-to-frame, jitter is not) — the Allan-slope regime split (drift ∝ τ^{+1/2}, jitter ∝ τ^{-1/2}) that names them rigorously is a later offline (Python Quality-Lab) increment, not computed here.

Both are converted from cents to a frequency multiplier by 2^(cents/1200) and injected at ONE site, pitch_noise_factor(), which multiplies the increment. When both depths are 0 that factor is exactly 1.0 (an early-out that advances no noise state), so a default-constructed OSC-VCO stays bit-for- bit a VaOscillator (see the master null in test_osc_vco.cpp). The source is deterministic and seed-reproducible (set_seed; no random_device, no clock) — the same seed and inputs yield bit-identical output, which the suite gates. The offline fitting tool and data-driven profiles remain out of scope: this is the ENGINE that has the parameters, hand-set, not the estimator that recovers them.

── Circuit-accuracy of the correction, honestly ─────────────────────────

The bow and the waveshaper are applied as memoryless maps of the core's already-bandlimited output. That is the standard analog-modeling composition, and it is honest about one thing: a memoryless nonlinearity on a bandlimited signal reintroduces some aliasing (the analog stages alias a little too), and the bow's reshape breaks the ramp's slope at the wrap without a fresh BLAMP. The core's own discontinuity correction is intact and measured with the character neutral; the bow is therefore gated for level/DC correctness, not for alias rejection. A circuit-accurate wrap correction for a bowed ramp would need the core's correction loop to accept a caller-supplied shape (value + slope + internal thresholds) rather than its four fixed shapes — an extraction worth doing if a later increment needs it, and called out rather than done by copying the loop here.

RT contract: no allocation, no locks, no I/O on the per-sample path. double throughout, matching the core; a float caller narrows once on store. The bow's exp and the waveshaper's tanh are libcalls, like the core's sin — none of them allocates, locks, or blocks.