VOLTAreference

Reference / Design a sound

FM, phase and ring modulation

Design a sound · playable examples

Open in the workbench →
On this page · 4 sections

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.

Listen to this example · four bars, then the tail
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, an fmOp, or a plain sine carrier (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, triangle and pulse accept Hz-valued audio expressions directly: sine (440Hz + sine 10Hz * 110Hz). fm is a convenient way to multiply a modulator by its depth. Wavetable, fmOp and AS3340 frequency ports still require their existing explicit fm form for audio modulation.
  • Linear FM, not exact PM, for fm. Modulating .frequency is 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 via phaseMod.
  • phaseMod carriers: wavetable, fmOp, and a plain sine. fmOp … |> phaseMod m d phase-modulates the operator (DX-style PM chaining — FmOperator.phase_mod, depth in cycles). A bare sine (freq pitch) |> phaseMod m d is accepted too: the lowerer promotes the oscillator to fmOp (freq pitch) (const 0), which is the same oscillator — the operator evaluates sin(τ·(φ + pm) + k·…) with k = 0, and Osc(Sine) is sin(τ·φ) — so the promotion is sample-exact and only fires for a plain sine (one carrying |> sync or |> soften keeps those Osc-only fields and is refused). saw/square/triangle/pulse/as3340 have no phase input; the diagnostic names the fmOp (freq pitch) (const 0) recipe rather than leaving you to find it.
  • Explicit note-on phase. fmOp … |> phase p starts the carrier at p cycles 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) (an fm term as the whole freq port, no freq 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 frequency below 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; nest fm for 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.