const RAND_SCALE: f32 = 1.0 / 32768.0;
const WINDOW_HALF_WIDTH: f32 = 0.5;
const SMOOTHER_HALF_LIFE: f32 = 0.15;
const BASS_SATURATION: f32 = 5.0;
const LATCH_PHASES: [[&[f32]; 3]; 3] = [
[&[0.0], &[2.0], &[1.0, 3.0]],
[&[2.0], &[1.0], &[0.0]],
[&[1.0], &[0.0, 2.0], &[3.0]],
];
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Mxd3q {
pub qxa: [f32; 4],
pub qxb: [f32; 4],
pub qxc: [f32; 4],
pub qxd: [f32; 4],
pub qsa: [f32; 4],
pub qsb: [f32; 4],
pub qsc: [f32; 4],
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Mxd3qAudio {
pub bass: f32,
pub mid: f32,
pub treb: f32,
pub bass_att: f32,
pub mid_att: f32,
pub treb_att: f32,
}
#[derive(Clone, Copy, Debug, Default)]
struct Detector {
last_beat: f32,
phase: f32,
}
impl Detector {
fn fire(&mut self, triggered: bool, elapsed: f32, delay: f32, modulus: f32) -> bool {
let fired = triggered && elapsed - self.last_beat > delay;
if fired {
self.last_beat = elapsed;
}
self.phase = (f32::from(fired) + self.phase) % modulus;
fired
}
}
#[derive(Clone, Debug, Default)]
pub struct Mxd3qBlock {
rng: u64,
bass_acc: f32,
energy: f32,
beat_timer: f32,
rands: [f32; 6],
env: [f32; 2],
treb_avg: f32,
mid_avg: f32,
detectors: [Detector; 3],
pulses: [f32; 3],
latches: [f32; 9],
kick: f32,
drift_step: f32,
drift: f32,
last: Mxd3q,
}
impl Mxd3qBlock {
fn next_rand(&mut self) -> f32 {
self.rng = self.rng.wrapping_mul(2_147_013).wrapping_add(2_531_011);
((self.rng >> 17) & 0x7fff) as f32
}
fn next_signed(&mut self) -> f32 {
let negative = self.next_rand() * RAND_SCALE <= 0.5;
let v = self.next_rand();
if negative { -v } else { v }
}
pub fn step(&mut self, audio: &Mxd3qAudio, fps: f32, elapsed: f32, q64: f32) -> Mxd3q {
let (bass, mid, treb) = (audio.bass, audio.mid, audio.treb);
let (bass_att, mid_att, treb_att) = (audio.bass_att, audio.mid_att, audio.treb_att);
let dt = if fps > 0.0 { 1.0 / fps } else { 0.0 };
let asum = mid_att + bass_att + treb_att;
let ssum = mid + bass + treb;
let e = asum * 0.2;
self.energy += e * e * 0.1;
let qx5 = self.energy * 0.4 + elapsed * 0.05;
if fps > 0.0 {
self.bass_acc += bass / fps;
}
let timer = if self.beat_timer >= 0.0 {
self.beat_timer - 0.0166
} else {
0.0
};
self.beat_timer = timer;
if ssum > 4.25 && timer < 0.0 {
self.beat_timer = 0.25;
}
if self.beat_timer > 0.24 {
self.rands[0] = self.next_rand() % 6.0 + 8.0;
self.rands[1] = self.next_rand() % 6.0 + 3.0;
self.rands[2] = self.next_rand() % 10.0;
self.rands[3] = self.next_rand() / 32767.0;
self.rands[4] = self.next_rand() / 32767.0;
self.rands[5] = self.next_rand() / 32767.0;
self.kick = self.next_signed() * RAND_SCALE * 10.0;
self.drift_step = self.next_signed() * RAND_SCALE * 0.05;
}
self.drift += self.drift_step;
self.env[0] = asum * 0.7 / 3.0 + self.env[0] * 0.3;
self.env[1] = self.env[0] * 0.3 + self.env[1] * 0.7;
self.treb_avg = self.treb_avg * 0.95 + treb_att * 0.05;
self.mid_avg = mid_att * 0.05 + self.mid_avg * 0.95;
let fired = [
self.detectors[0].fire(self.env[0] > self.env[1] * 1.15, elapsed, q64.max(0.0), 4.0),
self.detectors[1].fire(treb_att > self.treb_avg * 1.2, elapsed, 0.15, 3.0),
self.detectors[2].fire(mid_att > self.mid_avg * 1.2, elapsed, 0.2, 4.0),
];
let resets: [[bool; 3]; 3] = std::array::from_fn(|d| {
let phase = self.detectors[d].phase;
std::array::from_fn(|k| {
fired[d]
&& LATCH_PHASES[d][k]
.iter()
.any(|&p| (phase - p).abs() < WINDOW_HALF_WIDTH)
})
});
let decay = 0.5_f32.powf(dt / SMOOTHER_HALF_LIFE);
let hot = f32::from(bass_att > BASS_SATURATION);
for (s, &reset) in self.pulses.iter_mut().zip(&resets[0]) {
*s = (decay * *s).max(f32::from(reset)).max(hot);
}
for (latch, &reset) in self.latches.iter_mut().zip(resets.iter().flatten()) {
*latch = if reset { 0.0 } else { *latch + dt };
}
let l = &self.latches;
let out = Mxd3q {
qxa: [1.0, 2.0, 3.0, 27.0],
qxb: [qx5, self.bass_acc, self.rands[0], self.rands[1]],
qxc: [self.rands[2], self.rands[3], self.rands[4], self.rands[5]],
qxd: [self.kick, self.drift, 0.0, 0.0],
qsa: [self.pulses[0], self.pulses[1], self.pulses[2], l[0]],
qsb: [l[1], l[2], l[3], l[4]],
qsc: [l[5], l[6], l[7], l[8]],
};
self.last = out;
out
}
pub fn last(&self) -> Mxd3q {
self.last
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32, eps: f32) {
assert!((a - b).abs() <= eps, "{a} !~ {b}");
}
fn silence() -> Mxd3qAudio {
Mxd3qAudio {
bass: 1.0,
mid: 1.0,
treb: 1.0,
bass_att: 1.0,
mid_att: 1.0,
treb_att: 1.0,
}
}
fn latches(q: &Mxd3q) -> [f32; 9] {
let [a, b, c] = [q.qsa, q.qsb, q.qsc];
[a[3], b[0], b[1], b[2], b[3], c[0], c[1], c[2], c[3]]
}
fn silent_run(fps: f32, first: f32, frames: usize) -> (Mxd3qBlock, Mxd3q) {
let mut b = Mxd3qBlock::default();
let mut q = Mxd3q::default();
for f in 0..frames {
q = b.step(&silence(), fps, first + f as f32 / fps, 0.0);
}
(b, q)
}
#[test]
fn constants_are_fixed() {
let mut b = Mxd3qBlock::default();
let q = b.step(&silence(), 60.0, 1.0 / 60.0, 0.0);
assert_eq!(q.qxa, [1.0, 2.0, 3.0, 27.0]);
}
#[test]
fn zero_fps_keeps_the_accumulators_finite() {
let mut b = Mxd3qBlock::default();
for f in 0..10 {
let q = b.step(&silence(), 0.0, f as f32, 0.0);
assert!(q.qxb.iter().chain(&q.qsb).all(|v| v.is_finite()), "{q:?}");
assert_eq!(q.qxb[1], 0.0);
assert_eq!(latches(&q), [0.0; 9]);
}
}
#[test]
fn a_loud_frame_draws_on_the_frame_it_arms_the_timer() {
let loud = Mxd3qAudio {
bass: 2.0,
mid: 2.0,
treb: 2.0,
..silence()
};
let q = Mxd3qBlock::default().step(&loud, 60.0, 1.0 / 60.0, 0.0);
assert!((8.0..14.0).contains(&q.qxb[2]), "{:?}", q.qxb);
assert!((3.0..9.0).contains(&q.qxb[3]), "{:?}", q.qxb);
}
#[test]
fn the_timer_dips_below_zero_so_a_loud_frame_arms_every_other_frame_at_rest() {
let loud = Mxd3qAudio {
bass: 2.0,
mid: 2.0,
treb: 2.0,
..silence()
};
let mut b = Mxd3qBlock::default();
b.step(&silence(), 60.0, 0.0, 0.0); let q = b.step(&loud, 60.0, 1.0 / 60.0, 0.0); assert_eq!(q.qxb[2..], [0.0, 0.0], "armed too early: {:?}", q.qxb);
let q = b.step(&loud, 60.0, 2.0 / 60.0, 0.0); assert!((8.0..14.0).contains(&q.qxb[2]), "{:?}", q.qxb);
}
#[test]
fn the_signed_draws_are_held_between_beats_and_the_second_one_accumulates() {
let (_, q) = silent_run(60.0, 1.0, 50);
assert_eq!(q.qxd, [0.0; 4]);
let loud = Mxd3qAudio {
bass: 2.0,
mid: 2.0,
treb: 2.0,
..silence()
};
let mut b = Mxd3qBlock::default();
let armed = b.step(&loud, 60.0, 1.0 / 60.0, 0.0).qxd;
assert!(armed[0] != 0.0 && armed[0].abs() < 10.0, "{armed:?}");
let (mut prev, mut steps) = (armed[1], vec![]);
for f in 2..6 {
let q = b.step(&silence(), 60.0, f as f32 / 60.0, 0.0);
assert_eq!(q.qxd[0], armed[0], "the first draw is held");
assert_eq!(q.qxd[2..], [0.0, 0.0], "xd.zw are never written");
steps.push(q.qxd[1] - prev);
prev = q.qxd[1];
}
assert!(steps[0] != 0.0 && steps.iter().all(|&s| (s - steps[0]).abs() < 1e-6));
}
#[test]
fn detector_one_cascades_through_the_start_up_transient() {
let dt = 1.0 / 60.0;
let (mut b, q) = silent_run(60.0, dt, 6);
let l = latches(&q);
approx(l[0], 2.0 * dt, 1e-6); approx(l[1], 0.0, 0.0); approx(l[2], dt, 1e-6); for &v in &l[3..] {
approx(v, 6.0 * dt, 1e-6); }
let decay = 0.5_f32.powf(dt / 0.15);
approx(q.qsa[0], decay * decay, 1e-6);
approx(q.qsa[1], 1.0, 0.0);
approx(q.qsa[2], decay, 1e-6);
let q = b.step(&silence(), 60.0, 7.0 * dt, 0.0);
approx(latches(&q)[1], dt, 1e-6);
approx(latches(&q)[0], 3.0 * dt, 1e-6);
}
#[test]
fn q64_is_the_first_detectors_minimum_delay() {
let dt = 1.0 / 60.0;
let run = |q64: f32| {
let mut b = Mxd3qBlock::default();
let mut q = Mxd3q::default();
for f in 1..=10 {
q = b.step(&silence(), 60.0, f as f32 * dt, q64);
}
latches(&q)
};
for v in &run(1.0)[..3] {
approx(*v, 10.0 * dt, 1e-6); }
assert_eq!(run(-5.0), run(0.0), "a negative q64 clamps to 0");
assert!(run(0.0)[..3].iter().all(|&v| v < 10.0 * dt));
}
#[test]
fn detectors_two_and_three_walk_their_latches_in_counter_order() {
let dt = 1.0 / 60.0;
let loud = Mxd3qAudio {
mid_att: 4.0,
treb_att: 4.0,
..silence()
};
let (d2, d3) = ([4, 3, 5, 4, 3], [6, 7, 8, 7, 6]);
let mut b = Mxd3qBlock::default();
let mut age = [0usize; 9]; for k in 0..5 {
let q = b.step(&loud, 60.0, 1.0 + 0.3 * k as f32, 0.0);
for (i, a) in age.iter_mut().enumerate().skip(3) {
*a = if i == d2[k] || i == d3[k] { 0 } else { *a + 1 };
}
for (i, v) in latches(&q).into_iter().enumerate().skip(3) {
approx(v, age[i] as f32 * dt, 1e-5);
}
}
}
#[test]
fn a_hot_bass_att_saturates_the_pulse_smoothers_that_then_decay_with_a_150ms_half_life() {
let dt = 1.0 / 60.0;
let mut b = Mxd3qBlock::default();
let warm = Mxd3qAudio {
bass_att: 5.0,
..silence()
};
assert_eq!(
b.step(&warm, 60.0, 0.0, 0.0).qsa[..3],
[0.0; 3],
"5 is not above 5"
);
let hot = Mxd3qAudio {
bass_att: 5.01,
..silence()
};
assert_eq!(b.step(&hot, 60.0, 0.0, 0.0).qsa[..3], [1.0; 3]);
for n in 1..=9 {
let q = b.step(&silence(), 60.0, 0.0, 0.0);
for s in &q.qsa[..3] {
approx(*s, 0.5_f32.powf(n as f32 * dt / 0.15), 1e-5);
}
if n == 9 {
approx(q.qsa[0], 0.5, 1e-5); }
}
}
#[test]
fn every_latch_adds_one_frame_period_per_frame() {
let fps = 56.34_f32;
let (mut b, q) = silent_run(fps, 0.0, 400);
let next = b.step(&silence(), fps, 400.0 / fps, 0.0);
for (a, c) in latches(&q).into_iter().zip(latches(&next)) {
approx(c - a, 1.0 / fps, 1e-5);
}
}
#[test]
fn silence_matches_the_probe() {
const TOL: f32 = 0.1;
struct Shot {
fps: f32,
frames: usize,
bass: f32,
energy: f32,
sb: [f32; 3],
sc: [f32; 3],
}
for shot in [
Shot {
fps: 56.34,
frames: 382,
bass: 6.78,
energy: 5.84,
sb: [6.71, 6.59, 6.40],
sc: [6.27, 6.53, 6.27],
},
Shot {
fps: 55.5,
frames: 484,
bass: 8.72,
energy: 7.40,
sb: [8.66, 8.60, 8.35],
sc: [8.22, 8.47, 8.28],
},
] {
let (_, q) = silent_run(shot.fps, 0.0, shot.frames);
assert_eq!(q.qxa, [1.0, 2.0, 3.0, 27.0]);
approx(q.qxb[0], shot.energy, TOL); approx(q.qxb[1], shot.bass, TOL); assert_eq!(q.qxb[2..], [0.0, 0.0]); assert_eq!(q.qxc, [0.0; 4]);
assert_eq!(q.qxd, [0.0; 4]);
for v in &q.qsa[..3] {
assert!(v.abs() < 1e-6, "the smoothers have decayed: {:?}", q.qsa);
}
for (got, want) in q.qsb[..3].iter().zip(shot.sb) {
approx(*got, want, TOL);
}
for (got, want) in q.qsc[..3].iter().zip(shot.sc) {
approx(*got, want, TOL);
}
}
for (frames, want) in [(498, [8.78, 8.22, 8.22]), (580, [10.23, 9.66, 9.66])] {
let (_, q) = silent_run(56.34, 0.06, frames);
for (got, want) in [q.qsa[3], q.qsb[3], q.qsc[3]].into_iter().zip(want) {
approx(got, want, TOL);
}
}
}
}