Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FM Synthesis Basics

Frequency Modulation (FM) synthesis creates complex timbres by modulating one oscillator’s frequency with another. It’s the technology behind the DX7 and countless digital synths.

flowchart LR
    MOD[Modulator<br/>Oscillator] -->|FM| CAR[Carrier<br/>Oscillator]
    CAR --> OUT[Output]

    style MOD fill:#f9a826,color:#000
    style CAR fill:#4a9eff,color:#fff

The Mathematics

In FM synthesis, the carrier frequency is modulated by the modulator:

$$y(t) = A \sin(2\pi f_c t + I \sin(2\pi f_m t))$$

Where:

  • $f_c$ = carrier frequency (the pitch you hear)
  • $f_m$ = modulator frequency
  • $I$ = modulation index (depth)
  • $A$ = amplitude

The modulation index controls harmonic richness:

IndexSound Character
0Pure sine (no modulation)
1-2Warm, mellow
3-5Bright, electric piano-like
6+Harsh, metallic

The Carrier:Modulator Ratio

The frequency ratio determines the harmonic structure:

C:M RatioResult
1:1Symmetric harmonics
1:2Octave-related harmonics
2:1Subharmonics present
1:1.414Inharmonic (bell-like)
1:3.5Metallic, clangorous
graph TD
    subgraph "Harmonic (Musical)"
        H1["1:1, 1:2, 2:3"]
    end
    subgraph "Inharmonic (Percussive)"
        IH["1:1.4, 1:2.7, 1:π"]
    end

Building FM in Quiver

//! Tutorial: FM Synthesis Basics
//!
//! Frequency Modulation synthesis using two oscillators.
//! The modulator's output modulates the carrier's frequency,
//! creating rich, complex timbres from simple sine waves.
//!
//! # Why FM sounds the way it does
//!
//! Modulating a carrier's instantaneous frequency with another oscillator
//! (the modulator) doesn't just add one extra pitch — it generates a whole
//! family of new frequencies called *sidebands*, symmetric around the
//! carrier: `fc ± n * fm` for every integer `n = 1, 2, 3, ...`, where `fc`
//! is the carrier frequency and `fm` is the modulator frequency. How many of
//! those sidebands carry audible energy (and how loud each one is) is
//! governed by the **modulation index**, `I = deviation / fm` — the peak
//! frequency deviation the modulator pushes the carrier through, divided by
//! the modulator's own frequency. `I = 0` is just the bare carrier; as `I`
//! grows, energy spreads into more and more sidebands (their amplitudes
//! follow Bessel functions `J_n(I)`), which is why increasing the
//! modulation index alone brightens/thickens a tone even with a fixed
//! carrier:modulator ratio.
//!
//! This example uses `fm_lin`, the VCO's *linear*, through-zero FM input,
//! specifically because linear FM is what produces the textbook sidebands
//! above — the alternative `fm` input is *exponential* (each volt is an
//! octave, good for vibrato/pitch bends) and does not follow the same
//! sideband math. The carrier:modulator frequency ratio determines whether
//! those sidebands land back on harmonics of the carrier (integer ratios,
//! e.g. 1:2, 1:3 — sounds "musical"/harmonic) or land in between them
//! (irrational/non-integer ratios, e.g. 1:1.414 — sounds bell-like or
//! metallic/inharmonic).
//!
//! Run with: cargo run --example tutorial_fm

use quiver::prelude::*;

