use super::*;
const RATE: f32 = 48_000.0;
const SKIP_SECONDS: f32 = 3.0;
const MASTER_CLAMP: f32 = 0.95;
struct Level {
clamp_hits: u32,
peak: f32,
bands_db: [f32; 3],
}
fn render(song: &SongState, held: &[GestureKind], seconds: f32) -> Level {
let mut snapshot = LiveSessionSnapshot::from_song(song);
snapshot.gestures = GestureState::default();
for kind in held {
snapshot.gestures.lanes[*kind as usize] = GestureEnvelope {
amount: 1.0,
at_seconds: 0.0,
held: true,
restored: false,
};
}
let mut engine = FluidEngine::new(
RATE,
LiveSession::new(snapshot),
no_morph(),
Arc::new(FluidTelemetry::default()),
);
engine.reseed(7);
let one_pole = |hz: f32| 1.0 - (-std::f32::consts::TAU * hz / RATE).exp();
let (a_low, a_high) = (one_pole(250.0), one_pole(2_500.0));
let (mut lp_low, mut lp_high) = (0.0_f32, 0.0_f32);
let mut energy = [0.0_f64; 3];
let (mut clamp_hits, mut peak) = (0, 0.0_f32);
for frame in 0..(seconds * RATE) as usize {
let (l, r) = engine.next_stereo();
if (frame as f32) < SKIP_SECONDS * RATE {
continue;
}
for x in [l, r] {
clamp_hits += u32::from(x.abs() >= MASTER_CLAMP - 1e-6);
peak = peak.max(x.abs());
}
let mono = (l + r) * 0.5;
lp_low += a_low * (mono - lp_low);
lp_high += a_high * (mono - lp_high);
for (band, value) in [lp_low, lp_high - lp_low, mono - lp_high]
.into_iter()
.enumerate()
{
energy[band] += f64::from(value * value);
}
}
Level {
clamp_hits,
peak,
bands_db: energy.map(|e| 10.0 * e.max(1e-12).log10() as f32),
}
}
#[test]
fn full_gesture_throw_never_reaches_the_master_clamp() {
use GestureKind::*;
let songs = decode_auto_states();
for index in [5, 9, 18] {
let level = render(&songs[index], &[Bloom, Echo, Submerge], 9.0);
assert!(
level.clamp_hits == 0 && level.peak < 0.6,
"song {index}: {} clamp hits, peak {:.3}",
level.clamp_hits,
level.peak
);
}
}
#[test]
#[ignore = "diagnostic render, prints a level table"]
fn gesture_level_probe() {
use GestureKind::*;
let cases: [(&str, &[GestureKind]); 5] = [
("Bloom", &[Bloom]),
("Submerge", &[Submerge]),
("Echo", &[Echo]),
("Lift", &[Lift]),
("Bl+Ec+Su", &[Bloom, Echo, Submerge]),
];
let mut songs: Vec<(String, SongState)> = decode_auto_states()
.into_iter()
.enumerate()
.map(|(index, song)| (format!("auto {index:2}"), song))
.collect();
for progression in 0..PROGRESSIONS.len() {
let mut controls = FluidControls::default();
controls.pad.progression = progression as f32;
songs.push((
format!("fresh {progression}"),
SongState::from_controls(controls),
));
}
for (name, song) in &songs {
let dry = render(song, &[], 14.0);
println!("{name}: dry peak {:.3}, {} hits", dry.peak, dry.clamp_hits);
for (case, held) in cases {
let wet = render(song, held, 14.0);
let delta = |band: usize| wet.bands_db[band] - dry.bands_db[band];
println!(
" {case:<8} peak {:.3} hits {:>5} | low {:+5.1} mid {:+5.1} high {:+5.1} dB",
wet.peak,
wet.clamp_hits,
delta(0),
delta(1),
delta(2)
);
}
}
}
#[test]
fn wet_gestures_let_go_cleanly_within_the_release_time() {
for kind in [GestureKind::Bloom, GestureKind::Echo] {
assert_releases_cleanly(kind);
}
}
fn assert_releases_cleanly(kind: GestureKind) {
const RELEASE_AT: f64 = 4.0;
let song = &decode_auto_states()[9];
let render_frames = |gesture: GestureState| {
let mut snapshot = LiveSessionSnapshot::from_song(song);
snapshot.gestures = gesture;
let mut engine = FluidEngine::new(
RATE,
LiveSession::new(snapshot),
no_morph(),
Arc::new(FluidTelemetry::default()),
);
engine.reseed(7);
(0..((RELEASE_AT + 0.5) * f64::from(RATE)) as usize)
.map(|_| engine.next_stereo())
.collect::<Vec<_>>()
};
let mut released = GestureState::default();
released.lanes[kind as usize] = GestureEnvelope {
amount: 1.0,
at_seconds: RELEASE_AT,
held: false,
restored: false,
};
let dry = render_frames(GestureState::default());
let wet = render_frames(released);
let release_frame = (RELEASE_AT * f64::from(RATE)) as usize;
let end_frame = release_frame + (GESTURE_RELEASE_SECONDS * f64::from(RATE)) as usize + 2;
let wash = |frame: usize| (wet[frame].0 - dry[frame].0, wet[frame].1 - dry[frame].1);
let max_step = |frames: std::ops::Range<usize>| {
frames
.map(|frame| {
let (now, before) = (wash(frame), wash(frame - 1));
(now.0 - before.0).abs().max((now.1 - before.1).abs())
})
.fold(0.0_f32, f32::max)
};
let held_step = max_step(release_frame - 48_000..release_frame);
let release_step = max_step(release_frame..end_frame);
assert!(
release_step <= held_step,
"{} release stepped {release_step} against {held_step} while held",
kind.name()
);
let lingering = (end_frame..wet.len()).find(|frame| wet[*frame] != dry[*frame]);
assert_eq!(lingering, None, "{} outlived its release", kind.name());
}