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:
| Index | Sound Character |
|---|---|
| 0 | Pure sine (no modulation) |
| 1-2 | Warm, mellow |
| 3-5 | Bright, electric piano-like |
| 6+ | Harsh, metallic |
The Carrier:Modulator Ratio
The frequency ratio determines the harmonic structure:
| C:M Ratio | Result |
|---|---|
| 1:1 | Symmetric harmonics |
| 1:2 | Octave-related harmonics |
| 2:1 | Subharmonics present |
| 1:1.414 | Inharmonic (bell-like) |
| 1:3.5 | Metallic, 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
| Aspect | Subtractive | FM |
|---|---|---|
| Harmonics | Remove from rich source | Generate from sine waves |
| CPU | Filter computation | Multiple oscillators |
| Character | Warm, analog | Bright, digital |
| Control | Intuitive | Parameter-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