Let oscillators interact to create glassy keys, metallic bells, animated basses and more complex tones.
Hear it and change it#
Listen for: a chiming attack that settles into a simpler tone. One sine oscillator changes the frequency of another; the envelope reduces that change as the note decays.
tempo = 110bpm
timingVersion = 2
glasskeys = patch {
filtEnv = adsr 2ms 500ms 0.05 200ms,
source = sine (freq pitch + fm (sine (freq pitch * 2)) (filtEnv * 700Hz)),
ampEnv = adsr 3ms 500ms 0.15 350ms,
gain = vel * 0.3
}
play glasskeys "c4 [eb4 g4] bb4 g4" |> reverb 0.41 0.2
Make it yours: change the modulator ratio * 2 to * 2.7 for a more metallic color. See FM, phase and ring modulation.
What it is#
Audio-rate modulation of one oscillator by another — the source of bells, clangs, and metallic timbres that subtractive synthesis cannot reach. Four operators are shipped:
fm— linear FM: a modulator oscillator drives a carrier's frequency. The modulator's output, scaled by a depth in Hz of peak frequency deviation, sums into the carrier's frequency input. The dimensionless FM index is this deviation divided by the modulator frequency.ringMod— ring modulation: the product of two audio signals, keeping only their sum and difference tones (a hollow, inharmonic timbre).phaseMod— phase modulation: a modulator moves the phase of a wavetable, anfmOp, or a plainsinecarrier (depth in cycles). A plain sine is promoted to a zero-feedback operator. Other oscillator shapes do not have this phase input.fmOp— a sine FM operator with self-feedback: its phase is modulated by its own output (depth in radians). The one operator that cannot be wired from separate graph nodes — the feedback is a zero-delay cycle — so the engine renders the whole operator in one node. 0 is a pure sine; raising the feedback brightens it toward a sawtooth and on into a clangorous regime.
Surface#
Four voices, each one ready to drop into a patch's source field:
-- FM: a 2:1 modulator FM-ing a sine carrier; an envelope sweeps the index.
modulator = sine (freq pitch * 2)
clangIndex = filtEnv |> mods.gain 800Hz
fmSource = sine (freq pitch + fm modulator clangIndex)
-- Ring modulation: a saw times a sine tuned a fifth above.
ringSource = ringMod (saw (freq pitch)) (sine (freq pitch * 1.5))
-- Phase modulation: a signal-first modifier on a wavetable carrier.
-- `bite` (a macro) is the PM depth in cycles.
bite = macro "bite" 0.3
pmSource = wavetable "harmonics" (freq pitch) (const 0.35)
|> phaseMod (sine (freq pitch * 2)) bite
-- Feedback FM: a self-feedback sine. `bite` raises the feedback live, sweeping
-- a pure sine toward a sawtooth / clangorous buzz.
fbSource = fmOp (freq pitch) (const 1 + bite * 5)
Signatures:
fm : Sig Audio Bipolar -> Sig Control Hz -> Sig Audio Hz
ringMod : Sig Audio Bipolar -> Sig Audio Bipolar -> Sig Audio Bipolar
phaseMod : Sig Audio Bipolar -> Sig Audio Bipolar -> <depth> -> Sig Audio Bipolar
fmOp : Sig Control Hz -> <feedback> -> Sig Audio Bipolar
fmOp's freq routes through the same freq-port path as the analog oscillators so an external fm term still sums into an operator's frequency, and an fmOp output is an ordinary audio signal usable as a carrier or, via fm, as a modulator — so deep operator stacks compose by nesting, with feedback now available on any operator.
fm's depth is mandatory: it is the peak deviation in Hz, and any ordinary modulator (envelope, macro, LFO) can drive it. phaseMod accepts depth in cycles or dimensionless magnitudes, including live controls; 0.25 cycles is a quarter-turn. Unrelated physical units are errors. ringMod's mix and soften's amount likewise require dimensionless magnitudes.
fm, ringMod, phaseMod, sync and soften share descriptor-owned binding, unit checks and graph handlers. Aliases, imported/returned functions and container-held callables retain those contracts. A partial call missing a required port remains callable; once required ports are supplied, omitted options take their defaults. Named options remain available after partial application. Sync modes are prepared case-insensitive strings and can come from ordinary value-producing helpers. Phase modulation and sync validate the realized carrier kind before changing its oscillator. Real example: examples/songs/clang.volta (all three operators) and examples/songs/trance.volta (fm).
Notes & limits#
- Audio-rate frequency inputs.
sine,saw,square,triangleandpulseaccept Hz-valued audio expressions directly:sine (440Hz + sine 10Hz * 110Hz).fmis a convenient way to multiply a modulator by its depth. Wavetable,fmOpand AS3340 frequency ports still require their existing explicitfmform for audio modulation.
- Linear FM, not exact PM, for
fm. Modulating.frequencyis what most "FM synths" (including the DX7) approximate; it can accumulate DC drift if the modulator is not zero-mean. Exact phase modulation is available only viaphaseMod. phaseModcarriers: wavetable,fmOp, and a plainsine.fmOp … |> phaseMod m dphase-modulates the operator (DX-style PM chaining —FmOperator.phase_mod, depth in cycles). A baresine (freq pitch) |> phaseMod m dis accepted too: the lowerer promotes the oscillator tofmOp (freq pitch) (const 0), which is the same oscillator — the operator evaluatessin(τ·(φ + pm) + k·…)withk = 0, andOsc(Sine)issin(τ·φ)— so the promotion is sample-exact and only fires for a plain sine (one carrying|> syncor|> softenkeeps thoseOsc-only fields and is refused).saw/square/triangle/pulse/as3340have no phase input; the diagnostic names thefmOp (freq pitch) (const 0)recipe rather than leaving you to find it.- Explicit note-on phase.
fmOp … |> phase pstarts the carrier atpcycles on every note. Without the modifier an operator retains its existing phase-zero start, preserving intentional algorithm phase relationships. - Carrier with no pitch is legal.
sine (fm m 200Hz)(anfmterm as the whole freq port, nofreq pitch) routes the modulation as a 0 Hz carrier — well-defined, not a bug. - Through-zero FM. A modulator deep enough to push the instantaneous
frequencybelow 0 runs the oscillator phase backwards through the crossing — the waveform reverses direction, the classic through-zero-FM ("TZFM") clang. Increase FM depth to explore this change in tone as the instantaneous frequency crosses zero. - Feedback FM is
fmOp(shipped). Self-feedback only — an operator feeding another operator's feedback path is not modelled; nestfmfor multi- operator stacks instead. - There is no fixed menu of DX-style operator algorithms. Build the routing explicitly from operators. Saw, square, triangle, pulse and AS3340 sources do not accept
phaseMod; use a wavetable or sine operator for that input.