You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/dsp/oscillators.md
+101Lines changed: 101 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -235,6 +235,107 @@ the same allocation-free generation path for other sources. Fill its high-rate b
235
235
`getGenerationLatencyInSamples()` reports decimation-only latency, while the
236
236
existing `getLatencyInSamples()` still describes the complete up/down path.
237
237
238
+
## Prism spectral shaping
239
+
240
+
`PrismSpectrum<CoeffType = double>` shapes one `FourierSeries` into another. It is a
241
+
pure spectral transform with no DSP state and no transform behind it, so it composes
242
+
in front of whichever synthesis backend you were already going to use.
243
+
244
+
Applied to harmonic `h`, writing `u = log2 (h)` and `c` for the harmonic's complex
245
+
coefficient (`cosine + i sine`), the pipeline runs in this order:
246
+
247
+
| Stage | Operation | Control |
248
+
|---|---|---|
249
+
| Ridge |`gain = 0.15 + 0.85 * ridge^2`, `ridge = 0.5 + 0.5 * cos (2 pi (u / ridgeSpacing - color))`|`ridgeSpacing` in octaves, clamped to [0.25, 8]; `color` slides the comb and is periodic. |
250
+
| Tilt |`gain *= 2^(-tilt * u)`|`tilt` in gain octaves per harmonic octave, clamped to +/-4; 0 is flat. The ridge is periodic in `u`, so it cannot express an overall slope. |
251
+
| Odd/even | odd and even harmonics scaled against each other |`oddEven` clamped to [0, 1]; 0 keeps only odd, 1 only even, 0.5 is neutral. No transcendentals at all. |
252
+
| Formant |`gain *= 2^(formant * exp (-((u - formantPosition) / formantWidth)^2))`|`formant` is a signed depth in gain octaves, clamped to +/-4, 0 is bypass; `formantPosition` is the center in harmonic octaves, clamped to [0, 12]. One movable resonance against the ridge's periodic comb. |
253
+
| Squash |`\|c\|` becomes `\|c\|^squash`, phase kept |`squash` clamped to [0.1, 4]; 1 is bypass. |
254
+
| Squeeze |`c` multiplied by `1 - d * cis (-2 pi h w)`|`squeeze` is a pulse width in periods, clamped to [0, 0.5]; the depth `d` opens from 0 to 1 over the first `squeezeFadeWidth`, so 0 is a true bypass. |
255
+
| Dispersion + scatter |`c` multiplied by `cis ((dispersion - 0.5) * u^2 + scatter * angle[h])`|`dispersion` clamped to [0, 1], 0.5 is flat; `scatter` clamped to [0, 1], 0 is bypass. Both are rotations, so their angles add and one `sincos` serves both - scatter is free on top of dispersion. `angle[h]` is fixed per harmonic, so a shape stays a timbre instead of re-scattering every block. |
256
+
| Normalize | whole series scaled so `sum \|c\|` matches the source's | - |
257
+
258
+
Every stage is a per-harmonic scale or rotation, so **no stage can produce a
259
+
frequency the source did not already contain**. That is the whole antialiasing
260
+
argument: shaping cannot alias, and whatever plays the result still bandlimits per
261
+
pitch. It is also the rule any further stage must obey: `c[h]` may be multiplied by
262
+
anything, but a harmonic's frequency is pinned at `h` times the fundamental and cannot
263
+
be moved - stretched or inharmonic partials are not expressible in a periodic series at
264
+
all, and need a different oscillator rather than another stage here. Squeeze deserves the emphasis because it looks like it should alias -
265
+
pulse-width modulation normally does - but subtracting a phase-shifted copy of a
266
+
bandlimited signal is still bandlimited, so this is exact PWM. At `w = 0.5` the factor
267
+
is zero for even `h` and two for odd, the square-from-sawtooth identity.
268
+
269
+
The faded depth is not cosmetic. The raw factor tends to `i * h * 2 pi w` as the width
270
+
falls, and renormalizing that leaves a differentiator rather than the source spectrum,
271
+
so the stage has no bypass width - a knob stepping off zero would jump. Fading `d` in
272
+
over `squeezeFadeWidth` is what makes zero continuous with its neighbourhood, and it
273
+
costs one multiply. The factor stays a per-harmonic complex scale either way, so the
274
+
antialiasing argument is untouched.
275
+
276
+
Normalization preserves `sum |c|` rather than the waveform's peak, and the two are not
277
+
the same: `sum |c|` bounds a peak from well above (about three times over for a
278
+
sawtooth), and how close the waveform comes to that bound depends on how aligned its
279
+
harmonic phases are. `dispersion` and `scatter` control exactly that, so if constant
280
+
output level matters, measure the shaped waveform's peak and rescale - once per patch
281
+
alongside the shaping, not per voice.
282
+
283
+
Normalization is what makes the controls level-safe. The ridge gain, the companding
284
+
and the pulse-width factor (whose magnitude reaches 2) all change level, and
285
+
`sum |c|` is an upper bound on the waveform's peak, so preserving it guarantees the
286
+
output can never be louder than the source's worst case. The sum runs over harmonics
287
+
only; DC is not carried across and the destination's DC is always zero.
0 commit comments