Prove it
Renders this exact patch through an
OfflineAudioContext and compares the samples that come back against the curve
this page is drawing. Same patch, two completely separate paths to it.
Render and compare
let Web Audio normalize
Not run yet.
Per harmonic, requested against measured
Measured amplitudes are read back by correlating the
rendered buffer against each harmonic. That is only leakage free over a whole number of
periods, and a musical pitch rarely divides the sample rate evenly, so treat these as
accurate to roughly a percent. The max error above has no such caveat: it is a
sample by sample comparison.
What is going on
Any periodic sound is a stack of sines. Fourier's claim is that a repeating shape can
be written as a sum of sine waves at whole multiples of its own frequency. The sliders are
those multipliers. Nothing else is generating sound here.
The engine takes coefficients, not samples. createPeriodicWave(real, imag)
builds an oscillator straight from a Fourier series: entry k is harmonic k , entry
0 is DC and ignored. Measured convention, not remembered:
x(t) = sum real[k]·cos(2πkt) + imag[k]·sin(2πkt), both terms
positive. To put partial k at amplitude a and phase p :
real[k] = a·sin(p), imag[k] = a·cos(p).
It rescales your wave unless you stop it. By default the engine normalizes a periodic
wave to a peak of 1. Ask for a lone sine at 0.25 and you get back a peak of 1.0. This build
passes {disableNormalization: true} everywhere so the sliders mean what they
say. Tick the normalize box above and re-run: on a stock preset the largest sample error
goes from about 0.0001 to about 0.1, roughly 700 times worse. It is that mild only
because the presets are level matched to a peak of 0.9. Push the sliders up first and the
gap grows with the distance between your peak and 1.0.
Phase is loud to the eye and quiet to the ear. Load Sawtooth, then hit Random. The
waveform stops being a ramp, the spectrum does not move, and the tone barely changes. The
ear is largely insensitive to the relative phase of the harmonics in a steady tone, an
observation usually credited to Ohm and Helmholtz. Largely, not entirely: it does not hold
through an attack, and the peak level it throws around is completely real, which is why the
crest factor moves when the loudness does not.
Gibbs ringing never shrinks. Load Square and sweep the partial count. The wobble
beside each jump gets narrower and never gets shorter. The overshoot converges to
(2/π)·Si(π) = 1.178980 times the half jump, about 9% of the full
jump, and stays there for every finite number of partials.
High notes lose their top. Play near the top of the keyboard with every partial up
and hit Render and compare. Harmonics above the Nyquist frequency come back missing: the
engine drops them rather than let them fold back down as aliases. Same patch, duller tone,
purely because of where you played it.
Why this build exists
Build #1 in this playground shipped with a rendering bug that thirty six audited solves
walked straight past, because the audit checked the data and the bug was in the picture.
So this one has the comparison between the two built into the interface as a button, and
the checker asserts on it.