fn main() {
    let sample_rate = 44100.0;
    let mut patch = Patch::new(sample_rate);

    // Carrier oscillator - this is what we hear
    let carrier = patch.add("carrier", Vco::new(sample_rate));

    // Modulator oscillator - this modulates the carrier's frequency
    let modulator = patch.add("modulator", Vco::new(sample_rate));

    // Modulation index control (depth of FM effect)
    let mod_depth = patch.add("mod_depth", Attenuverter::new());

    // Output
    let output = patch.add("output", StereoOutput::new());

    // FM connection: modulator -> carrier's *linear* FM input (`fm_lin`).
    // Why `fm_lin` and not `fm`: only the linear input adds a frequency
    // deviation directly (`freq += (fm_lin / 5) * base_freq`), which is what
    // produces the `fc +- n*fm` sidebands described above. The exponential
    // `fm` input multiplies frequency instead, which is the right shape for
    // vibrato but doesn't follow the same sideband formula.
    patch
        .connect(modulator.out("sin"), mod_depth.in_("in"))
        .unwrap();
    patch
        .connect(mod_depth.out("out"), carrier.in_("fm_lin"))
        .unwrap();

    // Carrier to output (using sine for pure FM demonstration)
    patch
        .connect(carrier.out("sin"), output.in_("left"))
        .unwrap();
    patch
        .connect(carrier.out("sin"), output.in_("right"))
        .unwrap();

    patch.set_output(output.id());
    patch.compile().unwrap();

    println!("=== FM Synthesis Demo ===\n");
    println!("Two oscillators: Carrier (audible) + Modulator (creates harmonics)\n");

    // Generate samples at different modulation depths
    let samples_per_test = (sample_rate * 0.5) as usize;

    // Test different (carrier:modulator ratio, modulation depth) pairs. The
    // modulation index I = depth / ratio (see the doc comment above), so the
    // printed value is computed, not guessed.
    for (name, ratio, depth) in [
        ("Pure carrier (no FM)", 1.0_f64, 0.0_f64),
        ("Subtle FM", 1.0, 0.5),
        ("Medium FM", 1.0, 1.5),
        ("Heavy FM", 1.0, 3.0),
        ("Bell (1:sqrt(2) ratio)", 1.414, 2.0),
        ("Metallic (1:3.5 ratio)", 3.5, 2.0),
    ] {
        // Reset and reconfigure
        let mut test_patch = Patch::new(sample_rate);

        let carrier = test_patch.add("carrier", Vco::new(sample_rate));
        let modulator = test_patch.add("modulator", Vco::new(sample_rate));
        let mod_depth_node = test_patch.add("mod_depth", Attenuverter::new());
        // Sets the modulator's pitch to `ratio` times the carrier's: V/Oct is
        // logarithmic (1V = 1 octave = 2x frequency), so a frequency ratio
        // becomes a voltage offset of log2(ratio).
        let mod_ratio_cv = test_patch.add("mod_ratio_cv", Offset::new(ratio.log2()));
        // Attenuverter gain = level / 5V (see Attenuverter's doc), so driving
        // `level` with `depth * 5.0` makes the attenuverter's gain equal
        // `depth` directly.
        let depth_cv = test_patch.add("depth_cv", Offset::new(depth * 5.0));
        let output = test_patch.add("output", StereoOutput::new());

        // Set up FM with the given parameters
        test_patch
            .connect(mod_ratio_cv.out("out"), modulator.in_("voct"))
            .unwrap();
        test_patch
            .connect(depth_cv.out("out"), mod_depth_node.in_("level"))
            .unwrap();
        test_patch
            .connect(modulator.out("sin"), mod_depth_node.in_("in"))
            .unwrap();
        test_patch
            .connect(mod_depth_node.out("out"), carrier.in_("fm_lin"))
            .unwrap();
        test_patch
            .connect(carrier.out("sin"), output.in_("left"))
            .unwrap();

        test_patch.set_output(output.id());
        test_patch.compile().unwrap();

        // Generate samples
        let mut peak = 0.0_f64;
        let mut zero_crossings = 0;
        let mut last_sign = 0.0_f64;

        for i in 0..samples_per_test {
            let (left, _) = test_patch.tick();
            peak = peak.max(left.abs());

            // Count zero crossings (rough measure of harmonic content)
            if i > 0 {
                let current_sign = if left >= 0.0 { 1.0 } else { -1.0 };
                if current_sign != last_sign {
                    zero_crossings += 1;
                }
                last_sign = current_sign;
            }
        }

        // Zero crossing rate indicates harmonic complexity
        let zcr = zero_crossings as f64 / (samples_per_test as f64 / sample_rate);
        // I = deviation / modulator_frequency = depth / ratio (both already
        // expressed relative to the carrier frequency).
        let modulation_index = if ratio > 0.0 { depth / ratio } else { 0.0 };

        println!("{}", name);
        println!(
            "  C:M ratio = 1:{:.3}, modulation index I = {:.2}",
            ratio, modulation_index
        );
        println!("  Peak: {:.2}V, Zero-crossing rate: {:.0} Hz", peak, zcr);
        println!();
    }

    println!("FM synthesis creates complex timbres from simple oscillators.");
    println!("The carrier:modulator ratio determines harmonic vs inharmonic sound.");
    println!("The modulation index (I = deviation/fm) controls brightness and complexity.");
}

Sideband Theory

FM creates sidebands around the carrier frequency:

$$f_{sidebands} = f_c \pm n \cdot f_m$$

Where $n = 1, 2, 3, …$

       ▲
       │    ▲
   ▲   │    │   ▲
   │   │    │   │
───┴───┴────┴───┴───
  -2fm -fm  fc  +fm +2fm

The modulation index determines how many sidebands have significant amplitude (roughly $I + 1$ sidebands on each side).

Envelope the Index

The key to expressive FM is modulating the modulation index over time:

flowchart LR
    ENV[Envelope] -->|index| FM((FM<br/>Amount))
    MOD[Modulator] --> FM
    FM --> CAR[Carrier]

A decaying envelope creates the characteristic “bright attack, mellow sustain” of electric pianos.

Classic FM Sounds

Electric Piano (DX7 Style)

Carrier:Modulator = 1:1
Index envelope: Fast attack, medium decay
Starting index: ~5
Ending index: ~1

Brass

Carrier:Modulator = 1:1
Index envelope: Slow attack
Starting index: 2
Peak index: 8

Bell

Carrier:Modulator = 1:1.414 (√2)
Index: 8-10 (constant)
Long release envelope

Bass

Carrier:Modulator = 1:2
Fast index decay
Heavy carrier filtering

FM vs Subtractive

AspectSubtractiveFM
HarmonicsRemove from rich sourceGenerate from sine waves
CPUFilter computationMultiple oscillators
CharacterWarm, analogBright, digital
ControlIntuitiveParameter-sensitive

Stacking Operators

Classic FM synths use 4-6 “operators” (oscillators) in various configurations:

graph TB
    subgraph "Algorithm 1"
        A1[Op1] --> A2[Op2]
        A2 --> OUT1[Out]
    end

    subgraph "Algorithm 2"
        B1[Op1] --> B3[Op3]
        B2[Op2] --> B3
        B3 --> OUT2[Out]
    end

    subgraph "Algorithm 3"
        C1[Op1] --> C2[Op2]
        C1 --> C3[Op3]
        C2 --> OUT3a[Out]
        C3 --> OUT3b[Out]
    end

Each algorithm creates different timbral possibilities.


Next: Polyphonic Patches