use crate::dsp::{Biquad, OnePole, Rng};
use crate::engine::{self, Options};
use crate::midi::{Ev, EvKind, Song};
pub(crate) fn rms(seg: &[f32]) -> f32 {
if seg.is_empty() {
return 0.0;
}
(seg.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>() / seg.len() as f64).sqrt() as f32
}
pub(crate) fn band_rms(seg: &[f32], sr: f32, f: f32, q: f32) -> f32 {
let mut bp = Biquad::bandpass(f, q, sr);
let filtered: Vec<f32> = seg.iter().map(|&x| bp.process(x)).collect();
rms(&filtered)
}
pub(crate) fn spectral_band_rms(seg: &[f32], sr: f32, lo: f32, hi: f32) -> f32 {
assert!(lo >= 0.0 && hi > lo && hi < sr * 0.5);
let n = seg.len();
if n < 2 {
return 0.0;
}
let windowed: Vec<f32> = seg
.iter()
.enumerate()
.map(|(i, &x)| {
let window = 0.5 - 0.5 * (std::f32::consts::TAU * i as f32 / (n - 1) as f32).cos();
x * window
})
.collect();
let bin_hz = sr / n as f32;
let first = (lo / bin_hz).ceil().max(1.0) as usize;
let last = (hi / bin_hz).floor() as usize;
let energy = (first..=last)
.map(|bin| mag_at(&windowed, sr, bin as f32 * bin_hz).powi(2))
.sum::<f32>();
(0.5 * energy).sqrt()
}
pub(crate) fn spectral_centroid(seg: &[f32], sr: f32, lo: f32, hi: f32) -> f32 {
let n = seg.len().min(4096);
if n < 2 {
return 0.0;
}
let windowed: Vec<f32> = seg[..n]
.iter()
.enumerate()
.map(|(i, &x)| {
let window = 0.5 - 0.5 * (std::f32::consts::TAU * i as f32 / (n - 1) as f32).cos();
x * window
})
.collect();
let bin_hz = sr / n as f32;
let first = (lo / bin_hz).ceil().max(1.0) as usize;
let last = (hi / bin_hz).floor().min((n / 2) as f32) as usize;
let (weighted, total) = (first..=last).fold((0.0f64, 0.0f64), |(weighted, total), bin| {
let freq = bin as f32 * bin_hz;
let magnitude = mag_at(&windowed, sr, freq) as f64;
(weighted + freq as f64 * magnitude, total + magnitude)
});
if total > 0.0 {
(weighted / total) as f32
} else {
0.0
}
}
pub(crate) fn hp_rms(seg: &[f32], sr: f32, f: f32) -> f32 {
let mut hp = Biquad::highpass(f, 0.7, sr);
let filtered: Vec<f32> = seg.iter().map(|&x| hp.process(x)).collect();
rms(&filtered)
}
pub(crate) fn mag_at(seg: &[f32], sr: f32, f: f32) -> f32 {
let w = 2.0 * std::f64::consts::PI * (f as f64) / (sr as f64);
let coeff = 2.0 * w.cos();
let (mut s1, mut s2) = (0.0f64, 0.0f64);
for &x in seg {
let s0 = x as f64 + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
let power = s1 * s1 + s2 * s2 - coeff * s1 * s2;
(power.max(0.0).sqrt() / (seg.len() as f64 / 2.0)) as f32
}
pub(crate) fn centroid(seg: &[f32], sr: f32) -> f32 {
let (lo, hi, n) = (100.0f32, 12_000.0f32, 20u32);
let (mut num, mut den) = (0.0f64, 0.0f64);
for i in 0..n {
let f = lo * (hi / lo).powf(i as f32 / (n - 1) as f32);
let m = mag_at(seg, sr, f) as f64;
num += f as f64 * m;
den += m;
}
if den <= 0.0 {
0.0
} else {
(num / den) as f32
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct RenderSignature {
pub(crate) rms_db: f32,
pub(crate) centroid_hz: f32,
pub(crate) late_early_db: f32,
}
pub(crate) const SIGNATURE_RMS_TOL_DB: f32 = 0.15;
pub(crate) const SIGNATURE_CENTROID_TOL: f32 = 0.02;
pub(crate) const SIGNATURE_ENVELOPE_TOL_DB: f32 = 0.30;
fn signature_window(samples: &[f32], sr: f32, window: (f32, f32)) -> &[f32] {
let start = (window.0 * sr) as usize;
let end = (window.1 * sr) as usize;
assert!(
window.0 >= 0.0 && window.1 > window.0 && end <= samples.len(),
"signature window {:?} outside {:.3}s render",
window,
samples.len() as f32 / sr
);
&samples[start..end]
}
pub(crate) fn render_signature(
samples: &[f32],
sr: f32,
body: (f32, f32),
early: (f32, f32),
late: (f32, f32),
) -> RenderSignature {
let body = signature_window(samples, sr, body);
let early_rms = rms(signature_window(samples, sr, early)).max(1e-12);
let late_rms = rms(signature_window(samples, sr, late)).max(1e-12);
RenderSignature {
rms_db: 20.0 * rms(body).max(1e-12).log10(),
centroid_hz: centroid(body, sr),
late_early_db: 20.0 * (late_rms / early_rms).log10(),
}
}
pub(crate) fn signature_matches(got: RenderSignature, expected: RenderSignature) -> bool {
(got.rms_db - expected.rms_db).abs() <= SIGNATURE_RMS_TOL_DB
&& (got.centroid_hz / expected.centroid_hz.max(1e-12) - 1.0).abs() <= SIGNATURE_CENTROID_TOL
&& (got.late_early_db - expected.late_early_db).abs() <= SIGNATURE_ENVELOPE_TOL_DB
}
pub(crate) fn assert_render_signature(
label: &str,
got: RenderSignature,
expected: RenderSignature,
) {
assert!(
signature_matches(got, expected),
"{label} render signature drifted: got {got:?}, expected {expected:?} \
(tolerances: rms ±{SIGNATURE_RMS_TOL_DB:.2} dB, centroid ±{:.1}%, envelope ±{SIGNATURE_ENVELOPE_TOL_DB:.2} dB)",
SIGNATURE_CENTROID_TOL * 100.0
);
}
pub(crate) fn peak_locate(seg: &[f32], sr: f32, f_lo: f32, f_hi: f32) -> f32 {
let mut f = f_lo;
let (mut best_f, mut best_m) = (f_lo, 0.0f32);
while f <= f_hi {
let m = mag_at(seg, sr, f);
if m > best_m {
best_m = m;
best_f = f;
}
f *= 1.005;
}
best_f
}
pub(crate) fn pitch_hz(seg: &[f32], sr: f32) -> f32 {
let mut lp1 = OnePole::lowpass(700.0, sr);
let mut lp2 = OnePole::lowpass(700.0, sr);
let f: Vec<f32> = seg.iter().map(|&x| lp2.process(lp1.process(x))).collect();
let mut c = 0u32;
for w in f.windows(2) {
if w[0] <= 0.0 && w[1] > 0.0 {
c += 1;
}
}
c as f32 / (seg.len() as f32 / sr)
}
pub(crate) fn t60_of(seg: &[f32], sr: f32) -> f32 {
let win = (0.01 * sr) as usize;
if win == 0 || seg.len() < win * 2 {
return 0.0;
}
let env: Vec<f32> = seg.chunks(win).map(rms).collect();
let (mut pi, mut pv) = (0usize, 0.0f32);
for (i, &v) in env.iter().enumerate() {
if v > pv {
pi = i;
pv = v;
}
}
let target = pv * 10f32.powf(-30.0 / 20.0);
for (i, &v) in env.iter().enumerate().skip(pi + 1) {
if v <= target {
return ((i - pi) * win) as f32 / sr * 2.0;
}
}
f32::INFINITY
}
pub(crate) fn inter_corr(l: &[f32], r: &[f32]) -> f32 {
let n = l.len().min(r.len());
let (mut lr, mut ll, mut rr) = (0.0f64, 0.0f64, 0.0f64);
for i in 0..n {
lr += l[i] as f64 * r[i] as f64;
ll += l[i] as f64 * l[i] as f64;
rr += r[i] as f64 * r[i] as f64;
}
if ll <= 0.0 || rr <= 0.0 {
return 0.0;
}
(lr / (ll.sqrt() * rr.sqrt())) as f32
}
pub(crate) fn env_autocorr_peak(seg: &[f32], sr: f32, lag_lo_s: f32, lag_hi_s: f32) -> (f32, f32) {
env_autocorr_peak_detrend(seg, sr, lag_lo_s, lag_hi_s, 15.0)
}
pub(crate) const BW_TREM_PEAK_FLOOR: f32 = 0.55;
pub(crate) fn env_autocorr_peak_detrend(
seg: &[f32],
sr: f32,
lag_lo_s: f32,
lag_hi_s: f32,
detrend_hz: f32,
) -> (f32, f32) {
let mut lp = OnePole::lowpass(200.0, sr);
let env: Vec<f32> = seg.iter().map(|&x| lp.process(x.abs())).collect();
let mut slow = OnePole::lowpass(detrend_hz, sr);
let d: Vec<f64> = env.iter().map(|&x| (x - slow.process(x)) as f64).collect();
let zero: f64 = d.iter().map(|&x| x * x).sum();
if zero <= 0.0 {
return (0.0, 0.0);
}
let lag_lo = ((lag_lo_s * sr) as usize).max(1);
let lag_hi = ((lag_hi_s * sr) as usize).min(d.len().saturating_sub(1));
let (mut best, mut best_lag) = (f64::MIN, lag_lo);
for lag in lag_lo..=lag_hi {
let c: f64 = (0..d.len() - lag).map(|i| d[i] * d[i + lag]).sum::<f64>() / zero;
if c > best {
best = c;
best_lag = lag;
}
}
(best as f32, sr / best_lag as f32)
}
pub(crate) fn flatness(seg: &[f32], sr: f32, lo: f32, hi: f32) -> f32 {
let n = 24u32;
let (mut logs, mut sum) = (0.0f64, 0.0f64);
for i in 0..n {
let f = lo * (hi / lo).powf(i as f32 / (n - 1) as f32);
let m = (mag_at(seg, sr, f) as f64).max(1e-12);
logs += m.ln();
sum += m;
}
((logs / n as f64).exp() / (sum / n as f64)) as f32
}
pub(crate) fn kurtosis(seg: &[f32]) -> f32 {
let n = seg.len();
if n < 2 {
return 0.0;
}
let mean = seg.iter().map(|&x| x as f64).sum::<f64>() / n as f64;
let (mut m2, mut m4) = (0.0f64, 0.0f64);
for &x in seg {
let d = x as f64 - mean;
let d2 = d * d;
m2 += d2;
m4 += d2 * d2;
}
m2 /= n as f64;
m4 /= n as f64;
if m2 <= 0.0 {
return 0.0;
}
(m4 / (m2 * m2)) as f32
}
pub(crate) fn traj(seg: &[f32], sr: f32, lo: f32, hi: f32) -> Vec<f32> {
let win = (0.025 * sr) as usize;
let hop = (0.005 * sr) as usize;
if win == 0 || hop == 0 || seg.len() < win {
return Vec::new();
}
let mut e = Vec::new();
let mut i = 0;
while i + win <= seg.len() {
e.push(spectral_band_rms(&seg[i..i + win], sr, lo, hi));
i += hop;
}
(0..e.len())
.map(|k| {
let a = e[k.saturating_sub(1)];
let c = e[(k + 1).min(e.len() - 1)];
(a + e[k] + c) / 3.0
})
.collect()
}
pub(crate) fn traj_peak_time_s(tr: &[f32]) -> f32 {
if tr.is_empty() {
return 0.0;
}
let max = tr.iter().cloned().fold(0.0f32, f32::max);
let thr = 0.90 * max;
for (k, &v) in tr.iter().enumerate() {
if v >= thr {
return k as f32 * 0.005 + 0.0125;
}
}
0.0
}
pub(crate) fn mean_period_samples(seg: &[f32], sr: f32, f_expect: f32) -> f32 {
let mut b1 = Biquad::bandpass(f_expect, 4.0, sr);
let mut b2 = Biquad::bandpass(f_expect, 4.0, sr);
let f: Vec<f32> = seg.iter().map(|&x| b2.process(b1.process(x))).collect();
let skip = ((0.1 * sr) as usize).min(f.len() / 4);
let mut first = None;
let mut last = None;
let mut count = 0u32;
for i in skip..f.len().saturating_sub(1) {
if f[i] <= 0.0 && f[i + 1] > 0.0 {
let t = i as f64 + (f[i] / (f[i] - f[i + 1])) as f64;
if first.is_none() {
first = Some(t);
}
last = Some(t);
count += 1;
}
}
match (first, last) {
(Some(a), Some(b)) if count >= 2 => ((b - a) / (count - 1) as f64) as f32,
_ => 0.0,
}
}
pub(crate) fn fm_mod_rate(seg: &[f32], sr: f32, f0: f32, rate_lo: f32, rate_hi: f32) -> (f32, f32) {
let mut b1 = Biquad::bandpass(f0, 6.0, sr);
let mut b2 = Biquad::bandpass(f0, 6.0, sr);
let f: Vec<f32> = seg.iter().map(|&x| b2.process(b1.process(x))).collect();
let skip = ((0.15 * sr) as usize).min(f.len() / 4);
let mut times: Vec<f64> = Vec::new();
for i in skip..f.len().saturating_sub(1) {
if f[i] <= 0.0 && f[i + 1] > 0.0 {
times.push(i as f64 + (f[i] / (f[i] - f[i + 1])) as f64);
}
}
if times.len() < 32 {
return (0.0, 0.0);
}
let raw: Vec<f64> = times
.windows(2)
.map(|w| sr as f64 / (w[1] - w[0]))
.collect();
let cyc_sr = (raw.len() as f64 * sr as f64 / (times[times.len() - 1] - times[0])) as f32;
let sm = ((cyc_sr / (3.0 * rate_hi)) as usize).max(1);
let freqs: Vec<f64> = (0..raw.len())
.map(|i| {
let a = i.saturating_sub(sm);
let b = (i + sm + 1).min(raw.len());
raw[a..b].iter().sum::<f64>() / (b - a) as f64
})
.collect();
let half = ((0.2 * cyc_sr) as usize).max(1);
let d: Vec<f64> = (0..freqs.len())
.map(|i| {
let a = i.saturating_sub(half);
let b = (i + half + 1).min(freqs.len());
let mean = freqs[a..b].iter().sum::<f64>() / (b - a) as f64;
freqs[i] - mean
})
.collect();
let zero: f64 = d.iter().map(|&x| x * x).sum();
if zero <= 0.0 {
return (0.0, 0.0);
}
let lag_lo = ((cyc_sr / rate_hi) as usize).max(1);
let lag_hi = ((cyc_sr / rate_lo) as usize).min(d.len().saturating_sub(1));
if lag_hi <= lag_lo {
return (0.0, 0.0);
}
let mut corr = Vec::with_capacity(lag_hi - lag_lo + 1);
let mut best = f64::MIN;
let mut best_i = 0usize;
for lag in lag_lo..=lag_hi {
let c: f64 = (0..d.len() - lag).map(|i| d[i] * d[i + lag]).sum::<f64>() / zero;
corr.push(c);
if c > best {
best = c;
best_i = lag - lag_lo;
}
}
let mut i = best_i;
for j in 1..corr.len().saturating_sub(1) {
if corr[j] >= corr[j - 1] && corr[j] >= corr[j + 1] && corr[j] >= 0.85 * best {
i = j;
break;
}
}
let lag_f = if i >= 1 && i + 1 < corr.len() {
let (a, b, c) = (corr[i - 1], corr[i], corr[i + 1]);
let denom = a - 2.0 * b + c;
let delta = if denom.abs() > 1e-12 {
(0.5 * (a - c) / denom).clamp(-1.0, 1.0)
} else {
0.0
};
(lag_lo + i) as f64 + delta
} else {
(lag_lo + i) as f64
};
(best.max(0.0) as f32, (cyc_sr as f64 / lag_f) as f32)
}
fn fft_inplace(re: &mut [f32], im: &mut [f32]) {
let n = re.len();
debug_assert_eq!(n, im.len());
debug_assert!(n.is_power_of_two());
let mut j = 0usize;
for i in 1..n {
let mut bit = n >> 1;
while j & bit != 0 {
j ^= bit;
bit >>= 1;
}
j |= bit;
if i < j {
re.swap(i, j);
im.swap(i, j);
}
}
let mut len = 2usize;
while len <= n {
let ang = -std::f64::consts::TAU / len as f64;
let (wr_step, wi_step) = (ang.cos(), ang.sin());
let half = len / 2;
let mut base = 0usize;
while base < n {
let (mut wr, mut wi) = (1.0f64, 0.0f64);
for k in 0..half {
let a = base + k;
let b = a + half;
let tr = wr as f32 * re[b] - wi as f32 * im[b];
let ti = wr as f32 * im[b] + wi as f32 * re[b];
re[b] = re[a] - tr;
im[b] = im[a] - ti;
re[a] += tr;
im[a] += ti;
let nwr = wr * wr_step - wi * wi_step;
wi = wr * wi_step + wi * wr_step;
wr = nwr;
}
base += len;
}
len <<= 1;
}
}
pub(crate) fn fft_mag_hann(seg: &[f32], n: usize) -> Vec<f32> {
debug_assert!(n.is_power_of_two());
let mut re = vec![0f32; n];
let mut im = vec![0f32; n];
let m = seg.len().min(n);
for (i, slot) in re.iter_mut().enumerate().take(m) {
let w = 0.5 - 0.5 * (std::f32::consts::TAU * i as f32 / (n - 1) as f32).cos();
*slot = seg[i] * w;
}
fft_inplace(&mut re, &mut im);
let norm = 4.0 / n as f32; (0..n / 2)
.map(|b| (re[b] * re[b] + im[b] * im[b]).sqrt() * norm)
.collect()
}
pub(crate) fn spectral_tilt_db_oct(seg: &[f32], sr: f32, lo: f32, hi: f32) -> f32 {
let n = 4096usize;
let mag = fft_mag_hann(seg, n);
let bin_hz = sr / n as f32;
let steps = 48u32;
let (mut xs, mut ys) = (Vec::new(), Vec::new());
for i in 0..steps {
let f = lo * (hi / lo).powf(i as f32 / (steps - 1) as f32);
let bin = (f / bin_hz).round() as usize;
if bin == 0 || bin >= mag.len() {
continue;
}
xs.push(f.log2());
ys.push(20.0 * mag[bin].max(1e-9).log10());
}
let nn = xs.len() as f32;
if nn < 2.0 {
return 0.0;
}
let sx: f32 = xs.iter().sum();
let sy: f32 = ys.iter().sum();
let sxx: f32 = xs.iter().map(|x| x * x).sum();
let sxy: f32 = xs.iter().zip(ys.iter()).map(|(x, y)| x * y).sum();
let denom = nn * sxx - sx * sx;
if denom.abs() < 1e-9 {
0.0
} else {
(nn * sxy - sx * sy) / denom
}
}
pub(crate) fn crest(seg: &[f32]) -> f32 {
let r = rms(seg);
if r <= 1e-12 {
return 0.0;
}
seg.iter().fold(0f32, |a, &x| a.max(x.abs())) / r
}
pub(crate) fn att_sus_ratio(seg: &[f32], sr: f32, f0: f32) -> f32 {
let att_end = if f0 > 0.0 {
0.015f32.max(1.5 / f0)
} else {
0.015
};
let a_hi = ((att_end * sr) as usize).min(seg.len());
let s_lo = ((0.100 * sr) as usize).min(seg.len());
let s_hi = ((0.250 * sr) as usize).min(seg.len());
if a_hi == 0 || s_hi <= s_lo {
return 0.0;
}
let sus = rms(&seg[s_lo..s_hi]);
if sus <= 1e-12 {
return 0.0;
}
rms(&seg[..a_hi]) / sus
}
pub(crate) fn inter_harmonic_floor_db(seg: &[f32], sr: f32, f0: f32) -> f32 {
let n = 4096usize;
let mag = fft_mag_hann(seg, n);
let bin_hz = sr / n as f32;
let bin_of = |f: f32| (f / bin_hz).round() as usize;
let (mut harm, mut inter) = (Vec::new(), Vec::new());
let mut k = 1u32;
while (k as f32) * f0 < 0.45 * sr && k <= 16 {
let hb = bin_of(k as f32 * f0);
if hb == 0 || hb >= mag.len() {
break;
}
let lo = hb.saturating_sub(2);
let hi = (hb + 2).min(mag.len() - 1);
harm.push((lo..=hi).map(|b| mag[b]).fold(0f32, f32::max));
let ib = bin_of((k as f32 + 0.5) * f0);
if ib < mag.len() {
inter.push(mag[ib]);
}
k += 1;
}
if harm.is_empty() || inter.is_empty() {
return -120.0;
}
let median = |v: &mut Vec<f32>| {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v[v.len() / 2]
};
let h = median(&mut harm).max(1e-9);
let ih = median(&mut inter).max(1e-9);
20.0 * (ih / h).log10()
}
pub(crate) fn max_slew_norm(seg: &[f32]) -> f32 {
let peak = seg.iter().fold(0f32, |a, &x| a.max(x.abs())).max(1e-9);
seg.windows(2).fold(0f32, |a, w| a.max((w[1] - w[0]).abs())) / peak
}
pub(crate) fn dc_offset(seg: &[f32]) -> f32 {
if seg.is_empty() {
return 0.0;
}
(seg.iter().map(|&x| x as f64).sum::<f64>() / seg.len() as f64) as f32
}
pub(crate) fn reference_song() -> Song {
let mut ev: Vec<(f64, EvKind)> = Vec::new();
let progs: &[(u8, u8)] = &[
(0, 24), (1, 25), (2, 26), (3, 28), (4, 30), (5, 33), (6, 35), (7, 0), (8, 48), ];
for &(ch, prog) in progs {
ev.push((0.0, EvKind::Prog { ch, prog }));
}
let mut note = |ch: u8, t: f64, key: u8, vel: u8, dur: f64| {
ev.push((t, EvKind::NoteOn { ch, key, vel }));
ev.push((t + dur, EvKind::NoteOff { ch, key }));
};
note(0, 0.00, 52, 60, 0.6);
note(0, 0.70, 57, 90, 0.6);
note(0, 1.40, 64, 110, 0.9);
note(1, 0.10, 48, 40, 0.6);
note(1, 0.80, 55, 90, 0.6);
note(1, 1.50, 64, 120, 0.9);
note(2, 0.20, 55, 70, 0.5);
note(2, 0.90, 62, 100, 0.5);
note(2, 1.60, 67, 120, 0.8);
note(3, 0.00, 40, 100, 0.2);
note(3, 0.25, 40, 80, 0.2);
note(3, 0.50, 45, 110, 0.2);
note(3, 0.75, 47, 90, 0.2);
note(4, 0.30, 45, 90, 1.2);
note(4, 1.60, 52, 115, 1.2);
note(5, 0.00, 28, 70, 0.7);
note(5, 0.75, 33, 100, 0.7);
note(5, 1.50, 40, 120, 0.9);
note(6, 0.20, 31, 60, 0.8);
note(6, 1.10, 38, 95, 0.8);
note(6, 2.00, 43, 115, 0.8);
note(7, 0.00, 60, 80, 1.0);
note(7, 1.00, 64, 100, 1.0);
note(7, 2.00, 67, 110, 1.0);
note(8, 0.00, 60, 85, 2.0);
note(8, 0.50, 67, 95, 1.8);
let mut hit = |t: f64, key: u8, vel: u8| ev.push((t, EvKind::NoteOn { ch: 9, key, vel }));
for (i, &t) in [0.0, 0.5, 1.0, 1.5].iter().enumerate() {
hit(t, 36, if i % 2 == 0 { 110 } else { 92 });
}
hit(0.25, 38, 105);
hit(1.25, 38, 70);
for i in 0..8 {
hit(i as f64 * 0.25, 42, if i % 2 == 0 { 90 } else { 60 });
}
hit(1.875, 46, 100);
hit(2.00, 49, 115);
hit(2.20, 45, 100);
hit(2.35, 47, 100);
hit(2.50, 41, 100);
hit(2.75, 51, 85);
hit(3.00, 51, 95);
ev.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
Song {
events: ev.into_iter().map(|(sec, kind)| Ev { sec, kind }).collect(),
seconds: 4.0,
markers: Vec::new(),
title: String::new(),
initial_bpm: 120.0,
}
}
pub(crate) fn reference_opts(sr: f32, solo: u16) -> Options {
Options {
sr,
wet: 0.32,
tail: 1.5,
delay_s: 0.25,
samples: false,
solo,
}
}
#[allow(dead_code)] pub(crate) fn left(stereo: &[f32]) -> Vec<f32> {
stereo.iter().step_by(2).copied().collect()
}
#[allow(dead_code)] pub(crate) fn right(stereo: &[f32]) -> Vec<f32> {
stereo.iter().skip(1).step_by(2).copied().collect()
}
pub(crate) fn mono(stereo: &[f32]) -> Vec<f32> {
stereo
.chunks_exact(2)
.map(|p| 0.5 * (p[0] + p[1]))
.collect()
}
#[cfg(test)]
mod calibration {
use super::*;
fn sine(f: f32, amp: f32, sr: f32, secs: f32) -> Vec<f32> {
(0..(secs * sr) as usize)
.map(|i| amp * (std::f32::consts::TAU * f * i as f32 / sr).sin())
.collect()
}
fn noise(seed: u32, sr: f32, secs: f32) -> Vec<f32> {
let mut rng = Rng::new(seed);
(0..(secs * sr) as usize).map(|_| rng.white()).collect()
}
#[test]
fn mag_at_resolves_a_pure_tone() {
let sr = 44100.0;
let s = sine(1000.0, 1.0, sr, 1.0);
let on = mag_at(&s, sr, 1000.0);
let off = mag_at(&s, sr, 2000.0);
assert!(on > 0.8, "on-bin magnitude {on}");
assert!(off < 0.1 * on, "octave-away leakage {off} vs {on}");
}
#[test]
fn band_rms_peaks_in_band() {
let sr = 44100.0;
let s = sine(1000.0, 1.0, sr, 1.0);
let inb = band_rms(&s, sr, 1000.0, 2.0);
let out = band_rms(&s, sr, 2000.0, 2.0);
assert!(inb > 0.5, "in-band rms {inb}");
assert!(out < 0.4 * inb, "octave-away band rms {out} vs {inb}");
let hp = hp_rms(&s, sr, 4000.0);
assert!(hp < 0.1 * inb, "hp leakage {hp}");
let ranged = spectral_band_rms(&s, sr, 800.0, 1_200.0);
let excluded = spectral_band_rms(&s, sr, 2_000.0, 4_000.0);
assert!(ranged > 0.4, "range rms {ranged}");
assert!(excluded < 0.1 * ranged, "range leakage {excluded}/{ranged}");
let spectral_center = spectral_centroid(&s, sr, 100.0, 4_000.0);
assert!(
(spectral_center - 1_000.0).abs() < 20.0,
"centroid {spectral_center}"
);
}
#[test]
fn rms_and_pitch_recover_a_unit_sine() {
let sr = 44100.0;
let s = sine(440.0, 1.0, sr, 1.0);
let r = rms(&s);
assert!(
(r - std::f32::consts::FRAC_1_SQRT_2).abs() < 0.01,
"unit-sine rms {r}"
);
let p = pitch_hz(&s, sr);
assert!((p - 440.0).abs() < 5.0, "pitch {p}");
}
#[test]
fn centroid_orders_noise_above_low_sine() {
let sr = 44100.0;
let n = centroid(&noise(7, sr, 1.0), sr);
let t = centroid(&sine(200.0, 1.0, sr, 1.0), sr);
assert!(n > t * 2.0, "noise centroid {n} vs 200 Hz sine {t}");
}
#[test]
fn portable_render_signature_detects_level_spectrum_and_envelope_drift() {
let sr = 44100.0;
let mut base: Vec<f32> = (0..sr as usize)
.map(|i| {
let t = i as f32 / sr;
let env = if t < 0.5 { 1.0 } else { 0.55 };
env * (0.7 * (std::f32::consts::TAU * 440.0 * t).sin()
+ 0.3 * (std::f32::consts::TAU * 3520.0 * t).sin())
})
.collect();
let windows = ((0.05, 0.95), (0.10, 0.35), (0.65, 0.90));
let expected = render_signature(&base, sr, windows.0, windows.1, windows.2);
let gain = 10f32.powf(0.25 / 20.0);
let louder: Vec<f32> = base.iter().map(|&x| x * gain).collect();
assert!(!signature_matches(
render_signature(&louder, sr, windows.0, windows.1, windows.2),
expected
));
let mut lp = OnePole::lowpass(1200.0, sr);
let darker: Vec<f32> = base.iter().map(|&x| lp.process(x)).collect();
assert!(!signature_matches(
render_signature(&darker, sr, windows.0, windows.1, windows.2),
expected
));
for x in &mut base[(0.5 * sr) as usize..] {
*x *= 0.95;
}
assert!(!signature_matches(
render_signature(&base, sr, windows.0, windows.1, windows.2),
expected
));
}
#[test]
fn t60_of_recovers_a_known_decay() {
let sr = 44100.0;
let t60 = 0.5f32;
let s: Vec<f32> = (0..(sr as usize))
.map(|i| {
let t = i as f32 / sr;
10f32.powf(-3.0 * t / t60) * (std::f32::consts::TAU * 500.0 * t).sin()
})
.collect();
let m = t60_of(&s, sr);
assert!((m - t60).abs() < 0.1 * t60, "measured t60 {m} vs {t60}");
}
#[test]
fn inter_corr_calibrates() {
let sr = 44100.0;
let a = noise(3, sr, 0.5);
let b = noise(4, sr, 0.5);
let inv: Vec<f32> = a.iter().map(|&x| -x).collect();
assert!(inter_corr(&a, &a) > 0.99);
assert!(inter_corr(&a, &b).abs() < 0.1);
assert!(inter_corr(&a, &inv) < -0.99);
}
#[test]
fn peak_locate_finds_a_tone() {
let sr = 44100.0;
let s = sine(700.0, 1.0, sr, 0.5);
let p = peak_locate(&s, sr, 400.0, 1200.0);
assert!((p - 700.0).abs() < 8.0, "located {p}");
}
#[test]
fn env_autocorr_recovers_am_rate() {
let sr = 44100.0;
let s: Vec<f32> = (0..(sr as usize))
.map(|i| {
let t = i as f32 / sr;
(1.0 + 0.8 * (std::f32::consts::TAU * 60.0 * t).sin())
* (std::f32::consts::TAU * 6000.0 * t).sin()
})
.collect();
let (peak, rate) = env_autocorr_peak(&s, sr, 1.0 / 100.0, 1.0 / 40.0);
assert!(peak > 0.3, "autocorr peak {peak}");
assert!((rate - 60.0).abs() < 8.0, "AM rate {rate}");
}
#[test]
fn mean_period_recovers_a_known_tone() {
let sr = 44100.0;
let f = 130.8127; let want = sr / f;
let clean = sine(f, 0.5, sr, 2.0);
let p = mean_period_samples(&clean, sr, f);
assert!(
(p - want).abs() < 0.02,
"clean period {p:.4} vs {want:.4} samples"
);
let mut rng = Rng::new(3);
let noisy: Vec<f32> = clean.iter().map(|&x| x + 0.02 * rng.white()).collect();
let p = mean_period_samples(&noisy, sr, f);
assert!(
(p - want).abs() < 0.05,
"noisy period {p:.4} vs {want:.4} samples"
);
}
#[test]
fn fm_mod_rate_recovers_synthetic_vibrato() {
let sr = 44100.0;
let f0 = 130.8127f32;
let fm = |mod_hz: f32| -> Vec<f32> {
let mut rng = Rng::new(9);
let mut phase = 0.0f64;
(0..(4.0 * sr) as usize)
.map(|i| {
let t = i as f32 / sr;
let inst = f0 as f64
* (1.0 + 0.002 * (std::f64::consts::TAU * mod_hz as f64 * t as f64).sin());
phase += std::f64::consts::TAU * inst / sr as f64;
(phase.sin() * 0.5) as f32 + 0.01 * rng.white()
})
.collect()
};
let (peak, rate) = fm_mod_rate(&fm(4.8), sr, f0, 2.0, 10.0);
assert!(peak > 0.6, "true vibrato autocorr peak {peak:.3}");
assert!(
(rate - 4.8).abs() / 4.8 < 0.06,
"true vibrato rate {rate:.2} Hz"
);
let (slow_peak, slow_rate) = fm_mod_rate(&fm(0.29), sr, f0, 2.0, 10.0);
assert!(
slow_peak < 0.5 || (slow_rate - 4.8).abs() / 4.8 > 0.25,
"16x-slow vibrato must not read as a named-rate detection: \
peak {slow_peak:.3} rate {slow_rate:.2} Hz"
);
}
#[test]
fn flatness_separates_noise_from_tone() {
let sr = 44100.0;
let fn_ = flatness(&noise(9, sr, 1.0), sr, 500.0, 8000.0);
let ft = flatness(&sine(1000.0, 1.0, sr, 1.0), sr, 500.0, 8000.0);
assert!(fn_ > 0.4, "noise flatness {fn_}");
assert!(ft < 0.2, "tone flatness {ft}");
assert!(fn_ > 3.0 * ft, "noise {fn_} vs tone {ft}");
}
#[test]
fn kurtosis_flags_grain_over_smooth_noise() {
let sr = 44100.0;
let raw = noise(11, sr, 0.5);
let mut bp = Biquad::bandpass(4000.0, 3.0, sr);
let smooth: Vec<f32> = raw.iter().map(|&x| bp.process(x)).collect();
let k_smooth = kurtosis(&smooth);
let mut rng = Rng::new(5);
let grain: Vec<f32> = (0..(0.5 * sr) as usize)
.map(|_| {
let u = rng.white();
if u.abs() > 0.95 {
u.signum()
} else {
0.0
}
})
.collect();
let k_grain = kurtosis(&grain);
assert!(
(k_smooth - 3.0).abs() < 1.5,
"smooth-noise kurtosis {k_smooth} (expect ≈ 3)"
);
assert!(
k_grain > 3.0 * k_smooth,
"grain kurtosis {k_grain} not ≫ smooth {k_smooth}"
);
}
#[test]
fn bloom_trajectory_machinery_calibrates() {
let sr = 44100.0;
let syn = |bloom: bool| -> Vec<f32> {
let mut rng = Rng::new(11);
(0..(0.3 * sr) as usize)
.map(|i| {
let t = i as f32 / sr;
let low = (-t / 0.20).exp() * (std::f32::consts::TAU * 900.0 * t).sin();
let high_env = if bloom {
let x = t / 0.051;
x * x * (2.0 * (1.0 - x)).exp()
} else {
(-t / 0.06).exp() };
let high = high_env
* 0.5
* ((std::f32::consts::TAU * 7000.0 * t).sin()
+ (std::f32::consts::TAU * 9100.0 * t).sin());
low + high + 0.01 * rng.white()
})
.collect()
};
let a = syn(true);
let b = syn(false);
let low_a = traj_peak_time_s(&traj(&a, sr, 700.0, 1200.0));
let high_a = traj_peak_time_s(&traj(&a, sr, 6000.0, 11000.0));
let high_b = traj_peak_time_s(&traj(&b, sr, 6000.0, 11000.0));
println!(
"BLOOM machinery: SYN-A low={:.1}ms high={:.1}ms; SYN-B high={:.1}ms",
low_a * 1000.0,
high_a * 1000.0,
high_b * 1000.0
);
assert!(low_a <= 0.020, "SYN-A low peak too late: {low_a}");
assert!(
(0.028..=0.048).contains(&high_a),
"SYN-A high bloom time {high_a} (expect ~37 ms measured for a true 51 ms bloom)"
);
assert!(
high_a - low_a >= 0.015,
"SYN-A no bloom delay: {}",
high_a - low_a
);
assert!(high_b <= 0.020, "SYN-B false bloom: high={high_b}");
}
#[test]
fn fft_mag_hann_recovers_a_tone() {
let sr = 44100.0;
let n = 4096usize;
let bin = 100usize;
let f = bin as f32 * sr / n as f32; let s = sine(f, 1.0, sr, 0.25);
let mag = fft_mag_hann(&s, n);
let (peak_bin, peak) =
mag.iter().enumerate().fold(
(0usize, 0f32),
|(bi, bm), (i, &m)| {
if m > bm {
(i, m)
} else {
(bi, bm)
}
},
);
assert_eq!(peak_bin, bin, "peak bin {peak_bin} vs {bin}");
assert!((peak - 1.0).abs() < 0.15, "on-bin peak magnitude {peak}");
assert!(
mag[2 * bin] < 0.02 * peak,
"octave-away leakage {} vs peak {peak}",
mag[2 * bin]
);
}
#[test]
fn spectral_tilt_orders_by_slope() {
let sr = 44100.0;
let mean_tilt = |poles: u32| -> f32 {
(0..8)
.map(|seed| {
let white = noise(seed, sr, 0.2);
let mut a = OnePole::lowpass(50.0, sr);
let mut b = OnePole::lowpass(50.0, sr);
let sig: Vec<f32> = white
.iter()
.map(|&x| match poles {
0 => x,
1 => a.process(x),
_ => b.process(a.process(x)),
})
.collect();
spectral_tilt_db_oct(&sig, sr, 300.0, 9000.0)
})
.sum::<f32>()
/ 8.0
};
let (flat, t6, t12) = (mean_tilt(0), mean_tilt(1), mean_tilt(2));
assert!(flat.abs() < 2.0, "white tilt {flat}");
assert!((t6 + 6.0).abs() < 2.0, "one-pole tilt {t6} (expect ≈ −6)");
assert!(
(t12 + 12.0).abs() < 3.0,
"two-pole tilt {t12} (expect ≈ −12)"
);
assert!(t12 < t6 && t6 < flat, "ordering {t12} < {t6} < {flat}");
}
#[test]
fn crest_orders_sine_below_impulse() {
let sr = 44100.0;
let s = sine(500.0, 1.0, sr, 0.2);
let c = crest(&s);
assert!(
(c - std::f32::consts::SQRT_2).abs() < 0.05,
"sine crest {c}"
);
let mut imp = vec![0f32; (0.2 * sr) as usize];
imp[10] = 1.0;
imp[5000] = 1.0;
assert!(crest(&imp) > 10.0, "impulse crest {}", crest(&imp));
}
#[test]
fn att_sus_ratio_measures_front_load() {
let sr = 44100.0;
let f0 = 200.0;
let front: Vec<f32> = (0..(0.3 * sr) as usize)
.map(|i| {
let t = i as f32 / sr;
let a = if t < 0.015 { 2.0 } else { 1.0 };
a * (std::f32::consts::TAU * f0 * t).sin()
})
.collect();
let r = att_sus_ratio(&front, sr, f0);
assert!((r - 2.0).abs() < 0.2, "front-loaded att/sus {r}");
let flat = sine(f0, 1.0, sr, 0.3);
let rf = att_sus_ratio(&flat, sr, f0);
assert!((rf - 1.0).abs() < 0.15, "flat att/sus {rf}");
}
#[test]
fn inter_harmonic_floor_separates_tone_from_noise() {
let sr = 44100.0;
let f0 = 220.0;
let tone: Vec<f32> = (0..(0.3 * sr) as usize)
.map(|i| {
let t = i as f32 / sr;
(1..=8)
.map(|k| (1.0 / k as f32) * (std::f32::consts::TAU * k as f32 * f0 * t).sin())
.sum()
})
.collect();
let clean = inter_harmonic_floor_db(&tone, sr, f0);
let mut rng = Rng::new(5);
let noisy: Vec<f32> = tone.iter().map(|&x| x + 0.3 * rng.white()).collect();
let dirty = inter_harmonic_floor_db(&noisy, sr, f0);
assert!(clean < -25.0, "pure-tone inter-harmonic floor {clean}");
assert!(
dirty > clean + 10.0,
"noise raises the floor: {dirty} vs {clean}"
);
}
#[test]
fn slew_and_dc_calibrate() {
let sr = 44100.0;
let s = sine(1000.0, 1.0, sr, 0.1);
let sl = max_slew_norm(&s);
assert!((0.10..0.20).contains(&sl), "sine slew {sl}");
assert!(dc_offset(&s).abs() < 0.01, "zero-mean dc {}", dc_offset(&s));
let biased: Vec<f32> = s.iter().map(|&x| x + 0.25).collect();
assert!(
(dc_offset(&biased) - 0.25).abs() < 0.01,
"biased dc {}",
dc_offset(&biased)
);
}
}
#[cfg(test)]
mod guards {
use super::*;
const SR: f32 = 44100.0;
const GOLDEN: &[(u8, f32, f32)] = &[
(0, -39.19, 1079.7),
(1, -44.37, 989.4),
(2, -39.05, 604.8),
(3, -41.51, 481.6),
(4, -33.04, 811.3),
(5, -20.17, 244.7),
(6, -23.49, 180.7),
(7, -24.95, 588.8),
(8, -34.65, 2090.1),
(9, -21.92, 758.2),
];
const GOLDEN_MASTER_PEAK: f32 = 1.40295;
const RMS_TOL_DB: f32 = 2.5;
const CENTROID_TOL: f32 = 0.20;
fn solo_render(ch: u8) -> Vec<f32> {
let song = reference_song();
engine::render(&song, &reference_opts(SR, 1u16 << ch)).0
}
fn db(x: f32) -> f32 {
20.0 * x.max(1e-12).log10()
}
#[test]
fn golden_mix_balance_holds() {
assert!(
GOLDEN.len() > 1 && GOLDEN_MASTER_PEAK > 0.0,
"golden fixture not captured — run print_golden_fixture on the pre-work build"
);
for &(ch, g_rms_db, g_cent) in GOLDEN {
let out = solo_render(ch);
let m = mono(&out);
let r = db(rms(&m));
assert!(
(r - g_rms_db).abs() <= RMS_TOL_DB,
"ch {ch}: rms {r:.2} dB vs golden {g_rms_db:.2} dB"
);
let c = centroid(&m, SR);
assert!(
(c / g_cent - 1.0).abs() <= CENTROID_TOL,
"ch {ch}: centroid {c:.0} Hz vs golden {g_cent:.0} Hz"
);
}
let song = reference_song();
let (_, stats) = engine::render(&song, &reference_opts(SR, 0xFFFF));
assert!(
(db(stats.peak) - db(GOLDEN_MASTER_PEAK)).abs() <= RMS_TOL_DB,
"master peak {:.3} vs golden {GOLDEN_MASTER_PEAK:.3}",
stats.peak
);
}
#[test]
#[ignore]
fn print_golden_fixture() {
println!(" const GOLDEN: &[(u8, f32, f32)] = &[");
for ch in [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
let out = solo_render(ch);
let m = mono(&out);
println!(
" ({ch}, {:.2}, {:.1}),",
db(rms(&m)),
centroid(&m, SR)
);
}
let song = reference_song();
let (_, stats) = engine::render(&song, &reference_opts(SR, 0xFFFF));
println!(" ];");
println!(" const GOLDEN_MASTER_PEAK: f32 = {:.5};", stats.peak);
}
#[test]
fn determinism_bit_identical() {
let song = reference_song();
let a = engine::render(&song, &reference_opts(SR, 0xFFFF)).0;
let b = engine::render(&song, &reference_opts(SR, 0xFFFF)).0;
assert_eq!(a.len(), b.len());
assert!(
a.iter().zip(&b).all(|(x, y)| x.to_bits() == y.to_bits()),
"same-seed renders differ"
);
}
#[test]
fn gm_routing_pins_voice_kinds() {
let cases: &[(u8, &str)] = &[
(0, "modal"),
(6, "HARPSICHORD"), (7, "CLAVINET"),
(8, "modal"),
(16, "organ"),
(19, "organ"),
(22, "reed"), (24, "NYLON"),
(25, "STEEL"),
(26, "JAZZ"), (27, "CLEAN"),
(28, "MUTED"),
(29, "DRIVE"),
(30, "DRIVE"),
(31, "HARMONIC"), (32, "UPRIGHT"), (33, "BASS"),
(34, "PICK"), (35, "FRETLESS"),
(36, "SLAP"), (37, "SLAP_POP"), (38, "synthbass"), (39, "synthbass"), (40, "bowedstring"),
(46, "HARP"),
(47, "modal"),
(48, "sawstack"),
(52, "choir2"), (72, "wind"),
(96, "fx"),
(97, "fx"),
(98, "fx"),
(99, "fx"),
(100, "fx"),
(101, "fx"),
(102, "fx"),
(103, "fx"),
(104, "SITAR"),
(105, "BANJO"),
(106, "SHAMISEN"),
(107, "KOTO"),
(109, "reed"),
(110, "bowedstring"),
(111, "reed"),
(108, "modal"),
(112, "modal"),
(113, "modal"),
(114, "modal"),
(115, "modal"),
(116, "modal"),
(117, "modal"),
(118, "modal"),
(119, "reverse_cym"),
(120, "sfx"),
(127, "sfx"),
];
for &(prog, want) in cases {
let v = crate::voices::make(prog, 60, 100, SR, 7, false);
assert_eq!(v.kind(), want, "program {prog}");
}
let d = crate::drums::make(38, 100, SR, 7, crate::drums::Kit::V1, false, 0)
.expect("snare voice");
assert_eq!(d.kind(), "drum");
for p in 0u8..=127 {
assert_eq!(
engine::needs_drive(p),
matches!(p, 29 | 30),
"needs_drive({p})"
);
}
}
#[test]
#[ignore]
fn render_budget_advisory() {
let song = reference_song();
let started = std::time::Instant::now();
let (out, stats) = engine::render(&song, &reference_opts(SR, 0xFFFF));
let secs = started.elapsed().as_secs_f64();
println!(
"reference render: {secs:.2} s wall, {} samples, {} voices, peak {:.3}",
out.len(),
stats.voices_spawned,
stats.peak
);
assert!(
secs < 120.0,
"render blew the generous ceiling: {secs:.1} s"
);
}
}
#[cfg(test)]
mod distinctness {
use super::*;
const SR: f32 = 44100.0;
const NKEYS: usize = 2;
const PROBE_KEYS: [u8; NKEYS] = [48, 72];
const VEL: u8 = 100;
const SECS: f32 = 0.7;
const SEED: u32 = 7;
const EPS: f32 = 0.03;
const MARGIN: f32 = 1.5 * EPS;
type Feat = [f32; 5];
type FeatSet = [Feat; NKEYS];
const FLOOR: Feat = [100.0, 0.05, 0.10, 0.02, 0.05];
const ALLOW: &[(u8, u8, u8)] = &[
(29, 30, 5), ];
fn allow_reason(a: u8, b: u8) -> Option<u8> {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
ALLOW
.iter()
.find(|&&(x, y, _)| x == lo && y == hi)
.map(|&(_, _, w)| w)
}
fn render(program: u8, key: u8) -> Vec<f32> {
let mut v = crate::voices::make(program, key, VEL, SR, SEED, false);
let mut buf = vec![0f32; (SECS * SR) as usize];
v.render(&mut buf);
buf
}
fn f0_of(key: u8) -> f32 {
440.0 * 2f32.powf((key as f32 - 69.0) / 12.0)
}
fn feat_one(seg: &[f32], f0: f32) -> Feat {
let start = ((0.05 * SR) as usize).min(seg.len());
let body = &seg[start..];
let m = |mult: f32| mag_at(body, SR, f0 * mult);
let odd = m(1.0) + m(3.0) + m(5.0);
let even = m(2.0) + m(4.0) + m(6.0);
[
centroid(body, SR),
flatness(body, SR, 500.0, 9000.0),
odd / (even + 1e-6),
hp_rms(body, SR, 3000.0) / rms(body).max(1e-9),
m(2.0) / (m(1.0) + 1e-6),
]
}
fn features(program: u8) -> FeatSet {
std::array::from_fn(|k| feat_one(&render(program, PROBE_KEYS[k]), f0_of(PROBE_KEYS[k])))
}
fn feat_dist(a: &Feat, b: &Feat) -> f32 {
(0..5)
.map(|i| (a[i] - b[i]).abs() / (a[i].abs() + b[i].abs() + FLOOR[i]))
.fold(0.0, f32::max)
}
fn score(a: &FeatSet, b: &FeatSet) -> f32 {
a.iter()
.zip(b)
.map(|(x, y)| feat_dist(x, y))
.fold(0.0, f32::max)
}
fn all_feats() -> &'static [FeatSet] {
static CELL: std::sync::OnceLock<Vec<FeatSet>> = std::sync::OnceLock::new();
CELL.get_or_init(|| (0..128u8).map(features).collect())
}
#[test]
fn every_gm_family_is_free_of_unexpected_clones() {
let feats = all_feats();
let mut failures = Vec::new();
for fam in 0..16u8 {
let base = fam * 8;
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
let s = score(&feats[a as usize], &feats[b as usize]);
if s < EPS && allow_reason(a, b).is_none() {
failures.push(format!(
"GM {a} vs {b}: score {s:.4} < EPS {EPS} (unexpected clone)"
));
}
}
}
}
assert!(
failures.is_empty(),
"anti-clone matrix — unexpected clones:\n{}",
failures.join("\n")
);
}
#[test]
fn allowlisted_collapses_are_really_clones() {
let feats = all_feats();
for &(a, b, stage) in ALLOW {
let s = score(&feats[a as usize], &feats[b as usize]);
assert!(
s < EPS,
"GM {a} vs {b} is allowlisted as a Stage-{stage} collapse but now \
scores {s:.4} >= EPS {EPS} — already differentiated; delete this ALLOW entry"
);
}
}
#[test]
fn epsilon_is_calibrated_on_the_good_families() {
let feats = all_feats();
for (base, name) in [(56u8, "brass"), (64u8, "reed")] {
let mut min_s = f32::INFINITY;
let (mut ma, mut mb) = (0u8, 0u8);
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
let s = score(&feats[a as usize], &feats[b as usize]);
if s < min_s {
min_s = s;
(ma, mb) = (a, b);
}
}
}
assert!(
min_s >= MARGIN,
"{name}: tightest pair GM {ma}/{mb} scores {min_s:.4} < MARGIN {MARGIN} — \
EPS {EPS} has too thin a margin below the good families"
);
}
}
#[test]
#[ignore]
fn print_distinctness_matrix() {
let feats = all_feats();
for fam in 0..16u8 {
let base = fam * 8;
println!("--- GM family {}..={} ---", base, base + 7);
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
let s = score(&feats[a as usize], &feats[b as usize]);
let tag = match allow_reason(a, b) {
Some(_) => " [Collapse]",
None => "",
};
let flag = if s < EPS && allow_reason(a, b).is_none() {
" <== UNEXPECTED CLONE"
} else {
""
};
println!(" GM {a:3} vs {b:3}: {s:.4}{tag}{flag}");
}
}
}
}
}
#[cfg(all(not(feature = "embedded-samples"), ferrosintesis_repository_tests))]
#[test]
fn no_default_gate_is_paired_with_embedded_sample_coverage() {
const POLICY: &str = include_str!("../../../.deltic-integrate.toml");
let steps: Vec<&str> = POLICY
.lines()
.map(str::trim)
.filter(|l| !l.starts_with('#'))
.filter(|l| l.contains(r#"program = "cargo""#) && l.contains(r#""test""#))
.collect();
let modeled = |l: &&str| l.contains(r#""--no-default-features""#);
let embedded_workspace = steps
.iter()
.filter(|l| l.contains(r#""--workspace""#) && !modeled(l))
.count();
let modeled_only = steps
.iter()
.filter(|l| modeled(l) && l.contains(r#""ferrosintesis""#))
.count();
assert_eq!(
embedded_workspace, 2,
"fallback and workspace gates must both retain the embedded-sample test suite; \
cargo test steps seen: {steps:#?}"
);
assert_eq!(
modeled_only, 2,
"fallback and workspace gates must both retain the modeled-only test suite; \
cargo test steps seen: {steps:#?}"
);
assert_eq!(
embedded_workspace + modeled_only,
steps.len(),
"unclassified cargo test step in the gate: {steps:#?}"
);
}
#[cfg(feature = "embedded-samples")]
mod perceptual_distinctness {
use super::*;
const SR: f32 = 44100.0;
const SECS: f32 = 3.0;
const VEL: u8 = 100;
const SEED: u32 = 7;
const NKEYS: usize = 2;
const PROBE_KEYS: [u8; NKEYS] = [48, 72];
const NB: usize = 16;
const NH: usize = 10;
const WINDOWS: [(f32, f32); 5] = [
(0.02, 0.12),
(0.12, 0.45),
(0.45, 0.90),
(0.90, 1.70),
(1.70, 2.80),
];
const SHORT_NW: usize = 2;
const V_SHORT: usize = 0;
const V_LONG: usize = 1;
const SPAN_SHORT: (f32, f32) = (0.12, 0.45);
const SPAN_TAIL: (f32, f32) = (0.45, 2.80);
const SPAN_MOD: (f32, f32) = (0.12, 2.80);
const W_DIM: [f32; 7] = [0.15, 0.05, 0.12, 0.09, 0.12, 0.12, 0.35];
const DIM_CAP: f32 = 2.0;
const W_TAIL_SUM: f32 = 0.36;
const JND_ATTACK_LOG10: f32 = 0.12;
const JND_SLOPE_SHORT: f32 = 4.0;
const JND_SLOPE: f32 = 3.0;
const JND_SUSTAIN_DB: f32 = 3.0;
const FLOOR_ROUGH: f32 = 0.05;
const JND_ROUGH: f32 = 0.25;
const JND_CENT_LOG2: f32 = 0.20;
const FLOOR_HF: f32 = 0.02;
const JND_HF: f32 = 0.30;
const JND_D3_DB: f32 = 2.5;
const H_FLOOR_DB: f32 = -35.0;
const FLOOR_FLAT: f32 = 0.05;
const JND_FLAT: f32 = 0.30;
const FLOOR_HARM_FRAC: f32 = 0.05;
const JND_HARM_FRAC: f32 = 0.25;
const JND_LOG_KURT: f32 = 0.35;
const JND_MOD_DEPTH: f32 = 0.25;
const JND_AM_RATE_LOG2: f32 = 0.5;
const AM_RATE_GATE: f32 = 0.25;
const JND_ONSET_CENT_LOG2: f32 = 0.25;
const FLOOR_ONSET_SHARE: f32 = 0.02;
const JND_ONSET_SHARE: f32 = 0.30;
const JND_D7_DB: f32 = 2.5;
const D7_DROP_DB: f32 = -40.0;
const SHARED_ONSET_REL: f32 = 0.02;
const BAR_FULL: f32 = 0.075;
const BAR_TAIL: f32 = 0.76;
const MARGIN_MUL: f32 = 1.3;
const POS_FULL: &[(u8, u8)] = &[(64, 65), (16, 23), (17, 21), (83, 87), (11, 12)];
const POS_TAIL: &[(u8, u8)] = &[(48, 49)];
#[derive(Clone, Copy)]
enum Why {
Collapse(&'static str),
EarPending(&'static str),
}
const ALLOW: &[(u8, u8, Why)] = &[
(29, 30, Why::Collapse("a future GM29≠30 voicing split")),
(
40,
41,
Why::EarPending("viola is a repitched-violin proxy — same or different?"),
),
];
fn allow_reason(a: u8, b: u8) -> Option<Why> {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
ALLOW
.iter()
.find(|&&(x, y, _)| x == lo && y == hi)
.map(|&(_, _, w)| w)
}
fn f0_of(key: u8) -> f32 {
440.0 * 2f32.powf((key as f32 - 69.0) / 12.0)
}
fn render3s(program: u8, key: u8, samples: bool) -> Vec<f32> {
assert!(crate::embedded_samples_available());
let mut v = crate::voices::make(program, key, VEL, SR, SEED, samples);
let mut buf = vec![0f32; (SECS * SR) as usize];
v.render(&mut buf);
buf
}
fn seg(buf: &[f32], w: (f32, f32)) -> &[f32] {
&buf[(w.0 * SR) as usize..(w.1 * SR) as usize]
}
fn db(x: f32) -> f32 {
20.0 * x.max(1e-7).log10()
}
fn slope_db_per_s(buf: &[f32], early: (f32, f32), late: (f32, f32)) -> f32 {
let dt = 0.5 * (late.0 + late.1) - 0.5 * (early.0 + early.1);
((db(rms(seg(buf, late))) - db(rms(seg(buf, early)))) / dt).clamp(-150.0, 150.0)
}
fn harmonics(s: &[f32], f0: f32) -> ([f32; NH], f32) {
let mut mag = [0f32; NH];
for (k, m) in mag.iter_mut().enumerate() {
let fk = (k + 1) as f32 * f0;
if fk > 0.45 * SR {
break;
}
let mut best = 0f32;
for j in 0..9 {
best = best.max(mag_at(s, SR, fk * (0.96 + 0.01 * j as f32)));
}
*m = best;
}
let total = rms(s).max(1e-9);
let harm_frac = (mag.iter().map(|&m| 0.5 * m * m).sum::<f32>() / (total * total)).min(1.5);
let loudest = mag.iter().fold(1e-9f32, |a, &b| a.max(b));
let rel: [f32; NH] =
std::array::from_fn(|k| (20.0 * (mag[k].max(1e-9) / loudest).log10()).max(H_FLOOR_DB));
(rel, harm_frac)
}
fn tilt_residuals(rel: &[f32; NH]) -> [f32; NH] {
let xs: [f32; NH] = std::array::from_fn(|k| ((k + 1) as f32).log2());
let n = NH as f32;
let sx: f32 = xs.iter().sum();
let sy: f32 = rel.iter().sum();
let sxx: f32 = xs.iter().map(|x| x * x).sum();
let sxy: f32 = xs.iter().zip(rel.iter()).map(|(x, y)| x * y).sum();
let denom = n * sxx - sx * sx;
let beta = if denom.abs() > 1e-9 {
(n * sxy - sx * sy) / denom
} else {
0.0
};
let alpha = (sy - beta * sx) / n;
std::array::from_fn(|k| rel[k] - (alpha + beta * xs[k]))
}
fn am_depth_rate(s: &[f32], f0: f32) -> (f32, f32) {
let mut bp = Biquad::bandpass(f0, 8.0, SR);
let mut lp = OnePole::lowpass(30.0, SR);
let dec = (SR / 200.0) as usize;
let mut env: Vec<f64> = Vec::with_capacity(s.len() / dec + 1);
for (i, &x) in s.iter().enumerate() {
let e = lp.process(bp.process(x).abs());
if i % dec == dec - 1 {
env.push(e as f64);
}
}
let esr = SR / dec as f32;
let n = env.len();
if n < 8 {
return (0.0, 0.0);
}
let mean_env = env.iter().sum::<f64>() / n as f64;
if mean_env <= 1e-7 {
return (0.0, 0.0);
}
let half = (0.6 * esr) as usize;
let d: Vec<f64> = (0..n)
.map(|i| {
let a = i.saturating_sub(half);
let b = (i + half + 1).min(n);
env[i] - env[a..b].iter().sum::<f64>() / (b - a) as f64
})
.collect();
let zero: f64 = d.iter().map(|&x| x * x).sum();
if zero <= 0.0 || (zero / n as f64).sqrt() / mean_env < 0.02 {
return (0.0, 0.0);
}
let lag_lo = ((0.08 * esr) as usize).max(1);
let lag_hi = ((1.2 * esr) as usize).min(n - 2);
if lag_hi <= lag_lo {
return (0.0, 0.0);
}
let (mut best, mut best_lag) = (f64::MIN, lag_lo);
for lag in lag_lo..=lag_hi {
let c: f64 = (0..n - lag).map(|i| d[i] * d[i + lag]).sum::<f64>() / zero;
if c > best {
best = c;
best_lag = lag;
}
}
(best.max(0.0) as f32, esr / best_lag as f32)
}
#[derive(Clone)]
struct Passport {
w1: Vec<f32>,
w1_rms: f32,
attack_log10: f32,
slope_short: f32,
slope_long: f32,
slope_w34: f32,
slope_w45: f32,
sustain_db: f32,
roughness: f32,
cent_log2: [f32; 2],
hf_frac: [f32; 2],
h_resid: [[f32; NH]; 2],
h_lin: [[f32; NH]; 2],
flat: [f32; 2],
harm_frac: [f32; 2],
log_kurt: [f32; 2],
am_depth: f32,
am_rate_log2: f32,
fm_depth: f32,
onset_cent_log2: f32,
onset_hf: f32,
onset_flat: f32,
onset_share: f32,
cell_db: [[f32; NB]; 5],
}
fn band_f(i: usize) -> f32 {
100.0 * 100f32.powf(i as f32 / (NB - 1) as f32)
}
impl Passport {
fn new(program: u8, key: u8) -> Passport {
let buf = render3s(program, key, true);
let f0 = f0_of(key);
let att_span = &buf[..(0.45 * SR) as usize];
let mut lp = OnePole::lowpass(200.0, SR);
let env: Vec<f32> = att_span.iter().map(|&x| lp.process(x.abs())).collect();
let peak = env.iter().fold(0.0f32, |a, &b| a.max(b));
let attack_s = if peak <= 1e-7 {
0.45
} else {
env.iter()
.position(|&e| e >= 0.9 * peak)
.map(|i| i as f32 / SR)
.unwrap_or(0.45)
};
let rough = {
let s = seg(&buf, (0.90, 2.80));
let win = (0.010 * SR) as usize;
let e: Vec<f32> = s.chunks(win).map(rms).collect();
let mean = e.iter().sum::<f32>() / e.len() as f32;
if mean < 1e-6 {
0.0
} else {
(e.iter().map(|&v| (v - mean) * (v - mean)).sum::<f32>() / e.len() as f32)
.sqrt()
/ mean
}
};
let spans = [SPAN_SHORT, SPAN_TAIL];
let mut cent_log2 = [0f32; 2];
let mut hf_frac = [0f32; 2];
let mut h_resid = [[0f32; NH]; 2];
let mut h_lin = [[0f32; NH]; 2];
let mut flat = [0f32; 2];
let mut harm_frac = [0f32; 2];
let mut log_kurt = [0f32; 2];
for (v, &span) in spans.iter().enumerate() {
let s = seg(&buf, span);
cent_log2[v] = centroid(s, SR).max(50.0).log2();
hf_frac[v] = hp_rms(s, SR, 3000.0) / rms(s).max(1e-9);
let (rel, hfr) = harmonics(s, f0);
h_resid[v] = tilt_residuals(&rel);
h_lin[v] = std::array::from_fn(|k| 10f32.powf(rel[k] / 20.0));
flat[v] = flatness(s, SR, 500.0, 9000.0);
harm_frac[v] = hfr;
log_kurt[v] = kurtosis(s).max(1.5).ln();
}
let mod_span = seg(&buf, SPAN_MOD);
let (am_depth, am_rate) = am_depth_rate(mod_span, f0);
let fm_depth = fm_mod_rate(mod_span, SR, f0, 3.0, 9.0).0;
let onset = &buf[..(0.12 * SR) as usize];
let onset_share = rms(onset) / rms(&buf[..(2.80 * SR) as usize]).max(1e-9);
let mut cell_db = [[0f32; NB]; 5];
for (w, &win) in WINDOWS.iter().enumerate() {
let s = seg(&buf, win);
for (b, cell) in cell_db[w].iter_mut().enumerate() {
*cell = db(band_rms(s, SR, band_f(b), 3.5));
}
}
let w1: Vec<f32> = seg(&buf, WINDOWS[0]).to_vec();
let w1_rms = rms(&w1);
Passport {
w1,
w1_rms,
attack_log10: attack_s.max(0.005).log10(),
slope_short: slope_db_per_s(&buf, (0.12, 0.25), (0.32, 0.45)),
slope_long: slope_db_per_s(&buf, WINDOWS[1], WINDOWS[4]),
slope_w34: slope_db_per_s(&buf, WINDOWS[2], WINDOWS[3]),
slope_w45: slope_db_per_s(&buf, WINDOWS[3], WINDOWS[4]),
sustain_db: (db(rms(seg(&buf, WINDOWS[4]))) - db(rms(seg(&buf, (0.05, 0.30)))))
.clamp(-120.0, 20.0),
roughness: rough,
cent_log2,
hf_frac,
h_resid,
h_lin,
flat,
harm_frac,
log_kurt,
am_depth,
am_rate_log2: am_rate.max(0.5).log2(),
fm_depth,
onset_cent_log2: centroid(onset, SR).max(50.0).log2(),
onset_hf: hp_rms(onset, SR, 3000.0) / rms(onset).max(1e-9),
onset_flat: flatness(onset, SR, 500.0, 9000.0),
onset_share,
cell_db,
}
}
fn assert_finite(&self, label: &str) {
let mut all: Vec<f32> = vec![
self.w1_rms,
self.attack_log10,
self.slope_short,
self.slope_long,
self.slope_w34,
self.slope_w45,
self.sustain_db,
self.roughness,
self.am_depth,
self.am_rate_log2,
self.fm_depth,
self.onset_cent_log2,
self.onset_hf,
self.onset_flat,
self.onset_share,
];
all.extend_from_slice(&self.cent_log2);
all.extend_from_slice(&self.hf_frac);
all.extend_from_slice(&self.flat);
all.extend_from_slice(&self.harm_frac);
all.extend_from_slice(&self.log_kurt);
for v in 0..2 {
all.extend_from_slice(&self.h_resid[v]);
all.extend_from_slice(&self.h_lin[v]);
}
for row in &self.cell_db {
all.extend_from_slice(row);
}
assert!(
all.iter().all(|x| x.is_finite()),
"{label}: non-finite passport feature"
);
}
}
fn passports() -> &'static [[Passport; NKEYS]] {
static CELL: std::sync::OnceLock<Vec<[Passport; NKEYS]>> = std::sync::OnceLock::new();
CELL.get_or_init(|| {
(0..128u8)
.map(|p| std::array::from_fn(|k| Passport::new(p, PROBE_KEYS[k])))
.collect()
})
}
fn d_abs(a: f32, b: f32, jnd: f32) -> f32 {
(a - b).abs() / jnd
}
fn d_rel(a: f32, b: f32, floor: f32, jnd: f32) -> f32 {
((a - b).abs() / (a.abs() + b.abs() + floor)) / jnd
}
fn d3_dist(a: &Passport, b: &Passport, v: usize) -> f32 {
let (mut num, mut wsum) = (0f32, 0f32);
for k in 0..NH {
let w = (a.h_lin[v][k] * a.h_lin[v][k]).max(b.h_lin[v][k] * b.h_lin[v][k]);
let dr = a.h_resid[v][k] - b.h_resid[v][k];
num += w * dr * dr;
wsum += w;
}
if wsum <= 0.0 {
0.0
} else {
(num / wsum).sqrt() / JND_D3_DB
}
}
fn d7_dist(a: &Passport, b: &Passport, v: usize) -> f32 {
let nw = if v == V_SHORT {
SHORT_NW
} else {
WINDOWS.len()
};
let max_of = |p: &Passport| {
p.cell_db[..nw]
.iter()
.flatten()
.fold(f32::MIN, |m, &x| m.max(x))
};
let (ma, mb) = (max_of(a), max_of(b));
let (mut num, mut wsum) = (0f32, 0f32);
for w in 0..nw {
for k in 0..NB {
let ra = a.cell_db[w][k] - ma;
let rb = b.cell_db[w][k] - mb;
if ra < D7_DROP_DB && rb < D7_DROP_DB {
continue;
}
let wgt = 10f32.powf(ra / 10.0).max(10f32.powf(rb / 10.0));
num += wgt * (ra - rb).abs();
wsum += wgt;
}
}
if wsum <= 0.0 {
0.0
} else {
(num / wsum) / JND_D7_DB
}
}
fn dims_view(a: &Passport, b: &Passport, v: usize) -> [f32; 7] {
let d1 = if v == V_SHORT {
(d_abs(a.attack_log10, b.attack_log10, JND_ATTACK_LOG10)
+ d_abs(a.slope_short, b.slope_short, JND_SLOPE_SHORT))
/ 2.0
} else {
(d_abs(a.attack_log10, b.attack_log10, JND_ATTACK_LOG10)
+ d_abs(a.slope_long, b.slope_long, JND_SLOPE)
+ d_abs(a.sustain_db, b.sustain_db, JND_SUSTAIN_DB)
+ d_rel(a.roughness, b.roughness, FLOOR_ROUGH, JND_ROUGH))
/ 4.0
};
let d2 = (d_abs(a.cent_log2[v], b.cent_log2[v], JND_CENT_LOG2)
+ d_rel(a.hf_frac[v], b.hf_frac[v], FLOOR_HF, JND_HF))
/ 2.0;
let d3 = d3_dist(a, b, v);
let d4 = (d_rel(a.flat[v], b.flat[v], FLOOR_FLAT, JND_FLAT)
+ d_rel(
a.harm_frac[v],
b.harm_frac[v],
FLOOR_HARM_FRAC,
JND_HARM_FRAC,
)
+ d_abs(a.log_kurt[v], b.log_kurt[v], JND_LOG_KURT))
/ 3.0;
let d5 = if v == V_SHORT {
0.0
} else {
let mut sum = d_abs(a.am_depth, b.am_depth, JND_MOD_DEPTH)
+ d_abs(a.fm_depth, b.fm_depth, JND_MOD_DEPTH);
let mut n = 2.0;
if a.am_depth > AM_RATE_GATE && b.am_depth > AM_RATE_GATE {
sum += d_abs(a.am_rate_log2, b.am_rate_log2, JND_AM_RATE_LOG2);
n += 1.0;
}
sum / n
};
let d6 = (d_abs(a.onset_cent_log2, b.onset_cent_log2, JND_ONSET_CENT_LOG2)
+ d_rel(a.onset_hf, b.onset_hf, FLOOR_HF, JND_HF)
+ d_rel(a.onset_flat, b.onset_flat, FLOOR_FLAT, JND_FLAT)
+ d_rel(
a.onset_share,
b.onset_share,
FLOOR_ONSET_SHARE,
JND_ONSET_SHARE,
))
/ 4.0;
let d7 = d7_dist(a, b, v);
[d1, d2, d3, d4, d5, d6, d7]
}
fn s_of(dims: &[f32; 7]) -> f32 {
dims.iter()
.zip(W_DIM.iter())
.map(|(&d, &w)| w * d.min(DIM_CAP))
.sum()
}
fn score_full(a: &[Passport; NKEYS], b: &[Passport; NKEYS]) -> f32 {
(0..NKEYS)
.map(|k| {
0.4 * s_of(&dims_view(&a[k], &b[k], V_SHORT))
+ 0.6 * s_of(&dims_view(&a[k], &b[k], V_LONG))
})
.sum::<f32>()
/ NKEYS as f32
}
fn dims_tail(a: &Passport, b: &Passport) -> [f32; 3] {
let d1 = (d_abs(a.slope_w34, b.slope_w34, JND_SLOPE)
+ d_abs(a.slope_w45, b.slope_w45, JND_SLOPE)
+ d_rel(a.roughness, b.roughness, FLOOR_ROUGH, JND_ROUGH))
/ 3.0;
[d1, d3_dist(a, b, V_LONG), {
(d_rel(a.flat[V_LONG], b.flat[V_LONG], FLOOR_FLAT, JND_FLAT)
+ d_rel(
a.harm_frac[V_LONG],
b.harm_frac[V_LONG],
FLOOR_HARM_FRAC,
JND_HARM_FRAC,
)
+ d_abs(a.log_kurt[V_LONG], b.log_kurt[V_LONG], JND_LOG_KURT))
/ 3.0
}]
}
fn score_tail(a: &[Passport; NKEYS], b: &[Passport; NKEYS]) -> f32 {
(0..NKEYS)
.map(|k| {
let d = dims_tail(&a[k], &b[k]);
(W_DIM[0] * d[0].min(DIM_CAP)
+ W_DIM[2] * d[1].min(DIM_CAP)
+ W_DIM[3] * d[2].min(DIM_CAP))
/ W_TAIL_SUM
})
.sum::<f32>()
/ NKEYS as f32
}
fn is_shared_onset(a: &[Passport; NKEYS], b: &[Passport; NKEYS]) -> bool {
(0..NKEYS).all(|k| {
let denom = a[k].w1_rms.max(b[k].w1_rms);
if denom < 1e-5 {
return true;
}
let diff: f32 = rms(&a[k]
.w1
.iter()
.zip(b[k].w1.iter())
.map(|(&x, &y)| x - y)
.collect::<Vec<f32>>());
diff / denom < SHARED_ONSET_REL
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Tier {
Full,
Tail,
}
fn score_pair(a: &[Passport; NKEYS], b: &[Passport; NKEYS]) -> (Tier, f32, f32) {
if is_shared_onset(a, b) {
(Tier::Tail, score_tail(a, b), BAR_TAIL)
} else {
(Tier::Full, score_full(a, b), BAR_FULL)
}
}
#[test]
fn every_gm_family_sounds_free_of_unexpected_clones() {
let ps = passports();
let mut failures = Vec::new();
for fam in 0..16u8 {
let base = fam * 8;
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
let (tier, s, bar) = score_pair(&ps[a as usize], &ps[b as usize]);
if s < bar && allow_reason(a, b).is_none() {
failures.push(format!(
"GM {a} vs {b}: {tier:?} score {s:.4} < bar {bar} (perceived clone)"
));
}
}
}
}
assert!(
failures.is_empty(),
"perceptual anti-clone matrix — unexpected clones:\n{}",
failures.join("\n")
);
}
#[test]
fn slow_strings_clears_tail_bar_after_sample_handover() {
let ps = passports();
let (tier, score, bar) = score_pair(&ps[48], &ps[49]);
assert_eq!(
tier,
Tier::Tail,
"GM48/49 should still be judged on the post-handover tail while they share the strings onset"
);
assert!(
score >= bar,
"GM48/49 tail score {score:.4} < BAR_TAIL {bar:.4}; Slow Strings has no durable post-handover identity"
);
}
#[test]
fn allowlisted_perceived_clones_are_still_clones() {
let ps = passports();
for &(a, b, why) in ALLOW {
if let Why::Collapse(unit) = why {
let (tier, s, bar) = score_pair(&ps[a as usize], &ps[b as usize]);
assert!(
s < bar,
"GM {a} vs {b} is allowlisted as a {unit} collapse but now scores \
{s:.4} >= {tier:?} bar {bar} — already differentiated; delete this \
ALLOW entry (and its {unit} journal line)"
);
}
}
}
#[test]
fn perceptual_bar_is_calibrated() {
let ps = passports();
let margin = MARGIN_MUL * BAR_FULL;
for &(a, b) in POS_FULL {
let (tier, s, _) = score_pair(&ps[a as usize], &ps[b as usize]);
assert!(
tier == Tier::Full,
"POS pair GM {a}/{b} classified {tier:?} — a POS_FULL control must be \
independent-onset; if a bank change made these share an onset, T8 says \
investigate the routing, never reclassify silently"
);
assert!(
s >= margin,
"POS pair GM {a}/{b} scores {s:.4} < MARGIN_P {margin:.4} — the \
calibration gap collapsed (T5)"
);
}
for (base, name) in [(56u8, "brass"), (64u8, "reed")] {
let mut min_s = f32::INFINITY;
let (mut ma, mut mb) = (0u8, 0u8);
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
let (tier, s, _) = score_pair(&ps[a as usize], &ps[b as usize]);
if tier == Tier::Full && s < min_s {
min_s = s;
(ma, mb) = (a, b);
}
}
}
assert!(
min_s >= margin,
"{name}: tightest independent-onset pair GM {ma}/{mb} scores {min_s:.4} \
< MARGIN_P {margin:.4} — BAR_FULL has too thin a margin below the good \
families"
);
}
}
#[test]
fn bar_full_rejects_near_clone_negative_control() {
let original: [Passport; NKEYS] = std::array::from_fn(|k| Passport::new(64, PROBE_KEYS[k]));
let mut near_clone = original.clone();
for passport in &mut near_clone {
for sample in &mut passport.w1 {
*sample *= 1.03;
}
passport.w1_rms *= 1.03;
passport.attack_log10 += 0.5 * JND_ATTACK_LOG10;
}
let (tier, score, bar) = score_pair(&original, &near_clone);
assert_eq!(
tier,
Tier::Full,
"3% onset delta must place the negative control in the full tier"
);
assert!(
(0.025..=0.030).contains(&score),
"full-tier near-clone score {score:.5} drifted outside its 0.025..=0.030 anchor"
);
assert!(
score < bar,
"full-tier near-clone score {score:.5} must remain below BAR_FULL {bar}"
);
}
#[test]
fn byte_identical_arms_score_zero() {
let ps = passports();
let a = &ps[29];
let b = &ps[30];
for k in 0..NKEYS {
let diff: f32 = a[k]
.w1
.iter()
.zip(b[k].w1.iter())
.map(|(&x, &y)| (x - y).abs())
.fold(0.0, f32::max);
assert!(
diff == 0.0,
"GM 29/30 W1 renders differ at key {} — the shared DRIVE arm split \
without updating this oracle",
PROBE_KEYS[k]
);
}
assert!(score_full(a, b) < 1e-6, "GM 29/30 full score not ~0");
assert!(score_tail(a, b) < 1e-6, "GM 29/30 tail score not ~0");
}
#[test]
fn metric_is_a_premetric() {
let ps = passports();
for (i, p) in ps.iter().enumerate() {
for (k, pk) in p.iter().enumerate() {
pk.assert_finite(&format!("GM {i} key {}", PROBE_KEYS[k]));
}
}
for &p in &[0usize, 4, 19, 25, 29, 40, 52, 56, 64, 72, 88, 104] {
assert!(
score_full(&ps[p], &ps[p]) == 0.0 && score_tail(&ps[p], &ps[p]) == 0.0,
"GM {p}: self-distance is not exactly zero"
);
}
for &(a, b) in &[(0u8, 1u8), (4, 5), (24, 25), (56, 60), (64, 71), (88, 92)] {
let (pa, pb) = (&ps[a as usize], &ps[b as usize]);
assert!(
(score_full(pa, pb) - score_full(pb, pa)).abs() < 1e-9
&& (score_tail(pa, pb) - score_tail(pb, pa)).abs() < 1e-9
&& is_shared_onset(pa, pb) == is_shared_onset(pb, pa),
"GM {a}/{b}: metric is not symmetric"
);
}
}
#[test]
fn sample_layer_engaged_at_probe_keys() {
const LA_WRAPPED: &[u8] = &[
0, 1, 3, 6, 9, 12, 13, 24, 25, 40, 41, 42, 43, 46, 47, 48, 49, 56, 57, 58, 59, 60, 68,
69, 70, 71, 72, 73, 74, 75, 77, 79, 104, 105, 110,
];
let ps = passports();
let mut failures = Vec::new();
for &prog in LA_WRAPPED {
let mut rel_diffs = Vec::new();
for k in 0..NKEYS {
let on = &ps[prog as usize][k].w1;
let off = {
let mut v = crate::voices::make(prog, PROBE_KEYS[k], VEL, SR, SEED, false);
let mut buf = vec![0f32; (0.13 * SR) as usize];
v.render(&mut buf);
seg(&buf, WINDOWS[0]).to_vec()
};
let diff: Vec<f32> = on.iter().zip(off.iter()).map(|(&x, &y)| x - y).collect();
let denom = ps[prog as usize][k].w1_rms.max(rms(&off)).max(1e-9);
rel_diffs.push((PROBE_KEYS[k], rms(&diff) / denom));
}
if !rel_diffs.iter().any(|&(_, d)| d > 0.05) {
failures.push(format!(
"GM {prog}: W1 on/off waveform rel-diffs {rel_diffs:.3?} — LA layer \
not engaged at either probe key (silent bare-model fallback?)"
));
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn onset_tier_classification_is_stable() {
const SHARED_ONSET_PAIRS: &[(u8, u8)] = &[(29, 30), (48, 49)];
let ps = passports();
let mut got = Vec::new();
for fam in 0..16u8 {
let base = fam * 8;
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
if is_shared_onset(&ps[a as usize], &ps[b as usize]) {
got.push((a, b));
}
}
}
}
assert!(
got == SHARED_ONSET_PAIRS,
"shared-onset tier membership changed.\n pinned: {SHARED_ONSET_PAIRS:?}\n \
got: {got:?}\nA pair moving tiers changes its effective bar — re-pin \
deliberately (and re-adjudicate the pair) or fix the routing (T8)."
);
}
struct ClassRange {
label: &'static str,
programs: &'static [u8],
field: &'static str,
get: fn(&Passport) -> f32,
lo: f32,
hi: f32,
}
fn class_field_mean(prog: u8, get: fn(&Passport) -> f32) -> f32 {
let p = &passports()[prog as usize];
0.5 * (get(&p[0]) + get(&p[1]))
}
const CLASS_RANGES: &[ClassRange] = &[
ClassRange {
label: "organ 16-23",
programs: &[16, 17, 18, 19, 20, 21, 22, 23],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -6.0,
hi: 20.0,
},
ClassRange {
label: "keyboard+chrom-perc 0-15",
programs: &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -120.0,
hi: -10.0,
},
ClassRange {
label: "plucked 24-37,46,104-108",
programs: &[
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 46, 104, 105, 106, 107, 108,
],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -120.0,
hi: -10.0,
},
ClassRange {
label: "bowed-sustain 40-43,110",
programs: &[40, 41, 42, 43, 110],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -5.0,
hi: 20.0,
},
ClassRange {
label: "bowed-vibrato 40-43,110",
programs: &[40, 41, 42, 43, 110],
field: "fm_depth",
get: |p| p.fm_depth,
lo: 0.1,
hi: 1.0,
},
ClassRange {
label: "ensemble/choir 48-54",
programs: &[48, 49, 50, 51, 52, 53, 54],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -7.0,
hi: 20.0,
},
ClassRange {
label: "brass 56-63",
programs: &[56, 57, 58, 59, 60, 61, 62, 63],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -6.0,
hi: 20.0,
},
ClassRange {
label: "reed 64-71",
programs: &[64, 65, 66, 67, 68, 69, 70, 71],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -6.0,
hi: 20.0,
},
ClassRange {
label: "pipe 72-79",
programs: &[72, 73, 74, 75, 76, 77, 78, 79],
field: "sustain_db",
get: |p| p.sustain_db,
lo: -6.0,
hi: 20.0,
},
];
#[test]
fn class_identity_ranges_hold() {
for r in CLASS_RANGES {
for &prog in r.programs {
let v = class_field_mean(prog, r.get);
assert!(
(r.lo..=r.hi).contains(&v),
"{}: GM{prog} {} = {v:.4}, out of class range [{}, {}]",
r.label,
r.field,
r.lo,
r.hi
);
}
}
}
#[test]
fn class_ranges_reject_wrong_class() {
let controls: &[(&str, u8, &str)] = &[
(
"organ 16-23",
0,
"piano decays (sustain_db ~ -18), must fail the held-level floor",
),
(
"keyboard+chrom-perc 0-15",
19,
"organ holds (sustain_db ~ 0), must fail the decays ceiling",
),
(
"plucked 24-37,46,104-108",
40,
"violin bows/holds (sustain_db ~ +7), must fail the decays ceiling",
),
(
"bowed-sustain 40-43,110",
0,
"piano decays (sustain_db ~ -18), must fail the held floor",
),
(
"bowed-vibrato 40-43,110",
8,
"celesta has no vibrato (fm_depth ~ 0.001), must fail the vibrato floor",
),
(
"ensemble/choir 48-54",
0,
"piano decays (sustain_db ~ -18), must fail the held floor",
),
(
"brass 56-63",
0,
"piano decays (sustain_db ~ -18), must fail the held floor",
),
(
"reed 64-71",
0,
"piano decays (sustain_db ~ -18), must fail the held floor",
),
(
"pipe 72-79",
0,
"piano decays (sustain_db ~ -18), must fail the held floor",
),
];
for &(label, wrong, why) in controls {
let r = CLASS_RANGES
.iter()
.find(|r| r.label == label)
.unwrap_or_else(|| panic!("no class range labelled {label}"));
let v = class_field_mean(wrong, r.get);
assert!(
!(r.lo..=r.hi).contains(&v),
"{label}: wrong-class GM{wrong} {} = {v:.4} is INSIDE [{}, {}] — \
range lacks discriminating power ({why})",
r.field,
r.lo,
r.hi
);
}
}
#[test]
#[ignore]
fn print_passport_fields() {
let ps = passports();
println!("prog attack_log10 sustain_db flat_L harm_frac_L fm_depth cent_log2_L");
for prog in 0..128u8 {
let p = &ps[prog as usize];
let km = |g: fn(&Passport) -> f32| 0.5 * (g(&p[0]) + g(&p[1]));
println!(
"GM{prog:3} {:11.4} {:10.3} {:7.4} {:10.4} {:9.4} {:10.4}",
km(|q| q.attack_log10),
km(|q| q.sustain_db),
km(|q| q.flat[V_LONG]),
km(|q| q.harm_frac[V_LONG]),
km(|q| q.fm_depth),
km(|q| q.cent_log2[V_LONG]),
);
}
}
#[test]
#[ignore]
fn print_perceptual_matrix() {
let ps = passports();
for fam in 0..16u8 {
let base = fam * 8;
println!("--- GM family {}..={} ---", base, base + 7);
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
let (pa, pb) = (&ps[a as usize], &ps[b as usize]);
let (tier, s, bar) = score_pair(pa, pb);
let tag = match allow_reason(a, b) {
Some(Why::Collapse(u)) => format!(" [Collapse → {u}]"),
Some(Why::EarPending(q)) => format!(" [EarPending: {q}]"),
None => String::new(),
};
let flag = if s < bar && allow_reason(a, b).is_none() {
" <== PERCEIVED CLONE"
} else {
""
};
match tier {
Tier::Full => {
let d0 = dims_view(&pa[0], &pb[0], V_LONG);
let d1 = dims_view(&pa[1], &pb[1], V_LONG);
println!(
" GM {a:3} vs {b:3}: FULL {s:.4}{tag}{flag} longdims \
k48 {d0:.2?} k72 {d1:.2?}"
);
}
Tier::Tail => {
let d0 = dims_tail(&pa[0], &pb[0]);
let d1 = dims_tail(&pa[1], &pb[1]);
println!(
" GM {a:3} vs {b:3}: TAIL {s:.4}{tag}{flag} taildims \
k48 {d0:.2?} k72 {d1:.2?}"
);
}
}
}
}
}
println!("\n=== §2.4 / §7 calibration controls ===");
let mut neg_full: f32 = 0.0;
let mut neg_tail: f32 = 0.0;
for &(a, b) in &[(0u8, 1u8), (0, 3), (4, 5), (29, 30)] {
let (tier, s, _) = score_pair(&ps[a as usize], &ps[b as usize]);
println!("NEG GM {a}/{b}: {tier:?} {s:.4}");
match tier {
Tier::Full => neg_full = neg_full.max(s),
Tier::Tail => neg_tail = neg_tail.max(s),
}
}
let mut pos_min = f32::INFINITY;
for &(a, b) in POS_FULL {
let (tier, s, _) = score_pair(&ps[a as usize], &ps[b as usize]);
println!("POS GM {a}/{b}: {tier:?} {s:.4}");
pos_min = pos_min.min(s);
}
for &(a, b) in POS_TAIL {
let (tier, s, _) = score_pair(&ps[a as usize], &ps[b as usize]);
println!("POS_TAIL GM {a}/{b}: {tier:?} {s:.4}");
}
for (base, name) in [(56u8, "brass"), (64u8, "reed")] {
let mut fam_min = f32::INFINITY;
for a in base..base + 8 {
for b in (a + 1)..base + 8 {
let (tier, s, _) = score_pair(&ps[a as usize], &ps[b as usize]);
if tier == Tier::Full {
fam_min = fam_min.min(s);
}
}
}
println!("{name} family tier-1 minimum: {fam_min:.4}");
pos_min = pos_min.min(fam_min);
}
for &(a, b) in &[(40u8, 41u8), (72, 73), (40, 44)] {
let (tier, s, _) = score_pair(&ps[a as usize], &ps[b as usize]);
println!("ADJUDICATE GM {a}/{b}: {tier:?} {s:.4} (ear once, §6/§7)");
}
println!(
"BAR_FULL gap: [{:.4}, {:.4}] (geomean {:.4}); current {BAR_FULL}",
1.25 * neg_full,
pos_min / MARGIN_MUL,
(1.25 * neg_full * pos_min / MARGIN_MUL).sqrt().max(0.0)
);
println!(
"BAR_TAIL lower anchor: {:.4} (POS_tail anchored on nothing, §7); current {BAR_TAIL}",
1.25 * neg_tail
);
}
}
#[cfg(test)]
mod pluck_baseline {
use super::*;
use crate::voices::{self, Voice};
const SR: f32 = 44100.0;
const KEYS: [u8; 3] = [40, 52, 64];
const VELS: [u8; 3] = [50, 100, 120];
const SEEDS: [u32; 3] = [3, 7, 11];
const SHAPED_MIGRATED: &[&str] = &["STEEL", "JAZZ", "DULCIMER", "PIZZ"];
type HeadRow = (&'static str, u8, u8, f32, f32, f32, f32);
type FluidRow = (u8, u8, u8, f32, f32, f32, f32, f32);
const GRID: &[(&str, u8, u8)] = &[
("NYLON", 24, 0),
("HARP", 46, 0),
("PIZZ", 45, 0),
("FRETLESS", 35, 0),
("BASS", 33, 0),
("UPRIGHT", 32, 0),
("UKULELE", 24, 96),
("OUD", 105, 98),
("STEEL", 25, 0),
("JAZZ", 26, 0),
("CLEAN", 27, 0),
("DRIVE", 30, 0),
("PICK", 34, 0),
("DULCIMER", 15, 0),
];
#[rustfmt::skip]
const HEAD_BASELINE: &[HeadRow] = &[
("NYLON", 40, 50, -32.39, 1.21, -6.9, 3.84),
("NYLON", 40, 100, -22.66, 1.52, -4.8, 2.53),
("NYLON", 40, 120, -20.72, 1.65, -4.3, 2.01),
("NYLON", 52, 50, -32.14, 1.47, -8.2, 4.70),
("NYLON", 52, 100, -23.28, 1.88, -6.5, 5.45),
("NYLON", 52, 120, -21.07, 2.03, -6.2, 5.43),
("NYLON", 64, 50, -35.14, 2.14, -8.7, 0.72),
("NYLON", 64, 100, -25.54, 2.43, -6.6, 1.25),
("NYLON", 64, 120, -23.12, 2.57, -6.2, 1.95),
("HARP", 40, 50, -30.33, 1.48, -5.2, 0.91),
("HARP", 40, 100, -21.35, 1.54, -3.1, 0.22),
("HARP", 40, 120, -18.79, 1.56, -2.7, 0.26),
("HARP", 52, 50, -31.95, 1.81, -6.7, 2.02),
("HARP", 52, 100, -22.56, 1.82, -4.7, 1.42),
("HARP", 52, 120, -20.01, 1.86, -4.3, 1.39),
("HARP", 64, 50, -37.76, 3.95, -10.4, 0.90),
("HARP", 64, 100, -26.96, 3.04, -8.2, 2.11),
("HARP", 64, 120, -24.27, 2.96, -7.0, 2.02),
("PIZZ", 40, 50, -36.37, 3.06, -8.1, 2.93),
("PIZZ", 40, 100, -27.69, 3.89, -6.6, 0.81),
("PIZZ", 40, 120, -25.82, 4.17, -6.0, 0.90),
("PIZZ", 52, 50, -36.37, 4.27, -10.4, 6.52),
("PIZZ", 52, 100, -28.16, 4.88, -9.6, 9.03),
("PIZZ", 52, 120, -26.11, 5.19, -9.0, 9.11),
("PIZZ", 64, 50, -42.36, 17.01, -9.8, 1.26),
("PIZZ", 64, 100, -34.47, 19.01, -8.2, 1.39),
("PIZZ", 64, 120, -32.56, 19.72, -8.0, 1.64),
("FRETLESS", 40, 50, -27.95, 1.86, -7.2, 1.92),
("FRETLESS", 40, 100, -16.67, 1.50, -7.4, 2.77),
("FRETLESS", 40, 120, -14.06, 1.48, -7.3, 2.80),
("FRETLESS", 52, 50, -34.45, 2.36, -9.9, 2.17),
("FRETLESS", 52, 100, -24.13, 3.43, -9.7, 3.38),
("FRETLESS", 52, 120, -21.32, 3.27, -9.7, 3.13),
("FRETLESS", 64, 50, -33.22, 1.85, -8.7, 0.92),
("FRETLESS", 64, 100, -25.85, 2.82, -8.3, 1.32),
("FRETLESS", 64, 120, -24.22, 3.10, -8.3, 1.79),
("BASS", 40, 50, -21.85, 1.76, -6.5, 2.25),
("BASS", 40, 100, -12.09, 1.56, -7.3, 3.09),
("BASS", 40, 120, -9.86, 1.53, -7.6, 2.88),
("BASS", 52, 50, -23.60, 1.42, -10.0, 1.72),
("BASS", 52, 100, -17.55, 2.10, -10.9, 3.48),
("BASS", 52, 120, -15.83, 2.38, -10.7, 4.26),
("BASS", 64, 50, -23.31, 1.93, -8.4, 0.80),
("BASS", 64, 100, -15.74, 2.41, -8.4, 0.94),
("BASS", 64, 120, -14.19, 2.59, -8.3, 1.11),
("UPRIGHT", 40, 50, -33.30, 2.80, -7.5, 3.91),
("UPRIGHT", 40, 100, -22.19, 2.49, -8.3, 3.12),
("UPRIGHT", 40, 120, -19.30, 2.44, -8.4, 2.79),
("UPRIGHT", 52, 50, -37.70, 4.01, -8.7, 1.07),
("UPRIGHT", 52, 100, -27.50, 4.36, -10.1, 2.67),
("UPRIGHT", 52, 120, -24.39, 3.99, -10.4, 2.95),
("UPRIGHT", 64, 50, -39.93, 3.13, -9.1, 1.44),
("UPRIGHT", 64, 100, -31.13, 5.28, -9.2, 2.27),
("UPRIGHT", 64, 120, -29.33, 6.13, -9.1, 3.49),
("UKULELE", 40, 50, -34.25, 2.76, -5.7, 4.02),
("UKULELE", 40, 100, -25.77, 2.54, -4.0, 3.75),
("UKULELE", 40, 120, -23.44, 2.56, -3.5, 3.11),
("UKULELE", 52, 50, -35.14, 2.90, -8.1, 6.89),
("UKULELE", 52, 100, -26.01, 2.83, -5.8, 6.02),
("UKULELE", 52, 120, -23.87, 2.86, -5.2, 6.06),
("UKULELE", 64, 50, -41.72, 8.91, -9.1, 1.55),
("UKULELE", 64, 100, -30.74, 5.54, -7.9, 3.19),
("UKULELE", 64, 120, -28.05, 5.36, -7.5, 3.07),
("OUD", 40, 50, -33.47, 2.10, -7.2, 2.23),
("OUD", 40, 100, -25.33, 2.19, -6.4, 3.18),
("OUD", 40, 120, -23.26, 2.26, -6.1, 3.05),
("OUD", 52, 50, -38.94, 3.66, -10.2, 3.70),
("OUD", 52, 100, -27.79, 3.29, -9.8, 4.34),
("OUD", 52, 120, -24.93, 3.05, -9.2, 4.47),
("OUD", 64, 50, -50.08, 19.47, -10.7, 8.16),
("OUD", 64, 100, -36.75, 13.89, -10.1, 3.35),
("OUD", 64, 120, -32.87, 12.03, -9.4, 2.03),
("STEEL", 40, 50, -38.31, 2.72, -3.4, 1.89),
("STEEL", 40, 100, -26.14, 3.13, -1.9, 2.08),
("STEEL", 40, 120, -22.93, 3.20, -1.6, 2.03),
("STEEL", 52, 50, -40.64, 3.52, -6.1, 2.63),
("STEEL", 52, 100, -28.19, 3.88, -3.3, 2.18),
("STEEL", 52, 120, -24.95, 4.00, -2.9, 2.26),
("STEEL", 64, 50, -48.10, 8.10, -8.1, 3.33),
("STEEL", 64, 100, -34.64, 6.57, -6.4, 2.77),
("STEEL", 64, 120, -31.22, 6.46, -5.4, 2.72),
("JAZZ", 40, 50, -33.94, 1.84, -8.8, 1.81),
("JAZZ", 40, 100, -22.56, 1.88, -7.5, 3.36),
("JAZZ", 40, 120, -19.59, 1.89, -7.2, 3.56),
("JAZZ", 52, 50, -35.34, 2.45, -9.5, 8.41),
("JAZZ", 52, 100, -23.33, 2.16, -8.5, 8.42),
("JAZZ", 52, 120, -20.25, 2.13, -7.7, 8.30),
("JAZZ", 64, 50, -38.83, 13.53, -8.5, 0.71), ("JAZZ", 64, 100, -26.30, 5.24, -8.1, 2.26), ("JAZZ", 64, 120, -23.12, 4.68, -7.9, 3.07), ("CLEAN", 40, 50, -37.40, 2.49, -5.4, 2.55),
("CLEAN", 40, 100, -27.96, 2.80, -4.2, 2.46),
("CLEAN", 40, 120, -25.55, 2.83, -4.0, 1.91),
("CLEAN", 52, 50, -37.34, 3.51, -8.5, 7.52),
("CLEAN", 52, 100, -27.46, 3.69, -6.6, 7.12),
("CLEAN", 52, 120, -24.74, 3.72, -6.1, 6.60),
("CLEAN", 64, 50, -44.16, 10.66, -8.3, 1.94),
("CLEAN", 64, 100, -32.01, 6.05, -7.7, 1.94),
("CLEAN", 64, 120, -28.93, 5.49, -7.3, 1.71),
("DRIVE", 40, 50, -24.80, 2.07, -4.4, 2.25),
("DRIVE", 40, 100, -15.69, 2.10, -3.4, 1.08),
("DRIVE", 40, 120, -13.52, 2.14, -3.3, 1.21),
("DRIVE", 52, 50, -26.19, 2.23, -6.6, 2.53),
("DRIVE", 52, 100, -16.39, 2.29, -4.0, 1.41),
("DRIVE", 52, 120, -13.58, 2.30, -3.7, 1.34),
("DRIVE", 64, 50, -28.88, 3.68, -7.9, 2.05),
("DRIVE", 64, 100, -19.15, 3.25, -5.9, 1.69),
("DRIVE", 64, 120, -16.53, 3.20, -5.6, 1.41),
("PICK", 40, 50, -31.61, 2.20, -7.1, 2.10),
("PICK", 40, 100, -22.08, 2.15, -6.1, 2.75),
("PICK", 40, 120, -19.65, 2.23, -5.8, 2.80),
("PICK", 52, 50, -35.65, 4.20, -9.0, 2.03),
("PICK", 52, 100, -25.15, 3.99, -7.8, 1.76),
("PICK", 52, 120, -22.40, 3.73, -7.6, 1.94),
("PICK", 64, 50, -37.10, 3.74, -8.7, 1.95),
("PICK", 64, 100, -27.90, 5.23, -8.9, 3.86),
("PICK", 64, 120, -25.45, 5.41, -9.0, 3.99),
("DULCIMER", 40, 50, -34.54, 2.37, -5.1, 1.72),
("DULCIMER", 40, 100, -22.56, 2.32, -4.7, 1.89),
("DULCIMER", 40, 120, -19.39, 2.31, -4.4, 1.99),
("DULCIMER", 52, 50, -35.62, 3.23, -5.8, 3.27),
("DULCIMER", 52, 100, -23.37, 3.29, -4.7, 2.65),
("DULCIMER", 52, 120, -20.16, 3.19, -4.5, 2.44),
("DULCIMER", 64, 50, -40.49, 6.49, -10.0, 2.06),
("DULCIMER", 64, 100, -27.24, 4.70, -6.5, 1.79),
("DULCIMER", 64, 120, -23.85, 4.28, -6.0, 1.78),
];
#[rustfmt::skip]
const FLUIDR3_GOLDEN: &[FluidRow] = &[
(24, 40, 60, 1.15, -15.6, 2.48, 0.130, 1.11),
(24, 40, 100, 1.15, -11.5, 2.49, 0.256, 1.11),
(24, 52, 60, 1.16, -16.3, 2.44, 0.087, 1.17),
(24, 52, 100, 1.16, -11.8, 2.47, 0.175, 1.18),
(24, 64, 60, 2.26, -11.9, 2.04, 0.152, 2.51),
(24, 64, 100, 2.26, -7.2, 2.23, 0.295, 2.52),
(25, 40, 60, 0.97, -4.3, 2.57, 0.670, 0.99),
(25, 40, 100, 0.97, -4.3, 2.57, 0.670, 0.99),
(25, 52, 60, 1.07, -4.8, 2.99, 0.622, 1.18),
(25, 52, 100, 1.07, -4.8, 2.99, 0.622, 1.18),
(25, 64, 60, 1.54, -5.8, 3.71, 0.446, 1.55),
(25, 64, 100, 1.54, -5.8, 3.71, 0.446, 1.55),
(32, 40, 60, 1.01, -9.1, 2.40, 0.450, 0.98),
(32, 40, 100, 1.01, -9.1, 2.40, 0.450, 0.98),
(32, 52, 60, 1.44, -10.2, 2.40, 0.381, 2.03),
(32, 52, 100, 1.44, -10.2, 2.40, 0.381, 2.03),
(32, 64, 60, 1.70, -8.1, 2.65, 0.510, 1.79),
(32, 64, 100, 1.70, -8.1, 2.65, 0.507, 1.79),
(33, 40, 60, 1.35, -17.1, 2.19, 0.124, 1.37),
(33, 40, 100, 1.35, -17.7, 2.19, 0.126, 1.37),
(33, 52, 60, 1.28, -16.7, 2.41, 0.117, 1.16),
(33, 52, 100, 1.28, -16.9, 2.41, 0.116, 1.16),
(33, 64, 60, 1.24, -13.7, 2.72, 0.103, 1.25),
(33, 64, 100, 1.24, -13.7, 2.72, 0.103, 1.25),
(45, 40, 60, 0.60, -16.4, 1.82, 0.158, 0.71),
(45, 40, 100, 0.60, -16.9, 1.82, 0.134, 0.71),
(45, 52, 60, 0.48, -13.5, 1.77, 0.267, 0.51),
(45, 52, 100, 0.48, -13.7, 1.77, 0.264, 0.51),
(45, 64, 60, 0.89, -14.8, 1.71, 0.272, 0.93),
(45, 64, 100, 0.89, -14.7, 1.71, 0.267, 0.93),
(46, 40, 60, 2.00, -13.3, 2.02, 0.242, 1.92),
(46, 40, 100, 2.00, -13.3, 2.02, 0.240, 1.92),
(46, 52, 60, 0.81, -13.1, 2.57, 0.149, 1.00),
(46, 52, 100, 0.81, -13.2, 2.57, 0.148, 1.00),
(46, 64, 60, 2.88, -10.2, 1.76, 0.287, 2.66),
(46, 64, 100, 2.88, -10.2, 1.76, 0.291, 2.66),
];
fn f0_of(key: u8) -> f32 {
440.0 * 2f32.powf((key as f32 - 69.0) / 12.0)
}
fn make_bare(program: u8, bank: u8, key: u8, vel: u8, seed: u32) -> Box<dyn Voice> {
if bank == 0 {
voices::make(program, key, vel, SR, seed, false)
} else {
voices::make_variation(program, bank, key, vel, SR, seed, false, 0)
.expect("alt-bank preset must exist")
}
}
fn render_bare(program: u8, bank: u8, key: u8, vel: u8, seed: u32, secs: f32) -> Vec<f32> {
let mut v = make_bare(program, bank, key, vel, seed);
let mut buf = vec![0f32; (secs * SR) as usize];
v.render(&mut buf);
buf
}
fn db(x: f32) -> f32 {
20.0 * x.max(1e-9).log10()
}
fn median3(mut v: [f32; 3]) -> f32 {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v[1]
}
fn measure(program: u8, bank: u8, key: u8, vel: u8) -> (f32, f32, f32, f32) {
let f0 = f0_of(key);
let mut rms_db = [0f32; 3];
let mut asr = [0f32; 3];
let mut tilt = [0f32; 3];
for (i, &seed) in SEEDS.iter().enumerate() {
let buf = render_bare(program, bank, key, vel, seed, 0.40);
let sus = &buf[(0.05 * SR) as usize..(0.35 * SR) as usize];
rms_db[i] = db(rms(sus));
asr[i] = att_sus_ratio(&buf, SR, f0);
let onset = &buf[..(0.020 * SR) as usize];
tilt[i] = spectral_tilt_db_oct(onset, SR, 300.0, 9000.0);
}
let hi = rms_db.iter().copied().fold(f32::MIN, f32::max);
let lo = rms_db.iter().copied().fold(f32::MAX, f32::min);
(median3(rms_db), median3(asr), median3(tilt), hi - lo)
}
#[test]
#[ignore]
fn print_pluck_head_baseline() {
println!(" #[rustfmt::skip]");
println!(" const HEAD_BASELINE: &[HeadRow] = &[");
println!(
" // (preset, key, vel, rms_db[0.05-0.35s], att_sus, onset_tilt_db_oct, seed_spread_db)"
);
let (mut worst_asr, mut worst_name) = (0f32, "");
for &(name, program, bank) in GRID {
for &key in &KEYS {
for &vel in &VELS {
let (r, a, t, s) = measure(program, bank, key, vel);
println!(" (\"{name}\", {key}, {vel}, {r:.2}, {a:.2}, {t:.1}, {s:.2}),");
if a > worst_asr {
worst_asr = a;
worst_name = name;
}
}
}
}
println!(" ];");
println!(" // fiercest att/sus in the grid: {worst_name} = {worst_asr:.2} (FluidR3 natural ≈ 1.3)");
}
#[test]
#[ignore]
fn print_shaped_loudness_offset() {
let head = |name: &str, key: u8, vel: u8| -> Option<f32> {
HEAD_BASELINE
.iter()
.find(|r| r.0 == name && r.1 == key && r.2 == vel)
.map(|r| r.3)
};
let (mut sum, mut count) = (0f32, 0u32);
println!("preset key vel shaped_rms head_rms offset_dB");
for &(name, program, bank) in GRID {
if !SHAPED_MIGRATED.contains(&name) {
continue;
}
let mut psum = 0f32;
let mut pcount = 0u32;
for &key in &KEYS {
for &vel in &VELS {
let (r, _, _, _) = measure(program, bank, key, vel);
if let Some(h) = head(name, key, vel) {
let off = r - h;
println!("{name:<10} {key:>3} {vel:>3} {r:9.2} {h:8.2} {off:+.2}");
sum += off;
count += 1;
psum += off;
pcount += 1;
}
}
}
println!(
" {name} mean offset {:+.2} dB (per-preset exc_trim to hit K_SUS)",
psum / pcount as f32
);
}
let mean = sum / count as f32;
println!("GLOBAL mean offset {mean:+.2} dB over {count} cells");
}
#[test]
fn pizz_shaped_gain_is_smooth_and_level_only() {
#[rustfmt::skip]
const ANCHORS: &[(u8, u8, f32)] = &[
(40, 50, 2.95), (40, 100, 0.56), (40, 120, -0.45),
(52, 50, 4.58), (52, 100, 1.59), (52, 120, 0.68),
(64, 50, 1.75), (64, 100, -1.74), (64, 120, -2.91),
];
let gain_db = |key, vel| voices::pizz_shaped_gain_db_for_test(key, vel);
for &(key, vel, expected) in ANCHORS {
let got = gain_db(key, vel);
assert!(
(got - expected).abs() <= 0.01,
"PIZZ correction {key}/{vel}: {got:+.2} dB != {expected:+.2} dB"
);
}
assert!((gain_db(20, 10) - gain_db(40, 50)).abs() <= 1e-6);
assert!((gain_db(96, 127) - gain_db(64, 120)).abs() <= 1e-6);
for keys in [40u8, 46, 52, 58, 64].windows(2) {
for vels in [50u8, 75, 100, 110, 120].windows(2) {
let (k0, k1, v0, v1) = (keys[0], keys[1], vels[0], vels[1]);
let mid = gain_db((k0 + k1) / 2, (v0 + v1) / 2);
let corners = [
gain_db(k0, v0),
gain_db(k0, v1),
gain_db(k1, v0),
gain_db(k1, v1),
];
let lo = corners.iter().copied().fold(f32::INFINITY, f32::min);
let hi = corners.iter().copied().fold(f32::NEG_INFINITY, f32::max);
assert!(
mid >= lo - 1e-6 && mid <= hi + 1e-6,
"PIZZ correction overshot inside {k0}-{k1}/{v0}-{v1}: {mid:+.2} not in {lo:+.2}..{hi:+.2}"
);
}
}
for &key in &[40, 46, 52, 58, 64] {
for &vel in &[50, 75, 100, 110, 120] {
let gain = 10f32.powf(gain_db(key, vel) / 20.0);
for &seed in &[5, 21, 99] {
let corrected =
voices::render_pizz_shaped_gain_probe_for_test(key, vel, seed, 0.40, true);
let uncorrected =
voices::render_pizz_shaped_gain_probe_for_test(key, vel, seed, 0.40, false);
let diff: Vec<f32> = corrected
.iter()
.zip(&uncorrected)
.map(|(&c, &u)| c - u * gain)
.collect();
let rel = rms(&diff) / rms(&corrected).max(1e-9);
assert!(
rel <= 2e-5,
"PIZZ {key}/{vel} seed {seed}: correction is not scalar gain (relative RMS diff {rel:.6})"
);
}
}
}
}
#[test]
fn pizz_shaped_gain_matches_legacy_between_anchors() {
const GRID_KEYS: [u8; 5] = [40, 46, 52, 58, 64];
const GRID_VELS: [u8; 5] = [50, 75, 100, 110, 120];
const MATCHED_SEEDS: [u32; 3] = [5, 21, 99];
let mut worst = (0u8, 0u8, 0.0f32);
for &key in &GRID_KEYS {
for &vel in &GRID_VELS {
if KEYS.contains(&key) && VELS.contains(&vel) {
continue;
}
let mut offsets = [0.0; 3];
for (i, &seed) in MATCHED_SEEDS.iter().enumerate() {
let shaped =
voices::render_pizz_shaped_gain_probe_for_test(key, vel, seed, 0.40, true);
let legacy = voices::render_pizz_legacy_probe_for_test(key, vel, seed, 0.40);
let sustain = (0.05 * SR) as usize..(0.35 * SR) as usize;
offsets[i] =
db(rms(&shaped[sustain.clone()])) - db(rms(&legacy[sustain.clone()]));
}
let offset = offsets.iter().sum::<f32>() / offsets.len() as f32;
eprintln!("PIZZ intermediate {key}/{vel}: {offset:+.2} dB");
if offset.abs() > worst.2.abs() {
worst = (key, vel, offset);
}
}
}
assert!(
worst.2.abs() <= 0.5,
"PIZZ corrected Shaped vs Legacy worst intermediate {}/{}: {:+.2} dB",
worst.0,
worst.1,
worst.2
);
}
#[test]
#[ignore]
fn print_pizz_legacy_gain_surface() {
const MATCHED_SEEDS: [u32; 3] = [5, 21, 99];
println!("key vel correction_dB");
for &key in &[40, 46, 52, 58, 64] {
for &vel in &[50, 75, 100, 110, 120] {
let mut offsets = [0.0; 3];
for (i, &seed) in MATCHED_SEEDS.iter().enumerate() {
let shaped =
voices::render_pizz_shaped_gain_probe_for_test(key, vel, seed, 0.40, false);
let legacy = voices::render_pizz_legacy_probe_for_test(key, vel, seed, 0.40);
let sustain = (0.05 * SR) as usize..(0.35 * SR) as usize;
offsets[i] =
db(rms(&shaped[sustain.clone()])) - db(rms(&legacy[sustain.clone()]));
}
println!(
"{key:>3} {vel:>3} {:+.2} ({:+.2}, {:+.2}, {:+.2})",
-offsets.iter().sum::<f32>() / offsets.len() as f32,
-offsets[0],
-offsets[1],
-offsets[2]
);
}
}
}
#[test]
fn shaped_g7_mean_parity_and_seed_bound() {
const CANARY: &str = "NYLON";
let head = |name: &str, key: u8, vel: u8| -> (f32, f32) {
let row = HEAD_BASELINE
.iter()
.find(|r| r.0 == name && r.1 == key && r.2 == vel)
.unwrap_or_else(|| panic!("no HEAD row for {name} {key} {vel}"));
(row.3, row.6)
};
for &(name, program, bank) in GRID {
let migrated = SHAPED_MIGRATED.contains(&name);
if !migrated && name != CANARY {
continue;
}
let (mut preset_off_sum, mut preset_cells) = (0f32, 0u32);
for &key in &KEYS {
for &vel in &VELS {
let (r, _, _, spread) = measure(program, bank, key, vel);
let (h_rms, h_spread) = head(name, key, vel);
let h_rms = if name == CANARY {
h_rms + (2.350 - 2.119) * 20.0 * (vel as f32 / 127.0).log10()
} else {
h_rms
};
let o = r - h_rms;
if migrated {
let tol = if name == "PIZZ" {
0.5
} else {
2.5f32.max(h_spread)
};
assert!(
o.abs() <= tol,
"{name} {key} {vel}: G7 parity {o:+.2} dB > tol {tol:.2} (legacy spread {h_spread:.1})"
);
assert!(
spread <= 2.5,
"{name} {key} {vel}: seed spread {spread:.2} dB > 2.5 (legacy {h_spread:.1})"
);
preset_off_sum += o;
preset_cells += 1;
} else {
assert!(
o.abs() <= 0.05,
"canary {name} {key} {vel} drift {o:+.3} dB"
);
}
}
}
if migrated {
let mean = preset_off_sum / preset_cells as f32;
assert!(
mean.abs() <= 0.5,
"{name}: mean loudness offset {mean:+.2} dB > 0.5 — exc_trim needs re-fitting"
);
}
}
}
#[test]
fn head_baseline_documents_the_fierceness_gap() {
assert_eq!(HEAD_BASELINE.len(), GRID.len() * KEYS.len() * VELS.len());
for &(name, _, _) in GRID {
let cells = HEAD_BASELINE.iter().filter(|r| r.0 == name).count();
assert_eq!(cells, KEYS.len() * VELS.len(), "{name} cell count");
}
for &(name, key, vel, rms_db, asr, tilt, spread) in HEAD_BASELINE {
assert!(
rms_db.is_finite() && (-70.0..0.0).contains(&rms_db),
"{name} {key} {vel}: rms {rms_db}"
);
assert!(
asr.is_finite() && asr > 0.0,
"{name} {key} {vel}: att/sus {asr}"
);
assert!(tilt.is_finite() && (-30.0..10.0).contains(&tilt));
assert!(spread >= 0.0);
}
let fierce = HEAD_BASELINE.iter().filter(|r| r.4 > 2.0).count();
assert!(
fierce * 2 > HEAD_BASELINE.len(),
"expected most cells fierce; only {fierce}/{}",
HEAD_BASELINE.len()
);
let max_asr = HEAD_BASELINE.iter().map(|r| r.4).fold(0f32, f32::max);
assert!(
max_asr > 10.0,
"fiercest att/sus {max_asr} (expected the high-key blow-up)"
);
let max_spread = HEAD_BASELINE.iter().map(|r| r.6).fold(0f32, f32::max);
assert!(
max_spread > 4.0,
"max seed spread {max_spread} dB (expected the h1 luck)"
);
}
#[test]
fn fluidr3_golden_is_the_gentle_reference() {
const D_PROGS: [u8; 6] = [24, 25, 32, 33, 45, 46];
assert_eq!(FLUIDR3_GOLDEN.len(), D_PROGS.len() * 3 * 2); for &(prog, key, vel, asr, tilt, crest, flat, e) in FLUIDR3_GOLDEN {
assert!(D_PROGS.contains(&prog), "unexpected program {prog}");
assert!((40..=64).contains(&key) && (vel == 60 || vel == 100));
assert!(asr.is_finite() && asr > 0.0, "prog {prog}: att/sus {asr}");
assert!(tilt.is_finite() && (-30.0..0.0).contains(&tilt));
assert!(crest.is_finite() && crest > 1.0);
assert!((0.0..=1.0).contains(&flat));
assert!(e.is_finite() && e > 0.0);
}
let fluid_max = FLUIDR3_GOLDEN.iter().map(|r| r.3).fold(0f32, f32::max);
let head_max = HEAD_BASELINE.iter().map(|r| r.4).fold(0f32, f32::max);
assert!(fluid_max < 3.5, "FluidR3 fiercest att/sus {fluid_max}");
assert!(
head_max > 2.5 * fluid_max,
"HEAD ({head_max}) should be far fiercer than FluidR3 ({fluid_max})"
);
}
}