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.
//!
//! 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
| 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