Every lesson so far processed a sound that already existed. A source kernel makes the sound: freq pitch |> processO law init hands your law the note's frequency every sample and plays whatever it returns. A sawtooth is four lines — and, written the obvious way, it aliases. This lesson hears that, fixes it the way the builtin saw does, and turns the result into an instrument with a knob.
The starter's naiveSaw keeps a phase ph in one cell, climbing by hz / sampleRate each sample and wrapping at 1; the output is 2·ph − 1. The wrap is an instant jump, and an instant jump contains harmonics all the way up — past Nyquist, where they fold back down as aliases: tones that do not follow the note. Play the high notes and watch the spectrum: bars between the harmonics, and a whistle that moves the wrong way when the melody climbs. Put the caret inside the law and the DSP PROBE dock's oscillator view plays it at four pitches and reads the alias floor in dB.
The fix is a polyBLEP (polynomial band-limited step): subtract a tiny correction on the two samples around each jump, so the step is rounded exactly as much as a band-limited step would be. import "kernels" (polyBlep) and write 2.0 * ph - 1.0 - polyBlep ph dt, with dt = hz / sampleRate the phase step. That is the builtin saw's own trick; measured, the kernel stdlib's blSaw aliases within 0.01 dB of it (reference/user-kernels, Source kernels).
Name it: oscillator fatSaw (spread: Float = 7.0) = \hz -> hz |> processO (twin spread) [0.0, 0.0] declares a source with a parameter list like an effect. Call it where a builtin oscillator goes — source = fatSaw (freq pitch) { spread = 12.0 } — and hover the call: your @doc is there. Glide, vibrato and FM reach it through the frequency, exactly as they reach saw (freq pitch).
Every parameter is live. Pass a macro ({ spread = width }, or arithmetic over one: { spread = 2.0 + knob * 20.0 }) and the knob rides inside the law; or address it from a scene as <patch>.<oscillator>.<param> — lead.fatSaw.spread = ramp 3.0 25.0 — with no macro at all.
The starting point#
The lesson opens with this song in the editor. Every edit recompiles and plays.
-- Your own oscillator: a sawtooth written as a source kernel. `processO`
-- hands the law the note's frequency every sample; the cell `ph` is the
-- phase, 0 → 1 once per cycle. Play it and look at the spectrum on the high
-- notes: bars that do not belong to the note. That is aliasing.
naiveSaw = \hz ph ->
let next = ph + hz / sampleRate in
[2.0 * ph - 1.0, next - floor next]
lead = patch {
source = freq pitch |> processO naiveSaw 0.0,
ampEnv = adsr 2ms 200ms 0.6 120ms,
gain = vel * 0.2
}
leadT = play lead !"e5 g5 b5 d6 e6 d6 b5 g5"
What to watch#
Watch the spectrum on the top notes: with the naive saw the gaps between harmonics fill with bars that slide against the melody; subtract the polyBLEP and they drop away while the harmonics stay put.
Goals#
The workbench checks these as you work; the solution is there when you want it.
- Band-limit the reset with
polyBlep - Name it:
oscillator fatSaw (…) = \hz -> … - Ride a parameter with a macro
- Stretch: a hard-sync lead from the kernel stdlib
Make it yours#
Make spread sweep from a scene (lead.fatSaw.spread = ramp 3.0 25.0), then try the sync lead with its ratio addressed the same way. Which one sounds wider, and which one screams?
Further reading#
DSP cookbook in the reference.