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.
//!
//! 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 FM input
    patch
        .connect(modulator.out("sin"), mod_depth.in_("in"))
        .unwrap();
    patch
        .connect(mod_depth.out("out"), carrier.in_("fm"))
        .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 modulation indices
    for (name, ratio, depth) in [
        ("Pure carrier (no FM)", 1.0, 0.0),
        ("Subtle FM (index ~1)", 1.0, 0.5),
        ("Medium FM (index ~3)", 1.0, 1.5),
        ("Heavy FM (index ~5)", 1.0, 2.5),
        ("Bell (1:√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());
        let ratio_mult = test_patch.add("ratio", Attenuverter::new()); // Scale modulator pitch
        let output = test_patch.add("output", StereoOutput::new());

        // Set up FM with the given parameters
        test_patch
            .connect(modulator.out("sin"), mod_depth_node.in_("in"))
            .unwrap();
        test_patch
            .connect(mod_depth_node.out("out"), carrier.in_("fm"))
            .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);

        println!("{}", name);
        println!("  C:M ratio = 1:{:.3}, mod depth = {:.1}", ratio, depth);
        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 (depth) 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