// Sample indices address several signals at once. #![allow(clippy::needless_range_loop)] use braze_core::{cycles, envelope, Engine, Rate, Settings}; use common_dsp::Saw; use std::f32::consts::TAU; const RATE: f32 = 48000.; const TEMPO: f64 = 120.; fn engine(s: Settings) -> (Engine, usize) { let mut e = Engine::new(RATE, 2).unwrap(); e.set_settings(s).unwrap(); e.set_transport(Some(TEMPO), Some(0.)); let l = e.latency_samples() as usize; (e, l) } fn seconds(t: f32) -> usize { (t * RATE) as usize } fn render(e: &mut Engine, x: &[f32]) -> Vec<[f32; 2]> { x.iter().map(|&s| e.process_sample([s, s])).collect() } /// A saw at `hz` from 0.05 s on, with a 1 ms fade-in. fn saw(hz: f32, total: f32) -> Vec { let mut osc = Saw::new(RATE, hz); (0..seconds(total)) .map(|i| { let start = seconds(0.05); if i < start { return 0.; } 0.5 * osc.sample() * ((i - start) as f32 / seconds(0.001) as f32).min(1.) }) .collect() } /// Amplitude of harmonic `n` of `hz` over one period centred on sample `at`. fn harmonic(x: &[f32], at: usize, hz: f32, n: u32) -> f32 { let half = (RATE / hz / 2.).round() as usize; let w = TAU * hz * n as f32 / RATE; let (mut re, mut im) = (0f32, 0f32); for i in at - half..at + half { re += x[i] * (w * i as f32).cos(); im -= x[i] * (w * i as f32).sin(); } 2. * (re * re + im * im).sqrt() / (2 * half) as f32 } /// Beat position (quarter notes) of output sample `i`. fn beats(i: usize, latency: usize) -> f64 { (i as f64 - latency as f64) / RATE as f64 * TEMPO / 60. } /// Harmonics 1–4 of A1 at falling levels, a periodic note the tracker reads exactly. fn partials(t: f64) -> f64 { (1..=4) .map(|n| 0.4 / n as f64 * (std::f64::consts::TAU * 55. * n as f64 * t).sin()) .sum() } #[test] fn every_harmonic_beats_on_the_tempo_grid() { for (harmonic, rate) in [(1, Rate::Eighth), (2, Rate::Sixteenth), (3, Rate::Quarter)] { let s = Settings { harmonic, rate, depth: 100., snap: true, low_cut: 0., ..Settings::default() }; let (mut e, l) = engine(s); let x: Vec = (0..seconds(1.5)) .map(|i| partials(i as f64 / RATE as f64) as f32) .collect(); let y = render(&mut e, &x); let mut worst = 0f32; for i in seconds(0.5)..seconds(1.4) { // Cycles of the chosen harmonic, from the grid alone: peaks on every multiple // of `rate`. The copy lags the bass by (0.5 − frac(u)) periods. let u = beats(i, l) / rate.quarters() / harmonic as f64 + 0.5; let t = (i - l) as f64 / RATE as f64; let lag = (0.5 - u.fract()) / 55.; let ideal = (partials(t) + partials(t - lag)) / 2f64.sqrt(); worst = worst.max((y[i][0] - ideal as f32).abs()); } assert!(worst < 3e-3, "harmonic {harmonic}, {rate:?}: {worst}"); } } #[test] fn envelope_peaks_on_the_grid_and_harmonic_n_beats_n_over_k_as_fast() { let s = Settings { harmonic: 2, ..Settings::default() }; // At cycle 0 every harmonic peaks: twice the bass, less the loudness correction. for n in 1..=8 { assert!((envelope(&s, n, 0., 1.) - 2f32.sqrt()).abs() < 1e-5); } // Harmonic 2 nulls half a cycle later; harmonic 1, beating half as fast, a cycle later. assert!(envelope(&s, 2, 0.5, 1.) < 1e-5); assert!(envelope(&s, 1, 1., 1.) < 1e-5); assert!(envelope(&s, 4, 0.25, 1.) < 1e-5); } #[test] fn the_copy_is_seamless_across_its_jumps() { // A pure sine at exactly A1: the ideal copy is a smooth sine whose phase follows the beat. let s = Settings { harmonic: 1, rate: Rate::Sixteenth, depth: 100., snap: true, low_cut: 0., ..Settings::default() }; let (mut e, l) = engine(s); let x: Vec = (0..seconds(1.5)) .map(|i| 0.5 * (TAU * 55. * i as f32 / RATE).sin()) .collect(); let y = render(&mut e, &x); let mut worst = 0f32; for i in seconds(0.5)..seconds(1.4) { let u = cycles(&s, beats(i, l), 0) + 0.5; let dry = x[i - l]; let phase = TAU as f64 * (55. * (i - l) as f64 / RATE as f64 + u.fract()); let copy = -0.5 * phase.sin() as f32; let ideal = (dry + copy) / 2f32.sqrt(); worst = worst.max((y[i][0] - ideal).abs()); } assert!(worst < 2e-3, "{worst}"); } #[test] fn depth_zero_is_the_delayed_input() { let (mut e, l) = engine(Settings { depth: 0., low_cut: 0., ..Settings::default() }); let x = saw(55., 0.6); let y = render(&mut e, &x); for i in l..x.len() { assert_eq!(y[i][0], x[i - l]); } } #[test] fn width_moves_the_right_channel_half_a_cycle() { let s = Settings { harmonic: 1, width: 100., depth: 100., rate: Rate::Quarter, low_cut: 0., ..Settings::default() }; let (mut e, l) = engine(s); let x = saw(55., 1.5); let y = render(&mut e, &x); let left: Vec = y.iter().map(|v| v[0]).collect(); let right: Vec = y.iter().map(|v| v[1]).collect(); // On the beat the left fundamental peaks and the right one nulls; half a beat later // they swap. for (beat, loud, quiet) in [ (1., &left, &right), (1.5, &right, &left), (2., &left, &right), ] { let at = l + (beat / (TEMPO / 60.) * RATE as f64) as usize; let (a, b) = (harmonic(loud, at, 55., 1), harmonic(quiet, at, 55., 1)); assert!(20. * (a / b).log10() > 20., "beat {beat}: {a} vs {b}"); } } #[test] fn note_changes_and_transport_jumps_do_not_click() { let s = Settings { depth: 100., ..Settings::default() }; let (mut e, _) = engine(s); // Phase-continuous sine from A1 to D2, no gap. let mut phase = 0f32; let x: Vec = (0..seconds(1.2)) .map(|i| { let hz = if i < seconds(0.6) { 55. } else { 73.42 }; phase = (phase + hz / RATE).fract(); 0.5 * (TAU * phase).sin() }) .collect(); let mut y = Vec::new(); for (i, &v) in x.iter().enumerate() { if i == seconds(0.9) { // A locate to another bar. e.set_transport(Some(TEMPO), Some(37.3)); } y.push(e.process_sample([v, v])[0]); } // Two sines of peak 0.5 at up to 79 Hz move at most this much per sample (after the // loudness correction), plus some room for the fades. let bound = 2. * 0.5 * TAU * 79. / RATE / 2f32.sqrt() * 1.5; for i in seconds(0.2)..y.len() - 1 { assert!( (y[i + 1] - y[i]).abs() < bound, "{i}: {}", (y[i + 1] - y[i]).abs() ); } } #[test] fn low_cut_keeps_the_sub_steady_and_the_beat_on_the_grid() { let s = Settings { harmonic: 1, depth: 100., rate: Rate::Half, low_cut: 150., ..Settings::default() }; let (mut e, l) = engine(s); let x = saw(55., 2.4); let y: Vec = render(&mut e, &x).iter().map(|v| v[0]).collect(); // One cycle (a half note, 1 s at 120 BPM): the fundamental, under the cut, does not beat. let sub: Vec = (0..50) .map(|k| harmonic(&y, seconds(1.) + k * seconds(0.02), 55., 1)) .collect(); let (lo, hi) = sub .iter() .fold((f32::MAX, 0f32), |(a, b), &v| (a.min(v), b.max(v))); // Without the cut it would null fully; what remains is leakage of the beating // harmonics into the one-period window. assert!(20. * (hi / lo).log10() < 1.5, "{lo} .. {hi}"); // And it keeps its level: the loudness correction for the copy applies above the cut. let dry = harmonic(&x, seconds(1.) - l, 55., 1); let mean = sub.iter().sum::() / sub.len() as f32; assert!((20. * (mean / dry).log10()).abs() < 0.5, "{mean} vs {dry}"); // Harmonic 6 (330 Hz) beats 6 times per cycle: it peaks on the grid, the high-pass's // phase is matched on the bass, and nulls 1/12 cycle later. for beat in [2., 4.] { let peak = l + (beat / (TEMPO / 60.) * RATE as f64) as usize; let null = peak + (RATE / 12.) as usize; let (a, b) = (harmonic(&y, peak, 55., 6), harmonic(&y, null, 55., 6)); assert!(20. * (a / b).log10() > 15., "beat {beat}: {a} vs {b}"); } } #[test] fn the_ratio_follows_the_note() { let (mut e, _) = engine(Settings { harmonic: 1, ..Settings::default() }); for hz in [55., 110.] { let x = saw(hz, 0.5); render(&mut e, &x); let f = e.take_analyzer(); // An eighth at 120 BPM beats 4 times per second: r = 1 + 4 / f0. assert!((f.ratio - (1. + 4. / hz)).abs() < 2e-3, "{hz}: {}", f.ratio); } } #[test] fn settings_are_validated_and_serialise() { let mut e = Engine::new(RATE, 1).unwrap(); assert!(e .set_settings(Settings { harmonic: 0, ..Settings::default() }) .is_err()); let json = serde_json::to_string(&Settings::default()).unwrap(); assert_eq!( serde_json::from_str::(&json).unwrap(), Settings::default() ); assert!(Engine::new(1000., 2).is_err()); assert!(Engine::new(48000., 3).is_err()); }