Fifty Shades of Sinusoids
Sinusoids are pretty fundamental to signal theory, and we find them in all sorts of applications from additive synthesis to discrete summation formula, from Fourier transform to ring modulation.
Waveshaping
One of the most straightforward ways to make a sinusoid waveform is to produce a linearly increasing phase and map that with a trigonometric function. Because trig functions are periodic, we can actually use a periodic phasor to do this:
Import Gen
TrigSin(freq) {
Math:Sin( Gen:Phasor(freq) * #2 * Math:Pi )
}
snd = TrigSin(330) * 0.2
The problem with this approach is that computing trigonometrics takes a lot of time. For something so ubiquitous, we would want to be as light on the CPU as possible.
Turns out we don't exactly need fully precise trigonometry to sound like a pure tone. A polynomial approximation will be just fine. The technique used for the built-in Gen:Sin
is exactly this, and it is explained in more detail in another article.
Sin-Coef(n) {
; Formula for the nth polynomial coefficient for approximating sine
Math:Pow(#-1 n) / Math:Factorial(#2 * n + #1)
}
Horner(x coefs...) {
; Compute polynomial using Horner's scheme
Algorithm:Fold((c p) => c + x * p coefs...)
}
PolySin(freq) {
Use Algorithm[Map Count]
; generate 7 coefficients for a 14th order polynomial
coefs = Map(Sin-Coef Count(#7 #0))
; run phasor from -Pi to Pi, because that's where the best precision is
w = (Gen:Phasor(freq) * #2 - #1) * Math:Pi
; compute polynomial sinusoid
w * Horner(w * w coefs)
}
snd = PolySin(330) * 0.2
Essentially, we are still doing the same thing, but instead of a trigonometric function, we use a big polynomial that looks very similar in [-Pi,Pi].
Rotation to the Rescue
Waveshaping sinusoid oscillators are good when you need to compute waveforms for rapidly changing frequencies. However, when the frequency changes slowly, as is often the case in audio, we can make further efficiency gains by computing the sinusoid incrementally.
One way is to use complex numbers. Since multiplication by a number on the unit circle is just rotation around the origin, we can recursively rotate another unitary number and retain either its real or imaginary part for a sinusoid waveform.
Import Complex
ComplexSin(freq) {
; rotation coefficient
rot = Complex:Unitary(freq * #2 * Math:Pi / Audio:Rate())
; feedback loop, which must be initialized to non-zero
state = z-1(#1 state * Audio:Signal(rot))
; return the real part for a cosine wave
Complex:Real(state)
}
snd = ComplexSin(330) * 0.2
Magic Circle
A related approach is to use a critically stable filter and let it self-oscillate. A good candidate for this is the state variable filter.
The implementation might look like it has more computation than the complex recursive formula, but the opposite is true in practice, because our complex multiplication is equivalent to four real multiplies and two additions.
MagicSin(freq) {
f = Audio:Signal(#2 * Math:Pi * freq / Audio:Rate())
sin = sin-1 + f * cos-1
cos = cos-1 - f * sin
cos-1 = z-1(#1 cos)
sin-1 = z-1(#0 sin)
cos
}
snd = MagicSin(330) * 0.2
Guide for Waves
Turns out we can do even better for constant-frequency sinusoids: Professor Smith III has our back. Waveguides simulate the propagation of waves in a medium with discrete summation points, connected by edges with delays. The acoustical structure we are simulating is a combination of two acoustic tubes of different width, narrow end open, wide end closed. The rotation of a sinusoid is accomplished by some of the energy being transmitted through the junction and some being reflected.
For details, please see the reference. I am just going to leave an implementation here:
DWGSin(freq) {
R = #1
b1 = #1
theta = freq * Math:Pi * #2 / Audio:Rate()
g = R * R
t = Math:Tan(theta)
cp = Math:Sqrt(g / (g + t * t * ((#1 + g) * (#1 + g) + (#1 - g) * (#1 - g)) / #4 ))
x1 = z-1(#1 Audio:Signal(x0))
y1 = z-1(#0 Audio:Signal(y0))
tmp = cp * (x1 + y1)
x0 = tmp - y1
y0 = x1 + tmp
DWGSin = x0
}
snd = DWGSin(330) * 0.2
In comparison to the magic circle formula, we trade one multiplication for an extra addition and a slightly hairier intermediate coefficient calculation.
What to use When
The answer to that question depends on the frequency. If you modulate frequency at the audio rate, look at waveshaping. There is no additional math to bog down frequency updates. If you modulate heavily at a control rate, the complex recursive formula may be of interest. You can also try circle or guide, but be aware that their amplitude could explode if you tickle the frequency signal in a wrong way. C'est La Vie.
If you are just looking for a stable, pure tone, the waveguide is usually the best bet.