Oscillators — Part 3: Discontinuities and Aliasing
After Phase
In Part 1, we built a naïve sine oscillator and handled the constraints of discrete time. Phase is now wrapped, bounded, and updated correctly.
Yet the oscillator still fails.
Discontinuity
If everything about phase is handled correctly, any remaining instability must come from how phase is used, not how it is maintained.
When phase is mapped to an output, that mapping is not always smooth. Sine waves vary continuously, but many other waveforms do not. Sharp corners and jumps introduce discontinuities that discrete-time systems can only approximate.
The Four Basic Waveforms
Below are mathematical representations and naïve C++ implementations of sine, saw, square, and triangle waves. This assumes a phase accumulator \(p ∈ [0, 1)\) and frequency \(f\) at sample rate \(F_s\).
Shared Phase Accumulator
- Phase increment:
- Phase update and wrap:
struct Phase {
float p = 0.f; // phase
float dp = 0.f; // phase increment
void setFrequency(float f, float Fs) {
dp = f / Fs;
}
float tick() {
p += dp;
if (p >= 1.f)
p -= 1.f;
return p;
}
};
Waveforms
Sine Wave:
\[y(p) = \sin(2\pi p)\]float sine(float p) {
return std::sinf(2.f * float(M_PI) * p);
}
Sawtooth (rising):
\[y(p) = 2p - 1\]float saw(float p) {
return 2.f * p - 1.f;
}
Square Wave (50% Duty Cycle):
\[y(p) = \begin{cases} +1, & p < 0.5 \\ -1, & p \ge 0.5 \end{cases}\]float square(float p) {
return (p < 0.5f) ? 1.f : -1.f;
}
Triangle Wave:
\[y(p) = 1 - 4\left|p - \tfrac{1}{2}\right|\]float triangle(float p) {
return 1.f - 4.f * std::fabs(p - 0.5f);
}
Waveform Graph
Full Implementation
#pragma once
#include <cmath>
class Oscillator {
public:
enum class Waveform { Sine, Saw, Square, Triangle };
void setSampleRate(float sampleRate) {
Fs = sampleRate;
updatePhaseInc();
}
void setFrequency(float frequency) {
f = frequency;
updatePhaseInc();
}
void setWaveform(Waveform w) {
waveform = w;
}
void resetPhase(float p0 = 0.f) {
phase.p = p0; // assume p0 in [0,1) for this naïve example
}
float tick() {
const float p = phase.tick();
switch (waveform) {
case Waveform::Sine: return sine(p);
case Waveform::Saw: return saw(p);
case Waveform::Square: return square(p);
case Waveform::Triangle: return triangle(p);
}
return 0.f;
}
private:
// --- Phase accumulator ---
struct Phase {
float p = 0.f; // phase
float dp = 0.f; // phase increment
void setFrequency(float f, float Fs) { dp = f / Fs; }
float tick() {
p += dp;
if (p >= 1.f) p -= 1.f;
return p;
}
};
// --- Waveforms (naïve / band-unlimited) ---
static float sine(float p) {
return std::sinf(2.f * float(M_PI) * p);
}
static float saw(float p) {
return 2.f * p - 1.f;
}
static float square(float p) {
return (p < 0.5f) ? 1.f : -1.f;
}
static float triangle(float p) {
return 1.f - 4.f * std::fabs(p - 0.5f);
}
void updatePhaseInc() {
phase.setFrequency(f, Fs);
}
// Stored state
Phase phase;
Waveform waveform = Waveform::Sine;
float f = 440.f;
float Fs = 48000.f;
};
Spectral Consequences of Discontinuity
In discrete time, abrupt change becomes wideband energy. The sharper the transition, the broader the spectrum it demands. When that demand exceeds what the system can represent, the excess energy doesn’t disappear—it shows up where it doesn’t belong.
This problem—known as aliasing—is not a separate phenomenon, but the direct result of discontinuity in a finite system.1
Phase wrapping prevents unbounded growth. It does not remove sharp edges. A perfectly wrapped oscillator can still fail once discontinuities enter the picture.
Aliasing
Once discontinuities exist, their consequences are unavoidable. A discrete-time system can only represent a finite range of frequencies. When a waveform demands energy beyond that range, the excess does not vanish.
Instead, it folds back.
As harmonics exceed the representable bandwidth, they reflect around the Nyquist limit: (Fs / 2), and reappear at lower frequencies. These components are no longer harmonically related to the fundamental. As the oscillator’s frequency increases, they move downward rather than upward, producing inharmonic structure that was never present in the original signal.2
This behavior is still fully deterministic. Nothing “random” is happening and nothing is numerically broken. The oscillator is behaving exactly as a finite system must when asked to represent sharp change.
Aliasing is therefore not “artifact polluting an otherwise correct oscillator.” It is the inevitable spectral consequence of discontinuity colliding with the limitations of discrete time.
Try It Out
Use the visualizer below to see and hear how different naïve oscillators alias as their frequency increases. The oscillator frequency sweep runs from 20 Hz to just under the sample rate, deliberately crossing the Nyquist limit.
What’s Next
In the next article, we’ll discuss antialiasing.
Notes
-
Smith, Steven W. “The Sampling Theorem.” The Scientist and Engineer’s Guide to Digital Signal Processing. https://www.dspguide.com/ch3/2.htm. ↩
-
Smith, Julian O. “Aliasing of Sampled Signals.” Mathematics of the Discrete Fourier Transform (DFT), with Audio Applications. https://ccrma.stanford.edu/~jos/st/Aliasing_Sampled_Signals.html. ↩