Week 2 — Subtractive Oscillator


The Evergreen State College · Summer 2026 · 12 credits
Student: Travis Inskeep · Faculty Sponsor: Jessica Carey

Eidolon · Independent Learning ContractWK·02

Goal: build the oscillator that all three synthesis modes share, and the subtractive voice that runs on it: the five classic analogue shapes, band-limited so they don't alias. Done: a shared Oscillator base class drives a BezierOscillator that produces the five shapes as piecewise cubic Bezier segments; the three edged shapes are anti-aliased with minBLEP, and the triangle's corners with minBLAMP. The evidence below is rendered straight from the plugin's shipping subtractive voice.

The shared oscillator base class

Every mode needs the same clock, so the shared work lives in one abstract base, Oscillator. It owns a phase accumulator (a value that ramps from 0 to 1 once per cycle, advanced each sample by the frequency divided by the sample rate) and the note handling that keeps that rate in step with the pitch. It declares one pure-virtual method, processSample, that each mode fills in to turn the current phase into one output sample. The shipping subtractive voice is BezierOscillator, the first concrete implementation of that interface; Weeks 3 and 4 subclass the same base, inheriting the clock and note handling and supplying only their own processSample. Per voice, that method is driven inside processBlock, the audio-thread method from Week 1, so the whole oscillator section drops into the lifecycle already in place.

The subtractive oscillator was first built as SubtractiveOscillator, which generates each shape directly from the running phase: the saw is the phase ramp, the square compares phase against a threshold, and the triangle folds the ramp piecewise. The shipping build moved to BezierOscillator because representing the shapes as repositionable cubic Bezier segments unlocks audio-rate pulse-width modulation via segment repositioning and smooth shape morphing that direct-from-phase generation could not provide; minBLEP was ported into BezierOscillator in a later pass. The original SubtractiveOscillator remains in the codebase as the wavetable-capture source: Week 4's Basic Analog wavetable is captured straight from it.

The five waveforms

The BezierOscillator selects one of five shapes through a SubShape setting: saw, square, triangle, sine, and noise. Each shape (except noise) is built as a series of piecewise cubic Bezier segments, each defined by four control points; a cubic Bernstein polynomial evaluates the current segment at the phase position within it. The saw's segments trace a rising ramp and a sharp reset; the square's segments hold high and low levels with a repositionable transition that enables audio-rate pulse-width modulation via segment repositioning; the triangle's segments fold at the midpoint; the sine approximates the sinusoidal arc. Noise ignores the segment structure and draws from the coloured noise generator instead. The three edged shapes, saw, square, and triangle, still carry sharp transitions at segment boundaries, and that is exactly where aliasing begins.

Anti-aliasing: minBLEP and minBLAMP

An ideal saw or square holds infinitely many harmonics. Sampled at 48 kHz, every harmonic above the Nyquist frequency of 24 kHz folds back to an inharmonic tone that doesn't belong to the note, the metallic edge of a cheap digital oscillator. Band-limiting means removing those over-Nyquist harmonics before they can fold.

The real-time fix is the BLEP, a band-limited step. Rather than let the wave jump instantaneously, the oscillator adds a short precomputed correction that replaces the vertical edge with a band-limited transition, cancelling the harmonics that would otherwise alias. Eidolon uses the minimum-phase form, minBLEP: the correction's energy sits just after the discontinuity instead of straddling it, so it needs no lookahead, the right choice for an oscillator that runs one sample at a time.

The corrections land where the shape breaks. The saw has one step per cycle, at the phase wrap; the square has two, a rising edge at the wrap and a falling edge at the pulse-width point. At each, the oscillator measures how far past the current sample the true edge actually fell (from the phase's overshoot past 1) and inserts the minBLEP correction at that fractional offset, so the fix lands with sub-sample accuracy.

The triangle is a different problem: it never jumps, but its slope reverses at each corner. A step correction is wrong there; the matching tool is the BLAMP, a band-limited ramp, again in minimum-phase form, minBLAMP. It is the BLEP correction integrated once, and it rounds each slope kink just enough to band-limit it.

The corrections themselves are computed once, offline, into a small table: a windowed sinc (a Blackman-Harris window on the ideal band-limited step) put through a minimum-phase, or cepstral, transform and then integrated; integrating a second time gives the minBLAMP. Because a minimum-phase step leaves a small residual DC offset that shifts with pitch, a DC blocker (a one-pole high-pass near 15 Hz) follows the oscillator to remove it.

The result is honest rather than perfect. Measured against a naive sawtooth (below), minBLEP keeps the harmonic series intact and pushes the strongest aliased tones down by roughly 10 to 15 dB, clearing most of the lower band to below −80 dB; the residual that remains rises toward Nyquist and grows with the fundamental. It is a large, audible improvement, the trade a finite, real-time correction makes, not the silence of an offline render.

The noise source

The fifth shape is not periodic at all. Noise comes from a fast pseudo-random generator (an Xorshift sequence) seeded differently per voice so stacked voices don't correlate into an audible pattern. It carries no phase and needs no band-limiting: broadband noise already fills the spectrum, so there are no folding harmonics to cancel. A tilt control colours it, leaning the spectrum from flat white toward a darker or a brighter cast.

Evidence

All four assets are rendered headless from the plugin's own oscillator (one voice, no analogue coloration, 48 kHz), so they show the real code path, not a model of it.

Time-domain plots of the five subtractive shapes at 440 Hz: saw, square, triangle, sine, and white noise
The five shapes at 440 Hz, one cycle each. The slight rounding at the saw's reset and the square's edges is the minBLEP band-limiting at work.
Overlaid raw dBFS spectra of a 1760 Hz sawtooth: a naive discontinuous reference with aliases across the band and Eidolon's current minBLEP render with those off-harmonic components reduced
The same 1760 Hz sawtooth, overlaid as raw dBFS spectra: an analytic naive reference versus Eidolon's current minBLEP render. Naive sampling scatters aliased tones across the band; minBLEP preserves the harmonic series while reducing those off-harmonic components, with the finite correction's residual rising toward Nyquist.
The five shapes in order: saw, square, triangle, sine, noise.

Reflection

The band-limiting took the most of my attention and most of the difficulty. The minBLEP and minBLAMP corrections are only a few lines at the point of use, but understanding why they work, and building the offline table behind them (the windowed sinc, the minimum-phase transform, the double integration that yields the ramp) was the real study of the week. The shared base class and the five shape generators were more straightforward, a matter of care rather than discovery, since the clock and note handling carried over from Week 1. The noise source was the smallest piece. The honest surprise was in the evidence: my first spectrum looked clean until I measured it, and the residual aliasing I found there sent me back to build the naive-versus-minBLEP comparison the page now shows. Measuring the result turned out to matter as much as writing it.

References

Eidolon · Week 2 of 8 · The Evergreen State College