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.
See the sidebands appear as you scrub ratio and index in Sidebands from Nothing.
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.");
}
Run it with cargo run --example tutorial_fm.
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