use quiver::prelude::*;
fn main() {
let sample_rate = 44100.0;
let mut patch = Patch::new(sample_rate);
let carrier = patch.add("carrier", Vco::new(sample_rate));
let modulator = patch.add("modulator", Vco::new(sample_rate));
let mod_depth = patch.add("mod_depth", Attenuverter::new());
let output = patch.add("output", StereoOutput::new());
patch
.connect(modulator.out("sin"), mod_depth.in_("in"))
.unwrap();
patch
.connect(mod_depth.out("out"), carrier.in_("fm_lin"))
.unwrap();
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");
let samples_per_test = (sample_rate * 0.5) as usize;
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),
] {
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 mod_ratio_cv = test_patch.add("mod_ratio_cv", Offset::new(ratio.log2()));
let depth_cv = test_patch.add("depth_cv", Offset::new(depth * 5.0));
let output = test_patch.add("output", StereoOutput::new());
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();
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());
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;
}
}
let zcr = zero_crossings as f64 / (samples_per_test as f64 / sample_rate);
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.");
}