use crate::dsp::{key_freq, Biquad, DelayLine, OnePole, Rng};
use crate::midi::{EvKind, Song};
use crate::reverb::{CathedralReverb, Reverb};
use crate::{drums, voices};
use std::f32::consts::{FRAC_PI_2, TAU};
const BLOCK: usize = 64;
const VIB_RATE_HZ: f32 = 5.3; const VIB_DEPTH_CENTS: f32 = 35.0; const ST_CC1_VIB_DEPTH: f32 = 0.012; const LESLIE_SLOW_HZ: f32 = 0.9; const LESLIE_FAST_HZ: f32 = 6.8; const LESLIE_INERTIA_S: f32 = 1.5; const LESLIE_DEPTH_ADD: f32 = 0.10;
const CATHEDRAL_WET_SCALE: f32 = 1.30;
const ORGAN_SWELL_KNEE: f32 = 0.25;
const ORGAN_SWELL_GAMMA: f32 = 1.6;
const WAH_MIN_HZ: f32 = 300.0;
const WAH_MAX_HZ: f32 = 12000.0;
const WAH_Q: f32 = 1.4;
const WAH_SLEW_S: f32 = 0.02;
const RES_MIN_Q: f32 = 0.7;
const RES_MAX_Q: f32 = 8.0;
const PORTA_MIN_S: f32 = 0.005;
const PORTA_MAX_S: f32 = 0.6;
const ROOM_SEND: f32 = 0.35;
const DRUM_FORWARD: f32 = 1.0;
const AT_VIB_RATE_HZ: f32 = 5.0;
const AT_VIB_CENTS: f32 = 25.0; const AT_GAIN_DB: f32 = 2.5; const BAGPIPE_DRONE_KEY: u8 = u8::MAX;
const BAGPIPE_DRONE_HANG_S: f32 = 1.0;
fn vibrato_family(program: u8) -> bool {
matches!(
program,
22 | 24..=27 | 29..=46 | 48..=54 | 56..=71 | 72..=79 | 80..=87 | 104..=107
| 109..=111
)
}
fn organ_leslie_family(program: u8, bank: u8) -> bool {
matches!(program, 16..=18) || (program == 19 && bank != 2)
}
fn cathedral_organ(program: u8, bank: u8) -> bool {
program == 19 && bank == 2
}
fn cc1_pitch_vibrato_target(program: u8, alt: bool) -> bool {
vibrato_family(program) && !(alt && matches!(program, 52..=54 | 22))
}
fn aftertouch_family(program: u8) -> bool {
vibrato_family(program)
|| matches!(
program,
0..=23 | 47 | 48..=54 | 80..=95 | 96 | 98 | 100 | 102 | 108
)
}
fn vowel_family(program: u8) -> bool {
matches!(program, 52..=54 | 85 | 91)
}
type Vowel = ([f32; 3], [f32; 3], [f32; 3]);
type VowelAnchor = (f32, [f32; 3], [f32; 3], [f32; 3]);
const VOWEL_ANCHORS: [VowelAnchor; 4] = [
(
0.0,
[500.0, 1400.0, 2400.0],
[12.0, 10.0, 9.0],
[1.0, 0.30, 0.10],
), (
42.0,
[350.0, 600.0, 2400.0],
[10.0, 12.0, 9.0],
[1.0, 0.35, 0.10],
), (
84.0,
[600.0, 1040.0, 2250.0],
[9.0, 10.0, 9.0],
[1.0, 0.60, 0.35],
), (
127.0,
[400.0, 1900.0, 2600.0],
[9.0, 10.0, 9.0],
[1.0, 0.85, 0.50],
), ];
pub(crate) fn vowel_at(pos: f32) -> Vowel {
let p = pos.clamp(0.0, 127.0);
let hi = VOWEL_ANCHORS
.iter()
.position(|a| a.0 >= p)
.unwrap_or(VOWEL_ANCHORS.len() - 1);
if hi == 0 {
let a = &VOWEL_ANCHORS[0];
return (a.1, a.2, a.3);
}
let (a, b) = (&VOWEL_ANCHORS[hi - 1], &VOWEL_ANCHORS[hi]);
let t = (p - a.0) / (b.0 - a.0);
let mut f = [0f32; 3];
let mut q = [0f32; 3];
let mut g = [0f32; 3];
for i in 0..3 {
f[i] = a.1[i] + (b.1[i] - a.1[i]) * t;
q[i] = a.2[i] + (b.2[i] - a.2[i]) * t;
g[i] = a.3[i] + (b.3[i] - a.3[i]) * t;
}
(f, q, g)
}
pub(crate) fn needs_drive(prog: u8) -> bool {
matches!(prog, 29 | 30)
}
const DRIVEN_GUITAR_VOICE_LIMIT: usize = 8;
pub(crate) fn drum_pan(key: u8) -> f32 {
match key {
42 | 44 | 46 => 0.33,
41 => 0.55,
43 | 45 => 0.62,
47 | 48 => 0.42,
50 => 0.32,
51 | 53 => 0.70,
59 => 0.78,
49 | 55 => 0.25,
52 | 57 => 0.75,
_ => 0.5,
}
}
fn kit_balance(key: u8) -> f32 {
match key {
42 | 44 | 46 => 4.5, 38 | 40 => 1.8, 41 | 43 | 45 | 47 | 48 | 50 => 1.6, 49 => 0.5, 57 => 0.68, 51 | 59 => 0.55, 53 => 0.72, 52 => 0.68, 55 => 0.68, _ => 1.0, }
}
pub(crate) fn cab_biquads(sr2: f32) -> [Biquad; 5] {
cab_from_spec(cab_spec(), sr2)
}
fn cab_spec() -> CabSpec {
(
[(100.0, 1.2, 4.0), (500.0, 1.0, -3.0), (2600.0, 1.5, 5.0)],
[(4000.0, 0.9), (3800.0, 0.8)],
)
}
type CabSpec = ([(f32, f32, f32); 3], [(f32, f32); 2]);
fn cab_from_spec((peaks, cliff): CabSpec, sr2: f32) -> [Biquad; 5] {
[
Biquad::peak(peaks[0].0, peaks[0].1, peaks[0].2, sr2),
Biquad::peak(peaks[1].0, peaks[1].1, peaks[1].2, sr2),
Biquad::peak(peaks[2].0, peaks[2].1, peaks[2].2, sr2),
Biquad::lowpass(cliff[0].0, cliff[0].1, sr2),
Biquad::lowpass(cliff[1].0, cliff[1].1, sr2),
]
}
pub(crate) const CAB_CLIFF: usize = 3;
pub(crate) fn cab_biquads_lead(sr2: f32, program: u8) -> [Biquad; 5] {
cab_from_spec(cab_spec_lead(program), sr2)
}
fn cab_spec_lead(program: u8) -> CabSpec {
let (body, pres_hz, pres) = if program == 30 {
(4.5, 2600.0, 3.0) } else {
(2.5, 2800.0, 5.0) };
(
[(120.0, 1.0, 2.0), (600.0, 0.75, body), (pres_hz, 1.2, pres)],
[(4000.0, 0.9), (3800.0, 0.8)],
)
}
const MICRO_CAB_TAPS: [(f32, f32); 5] = [
(0.360, 0.110),
(0.593, 0.110),
(0.625, -0.157),
(0.783, 0.115),
(0.895, -0.238),
];
pub(crate) const MICRO_CAB_LEAD_DEPTH: f32 = 0.25;
const AMP_NRPN_MSB: u8 = 0x30;
const AMP_PARAM_COUNT: usize = 6;
const AMP_NEUTRAL: u8 = 64;
const AMP_DRIVE: usize = 0; const AMP_TONE: usize = 1; const AMP_TIGHT: usize = 2; const AMP_BODY: usize = 3; const AMP_PRES: usize = 4; const AMP_CABTONE: usize = 5;
const DRIVE_OVERSAMPLE: usize = 4;
const AMP_DRIVE_K: f32 = 2.0;
const AMP_DRIVE_POST_C: f32 = 1.6;
const AMP_EQ_DB: f32 = 15.0;
const AMP_TONE_DB: f32 = 12.0;
const AMP_TIGHT_OCT: f32 = 0.9;
const AMP_TIGHT_MAX_HZ: f32 = 200.0;
const AMP_CABTONE_OCT: f32 = 1.74;
pub(crate) struct MicroCab {
buf: Vec<f32>,
pos: usize,
taps: [(usize, f32); 5],
direct: f32,
}
impl MicroCab {
pub(crate) fn with_depth(sr2: f32, depth: f32) -> Self {
let taps_s: Vec<(usize, f32)> = MICRO_CAB_TAPS
.iter()
.map(|&(ms, g)| (((ms * 1e-3 * sr2).round() as usize).max(1), g * depth))
.collect();
let mut avg = 0.0;
let grid: Vec<f32> = (0..28).map(|k| 900.0 + k as f32 * 100.0).collect();
for &f in &grid {
let w = std::f32::consts::TAU * f / sr2;
let (mut re, mut im) = (1.0f32, 0.0f32);
for &(d, g) in &taps_s {
re += g * (w * d as f32).cos();
im -= g * (w * d as f32).sin();
}
avg += (re * re + im * im).sqrt();
}
avg /= grid.len() as f32;
let norm = 1.0 / avg.max(1e-6);
let max_d = taps_s.iter().map(|&(d, _)| d).max().unwrap_or(1);
let mut taps = [(0usize, 0f32); 5];
for (o, &(d, g)) in taps.iter_mut().zip(&taps_s) {
*o = (d, g * norm);
}
MicroCab {
buf: vec![0.0; (max_d + 1).next_power_of_two()],
pos: 0,
taps,
direct: norm,
}
}
#[inline]
pub(crate) fn process(&mut self, x: f32) -> f32 {
let mask = self.buf.len() - 1;
self.buf[self.pos] = x;
let mut y = self.direct * x;
for &(d, g) in &self.taps {
y += g * self.buf[(self.pos + self.buf.len() - d) & mask];
}
self.pos = (self.pos + 1) & mask;
y
}
}
#[derive(Clone, Copy)]
struct DriveBase {
pre_hz: f32,
voice: (f32, f32, f32), g1: f32,
g2: f32,
post: f32,
cab1: (f32, f32, f32), cab2: (f32, f32, f32), cliff: [(f32, f32); 2], }
struct Drive {
program: u8,
alt: bool,
pre: Biquad,
voice: Biquad,
g1: f32,
bias: f32,
tilt: Biquad,
g2: f32,
post: f32,
dcb: Biquad,
cab: [Biquad; 5],
micro: MicroCab,
prev: f32,
base: DriveBase,
sr_os: f32,
g1_target: f32,
g2_target: f32,
post_target: f32,
slew: f32,
active: bool,
}
impl Drive {
fn new(program: u8, alt: bool, sr: f32) -> Self {
let sr_os = sr * DRIVE_OVERSAMPLE as f32;
if alt {
let (g1, g2, post, bias, voice, tilt) = if program == 30 {
(
9.0,
3.4,
0.0714,
0.85,
(650.0, 1.0, -4.5),
Biquad::peak(800.0, 0.9, -2.0, sr_os),
)
} else {
(
8.0,
2.8,
0.0789,
0.85,
(1000.0, 0.8, 4.0),
Biquad::peak(1400.0, 0.8, 2.5, sr_os),
)
};
return Drive::from_stages(
program,
alt,
sr_os,
120.0,
voice,
g1,
bias,
tilt,
g2,
post,
cab_biquads_lead(sr_os, program),
cab_spec_lead(program),
);
}
let (g1, g2, post, bias) = if program == 30 {
(8.0, 3.0, 0.145, 0.9)
} else {
(9.0, 2.0, 0.12, 0.9)
};
let (voice, tilt) = if program == 30 {
(
(650.0, 0.9, -5.0),
Biquad::peak(700.0, 0.9, -4.0, sr_os),
)
} else {
(
(800.0, 0.8, 4.0),
Biquad::peak(1200.0, 0.8, 2.0, sr_os),
)
};
Drive::from_stages(
program,
alt,
sr_os,
90.0,
voice,
g1,
bias,
tilt,
g2,
post,
cab_biquads(sr_os),
cab_spec(),
)
}
#[allow(clippy::too_many_arguments)]
fn from_stages(
program: u8,
alt: bool,
sr_os: f32,
pre_hz: f32,
voice: (f32, f32, f32),
g1: f32,
bias: f32,
tilt: Biquad,
g2: f32,
post: f32,
cab: [Biquad; 5],
cab_base: CabSpec,
) -> Self {
let (peaks, cliff) = cab_base;
Drive {
program,
alt,
pre: Biquad::highpass(pre_hz, 0.7, sr_os),
voice: Biquad::peak(voice.0, voice.1, voice.2, sr_os),
g1,
bias,
tilt,
g2,
post,
dcb: Biquad::highpass(20.0, 0.7, sr_os),
cab,
micro: MicroCab::with_depth(sr_os, if alt { MICRO_CAB_LEAD_DEPTH } else { 1.0 }),
prev: 0.0,
base: DriveBase {
pre_hz,
voice,
g1,
g2,
post,
cab1: peaks[1],
cab2: peaks[2],
cliff,
},
sr_os,
g1_target: g1,
g2_target: g2,
post_target: post,
slew: 1.0 - (-(DRIVE_OVERSAMPLE as f32) / (WAH_SLEW_S * sr_os)).exp(),
active: false,
}
}
fn amp_sim(sr: f32, drive_0_127: u8) -> Self {
let sr_os = sr * DRIVE_OVERSAMPLE as f32;
let d = drive_0_127 as f32 / 127.0;
let g1 = 1.0 + d * d * 16.0;
let g2 = 1.5 + d * 1.5;
let bias = 0.7;
let post = 0.28 / (1.0 + d * 1.5);
let voice = (800.0, 0.8, 4.0);
let tilt = Biquad::peak(1200.0, 0.8, 2.0, sr_os);
Drive::from_stages(
0,
false,
sr_os,
90.0,
voice,
g1,
bias,
tilt,
g2,
post,
cab_biquads(sr_os),
cab_spec(),
)
}
#[inline]
fn chain(&mut self, x: f32) -> f32 {
let v = self.voice.process(self.pre.process(x));
let s1 = (v * self.g1 + self.bias).tanh() - self.bias.tanh();
let s2 = (self.tilt.process(s1) * self.g2).tanh();
let mut y = self.dcb.process(s2);
for c in &mut self.cab[..CAB_CLIFF] {
y = c.process(y);
}
y = self.micro.process(y);
for c in &mut self.cab[CAB_CLIFF..] {
y = c.process(y);
}
y
}
#[inline]
fn step(&mut self, x: f32) -> f32 {
let prev = self.prev;
self.prev = x;
for i in 1..DRIVE_OVERSAMPLE {
let t = i as f32 / DRIVE_OVERSAMPLE as f32;
let _ = self.chain(prev + (x - prev) * t);
}
self.chain(x) * self.post
}
fn flush(&mut self) {
self.pre.flush();
self.voice.flush();
self.tilt.flush();
self.dcb.flush();
for c in &mut self.cab {
c.flush();
}
}
#[cfg(test)]
fn visit_state(&self, visit: &mut dyn FnMut(&'static str, f32)) {
for bq in [&self.pre, &self.voice, &self.tilt, &self.dcb]
.into_iter()
.chain(&self.cab)
{
for x in bq.debug_state() {
visit("drive biquad", x);
}
}
for &x in &self.micro.buf {
visit("drive micro-cab line", x);
}
visit("drive upsampler", self.prev);
}
fn process(&mut self, buf: &mut [f32]) {
self.flush();
if !self.active {
for x in buf.iter_mut() {
*x = self.step(*x);
}
return;
}
for x in buf.iter_mut() {
self.g1 += self.slew * (self.g1_target - self.g1);
self.g2 += self.slew * (self.g2_target - self.g2);
self.post += self.slew * (self.post_target - self.post);
*x = self.step(*x);
}
}
fn apply_params(&mut self, cur: &[f32; AMP_PARAM_COUNT], snap: bool) {
let sr_os = self.sr_os;
let n = |i: usize| (cur[i] - AMP_NEUTRAL as f32) / AMP_NEUTRAL as f32;
let nd = n(AMP_DRIVE);
self.g1_target = self.base.g1 * 2f32.powf(AMP_DRIVE_K * nd);
self.g2_target = self.base.g2 * 2f32.powf(0.5 * AMP_DRIVE_K * nd);
self.post_target = self.base.post * 2f32.powf(-AMP_DRIVE_POST_C * nd);
let (vh, vq, vdb) = self.base.voice;
self.voice
.retune_peak(vh, vq, vdb + AMP_TONE_DB * n(AMP_TONE), sr_os);
let pre_hz =
(self.base.pre_hz * 2f32.powf(AMP_TIGHT_OCT * n(AMP_TIGHT))).min(AMP_TIGHT_MAX_HZ);
self.pre.retune_highpass(pre_hz, 0.7, sr_os);
let (b1h, b1q, b1db) = self.base.cab1;
self.cab[1].retune_peak(b1h, b1q, b1db + AMP_EQ_DB * n(AMP_BODY), sr_os);
let (b2h, b2q, b2db) = self.base.cab2;
self.cab[2].retune_peak(b2h, b2q, b2db + AMP_EQ_DB * n(AMP_PRES), sr_os);
let f = 2f32.powf(AMP_CABTONE_OCT * n(AMP_CABTONE).min(0.0));
self.cab[3].retune_lowpass(self.base.cliff[0].0 * f, self.base.cliff[0].1, sr_os);
self.cab[4].retune_lowpass(self.base.cliff[1].0 * f, self.base.cliff[1].1, sr_os);
if snap || !self.active {
self.g1 = self.g1_target;
self.g2 = self.g2_target;
self.post = self.post_target;
}
self.active = true;
}
#[cfg(test)]
fn with_flat_voice(mut self) -> Self {
self.voice = Biquad::peak(800.0, 0.8, 0.0, 88_200.0);
self
}
}
fn fx_profile(program: u8, bank: u8) -> (f32, f32) {
match program {
19 if bank == 2 => (0.0, 0.0), 16..=23 => (0.20, 0.0), 24 | 25 => (0.12, 0.08), 26..=31 => (0.10, 0.30), 42 | 43 => (0.0, 0.06), 40..=45 | 110 => (0.10, 0.10), 46 => (0.15, 0.0), 48..=51 => (0.35, 0.0), 52..=54 => (0.30, 0.0), 56..=60 => (0.0, 0.0), 61..=63 => (0.25, 0.0), 64..=67 => (0.06, 0.10), 109 => (0.06, 0.0), 111 => (0.04, 0.08), 72..=79 => (0.0, 0.22), 80..=87 => (0.15, 0.25), 88..=95 => (0.45, 0.0), 96 => (0.30, 0.20), 97 => (0.35, 0.25), 98 => (0.30, 0.35), 99 => (0.30, 0.25), 100 => (0.30, 0.30), 101 => (0.25, 0.30), 102 => (0.30, 0.05), 103 => (0.20, 0.30), 8 | 10 => (0.0, 0.06), 9 => (0.0, 0.0), 14 => (0.0, 0.08), 15 => (0.10, 0.0), _ => (0.0, 0.0),
}
}
#[rustfmt::skip] pub(crate) const PROGRAM_TRIM_DB: [f32; 128] = [
0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 6.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, -6.0, 0.0, -4.5, -3.0, -1.5, -6.0, -3.0, -5.0, -1.0, -4.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -4.0, -4.0, -3.5, -6.0, -4.0, 0.0, 0.0, 0.0, 6.0, 1.5, 6.0, 5.0, 2.5, 6.0, 5.0, 0.0, -6.0, -6.0, -6.0, -2.0, -3.0, -2.0, 0.0, 5.0, 0.0, -2.5, -3.0, -3.0, 2.0, 1.5, 0.0, 0.0, -4.0, -4.0, -2.0, -5.0, -6.0, -6.0, 0.0, 0.0, -4.0, 0.0, 0.0, 0.0, -2.0, 5.5, 1.0, 1.0, 4.0, 0.0, 2.0, 5.0, 3.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ];
#[inline]
pub(crate) fn program_trim_lin(program: u8) -> f32 {
let db = PROGRAM_TRIM_DB[program as usize];
if db == 0.0 {
1.0
} else {
10f32.powf(db / 20.0)
}
}
struct Chorus {
dl: DelayLine,
phase: f32,
rate: f32,
base: f32,
depth: f32,
sr: f32,
}
impl Chorus {
fn new(sr: f32) -> Self {
Chorus {
dl: DelayLine::new((0.040 * sr) as usize),
phase: 0.0,
rate: 0.35,
base: 0.018 * sr,
depth: 0.005 * sr,
sr,
}
}
fn process(&mut self, send: &[f32], l: &mut [f32], r: &mut [f32]) {
for i in 0..send.len() {
self.dl.push(send[i]);
self.phase += TAU * self.rate / self.sr;
if self.phase > TAU {
self.phase -= TAU;
}
let tl = self.dl.tap(self.base + self.depth * self.phase.sin());
let tr = self
.dl
.tap(self.base + self.depth * (self.phase + FRAC_PI_2).sin());
l[i] += tl * 0.55;
r[i] += tr * 0.55;
}
}
fn reconfigure(&mut self, rate_hz: f32, base_s: f32, depth_s: f32) {
assert!(
base_s + depth_s <= 0.040,
"chorus sweep {base_s}+{depth_s}s exceeds the 40 ms line"
);
self.rate = rate_hz;
self.base = base_s * self.sr;
self.depth = depth_s * self.sr;
}
}
struct PingPong {
left: DelayLine,
right: DelayLine,
time: f32,
feedback: f32,
lp_l: OnePole,
lp_r: OnePole,
}
impl PingPong {
fn new(sr: f32, time_s: f32) -> Self {
let samples = (time_s * sr).max(64.0);
PingPong {
left: DelayLine::new(samples as usize + 4),
right: DelayLine::new(samples as usize + 4),
time: samples,
feedback: 0.34,
lp_l: OnePole::lowpass(3200.0, sr),
lp_r: OnePole::lowpass(3200.0, sr),
}
}
fn process(&mut self, send: &[f32], l: &mut [f32], r: &mut [f32]) {
self.lp_l.flush();
self.lp_r.flush();
for i in 0..send.len() {
let out_l = self.left.tap(self.time);
let out_r = self.right.tap(self.time);
self.left.push(crate::dsp::flush_denormal(
send[i] + self.lp_r.process(out_r) * self.feedback,
));
self.right.push(crate::dsp::flush_denormal(
self.lp_l.process(out_l) * self.feedback,
));
l[i] += out_l * 0.8;
r[i] += out_r * 0.8;
}
}
}
struct Sympathetic {
combs: Vec<(DelayLine, f32, OnePole)>,
hp: Biquad,
feedback: f32,
input: f32,
ret: f32,
}
impl Sympathetic {
fn new(
sr: f32,
freqs: &[f32],
damp_hz: f32,
feedback: f32,
hp_hz: f32,
input: f32,
ret: f32,
) -> Self {
let combs = freqs
.iter()
.map(|&f| {
let d = sr / f;
(
DelayLine::new(d as usize + 4),
d,
OnePole::lowpass(damp_hz, sr),
)
})
.collect();
Sympathetic {
combs,
hp: Biquad::highpass(hp_hz, 0.7, sr),
feedback,
input,
ret,
}
}
fn piano(sr: f32) -> Self {
let freqs: Vec<f32> = (0..12)
.map(|k| 130.81 * 2f32.powf(k as f32 / 12.0))
.collect();
Self::new(sr, &freqs, 2600.0, 0.85, 170.0, 0.05, 0.55)
}
fn guitar(sr: f32) -> Self {
Self::new(
sr,
&[82.41, 110.0, 146.83, 196.0, 246.94, 329.63],
3400.0,
0.85,
120.0,
0.03,
0.30,
)
}
fn sitar(sr: f32) -> Self {
let freqs: Vec<f32> = (0..13)
.map(|k| 261.63 * 2f32.powf(k as f32 / 12.0))
.collect();
let damp_hz = 5200.0;
let mut s = Self::new(sr, &freqs, damp_hz, 0.955, 200.0, 0.035, 0.35);
let a = 1.0 - (-2.0 * std::f32::consts::PI * (damp_hz / sr).min(0.49)).exp();
let b = 1.0 - a;
for ((_, d, _), &f) in s.combs.iter_mut().zip(freqs.iter()) {
let w = 2.0 * std::f32::consts::PI * f / sr;
let d1p = ((b * w.sin()) / (1.0 - b * w.cos())).atan() / w;
*d = (sr / f - d1p - 1.0).max(2.0);
}
s
}
fn process(&mut self, send: &[f32], l: &mut [f32], r: &mut [f32]) {
self.hp.flush();
for (_, _, damp) in &mut self.combs {
damp.flush();
}
for i in 0..send.len() {
let x = self.hp.process(send[i]) * self.input;
let mut sum = 0.0;
for (dl, d, damp) in &mut self.combs {
let y = crate::dsp::flush_denormal(dl.tap(*d));
dl.push(x + damp.process(y) * self.feedback);
sum += y;
}
let w = sum * self.ret;
l[i] += w;
r[i] += w;
}
}
}
struct BusGlue {
env: f32,
gain: f32,
atk: f32,
rel: f32,
thr: f32,
shelf_l: Biquad,
shelf_r: Biquad,
}
impl BusGlue {
fn new(sr: f32) -> Self {
BusGlue {
env: 0.0,
gain: 1.0,
atk: 1.0 - (-1.0 / (0.015 * sr)).exp(),
rel: 1.0 - (-1.0 / (0.250 * sr)).exp(),
thr: 0.32,
shelf_l: Biquad::peak(95.0, 0.7, 1.5, sr),
shelf_r: Biquad::peak(95.0, 0.7, 1.5, sr),
}
}
fn process(&mut self, l: &mut [f32], r: &mut [f32]) {
self.shelf_l.flush();
self.shelf_r.flush();
for i in 0..l.len() {
let xl = self.shelf_l.process(l[i]);
let xr = self.shelf_r.process(r[i]);
let level = xl.abs().max(xr.abs());
let k = if level > self.env { self.atk } else { self.rel };
self.env = crate::dsp::flush_denormal(self.env + k * (level - self.env));
let target = if self.env > self.thr {
(self.thr / self.env).sqrt() } else {
1.0
};
let kg = if target < self.gain {
self.atk
} else {
self.rel
};
self.gain += kg * (target - self.gain);
let gl = xl * self.gain;
let gr = xr * self.gain;
l[i] = gl + 0.12 * ((gl * 1.4).tanh() / 1.4 - gl);
r[i] = gr + 0.12 * ((gr * 1.4).tanh() / 1.4 - gr);
}
}
}
struct Strip {
program: u8,
kit: drums::Kit, alt_bank: bool, alt_bank_value: u8, xg_drum: bool, gs_drum: bool, volume: f32, pan: f32, volume_target: f32,
volume_authored: bool,
volume_primed: bool,
pan_target: f32,
pan_authored: bool,
pan_primed: bool,
bend: f32, legato: bool, sustain: bool, bend_wheel: f32, bend_range: f32, fine: f32, rpn_msb: u8, rpn_lsb: u8,
data_msb: u8, nrpn_msb: u8,
nrpn_lsb: u8,
nrpn_data_msb: u8,
porta_on: bool,
porta_time: f32, last_freq: Option<f32>, bank_lsb: u8,
porta_control: Option<u8>,
bagpipe_drone_holds: u8, bagpipe_drone_live: bool, bagpipe_drone_hang: u32, soft: bool, sost_down: bool, vowel_authored: bool, vowel_target: f32, vowel_cur: f32, at_authored: bool, at_target: f32, at_cur: f32,
at_phase: f32, at_gain: f32, vib_mult: f32, res: f32, res_target: f32,
expr_target: f32,
expr: f32,
expr_authored: bool,
breath_authored: bool,
breath_target: f32,
breath: f32,
mod_target: f32, mod_cur: f32,
mod_engaged: bool, mod_authored: bool, vib_phase: f32,
leslie_rate: f32, leslie_depth: f32,
reverb_send: f32,
reverb_authored: bool,
chorus_send: f32,
chorus_authored: bool,
delay_send: f32,
delay_authored: bool,
organ_wind: f32,
organ_trem_phase: f32,
drive: Option<Drive>,
amp_params: [u8; AMP_PARAM_COUNT],
amp_cur: [f32; AMP_PARAM_COUNT],
amp_primed: [bool; AMP_PARAM_COUNT],
amp_authored: bool,
xg_insert: Option<(Drive, f32)>,
wah: Option<Biquad>, wah_legacy: Option<Biquad>,
wah_cathedral: Option<Biquad>,
wah_r: Option<Biquad>,
cutoff: f32, cutoff_target: f32,
haas: DelayLine,
haas_delay: f32, }
impl Strip {
#[inline]
fn drum_room_send(&self) -> f32 {
if self.reverb_authored {
ROOM_SEND * self.reverb_send
} else {
ROOM_SEND
}
}
fn amp_prime_all(&mut self) {
for i in 0..AMP_PARAM_COUNT {
if !self.amp_primed[i] {
self.amp_cur[i] = self.amp_params[i] as f32;
self.amp_primed[i] = true;
}
}
}
fn new(sr: f32) -> Self {
Strip {
program: 0,
kit: drums::Kit::V3,
alt_bank: false,
alt_bank_value: 0,
xg_drum: false,
gs_drum: false,
volume: (100.0f32 / 127.0).powi(2),
pan: 0.5,
volume_target: (100.0f32 / 127.0).powi(2),
volume_authored: false,
volume_primed: false,
pan_target: 0.5,
pan_authored: false,
pan_primed: false,
bend: 1.0,
legato: false,
sustain: false,
bend_wheel: 0.0,
bend_range: 2.0,
fine: 1.0,
rpn_msb: 127,
rpn_lsb: 127,
data_msb: 0,
nrpn_msb: 127,
nrpn_lsb: 127,
nrpn_data_msb: 0,
porta_on: false,
porta_time: PORTA_MIN_S,
last_freq: None,
bank_lsb: 0,
porta_control: None,
bagpipe_drone_holds: 0,
bagpipe_drone_live: false,
bagpipe_drone_hang: 0,
soft: false,
sost_down: false,
vowel_authored: false,
vowel_target: 0.0,
vowel_cur: 0.0,
at_authored: false,
at_target: 0.0,
at_cur: 0.0,
at_phase: 0.0,
at_gain: 1.0,
vib_mult: 1.0,
res: WAH_Q,
res_target: WAH_Q,
expr_target: 1.0,
expr: 1.0,
expr_authored: false,
breath_authored: false,
breath_target: 1.0,
breath: 1.0,
mod_target: 0.0,
mod_cur: 0.0,
mod_engaged: false,
mod_authored: false,
vib_phase: 0.0,
leslie_rate: 0.0,
leslie_depth: 0.0,
reverb_send: 0.3,
reverb_authored: false,
chorus_send: 0.0,
chorus_authored: false,
delay_send: 0.0,
delay_authored: false,
organ_wind: 0.0,
organ_trem_phase: 0.0,
drive: None,
amp_params: [AMP_NEUTRAL; AMP_PARAM_COUNT],
amp_cur: [AMP_NEUTRAL as f32; AMP_PARAM_COUNT],
amp_primed: [false; AMP_PARAM_COUNT],
amp_authored: false,
xg_insert: None,
wah: None,
wah_legacy: None,
wah_cathedral: None,
wah_r: None,
cutoff: WAH_MAX_HZ,
cutoff_target: WAH_MAX_HZ,
haas: DelayLine::new((0.006 * sr) as usize + 4),
haas_delay: 0.0,
}
}
}
pub(crate) const MIN_SAMPLE_RATE: u32 = 8_000;
pub(crate) const MAX_SAMPLE_RATE: u32 = 384_000;
pub(crate) const MAX_TAIL_S: f32 = 3_600.0;
pub(crate) const MAX_ECHO_S: f32 = 10.0;
pub(crate) const MAX_MASTER_GAIN: f32 = 8.0;
pub(crate) fn sanitize_knob(value: f32, lo: f32, hi: f32, fallback: f32) -> f32 {
if value.is_finite() {
value.clamp(lo, hi)
} else {
fallback
}
}
pub(crate) fn sanitize_sample_rate(rate: u32) -> u32 {
rate.clamp(MIN_SAMPLE_RATE, MAX_SAMPLE_RATE)
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Options {
pub(crate) sr: f32,
pub(crate) wet: f32,
pub(crate) tail: f32,
pub(crate) delay_s: f32,
pub(crate) samples: bool,
pub(crate) solo: u16,
}
impl Default for Options {
fn default() -> Self {
Self {
sr: 44_100.0,
wet: 0.32,
tail: 6.0,
delay_s: 0.375,
samples: true,
solo: 0xFFFF,
}
}
}
impl Options {
pub fn with_sample_rate(mut self, sr: u32) -> Self {
self.sr = sanitize_sample_rate(sr) as f32;
self
}
pub fn with_reverb(mut self, wet: f32) -> Self {
self.wet = sanitize_knob(wet, 0.0, 1.0, Self::default().wet);
self
}
pub fn with_tail(mut self, tail: f32) -> Self {
self.tail = sanitize_knob(tail, 0.0, MAX_TAIL_S, Self::default().tail);
self
}
pub fn with_samples(mut self, samples: bool) -> Self {
self.samples = samples;
self
}
pub fn with_echo(mut self, delay_s: f32) -> Self {
self.delay_s = sanitize_knob(delay_s, 0.0, MAX_ECHO_S, Self::default().delay_s);
self
}
pub fn with_solo(mut self, solo: u16) -> Self {
self.solo = solo;
self
}
pub fn sample_rate(&self) -> u32 {
self.sr as u32
}
pub fn reverb(&self) -> f32 {
self.wet
}
pub fn tail(&self) -> f32 {
self.tail
}
pub fn echo(&self) -> f32 {
self.delay_s
}
pub fn samples(&self) -> bool {
self.samples
}
pub fn solo(&self) -> u16 {
self.solo
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct Stats {
pub voices_spawned: u64,
pub peak: f32,
pub max_polyphony: usize,
#[cfg(test)]
pub(crate) cathedral_return_peak: f32,
}
impl Default for Stats {
fn default() -> Self {
Self {
voices_spawned: 0,
peak: 0.0,
max_polyphony: 0,
#[cfg(test)]
cathedral_return_peak: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct Progress {
pub rendered_seconds: f64,
pub total_seconds: f64,
pub active_voices: usize,
}
impl Progress {
pub fn fraction(&self) -> f64 {
if self.total_seconds <= 0.0 {
return 1.0;
}
(self.rendered_seconds / self.total_seconds).clamp(0.0, 1.0)
}
}
struct Active {
ch: u8,
key: u8,
program: u8, held: bool, sost: bool, sost_held: bool, glide: Option<(f32, f32)>,
alt: bool, alt_bank_value: u8, bank_lsb: u8,
is_drum: bool,
poly_authored: bool,
poly_target: f32, poly_cur: f32, poly_phase: f32, poly_mult: f32, poly_gain: f32, voice: Box<dyn voices::Voice>,
}
#[derive(Clone, Copy)]
pub(crate) struct CoreOptions {
pub sr: f32,
pub wet: f32,
pub delay_s: f32,
pub samples: bool,
pub solo: u16,
pub gtr_symp_on: bool,
pub drum_room_on: bool,
pub sitar_symp_on: bool,
}
impl CoreOptions {
fn from_options(
opt: &Options,
gtr_symp_on: bool,
drum_room_on: bool,
sitar_symp_on: bool,
) -> Self {
Self {
sr: opt.sr,
wet: opt.wet,
delay_s: opt.delay_s,
samples: opt.samples,
solo: opt.solo,
gtr_symp_on,
drum_room_on,
sitar_symp_on,
}
}
}
const DEFAULT_HALL_ROOM: f32 = 0.86;
const DEFAULT_HALL_DAMP: f32 = 0.35;
const HALL1_TYPE: [u8; 2] = [0x01, 0x00];
const HALL1_ROOM: f32 = 0.92;
const HALL1_DAMP: f32 = 0.22;
const CHORUS1_TYPE: [u8; 2] = [0x41, 0x00];
const CHORUS1_RATE: f32 = 0.8;
const CHORUS1_BASE_S: f32 = 0.018;
const CHORUS1_DEPTH_S: f32 = 0.006;
const DEFAULT_CHORUS_RATE: f32 = 0.35;
const DEFAULT_CHORUS_BASE_S: f32 = 0.018;
const DEFAULT_CHORUS_DEPTH_S: f32 = 0.005;
const AMP_SIM_TYPE_MSB: u8 = 0x4B;
const XG_VAR_INSERTION: u8 = 0x00;
#[derive(Clone, Copy)]
struct XgEffects {
var_type_msb: u8, var_type_lsb: u8, var_connection: u8, var_part: u8, var_drive: u8, var_drywet: u8, }
impl XgEffects {
fn new() -> Self {
XgEffects {
var_type_msb: 0,
var_type_lsb: 0,
var_connection: 0,
var_part: 127,
var_drive: 0,
var_drywet: 127,
}
}
}
pub(crate) struct EngineCore {
opt: CoreOptions,
strips: Vec<Strip>,
active: Vec<Active>,
voice_by_ch: [Vec<usize>; 16],
reverb: Reverb,
cathedral: CathedralReverb,
rev_hp: Biquad,
#[cfg(test)]
cathedral_send_hp: Biquad,
#[cfg(test)]
cathedral_send_hpf_on: bool,
chorus: Chorus,
echo: Option<PingPong>,
symp: Sympathetic,
gtr_symp: Sympathetic,
sitar_symp: Sympathetic,
sitar_seen: bool,
drum_room: Reverb,
glue: BusGlue,
stats: Stats,
voice_seed_index: u64,
ch_buf: Vec<[f32; BLOCK]>,
legacy_buf: Vec<[f32; BLOCK]>,
cathedral_buf: Vec<[f32; BLOCK]>,
scratch: [f32; BLOCK],
send_rev: [f32; BLOCK],
send_cathedral: [f32; BLOCK],
send_cho: [f32; BLOCK],
send_del: [f32; BLOCK],
send_sym: [f32; BLOCK],
send_sym_gtr: [f32; BLOCK],
send_sym_sitar: [f32; BLOCK],
send_room: [f32; BLOCK],
drum_l: [f32; BLOCK],
drum_r: [f32; BLOCK],
mix_l: [f32; BLOCK],
mix_r: [f32; BLOCK],
expr_smooth: f32,
leslie_k: f32,
wah_smooth: f32,
drum_rr: [u8; 128],
fret_noise_rr: [usize; 16],
now: u64,
key_on_at: Vec<[u64; 128]>,
pitched_rr: Vec<[u8; 128]>,
xg: XgEffects,
}
const TREM_IOI_MAX_S: f32 = 0.10;
const NOTE_VOICE_SEED_XOR: u32 = 0x9E37;
const VOICE_SEED_STEP: u32 = 2_654_435_761;
pub(crate) fn note_voice_seed(index: u64) -> u32 {
NOTE_VOICE_SEED_XOR ^ (index as u32).wrapping_mul(VOICE_SEED_STEP)
}
impl EngineCore {
pub(crate) fn new(opt: CoreOptions) -> Self {
let opt = CoreOptions {
samples: opt.samples && crate::embedded_samples_available(),
..opt
};
let sr = opt.sr;
Self {
opt,
strips: (0..16).map(|_| Strip::new(sr)).collect(),
active: Vec::new(),
voice_by_ch: std::array::from_fn(|_| Vec::with_capacity(64)),
reverb: Reverb::new(sr, DEFAULT_HALL_ROOM, DEFAULT_HALL_DAMP, opt.wet),
cathedral: CathedralReverb::new(sr, opt.wet * CATHEDRAL_WET_SCALE),
rev_hp: Biquad::highpass(150.0, 0.7, sr),
#[cfg(test)]
cathedral_send_hp: Biquad::highpass(150.0, 0.7, sr),
#[cfg(test)]
cathedral_send_hpf_on: false,
chorus: Chorus::new(sr),
echo: (opt.delay_s > 0.0).then(|| PingPong::new(sr, opt.delay_s)),
symp: Sympathetic::piano(sr),
gtr_symp: Sympathetic::guitar(sr),
sitar_symp: Sympathetic::sitar(sr),
sitar_seen: false,
drum_room: Reverb::with_predelay(sr, 0.42, 0.55, opt.wet * 0.9, 0.003),
glue: BusGlue::new(sr),
stats: Stats::default(),
voice_seed_index: 0,
ch_buf: vec![[0f32; BLOCK]; 16],
legacy_buf: vec![[0f32; BLOCK]; 16],
cathedral_buf: vec![[0f32; BLOCK]; 16],
scratch: [0f32; BLOCK],
send_rev: [0f32; BLOCK],
send_cathedral: [0f32; BLOCK],
send_cho: [0f32; BLOCK],
send_del: [0f32; BLOCK],
send_sym: [0f32; BLOCK],
send_sym_gtr: [0f32; BLOCK],
send_sym_sitar: [0f32; BLOCK],
send_room: [0f32; BLOCK],
drum_l: [0f32; BLOCK],
drum_r: [0f32; BLOCK],
mix_l: [0f32; BLOCK],
mix_r: [0f32; BLOCK],
expr_smooth: 1.0 - (-(BLOCK as f32) / (0.03 * sr)).exp(),
leslie_k: 1.0 - (-(BLOCK as f32) / (LESLIE_INERTIA_S * sr)).exp(),
wah_smooth: 1.0 - (-(BLOCK as f32) / (WAH_SLEW_S * sr)).exp(),
drum_rr: [0; 128],
fret_noise_rr: [0; 16],
now: 0,
key_on_at: vec![[u64::MAX; 128]; 16],
pitched_rr: vec![[0u8; 128]; 16],
xg: XgEffects::new(),
}
}
pub(crate) fn hard_reset(&mut self) {
let opt = self.opt;
*self = Self::new(opt);
}
fn gm_system_on(&mut self) {
let stats = self.stats;
self.hard_reset();
self.stats = stats;
}
pub(crate) fn active_voice_count(&self) -> usize {
self.active.len()
}
pub(crate) fn stats(&self) -> Stats {
self.stats
}
fn is_bagpipe_drone(a: &Active, ch: u8) -> bool {
a.ch == ch && a.program == 109 && a.key == BAGPIPE_DRONE_KEY
}
fn ensure_bagpipe_drone(&mut self, ch: u8, key: u8, vel: u8) {
if self
.active
.iter()
.any(|a| Self::is_bagpipe_drone(a, ch) && !a.voice.released())
{
return;
}
let seed = 0xBA60 ^ (self.voice_seed_index as u32).wrapping_mul(VOICE_SEED_STEP);
let sampled = self.opt.samples && !self.strips[ch as usize].alt_bank;
let voice: Box<dyn voices::Voice> = if sampled {
Box::new(voices::bagpipe_drone_sampled(key, self.opt.sr))
} else {
Box::new(voices::bagpipe_drone(key, vel, self.opt.sr, seed))
};
self.active.push(Active {
ch,
key: BAGPIPE_DRONE_KEY,
program: 109,
held: false,
sost: false,
sost_held: false,
glide: None,
alt: false,
alt_bank_value: 0,
bank_lsb: 0, is_drum: false, poly_authored: false,
poly_target: 0.0,
poly_cur: 0.0,
poly_phase: 0.0,
poly_mult: 1.0,
poly_gain: 1.0,
voice,
});
self.stats.voices_spawned += 1;
self.voice_seed_index += 1;
self.strips[ch as usize].bagpipe_drone_live = true;
self.strips[ch as usize].bagpipe_drone_hang = 0;
}
fn release_bagpipe_drone_if_idle(&mut self, ch: u8) {
if self.strips[ch as usize].bagpipe_drone_holds > 0 {
return;
}
let has_chant = self.active.iter().any(|a| {
a.ch == ch && a.program == 109 && a.key != BAGPIPE_DRONE_KEY && !a.voice.released()
});
if has_chant {
return;
}
for a in self
.active
.iter_mut()
.filter(|a| Self::is_bagpipe_drone(a, ch) && !a.voice.released())
{
a.voice.note_off();
}
self.strips[ch as usize].bagpipe_drone_live = false;
self.strips[ch as usize].bagpipe_drone_hang = 0;
}
fn tick_bagpipe_drone_latch(&mut self) {
let hang_blocks = (BAGPIPE_DRONE_HANG_S * self.opt.sr / BLOCK as f32) as u32;
for ci in 0..self.strips.len() {
if !self.strips[ci].bagpipe_drone_live {
continue;
}
let ch = ci as u8;
let has_drone = self
.active
.iter()
.any(|a| Self::is_bagpipe_drone(a, ch) && !a.voice.released());
if !has_drone {
self.strips[ci].bagpipe_drone_live = false;
self.strips[ci].bagpipe_drone_hang = 0;
continue;
}
let holding = self.strips[ci].bagpipe_drone_holds > 0
|| self.active.iter().any(|a| {
a.ch == ch
&& a.program == 109
&& a.key != BAGPIPE_DRONE_KEY
&& !a.voice.released()
});
if holding {
self.strips[ci].bagpipe_drone_hang = 0;
} else {
self.strips[ci].bagpipe_drone_hang += 1;
if self.strips[ci].bagpipe_drone_hang >= hang_blocks {
for a in self
.active
.iter_mut()
.filter(|a| Self::is_bagpipe_drone(a, ch) && !a.voice.released())
{
a.voice.note_off();
}
self.strips[ci].bagpipe_drone_live = false;
self.strips[ci].bagpipe_drone_hang = 0;
}
}
}
}
fn make_room_for_driven_guitar(&mut self, ch: u8) {
let active = self
.active
.iter()
.filter(|a| a.ch == ch && needs_drive(a.program) && !a.voice.released())
.count();
let release_count = active.saturating_sub(DRIVEN_GUITAR_VOICE_LIMIT - 1);
for a in self
.active
.iter_mut()
.filter(|a| a.ch == ch && needs_drive(a.program) && !a.voice.released())
.take(release_count)
{
a.held = false;
a.sost = false;
a.sost_held = false;
a.voice.note_off();
}
}
pub(crate) fn reserve_voices(&mut self, cap: usize) {
self.active.reserve(cap.saturating_sub(self.active.len()));
for bucket in &mut self.voice_by_ch {
bucket.reserve(cap.saturating_sub(bucket.len()));
}
}
pub(crate) fn enforce_voice_cap(&mut self, cap: usize) {
let len = self.active.len();
if len <= cap {
return;
}
let excess = len - cap;
let released = self.active.iter().filter(|a| a.voice.released()).count();
let mut released_budget = excess.min(released);
let mut unreleased_budget = excess - released_budget;
self.active.retain(|a| {
if a.voice.released() {
if released_budget > 0 {
released_budget -= 1;
return false;
}
} else if unreleased_budget > 0 {
unreleased_budget -= 1;
return false;
}
true
});
}
pub(crate) fn handle_event(&mut self, kind: EvKind) {
match kind {
EvKind::NoteOn { ch, .. } | EvKind::NoteOff { ch, .. }
if self.opt.solo & (1u16 << ch) == 0 => {}
EvKind::NoteOn { ch, key, vel } => self.note_on(ch, key, vel),
EvKind::NoteOff { ch, key } => self.note_off(ch, key),
EvKind::Cc { ch, num, val } => self.cc(ch, num, val),
EvKind::Bend { ch, semis } => self.bend(ch, semis),
EvKind::Aftertouch { ch, val } => {
let s = &mut self.strips[ch as usize];
s.at_target = val as f32 / 127.0;
s.at_authored = true;
}
EvKind::PolyAftertouch { ch, key, val } => {
if ch != 9 {
let p = val as f32 / 127.0;
for a in self.active.iter_mut().filter(|a| {
a.ch == ch
&& a.key == key
&& !a.voice.released()
&& aftertouch_family(a.program)
}) {
a.poly_target = p;
a.poly_authored = true;
}
}
}
EvKind::Prog { ch, prog } => self.program_change(ch, prog),
EvKind::DrumMode { ch, on } => {
if self.strips[ch as usize].gs_drum != on {
self.strips[ch as usize].gs_drum = on;
self.rederive_routing_derived_state(ch);
}
}
EvKind::GsReset => {
for ch in 0..self.strips.len() {
if self.strips[ch].gs_drum {
self.strips[ch].gs_drum = false;
self.rederive_routing_derived_state(ch as u8);
}
}
}
EvKind::GmReset => self.gm_system_on(),
EvKind::XgReset => self.xg_reset(),
EvKind::XgEffectParam { addr_lo, data, len } => {
self.handle_xg_effect_param(addr_lo, data, len)
}
}
}
fn xg_reset(&mut self) {
self.xg = XgEffects::new();
self.reverb
.reconfigure(DEFAULT_HALL_ROOM, DEFAULT_HALL_DAMP);
self.chorus.reconfigure(
DEFAULT_CHORUS_RATE,
DEFAULT_CHORUS_BASE_S,
DEFAULT_CHORUS_DEPTH_S,
);
self.resolve_variation();
}
fn handle_xg_effect_param(&mut self, addr_lo: u8, data: [u8; 2], len: u8) {
let value14 = ((data[0] as u16) << 7) | (data[1] as u16);
let value7 = value14.min(127) as u8;
match addr_lo {
0x00 if data == HALL1_TYPE => self.reverb.reconfigure(HALL1_ROOM, HALL1_DAMP),
0x20 if data == CHORUS1_TYPE => {
self.chorus
.reconfigure(CHORUS1_RATE, CHORUS1_BASE_S, CHORUS1_DEPTH_S)
}
0x40 => {
self.xg.var_type_msb = data[0];
self.xg.var_type_lsb = if len >= 2 { data[1] } else { 0 };
self.resolve_variation();
}
0x42 => {
self.xg.var_drive = value7;
self.resolve_variation();
}
0x54 => {
self.xg.var_drywet = value7;
self.resolve_variation();
}
0x5A => {
self.xg.var_connection = data[0];
self.resolve_variation();
}
0x5B => {
self.xg.var_part = data[0];
self.resolve_variation();
}
_ => {}
}
}
fn resolve_variation(&mut self) {
for s in &mut self.strips {
s.xg_insert = None;
}
let is_amp_sim = self.xg.var_type_msb == AMP_SIM_TYPE_MSB;
let insertion = self.xg.var_connection == XG_VAR_INSERTION;
let part = self.xg.var_part as usize;
if is_amp_sim && insertion && part < self.strips.len() {
let wet = f32::from(self.xg.var_drywet.saturating_sub(1)) / 126.0;
let insert = Drive::amp_sim(self.opt.sr, self.xg.var_drive);
self.strips[part].xg_insert = Some((insert, wet.clamp(0.0, 1.0)));
}
}
fn note_on(&mut self, ch: u8, key: u8, vel: u8) {
let sr = self.opt.sr;
let ci = ch as usize;
let porta_from = self.strips[ci].last_freq;
let program = self.strips[ci].program;
let bagpipe_drone_control =
ch != 9 && program == 109 && key <= voices::BAGPIPE_DRONE_CONTROL_MAX;
let trem_prev = if ch != 9 {
let prev = self.key_on_at[ci][key as usize];
self.key_on_at[ci][key as usize] = self.now;
prev
} else {
u64::MAX
};
let trem_restrike =
trem_prev != u64::MAX && (self.now - trem_prev) as f32 <= TREM_IOI_MAX_S * sr;
if ch != 9 && !bagpipe_drone_control {
self.strips[ci].last_freq = Some(key_freq(key));
}
if bagpipe_drone_control {
self.strips[ci].bagpipe_drone_holds =
self.strips[ci].bagpipe_drone_holds.saturating_add(1);
self.ensure_bagpipe_drone(ch, key, vel);
return;
}
if ch != 9 && program == 109 {
self.ensure_bagpipe_drone(ch, key, vel);
}
if ch != 9 && self.strips[ci].legato {
let mut ringing = self.active.iter_mut().filter(|a| {
a.ch == ch
&& a.key != BAGPIPE_DRONE_KEY
&& !a.held
&& !a.sost_held
&& !a.voice.released()
});
if let (Some(a), None) = (ringing.next(), ringing.next()) {
if a.voice.legato_to(key, vel) {
a.key = key;
return;
}
}
}
let is_drum = ch == 9 || self.strips[ci].xg_drum || self.strips[ci].gs_drum;
if ch != 9 && trem_restrike && !is_drum {
let prog = self.strips[ci].program;
let alt = self.strips[ci].alt_bank;
let alt_value = self.strips[ci].alt_bank_value;
let bank_lsb = self.strips[ci].bank_lsb;
if let Some(a) = self.active.iter_mut().rev().find(|a| {
a.ch == ch
&& a.key == key
&& a.program == prog
&& a.alt == alt
&& a.alt_bank_value == alt_value
&& a.bank_lsb == bank_lsb
&& !a.is_drum
}) {
if a.voice.retrigger(key, vel) {
a.held = false;
a.sost_held = false;
if let Some(sounding) = a.voice.rr_phase() {
self.pitched_rr[ci][key as usize] = (sounding as u8).wrapping_add(1);
}
self.stats.voices_spawned += 1;
self.voice_seed_index += 1;
return;
}
}
}
let porta_ctrl = self.strips[ci].porta_control.take();
let vel = if ch != 9
&& self.strips[ci].soft
&& voices::is_acoustic_piano(self.strips[ci].program)
{
((vel as f32 * 0.75).round() as u8).max(1)
} else {
vel
};
if is_drum && matches!(key, 42 | 44 | 46) {
for a in self
.active
.iter_mut()
.filter(|a| a.ch == ch && matches!(a.key, 42 | 44 | 46))
{
a.voice.choke();
}
}
if !is_drum && needs_drive(program) {
self.make_room_for_driven_guitar(ch);
}
let seed = note_voice_seed(self.voice_seed_index);
let voice = if is_drum {
let rr = self.drum_rr[key as usize];
self.drum_rr[key as usize] = rr.wrapping_add(1);
drums::make(
key,
vel,
sr,
seed,
self.strips[ci].kit,
self.opt.samples,
rr,
)
} else if program == 120 && self.opt.samples {
let takes = crate::sampler::fret_noise_round_robins();
let phase = &mut self.fret_noise_rr[ci];
let rr = *phase;
*phase = (rr + 1) % takes.max(1);
Some(
crate::sampler::sampled_fret_noise(vel, sr, rr)
.unwrap_or_else(|| voices::make(program, key, vel, sr, seed, false)),
)
} else {
let prog = self.strips[ci].program;
Some(if self.strips[ci].alt_bank {
crate::altbank::make(
prog,
self.strips[ci].alt_bank_value,
key,
vel,
sr,
seed,
self.opt.samples,
)
} else {
let rr = if voices::variation_round_robins(prog, self.strips[ci].bank_lsb).is_some()
{
let c = &mut self.pitched_rr[ci][key as usize];
let r = *c;
*c = c.wrapping_add(1);
r
} else {
0
};
voices::make_variation(
prog,
self.strips[ci].bank_lsb,
key,
vel,
sr,
seed,
self.opt.samples,
rr,
)
.unwrap_or_else(|| voices::make(prog, key, vel, sr, seed, self.opt.samples))
})
};
if let Some(mut voice) = voice {
let s = &self.strips[ci];
if s.bend != 1.0 {
voice.set_pitch(s.bend);
}
if s.vowel_authored && vowel_family(s.program) {
let (f, q, g) = vowel_at(s.vowel_cur);
voice.set_vowel(f, q, g);
}
if matches!(s.program, 56..=63) {
voice.set_breath((s.expr * s.breath).sqrt().min(1.3), 0.0);
}
if matches!(s.program, 64..=71 | 109 | 111)
&& (s.expr_authored || s.breath_authored || s.at_authored)
{
voice.set_breath((s.expr * s.breath).sqrt().min(1.0), 0.0);
}
if matches!(s.program, 72..=79)
&& (s.expr_authored || s.breath_authored || s.at_authored)
{
voice.set_breath((s.expr * s.breath).sqrt().min(1.0), 0.0);
}
let glide = if ch != 9 {
let src = porta_ctrl
.map(key_freq)
.or(if s.porta_on { porta_from } else { None });
src.and_then(|from| {
let semis = 12.0 * (from / key_freq(key)).log2();
(semis.abs() > 1e-3).then(|| {
let k = 1.0 - (-(BLOCK as f32) / (s.porta_time.max(1e-3) * sr)).exp();
voice.set_pitch(s.bend * 2f32.powf(semis / 12.0));
(semis, k)
})
})
} else {
None
};
self.active.push(Active {
ch,
key,
program: if is_drum {
128
} else {
self.strips[ci].program
},
held: false,
sost: false,
sost_held: false,
glide,
alt: self.strips[ci].alt_bank,
alt_bank_value: self.strips[ci].alt_bank_value,
bank_lsb: self.strips[ci].bank_lsb,
is_drum,
poly_authored: false,
poly_target: 0.0,
poly_cur: 0.0,
poly_phase: 0.0,
poly_mult: 1.0,
poly_gain: 1.0,
voice,
});
self.stats.voices_spawned += 1;
self.voice_seed_index += 1;
}
}
fn note_off(&mut self, ch: u8, key: u8) {
let mut drone_stop = false;
if ch != 9 && key <= voices::BAGPIPE_DRONE_CONTROL_MAX {
let holds = &mut self.strips[ch as usize].bagpipe_drone_holds;
drone_stop = *holds == 1; *holds = holds.saturating_sub(1);
}
if let Some(a) = self
.active
.iter_mut()
.find(|a| a.ch == ch && a.key == key && !a.held && !a.sost_held && !a.voice.released())
{
let s = &self.strips[ch as usize];
if ch != 9 && s.sustain {
a.held = true;
} else if ch != 9 && a.sost {
a.sost_held = true;
} else {
a.voice.note_off();
}
}
if drone_stop {
self.release_bagpipe_drone_if_idle(ch);
}
}
fn cc(&mut self, ch: u8, num: u8, val: u8) {
let sr = self.opt.sr;
let s = &mut self.strips[ch as usize];
let v = val as f32 / 127.0;
match num {
0 => {
if val == 127 {
s.xg_drum = true;
s.alt_bank = false;
s.alt_bank_value = 0;
} else {
s.xg_drum = false;
s.alt_bank = val != 0; s.alt_bank_value = val; }
self.rederive_routing_derived_state(ch);
}
1 => {
s.mod_target = v;
s.mod_authored = true;
}
2 => {
s.breath_target = v * v;
s.breath_authored = true;
}
5 => s.porta_time = PORTA_MIN_S * (PORTA_MAX_S / PORTA_MIN_S).powf(v),
6 | 38 => {
if s.rpn_msb != 127 || s.rpn_lsb != 127 {
if num == 6 {
s.data_msb = val;
}
let (msb, lsb) = if num == 6 {
(val, 0)
} else {
(s.data_msb, val)
};
match (s.rpn_msb, s.rpn_lsb) {
(0, 0) => s.bend_range = msb.clamp(1, 24) as f32 + lsb as f32 / 100.0,
(0, 1) => {
let raw = ((msb as i32) << 7 | lsb as i32) - 8192;
let cents = raw as f32 * (100.0 / 8192.0);
s.fine = 2f32.powf(cents / 1200.0);
}
_ => {}
}
if matches!((s.rpn_msb, s.rpn_lsb), (0, 0) | (0, 1)) {
s.bend = 2f32.powf(s.bend_wheel * (s.bend_range * 0.5) / 12.0) * s.fine;
let mult = s.bend;
for a in self.active.iter_mut().filter(|a| a.ch == ch) {
a.voice.set_pitch(mult);
}
}
} else if s.nrpn_msb != 127 || s.nrpn_lsb != 127 {
if num == 6 {
s.nrpn_data_msb = val;
if s.nrpn_msb == AMP_NRPN_MSB && (s.nrpn_lsb as usize) < AMP_PARAM_COUNT {
s.amp_params[s.nrpn_lsb as usize] = val;
s.amp_authored = true;
}
}
}
}
7 => {
s.volume_target = v * v;
s.volume_authored = true;
}
10 => {
s.pan_target = v;
s.pan_authored = true;
}
11 => {
s.expr_target = v * v;
s.expr_authored = true;
}
32 => s.bank_lsb = val, 64 => {
s.sustain = val >= 64;
if !s.sustain {
for a in self.active.iter_mut().filter(|a| a.ch == ch && a.held) {
a.held = false;
if a.sost {
a.sost_held = true;
} else {
a.voice.note_off();
}
}
}
}
65 => s.porta_on = val >= 64,
66 => {
let down = val >= 64;
if down && !s.sost_down {
for a in self
.active
.iter_mut()
.filter(|a| a.ch == ch && !a.voice.released())
{
a.sost = true;
}
} else if !down && s.sost_down {
for a in self.active.iter_mut().filter(|a| a.ch == ch && a.sost) {
a.sost = false;
if a.sost_held {
a.sost_held = false;
if s.sustain {
a.held = true;
} else {
a.voice.note_off();
}
}
}
}
s.sost_down = down;
}
67 => s.soft = val >= 64,
68 => s.legato = val >= 64,
70 => {
if !s.vowel_authored {
s.vowel_cur = val as f32;
}
s.vowel_target = val as f32;
s.vowel_authored = true;
}
71 => {
s.res_target = RES_MIN_Q * (RES_MAX_Q / RES_MIN_Q).powf(v);
if s.wah.is_none() {
s.wah = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
s.wah_legacy = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
s.wah_cathedral = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
if ch == 9 {
s.wah_r = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
}
}
}
74 => {
s.cutoff_target = WAH_MIN_HZ * (WAH_MAX_HZ / WAH_MIN_HZ).powf(v);
if val < 127 && s.wah.is_none() {
s.wah = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
s.wah_legacy = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
s.wah_cathedral = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
if ch == 9 {
s.wah_r = Some(Biquad::lowpass(WAH_MAX_HZ, WAH_Q, sr));
}
}
}
84 => s.porta_control = Some(val), 91 => {
s.reverb_send = v;
s.reverb_authored = true;
}
93 => {
s.chorus_send = v;
s.chorus_authored = true;
}
94 => {
s.delay_send = v;
s.delay_authored = true;
}
98 => {
s.nrpn_lsb = val;
s.rpn_msb = 127;
s.rpn_lsb = 127;
}
99 => {
s.nrpn_msb = val;
s.rpn_msb = 127;
s.rpn_lsb = 127;
}
100 => {
s.rpn_lsb = val;
s.nrpn_msb = 127;
s.nrpn_lsb = 127;
}
101 => {
s.rpn_msb = val;
s.nrpn_msb = 127;
s.nrpn_lsb = 127;
}
120 => self.all_sound_off(ch),
121 => self.reset_all_controllers(ch),
123 => self.all_notes_off(ch),
_ => {}
}
}
fn bend(&mut self, ch: u8, semis: f32) {
let s = &mut self.strips[ch as usize];
s.bend_wheel = semis;
let mult = 2f32.powf(semis * (s.bend_range * 0.5) / 12.0) * s.fine;
s.bend = mult;
for a in self.active.iter_mut().filter(|a| a.ch == ch) {
a.voice.set_pitch(mult);
}
}
fn program_change(&mut self, ch: u8, prog: u8) {
self.strips[ch as usize].program = prog;
self.rederive_routing_derived_state(ch);
}
fn rederive_routing_derived_state(&mut self, ch: u8) {
let s = &mut self.strips[ch as usize];
let prog = s.program;
let is_drum_part = ch == 9 || s.xg_drum || s.gs_drum;
if is_drum_part {
s.kit = match prog {
40 => drums::Kit::Brush,
25 => drums::Kit::V1,
24 => drums::Kit::Synth,
_ => drums::Kit::V3,
};
}
let (cho, del) = if is_drum_part {
(0.0, 0.0)
} else {
fx_profile(prog, s.alt_bank_value)
};
if !s.chorus_authored {
s.chorus_send = cho;
}
if !s.delay_authored {
s.delay_send = del;
}
if needs_drive(prog) && !is_drum_part {
let alt = s.alt_bank;
if s.drive.as_ref().map(|d| (d.program, d.alt)) != Some((prog, alt)) {
let mut d = Drive::new(prog, alt, self.opt.sr);
if s.amp_authored {
s.amp_prime_all();
d.apply_params(&s.amp_cur, true);
}
s.drive = Some(d);
}
} else {
s.drive = None;
}
}
fn rederive_program_drive(&mut self, ch: u8) {
let ci = ch as usize;
let prog = self.strips[ci].program;
let bank = self.strips[ci].alt_bank_value;
let mut drive = needs_drive(prog).then(|| Drive::new(prog, bank != 0, self.opt.sr));
if let Some(d) = drive.as_mut() {
let s = &mut self.strips[ci];
if s.amp_authored {
s.amp_prime_all();
d.apply_params(&s.amp_cur, true);
}
}
self.strips[ci].drive = drive;
}
fn all_sound_off(&mut self, ch: u8) {
if ch != 9 {
self.strips[ch as usize].bagpipe_drone_holds = 0;
self.strips[ch as usize].bagpipe_drone_live = false;
self.strips[ch as usize].bagpipe_drone_hang = 0;
}
self.active.retain_mut(|a| {
if a.ch == ch {
a.voice.choke();
false
} else {
true
}
});
}
fn all_notes_off(&mut self, ch: u8) {
if ch != 9 {
self.strips[ch as usize].bagpipe_drone_holds = 0;
}
let sustain = self.strips[ch as usize].sustain;
for a in self
.active
.iter_mut()
.filter(|a| a.ch == ch && !a.voice.released())
{
if ch != 9 && sustain {
a.held = true;
} else if ch != 9 && a.sost {
a.sost_held = true;
} else {
a.voice.note_off();
}
}
}
fn reset_all_controllers(&mut self, ch: u8) {
let ci = ch as usize;
let s = &mut self.strips[ci];
s.bend_wheel = 0.0;
s.bend = s.fine;
s.rpn_msb = 127;
s.rpn_lsb = 127;
s.data_msb = 0;
s.nrpn_msb = 127;
s.nrpn_lsb = 127;
s.nrpn_data_msb = 0;
s.porta_on = false;
s.last_freq = None;
s.porta_control = None; s.sustain = false;
s.sost_down = false;
s.soft = false;
s.legato = false;
s.at_authored = false;
s.at_target = 0.0;
s.at_cur = 0.0;
s.at_gain = 1.0;
s.mod_target = 0.0;
s.mod_cur = 0.0;
s.mod_engaged = false;
s.mod_authored = false;
s.vib_mult = 1.0;
s.organ_trem_phase = 0.0;
s.expr_target = 1.0;
s.expr = 1.0;
s.expr_authored = false;
s.breath_authored = false;
s.breath_target = 1.0;
s.breath = 1.0;
self.rederive_program_drive(ch);
let program = self.strips[ci].program;
let bend = self.strips[ci].bend;
let vowel = self.strips[ci].vowel_cur;
let choir_vowel = matches!(program, 52..=54).then(|| vowel_at(vowel));
for a in self.active.iter_mut().filter(|a| a.ch == ch) {
if a.held || a.sost_held {
a.voice.note_off();
}
a.held = false;
a.sost = false;
a.sost_held = false;
a.glide = None;
a.poly_authored = false;
a.poly_target = 0.0;
a.poly_cur = 0.0;
a.poly_phase = 0.0;
a.poly_mult = 1.0;
a.poly_gain = 1.0;
a.voice.set_pitch(bend);
if organ_leslie_family(a.program, a.alt_bank_value) {
let (rate, depth) = voices::organ_trem_base(a.program);
a.voice.set_trem(rate, depth);
}
if let Some((freqs, qs, gains)) = choir_vowel {
a.voice.set_vowel(freqs, qs, gains);
}
}
}
pub(crate) fn render_block_add(&mut self, n: usize, out: &mut [f32]) {
debug_assert!(n <= BLOCK);
debug_assert!(out.len() >= n * 2);
self.now += n as u64;
let sr = self.opt.sr;
self.tick_bagpipe_drone_latch();
self.rebuild_voice_buckets();
self.apply_mod_wheel_leslie_and_vibrato(n, sr);
self.apply_cathedral_organ_wind(n, sr);
self.apply_vowel_aftertouch_breath_and_glide(n, sr);
self.render_melodic_voices(n);
self.mix_channel_strips(n, sr);
self.render_and_mix_drums(n, sr);
self.apply_master_buses(n);
self.accumulate_output(n, out);
}
fn rebuild_voice_buckets(&mut self) {
for bucket in self.voice_by_ch.iter_mut() {
bucket.clear();
}
for (i, a) in self.active.iter().enumerate() {
self.voice_by_ch[a.ch as usize].push(i);
}
}
fn apply_mod_wheel_leslie_and_vibrato(&mut self, n: usize, sr: f32) {
for (ci, strip) in self.strips.iter_mut().enumerate() {
strip.mod_cur += self.expr_smooth * (strip.mod_target - strip.mod_cur);
let on = strip.mod_cur > 1e-3;
let leslie_program = self.voice_by_ch[ci]
.iter()
.map(|&i| &self.active[i])
.find(|a| organ_leslie_family(a.program, a.alt_bank_value))
.map(|a| a.program)
.or_else(|| {
(strip.mod_authored && organ_leslie_family(strip.program, strip.alt_bank_value))
.then_some(strip.program)
});
let active_pitch_vibrato = self.voice_by_ch[ci]
.iter()
.map(|&i| &self.active[i])
.any(|a| cc1_pitch_vibrato_target(a.program, a.alt));
let pending_pitch_vibrato = vibrato_family(strip.program);
if ci == 9 || (!on && !strip.mod_engaged && leslie_program.is_none()) {
continue;
}
let m = if on { strip.mod_cur } else { 0.0 };
let mut engaged = false;
if let Some(program) = leslie_program {
let (base_rate, base_depth) = voices::organ_trem_base(program);
if !strip.mod_engaged || strip.leslie_depth <= 0.0 {
strip.leslie_rate = if strip.mod_authored {
LESLIE_SLOW_HZ
} else {
base_rate
};
strip.leslie_depth = base_depth;
}
let target_rate = if strip.mod_authored {
LESLIE_SLOW_HZ + (LESLIE_FAST_HZ - LESLIE_SLOW_HZ) * m
} else {
base_rate
};
let target_depth = base_depth + LESLIE_DEPTH_ADD * m;
strip.leslie_rate += self.leslie_k * (target_rate - strip.leslie_rate);
strip.leslie_depth += self.leslie_k * (target_depth - strip.leslie_depth);
for &i in &self.voice_by_ch[ci] {
let a = &mut self.active[i];
if !organ_leslie_family(a.program, a.alt_bank_value) {
continue;
}
a.voice.set_trem(strip.leslie_rate, strip.leslie_depth);
}
engaged |= strip.mod_authored || on || (strip.leslie_rate - base_rate).abs() > 0.01;
}
if (on || strip.mod_engaged) && (active_pitch_vibrato || pending_pitch_vibrato) {
strip.vib_phase += TAU * VIB_RATE_HZ * n as f32 / sr;
if strip.vib_phase > TAU {
strip.vib_phase -= TAU;
}
let factor = 2f32.powf(m * VIB_DEPTH_CENTS / 1200.0 * strip.vib_phase.sin());
strip.vib_mult = factor;
let mult = strip.bend * factor;
for &i in &self.voice_by_ch[ci] {
let a = &mut self.active[i];
if a.alt && matches!(a.program, 48..=51) {
let base = crate::altbank::strings_vib_base(a.program);
a.voice.set_vib(base + (ST_CC1_VIB_DEPTH - base) * m);
} else if a.alt && matches!(a.program, 52..=54) {
} else if a.alt && a.program == 22 {
} else if vibrato_family(a.program) {
a.voice.set_pitch(mult);
}
}
engaged |= on;
} else if !active_pitch_vibrato {
strip.vib_mult = 1.0;
}
strip.mod_engaged = engaged;
}
}
fn apply_cathedral_organ_wind(&mut self, n: usize, sr: f32) {
for (ci, strip) in self.strips.iter_mut().enumerate() {
if ci == 9 {
continue;
}
let cat_notes = self.voice_by_ch[ci]
.iter()
.map(|&i| &self.active[i])
.filter(|a| cathedral_organ(a.program, a.alt_bank_value))
.count();
let load = cat_notes.saturating_sub(1).min(9) as f32 / 9.0;
let target = load;
let tau = if target > strip.organ_wind {
0.35
} else {
1.20
};
let k = 1.0 - (-(BLOCK as f32) / (tau * sr)).exp();
strip.organ_wind += k * (target - strip.organ_wind);
let trem = if cat_notes > 0 && strip.mod_cur > 1e-4 {
strip.organ_trem_phase += TAU * 5.5 * n as f32 / sr;
if strip.organ_trem_phase > TAU {
strip.organ_trem_phase -= TAU;
}
strip.organ_trem_phase.sin() * strip.mod_cur
} else {
0.0
};
let drive = if strip.expr_authored {
((strip.expr - ORGAN_SWELL_KNEE) / (1.0 - ORGAN_SWELL_KNEE))
.clamp(0.0, 1.0)
.powf(ORGAN_SWELL_GAMMA)
} else {
0.0
};
for &i in &self.voice_by_ch[ci] {
let a = &mut self.active[i];
if !cathedral_organ(a.program, a.alt_bank_value) {
continue;
}
a.voice.set_organ_pressure(strip.organ_wind, trem);
a.voice.set_organ_swell(drive);
}
}
}
fn apply_vowel_aftertouch_breath_and_glide(&mut self, n: usize, sr: f32) {
for (ci, strip) in self.strips.iter_mut().enumerate() {
if ci == 9 {
continue;
}
if strip.vowel_authored && vowel_family(strip.program) {
strip.vowel_cur += self.expr_smooth * (strip.vowel_target - strip.vowel_cur);
let (f, q, g) = vowel_at(strip.vowel_cur);
for &i in &self.voice_by_ch[ci] {
self.active[i].voice.set_vowel(f, q, g);
}
}
let mut any_poly = false;
for &i in &self.voice_by_ch[ci] {
let a = &mut self.active[i];
if !a.poly_authored {
continue;
}
a.poly_cur += self.expr_smooth * (a.poly_target - a.poly_cur);
a.poly_gain = 10f32.powf(a.poly_cur * AT_GAIN_DB / 20.0);
a.poly_phase += TAU * AT_VIB_RATE_HZ * n as f32 / sr;
if a.poly_phase > TAU {
a.poly_phase -= TAU;
}
a.poly_mult = 2f32.powf(a.poly_cur * AT_VIB_CENTS / 1200.0 * a.poly_phase.sin());
any_poly = true;
}
let channel_at = strip.at_authored && aftertouch_family(strip.program);
let mut at_vib = 1.0f32;
if channel_at {
strip.at_cur += self.expr_smooth * (strip.at_target - strip.at_cur);
strip.at_gain = 10f32.powf(strip.at_cur * AT_GAIN_DB / 20.0);
strip.at_phase += TAU * AT_VIB_RATE_HZ * n as f32 / sr;
if strip.at_phase > TAU {
strip.at_phase -= TAU;
}
at_vib = 2f32.powf(strip.at_cur * AT_VIB_CENTS / 1200.0 * strip.at_phase.sin());
for &i in &self.voice_by_ch[ci] {
let a = &mut self.active[i];
if a.is_drum {
continue;
}
let vm = if a.alt && (matches!(a.program, 48..=54) || a.program == 22) {
1.0
} else {
strip.vib_mult
};
a.voice.set_pitch(strip.bend * vm * at_vib * a.poly_mult);
}
}
if any_poly && !channel_at {
for &i in &self.voice_by_ch[ci] {
let a = &mut self.active[i];
if !(a.poly_authored && a.glide.is_none()) {
continue;
}
let vm = if a.alt && (matches!(a.program, 48..=54) || a.program == 22) {
1.0
} else {
strip.vib_mult
};
a.voice.set_pitch(strip.bend * vm * a.poly_mult);
}
}
if matches!(strip.program, 56..=63) {
let p = (strip.expr * strip.breath).sqrt().min(1.3);
let g = if strip.at_authored { strip.at_cur } else { 0.0 };
for &i in &self.voice_by_ch[ci] {
self.active[i].voice.set_breath(p, g);
}
}
if matches!(strip.program, 64..=71 | 109 | 111)
&& (strip.expr_authored || strip.breath_authored || strip.at_authored)
{
let p = (strip.expr * strip.breath).sqrt().min(1.0);
let g = if strip.at_authored { strip.at_cur } else { 0.0 };
for &i in &self.voice_by_ch[ci] {
self.active[i].voice.set_breath(p, g);
}
}
if matches!(strip.program, 72..=79)
&& (strip.expr_authored || strip.breath_authored || strip.at_authored)
{
let p = (strip.expr * strip.breath).sqrt().min(1.0);
for &i in &self.voice_by_ch[ci] {
self.active[i].voice.set_breath(p, 0.0);
}
}
for &i in &self.voice_by_ch[ci] {
let a = &mut self.active[i];
if let Some((semis, k)) = &mut a.glide {
*semis -= *k * *semis;
let done = semis.abs() < 0.005;
let gm = if done { 1.0 } else { 2f32.powf(*semis / 12.0) };
let vm = if a.alt && (matches!(a.program, 48..=54) || a.program == 22) {
1.0
} else {
strip.vib_mult
};
a.voice
.set_pitch(strip.bend * vm * at_vib * a.poly_mult * gm);
if done {
a.glide = None;
}
}
}
}
}
fn render_melodic_voices(&mut self, n: usize) {
for buf in self.ch_buf.iter_mut() {
buf[..n].fill(0.0);
}
for buf in self.legacy_buf.iter_mut() {
buf[..n].fill(0.0);
}
for buf in self.cathedral_buf.iter_mut() {
buf[..n].fill(0.0);
}
self.stats.max_polyphony = self.stats.max_polyphony.max(self.active.len());
self.active.retain_mut(|a| {
if a.is_drum {
return true; }
self.scratch[..n].fill(0.0);
let alive = a.voice.render(&mut self.scratch[..n]);
if a.poly_gain != 1.0 {
for x in self.scratch[..n].iter_mut() {
*x *= a.poly_gain;
}
}
let buf = if cathedral_organ(a.program, a.alt_bank_value) {
&mut self.cathedral_buf[a.ch as usize]
} else if a.program == 19 && a.alt {
&mut self.legacy_buf[a.ch as usize]
} else {
&mut self.ch_buf[a.ch as usize]
};
for (dst, src) in buf[..n].iter_mut().zip(self.scratch[..n].iter()) {
*dst += *src;
}
alive
});
}
fn mix_channel_strips(&mut self, n: usize, sr: f32) {
self.mix_l[..n].fill(0.0);
self.mix_r[..n].fill(0.0);
self.send_rev[..n].fill(0.0);
self.send_cathedral[..n].fill(0.0);
self.send_cho[..n].fill(0.0);
self.send_del[..n].fill(0.0);
self.send_sym[..n].fill(0.0);
self.send_sym_gtr[..n].fill(0.0);
self.send_sym_sitar[..n].fill(0.0);
self.send_room[..n].fill(0.0);
for (ci, strip) in self.strips.iter_mut().enumerate() {
let buf = &mut self.ch_buf[ci];
let legacy = &mut self.legacy_buf[ci];
let cathedral = &mut self.cathedral_buf[ci];
for wah in [
&mut strip.wah,
&mut strip.wah_legacy,
&mut strip.wah_cathedral,
&mut strip.wah_r,
]
.into_iter()
.flatten()
{
wah.flush();
}
if let Some((xg, wet)) = &mut strip.xg_insert {
let wet = *wet;
if wet >= 1.0 {
xg.process(&mut buf[..n]);
} else {
let mut dry = [0f32; BLOCK];
dry[..n].copy_from_slice(&buf[..n]);
xg.process(&mut buf[..n]);
for (b, d) in buf[..n].iter_mut().zip(&dry[..n]) {
*b = *d * (1.0 - wet) + *b * wet;
}
}
} else if let Some(drive) = &mut strip.drive {
if strip.amp_authored {
for i in 0..AMP_PARAM_COUNT {
let target = strip.amp_params[i] as f32;
if strip.amp_primed[i] {
strip.amp_cur[i] += self.wah_smooth * (target - strip.amp_cur[i]);
} else {
strip.amp_cur[i] = target;
strip.amp_primed[i] = true;
}
}
drive.apply_params(&strip.amp_cur, false);
}
drive.process(&mut buf[..n]);
}
if let Some(wah) = &mut strip.wah {
strip.cutoff += self.wah_smooth * (strip.cutoff_target - strip.cutoff);
strip.res += self.wah_smooth * (strip.res_target - strip.res);
wah.retune_lowpass(strip.cutoff, strip.res, sr);
if ci != 9 {
for x in buf[..n].iter_mut() {
*x = wah.process(*x);
}
}
}
if let Some(wah) = &mut strip.wah_legacy {
wah.retune_lowpass(strip.cutoff, strip.res, sr);
for x in legacy[..n].iter_mut() {
*x = wah.process(*x);
}
}
if let Some(wah) = &mut strip.wah_cathedral {
wah.retune_lowpass(strip.cutoff, strip.res, sr);
for x in cathedral[..n].iter_mut() {
*x = wah.process(*x);
}
}
if strip.volume_authored {
if strip.volume_primed {
strip.volume += self.expr_smooth * (strip.volume_target - strip.volume);
} else {
strip.volume = strip.volume_target;
strip.volume_primed = true;
}
}
if strip.pan_authored {
if strip.pan_primed {
strip.pan += self.expr_smooth * (strip.pan_target - strip.pan);
} else {
strip.pan = strip.pan_target;
strip.pan_primed = true;
}
strip.haas_delay = 0.005 * sr * (strip.pan - 0.5).abs() * 2.0;
}
strip.expr += self.expr_smooth * (strip.expr_target - strip.expr);
if strip.breath_authored {
strip.breath += self.expr_smooth * (strip.breath_target - strip.breath);
}
let trim = if ci != 9 {
program_trim_lin(strip.program)
} else {
1.0
};
let g = strip.volume * strip.expr * strip.at_gain * strip.breath * trim;
if g < 1e-6 {
continue;
}
let theta = strip.pan * FRAC_PI_2;
let (gl, gr) = (g * theta.cos(), g * theta.sin());
let rs = strip.reverb_send * 0.9;
let is_piano = ci != 9 && voices::is_acoustic_piano(strip.program);
let is_ac_gtr = ci != 9 && matches!(strip.program, 24 | 25);
let is_sitar = ci != 9 && strip.program == 104;
if is_sitar {
self.sitar_seen = true;
}
let haas = strip.haas_delay;
let legacy_cho = if strip.chorus_authored {
strip.chorus_send
} else {
0.20
};
let legacy_del = if strip.delay_authored {
strip.delay_send
} else {
0.0
};
let cathedral_cho = if strip.chorus_authored {
strip.chorus_send
} else {
0.0
};
let cathedral_del = if strip.delay_authored {
strip.delay_send
} else {
0.0
};
for i in 0..n {
let (ordinary_x, legacy_x, cathedral_x) = (buf[i], legacy[i], cathedral[i]);
let x = ordinary_x + legacy_x + cathedral_x;
strip.haas.push(x);
let (xl, xr) = if haas < 1.0 {
(x, x)
} else if strip.pan < 0.5 {
(x, strip.haas.tap(haas))
} else {
(strip.haas.tap(haas), x)
};
self.mix_l[i] += xl * gl;
self.mix_r[i] += xr * gr;
let (ordinary_s, legacy_s, cathedral_s) =
(ordinary_x * g, legacy_x * g, cathedral_x * g);
let xs = ordinary_s + legacy_s + cathedral_s;
self.send_rev[i] += (ordinary_s + legacy_s) * rs;
self.send_cathedral[i] += cathedral_s * rs;
self.send_cho[i] += ordinary_s * strip.chorus_send
+ legacy_s * legacy_cho
+ cathedral_s * cathedral_cho;
self.send_del[i] += ordinary_s * strip.delay_send
+ legacy_s * legacy_del
+ cathedral_s * cathedral_del;
if is_piano {
self.send_sym[i] += xs;
}
if is_ac_gtr {
self.send_sym_gtr[i] += xs;
}
if is_sitar {
self.send_sym_sitar[i] += xs;
}
}
}
}
fn render_and_mix_drums(&mut self, n: usize, sr: f32) {
self.drum_l[..n].fill(0.0);
self.drum_r[..n].fill(0.0);
self.active.retain_mut(|a| {
if !a.is_drum {
return true;
}
self.scratch[..n].fill(0.0);
let alive = a.voice.render(&mut self.scratch[..n]);
let s = &self.strips[a.ch as usize];
let pan = (drum_pan(a.key) + (s.pan - 0.5)).clamp(0.0, 1.0);
let theta = pan * FRAC_PI_2;
let bal = kit_balance(a.key);
let (ul, ur) = (theta.cos() * bal, theta.sin() * bal);
if a.ch == 9 {
for i in 0..n {
self.drum_l[i] += self.scratch[i] * ul;
self.drum_r[i] += self.scratch[i] * ur;
}
} else {
let gc = s.volume * s.expr * s.at_gain * s.breath * DRUM_FORWARD;
if gc >= 1e-6 {
let rs = s.reverb_send * 0.9;
let room_send = s.drum_room_send();
for i in 0..n {
let xl = (self.scratch[i] * ul) * gc;
let xr = (self.scratch[i] * ur) * gc;
self.mix_l[i] += xl;
self.mix_r[i] += xr;
let mono = 0.5 * (xl + xr);
self.send_rev[i] += mono * rs;
self.send_room[i] += mono * room_send;
}
}
}
alive
});
let s9 = &mut self.strips[9];
if let Some(wl) = &mut s9.wah {
for x in self.drum_l[..n].iter_mut() {
*x = wl.process(*x);
}
}
if let Some(wr) = &mut s9.wah_r {
wr.retune_lowpass(s9.cutoff, s9.res, sr);
for x in self.drum_r[..n].iter_mut() {
*x = wr.process(*x);
}
}
let g9 = s9.volume * s9.expr * s9.at_gain * s9.breath * DRUM_FORWARD;
if g9 >= 1e-6 {
let rs = s9.reverb_send * 0.9;
let room_send = s9.drum_room_send();
for i in 0..n {
let (xl, xr) = (self.drum_l[i] * g9, self.drum_r[i] * g9);
self.mix_l[i] += xl;
self.mix_r[i] += xr;
let mono = 0.5 * (xl + xr);
self.send_rev[i] += mono * rs;
self.send_room[i] += mono * room_send;
}
}
}
fn apply_master_buses(&mut self, n: usize) {
self.symp.process(
&self.send_sym[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
if self.opt.gtr_symp_on {
self.gtr_symp.process(
&self.send_sym_gtr[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
}
if self.opt.sitar_symp_on && self.sitar_seen {
self.sitar_symp.process(
&self.send_sym_sitar[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
}
self.chorus.process(
&self.send_cho[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
if let Some(echo) = &mut self.echo {
echo.process(
&self.send_del[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
}
if self.opt.drum_room_on {
self.drum_room.process(
&self.send_room[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
}
self.rev_hp.flush();
for x in self.send_rev[..n].iter_mut() {
*x = self.rev_hp.process(*x);
}
self.reverb.process(
&self.send_rev[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
#[cfg(test)]
if self.cathedral_send_hpf_on {
for x in self.send_cathedral[..n].iter_mut() {
*x = self.cathedral_send_hp.process(*x);
}
}
self.cathedral.process(
&self.send_cathedral[..n],
&mut self.mix_l[..n],
&mut self.mix_r[..n],
);
#[cfg(test)]
{
self.stats.cathedral_return_peak = self
.stats
.cathedral_return_peak
.max(self.cathedral.debug_return_peak());
}
self.glue
.process(&mut self.mix_l[..n], &mut self.mix_r[..n]);
}
#[cfg(test)]
fn visit_always_running_state(&self, visit: &mut dyn FnMut(&'static str, f32)) {
for x in self.rev_hp.debug_state() {
visit("reverb send high-pass", x);
}
self.reverb.visit_state(visit);
self.drum_room.visit_state(visit);
self.cathedral.visit_state(visit);
for &x in self.chorus.dl.debug_buf() {
visit("chorus line", x);
}
if let Some(echo) = &self.echo {
for &x in echo.left.debug_buf().iter().chain(echo.right.debug_buf()) {
visit("echo line", x);
}
visit("echo damp", echo.lp_l.debug_state());
visit("echo damp", echo.lp_r.debug_state());
}
for symp in [&self.symp, &self.gtr_symp, &self.sitar_symp] {
for x in symp.hp.debug_state() {
visit("sympathetic high-pass", x);
}
for (line, _, damp) in &symp.combs {
visit("sympathetic damp", damp.debug_state());
for &x in line.debug_buf() {
visit("sympathetic comb line", x);
}
}
}
visit("glue envelope", self.glue.env);
for x in self
.glue
.shelf_l
.debug_state()
.into_iter()
.chain(self.glue.shelf_r.debug_state())
{
visit("glue shelf", x);
}
for strip in &self.strips {
if let Some(drive) = &strip.drive {
drive.visit_state(visit);
}
if let Some((drive, _)) = &strip.xg_insert {
drive.visit_state(visit);
}
for wah in [
&strip.wah,
&strip.wah_legacy,
&strip.wah_cathedral,
&strip.wah_r,
]
.into_iter()
.flatten()
{
for x in wah.debug_state() {
visit("strip CC74 filter", x);
}
}
for &x in strip.haas.debug_buf() {
visit("strip Haas line", x);
}
}
}
fn accumulate_output(&mut self, n: usize, out: &mut [f32]) {
for i in 0..n {
out[i * 2] += self.mix_l[i];
out[i * 2 + 1] += self.mix_r[i];
let m = self.mix_l[i].abs().max(self.mix_r[i].abs());
if m > self.stats.peak {
self.stats.peak = m;
}
}
}
}
pub fn render(song: &Song, opt: &Options) -> (Vec<f32>, Stats) {
render_buses_with_progress(song, opt, true, true, true, &mut |_| {})
}
pub fn render_with_progress(
song: &Song,
opt: &Options,
on_progress: &mut dyn FnMut(Progress),
) -> (Vec<f32>, Stats) {
render_buses_with_progress(song, opt, true, true, true, on_progress)
}
#[cfg(test)]
pub(crate) fn render_buses(
song: &Song,
opt: &Options,
gtr_symp_on: bool,
drum_room_on: bool,
sitar_symp_on: bool,
) -> (Vec<f32>, Stats) {
render_buses_with_progress(
song,
opt,
gtr_symp_on,
drum_room_on,
sitar_symp_on,
&mut |_| {},
)
}
pub(crate) fn render_buses_with_progress(
song: &Song,
opt: &Options,
gtr_symp_on: bool,
drum_room_on: bool,
sitar_symp_on: bool,
on_progress: &mut dyn FnMut(Progress),
) -> (Vec<f32>, Stats) {
let total = render_sample_count(song, opt);
let mut out = vec![0f32; total * 2]; let stats = drive_render(
song,
opt,
gtr_symp_on,
drum_room_on,
sitar_symp_on,
on_progress,
|core, block_start, n| {
core.render_block_add(n, &mut out[block_start * 2..(block_start + n) * 2]);
Ok::<(), std::convert::Infallible>(())
},
)
.expect("an infallible render sink cannot fail");
(out, stats)
}
pub(crate) fn render_sample_count(song: &Song, opt: &Options) -> usize {
((song.seconds + opt.tail as f64) * opt.sr as f64) as usize
}
pub(crate) fn render_stream_with_progress(
song: &Song,
opt: &Options,
on_progress: &mut dyn FnMut(Progress),
emit: &mut dyn FnMut(&[f32]) -> std::io::Result<()>,
) -> std::io::Result<Stats> {
let mut block = [0.0f32; BLOCK * 2];
drive_render(song, opt, true, true, true, on_progress, |core, _, n| {
block[..n * 2].fill(0.0);
core.render_block_add(n, &mut block[..n * 2]);
emit(&block[..n * 2])
})
}
fn drive_render<E>(
song: &Song,
opt: &Options,
gtr_symp_on: bool,
drum_room_on: bool,
sitar_symp_on: bool,
on_progress: &mut dyn FnMut(Progress),
mut emit_block: impl FnMut(&mut EngineCore, usize, usize) -> Result<(), E>,
) -> Result<Stats, E> {
let sr = opt.sr;
let total = render_sample_count(song, opt);
let mut core = EngineCore::new(CoreOptions::from_options(
opt,
gtr_symp_on,
drum_room_on,
sitar_symp_on,
));
let events: Vec<(usize, EvKind)> = song
.events
.iter()
.map(|e| ((e.sec * sr as f64) as usize, e.kind))
.collect();
let mut ev_i = 0;
let mut next_report = total / 10;
let mut block_start = 0usize;
while block_start < total {
let n = BLOCK.min(total - block_start);
while ev_i < events.len() && events[ev_i].0 < block_start + n {
let (_, kind) = events[ev_i];
ev_i += 1;
core.handle_event(kind);
}
emit_block(&mut core, block_start, n)?;
block_start += n;
if block_start >= next_report {
on_progress(Progress {
rendered_seconds: block_start as f64 / sr as f64,
total_seconds: total as f64 / sr as f64,
active_voices: core.active_voice_count(),
});
next_report += total / 10;
}
}
Ok(core.stats())
}
fn dither_quantize(samples: &[f32], scale: f32) -> Vec<i16> {
let mut quantizer = DitherQuantizer::new();
samples
.iter()
.map(|&x| quantizer.quantize(x, scale))
.collect()
}
pub(crate) struct DitherQuantizer {
rng: Rng,
}
impl DitherQuantizer {
pub(crate) fn new() -> Self {
Self {
rng: Rng::new(0xD17E),
}
}
pub(crate) fn quantize(&mut self, sample: f32, scale: f32) -> i16 {
let dither = (self.rng.white() + self.rng.white()) * 0.5;
((sample * scale).clamp(-1.0, 1.0) * 32767.0 + dither)
.round()
.clamp(-32768.0, 32767.0) as i16
}
}
pub fn normalize_to_i16(samples: &[f32], peak: f32, target: f32) -> Vec<i16> {
let scale = if peak > 1e-9 { target / peak } else { 1.0 };
dither_quantize(samples, scale)
}
pub(crate) const LOUDNESS_MAX_ITERS: usize = 6;
pub(crate) const LOUDNESS_TOL_DB: f32 = 0.3;
pub fn normalize_loudness(
samples: &[f32],
sr: u32,
target_lufs: f32,
ceiling_dbtp: f32,
) -> Vec<i16> {
let mut buf: Vec<f32> = samples.to_vec();
if !target_lufs.is_finite() || !ceiling_dbtp.is_finite() {
return dither_quantize(&buf, 1.0);
}
for _ in 0..LOUDNESS_MAX_ITERS {
let measured = crate::loudness::integrated_lufs(&buf, sr);
if !measured.is_finite() {
break; }
let delta = target_lufs - measured;
let on_target = delta.abs() < LOUDNESS_TOL_DB;
if !on_target {
let g = 10f32.powf(delta / 20.0);
for x in buf.iter_mut() {
*x *= g;
}
}
crate::loudness::limit_true_peak(&mut buf, sr, ceiling_dbtp);
if on_target {
break;
}
}
dither_quantize(&buf, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::midi::Ev;
#[test]
fn silent_session_state_never_parks_subnormal() {
let sr = 44_100.0;
let mut core = EngineCore::new(CoreOptions {
sr,
wet: 0.32,
delay_s: 0.375,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
});
let cc = |ch, num, val| EvKind::Cc { ch, num, val };
for (ch, prog) in [(0, 0), (1, 29), (2, 24), (3, 104), (4, 19), (5, 48)] {
core.handle_event(EvKind::Prog { ch, prog });
}
for ch in 0..16 {
core.handle_event(cc(ch, 91, 100));
core.handle_event(cc(ch, 93, 80));
core.handle_event(cc(ch, 94, 60));
}
for ch in [0, 1, 9] {
core.handle_event(cc(ch, 74, 50));
core.handle_event(cc(ch, 71, 90));
}
for ch in 0..6 {
core.note_on(ch, 60 + ch, 100);
}
for key in [36, 38, 42] {
core.note_on(9, key, 110);
}
let mut out = vec![0.0f32; BLOCK * 2];
let mut render = |core: &mut EngineCore, seconds: f32| {
for _ in 0..(seconds * sr) as usize / BLOCK {
out.fill(0.0);
core.render_block_add(BLOCK, &mut out);
}
};
render(&mut core, 1.0);
for ch in 0..16 {
core.handle_event(cc(ch, 120, 0));
}
render(&mut core, 10.0);
assert!(core.active.is_empty(), "every voice must be gone");
let mut parked = std::collections::BTreeMap::<&str, usize>::new();
core.visit_always_running_state(&mut |site, x| {
if x.is_subnormal() {
*parked.entry(site).or_default() += 1;
}
});
assert!(
parked.is_empty(),
"always-running state still holds subnormals 10 s after all-sound-off \
(site: count): {parked:?}"
);
}
fn amp_protocol_readme_rows(readme: &str) -> Vec<(usize, String)> {
let amp_section = readme
.split("### Score-authored amp (driven guitars 29/30)")
.nth(1)
.expect("README score-authored amp section")
.split("\n### ")
.next()
.expect("README score-authored amp section body");
amp_section
.lines()
.filter_map(|line| {
let mut cells = line.trim().strip_prefix('|')?.split('|');
let index = cells.next()?.trim().parse().ok()?;
Some((index, cells.next()?.trim().to_string()))
})
.filter(|(index, _)| *index < AMP_PARAM_COUNT)
.collect()
}
#[cfg(ferrosintesis_repository_tests)]
#[test]
fn amp_protocol_has_one_definition() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/")
.parent()
.expect("repo root")
.to_path_buf();
let read = |rel: &str| {
std::fs::read_to_string(root.join(rel))
.unwrap_or_else(|e| panic!("cannot read {rel}: {e}"))
};
let lab = read("crates/amp-lab/src/amp.rs");
let readme = read("crates/ferrosintesis/README.md");
let norm = |s: &str| -> String {
s.chars()
.filter(|c| c.is_ascii_alphabetic())
.collect::<String>()
.to_uppercase()
};
let prefix_match = |a: &str, b: &str| {
let (a, b) = (norm(a), norm(b));
!a.is_empty() && (a.starts_with(&b) || b.starts_with(&a))
};
let lab_msb = u8::from_str_radix(
lab.split("pub const AMP_NRPN_MSB: u8 = 0x")
.nth(1)
.expect("amp-lab AMP_NRPN_MSB")
.split(';')
.next()
.unwrap()
.trim(),
16,
)
.expect("amp-lab MSB hex");
assert_eq!(
lab_msb, AMP_NRPN_MSB,
"amp-lab publishes NRPN MSB 0x{lab_msb:02X} but the synth uses 0x{AMP_NRPN_MSB:02X} \
— a file authored against amp-lab would address a different block"
);
let readme_msb = u8::from_str_radix(
readme
.split("MSB (CC99) = `0x")
.nth(1)
.expect("README NRPN MSB")
.split('`')
.next()
.unwrap()
.trim(),
16,
)
.expect("README MSB hex");
assert_eq!(
readme_msb, AMP_NRPN_MSB,
"the README documents NRPN MSB 0x{readme_msb:02X}, the synth uses \
0x{AMP_NRPN_MSB:02X}"
);
let lab_neutral: u8 = lab
.split("pub const NEUTRAL: u8 = ")
.nth(1)
.expect("amp-lab NEUTRAL")
.split(';')
.next()
.unwrap()
.trim()
.parse()
.expect("amp-lab NEUTRAL value");
assert_eq!(
lab_neutral, AMP_NEUTRAL,
"amp-lab's neutral is {lab_neutral}, the synth's is {AMP_NEUTRAL} — every \
knob would be offset from a different origin"
);
assert!(
readme.contains(&format!("{AMP_NEUTRAL} = as-shipped")),
"the README no longer states `{AMP_NEUTRAL} = as-shipped` as the neutral"
);
let engine_src = read("crates/ferrosintesis/src/engine.rs");
let mut mine: Vec<(usize, String)> = Vec::new();
for line in engine_src.lines() {
let t = line.trim();
if let Some(rest) = t.strip_prefix("const AMP_") {
if let Some((name, tail)) = rest.split_once(": usize = ") {
if let Ok(idx) = tail.split(';').next().unwrap_or("").trim().parse::<usize>() {
if name != "PARAM_COUNT" {
mine.push((idx, name.to_string()));
}
}
}
}
}
mine.sort();
assert_eq!(
mine.len(),
AMP_PARAM_COUNT,
"found {} AMP_* index constants in engine.rs but AMP_PARAM_COUNT is \
{AMP_PARAM_COUNT} — the protocol gained or lost a knob without the count \
following: {mine:?}",
mine.len()
);
for (i, (idx, name)) in mine.iter().enumerate() {
assert_eq!(
*idx, i,
"engine.rs AMP_{name} sits at index {idx}, breaking the contiguous \
0..{AMP_PARAM_COUNT} block the NRPN LSB addresses"
);
}
let knobs: Vec<(usize, String)> = lab
.split("Knob {")
.skip(1)
.filter_map(|b| {
let idx = b
.split("idx:")
.nth(1)?
.split(',')
.next()?
.trim()
.parse()
.ok()?;
let name = b.split("name: \"").nth(1)?.split('"').next()?.to_string();
Some((idx, name))
})
.collect();
assert_eq!(
knobs.len(),
AMP_PARAM_COUNT,
"amp-lab publishes {} knobs, the synth implements {AMP_PARAM_COUNT}",
knobs.len()
);
let rows = amp_protocol_readme_rows(&readme);
assert_eq!(
rows.len(),
AMP_PARAM_COUNT,
"the README's NRPN table documents {} of {AMP_PARAM_COUNT} knobs",
rows.len()
);
for (i, (_, mine_name)) in mine.iter().enumerate() {
let (_, lab_name) = knobs.iter().find(|(k, _)| *k == i).expect("amp-lab knob");
let (_, doc_name) = rows.iter().find(|(k, _)| *k == i).expect("README row");
assert!(
prefix_match(mine_name, lab_name),
"LSB {i}: the synth calls it AMP_{mine_name}, amp-lab calls it \
\"{lab_name}\" — the same index means two different knobs"
);
assert!(
prefix_match(lab_name, doc_name),
"LSB {i}: amp-lab calls it \"{lab_name}\", the README documents \
\"{doc_name}\""
);
}
}
#[test]
fn amp_protocol_readme_scan_ignores_other_numbered_tables() {
let readme = r#"
### Unrelated table
| 0 | Wrong before |
### Score-authored amp (driven guitars 29/30)
| LSB | knob |
|-----|------|
| 0 | Drive |
| 1 | Tone |
### Later table
| 2 | Wrong after |
"#;
assert_eq!(
amp_protocol_readme_rows(readme),
[(0, "Drive".to_string()), (1, "Tone".to_string())]
);
}
#[test]
fn program_trim_scope_and_calibration() {
let is_corrected = |p: u8| {
matches!(p,
6 | 5 | 8 | 14 | 110 | 119 | 16..=23 | 40..=44 | 48..=54 | 56..=63 | 64..=71 | 72..=79 | 80..=87 | 88..=95) };
for p in 0u8..128 {
if !is_corrected(p) {
assert_eq!(
PROGRAM_TRIM_DB[p as usize], 0.0,
"program {p} is outside the conservative scope and must stay at 0.0 dB"
);
assert_eq!(
program_trim_lin(p),
1.0,
"untouched program {p} must be unity gain"
);
}
}
assert_eq!(PROGRAM_TRIM_DB[48], 6.0); assert_eq!(PROGRAM_TRIM_DB[50], 6.0); assert_eq!(PROGRAM_TRIM_DB[53], 6.0); assert_eq!(PROGRAM_TRIM_DB[56], -6.0); assert_eq!(PROGRAM_TRIM_DB[58], -6.0); assert_eq!(PROGRAM_TRIM_DB[43], -6.0); assert_eq!(PROGRAM_TRIM_DB[73], -4.0); assert_eq!(PROGRAM_TRIM_DB[19], -6.0); assert_eq!(PROGRAM_TRIM_DB[6], 6.0);
assert_eq!(PROGRAM_TRIM_DB[5], 1.0); assert_eq!(PROGRAM_TRIM_DB[8], 3.0); assert_eq!(PROGRAM_TRIM_DB[14], -6.0); assert_eq!(PROGRAM_TRIM_DB[110], -6.0); assert_eq!(PROGRAM_TRIM_DB[119], 1.0);
assert_eq!(PROGRAM_TRIM_DB[11], 0.0);
assert_eq!(PROGRAM_TRIM_DB[0], 0.0);
assert_eq!(PROGRAM_TRIM_DB[1], 0.0);
assert_eq!(PROGRAM_TRIM_DB[3], 0.0);
for (p, &db) in PROGRAM_TRIM_DB.iter().enumerate() {
assert!(
(-6.0..=6.0).contains(&db),
"program {p} trim {db} dB is outside the ±6 dB clamp"
);
}
assert!(
(program_trim_lin(56) - 0.5012).abs() < 1e-3,
"-6 dB ≈ 0.501×"
);
assert!(
(program_trim_lin(53) - 1.9953).abs() < 1e-3,
"+6 dB ≈ 1.995×"
);
assert_eq!(
program_trim_lin(0),
1.0,
"untouched program must be exactly unity"
);
}
#[test]
fn render_with_progress_is_bit_identical_to_render() {
let mut events = Vec::new();
for (sec, kind) in [
(0.0, EvKind::Prog { ch: 0, prog: 46 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 91,
val: 80,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(
0.05,
EvKind::NoteOn {
ch: 9,
key: 38,
vel: 110,
},
),
(
0.40,
EvKind::NoteOn {
ch: 0,
key: 67,
vel: 90,
},
),
(0.90, EvKind::NoteOff { ch: 0, key: 60 }),
(1.20, EvKind::NoteOff { ch: 0, key: 67 }),
] {
events.push(Ev { sec, kind });
}
let song = Song {
events,
seconds: 1.5,
markers: Vec::new(),
title: String::new(),
initial_bpm: 120.0,
};
let opt = Options::default().with_tail(0.5);
let (plain, plain_stats) = render(&song, &opt);
let mut seen = Vec::new();
let (observed, observed_stats) = render_with_progress(&song, &opt, &mut |p| seen.push(p));
let mut streamed = Vec::new();
let streamed_stats = render_stream_with_progress(&song, &opt, &mut |_| {}, &mut |block| {
streamed.extend_from_slice(block);
Ok(())
})
.unwrap();
assert_eq!(
plain, observed,
"attaching a progress callback changed the rendered audio"
);
assert_eq!(
plain, streamed,
"streaming blocks changed the rendered audio"
);
assert_eq!(plain_stats, streamed_stats);
assert_eq!(plain_stats.peak, observed_stats.peak);
assert_eq!(plain_stats.voices_spawned, observed_stats.voices_spawned);
assert!(!seen.is_empty(), "progress callback never fired");
assert!(seen.iter().all(|p| (0.0..=1.0).contains(&p.fraction())));
assert!(seen
.windows(2)
.all(|w| w[1].rendered_seconds >= w[0].rendered_seconds));
}
#[test]
fn loudness_normalization_applies_the_ceiling_when_already_on_target() {
let sr = 44_100u32;
let ceiling = -6.0f32;
let samples: Vec<f32> = (0..sr as usize * 2)
.flat_map(|i| {
let s = 0.9 * (2.0 * std::f32::consts::PI * 997.0 * i as f32 / sr as f32).sin();
[s, s]
})
.collect();
let target = crate::loudness::integrated_lufs(&samples, sr);
let peak_in = crate::loudness::true_peak_dbtp(&samples, sr);
assert!(
peak_in > ceiling + 1.0,
"premise: the input must exceed the ceiling, but it is {peak_in:.2} dBTP"
);
let out = normalize_loudness(&samples, sr, target, ceiling);
let back: Vec<f32> = out.iter().map(|&s| s as f32 / 32768.0).collect();
let peak_out = crate::loudness::true_peak_dbtp(&back, sr);
assert!(
peak_out <= ceiling + 0.3,
"already-on-target signal was returned unlimited: {peak_out:.2} dBTP \
against a {ceiling:.1} dBTP ceiling"
);
let loudness_out = crate::loudness::integrated_lufs(&back, sr);
assert!(
loudness_out <= target + 0.3,
"loudness overshot the target: {loudness_out:.2} LUFS against {target:.2}"
);
}
#[test]
fn normalize_loudness_hits_target_and_ceiling() {
let fs = 44100.0;
let n = (fs * 5.0) as usize;
let mut sig = Vec::with_capacity(n * 2);
for i in 0..n {
let s = 0.03 * (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / fs).sin();
sig.push(s);
sig.push(s);
}
let pcm = normalize_loudness(&sig, fs as u32, -18.0, -1.0);
let dec: Vec<f32> = pcm.iter().map(|&s| s as f32 / 32768.0).collect();
let lufs = crate::loudness::integrated_lufs(&dec, fs as u32);
let tp = crate::loudness::true_peak_dbtp(&dec, fs as u32);
assert!(
(lufs - (-18.0)).abs() < 0.5,
"decoded loudness should be ~-18 LUFS, got {lufs}"
);
assert!(
tp <= -0.8,
"decoded true peak should be <= ceiling, got {tp}"
);
}
#[test]
fn normalize_loudness_is_pure_scalar_under_ceiling() {
let fs = 44100.0;
let n = (fs * 5.0) as usize;
let mut sig = Vec::with_capacity(n * 2);
for i in 0..n {
let s = 0.05 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / fs).sin();
sig.push(s);
sig.push(s);
}
let measured = crate::loudness::integrated_lufs(&sig, fs as u32);
let pcm = normalize_loudness(&sig, fs as u32, measured, 0.0);
let reference = normalize_to_i16(&sig, 1.0, 1.0);
let max_lsb = pcm
.iter()
.zip(&reference)
.map(|(a, b)| (a - b).unsigned_abs())
.max()
.unwrap();
assert!(max_lsb <= 1, "pure-scalar path drifted by {max_lsb} LSB");
}
#[test]
fn normalize_loudness_recovers_on_high_crest() {
let fs = 44100.0;
let n = (fs * 6.0) as usize;
let burst_period = fs as usize / 3; let burst_len = fs as usize / 200; let mut sig = Vec::with_capacity(n * 2);
for i in 0..n {
let t = i as f32 / fs;
let mut s = 0.05 * (2.0 * std::f32::consts::PI * 180.0 * t).sin(); if i % burst_period < burst_len {
s += 0.55 * (2.0 * std::f32::consts::PI * 1200.0 * t).sin(); }
sig.push(s);
sig.push(s);
}
let raw = crate::loudness::integrated_lufs(&sig, fs as u32);
let g = 10f32.powf((-18.0 - raw) / 20.0);
let mut once: Vec<f32> = sig.iter().map(|&x| x * g).collect();
crate::loudness::limit_true_peak(&mut once, fs as u32, -6.0);
let single = crate::loudness::integrated_lufs(&once, fs as u32);
assert!(
single < -18.0 - 0.5,
"test signal must under-shoot single-pass (got {single}) or it proves nothing"
);
let pcm = normalize_loudness(&sig, fs as u32, -18.0, -6.0);
let dec: Vec<f32> = pcm.iter().map(|&s| s as f32 / 32768.0).collect();
let lufs = crate::loudness::integrated_lufs(&dec, fs as u32);
let tp = crate::loudness::true_peak_dbtp(&dec, fs as u32);
assert!(
(lufs - (-18.0)).abs() < 0.4,
"recovery loop should reach ~-18 LUFS, got {lufs} (single-pass was {single})"
);
assert!(
tp <= -5.8,
"true peak must respect the -6 dBTP ceiling, got {tp}"
);
}
fn test_song(events: Vec<(f64, EvKind)>, seconds: f64) -> Song {
Song {
events: events
.into_iter()
.map(|(sec, kind)| Ev { sec, kind })
.collect(),
seconds,
markers: Vec::new(),
title: String::new(),
initial_bpm: 120.0,
}
}
fn test_opts(sr: f32) -> Options {
Options {
sr,
wet: 0.0,
tail: 0.5,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
}
}
fn left(stereo: &[f32]) -> Vec<f32> {
stereo.iter().step_by(2).copied().collect()
}
fn rms(seg: &[f32]) -> f32 {
(seg.iter().map(|&x| (x * x) as f64).sum::<f64>() / seg.len() as f64).sqrt() as f32
}
#[test]
fn gm0_fra_phrase_keeps_energy_through_short_key_up_gaps() {
let sr = 44100.0;
let onsets = [0.100f64, 0.475, 0.850];
let keys = [62u8, 66, 69];
let mut events = vec![
(0.0, EvKind::Prog { ch: 0, prog: 0 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 91,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 94,
val: 0,
},
),
];
for (&onset, &key) in onsets.iter().zip(keys.iter()) {
events.push((
onset,
EvKind::NoteOn {
ch: 0,
key,
vel: 104,
},
));
events.push((onset + 0.3125, EvKind::NoteOff { ch: 0, key }));
}
events.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let mut opts = test_opts(sr);
opts.samples = true;
let rendered = left(&render(&test_song(events, 1.35), &opts).0);
let window = (0.020 * sr) as usize;
let mut drops = Vec::new();
for &onset in &onsets {
let note_off = onset + 0.3125;
let pre_end = (note_off * sr as f64) as usize;
let gap_end = ((note_off + 0.0625) * sr as f64) as usize;
let before = rms(&rendered[pre_end - window..pre_end]);
let after = rms(&rendered[gap_end - window..gap_end]);
drops.push(20.0 * (before / after.max(1e-9)).log10());
}
for (index, (&drop, &key)) in drops.iter().zip(keys.iter()).enumerate() {
let (t60, _) = voices::PianoDamper::Felt.t60_for(key);
let expect = 60.0 * 0.0625 / t60;
assert!(
drop <= expect * 1.10,
"FRA note {} (key {key}) gap drop {drop:.2} dB exceeds 1.10x the \
{expect:.2} dB its {t60:.3}s damper predicts — the note is cut, not \
damped: {drops:?}",
index + 1,
);
assert!(
drop >= expect * 0.40,
"FRA note {} (key {key}) gap drop {drop:.2} dB is under 0.40x the \
{expect:.2} dB its {t60:.3}s damper predicts — the note is not \
damping between repeats: {drops:?}",
index + 1,
);
}
}
fn repeat_song(prog: u8, key: u8, n: usize, ioi: f64, gate: f64, secs: f64) -> Song {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog })];
for i in 0..n {
let t = 0.1 + i as f64 * ioi;
ev.push((
t,
EvKind::NoteOn {
ch: 0,
key,
vel: 55,
},
));
ev.push((t + gate, EvKind::NoteOff { ch: 0, key }));
}
test_song(ev, secs)
}
fn carrier_p10_over_p90(out: &[f32], sr: f32, f0: f32, t0: f64, t1: f64) -> f32 {
let hop = (0.010 * sr) as usize;
let (a, b) = (
(t0 * sr as f64) as usize / hop,
(t1 * sr as f64) as usize / hop,
);
let mut mags: Vec<f32> = (a..b)
.map(|k| crate::testutil::mag_at(&out[k * hop..(k + 1) * hop], sr, f0))
.collect();
mags.sort_by(|x, y| x.partial_cmp(y).unwrap());
mags[mags.len() / 10] / mags[mags.len() * 9 / 10].max(1e-12)
}
#[test]
fn fast_same_key_restrikes_shimmer_not_click_train() {
let sr = 44100.0;
let key = 83u8; let f0 = crate::dsp::key_freq(key);
let song = repeat_song(25, key, 40, 0.075, 0.045, 3.6);
let out = left(&render(&song, &test_opts(sr)).0);
let carrier = carrier_p10_over_p90(&out, sr, f0, 0.6, 2.9);
assert!(
carrier > 0.10,
"tremolo carrier collapses between strokes: f0 p10/p90 = {carrier:.3} \
(fresh-spawn click train ≈ 0.03, restrike shimmer ≈ 0.22)"
);
let frame = (0.020 * sr) as usize;
let a = (0.6 * sr) as usize / frame;
let b = (2.9 * sr) as usize / frame;
let mut frames: Vec<f32> = (a..b)
.map(|k| rms(&out[k * frame..(k + 1) * frame]))
.collect();
frames.sort_by(|x, y| x.partial_cmp(y).unwrap());
let ripple = frames[frames.len() / 10] / frames[frames.len() * 9 / 10].max(1e-12);
assert!(
ripple < 0.63,
"strokes no longer articulate (broadband ripple {ripple:.3} ≥ 0.63 \
≈ under 4 dB): the tremolo degenerated into a slur"
);
let slow = repeat_song(25, key, 12, 0.200, 0.045, 3.6);
let out = left(&render(&slow, &test_opts(sr)).0);
let slow_carrier = carrier_p10_over_p90(&out, sr, f0, 0.6, 2.4);
assert!(
slow_carrier < 0.05,
"slow same-note picking lost its articulation: f0 p10/p90 = \
{slow_carrier:.3} (articulated ≈ 0.006) — the tremolo IOI gate \
has widened into picking territory"
);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn a_routing_change_inside_the_tremolo_window_spawns_the_new_voice() {
let sr = 44100.0;
let opts = || CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: true,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
};
const KEY: u8 = 74;
let ioi = (0.075 * sr) as u64;
let cc = |ch: u8, num: u8, val: u8| EvKind::Cc { ch, num, val };
#[allow(clippy::type_complexity)]
let cases: Vec<(&str, Vec<EvKind>, Vec<EvKind>)> = vec![
(
"steel guitar -> mandolin (CC32 0 -> 96)",
vec![EvKind::Prog { ch: 0, prog: 25 }, cc(0, 32, 0)],
vec![cc(0, 32, 96), EvKind::Prog { ch: 0, prog: 25 }],
),
(
"mandolin -> steel guitar (CC32 96 -> 0)",
vec![EvKind::Prog { ch: 0, prog: 25 }, cc(0, 32, 96)],
vec![cc(0, 32, 0), EvKind::Prog { ch: 0, prog: 25 }],
),
(
"alt bank 1 -> alt bank 2 (both CC0 nonzero, on a plucked program)",
vec![EvKind::Prog { ch: 0, prog: 25 }, cc(0, 0, 1)],
vec![cc(0, 0, 2)],
),
(
"melodic -> XG drum part (CC0 -> 127)",
vec![EvKind::Prog { ch: 0, prog: 25 }, cc(0, 0, 0)],
vec![cc(0, 0, 127)],
),
];
let mut wrong: Vec<&str> = Vec::new();
for (what, setup, change) in cases {
let mut core = EngineCore::new(opts());
for ev in setup {
core.handle_event(ev);
}
core.note_on(0, KEY, 100);
assert_eq!(core.active.len(), 1, "{what}: the first note did not spawn");
core.now += ioi;
for ev in change {
core.handle_event(ev);
}
core.note_on(0, KEY, 100);
if core.active.len() != 2 {
wrong.push(what);
}
}
assert!(
wrong.is_empty(),
"{} routing change(s) re-picked the PREVIOUS bank's voice instead of \
spawning the newly selected one — the requested voice never sounds:\n {}",
wrong.len(),
wrong.join("\n ")
);
let mut core = EngineCore::new(opts());
core.handle_event(EvKind::Prog { ch: 0, prog: 25 });
core.handle_event(cc(0, 32, 96));
core.note_on(0, KEY, 100);
core.now += ioi;
core.handle_event(cc(0, 32, 96)); core.note_on(0, KEY, 100);
assert_eq!(
core.active.len(),
1,
"an UNCHANGED bank stopped taking the tremolo-restrike path — TREM1 is \
disabled and a fast repeated figure is back to a click train"
);
let mut core = EngineCore::new(opts());
core.handle_event(EvKind::Prog { ch: 0, prog: 25 });
core.handle_event(cc(0, 32, 0));
core.note_on(0, KEY, 100);
core.now += ioi;
core.handle_event(cc(0, 32, 96));
core.note_on(0, KEY, 100);
let spawned = core.active.last().expect("the new voice");
assert_eq!(
spawned.bank_lsb, 96,
"the spawned voice froze the OLD bank LSB — its routing identity is wrong \
and the next restrike would match on a lie"
);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn the_mandolin_strike_phase_is_bank_scoped_and_survives_a_retrigger() {
let sr = 44100.0;
let opts = || CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: true,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
};
const KEY: u8 = 74;
let takes = voices::variation_round_robins(25, 96).expect("the mandolin rotates takes");
let fast = (0.075 * sr) as u64; let gap = (0.400 * sr) as u64; let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
let mandolin = |core: &mut EngineCore| {
core.handle_event(EvKind::Prog { ch: 0, prog: 25 });
core.handle_event(cc(32, 96));
};
let sounding = |core: &EngineCore| {
core.active
.last()
.expect("a voice")
.voice
.rr_phase()
.expect("the mandolin voice owns a round-robin phase")
};
let mut core = EngineCore::new(opts());
mandolin(&mut core);
core.note_on(0, KEY, 100);
assert_eq!(sounding(&core), 0, "the first stroke is not take 0");
core.now += fast;
core.note_on(0, KEY, 100);
assert_eq!(
sounding(&core),
1,
"the fast stroke did not rotate to the next take"
);
core.now += gap;
core.note_on(0, KEY, 100);
assert_eq!(
sounding(&core),
2,
"the fresh voice after the gap replayed a take the retrigger already \
played — the engine counter did not follow the voice's rotation, so the \
recorded down/up pick order breaks at every phrase boundary"
);
let mut core = EngineCore::new(opts());
mandolin(&mut core);
core.note_on(0, KEY, 100);
for _ in 0..takes {
core.now += fast;
core.note_on(0, KEY, 100);
}
assert_eq!(sounding(&core), 0, "the take sequence did not wrap");
core.now += gap;
core.note_on(0, KEY, 100);
assert_eq!(
sounding(&core),
1,
"the phrase after a wrapped tremolo run restarted the sequence instead of \
continuing it"
);
for (what, setup) in [
("base steel guitar (LSB 0)", 0u8),
("an undefined LSB variation", 7u8),
] {
let mut core = EngineCore::new(opts());
core.handle_event(EvKind::Prog { ch: 0, prog: 25 });
core.handle_event(cc(32, setup));
for _ in 0..3 {
core.now += gap;
core.note_on(0, KEY, 100);
}
core.now += gap;
mandolin(&mut core);
core.note_on(0, KEY, 100);
assert_eq!(
sounding(&core),
0,
"{what} consumed the mandolin's strike phase: its first stroke is not \
take 0"
);
}
}
#[cfg(feature = "embedded-samples")]
#[test]
fn sampled_fret_noise_cycles_per_channel_across_interleaved_events() {
let mut core = EngineCore::new(CoreOptions {
sr: 44_100.0,
wet: 0.0,
delay_s: 0.0,
samples: true,
solo: 0xFFFF,
gtr_symp_on: false,
drum_room_on: false,
sitar_symp_on: false,
});
for ch in [0, 1] {
core.handle_event(EvKind::Prog { ch, prog: 120 });
}
core.handle_event(EvKind::Prog { ch: 2, prog: 0 });
let takes = ferrosintesis_samples_fretnoise::ROUND_ROBINS;
let mut next = [0usize; 2];
let mut seen = [
std::collections::BTreeSet::new(),
std::collections::BTreeSet::new(),
];
for event in 0..528usize {
let ch = (event % 2) as u8;
core.handle_event(EvKind::NoteOn {
ch,
key: 48 + (event % 25) as u8,
vel: 100,
});
let sounding = core
.active
.last()
.expect("GM120 spawned a voice")
.voice
.rr_phase()
.expect("the sampled fret-noise voice reports its take");
assert_eq!(
sounding,
next[ch as usize] % takes,
"channel {ch} event {} did not take the next canonical round robin",
next[ch as usize]
);
seen[ch as usize].insert(sounding);
next[ch as usize] += 1;
if event % 4 == 0 {
core.handle_event(EvKind::NoteOn {
ch: 2,
key: 60 + (event % 12) as u8,
vel: 80,
});
}
if event % 11 == 0 {
core.handle_event(EvKind::Prog { ch, prog: 0 });
core.handle_event(EvKind::NoteOn {
ch,
key: 36 + (event % 24) as u8,
vel: 80,
});
core.handle_event(EvKind::Prog { ch, prog: 120 });
}
}
for (ch, phases) in seen.iter().enumerate() {
assert_eq!(
phases.len(),
takes,
"channel {ch} did not reach all {takes} fret-noise takes"
);
}
}
fn drum_song(hits: &[(f64, u8, u8)], secs: f64, ccs: &[(u8, u8)]) -> Song {
let mut ev: Vec<(f64, EvKind)> = ccs
.iter()
.map(|&(num, val)| (0.0, EvKind::Cc { ch: 9, num, val }))
.collect();
for &(t, key, vel) in hits {
ev.push((t, EvKind::NoteOn { ch: 9, key, vel }));
}
ev.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
test_song(ev, secs)
}
fn drum_prog_song(prog: Option<u8>) -> Song {
let mut ev = Vec::new();
if let Some(prog) = prog {
ev.push((0.0, EvKind::Prog { ch: 9, prog }));
}
for (t, key, vel) in [
(0.05, 36, 110),
(0.30, 38, 104),
(0.55, 46, 96),
(0.85, 49, 108),
(1.30, 51, 102),
] {
ev.push((t, EvKind::NoteOn { ch: 9, key, vel }));
}
test_song(ev, 2.0)
}
fn right(stereo: &[f32]) -> Vec<f32> {
stereo.iter().skip(1).step_by(2).copied().collect()
}
#[test]
fn channel_10_program_change_keeps_v3_default_kit() {
let sr = 44100.0;
let opts = test_opts(sr);
let no_pc = render(&drum_prog_song(None), &opts).0;
assert!(rms(&no_pc) > 1e-4, "default drum kit should sound");
for prog in [0u8, 8, 16] {
let got = render(&drum_prog_song(Some(prog)), &opts).0;
assert_eq!(
got, no_pc,
"channel-10 Program Change {prog} changed the V3 kit"
);
}
}
#[cfg(feature = "embedded-samples")]
#[test]
fn channel_10_program_24_selects_the_synth_kit() {
let sr = 44100.0;
let sampled = Options {
samples: true,
..test_opts(sr)
};
let default_kit = render(&drum_prog_song(None), &sampled).0;
let synth = render(&drum_prog_song(Some(24)), &sampled).0;
assert_ne!(
synth, default_kit,
"channel-10 PC 24 did not switch off the sampled kit"
);
let modeled = render(&drum_prog_song(Some(24)), &test_opts(sr)).0;
assert_eq!(
synth, modeled,
"synth kit differs from the samples-off modeled path"
);
}
#[test]
fn channel_10_program_25_selects_the_original_v1_kit() {
let sr = 44100.0;
let sampled = Options {
samples: true,
..test_opts(sr)
};
let default_kit = render(&drum_prog_song(None), &sampled).0;
let original = render(&drum_prog_song(Some(25)), &sampled).0;
let synth = render(&drum_prog_song(Some(24)), &sampled).0;
assert!(rms(&original) > 1e-4, "PC 25 original kit should sound");
assert_ne!(
original, default_kit,
"channel-10 PC 25 did not leave the sampled default kit"
);
assert_ne!(
original, synth,
"channel-10 PC 25 selected the synth kit, not the original V1 kit"
);
let dry = render(&drum_prog_song(Some(25)), &test_opts(sr)).0;
assert_eq!(
original, dry,
"the original V1 kit must ignore the samples flag"
);
}
#[test]
fn xg_bank_127_routes_channel_to_drums() {
let sr = 44100.0;
let opt = test_opts(sr);
let key = 62u8;
let hit = |ch: u8, xg: bool| -> Vec<f32> {
let mut ev = Vec::new();
if xg {
ev.push((
0.0,
EvKind::Cc {
ch,
num: 0,
val: 127,
},
)); }
ev.push((0.0, EvKind::Prog { ch, prog: 16 })); ev.push((0.05, EvKind::NoteOn { ch, key, vel: 100 }));
render(&test_song(ev, 1.5), &opt).0
};
let drum9 = hit(9, true); let xg13 = hit(13, true); let organ = hit(13, false);
assert_eq!(
xg13, drum9,
"CC0=127 on ch13 must render key {key} as a drum, identical to ch9"
);
assert_ne!(
xg13, organ,
"CC0=127 must route away from the melodic Drawbar Organ"
);
let f0 = crate::dsp::key_freq(key); let tail = |out: &[f32]| {
let m = left(out);
let a = sr as usize; let b = (1.4 * sr as f64) as usize;
crate::testutil::mag_at(&m[a..b], sr, f0)
};
assert!(
tail(&xg13) < 0.1 * tail(&organ),
"drum tail at D4 ({}) must be far below the sustained organ ({})",
tail(&xg13),
tail(&organ)
);
}
#[test]
fn declared_drum_channels_honour_the_kit_ladder() {
let sr = 44100.0;
let opt = Options {
samples: true,
..test_opts(sr)
};
let hits = |ch: u8| {
[
(
0.05,
EvKind::NoteOn {
ch,
key: 38,
vel: 108,
},
),
(
0.40,
EvKind::NoteOn {
ch,
key: 42,
vel: 96,
},
),
(
0.75,
EvKind::NoteOn {
ch,
key: 38,
vel: 120,
},
),
]
};
let ch10 = |prog: u8| {
let mut ev = vec![(0.0, EvKind::Prog { ch: 9, prog })];
ev.extend(hits(9));
render(&test_song(ev, 2.0), &opt).0
};
let xg = |ch: u8, prog: u8| {
let mut ev = vec![
(
0.0,
EvKind::Cc {
ch,
num: 0,
val: 127,
},
),
(0.0, EvKind::Prog { ch, prog }),
];
ev.extend(hits(ch));
render(&test_song(ev, 2.0), &opt).0
};
let gs = |ch: u8, prog: u8| {
let mut ev = vec![
(0.0, EvKind::DrumMode { ch, on: true }),
(0.0, EvKind::Prog { ch, prog }),
];
ev.extend(hits(ch));
render(&test_song(ev, 2.0), &opt).0
};
let default_kit = ch10(0);
let brush_ch10 = ch10(40);
assert_ne!(
brush_ch10, default_kit,
"PC 40 must select the brush kit on ch10 — the reference itself is broken"
);
assert!(rms(&brush_ch10) > 1e-4, "the brush reference must sound");
let maxdiff = |a: &[f32], b: &[f32]| {
a.iter()
.zip(b)
.map(|(x, y)| (x - y).abs())
.fold(0f32, f32::max)
};
const SAME: f32 = 1e-3;
for (label, got) in [("XG", xg(13, 40)), ("GS", gs(11, 40))] {
let d = maxdiff(&got, &brush_ch10);
assert!(
d < SAME,
"{label}-declared drum channel ignored PC 40 (max|diff| vs the ch10 \
brush kit = {d:.5}); it must select the brush kit as channel 10 does"
);
}
for (label, got) in [("XG", xg(13, 0)), ("GS", gs(11, 0))] {
let d = maxdiff(&got, &default_kit);
assert!(
d < SAME,
"{label}-declared drum channel with PC 0 is not the default kit \
(max|diff| = {d:.5})"
);
}
let kit_delta = maxdiff(&brush_ch10, &default_kit);
assert!(
kit_delta > 50.0 * SAME,
"brush and default kits are barely distinguishable (max|diff| = \
{kit_delta:.5}); the checks above would be passing vacuously"
);
}
#[test]
fn gs_rhythm_part_routes_channel_to_drums() {
let sr = 44100.0;
let opt = test_opts(sr);
let key = 62u8;
let render_ev = |ev: Vec<(f64, EvKind)>| render(&test_song(ev, 1.5), &opt).0;
let gs = render_ev(vec![
(0.0, EvKind::DrumMode { ch: 11, on: true }), (
0.05,
EvKind::NoteOn {
ch: 11,
key,
vel: 100,
},
),
]);
let drum9 = render_ev(vec![(
0.05,
EvKind::NoteOn {
ch: 9,
key,
vel: 100,
},
)]);
let melodic = render_ev(vec![
(0.0, EvKind::Prog { ch: 11, prog: 0 }), (
0.05,
EvKind::NoteOn {
ch: 11,
key,
vel: 100,
},
),
]);
assert_eq!(
gs, drum9,
"GS rhythm part (ch11) must render as a drum, identical to ch9"
);
assert_ne!(
gs, melodic,
"GS rhythm part must not render as a melodic voice"
);
}
#[test]
fn gs_rhythm_survives_bank_select_but_not_reset() {
let sr = 44100.0;
let opt = test_opts(sr);
let key = 62u8;
let render_ev = |ev: Vec<(f64, EvKind)>| render(&test_song(ev, 1.5), &opt).0;
let drum9 = render_ev(vec![(
0.05,
EvKind::NoteOn {
ch: 9,
key,
vel: 100,
},
)]);
let melodic = render_ev(vec![
(0.0, EvKind::Prog { ch: 11, prog: 0 }),
(
0.05,
EvKind::NoteOn {
ch: 11,
key,
vel: 100,
},
),
]);
let after_bank = render_ev(vec![
(0.0, EvKind::DrumMode { ch: 11, on: true }),
(
0.01,
EvKind::Cc {
ch: 11,
num: 0,
val: 0,
},
), (
0.02,
EvKind::Cc {
ch: 11,
num: 32,
val: 0,
},
), (0.03, EvKind::Prog { ch: 11, prog: 0 }),
(
0.05,
EvKind::NoteOn {
ch: 11,
key,
vel: 100,
},
),
]);
assert_eq!(
after_bank, drum9,
"GS rhythm part must survive CC0=0 + PC (proves gs_drum is not the CC0-driven xg_drum)"
);
let after_reset = render_ev(vec![
(0.0, EvKind::DrumMode { ch: 11, on: true }),
(0.02, EvKind::GsReset),
(0.03, EvKind::Prog { ch: 11, prog: 0 }),
(
0.05,
EvKind::NoteOn {
ch: 11,
key,
vel: 100,
},
),
]);
assert_eq!(
after_reset, melodic,
"GS Reset must clear the rhythm part (renders melodic, identical to a plain ch11 note)"
);
}
#[test]
fn gs_routing_changes_rederive_program_state_without_a_program_change() {
let sr = 44100.0;
let opt = test_opts(sr);
let key = 62u8;
let render_ev = |ev: Vec<(f64, EvKind)>| render(&test_song(ev, 1.5), &opt).0;
let note = |ch: u8| (0.9, EvKind::NoteOn { ch, key, vel: 100 });
for prog in [29u8, 52] {
let melodic = render_ev(vec![(0.0, EvKind::Prog { ch: 11, prog }), note(11)]);
let drum = render_ev(vec![
(0.0, EvKind::DrumMode { ch: 11, on: true }),
(0.1, EvKind::Prog { ch: 11, prog }),
note(11),
]);
assert_ne!(
melodic, drum,
"prog {prog}: the two references are identical, so this test cannot \
distinguish anything"
);
let reset_no_pc = render_ev(vec![
(0.0, EvKind::DrumMode { ch: 11, on: true }),
(0.1, EvKind::Prog { ch: 11, prog }),
(0.2, EvKind::GsReset),
note(11),
]);
assert_eq!(
reset_no_pc, melodic,
"prog {prog}: after GS Reset the channel is melodic again but kept \
drum-derived sends/drive — a Program Change should not be required to \
repair it"
);
let declared_no_pc = render_ev(vec![
(0.0, EvKind::Prog { ch: 11, prog }),
(0.2, EvKind::DrumMode { ch: 11, on: true }),
note(11),
]);
assert_eq!(
declared_no_pc, drum,
"prog {prog}: a newly declared rhythm part kept melodic sends/drive"
);
}
}
#[cfg(feature = "embedded-samples")]
#[test]
fn kit_internal_balance_hats_audible_crash_an_accent() {
let sr = 44100.0;
let mut opt = test_opts(sr);
opt.samples = true;
let hits = [
(0.0, 36, 100),
(1.0, 38, 100),
(2.0, 42, 100),
(3.0, 49, 100),
(4.0, 45, 100),
];
let out = render(&drum_song(&hits, 5.0, &[]), &opt).0;
let mono = left(&out);
let db = |t: f64| {
let a = (t * sr as f64) as usize;
let b = ((t + 0.4) * sr as f64) as usize;
20.0 * rms(&mono[a..b.min(mono.len())]).max(1e-9).log10()
};
let (kick, snare, hat, crash, tom) = (db(0.0), db(1.0), db(2.0), db(3.0), db(4.0));
assert!(
hat - kick > -16.0,
"hi-hats buried vs kick: {:.1} dB",
hat - kick
);
assert!(crash < kick, "crash not below kick: {:.1} dB", crash - kick);
assert!(
snare - kick > -9.0,
"snare too far back: {:.1} dB",
snare - kick
);
assert!(tom - kick > -9.0, "toms too far back: {:.1} dB", tom - kick);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn samples_option_reaches_channel_10_drums() {
let sr = 44100.0;
let song = drum_prog_song(None);
let off = render(&song, &test_opts(sr)).0;
let mut on_opts = test_opts(sr);
on_opts.samples = true;
let on = render(&song, &on_opts).0;
assert_ne!(off, on, "samples=true did not alter channel-10 drums");
assert!(
on.iter().all(|x| x.is_finite()),
"sampled drum render produced non-finite samples"
);
let db = 20.0 * (rms(&on) / rms(&off).max(1e-12)).log10();
assert!(
db.abs() <= 6.0,
"samples changed drum render level by {db:+.2} dB"
);
}
#[test]
fn kit_images_in_stereo_without_mono_loss() {
let sr = 44100.0;
let mut hits = Vec::new();
for i in 0..8 {
hits.push((i as f64 * 0.25, 42u8, 90u8)); }
for i in 0..8 {
hits.push((0.125 + i as f64 * 0.25, 51, 120)); }
let song = drum_song(&hits, 2.2, &[]);
let out = render(&song, &test_opts(sr)).0;
let (l, r) = (left(&out), right(&out));
let corr = crate::testutil::inter_corr(&l, &r);
assert!(
corr < 0.93,
"kit stereo field collapsed toward a mono point source: corr {corr}"
);
let hat_song = drum_song(
&(0..8)
.map(|i| (i as f64 * 0.25, 42, 90))
.collect::<Vec<_>>(),
2.2,
&[],
);
let hout = render(&hat_song, &test_opts(sr)).0;
let (hl, hr) = (left(&hout), right(&hout));
assert!(
rms(&hl) > 1.2 * rms(&hr),
"hats not left: L {} R {}",
rms(&hl),
rms(&hr)
);
let mono: Vec<f32> = hl.iter().zip(&hr).map(|(a, b)| 0.5 * (a + b)).collect();
let mid = ((rms(&hl).powi(2) + rms(&hr).powi(2)) / 2.0).sqrt();
let loss_db = 20.0 * (rms(&mono) / mid.max(1e-12)).log10();
assert!(loss_db > -1.0, "mono collapse: {loss_db:.2} dB");
let kick_song = drum_song(&[(0.05, 36, 110), (0.55, 38, 105)], 1.2, &[]);
let kout = render(&kick_song, &test_opts(sr)).0;
let (kl, kr) = (left(&kout), right(&kout));
let bal = rms(&kl) / rms(&kr).max(1e-12);
assert!((0.95..=1.05).contains(&bal), "kick/snare off centre: {bal}");
}
#[test]
fn drum_strip_parity_preserved() {
let sr = 44100.0;
let hits: Vec<(f64, u8, u8)> = (0..6).map(|i| (0.05 + i as f64 * 0.2, 42, 100)).collect();
let silent = render(&drum_song(&hits, 1.6, &[(7, 0)]), &test_opts(sr)).0;
assert!(
rms(&silent) < 1e-6,
"CC7=0 drums still audible: {}",
rms(&silent)
);
let plain = render(&drum_song(&hits, 1.6, &[]), &test_opts(sr)).0;
let dark = render(&drum_song(&hits, 1.6, &[(74, 20)]), &test_opts(sr)).0;
let cent = |s: &[f32]| {
let m: Vec<f32> = s.chunks_exact(2).map(|p| 0.5 * (p[0] + p[1])).collect();
crate::testutil::centroid(&m, sr)
};
assert!(
cent(&dark) < 0.8 * cent(&plain),
"ch-9 wah dead: dark {} vs plain {}",
cent(&dark),
cent(&plain)
);
let wet_opts = || Options {
wet: 0.32,
..test_opts(sr)
};
let kick = |cc91: u8| {
let song = drum_song(&[(0.05, 36, 115)], 2.0, &[(91, cc91)]);
render(&song, &wet_opts()).0
};
let tail =
|s: &[f32], t0: f32, t1: f32| rms(&left(s)[(t0 * sr) as usize..(t1 * sr) as usize]);
assert!(
tail(&kick(127), 1.2, 1.9) > 2.0 * tail(&kick(0), 1.2, 1.9),
"drum hall send lost"
);
let panned = render(&drum_song(&hits, 1.6, &[(10, 127)]), &test_opts(sr)).0;
let (pl, pr) = (left(&panned), right(&panned));
assert!(
rms(&pr) > 1.2 * rms(&pl),
"CC10 ignored on ch 9: L {} R {}",
rms(&pl),
rms(&pr)
);
}
#[test]
fn cc121_resets_controllers_but_preserves_mix_state() {
let sr = 44100.0;
let mut core = EngineCore::new(CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
});
core.handle_event(EvKind::Prog { ch: 0, prog: 30 });
core.handle_event(EvKind::Cc {
ch: 0,
num: 7,
val: 80,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 10,
val: 20,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 64,
val: 127,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 1,
val: 100,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 74,
val: 20,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 91,
val: 50,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 93,
val: 100,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 94,
val: 40,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 5,
val: 90,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 101,
val: 0,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 100,
val: 0,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 6,
val: 12,
});
let porta_time_before = core.strips[0].porta_time;
core.handle_event(EvKind::Cc {
ch: 0,
num: 121,
val: 0,
});
let mut sink = [0f32; BLOCK * 2];
core.render_block_add(BLOCK, &mut sink);
let s = &core.strips[0];
assert!(!s.sustain, "CC64 must reset");
assert!(!s.mod_authored, "CC1 must reset");
assert_eq!(s.expr, 1.0, "CC11 must reset to full");
assert_eq!(s.expr_target, 1.0);
assert_eq!(s.rpn_msb, 127, "RPN selector must park at Null");
assert_eq!(s.rpn_lsb, 127);
assert_eq!(s.bend_wheel, 0.0, "pitch bend must return to centre");
assert!((s.volume - (80.0f32 / 127.0).powi(2)).abs() < 1e-6);
assert!((s.pan - 20.0 / 127.0).abs() < 1e-6);
assert!(
s.drive.is_some(),
"program-derived drive should be restored"
);
assert_eq!(
s.reverb_send,
50.0 / 127.0,
"CC121 discarded the authored CC91 reverb send"
);
assert_eq!(
s.chorus_send,
100.0 / 127.0,
"CC121 discarded the authored CC93 chorus send"
);
assert_eq!(
s.delay_send,
40.0 / 127.0,
"CC121 discarded the authored CC94 delay send"
);
assert!(s.reverb_authored && s.chorus_authored && s.delay_authored);
assert_eq!(
s.bend_range, 12.0,
"CC121 reverted the RPN-set pitch-bend range"
);
assert_eq!(s.fine, 1.0);
assert_eq!(s.bend, s.fine);
assert!(
s.wah.is_some(),
"CC121 tore down the CC74 filter — CC74 is a preserved sound controller"
);
assert!(
s.cutoff_target < WAH_MAX_HZ,
"CC121 reverted the authored CC74 cutoff"
);
assert_eq!(s.res_target, WAH_Q, "CC71 was never authored here");
assert!(!s.porta_on, "CC65 must reset");
assert_eq!(
s.porta_time, porta_time_before,
"CC121 reverted the authored CC5 portamento time"
);
}
#[test]
fn gm_system_on_restores_fresh_state_and_preserves_stats() {
let mut core = slew_test_core(44_100.0);
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
for kind in [
EvKind::Prog { ch: 0, prog: 30 },
cc(7, 80),
cc(101, 0),
cc(100, 0),
cc(6, 12),
EvKind::DrumMode { ch: 0, on: true },
EvKind::XgEffectParam {
addr_lo: 0x40,
data: [0x4B, 0x11],
len: 2,
},
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
EvKind::NoteOn {
ch: 1,
key: 64,
vel: 100,
},
] {
core.handle_event(kind);
}
let mut audio = [0.0; BLOCK * 2];
core.render_block_add(BLOCK, &mut audio);
let before = core.stats();
assert_eq!(before.voices_spawned, 2);
assert_eq!(before.max_polyphony, 2);
assert!(before.peak > 0.0);
assert_eq!(core.voice_seed_index, 2);
assert_eq!(core.strips[0].program, 30);
assert_eq!(core.strips[0].bend_range, 12.0);
assert!(core.strips[0].gs_drum);
assert_eq!(core.xg.var_type_msb, 0x4B);
core.handle_event(EvKind::GmReset);
let strip = &core.strips[0];
assert_eq!(core.active_voice_count(), 0);
assert_eq!(strip.program, 0);
assert_eq!(strip.volume, (100.0f32 / 127.0).powi(2));
assert!(!strip.volume_authored);
assert_eq!(strip.bend_range, 2.0);
assert_eq!((strip.rpn_msb, strip.rpn_lsb), (127, 127));
assert!(!strip.gs_drum);
assert_eq!(core.xg.var_type_msb, 0);
assert_eq!(core.xg.var_part, 127);
assert_eq!(core.stats(), before);
assert_eq!(core.voice_seed_index, 0);
}
#[test]
fn nrpn_select_does_not_corrupt_the_rpn_bend_range() {
let sr = 44100.0;
let mut core = EngineCore::new(CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
});
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
core.handle_event(cc(101, 0)); core.handle_event(cc(100, 0)); core.handle_event(cc(6, 2)); assert_eq!(core.strips[0].bend_range, 2.0);
core.handle_event(cc(99, 1)); core.handle_event(cc(98, 8)); core.handle_event(cc(6, 80));
assert_eq!(
core.strips[0].bend_range, 2.0,
"NRPN select failed to invalidate the RPN latch — bend range corrupted"
);
assert_eq!(core.strips[0].rpn_msb, 127);
assert_eq!(core.strips[0].rpn_lsb, 127);
}
#[test]
fn program_change_preserves_authored_effect_sends() {
let sr = 44100.0;
let core_opts = || CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
};
let mut core = EngineCore::new(core_opts());
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
core.handle_event(EvKind::Prog { ch: 0, prog: 30 });
core.handle_event(cc(93, 100));
core.handle_event(cc(94, 20));
let (authored_cho, authored_del) = (100.0 / 127.0, 20.0 / 127.0);
assert_eq!(core.strips[0].chorus_send, authored_cho);
assert_eq!(core.strips[0].delay_send, authored_del);
core.handle_event(EvKind::Prog { ch: 0, prog: 48 });
assert_eq!(
core.strips[0].chorus_send, authored_cho,
"Program Change discarded the authored CC93 chorus send"
);
assert_eq!(
core.strips[0].delay_send, authored_del,
"Program Change discarded the authored CC94 delay send"
);
assert!(core.strips[0].chorus_authored);
assert!(core.strips[0].delay_authored);
let mut bare = EngineCore::new(core_opts());
bare.handle_event(EvKind::Prog { ch: 0, prog: 30 });
assert_eq!(bare.strips[0].chorus_send, 0.10);
assert_eq!(bare.strips[0].delay_send, 0.30);
bare.handle_event(EvKind::Prog { ch: 0, prog: 48 });
assert_eq!(bare.strips[0].chorus_send, 0.35);
assert_eq!(bare.strips[0].delay_send, 0.0);
let mut half = EngineCore::new(core_opts());
half.handle_event(EvKind::Cc {
ch: 0,
num: 93,
val: 64,
});
half.handle_event(EvKind::Prog { ch: 0, prog: 48 });
assert_eq!(half.strips[0].chorus_send, 64.0 / 127.0);
assert_eq!(half.strips[0].delay_send, 0.0);
}
fn slew_test_core(sr: f32) -> EngineCore {
EngineCore::new(CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
})
}
fn advance_blocks(core: &mut EngineCore, blocks: usize) {
let mut sink = [0f32; BLOCK * 2];
for _ in 0..blocks {
core.render_block_add(BLOCK, &mut sink);
}
}
#[test]
fn cc7_cc10_unauthored_channel_is_unchanged() {
let sr = 44100.0;
let default_vol = (100.0f32 / 127.0).powi(2);
let mut core = slew_test_core(sr);
advance_blocks(&mut core, 50);
let s = &core.strips[0];
assert_eq!(s.volume, default_vol);
assert_eq!(s.pan, 0.5);
assert_eq!(s.haas_delay, 0.0);
assert!(!s.volume_authored && !s.pan_authored);
assert!(!s.volume_primed && !s.pan_primed);
}
#[test]
fn cc7_cc10_prime_on_first_block_is_last_write_wins() {
let sr = 44100.0;
let default_vol = (100.0f32 / 127.0).powi(2);
let mut core = slew_test_core(sr);
core.handle_event(EvKind::Cc {
ch: 0,
num: 7,
val: 80,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 10,
val: 96,
});
core.handle_event(EvKind::Cc {
ch: 1,
num: 10,
val: 54,
});
core.handle_event(EvKind::Cc {
ch: 1,
num: 10,
val: 64,
});
assert!(core.strips[0].volume_authored && !core.strips[0].volume_primed);
assert_eq!(core.strips[0].volume, default_vol);
assert_eq!(core.strips[0].pan, 0.5);
advance_blocks(&mut core, 1);
assert!((core.strips[0].volume - (80.0f32 / 127.0).powi(2)).abs() < 1e-9);
assert!((core.strips[0].pan - 96.0 / 127.0).abs() < 1e-9);
assert!(core.strips[0].volume_primed && core.strips[0].pan_primed);
assert!(
(core.strips[1].pan - 64.0 / 127.0).abs() < 1e-9,
"same-tick burst must snap to the last authored value; got {}",
core.strips[1].pan
);
let want_haas = 0.005 * sr * (96.0f32 / 127.0 - 0.5).abs() * 2.0;
assert!((core.strips[0].haas_delay - want_haas).abs() < 1e-6);
}
#[test]
fn cc7_cc10_post_prime_change_slews_one_pole_monotone() {
let sr = 44100.0;
let smooth = 1.0 - (-(BLOCK as f32) / (0.03 * sr)).exp();
let mut core = slew_test_core(sr);
core.handle_event(EvKind::Cc {
ch: 0,
num: 7,
val: 127,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 10,
val: 0,
});
advance_blocks(&mut core, 1);
assert!(
core.strips[0].pan.abs() < 1e-9,
"pan primed to hard-left (0)"
);
assert!(
(core.strips[0].volume - 1.0).abs() < 1e-9,
"volume primed to full"
);
let v0 = core.strips[0].volume; let p0 = core.strips[0].pan; core.handle_event(EvKind::Cc {
ch: 0,
num: 7,
val: 0,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 10,
val: 127,
});
advance_blocks(&mut core, 1);
let want_v = v0 + smooth * (0.0 - v0);
let want_p = p0 + smooth * (1.0 - p0);
assert!(
(core.strips[0].volume - want_v).abs() < 1e-6,
"volume one-pole step"
);
assert!(
(core.strips[0].pan - want_p).abs() < 1e-6,
"pan one-pole step"
);
assert!(core.strips[0].volume < v0 && core.strips[0].volume > 0.0);
assert!(core.strips[0].pan > p0 && core.strips[0].pan < 1.0);
let mut prev_v = core.strips[0].volume;
let mut prev_p = core.strips[0].pan;
for _ in 0..500 {
advance_blocks(&mut core, 1);
let (v, p) = (core.strips[0].volume, core.strips[0].pan);
assert!(
v <= prev_v + 1e-7 && v >= -1e-6,
"volume monotone down, no undershoot"
);
assert!(
p >= prev_p - 1e-7 && p <= 1.0 + 1e-6,
"pan monotone up, no overshoot"
);
prev_v = v;
prev_p = p;
}
assert!(core.strips[0].volume.abs() < 1e-3, "volume converges to 0");
assert!(
(core.strips[0].pan - 1.0).abs() < 1e-3,
"pan converges to 1"
);
let want_haas = 0.005 * sr * (core.strips[0].pan - 0.5).abs() * 2.0;
assert!((core.strips[0].haas_delay - want_haas).abs() < 1e-6);
}
#[test]
fn cc10_haas_tap_advances_smoothly_not_in_jumps() {
let sr = 44100.0;
let full_range = 0.005 * sr; let mut core = slew_test_core(sr);
core.handle_event(EvKind::Cc {
ch: 0,
num: 10,
val: 0,
});
advance_blocks(&mut core, 1);
let mut prev = core.strips[0].haas_delay;
let mut max_block_step = 0.0f32;
let mut next_v = 1u8;
for block in 0..700usize {
if block % 4 == 0 && next_v <= 127 {
core.handle_event(EvKind::Cc {
ch: 0,
num: 10,
val: next_v,
});
next_v += 1;
}
advance_blocks(&mut core, 1);
let h = core.strips[0].haas_delay;
max_block_step = max_block_step.max((h - prev).abs());
prev = h;
}
assert!(
max_block_step < 0.02 * full_range,
"Haas tap should advance smoothly: max per-block step {:.3} samples (range {:.1})",
max_block_step,
full_range
);
}
#[test]
fn drum_room_early_reflections() {
let sr = 44100.0;
let song = drum_song(&[(0.05, 36, 115)], 1.0, &[]);
let opts = Options {
wet: 0.32,
..test_opts(sr)
};
let with = render_buses(&song, &opts, true, true, true).0;
let without = render_buses(&song, &opts, true, false, true).0;
let win = |s: &[f32]| left(s)[(0.055 * sr) as usize..(0.085 * sr) as usize].to_vec();
let diff: Vec<f32> = win(&with)
.iter()
.zip(win(&without))
.map(|(a, b)| a - b)
.collect();
assert!(rms(&diff) > 1e-4, "no early room energy: {}", rms(&diff));
let piano = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 0 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(0.8, EvKind::NoteOff { ch: 0, key: 60 }),
],
1.2,
);
let a = render_buses(&piano, &opts, true, true, true).0;
let b = render_buses(&piano, &opts, true, false, true).0;
assert!(
a.iter().zip(&b).all(|(x, y)| x.to_bits() == y.to_bits()),
"non-ch9 audio leaked into the drum room"
);
}
#[test]
fn authored_cc91_zero_dries_the_channel_10_room() {
let sr = 44100.0;
let song = drum_song(&[(0.05, 36, 115)], 1.0, &[(91, 0)]);
let opts = Options {
wet: 0.32,
..test_opts(sr)
};
let with_room = render_buses(&song, &opts, true, true, true).0;
let without_room = render_buses(&song, &opts, true, false, true).0;
assert!(
with_room
.iter()
.zip(&without_room)
.all(|(with, without)| with.to_bits() == without.to_bits()),
"authored channel-10 CC91=0 left drum-room energy"
);
}
#[test]
fn authored_cc91_scales_the_drum_room_linearly() {
let sr = 44100.0;
let mut core = EngineCore::new(CoreOptions {
sr,
wet: 0.32,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
});
assert_eq!(
core.strips[9].drum_room_send(),
ROOM_SEND,
"unauthored CC91 must retain the exact historical room constant"
);
for &val in &[0u8, 32, 64, 96, 127] {
core.handle_event(EvKind::Cc {
ch: 9,
num: 91,
val,
});
assert_eq!(
core.strips[9].drum_room_send(),
ROOM_SEND * (val as f32 / 127.0),
"CC91={val} did not scale the drum room linearly"
);
}
}
#[test]
fn authored_cc91_zero_dries_gs_and_xg_drum_rooms() {
let sr = 44100.0;
let opts = Options {
wet: 0.32,
..test_opts(sr)
};
for (name, route) in [
("GS", EvKind::DrumMode { ch: 11, on: true }),
(
"XG",
EvKind::Cc {
ch: 11,
num: 0,
val: 127,
},
),
] {
let song = test_song(
vec![
(0.0, route),
(
0.01,
EvKind::Cc {
ch: 11,
num: 91,
val: 0,
},
),
(
0.05,
EvKind::NoteOn {
ch: 11,
key: 36,
vel: 115,
},
),
],
1.0,
);
let with_room = render_buses(&song, &opts, true, true, true).0;
let without_room = render_buses(&song, &opts, true, false, true).0;
assert!(
with_room
.iter()
.zip(&without_room)
.all(|(with, without)| with.to_bits() == without.to_bits()),
"authored {name} rhythm-part CC91=0 left drum-room energy"
);
}
}
#[test]
fn guitar_sympathetic_rings_open_strings() {
let sr = 44100.0;
let strum = |prog: u8| {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog })];
for (i, &key) in [40u8, 45, 50, 55, 59, 64].iter().enumerate() {
let t = i as f64 * 0.012;
ev.push((
t,
EvKind::NoteOn {
ch: 0,
key,
vel: 105,
},
));
ev.push((1.2, EvKind::NoteOff { ch: 0, key }));
}
test_song(ev, 1.6)
};
let opts = test_opts(sr);
let with = render_buses(&strum(24), &opts, true, true, true).0;
let without = render_buses(&strum(24), &opts, false, true, true).0;
let d: Vec<f32> = left(&with)
.iter()
.zip(left(&without))
.map(|(a, b)| a - b)
.collect();
let tail = &d[(0.3 * sr) as usize..(0.8 * sr) as usize];
let ring: f32 = [82.41f32, 110.0, 146.83]
.iter()
.map(|&f| crate::testutil::band_rms(tail, sr, f, 8.0))
.sum();
assert!(ring > 1e-5, "sympathetic strings silent: {ring}");
let e_with = render_buses(&strum(30), &opts, true, true, true).0;
let e_without = render_buses(&strum(30), &opts, false, true, true).0;
assert!(
e_with
.iter()
.zip(&e_without)
.all(|(x, y)| x.to_bits() == y.to_bits()),
"electric guitar leaked into the sympathetic bus"
);
}
#[test]
fn sitar_tarab_strings_ring_sympathetically() {
let sr = 44100.0;
let song = |prog: u8| {
test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog }),
(
0.02,
EvKind::NoteOn {
ch: 0,
key: 62,
vel: 105,
},
),
(1.0, EvKind::NoteOff { ch: 0, key: 62 }),
],
1.8,
)
};
let opts = test_opts(sr);
let with = render_buses(&song(104), &opts, true, true, true).0;
let without = render_buses(&song(104), &opts, true, true, false).0;
let d: Vec<f32> = left(&with)
.iter()
.zip(left(&without))
.map(|(a, b)| a - b)
.collect();
let (w0, w1) = ((0.20 * sr) as usize, (1.20 * sr) as usize);
let ring = rms(&d[w0..w1]);
let dry = rms(&left(&without)[w0..w1]);
assert!(
ring > 1e-3 * dry,
"tarab strings silent: return {ring} vs dry {dry}"
);
let played = crate::testutil::band_rms(&d[w0..w1], sr, 293.66, 8.0);
let probe = crate::testutil::band_rms(&d[w0..w1], sr, 225.0, 8.0);
assert!(
played > 2.0 * probe,
"tarab return not comb-shaped: D4 {played} vs 225 Hz {probe}"
);
let b_with = render_buses(&song(105), &opts, true, true, true).0;
let b_without = render_buses(&song(105), &opts, true, true, false).0;
assert!(
b_with
.iter()
.zip(&b_without)
.all(|(x, y)| x.to_bits() == y.to_bits()),
"banjo leaked into the tarab bus"
);
}
#[test]
fn closed_hat_chokes_open_in_engine() {
let sr = 44100.0;
let open_only = drum_song(&[(0.05, 46, 110)], 1.0, &[]);
let choked = drum_song(&[(0.05, 46, 110), (0.25, 42, 90)], 1.0, &[]);
let a = render(&open_only, &test_opts(sr)).0;
let b = render(&choked, &test_opts(sr)).0;
let w = |s: &[f32]| rms(&left(s)[(0.32 * sr) as usize..(0.45 * sr) as usize]);
assert!(
w(&b) < 0.3 * w(&a),
"open hat survived the choke: {} vs {}",
w(&b),
w(&a)
);
}
#[cfg(feature = "embedded-samples")]
fn ncc_max(a: &[f32], b: &[f32], max_lag: usize) -> f32 {
fn ncc_at(a: &[f32], b: &[f32]) -> f32 {
let (mut sab, mut saa, mut sbb) = (0f64, 0f64, 0f64);
for (&x, &y) in a.iter().zip(b) {
sab += (x * y) as f64;
saa += (x * x) as f64;
sbb += (y * y) as f64;
}
(sab / (saa.sqrt() * sbb.sqrt()).max(1e-30)) as f32
}
let n = a.len().min(b.len()) - max_lag;
let mut best = 0f32;
for lag in 0..=max_lag {
best = best.max(ncc_at(&a[..n], &b[lag..lag + n]).abs());
best = best.max(ncc_at(&a[lag..lag + n], &b[..n]).abs());
}
best
}
#[cfg(feature = "embedded-samples")]
fn hit_onset(w: &[f32]) -> usize {
let peak = w.iter().fold(0f32, |m, &x| m.max(x.abs()));
w.iter().position(|&x| x.abs() > 0.05 * peak).unwrap_or(0)
}
#[cfg(feature = "embedded-samples")]
fn warp_ncc(a: &[f32], b: &[f32]) -> f32 {
let (a, b) = (&a[hit_onset(a)..], &b[hit_onset(b)..]);
let w0 = 1323usize; let n = 1764usize; let lag = 48usize; let steps = 220;
let da: Vec<f32> = a[w0..w0 + n + lag + 1]
.windows(2)
.map(|p| p[1] - p[0])
.collect();
let mut warped = vec![0f32; w0 + n + lag + 1];
let mut best = 0f32;
for s in 0..=steps {
let r = 0.945 + 0.11 * s as f32 / steps as f32;
for (i, w) in warped.iter_mut().enumerate() {
let x = i as f32 * r;
let j = x as usize;
let frac = x - j as f32;
*w = b[j] * (1.0 - frac) + b[j + 1] * frac;
}
let dw: Vec<f32> = warped[w0..].windows(2).map(|p| p[1] - p[0]).collect();
best = best.max(ncc_max(&da, &dw, lag));
}
best
}
#[cfg(feature = "embedded-samples")]
#[test]
fn sampled_ride_hits_are_decorrelated() {
let sr = 44100.0;
let n_hits = 8usize;
let spacing = 1.3f64; let hits: Vec<(f64, u8, u8)> = (0..n_hits)
.map(|i| (0.05 + spacing * i as f64, 51u8, 100u8))
.collect();
let song = drum_song(
&hits,
0.05 + spacing * n_hits as f64,
&[(91, 0), (93, 0), (94, 0)],
);
let opt = Options {
samples: true,
..test_opts(sr)
};
let s = render(&song, &opt).0;
let mono: Vec<f32> = s.chunks_exact(2).map(|p| p[0] + p[1]).collect();
let win = |i: usize| {
let at = ((0.05 + spacing * i as f64) * sr as f64) as usize;
&mono[at..at + (0.20 * sr) as usize]
};
let mut worst_any = 0f32;
for i in 0..n_hits {
for j in (i + 1)..n_hits {
assert_ne!(win(i), win(j), "hits {i} and {j} are bit-identical");
let r = ncc_max(win(i), win(j), 128);
if j == i + 1 || j == i + 4 {
println!("ride hits {i}-{j}: plain ncc {r:.3}");
}
worst_any = worst_any.max(r);
}
}
for i in 0..n_hits - 4 {
let r = warp_ncc(win(i), win(i + 4));
println!("ride hits {i}-{} (same take): warp ncc {r:.3}", i + 4);
}
let mut worst_consecutive = 0f32;
for i in 0..n_hits - 1 {
let r = warp_ncc(win(i), win(i + 1));
println!("ride hits {i}-{}: warp ncc {r:.3}", i + 1);
worst_consecutive = worst_consecutive.max(r);
}
assert!(
worst_consecutive < 0.60,
"consecutive ride hits warp-correlate {worst_consecutive:.3} — round-robin repeat"
);
assert!(
worst_any < 0.98,
"some ride hit pair is a clone: ncc {worst_any:.3}"
);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn sampled_closed_or_pedal_hat_chokes_open_in_engine() {
let sr = 44100.0;
let opt = Options {
samples: true,
..test_opts(sr)
};
let open_only = drum_song(&[(0.05, 46, 110)], 1.0, &[]);
let a = render(&open_only, &opt).0;
let w = |s: &[f32]| rms(&left(s)[(0.28 * sr) as usize..(0.45 * sr) as usize]);
for choker in [42u8, 44] {
let choked = drum_song(&[(0.05, 46, 110), (0.25, choker, 90)], 1.0, &[]);
let b = render(&choked, &opt).0;
let solo = drum_song(&[(0.25, choker, 90)], 1.0, &[]);
let c = render(&solo, &opt).0;
let residual = (w(&b).powi(2) - w(&c).powi(2)).max(0.0).sqrt();
println!(
"choker {choker}: open-only {:.5}, choked {:.5}, choker-solo {:.5}, residual {:.5}",
w(&a),
w(&b),
w(&c),
residual
);
assert!(
residual < 0.35 * w(&a),
"sampled open hat survived the key-{choker} choke: residual {residual} vs open ring {}",
w(&a)
);
}
}
#[cfg(feature = "embedded-samples")]
#[test]
fn sampled_fast_closed_hats_do_not_machine_gun() {
let sr = 44100.0;
let clone_hat = || {
let mut w = vec![0f32; (0.10 * sr) as usize];
let mut v = drums::make(42, 100, sr, 0xBEEF, drums::Kit::V3, true, 3).unwrap();
v.render(&mut w);
w
};
let (c1, c2) = (clone_hat(), clone_hat());
assert_eq!(c1, c2, "identical seed+take must render bit-identically");
let clone_ncc = ncc_max(&c1, &c2, 128);
assert!(
clone_ncc > 0.999,
"clone detector miscalibrated: {clone_ncc:.4}"
);
let n = 16usize;
let dt = 0.125f64; let hits: Vec<(f64, u8, u8)> = (0..n).map(|i| (0.05 + dt * i as f64, 42, 100)).collect();
let song = drum_song(
&hits,
0.05 + dt * n as f64 + 0.3,
&[(91, 0), (93, 0), (94, 0)],
);
let opt = Options {
samples: true,
..test_opts(sr)
};
let s = render(&song, &opt).0;
let mono: Vec<f32> = s.chunks_exact(2).map(|p| p[0] + p[1]).collect();
let win = |i: usize| -> &[f32] {
let at = ((0.05 + dt * i as f64) * sr as f64) as usize;
&mono[at..at + (0.10 * sr) as usize]
};
for i in 0..n {
assert!(
rms(win(i)) > 1e-4,
"hat hit {i} is silent — sampled hat not routed?"
);
}
let mut worst_any = 0f32;
for i in 0..n {
for j in (i + 1)..n {
assert_ne!(win(i), win(j), "hat hits {i} and {j} are bit-identical");
worst_any = worst_any.max(ncc_max(win(i), win(j), 128));
}
}
let mut worst_diff = f32::INFINITY;
for i in 0..n - 4 {
let (a, b) = (win(i), win(i + 4));
let diff: Vec<f32> = a.iter().zip(b).map(|(x, y)| x - y).collect();
worst_diff = worst_diff.min(rms(&diff) / rms(a).max(1e-9));
}
println!(
"fast hats: max pairwise ncc {worst_any:.3} (clone reads {clone_ncc:.3}), \
min same-take diff/rms {worst_diff:.3}"
);
assert!(
worst_any < 0.35,
"some hat pair correlates {worst_any:.3} (limit 0.35) — MACHINE-GUN: \
the hat jitter profile is not decorrelating repeats"
);
assert!(
worst_diff > 0.4,
"same-take hats nearly identical (diff/rms {worst_diff:.3}) — \
MACHINE-GUN: the micro-variation is not decorrelating repeats"
);
}
#[test]
fn cabinet_response_shape() {
let sr2 = 88_200.0;
let mut cab = cab_biquads(sr2);
let mut ir = vec![0f32; 16384];
ir[0] = 1.0;
for x in ir.iter_mut() {
let mut y = *x;
for c in cab.iter_mut() {
y = c.process(y);
}
*x = y;
}
let db = |f: f32| 20.0 * crate::testutil::mag_at(&ir, sr2, f).max(1e-12).log10();
assert!(
db(2600.0) - db(1000.0) >= 3.0,
"presence: 2600 {:.1} dB vs 1000 {:.1} dB",
db(2600.0),
db(1000.0)
);
assert!(
db(6000.0) - db(3000.0) <= -18.0,
"cliff: 6000 {:.1} dB vs 3000 {:.1} dB",
db(6000.0),
db(3000.0)
);
assert!(
db(100.0) - db(300.0) >= 2.0,
"low resonance: 100 {:.1} dB vs 300 {:.1} dB",
db(100.0),
db(300.0)
);
}
#[test]
fn micro_cab_ripple_fine_structure() {
let sr2 = 88_200.0;
let mut mc = MicroCab::with_depth(sr2, 1.0);
let mut ir = vec![0f32; 4096];
ir[0] = 1.0;
for x in ir.iter_mut() {
*x = mc.process(*x);
}
let db = |f: f32| 20.0 * crate::testutil::mag_at(&ir, sr2, f).max(1e-12).log10();
let grid: Vec<f32> = (0..55).map(|k| 900.0 + k as f32 * 50.0).collect();
let curve: Vec<f32> = grid.iter().map(|&f| db(f)).collect();
const DEPTH: f32 = 1.5;
let (mut alternations, mut dir) = (0usize, 0i8);
let (mut mn, mut mx) = (curve[0], curve[0]);
for &v in &curve[1..] {
if dir >= 0 {
mx = mx.max(v);
}
if dir <= 0 {
mn = mn.min(v);
}
if dir >= 0 && mx - v >= DEPTH {
alternations += 1;
dir = -1;
mn = v;
} else if dir <= 0 && v - mn >= DEPTH {
alternations += 1;
dir = 1;
mx = v;
}
}
assert!(
alternations >= 6,
"fine structure too smooth: {alternations} alternations >= 1.5 dB in 900-3600 Hz ({curve:?})"
);
let mut ident = vec![0f32; 4096];
ident[0] = 1.0;
let dbi = |f: f32| 20.0 * crate::testutil::mag_at(&ident, sr2, f).max(1e-12).log10();
let avg: f32 = grid.iter().map(|&f| db(f) - dbi(f)).sum::<f32>() / grid.len() as f32;
assert!(
avg.abs() < 1.5,
"band-average moved: {avg:.2} dB (normalization contract)"
);
let (lo, hi) = curve
.iter()
.fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
assert!(
hi - lo <= 10.0,
"standalone ripple swing {:.1} dB — too deep to stay musical",
hi - lo
);
}
#[test]
fn sustaining_lead_cab_ripple_is_bounded_at_every_held_pitch() {
let sr2 = 88_200.0;
let curve = |depth: f32| -> Vec<f32> {
let mut mc = MicroCab::with_depth(sr2, depth);
let mut ir = vec![0f32; 4096];
ir[0] = 1.0;
for x in ir.iter_mut() {
*x = mc.process(*x);
}
let mut ident = vec![0f32; 4096];
ident[0] = 1.0;
(0..=270)
.map(|k| {
let f = 900.0 + k as f32 * 10.0;
20.0 * (crate::testutil::mag_at(&ir, sr2, f).max(1e-12)
/ crate::testutil::mag_at(&ident, sr2, f).max(1e-12))
.log10()
})
.collect()
};
let worst = |c: &[f32]| c.iter().fold(0f32, |m, v| m.max(v.abs()));
let swing = |c: &[f32]| {
let (lo, hi) = c
.iter()
.fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
hi - lo
};
let (full, lead) = (curve(1.0), curve(MICRO_CAB_LEAD_DEPTH));
assert!(
worst(&full) > 2.5,
"full-depth ripple only {:.2} dB deep — the premise of this fix is gone",
worst(&full)
);
assert!(
worst(&lead) < 1.0,
"sustaining lead can lose {:.2} dB on a held pitch (want < 1.0)",
worst(&lead)
);
assert!(
swing(&lead) > 0.8,
"lead ripple flattened to {:.2} dB swing — that is a DI, not a cab",
swing(&lead)
);
let at = |c: &[f32], f: f32| c[((f - 900.0) / 10.0).round() as usize];
for (name, f0, want) in [("C6", 1050.0, 1.0), ("E6", 1320.0, 0.5)] {
let gain = at(&lead, f0) - at(&full, f0);
assert!(
gain >= want,
"{name} ({f0} Hz) recovers only {gain:.2} dB (want >= {want})"
);
}
}
#[test]
fn cc0_bank_select_reaches_the_driven_cabinet_in_either_order() {
let sr = 44100.0;
let opts = test_opts(sr);
let song = |bank_first: bool, bank: u8| {
let mut ev = Vec::new();
if bank_first {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: bank,
},
));
ev.push((0.0, EvKind::Prog { ch: 0, prog: 29 }));
} else {
ev.push((0.0, EvKind::Prog { ch: 0, prog: 29 }));
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: bank,
},
));
}
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 88,
vel: 104,
},
));
test_song(ev, 2.0)
};
let alt_first = render(&song(true, 1), &opts).0;
let alt_after = render(&song(false, 1), &opts).0;
let default_bank = render(&song(true, 0), &opts).0;
assert!(rms(&alt_first) > 1e-4, "the driven lead should sound");
assert_eq!(
alt_first, alt_after,
"CC0 after the program change did not reach the driven cabinet — \
the insert was left on the default bank's ripple depth"
);
assert_ne!(
alt_first, default_bank,
"the alt bank renders identically to the default — the shallower \
lead cabinet ripple is not wired up"
);
}
#[test]
fn micro_cab_combined_keeps_macro_shape() {
let sr2 = 88_200.0;
let run = |with_micro: bool| {
let mut cab = cab_biquads(sr2);
let mut mc = MicroCab::with_depth(sr2, 1.0);
let mut ir = vec![0f32; 16384];
ir[0] = 1.0;
for x in ir.iter_mut() {
let mut y = *x;
for c in &mut cab[..CAB_CLIFF] {
y = c.process(y);
}
if with_micro {
y = mc.process(y);
}
for c in &mut cab[CAB_CLIFF..] {
y = c.process(y);
}
*x = y;
}
ir
};
let ir = run(true);
let db = |ir: &[f32], f: f32| 20.0 * crate::testutil::mag_at(ir, sr2, f).max(1e-12).log10();
assert!(
db(&ir, 2600.0) - db(&ir, 1000.0) >= 3.0,
"presence lost: 2600 {:.1} vs 1000 {:.1}",
db(&ir, 2600.0),
db(&ir, 1000.0)
);
assert!(
db(&ir, 6000.0) - db(&ir, 3000.0) <= -18.0,
"cliff lost: 6000 {:.1} vs 3000 {:.1}",
db(&ir, 6000.0),
db(&ir, 3000.0)
);
assert!(
db(&ir, 100.0) - db(&ir, 300.0) >= 2.0,
"low resonance lost: 100 {:.1} vs 300 {:.1}",
db(&ir, 100.0),
db(&ir, 300.0)
);
let vals: Vec<f32> = (0..28).map(|k| db(&ir, 900.0 + k as f32 * 100.0)).collect();
let (mn, mx) = vals
.iter()
.fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
assert!(
mx - mn <= 14.0,
"ripple guts the band: {:.1} dB swing in 900-3600",
mx - mn
);
let base = run(false);
let band_avg = |ir: &[f32]| {
(0..28)
.map(|k| db(ir, 300.0 + k as f32 * 100.0))
.sum::<f32>()
/ 28.0
};
let delta = band_avg(&ir) - band_avg(&base);
assert!(
delta.abs() < 1.5,
"combined cab level moved {delta:.2} dB vs biquads-only"
);
}
#[test]
fn drive_asymmetry_and_dc() {
let sr = 44100.0;
let f0 = 110.0;
let measure = |prog: u8, alt: bool, amp: f32| {
let mut drive = Drive::new(prog, alt, sr);
let mut buf: Vec<f32> = (0..(sr as usize))
.map(|i| amp * (std::f32::consts::TAU * f0 * i as f32 / sr).sin())
.collect();
drive.process(&mut buf);
let seg = &buf[(0.05 * sr) as usize..];
let dc = seg.iter().map(|&x| x as f64).sum::<f64>() / seg.len() as f64;
let m1 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
let m2 = crate::testutil::mag_at(seg, sr, 2.0 * f0);
let m3 = crate::testutil::mag_at(seg, sr, 3.0 * f0);
(dc, m1, m2, m3)
};
let (dc, _, m2, m3) = measure(29, false, 0.5);
assert!(dc.abs() < 1e-3, "sustained DC {dc}");
assert!(m2 > 0.1 * m3, "2nd harmonic {m2} vs 3rd {m3}");
for prog in [29u8, 30] {
for alt in [false, true] {
for amp in [0.5f32, 0.05] {
let (dc, m1, m2, m3) = measure(prog, alt, amp);
let bank = if alt { "alt " } else { "main" };
assert!(
dc.abs() < 1e-3,
"prog {prog} {bank} amp {amp}: sustained DC {dc}"
);
let h2 = 20.0 * (m2 / m1).log10();
assert!(
h2 >= -40.0,
"prog {prog} {bank} amp {amp}: the biased shaper produced no real 2nd \
harmonic — H2 {h2:.1} dBc"
);
if amp < 0.1 {
assert!(
m2 > 0.1 * m3,
"prog {prog} {bank} amp {amp}: the biased shaper lost its \
even-harmonic warmth at the tail operating point — \
2nd harmonic {m2} vs 3rd {m3}"
);
}
}
}
}
}
#[test]
fn drive_pre_voicing_direction() {
let sr = 44100.0;
let input: Vec<f32> = {
let mut rng = crate::dsp::Rng::new(11);
(0..(sr as usize)).map(|_| rng.white() * 0.3).collect()
};
let ratio = |prog: u8, flat: bool| {
let mut d = Drive::new(prog, false, sr);
if flat {
d = d.with_flat_voice();
}
let mut buf = input.clone();
d.process(&mut buf);
crate::testutil::band_rms(&buf, sr, 600.0, 1.0)
/ crate::testutil::band_rms(&buf, sr, 2000.0, 1.0).max(1e-9)
};
assert!(
ratio(30, false) < ratio(30, true),
"prog 30 should scoop the mids"
);
assert!(
ratio(29, false) > ratio(29, true),
"prog 29 should push the mids"
);
}
#[cfg(test)]
fn insert_band_db(prog: u8, alt: bool, sr: f32, freqs: &[f32]) -> Vec<f32> {
let mut b: Vec<f32> = {
let mut rng = crate::dsp::Rng::new(7);
(0..(4.0 * sr) as usize)
.map(|_| rng.white() * 0.02)
.collect()
};
Drive::new(prog, alt, sr).process(&mut b);
freqs
.iter()
.map(|&f| {
20.0 * crate::testutil::band_rms(&b[(0.2 * sr) as usize..], sr, f, 4.0)
.max(1e-12)
.log10()
})
.collect()
}
#[cfg(test)]
fn lead_minus_main_db(prog: u8, sr: f32, freqs: &[f32]) -> Vec<f32> {
let m = insert_band_db(prog, false, sr, freqs);
let a = insert_band_db(prog, true, sr, freqs);
a.iter().zip(&m).map(|(x, y)| x - y).collect()
}
#[test]
fn alt_drive_is_a_distinct_lead_voicing() {
let sr = 44100.0;
for (prog, tilt_floor, low_floor) in [(29u8, 1.5f32, -2.5f32), (30, 2.5, -4.0)] {
let d = lead_minus_main_db(prog, sr, &[100.0, 800.0, 2800.0, 6000.0]);
let (low, body, pres, fizz) = (d[0], d[1], d[2], d[3]);
let tilt = body - pres;
println!(
"lead voicing prog {prog}: tilt(800-2800) {tilt:+.2} dB, \
low(100-800) {:+.2}, fizz(6000-800) {:+.2}",
low - body,
fizz - body
);
assert!(
tilt >= tilt_floor,
"prog {prog}: the lead amp is not more body-forward than the rhythm amp \
— tilt(800-2800) {tilt:+.2} dB < {tilt_floor:+.1}"
);
assert!(
tilt <= 12.0,
"prog {prog}: the lead amp has ratcheted into a mid honk — \
tilt(800-2800) {tilt:+.2} dB > +12"
);
let low_rel = low - body;
assert!(
low_rel <= low_floor,
"prog {prog}: the lead amp did not tighten the low end — \
low(100-800) {low_rel:+.2} dB > {low_floor:+.1}"
);
assert!(
low_rel >= -16.0,
"prog {prog}: the lead amp gutted the low end — \
low(100-800) {low_rel:+.2} dB < -16"
);
let fizz_rel = fizz - body;
assert!(
fizz_rel <= -1.0,
"prog {prog}: the lead amp is fizzier than the rhythm amp — \
fizz(6000-800) {fizz_rel:+.2} dB > -1"
);
}
}
#[test]
fn alt_bank_leads_stay_distinct_from_each_other() {
let sr = 44100.0;
const F: [f32; 6] = [120.0, 300.0, 600.0, 1200.0, 2400.0, 3400.0];
let shape = |prog: u8, alt: bool| {
let v = insert_band_db(prog, alt, sr, &F);
let mean = v.iter().sum::<f32>() / v.len() as f32;
v.into_iter().map(|x| x - mean).collect::<Vec<f32>>()
};
let dist = |a: &[f32], b: &[f32]| {
(a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f32>() / a.len() as f32).sqrt()
};
let (l29, l30) = (shape(29, true), shape(30, true));
let (m29, m30) = (shape(29, false), shape(30, false));
let lead_d = dist(&l29, &l30);
let main_d = dist(&m29, &m30);
println!(
"alt leads: 29-vs-30 spectral distance {lead_d:.2} dB (their mains: {main_d:.2} dB)"
);
assert!(
lead_d >= 1.5,
"the two alt leads have collapsed into one amp: spectral distance only \
{lead_d:.2} dB (their mains differ by {main_d:.2} dB)"
);
let mid = |prog: u8, alt: bool| {
let v = insert_band_db(prog, alt, sr, &[600.0, 2000.0]);
v[0] - v[1]
};
let (lead_gap, main_gap) = (
mid(29, true) - mid(30, true),
mid(29, false) - mid(30, false),
);
println!(
"alt leads: 29-vs-30 600:2000 mid balance {lead_gap:+.2} dB \
(their mains {main_gap:+.2} dB — same sign required)"
);
assert!(
lead_gap >= 1.0,
"the two alt leads no longer sit on the push-vs-scoop axis their mains use: \
29's lead should be the mid-pushed one and 30's the mid-scooped one, \
measured {lead_gap:+.2} dB (their mains differ by {main_gap:+.2} dB)"
);
let span = |prog: u8| {
let at = |amp: f32| {
let mut d = Drive::new(prog, true, sr);
let mut buf: Vec<f32> = (0..(sr as usize))
.map(|i| amp * (std::f32::consts::TAU * 220.0 * i as f32 / sr).sin())
.collect();
d.process(&mut buf);
20.0 * crate::testutil::spectral_band_rms(
&buf[(0.2 * sr) as usize..],
sr,
300.0,
3400.0,
)
.max(1e-12)
.log10()
};
at(0.5) - at(0.05)
};
let (s29, s30) = (span(29), span(30));
println!("alt leads: loud-quiet span — 29 {s29:.2} dB, 30 {s30:.2} dB (smaller = more compressed)");
assert!(
s30 <= s29 - 0.5,
"the two alt leads share gain staging: 30-lead should compress harder than \
29-lead, spans {s30:.2} vs {s29:.2} dB"
);
}
#[test]
fn sustained_lead_stays_harmonic_through_drive() {
let sr = 44100.0;
let events = vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 29 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 94,
val: 0,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 76,
vel: 100,
},
),
];
let out = left(&render(&test_song(events, 3.2), &test_opts(sr)).0);
let seg = &out[(2.5 * sr) as usize..(3.0 * sr) as usize];
let f0 = 659.26;
let m1 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
let m2 = crate::testutil::mag_at(seg, sr, 2.0 * f0);
let rel = 20.0 * (m2 / m1).log10();
println!("V6b: 2f0 at {rel:.1} dB rel f0 in the 2.5–3.0 s window");
assert!(
rel >= -30.0,
"held lead lost its harmonics: 2f0 {rel:.1} dB rel f0"
);
}
#[test]
fn driven_sustain_stays_distorted() {
let sr = 44100.0;
for (prog, key, floor) in [
(29u8, 40u8, -29.0f32), (29, 45, -28.0), (30, 40, -33.0),
(30, 45, -29.0),
] {
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
let events = vec![
(0.0, cc(0, 1)),
(0.0, EvKind::Prog { ch: 0, prog }),
(0.0, cc(93, 0)),
(0.0, cc(94, 0)),
(
0.05,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
),
];
let out = left(&render(&test_song(events, 6.2), &test_opts(sr)).0);
let f0 = 440.0 * 2f32.powf((key as f32 - 69.0) / 12.0);
let seg = &out[(5.5 * sr) as usize..(6.0 * sr) as usize];
let m1 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
let mut pow = 0.0f32;
for n in 2..=16u32 {
let m = crate::testutil::mag_at(seg, sr, n as f32 * f0);
pow += m * m;
}
let thd = 20.0 * (pow.sqrt() / m1).log10();
println!("driven sustain prog {prog} key {key}: THD(2..16) {thd:.1} dB rel f0");
assert!(
thd >= floor,
"prog {prog} key {key}: held sustain went clean — THD {thd:.1} dB \
rel f0 (floor {floor})"
);
assert!(
thd <= -12.0,
"prog {prog} key {key}: held sustain turned to fuzz — THD {thd:.1} dB \
rel f0 exceeds the −12 dB harshness ceiling"
);
}
}
#[test]
fn driven_banks_diverge_at_lead_pitches() {
let sr = 44100.0;
const BANDS: [(f32, f32); 6] = [
(200.0, 400.0),
(400.0, 800.0),
(800.0, 1600.0),
(1600.0, 2400.0),
(2400.0, 3200.0),
(3200.0, 4200.0),
];
let render_bank = |prog: u8, key: u8, bank: u8| {
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
let events = vec![
(0.0, cc(0, bank)),
(0.0, EvKind::Prog { ch: 0, prog }),
(0.0, cc(93, 0)),
(0.0, cc(94, 0)),
(
0.05,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
),
];
left(&render(&test_song(events, 1.2), &test_opts(sr)).0)
};
let shape = |b: &[f32]| {
let s = &b[(0.15 * sr) as usize..(0.55 * sr) as usize];
let mut v: Vec<f32> = BANDS
.iter()
.map(|&(lo, hi)| {
20.0 * crate::testutil::spectral_band_rms(s, sr, lo, hi)
.max(1e-12)
.log10()
})
.collect();
let mean = v.iter().sum::<f32>() / v.len() as f32;
for x in v.iter_mut() {
*x -= mean;
}
v
};
let distance = |a: &[f32], b: &[f32]| {
(a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f32>() / a.len() as f32).sqrt()
};
let mut bad: Vec<String> = Vec::new();
for prog in [29u8, 30] {
let mut ds = Vec::new();
for key in [57u8, 59, 61, 63, 65, 67, 69] {
let main = shape(&render_bank(prog, key, 0));
let alt = shape(&render_bank(prog, key, 1));
let d = distance(&main, &alt);
println!(
"lead-pitch divergence prog {prog} key {key}: spectral distance {d:.2} dB"
);
if d < 0.8 {
bad.push(format!("prog {prog} key {key} collapsed: {d:.2} dB"));
}
ds.push(d);
}
let mean = ds.iter().sum::<f32>() / ds.len() as f32;
println!("lead-pitch divergence prog {prog}: MEAN {mean:.2} dB over the register");
if mean < 2.0 {
bad.push(format!("prog {prog} register mean {mean:.2} dB"));
}
}
let ctl = shape(&render_bank(29, 62, 0));
assert!(
distance(&ctl, &ctl) < 1e-6,
"the spectral-distance metric is not self-consistent"
);
assert!(
bad.is_empty(),
"the alt bank still sounds like the default bank through the full engine \
(per-key distance < 0.8 dB, or register mean < 2.0 dB) at: {bad:?}"
);
}
#[test]
fn drive_alias_floor() {
let sr = 44100.0;
let fold = |f: f32, fs: f32| {
let r = f % fs;
if r > fs / 2.0 {
fs - r
} else {
r
}
};
for prog in [29u8, 30] {
for alt in [false, true] {
for f0 in [997.0f32, 1301.0, 1763.0] {
let mut d = Drive::new(prog, alt, sr);
let mut buf: Vec<f32> = (0..(sr as usize))
.map(|i| 0.5 * (std::f32::consts::TAU * f0 * i as f32 / sr).sin())
.collect();
d.process(&mut buf);
let seg = &buf[(0.2 * sr) as usize..];
let m0 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
let mut worst = -200.0f32;
let mut worst_bin = 0.0f32;
for n in 2..=80u32 {
let fh = n as f32 * f0;
if fh <= sr / 2.0 {
continue; }
let alias = fold(fold(fh, sr * DRIVE_OVERSAMPLE as f32), sr);
if !(100.0..=20_000.0).contains(&alias) {
continue;
}
if (alias / f0 - (alias / f0).round()).abs() * f0 < 8.0 {
continue;
}
let rel = 20.0 * (crate::testutil::mag_at(seg, sr, alias) / m0).log10();
if rel > worst {
worst = rel;
worst_bin = alias;
}
}
println!(
"V9 prog {prog} {} f0 {f0:.0}: worst alias {worst:.1} dB rel f0 at {worst_bin:.0} Hz",
if alt { "alt " } else { "main" }
);
assert!(
worst <= -40.0,
"prog {prog} {} f0 {f0:.0}: alias at {worst_bin:.0} Hz is {worst:.1} dB rel f0",
if alt { "alt" } else { "main" }
);
}
}
}
}
fn amp_settled(
prog: u8,
alt: bool,
sr: f32,
params: [u8; AMP_PARAM_COUNT],
input: &[f32],
) -> Vec<f32> {
let mut d = Drive::new(prog, alt, sr);
let cur = params.map(|v| v as f32);
d.apply_params(&cur, true);
let mut out = input.to_vec();
d.process(&mut out);
out
}
fn amp_blocks(
prog: u8,
alt: bool,
sr: f32,
input: &[f32],
params: impl Fn(usize) -> [u8; AMP_PARAM_COUNT],
) -> Vec<f32> {
let mut d = Drive::new(prog, alt, sr);
let mut cur = [AMP_NEUTRAL as f32; AMP_PARAM_COUNT];
let mut primed = [false; AMP_PARAM_COUNT];
let smooth = 1.0 - (-(BLOCK as f32) / (WAH_SLEW_S * sr)).exp();
let mut out = input.to_vec();
for (bi, chunk) in out.chunks_mut(BLOCK).enumerate() {
let p = params(bi);
for i in 0..AMP_PARAM_COUNT {
let t = p[i] as f32;
if primed[i] {
cur[i] += smooth * (t - cur[i]);
} else {
cur[i] = t;
primed[i] = true;
}
}
d.apply_params(&cur, false);
d.process(chunk);
}
out
}
fn noise(sr: f32, secs: f32, amp: f32, seed: u32) -> Vec<f32> {
let mut rng = crate::dsp::Rng::new(seed);
(0..(secs * sr) as usize)
.map(|_| rng.white() * amp)
.collect()
}
fn sine(sr: f32, secs: f32, f0: f32, amp: f32) -> Vec<f32> {
(0..(secs * sr) as usize)
.map(|i| amp * (std::f32::consts::TAU * f0 * i as f32 / sr).sin())
.collect()
}
#[test]
fn amp_neutral_is_bit_identical() {
let sr = 44100.0;
let input = noise(sr, 1.0, 0.2, 0x0028_0001);
for prog in [29u8, 30] {
for alt in [false, true] {
let mut d0 = Drive::new(prog, alt, sr);
let mut base = input.clone();
d0.process(&mut base);
let authored =
amp_blocks(prog, alt, sr, &input, |_| [AMP_NEUTRAL; AMP_PARAM_COUNT]);
assert_eq!(
base.len(),
authored.len(),
"prog {prog} alt {alt}: length mismatch"
);
for (i, (a, b)) in base.iter().zip(&authored).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"prog {prog} alt {alt} sample {i}: neutral render is not bit-identical ({a} vs {b})"
);
}
}
}
}
#[test]
fn amp_each_knob_moves_its_axis() {
let sr = 44100.0;
struct Case {
knob: usize,
f0: f32,
amp: f32,
lo: f32,
hi: f32,
more: u8, less: u8,
min_db: f32,
}
let c = |knob, f0, amp, lo, hi, more, less, min_db| Case {
knob,
f0,
amp,
lo,
hi,
more,
less,
min_db,
};
let cases = [
c(AMP_TONE, 110.0, 0.02, 400.0, 1600.0, 127, 0, 2.4),
c(AMP_TIGHT, 82.4, 0.30, 40.0, 110.0, 0, 127, 3.0), c(AMP_BODY, 150.0, 0.10, 350.0, 750.0, 127, 0, 22.0),
c(AMP_PRES, 150.0, 0.10, 2000.0, 3400.0, 127, 0, 22.0),
c(AMP_CABTONE, 150.0, 0.10, 2500.0, 3600.0, 127, 0, 13.0), ];
for prog in [29u8, 30] {
for alt in [false, true] {
for &Case {
knob,
f0,
amp,
lo,
hi,
more,
less,
min_db,
} in &cases
{
let input = sine(sr, 0.6, f0, amp);
let band = |v: u8| {
let mut p = [64u8; AMP_PARAM_COUNT];
p[knob] = v;
let out = amp_settled(prog, alt, sr, p, &input);
let seg = &out[(0.2 * sr) as usize..];
20.0 * crate::testutil::spectral_band_rms(seg, sr, lo, hi)
.max(1e-12)
.log10()
};
let hi_db = band(more);
let lo_db = band(less);
let spread = hi_db - lo_db;
println!(
"knob {knob} prog {prog} {}: spread {spread:.2} dB (more {hi_db:.2} less {lo_db:.2})",
if alt { "alt" } else { "main" }
);
let neutral = amp_settled(prog, alt, sr, [64; AMP_PARAM_COUNT], &input);
let mut plain = Drive::new(prog, alt, sr);
let mut plain_out = input.clone();
plain.process(&mut plain_out);
assert_eq!(
neutral.iter().map(|x| x.to_bits()).collect::<Vec<_>>(),
plain_out.iter().map(|x| x.to_bits()).collect::<Vec<_>>(),
"prog {prog} alt {alt} knob {knob}: value 64 is not identity"
);
assert!(
spread >= min_db,
"prog {prog} alt {alt} knob {knob}: spread {spread:.2} dB < {min_db} \
(more={more} {hi_db:.2} less={less} {lo_db:.2})"
);
}
}
}
}
#[test]
#[ignore]
fn amp_drive_ceiling_sweep() {
let sr = 44100.0;
let fold = |f: f32, fs: f32| {
let r = f % fs;
if r > fs / 2.0 {
fs - r
} else {
r
}
};
let n_max = (127.0 - 64.0) / 64.0; println!("\n k worst alias (dBc) headroom vs -40 verdict");
for k in [1.2f32, 1.5, 1.8, 2.0, 2.2, 2.5, 3.0] {
let mut worst_all = -200.0f32;
for prog in [29u8, 30] {
for alt in [false, true] {
for f0 in [997.0f32, 1301.0, 1763.0] {
let mut d = Drive::new(prog, alt, sr);
d.g1 = d.base.g1 * 2f32.powf(k * n_max);
d.g2 = d.base.g2 * 2f32.powf(0.5 * k * n_max);
d.post = d.base.post * 2f32.powf(-AMP_DRIVE_POST_C * n_max);
let mut buf: Vec<f32> = (0..(sr as usize))
.map(|i| 0.5 * (std::f32::consts::TAU * f0 * i as f32 / sr).sin())
.collect();
d.process(&mut buf);
let seg = &buf[(0.2 * sr) as usize..];
let m0 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
for h in 2..=80u32 {
let fh = h as f32 * f0;
if fh <= sr / 2.0 {
continue;
}
let alias = fold(fold(fh, sr * DRIVE_OVERSAMPLE as f32), sr);
if !(100.0..=20_000.0).contains(&alias) {
continue;
}
if (alias / f0 - (alias / f0).round()).abs() * f0 < 8.0 {
continue;
}
let rel = 20.0 * (crate::testutil::mag_at(seg, sr, alias) / m0).log10();
worst_all = worst_all.max(rel);
}
}
}
}
let g1x = 2f32.powf(k * n_max);
println!(
"{k:4.1} {worst_all:8.1} {:+6.1} {} (g1 x{g1x:.2} at 127)",
-40.0 - worst_all,
if worst_all <= -40.0 { "OK " } else { "OVER" }
);
}
}
#[test]
#[ignore]
fn amp_range_headroom_sweep() {
let sr = 44100.0;
let n_max = (127.0 - 64.0) / 64.0;
let probe = sine(sr, 0.6, 150.0, 0.10);
println!("\n--- cabinet EQ swing (Body, cab[1]) ---");
println!(" +/-dB spread(dB) peak DC verdict");
for eq in [9.0f32, 12.0, 15.0, 18.0] {
let mut spread_min = 999.0f32;
let mut peak_max = 0.0f32;
let mut dc_max = 0.0f32;
for prog in [29u8, 30] {
for alt in [false, true] {
let band = |sign: f32| {
let mut d = Drive::new(prog, alt, sr);
let (h, q, db) = d.base.cab1;
d.cab[1].retune_peak(h, q, db + eq * sign * n_max, d.sr_os);
let mut b = probe.clone();
d.process(&mut b);
let seg = &b[(0.2 * sr) as usize..];
(
20.0 * crate::testutil::spectral_band_rms(seg, sr, 350.0, 750.0)
.max(1e-12)
.log10(),
b.iter().fold(0.0f32, |m, x| m.max(x.abs())),
crate::testutil::dc_offset(seg).abs(),
)
};
let (hi, p1, d1) = band(1.0);
let (lo, p2, d2) = band(-1.0);
spread_min = spread_min.min(hi - lo);
peak_max = peak_max.max(p1.max(p2));
dc_max = dc_max.max(d1.max(d2));
}
}
println!(
"{eq:6.0} {spread_min:8.1} {peak_max:6.3} {dc_max:.2e} {}",
if peak_max < 1.5 && dc_max < 1e-3 {
"safe"
} else {
"CHECK"
}
);
}
println!("\n--- JOINT pre-clip ceiling: Tone swing at Drive 127 ---");
println!(" toneDB worst alias(dBc) verdict (both pre-clip knobs at max)");
{
let fold = |f: f32, fs: f32| {
let r = f % fs;
if r > fs / 2.0 {
fs - r
} else {
r
}
};
let n_max = (127.0 - 64.0) / 64.0;
for tone_db in [4.0f32, 5.0, 6.0, 7.0, 8.0, 9.0] {
let mut worst_all = -200.0f32;
for prog in [29u8, 30] {
for alt in [false, true] {
for f0 in [997.0f32, 1301.0, 1763.0] {
let mut d = Drive::new(prog, alt, sr);
d.g1 = d.base.g1 * 2f32.powf(AMP_DRIVE_K * n_max);
d.g2 = d.base.g2 * 2f32.powf(0.5 * AMP_DRIVE_K * n_max);
d.post = d.base.post * 2f32.powf(-AMP_DRIVE_POST_C * n_max);
let (vh, vq, vdb) = d.base.voice;
d.voice.retune_peak(vh, vq, vdb + tone_db * n_max, d.sr_os);
let mut b: Vec<f32> = (0..(sr as usize))
.map(|i| 0.5 * (std::f32::consts::TAU * f0 * i as f32 / sr).sin())
.collect();
d.process(&mut b);
let seg = &b[(0.2 * sr) as usize..];
let m0 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
for h in 2..=80u32 {
let fh = h as f32 * f0;
if fh <= sr / 2.0 {
continue;
}
let alias = fold(fold(fh, sr * DRIVE_OVERSAMPLE as f32), sr);
if !(100.0..=20_000.0).contains(&alias) {
continue;
}
if (alias / f0 - (alias / f0).round()).abs() * f0 < 8.0 {
continue;
}
worst_all = worst_all.max(
20.0 * (crate::testutil::mag_at(seg, sr, alias) / m0).log10(),
);
}
}
}
}
println!(
"{tone_db:6.0} {worst_all:12.1} {}",
if worst_all <= -40.0 { "OK " } else { "OVER" }
);
}
}
println!("\n--- WORST CASE: Presence boost at Drive 127 + Tone boost max ---");
println!(" presDB worst alias(dBc) verdict");
{
let fold = |f: f32, fs: f32| {
let r = f % fs;
if r > fs / 2.0 {
fs - r
} else {
r
}
};
let nm = (127.0 - 64.0) / 64.0;
for pres_db in [3.0f32, 4.0, 5.0, 6.0, 9.0, 12.0, 15.0] {
let mut worst_all = -200.0f32;
for prog in [29u8, 30] {
for alt in [false, true] {
for f0 in [997.0f32, 1301.0, 1763.0] {
let mut d = Drive::new(prog, alt, sr);
d.g1 = d.base.g1 * 2f32.powf(AMP_DRIVE_K * nm);
d.g2 = d.base.g2 * 2f32.powf(0.5 * AMP_DRIVE_K * nm);
d.post = d.base.post * 2f32.powf(-AMP_DRIVE_POST_C * nm);
let (vh, vq, vdb) = d.base.voice;
d.voice.retune_peak(vh, vq, vdb + AMP_TONE_DB * nm, d.sr_os);
let (ph, pq, pdb) = d.base.cab2;
d.cab[2].retune_peak(ph, pq, pdb + pres_db * nm, d.sr_os);
let mut b: Vec<f32> = (0..(sr as usize))
.map(|i| 0.5 * (std::f32::consts::TAU * f0 * i as f32 / sr).sin())
.collect();
d.process(&mut b);
let seg = &b[(0.2 * sr) as usize..];
let m0 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
for h in 2..=80u32 {
let fh = h as f32 * f0;
if fh <= sr / 2.0 {
continue;
}
let a = fold(fold(fh, sr * DRIVE_OVERSAMPLE as f32), sr);
if !(100.0..=20_000.0).contains(&a) {
continue;
}
if (a / f0 - (a / f0).round()).abs() * f0 < 8.0 {
continue;
}
worst_all = worst_all
.max(20.0 * (crate::testutil::mag_at(seg, sr, a) / m0).log10());
}
}
}
}
println!(
"{pres_db:6.0} {worst_all:12.1} {}",
if worst_all <= -40.0 { "OK " } else { "OVER" }
);
}
}
println!("\n--- Cab Tone floor (cliff corner multiplier at knob 0) ---");
println!(" floor corner(Hz) HF spread(dB) peak verdict");
for floor in [0.5f32, 0.4, 0.3, 0.25] {
let mut spread_min = 999.0f32;
let mut peak_max = 0.0f32;
for prog in [29u8, 30] {
for alt in [false, true] {
let meas = |f: f32| {
let mut d = Drive::new(prog, alt, sr);
d.cab[3].retune_lowpass(d.base.cliff[0].0 * f, d.base.cliff[0].1, d.sr_os);
d.cab[4].retune_lowpass(d.base.cliff[1].0 * f, d.base.cliff[1].1, d.sr_os);
let mut b = probe.clone();
d.process(&mut b);
let seg = &b[(0.2 * sr) as usize..];
(
20.0 * crate::testutil::spectral_band_rms(seg, sr, 2500.0, 3600.0)
.max(1e-12)
.log10(),
b.iter().fold(0.0f32, |m, x| m.max(x.abs())),
)
};
let (bright, p1) = meas(1.0);
let (dark, p2) = meas(floor);
spread_min = spread_min.min(bright - dark);
peak_max = peak_max.max(p1.max(p2));
}
}
println!(
"{floor:6.2} {:8.0} {spread_min:11.1} {peak_max:6.3} {}",
4000.0 * floor,
if peak_max < 1.5 { "safe" } else { "CHECK" }
);
}
}
#[test]
fn amp_drive_knob_holds_alias_floor() {
let sr = 44100.0;
let fold = |f: f32, fs: f32| {
let r = f % fs;
if r > fs / 2.0 {
fs - r
} else {
r
}
};
for (drive, tone, pres) in [
(0u8, 64u8, 64u8),
(64, 64, 64),
(127, 64, 64),
(64, 127, 64),
(64, 0, 64),
(64, 64, 127),
(127, 127, 127),
] {
for prog in [29u8, 30] {
for alt in [false, true] {
for f0 in [997.0f32, 1301.0, 1763.0] {
let mut p = [64u8; AMP_PARAM_COUNT];
p[AMP_DRIVE] = drive;
p[AMP_TONE] = tone;
p[AMP_PRES] = pres;
let input = sine(sr, 1.0, f0, 0.5);
let buf = amp_settled(prog, alt, sr, p, &input);
let seg = &buf[(0.2 * sr) as usize..];
let m0 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
let mut worst = -200.0f32;
for n in 2..=80u32 {
let fh = n as f32 * f0;
if fh <= sr / 2.0 {
continue;
}
let alias = fold(fold(fh, sr * DRIVE_OVERSAMPLE as f32), sr);
if !(100.0..=20_000.0).contains(&alias) {
continue;
}
if (alias / f0 - (alias / f0).round()).abs() * f0 < 8.0 {
continue;
}
let rel = 20.0 * (crate::testutil::mag_at(seg, sr, alias) / m0).log10();
worst = worst.max(rel);
}
assert!(
worst <= -40.0,
"Drive {drive} Tone {tone} Pres {pres} prog {prog} {} f0 {f0:.0}: alias {worst:.1} dBc > -40",
if alt { "alt" } else { "main" }
);
}
}
}
}
}
#[test]
#[ignore]
fn amp_coupling_matrix() {
let sr = 44100.0;
let axes = [
(AMP_DRIVE, 220.0f32, 0.30f32, 1500.0f32, 6000.0f32, 127u8),
(AMP_TONE, 110.0, 0.02, 400.0, 1600.0, 127),
(AMP_TIGHT, 82.4, 0.30, 40.0, 110.0, 0),
(AMP_BODY, 150.0, 0.10, 350.0, 750.0, 127),
(AMP_PRES, 150.0, 0.10, 2000.0, 3400.0, 127),
(AMP_CABTONE, 150.0, 0.10, 2500.0, 3600.0, 0), ];
let names = ["Drive", "Tone", "Tight", "Body", "Pres", "CabTn"];
for prog in [29u8, 30] {
for alt in [false, true] {
println!(
"\n=== coupling prog {prog} {} (row=driven knob, col=measured axis, dB) ===",
if alt { "alt" } else { "main" }
);
print!("{:8}", "");
for n in names {
print!("{n:>8}");
}
println!();
for &(rk, _, _, _, _, more) in &axes {
print!("{:8}", names[rk]);
for &(_, f0, amp, lo, hi, _) in &axes {
let input = sine(sr, 0.6, f0, amp);
let meas = |v: u8| {
let mut p = [64u8; AMP_PARAM_COUNT];
p[rk] = v;
let out = amp_settled(prog, alt, sr, p, &input);
let seg = &out[(0.2 * sr) as usize..];
20.0 * crate::testutil::spectral_band_rms(seg, sr, lo, hi)
.max(1e-12)
.log10()
};
print!("{:8.1}", meas(more) - meas(64));
}
println!();
}
}
}
}
#[test]
fn amp_drive_level_residual_bounded() {
let sr = 44100.0;
let tol_db = 9.0;
for prog in [29u8, 30] {
for alt in [false, true] {
for f0 in [147.0f32, 330.0] {
for amp in [0.3f32, 0.05] {
let input = sine(sr, 0.6, f0, amp);
let level = |drive: u8| {
let mut p = [64u8; AMP_PARAM_COUNT];
p[AMP_DRIVE] = drive;
let out = amp_settled(prog, alt, sr, p, &input);
let seg = &out[(0.2 * sr) as usize..];
20.0 * crate::testutil::spectral_band_rms(seg, sr, 120.0, 6000.0)
.max(1e-12)
.log10()
};
let (l0, l64, l127) = (level(0), level(64), level(127));
let spread = (l0 - l64).abs().max((l127 - l64).abs());
println!(
"level prog {prog} {} f0 {f0:.0} amp {amp}: 0={l0:.2} 64={l64:.2} 127={l127:.2} spread {spread:.2}",
if alt { "alt" } else { "main" }
);
assert!(
spread <= tol_db,
"prog {prog} {} f0 {f0:.0} amp {amp}: level residual {spread:.2} dB > {tol_db}",
if alt { "alt" } else { "main" }
);
}
}
}
}
}
#[test]
fn amp_sweep_has_no_block_rate_zipper() {
let sr = 44100.0;
let block_hz = sr / BLOCK as f32; let f0 = 220.0;
let input = sine(sr, 1.2, f0, 0.3);
let nb = input.len().div_ceil(BLOCK);
let swept = amp_blocks(29, false, sr, &input, |bi| {
let mut p = [64u8; AMP_PARAM_COUNT];
let frac = ((bi as f32 / nb as f32) - 0.33) / 0.34;
p[AMP_DRIVE] = (64.0 + 63.0 * frac.clamp(0.0, 1.0)) as u8;
p
});
let control = amp_blocks(29, false, sr, &input, |_| [96; AMP_PARAM_COUNT]);
let seg = &swept[(0.1 * sr) as usize..];
let ctl = &control[(0.1 * sr) as usize..];
let mut worst = -200.0f32;
for k in 1..=3 {
for side in [-1.0f32, 1.0] {
let f = f0 + side * k as f32 * block_hz;
if f < 40.0 || f > sr / 2.0 - 40.0 {
continue;
}
let s = 20.0 * crate::testutil::mag_at(seg, sr, f).max(1e-12).log10();
let c = 20.0 * crate::testutil::mag_at(ctl, sr, f).max(1e-12).log10();
worst = worst.max(s - c);
}
}
println!("zipper: worst block-rate sideband {worst:.1} dB over control");
assert!(
worst <= 12.0,
"swept Drive raised a block-rate ({block_hz:.0} Hz) sideband {worst:.1} dB above the steady control — zipper"
);
}
#[test]
fn amp_single_step_does_not_click() {
let sr = 44100.0;
let f0 = 196.0;
let input = sine(sr, 1.0, f0, 0.3);
let nb = input.len().div_ceil(BLOCK);
let step_block = nb / 2;
let stepped = amp_blocks(29, true, sr, &input, |bi| {
if bi >= step_block {
let mut p = [64u8; AMP_PARAM_COUNT];
p[AMP_DRIVE] = 127;
p[AMP_BODY] = 127;
p
} else {
[64; AMP_PARAM_COUNT]
}
});
let steady = amp_blocks(29, true, sr, &input, |_| [AMP_NEUTRAL; AMP_PARAM_COUNT]);
let s_click = crate::testutil::max_slew_norm(&stepped);
let base_click = crate::testutil::max_slew_norm(&steady);
println!("step click {s_click:.4} vs steady {base_click:.4}");
assert!(
s_click <= base_click * 2.0 + 0.02,
"single-step Body/Drive jump clicked: peak slew {s_click:.4} vs steady {base_click:.4}"
);
}
#[test]
fn amp_params_follow_the_state_table() {
let sr = 44100.0;
let opts = || CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
};
let author = |core: &mut EngineCore| {
core.handle_event(EvKind::Prog { ch: 0, prog: 29 });
core.handle_event(EvKind::Cc {
ch: 0,
num: 99,
val: AMP_NRPN_MSB,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 98,
val: AMP_DRIVE as u8,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 6,
val: 100,
});
};
let mut core = EngineCore::new(opts());
author(&mut core);
assert!(core.strips[0].amp_authored);
assert_eq!(core.strips[0].amp_params[AMP_DRIVE], 100);
core.handle_event(EvKind::Prog { ch: 0, prog: 30 });
assert!(
core.strips[0].amp_authored,
"Program Change dropped amp authoring"
);
assert_eq!(
core.strips[0].amp_params[AMP_DRIVE], 100,
"Program Change reset an amp param"
);
assert!(
core.strips[0].drive.as_ref().unwrap().active,
"rebuilt Drive did not re-apply the rig"
);
let mut core = EngineCore::new(opts());
author(&mut core);
core.handle_event(EvKind::Cc {
ch: 0,
num: 0,
val: 1,
});
assert_eq!(
core.strips[0].amp_params[AMP_DRIVE], 100,
"CC0 reset an amp param"
);
assert!(
core.strips[0].drive.as_ref().unwrap().active,
"CC0-rebuilt Drive did not re-apply the rig"
);
let mut core = EngineCore::new(opts());
author(&mut core);
core.handle_event(EvKind::Cc {
ch: 0,
num: 121,
val: 0,
});
assert_eq!(
core.strips[0].amp_params[AMP_DRIVE], 100,
"CC121 reset an amp param value"
);
assert_eq!(
core.strips[0].nrpn_msb, 127,
"CC121 did not null the NRPN selector"
);
assert!(
core.strips[0].drive.as_ref().unwrap().active,
"CC121-rebuilt Drive did not re-apply the rig"
);
let mut core = EngineCore::new(opts());
author(&mut core);
core.handle_event(EvKind::GmReset);
assert!(
!core.strips[0].amp_authored,
"GM System On did not clear amp authoring"
);
assert_eq!(
core.strips[0].amp_params[AMP_DRIVE], 64,
"GM System On did not reset amp params to neutral"
);
let mut core = EngineCore::new(opts());
core.handle_event(EvKind::Prog { ch: 0, prog: 29 });
core.handle_event(EvKind::Cc {
ch: 0,
num: 99,
val: AMP_NRPN_MSB,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 98,
val: AMP_DRIVE as u8,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 6,
val: 127,
});
core.handle_event(EvKind::Cc {
ch: 0,
num: 0,
val: 1,
}); let g1 = core.strips[0].drive.as_ref().unwrap().g1;
let neutral_g1 = Drive::new(29, true, sr).g1;
assert!(
g1 > neutral_g1 * 1.5,
"rebuild before first block left g1 at {g1} (neutral {neutral_g1}); the authored Drive=127 did not snap"
);
}
#[test]
fn amp_nrpn_never_corrupts_the_rpn_bend_range() {
let sr = 44100.0;
let opts = CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
};
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
let mut core = EngineCore::new(opts);
core.handle_event(cc(99, AMP_NRPN_MSB));
core.handle_event(cc(98, AMP_DRIVE as u8));
core.handle_event(cc(6, 100)); assert_eq!(core.strips[0].amp_params[AMP_DRIVE], 100);
core.handle_event(cc(101, 0)); core.handle_event(cc(100, 0));
core.handle_event(cc(38, 0)); assert_ne!(
core.strips[0].bend_range, 24.0,
"amp NRPN Data Entry leaked into the RPN bend range (KILN-00034 direction A)"
);
let mut core = EngineCore::new(opts);
core.handle_event(cc(6, 100)); core.handle_event(cc(101, 0));
core.handle_event(cc(100, 0));
core.handle_event(cc(38, 0));
assert_ne!(
core.strips[0].bend_range, 24.0,
"null-selector Data Entry leaked into the RPN bend range (KILN-00034 null path)"
);
let mut core = EngineCore::new(opts);
core.handle_event(cc(99, AMP_NRPN_MSB));
core.handle_event(cc(98, AMP_PRES as u8));
core.handle_event(cc(6, 120));
assert_eq!(core.strips[0].amp_params[AMP_PRES], 120);
assert!(core.strips[0].amp_authored);
}
#[test]
#[ignore]
fn drive_level_probe() {
let sr = 44100.0;
for prog in [29u8, 30] {
for alt in [false, true] {
for amp in [0.5f32, 0.05] {
let mut d = Drive::new(prog, alt, sr);
let mut buf: Vec<f32> = (0..(sr as usize))
.map(|i| amp * (std::f32::consts::TAU * 220.0 * i as f32 / sr).sin())
.collect();
d.process(&mut buf);
let seg = &buf[(0.2 * sr) as usize..];
let r = crate::testutil::rms(seg);
let b = crate::testutil::spectral_band_rms(seg, sr, 300.0, 3400.0);
println!(
"drive probe prog {prog} {} amp {amp}: wide {:.2} dBFS band300-3400 {:.2} dBFS",
if alt { "alt " } else { "main" },
20.0 * r.max(1e-12).log10(),
20.0 * b.max(1e-12).log10()
);
}
}
}
}
#[test]
#[ignore]
fn drive_voicing_fingerprint_probe() {
let sr = 44100.0;
let input: Vec<f32> = {
let mut rng = crate::dsp::Rng::new(7);
(0..(4.0 * sr) as usize)
.map(|_| rng.white() * 0.02)
.collect()
};
for prog in [29u8, 30] {
let run = |alt: bool| {
let mut d = Drive::new(prog, alt, sr);
let mut b = input.clone();
d.process(&mut b);
b
};
let (m, a) = (run(false), run(true));
let db = |b: &[f32], f: f32| {
20.0 * crate::testutil::band_rms(&b[(0.2 * sr) as usize..], sr, f, 4.0)
.max(1e-12)
.log10()
};
print!("fingerprint prog {prog} (lead-main dB):");
for f in [
100.0f32, 250.0, 500.0, 800.0, 1400.0, 2500.0, 2800.0, 3000.0, 6000.0,
] {
print!(" {f:.0}:{:+.2}", db(&a, f) - db(&m, f));
}
println!();
}
for prog in [29u8, 30] {
for alt in [false, true] {
for f0 in [110.0f32, 165.0, 220.0, 330.0] {
for amp in [0.5f32, 0.05] {
let mut d = Drive::new(prog, alt, sr);
let mut buf: Vec<f32> = (0..(sr as usize))
.map(|i| amp * (std::f32::consts::TAU * f0 * i as f32 / sr).sin())
.collect();
d.process(&mut buf);
let seg = &buf[(0.2 * sr) as usize..];
let m1 = crate::testutil::mag_at(seg, sr, f0).max(1e-12);
let h2 = 20.0 * (crate::testutil::mag_at(seg, sr, 2.0 * f0) / m1).log10();
let h3 = 20.0 * (crate::testutil::mag_at(seg, sr, 3.0 * f0) / m1).log10();
println!(
"harmonics prog {prog} {} f0 {f0:.0} amp {amp}: \
H2 {h2:+.1} dBc H3 {h3:+.1} dBc H2-H3 {:+.1}",
if alt { "alt " } else { "main" },
h2 - h3
);
}
}
}
}
}
fn rms_env(sig: &[f32], sr: f32) -> Vec<(f32, f32)> {
let win = (0.030 * sr) as usize;
let hop = (0.010 * sr) as usize;
let mut v = Vec::new();
let mut a = 0usize;
while a + win <= sig.len() {
v.push((a as f32 / sr, rms(&sig[a..a + win])));
a += hop;
}
v
}
fn o_attack_render(
program: u8,
key: u8,
vel: u8,
secs: f64,
samples: bool,
sr: f32,
) -> Vec<f32> {
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
let events = vec![
(
0.0,
EvKind::Prog {
ch: 0,
prog: program,
},
),
(0.0, cc(91, 0)),
(0.0, cc(93, 0)),
(0.0, cc(94, 0)),
(0.05, EvKind::NoteOn { ch: 0, key, vel }),
];
let mut opt = test_opts(sr);
opt.samples = samples;
left(&render(&test_song(events, secs), &opt).0)
}
fn assert_attack_dominates(sig: &[f32], sr: f32, note_s: f32, attack_s: f32, name: &str) {
let env = rms_env(sig, sr);
let attack = env
.iter()
.filter(|(t, _)| *t >= note_s - 0.005 && *t < note_s + attack_s)
.map(|&(_, r)| r)
.fold(0f32, f32::max);
assert!(attack > 1e-5, "{name}: silent attack window");
let (mut worst, mut worst_t) = (0f32, 0f32);
for &(t, r) in env.iter().filter(|(t, _)| *t >= note_s + attack_s) {
if r > worst {
(worst, worst_t) = (r, t);
}
}
assert!(
worst <= attack,
"{name}: late bloom — window at {worst_t:.2} s has rms {worst:.5} \
above the attack's {attack:.5}"
);
}
#[test]
fn o_attack_struck_attack_dominates() {
let sr = 44100.0;
for (program, key, attack_s, name) in [
(0u8, 60u8, 0.15f32, "acoustic piano"),
(6, 60, 0.12, "harpsichord"), (9, 84, 0.12, "glockenspiel"),
(12, 72, 0.10, "marimba"),
(14, 65, 0.20, "tubular bells"),
(47, 50, 0.20, "timpani"),
(24, 52, 0.12, "nylon guitar"),
(25, 52, 0.12, "steel guitar"),
(45, 60, 0.10, "pizzicato"),
(46, 60, 0.12, "harp"),
] {
let sig = o_attack_render(program, key, 100, 3.0, false, sr);
assert_attack_dominates(&sig, sr, 0.05, attack_s, name);
}
}
#[test]
fn o_attack_sampled_struck_attack_dominates() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for (program, key, attack_s, name) in [
(0u8, 60u8, 0.20f32, "acoustic piano (LA)"),
(24, 52, 0.12, "nylon guitar (LA)"),
(24, 57, 0.12, "nylon guitar (LA), mid"),
(24, 64, 0.12, "nylon guitar (LA), high"),
] {
let sig = o_attack_render(program, key, 100, 3.0, true, sr);
assert_attack_dominates(&sig, sr, 0.05, attack_s, name);
}
}
#[test]
fn o_attack_sustained_plucks_no_late_bloom() {
let sr = 44100.0;
for (program, vel, secs, name) in [
(29u8, 20u8, 4.0, "overdrive pp"),
(29, 100, 4.0, "overdrive ff"),
(30, 30, 4.0, "distortion p"),
(30, 100, 8.0, "distortion ff, long hold"),
] {
let sig = o_attack_render(program, 45, vel, secs, false, sr);
assert_attack_dominates(&sig, sr, 0.05, 0.15, name);
}
}
#[test]
fn o_attack_drive_never_blooms_on_decay() {
let sr = 44100.0;
let n = (2.0 * sr) as usize;
for prog in [29u8, 30] {
let mut d = Drive::new(prog, false, sr);
let mut buf: Vec<f32> = (0..n)
.map(|i| {
let t = i as f32 / sr;
0.5 * 10f32.powf(-15.0 * t / 20.0) * (std::f32::consts::TAU * 220.0 * t).sin()
})
.collect();
d.process(&mut buf);
let win = (0.05 * sr) as usize;
let env: Vec<f32> = buf.chunks_exact(win).map(rms).collect();
for (k, pair) in env.windows(2).enumerate().skip(1) {
assert!(
pair[1] <= pair[0] * 1.06,
"prog {prog}: output rose on a decaying input at window {k}: \
{:.5} -> {:.5}",
pair[0],
pair[1]
);
}
let db = |a: f32, z: f32| {
20.0 * rms(&buf[(a * sr) as usize..(z * sr) as usize])
.max(1e-12)
.log10()
};
let decay = db(0.05, 0.10) - db(1.80, 1.95);
assert!(
decay >= 12.0,
"prog {prog}: tail held up — decay {decay:.1} dB < 12 \
(a sag-like envelope-inverse recovery gain)"
);
}
}
#[test]
#[ignore = "diagnostic probe, not a gate"]
fn o_attack_held_note_probe() {
let sr = 44100.0;
for prog in [29u8, 30] {
let sig = o_attack_render(prog, 45, 100, 3.5, false, sr);
let w = |a: f32, z: f32| rms(&sig[(a * sr) as usize..(z * sr) as usize]);
let (early, late) = (w(0.2, 0.5), w(2.5, 3.0));
println!(
"prog {prog}: early {early:.5} ({:.1} dB), late {late:.5} ({:.1} dB), \
drop {:.1} dB",
20.0 * early.max(1e-9).log10(),
20.0 * late.max(1e-9).log10(),
20.0 * (early.max(1e-9) / late.max(1e-9)).log10()
);
}
}
#[test]
fn glue_compresses_and_stays_sane() {
let sr = 44100.0;
let mut glue = BusGlue::new(sr);
let mut l: Vec<f32> = (0..44100)
.map(|i| (i as f32 * 220.0 / sr * std::f32::consts::TAU).sin() * 0.8)
.collect();
let mut r = l.clone();
glue.process(&mut l, &mut r);
let out_peak = l[22050..].iter().fold(0f32, |m, &x| m.max(x.abs()));
assert!(out_peak < 0.8, "no gain reduction: {out_peak}");
assert!(out_peak > 0.3, "over-compressed: {out_peak}");
assert!(l.iter().all(|x| x.is_finite()));
let mut glue2 = BusGlue::new(sr);
let mut ql: Vec<f32> = (0..44100)
.map(|i| (i as f32 * 220.0 / sr * std::f32::consts::TAU).sin() * 0.05)
.collect();
let mut qr = ql.clone();
glue2.process(&mut ql, &mut qr);
let q_peak = ql[22050..].iter().fold(0f32, |m, &x| m.max(x.abs()));
assert!(
(q_peak - 0.05).abs() < 0.012,
"quiet signal changed: {q_peak}"
);
}
fn cycle_freq_spread(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 times = Vec::new();
for i in 1..f.len() {
if f[i - 1] <= 0.0 && f[i] > 0.0 {
times.push((i - 1) as f32 - f[i - 1] / (f[i] - f[i - 1]));
}
}
let freqs: Vec<f32> = times.windows(2).map(|w| sr / (w[1] - w[0])).collect();
let smooth: Vec<f32> = freqs
.windows(8)
.map(|w| w.iter().sum::<f32>() / 8.0)
.collect();
let hi = smooth.iter().fold(f32::MIN, |m, &x| m.max(x));
let lo = smooth.iter().fold(f32::MAX, |m, &x| m.min(x));
hi - lo
}
fn cycle_freq_spread_f0(seg: &[f32], sr: f32, f0: f32) -> f32 {
let q = 1.3;
let mut fwd = Biquad::bandpass(f0, q, sr);
let forward: Vec<f32> = seg.iter().map(|&x| fwd.process(x)).collect();
let mut rev = Biquad::bandpass(f0, q, sr);
let mut back: Vec<f32> = forward.iter().rev().map(|&x| rev.process(x)).collect();
back.reverse();
cycle_freq_spread(&back, sr)
}
fn render_cc1_program(program: u8, cc1: Option<u8>, cc1_before_note: bool) -> Vec<f32> {
let mut events = Vec::new();
if program == 19 {
events.push((
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
));
}
events.extend([
(
0.0,
EvKind::Prog {
ch: 0,
prog: program,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
]);
if cc1_before_note {
if let Some(val) = cc1 {
events.push((0.0, EvKind::Cc { ch: 0, num: 1, val }));
}
}
events.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
));
if !cc1_before_note {
if let Some(val) = cc1 {
events.push((0.60, EvKind::Cc { ch: 0, num: 1, val }));
}
}
events.push((3.85, EvKind::NoteOff { ch: 0, key: 69 }));
left(&render(&test_song(events, 4.0), &test_opts(44100.0)).0)
}
fn render_cc1_events(events: Vec<(f64, EvKind)>) -> Vec<f32> {
left(&render(&test_song(events, 4.0), &test_opts(44100.0)).0)
}
fn am_rate(mono: &[f32], sr: f32, t0: f32, t1: f32) -> f32 {
let mut env_lps = [OnePole::lowpass(12.0, sr); 4];
let env: Vec<f32> = mono
.iter()
.map(|&x| {
let mut y = x.abs();
for lp in env_lps.iter_mut() {
y = lp.process(y);
}
y
})
.collect();
let mut trend = OnePole::lowpass(1.2, sr);
let det: Vec<f32> = env.iter().map(|&x| x - trend.process(x)).collect();
let seg = &det[(t0 * sr) as usize..(t1 * sr) as usize];
let crossings = seg.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count();
crossings as f32 / (t1 - t0)
}
fn render_bowed_program_with_mod(program: u8, mod_val: u8) -> Vec<f32> {
let song = test_song(
vec![
(
0.0,
EvKind::Prog {
ch: 0,
prog: program,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: mod_val,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(2.4, EvKind::NoteOff { ch: 0, key: 69 }),
],
2.5,
);
left(&render(&song, &test_opts(44100.0)).0)
}
fn render_bowed_controls(
program: u8,
key: u8,
mod_val: u8,
aftertouch: u8,
samples: bool,
) -> Vec<f32> {
let song = test_song(
vec![
(
0.0,
EvKind::Prog {
ch: 0,
prog: program,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: mod_val,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
),
(
0.10,
EvKind::Aftertouch {
ch: 0,
val: aftertouch,
},
),
(2.4, EvKind::NoteOff { ch: 0, key }),
],
2.5,
);
let opt = Options {
samples,
..test_opts(44100.0)
};
left(&render(&song, &opt).0)
}
fn render_bowed_with_mod(mod_val: u8) -> Vec<f32> {
render_bowed_program_with_mod(44, mod_val)
}
#[test]
fn cc1_mod_wheel_adds_vibrato() {
let sr = 44100.0;
let plain = render_bowed_with_mod(0);
let modded = render_bowed_with_mod(127);
let (a, b) = ((0.8 * sr) as usize, (2.2 * sr) as usize);
let f0 = crate::dsp::key_freq(69);
let spread_plain = autocorr_cents_spread(&plain[a..b], sr, f0);
let spread_mod = autocorr_cents_spread(&modded[a..b], sr, f0);
assert!(
(3.0..=42.0).contains(&spread_plain),
"GM44 autonomous excursion {spread_plain:.1} cents"
);
assert!(
spread_mod > 2.0 * spread_plain,
"GM44 CC1 {spread_mod:.1} vs natural {spread_plain:.1} cents"
);
}
#[test]
fn gm110_fiddle_routes_to_bowedstring_and_takes_mod_vibrato() {
let sr = 44100.0;
let routed = crate::voices::make(110, 69, 100, sr, 5, true);
assert_eq!(
routed.kind(),
"bowedstring",
"GM 110 must use the bowed-waveguide/LA fiddle path"
);
assert!(vibrato_family(110), "GM 110 must take authored CC1 vibrato");
assert_eq!(
fx_profile(110, 0),
fx_profile(40, 0),
"GM 110 should use the fiddle bus profile"
);
}
#[test]
fn bowed_natural_cc1_aftertouch_composition_is_bounded() {
let sr = 44100.0;
let range = ((0.8 * sr) as usize, (2.2 * sr) as usize);
for (program, key, samples) in [(44u8, 69u8, false), (44, 69, true)] {
let plain = render_bowed_controls(program, key, 0, 0, samples);
let modded = render_bowed_controls(program, key, 127, 0, samples);
let composed = render_bowed_controls(program, key, 127, 127, samples);
let f0 = crate::dsp::key_freq(key);
let plain_c = autocorr_cents_spread(&plain[range.0..range.1], sr, f0);
let mod_c = autocorr_cents_spread(&modded[range.0..range.1], sr, f0);
let both_c = autocorr_cents_spread(&composed[range.0..range.1], sr, f0);
assert!(
(3.0..=42.0).contains(&plain_c),
"GM{program} samples={samples}: autonomous excursion {plain_c:.1} cents"
);
assert!(
mod_c >= 2.0 * plain_c,
"GM{program} samples={samples}: CC1 {mod_c:.1} vs natural {plain_c:.1} cents"
);
assert!(
both_c < 150.0,
"GM{program} samples={samples}: composed excursion {both_c:.1} cents"
);
let seg = &composed[range.0..range.1];
let win = (0.01 * sr) as usize;
let levels: Vec<f32> = seg.chunks(win).map(rms).collect();
let mut ordered = levels.clone();
ordered.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = ordered[ordered.len() / 2];
let floor = levels.iter().copied().fold(f32::INFINITY, f32::min);
assert!(
floor >= 0.25 * median,
"GM{program} samples={samples}: controller beat dropout {floor:.5} vs median {median:.5}"
);
}
}
fn autocorr_cents_spread(seg: &[f32], sr: f32, f0: f32) -> f32 {
let win = 2048usize;
let hop = 2048usize;
let lag_lo = (sr / (f0 * 1.5)).floor().max(2.0) as usize;
let lag_hi = ((sr / (f0 * 0.67)).ceil() as usize).min(win - 2);
let mut freqs = Vec::new();
let mut start = 0;
while start + win <= seg.len() {
let w = &seg[start..start + win];
let mean = w.iter().sum::<f32>() / win as f32;
let acf = |lag: usize| -> f32 {
let mut a = 0.0f32;
for i in 0..(win - lag) {
a += (w[i] - mean) * (w[i + lag] - mean);
}
a
};
let mut best_lag = lag_lo;
let mut best = f32::MIN;
for lag in lag_lo..=lag_hi {
let a = acf(lag);
if a > best {
best = a;
best_lag = lag;
}
}
let refined = if best_lag > lag_lo && best_lag < lag_hi {
let (y0, y1, y2) = (acf(best_lag - 1), best, acf(best_lag + 1));
let d = y0 - 2.0 * y1 + y2;
best_lag as f32 + if d != 0.0 { 0.5 * (y0 - y2) / d } else { 0.0 }
} else {
best_lag as f32
};
freqs.push(sr / refined);
start += hop;
}
if freqs.len() < 2 {
return 0.0;
}
let hi = freqs.iter().copied().fold(f32::MIN, f32::max);
let lo = freqs.iter().copied().fold(f32::MAX, f32::min);
1200.0 * (hi / lo).log2()
}
#[test]
fn bowedstring_family_pitch_bounded() {
let sr = 44100.0;
let range = ((0.8 * sr) as usize, (2.2 * sr) as usize);
for (program, key, samples) in [
(43u8, 45u8, false),
(43, 45, true),
(43, 40, false),
(40, 69, false),
(40, 69, true),
(41, 60, false),
(42, 48, false),
(44, 69, false),
(44, 69, true),
(110, 69, false),
(110, 69, true),
] {
let f0 = crate::dsp::key_freq(key);
let plain = render_bowed_controls(program, key, 0, 0, samples);
let modded = render_bowed_controls(program, key, 127, 0, samples);
let composed = render_bowed_controls(program, key, 127, 127, samples);
let plain_c = autocorr_cents_spread(&plain[range.0..range.1], sr, f0);
let mod_c = autocorr_cents_spread(&modded[range.0..range.1], sr, f0);
let both_c = autocorr_cents_spread(&composed[range.0..range.1], sr, f0);
assert!(
(3.0..=42.0).contains(&plain_c),
"GM{program} key={key} samples={samples}: autonomous excursion {plain_c:.1} cents"
);
assert!(
mod_c >= 2.0 * plain_c,
"GM{program} key={key} samples={samples}: CC1 {mod_c:.1} vs natural {plain_c:.1} cents"
);
assert!(
both_c < 150.0,
"GM{program} key={key} samples={samples}: composed excursion {both_c:.1} cents"
);
let seg = &composed[range.0..range.1];
let win = (0.01 * sr) as usize;
let levels: Vec<f32> = seg.chunks(win).map(rms).collect();
let mut ordered = levels.clone();
ordered.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = ordered[ordered.len() / 2];
let floor = levels.iter().copied().fold(f32::INFINITY, f32::min);
assert!(
floor >= 0.25 * median,
"GM{program} key={key} samples={samples}: controller beat dropout {floor:.5} vs median {median:.5}"
);
}
}
#[test]
fn bagpipe_shanai_route_to_reed_engine_with_drone_or_double_reed() {
let sr = 44100.0;
assert_eq!(
crate::voices::make(109, 67, 100, sr, 5, false).kind(),
"reed",
"GM109 bagpipe chanter must use the reed voice"
);
assert_eq!(
crate::voices::make(111, 72, 100, sr, 5, false).kind(),
"reed",
"GM111 shanai must use the reed voice"
);
assert!(vibrato_family(109), "bagpipe should take authored reed CC1");
assert!(vibrato_family(111), "shanai should take authored reed CC1");
let bagpipe = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 109 }),
(
0.0,
EvKind::NoteOn {
ch: 0,
key: 43,
vel: 70,
},
),
(
0.12,
EvKind::NoteOn {
ch: 0,
key: 67,
vel: 104,
},
),
(0.42, EvKind::NoteOff { ch: 0, key: 67 }),
(
0.50,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 104,
},
),
(0.82, EvKind::NoteOff { ch: 0, key: 69 }),
(1.10, EvKind::NoteOff { ch: 0, key: 43 }),
],
1.45,
);
let (bagpipe_out, stats) = render(&bagpipe, &test_opts(sr));
assert_eq!(
stats.voices_spawned, 3,
"one low drone control plus two chanters should spawn one channel drone and two reed chanters"
);
let bagpipe_l = left(&bagpipe_out);
let root = key_freq(43);
let fifth = root * 1.5;
let drone_only = &bagpipe_l[(0.05 * sr) as usize..(0.11 * sr) as usize];
let drone_root = crate::testutil::band_rms(drone_only, sr, root, 8.0);
let drone_fifth = crate::testutil::band_rms(drone_only, sr, fifth, 8.0);
let drone_level = rms(drone_only);
assert!(
drone_root > 0.18 * drone_level,
"bagpipe drone root band weak: root/drone {:.3}",
drone_root / drone_level.max(1e-9)
);
assert!(
drone_fifth < 0.55 * drone_root,
"bagpipe drone carries a FIFTH ({:.3} of root) — Highland drones \
are a tenor pair plus a bass octave",
drone_fifth / drone_root.max(1e-9)
);
let body = &bagpipe_l[(0.20 * sr) as usize..(0.74 * sr) as usize];
let body_rms = rms(body);
let body_drone = crate::testutil::band_rms(body, sr, root, 8.0);
let body_bass = crate::testutil::band_rms(body, sr, root * 0.5, 6.0);
let chanter = crate::testutil::band_rms(body, sr, key_freq(67), 10.0)
+ crate::testutil::band_rms(body, sr, key_freq(69), 10.0);
assert!(
body_drone > 0.06 * body_rms && chanter > 0.10 * body_rms,
"bagpipe needs drone plus chanter: drone/body {:.3}, chanter/body {:.3}",
body_drone / body_rms.max(1e-9),
chanter / body_rms.max(1e-9)
);
assert!(
body_bass > 0.04 * body_rms,
"bagpipe drone has no bass octave: bass/body {:.3}",
body_bass / body_rms.max(1e-9)
);
assert!(
body_rms < 0.12,
"bagpipe drone+chanter level too hot before normalisation: {body_rms:.4}"
);
let release_tail = rms(&bagpipe_l[(1.30 * sr) as usize..(1.42 * sr) as usize]);
assert!(
release_tail < 0.25 * body_rms,
"bagpipe drone did not release with the channel: tail/body {:.3}",
release_tail / body_rms.max(1e-9)
);
let render_voice = |program: u8, key: u8, secs: f32| {
let mut voice = crate::voices::make(program, key, 110, sr, 9, false);
let mut buf = vec![0f32; (secs * sr) as usize];
voice.render(&mut buf);
buf
};
let shanai = render_voice(111, 72, 1.4);
let clarinet = render_voice(71, 72, 1.4);
let seg_start = (0.35 * sr) as usize;
let seg_end = (1.20 * sr) as usize;
let sh_seg = &shanai[seg_start..seg_end];
let cl_seg = &clarinet[seg_start..seg_end];
let prom = |s: &[f32], f: f32| crate::testutil::band_rms(s, sr, f, 2.5) / rms(s).max(1e-9);
assert!(
prom(sh_seg, 1200.0) > 1.25 * prom(cl_seg, 1200.0),
"shanai nasal formant too weak vs clarinet: {:.3} vs {:.3}",
prom(sh_seg, 1200.0),
prom(cl_seg, 1200.0)
);
let f0 = key_freq(72);
let sh_h1 = crate::testutil::band_rms(sh_seg, sr, f0, 12.0);
let sh_h2 = crate::testutil::band_rms(sh_seg, sr, f0 * 2.0, 12.0);
let cl_h1 = crate::testutil::band_rms(cl_seg, sr, f0, 12.0);
let cl_h2 = crate::testutil::band_rms(cl_seg, sr, f0 * 2.0, 12.0);
assert!(
sh_h2 / sh_h1.max(1e-9) > 2.0 * (cl_h2 / cl_h1.max(1e-9)),
"shanai should be even-rich like a double reed: shanai {:.3}, clarinet {:.3}",
sh_h2 / sh_h1.max(1e-9),
cl_h2 / cl_h1.max(1e-9)
);
let high = render_voice(111, 87, 0.24);
let seg = &high[(0.06 * sr) as usize..(0.22 * sr) as usize];
let f_alias = sr / 35.5;
let fund = crate::testutil::mag_at(seg, sr, f_alias).max(1e-12);
let mut acc = 0.0f64;
let mut cnt = 0u32;
let mut k = 1.5f32;
while f_alias * k < 0.4 * sr {
let r = crate::testutil::mag_at(seg, sr, f_alias * k) / fund;
acc += (r as f64) * (r as f64);
cnt += 1;
k += 1.0;
}
let alias = (acc / cnt as f64).sqrt() as f32;
assert!(
alias < 0.03,
"shanai fold-back alias floor too high: {alias:.4}"
);
}
#[test]
fn bagpipe_drone_is_continuous_across_chanter_note_boundaries() {
let sr = 44100.0;
let song = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 109 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 67,
vel: 96,
},
),
(0.60, EvKind::NoteOff { ch: 0, key: 67 }),
(
0.62,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 96,
},
),
(1.20, EvKind::NoteOff { ch: 0, key: 69 }),
],
1.30,
);
let (out, stats) = render(&song, &test_opts(sr));
let l = left(&out);
let win = |a: f32, b: f32| &l[(a * sr) as usize..(b * sr) as usize];
let tenor_w1 = crate::testutil::band_rms(win(0.25, 0.55), sr, 196.0, 8.0);
let tenor_w2 = crate::testutil::band_rms(win(0.70, 1.15), sr, 196.0, 8.0);
let tenor_hold = tenor_w2 / tenor_w1.max(1e-9);
let mut env98: Vec<(f32, f32)> = Vec::new();
let mut t = 0.20f32;
while t + 0.10 <= 1.151 {
env98.push((
t,
crate::testutil::band_rms(win(t, t + 0.10), sr, 98.0, 8.0),
));
t += 0.05;
}
let mut all: Vec<f32> = env98.iter().map(|&(_, e)| e).collect();
all.sort_by(|a, b| a.partial_cmp(b).unwrap());
let med = all[all.len() / 2];
let dip = env98
.iter()
.filter(|&&(t0, _)| (0.40..=0.90).contains(&t0))
.map(|&(_, e)| e)
.fold(f32::INFINITY, f32::min)
/ med.max(1e-9);
let body = rms(win(0.25, 1.15));
println!(
"BP-O3: voices {}, tenor hold {tenor_hold:.3}, bass med/body {:.3}, \
bass dip {dip:.3}",
stats.voices_spawned,
med / body.max(1e-9)
);
assert_eq!(
stats.voices_spawned, 3,
"two chanter notes must share ONE latched channel drone \
(2 chanters + 1 drone); more means a re-spawn per note"
);
assert!(
med > 0.06 * body,
"the drone has no steady bass octave at 98 Hz (med/body {:.4})",
med / body.max(1e-9)
);
assert!(
tenor_hold >= 0.25,
"tenor drone collapsed across the note boundary (note2/note1 \
196 Hz band {tenor_hold:.3}) — released and re-spawned at the \
new note's pitch"
);
assert!(
dip >= 0.5,
"bass drone notches at the chanter note boundary: min/median {dip:.3}"
);
}
#[cfg(feature = "embedded-samples")]
fn bp_opts(sr: f32, samples: bool) -> Options {
Options {
samples,
..test_opts(sr)
}
}
#[cfg(feature = "embedded-samples")]
fn bagpipe_song(key: u8, secs: f64, bank: Option<u8>) -> Song {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog: 109 })];
if let Some(v) = bank {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: v,
},
));
}
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
));
ev.push((secs - 0.1, EvKind::NoteOff { ch: 0, key }));
test_song(ev, secs)
}
#[cfg(feature = "embedded-samples")]
#[test]
fn bagpipe_sampled_dispatch_and_alt_bank_are_coherent() {
let sr = 44100.0;
let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
let (model, _) = render(&bagpipe_song(69, 1.0, None), &bp_opts(sr, false));
let (sampled, _) = render(&bagpipe_song(69, 1.0, None), &bp_opts(sr, true));
let (alt, _) = render(&bagpipe_song(69, 1.0, Some(1)), &bp_opts(sr, true));
assert_ne!(
bits(&model),
bits(&sampled),
"samples-on 109 did not engage the sampled bagpipe"
);
assert_eq!(
bits(&alt),
bits(&model),
"alt-bank (CC0=1) 109 must render the byte-identical model — the \
mandatory altbank arm is missing, or the drone ignored alt_bank"
);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn bagpipe_sampled_drone_octave_and_control_pitch() {
let sr = 44100.0;
let song = {
let ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 109 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 43,
vel: 100,
},
),
(1.4, EvKind::NoteOff { ch: 0, key: 43 }),
];
test_song(ev, 1.5)
};
let (out, _) = render(&song, &bp_opts(sr, true));
let l = left(&out);
let win = &l[(0.4 * sr) as usize..(1.2 * sr) as usize];
let tenor = crate::testutil::band_rms(win, sr, 98.0, 8.0);
let bass = crate::testutil::band_rms(win, sr, 49.0, 6.0);
let body = rms(win).max(1e-9);
assert!(
tenor > 0.06 * body,
"no tenor at the control pitch (98 Hz {:.4} of body) — the \
drone-control at-pitch branch is missing",
tenor / body
);
assert!(
bass > 0.04 * body,
"no bass octave below (49 Hz {:.4} of body)",
bass / body
);
let (m, _) = render(&song, &bp_opts(sr, false));
let (rs, rm) = (rms(&left(&out)), rms(&left(&m)).max(1e-9));
assert!(
(0.4..=2.5).contains(&(rs / rm)),
"sampled drone level {rs:.4} is off the modeled {rm:.4} (x{:.2})",
rs / rm
);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn bagpipe_sampled_coverage_extremes_are_finite() {
for &sr in &[44100.0f32, 48000.0, 96000.0] {
for &key in &[55u8, 81, 84] {
let (out, _) = render(&bagpipe_song(key, 0.6, None), &bp_opts(sr, true));
assert!(
out.iter().all(|x| x.is_finite()),
"non-finite sample at key {key}, {sr} Hz"
);
assert!(
rms(&left(&out)) > 1e-4,
"silent render at key {key}, {sr} Hz"
);
}
}
}
fn pitch_spread(sig: &[f32], sr: f32, f_lo: f32, f_hi: f32) -> f32 {
let w = (0.08 * sr) as usize;
let hop = (0.02 * sr) as usize;
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
let mut i = 0;
while i + w <= sig.len() {
let p = crate::testutil::peak_locate(&sig[i..i + w], sr, f_lo, f_hi);
lo = lo.min(p);
hi = hi.max(p);
i += hop;
}
hi - lo
}
fn render_sawstack_with_mod(prog: u8, mod_val: Option<u8>) -> Vec<f32> {
let mut events = vec![
(0.0, EvKind::Prog { ch: 0, prog }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
];
if let Some(val) = mod_val {
events.push((0.0, EvKind::Cc { ch: 0, num: 1, val }));
}
events.extend([
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(2.4, EvKind::NoteOff { ch: 0, key: 69 }),
]);
left(&render(&test_song(events, 2.5), &test_opts(44100.0)).0)
}
fn render_lead_with_mod(mod_val: u8) -> Vec<f32> {
let song = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 81 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: mod_val,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(2.4, EvKind::NoteOff { ch: 0, key: 69 }),
],
2.5,
);
left(&render(&song, &test_opts(44100.0)).0)
}
#[test]
fn cc1_mod_wheel_adds_vibrato_on_lead() {
let sr = 44100.0;
let plain = render_lead_with_mod(0);
let modded = render_lead_with_mod(127);
let (a, b) = ((0.6 * sr) as usize, (2.2 * sr) as usize);
let spread_plain = pitch_spread(&plain[a..b], sr, 400.0, 480.0);
let spread_mod = pitch_spread(&modded[a..b], sr, 400.0, 480.0);
assert!(
spread_mod > 12.0,
"lead mod vibrato too shallow: {spread_mod} Hz"
);
assert!(
spread_mod > 2.0 * spread_plain,
"plain {spread_plain} Hz vs mod {spread_mod} Hz"
);
}
fn sawstack_legato_render(prog: u8, cc68: Option<u8>) -> (Vec<f32>, Stats) {
let mut events = vec![(0.0, EvKind::Prog { ch: 0, prog })];
if let Some(val) = cc68 {
events.push((
0.0,
EvKind::Cc {
ch: 0,
num: 68,
val,
},
));
}
events.extend([
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(
0.30,
EvKind::NoteOn {
ch: 0,
key: 64,
vel: 100,
},
),
(1.0, EvKind::NoteOff { ch: 0, key: 64 }),
]);
let (stereo, stats) = render(&test_song(events, 1.2), &test_opts(44100.0));
(left(&stereo), stats)
}
#[test]
fn strings_choir_cc1_vibrato_and_cc68_legato_are_opt_in() {
let sr = 44100.0;
for prog in 48..=54 {
let untouched = render_sawstack_with_mod(prog, None);
let wheel_zero = render_sawstack_with_mod(prog, Some(0));
assert!(
untouched
.iter()
.zip(&wheel_zero)
.all(|(a, b)| a.to_bits() == b.to_bits()),
"program {prog} CC1=0 should not change an otherwise untouched channel"
);
let modded = render_sawstack_with_mod(prog, Some(127));
let (a, b) = ((0.8 * sr) as usize, (2.2 * sr) as usize);
let spread_plain = pitch_spread(&wheel_zero[a..b], sr, 360.0, 520.0);
let spread_mod = pitch_spread(&modded[a..b], sr, 360.0, 520.0);
assert!(
spread_mod > 9.0,
"program {prog} CC1 vibrato too shallow: {spread_mod} Hz"
);
assert!(
spread_mod > 1.5 * spread_plain,
"program {prog} plain {spread_plain} Hz vs mod {spread_mod} Hz"
);
assert_eq!(
sawstack_legato_render(prog, None).1.voices_spawned,
2,
"program {prog} without CC68 must still re-attack"
);
let (slurred, stats) = sawstack_legato_render(prog, Some(127));
assert_eq!(
stats.voices_spawned, 1,
"program {prog} with CC68 should slur into one voice"
);
let f_after = crate::testutil::peak_locate(
&slurred[(0.55 * sr) as usize..(0.9 * sr) as usize],
sr,
300.0,
360.0,
);
let want = key_freq(64);
assert!(
(f_after - want).abs() < 6.0,
"program {prog} slurred pitch {f_after}, want {want}"
);
}
assert_eq!(
sawstack_legato_render(89, Some(127)).1.voices_spawned,
2,
"pads must stay outside the CC68 slur family"
);
}
#[test]
fn default_gm19_cc1_ramps_the_leslie() {
let sr = 44100.0;
let cc = |num: u8, val: u8| EvKind::Cc { ch: 0, num, val };
let out = render_cc1_events(vec![
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(0.0, cc(93, 0)),
(0.0, cc(1, 127)),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
]);
let early = am_rate(&out, sr, 0.15, 1.15);
let late = am_rate(&out, sr, 2.9, 3.9);
assert!(
late > early + 2.5 && late >= 5.5,
"default GM19 CC1 must Leslie-ramp: early {early:.2} Hz late {late:.2} Hz"
);
}
#[test]
fn gm22_cc1_is_harmonica_vibrato_not_leslie() {
let sr = 44100.0;
let gm19 = render_cc1_program(19, Some(127), true);
let gm19_early = am_rate(&gm19, sr, 0.15, 1.15);
let gm19_late = am_rate(&gm19, sr, 2.9, 3.9);
assert!(
gm19_late > gm19_early + 2.5 && gm19_late >= 5.5,
"GM19 Leslie ramp regressed: early {gm19_early:.2} Hz late {gm19_late:.2} Hz"
);
let gm22_plain = render_cc1_program(22, None, true);
let gm22_mod = render_cc1_program(22, Some(127), true);
let (a, b) = ((0.8 * sr) as usize, (2.2 * sr) as usize);
let plain_spread = cycle_freq_spread(&gm22_plain[a..b], sr);
let mod_spread = cycle_freq_spread(&gm22_mod[a..b], sr);
assert!(
mod_spread > 8.0 && mod_spread >= 2.0 * plain_spread.max(1.0),
"GM22 CC1 should be pitch vibrato, not inert: plain {plain_spread:.2} Hz mod {mod_spread:.2} Hz"
);
assert!(
!organ_leslie_family(22, 0),
"GM22 must stay out of the Leslie controller branch"
);
for program in [20u8, 21, 23] {
assert!(
!organ_leslie_family(program, 0),
"GM{program} must stay out of the Leslie controller branch"
);
let plain = render_cc1_program(program, None, true);
let modded = render_cc1_program(program, Some(127), true);
assert_eq!(modded, plain, "GM{program} CC1 must be inert");
}
let gm19_then_22 = render_cc1_events(vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 127,
},
),
(
0.019,
EvKind::Cc {
ch: 0,
num: 0,
val: 0,
},
),
(0.02, EvKind::Prog { ch: 0, prog: 22 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(3.85, EvKind::NoteOff { ch: 0, key: 69 }),
]);
let changed_spread = cycle_freq_spread(&gm19_then_22[a..b], sr);
assert!(
changed_spread > 8.0,
"program change 19->22 lost GM22 CC1 vibrato: {changed_spread:.2} Hz"
);
let gm22_then_19 = render_cc1_events(vec![
(0.0, EvKind::Prog { ch: 0, prog: 22 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 127,
},
),
(
0.019,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.02, EvKind::Prog { ch: 0, prog: 19 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(3.85, EvKind::NoteOff { ch: 0, key: 69 }),
]);
let back_early = am_rate(&gm22_then_19, sr, 0.15, 1.15);
let back_late = am_rate(&gm22_then_19, sr, 2.9, 3.9);
assert!(
back_late > back_early + 2.5 && back_late >= 5.5,
"program change 22->19 lost Leslie ramp: early {back_early:.2} late {back_late:.2}"
);
let held_gm19_after_prog22 = render_cc1_events(vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 127,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(
0.59,
EvKind::Cc {
ch: 0,
num: 0,
val: 0,
},
),
(0.60, EvKind::Prog { ch: 0, prog: 22 }),
(3.85, EvKind::NoteOff { ch: 0, key: 69 }),
]);
let held19_early = am_rate(&held_gm19_after_prog22, sr, 0.15, 1.15);
let held19_late = am_rate(&held_gm19_after_prog22, sr, 2.9, 3.9);
let held19_pitch_spread =
cycle_freq_spread_f0(&held_gm19_after_prog22[a..b], sr, key_freq(69));
println!(
"gm22_cc1: held19_pitch_spread {held19_pitch_spread:.2} Hz (f0-filtered), GM22 mod_spread {mod_spread:.2} Hz plain_spread {plain_spread:.2} Hz"
);
assert!(
held19_late > held19_early + 2.5 && held19_late >= 5.5,
"held GM19 lost Leslie after program change to GM22: early {held19_early:.2} late {held19_late:.2}"
);
assert!(
held19_pitch_spread < 4.0,
"held GM19 picked up GM22 pitch vibrato after program change: {held19_pitch_spread:.2} Hz"
);
let held_gm22_after_prog19 = render_cc1_events(vec![
(0.0, EvKind::Prog { ch: 0, prog: 22 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 127,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(
0.59,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.60, EvKind::Prog { ch: 0, prog: 19 }),
(3.85, EvKind::NoteOff { ch: 0, key: 69 }),
]);
let held22_pitch_spread = cycle_freq_spread(&held_gm22_after_prog19[a..b], sr);
assert!(
held22_pitch_spread > 8.0,
"held GM22 lost harmonica vibrato after program change to GM19: {held22_pitch_spread:.2} Hz"
);
let held_plain = render_cc1_program(22, None, false);
let held_mod = render_cc1_program(22, Some(127), false);
let held_plain_spread = cycle_freq_spread(&held_plain[(1.0 * sr) as usize..b], sr);
let held_mod_spread = cycle_freq_spread(&held_mod[(1.0 * sr) as usize..b], sr);
assert!(
held_mod_spread > held_plain_spread + 6.0,
"held GM22 note did not pick up CC1 vibrato: plain {held_plain_spread:.2} mod {held_mod_spread:.2}"
);
let reset = render_cc1_events(vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 127,
},
),
(
0.02,
EvKind::Cc {
ch: 0,
num: 121,
val: 0,
},
),
(
0.03,
EvKind::Cc {
ch: 0,
num: 0,
val: 0,
},
),
(0.04, EvKind::Prog { ch: 0, prog: 22 }),
(
0.06,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(3.85, EvKind::NoteOff { ch: 0, key: 69 }),
]);
let reset_early = am_rate(&reset, sr, 0.15, 1.15);
let reset_late = am_rate(&reset, sr, 2.9, 3.9);
let reset_spread = cycle_freq_spread(&reset[a..b], sr);
let plain_early = am_rate(&gm22_plain, sr, 0.15, 1.15);
let plain_late = am_rate(&gm22_plain, sr, 2.9, 3.9);
assert!(
(reset_early - plain_early).abs() <= 1.0
&& (reset_late - plain_late).abs() <= 1.0
&& (reset_spread - plain_spread).abs() <= 1.0
&& reset_spread < mod_spread * 0.6,
"GM reset leaked CC1 into GM22: AM {reset_early:.2}->{reset_late:.2} vs \
never-modulated control {plain_early:.2}->{plain_late:.2}; spread \
{reset_spread:.2} vs control {plain_spread:.2} / modulated {mod_spread:.2}"
);
let alt_plain = render_cc1_events(vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 22 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(3.85, EvKind::NoteOff { ch: 0, key: 69 }),
]);
let alt_mod = render_cc1_events(vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 22 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 127,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(3.85, EvKind::NoteOff { ch: 0, key: 69 }),
]);
assert_eq!(
alt_mod, alt_plain,
"alt-bank GM22 must not take default-bank CC1 vibrato"
);
}
#[test]
fn cc1_leslie_spins_up_with_inertia() {
let sr = 44100.0;
let song = test_song(
vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 127,
},
),
(
0.02,
EvKind::NoteOn {
ch: 0,
key: 57,
vel: 100,
},
),
(3.95, EvKind::NoteOff { ch: 0, key: 57 }),
],
4.0,
);
let mono = left(&render(&song, &test_opts(sr)).0);
let early = am_rate(&mono, sr, 0.15, 1.15);
let late = am_rate(&mono, sr, 2.9, 3.9);
println!("cc1_leslie_spins_up_with_inertia: early {early:.2} Hz late {late:.2} Hz");
assert!(
late > early + 3.0,
"sweep too narrow: early {early} Hz, late {late} Hz"
);
assert!(
early < 3.5,
"rotor started too fast (should brake to slow): {early} Hz"
);
assert!(late >= 5.5, "rotor never got fast: {late} Hz");
}
#[test]
fn leslie_and_vibrato_measures_still_catch_defects() {
let sr = 44100.0;
let stalled = render_cc1_events(vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.02,
EvKind::NoteOn {
ch: 0,
key: 57,
vel: 100,
},
),
(3.95, EvKind::NoteOff { ch: 0, key: 57 }),
]);
let early = am_rate(&stalled, sr, 0.15, 1.15);
let late = am_rate(&stalled, sr, 2.9, 3.9);
assert!(
!(late > early + 3.0 && late >= 5.5),
"stalled Leslie would falsely pass cc1_leslie — the measurement masks a stuck rotor: early {early:.2} late {late:.2}"
);
let (a, b) = ((0.8 * sr) as usize, (2.2 * sr) as usize);
let gm22_mod = render_cc1_program(22, Some(127), true);
let gm22_f0 = cycle_freq_spread_f0(&gm22_mod[a..b], sr, key_freq(69));
assert!(
gm22_f0 > 8.0,
"f0-band-limit nulled GM22's real pitch vibrato: {gm22_f0:.2} Hz"
);
}
fn render_pluck_with_cc74(cc74: Option<u8>) -> Vec<f32> {
let mut events = vec![
(0.0, EvKind::Prog { ch: 0, prog: 25 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 94,
val: 0,
},
),
];
if let Some(val) = cc74 {
events.push((
0.0,
EvKind::Cc {
ch: 0,
num: 74,
val,
},
));
}
events.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 64,
vel: 110,
},
));
events.push((1.5, EvKind::NoteOff { ch: 0, key: 64 }));
render(&test_song(events, 1.6), &test_opts(44100.0)).0
}
#[test]
fn cc74_brightness_filter() {
let sr = 44100.0;
let dark = left(&render_pluck_with_cc74(Some(20)));
let open = render_pluck_with_cc74(Some(127));
let none = render_pluck_with_cc74(None);
assert!(open == none, "CC74=127 is not a true bypass");
let hf_frac = |sig: &[f32]| {
let mut hp = Biquad::highpass(3000.0, 0.7, sr);
let (mut hf, mut total) = (0.0f64, 0.0f64);
for &x in sig {
let y = hp.process(x);
hf += (y * y) as f64;
total += (x * x) as f64;
}
hf / total.max(1e-12)
};
let f_dark = hf_frac(&dark);
let f_open = hf_frac(&left(&open));
assert!(
f_dark < 0.25 * f_open,
"no darkening: dark {f_dark} vs open {f_open}"
);
}
#[test]
fn cc64_sustain_pedal_holds_notes() {
let sr = 44100.0;
let events = |pedal: bool| {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog: 19 })];
if pedal {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 64,
val: 127,
},
));
}
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((0.5, EvKind::NoteOff { ch: 0, key: 60 }));
if pedal {
ev.push((
2.0,
EvKind::Cc {
ch: 0,
num: 64,
val: 0,
},
));
}
ev
};
let held = left(&render(&test_song(events(true), 3.2), &test_opts(sr)).0);
let plain = left(&render(&test_song(events(false), 3.2), &test_opts(sr)).0);
let w = |sig: &[f32], t0: f32, t1: f32| rms(&sig[(t0 * sr) as usize..(t1 * sr) as usize]);
let held_mid = w(&held, 1.4, 1.9);
let plain_mid = w(&plain, 1.4, 1.9);
assert!(
held_mid > 10.0 * plain_mid.max(1e-9),
"pedal didn't hold: {held_mid} vs {plain_mid}"
);
let after = w(&held, 3.0, 3.4);
assert!(
after < 0.05 * held_mid,
"pedal-up didn't release: {after} vs {held_mid}"
);
}
#[test]
fn solo_mutes_other_channels() {
let sr = 44100.0;
let song = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 25 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(
0.06,
EvKind::NoteOn {
ch: 2,
key: 67,
vel: 100,
},
),
(0.5, EvKind::NoteOff { ch: 0, key: 60 }),
(0.5, EvKind::NoteOff { ch: 2, key: 67 }),
],
1.0,
);
let mut opt = test_opts(sr);
opt.solo = 1 << 5; let (out, stats) = render(&song, &opt);
assert_eq!(stats.voices_spawned, 0);
assert!(
out.iter().all(|&x| x == 0.0),
"solo of an empty channel must be silent"
);
opt.solo = 1 << 0;
let (out2, stats2) = render(&song, &opt);
assert_eq!(stats2.voices_spawned, 1);
assert!(out2.iter().any(|&x| x != 0.0));
}
fn mean_freq(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 = 0;
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)
}
fn energy_above(sig: &[f32], hz: f32, sr: f32) -> f64 {
let mut hp = Biquad::highpass(hz, 0.7, sr);
let (mut hi, mut total) = (0.0f64, 0.0f64);
for &x in sig {
let y = hp.process(x);
hi += (y * y) as f64;
total += (x * x) as f64;
}
hi / total.max(1e-12)
}
fn band_frac(sig: &[f32], center: f32, q: f32, sr: f32) -> f64 {
let mut bp = Biquad::bandpass(center, q, sr);
let (mut band, mut total) = (0.0f64, 0.0f64);
for &x in sig {
let y = bp.process(x);
band += (y * y) as f64;
total += (x * x) as f64;
}
band / total.max(1e-12)
}
fn render_program_vowel(program: u8, cc70: u8) -> Vec<f32> {
let song = test_song(
vec![
(
0.0,
EvKind::Prog {
ch: 0,
prog: program,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
), (
0.0,
EvKind::Cc {
ch: 0,
num: 70,
val: cc70,
},
),
(
0.02,
EvKind::NoteOn {
ch: 0,
key: 48,
vel: 90,
},
),
(2.4, EvKind::NoteOff { ch: 0, key: 48 }),
],
2.5,
);
left(&render(&song, &test_opts(44100.0)).0)
}
fn render_program_late_vowel(program: u8, cc70: Option<u8>) -> Vec<f32> {
let mut events = vec![
(
0.0,
EvKind::Prog {
ch: 0,
prog: program,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.02,
EvKind::NoteOn {
ch: 0,
key: 48,
vel: 90,
},
),
];
if let Some(val) = cc70 {
events.push((
0.70,
EvKind::Cc {
ch: 0,
num: 70,
val,
},
));
}
events.push((2.4, EvKind::NoteOff { ch: 0, key: 48 }));
left(&render(&test_song(events, 2.5), &test_opts(44100.0)).0)
}
fn render_choir_vowel(cc70: u8) -> Vec<f32> {
render_program_vowel(52, cc70)
}
#[test]
fn cc70_vowel_morph_opens_formants() {
let sr = 44100.0;
let mm = render_choir_vowel(0);
let ah = render_choir_vowel(84);
let (a, b) = ((1.0 * sr) as usize, (2.0 * sr) as usize);
let f_mm = energy_above(&mm[a..b], 1500.0, sr);
let f_ah = energy_above(&ah[a..b], 1500.0, sr);
assert!(
f_ah > 1.5 * f_mm,
"vowel didn't open: mm {f_mm} vs ah {f_ah}"
);
}
#[test]
fn choir_pad_91_cc70_vowel_morph_opens_formants() {
let sr = 44100.0;
let mm = render_program_vowel(91, 0);
let ah = render_program_vowel(91, 84);
let (a, b) = ((1.0 * sr) as usize, (2.0 * sr) as usize);
let f_mm = energy_above(&mm[a..b], 1500.0, sr);
let f_ah = energy_above(&ah[a..b], 1500.0, sr);
assert!(
f_ah > 1.5 * f_mm,
"choir-pad vowel didn't open: mm {f_mm} vs ah {f_ah}"
);
let plain = render_program_late_vowel(91, None);
let late = render_program_late_vowel(91, Some(0));
let (pre_a, pre_b) = ((0.2 * sr) as usize, (0.55 * sr) as usize);
assert!(
plain[pre_a..pre_b]
.iter()
.zip(&late[pre_a..pre_b])
.all(|(a, b)| a.to_bits() == b.to_bits()),
"future CC70 changed program 91 before the controller was authored"
);
let (a, b) = ((1.4 * sr) as usize, (2.2 * sr) as usize);
let f_plain = energy_above(&plain[a..b], 1500.0, sr);
let f_late = energy_above(&late[a..b], 1500.0, sr);
assert!(
f_late < 0.8 * f_plain,
"late-authored closed vowel didn't retarget the default open choir formant: plain {f_plain} vs mm {f_late}"
);
}
#[test]
fn rpn_bend_range_and_fine_tune() {
let sr = 44100.0;
let cc = |num, val| EvKind::Cc { ch: 0, num, val };
let render_freq = |setup: Vec<(f64, EvKind)>, win: (f32, f32), band: (f32, f32)| -> f32 {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 25 }),
(0.0, cc(93, 0)),
(0.0, cc(94, 0)),
];
ev.extend(setup);
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 110,
},
));
ev.push((2.5, EvKind::NoteOff { ch: 0, key: 69 }));
let mono = left(&render(&test_song(ev, 2.6), &test_opts(sr)).0);
let seg = &mono[(win.0 * sr) as usize..(win.1 * sr) as usize];
let c = crate::testutil::peak_locate(seg, sr, band.0, band.1);
let lm = crate::testutil::mag_at(seg, sr, c / 1.005);
let cm = crate::testutil::mag_at(seg, sr, c);
let hm = crate::testutil::mag_at(seg, sr, c * 1.005);
let denom = lm - 2.0 * cm + hm;
let delta = if denom.abs() > 1e-9 {
(0.5 * (lm - hm) / denom).clamp(-1.0, 1.0)
} else {
0.0
};
c * 1.005f32.powf(delta)
};
let win = (0.25, 0.7);
let narrow = render_freq(
vec![(0.04, EvKind::Bend { ch: 0, semis: 1.0 })],
win,
(430.0, 510.0),
);
let wide = render_freq(
vec![
(0.01, cc(101, 0)),
(0.01, cc(100, 0)),
(0.01, cc(6, 12)),
(0.04, EvKind::Bend { ch: 0, semis: 1.0 }),
],
win,
(560.0, 690.0),
);
assert!((narrow - 466.2).abs() < 12.0, "narrow bend: {narrow} Hz");
assert!((wide - 622.3).abs() < 20.0, "wide bend: {wide} Hz");
let plain = render_freq(vec![], win, (410.0, 475.0));
let fine = render_freq(
vec![(0.01, cc(101, 0)), (0.01, cc(100, 1)), (0.01, cc(6, 96))],
win,
(410.0, 475.0),
);
let ratio = fine / plain;
assert!(
(ratio - 2f32.powf(50.0 / 1200.0)).abs() < 0.008,
"fine-tune ratio {ratio} (plain {plain} -> fine {fine})"
);
}
#[test]
fn modal_organ_pitch_controls_move_sustained_notes() {
let sr = 44100.0;
let cc = |num, val| EvKind::Cc { ch: 0, num, val };
let cases = [(11u8, "modal vibraphone"), (19u8, "organ")];
let render_case =
|prog: u8, mut ev: Vec<(f64, EvKind)>, seconds: f64| -> (Vec<f32>, Stats) {
let mut base = vec![
(0.0, EvKind::Prog { ch: 0, prog }),
(0.0, cc(91, 0)),
(0.0, cc(93, 0)),
(0.0, cc(94, 0)),
];
base.append(&mut ev);
let (stereo, stats) = render(&test_song(base, seconds), &test_opts(sr));
(left(&stereo), stats)
};
let peak = |sig: &[f32], t0: f32, t1: f32, lo: f32, hi: f32| {
crate::testutil::peak_locate(&sig[(t0 * sr) as usize..(t1 * sr) as usize], sr, lo, hi)
};
for (prog, name) in cases {
let (bent, _) = render_case(
prog,
vec![
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 110,
},
),
(1.0, EvKind::Bend { ch: 0, semis: 1.0 }),
(2.45, EvKind::NoteOff { ch: 0, key: 69 }),
],
2.7,
);
let pre = peak(&bent, 0.35, 0.80, 420.0, 460.0);
let post = peak(&bent, 1.45, 2.10, 445.0, 490.0);
assert!((pre - 440.0).abs() < 8.0, "{name}: pre-bend pitch {pre}");
assert!(
(post - 466.2).abs() < 10.0,
"{name}: pitch bend stayed inert, got {post} Hz"
);
let (wide, _) = render_case(
prog,
vec![
(0.01, cc(101, 0)),
(0.01, cc(100, 0)),
(0.01, cc(6, 12)),
(0.04, EvKind::Bend { ch: 0, semis: 1.0 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 110,
},
),
(1.2, EvKind::NoteOff { ch: 0, key: 69 }),
],
1.5,
);
let wide_pitch = peak(&wide, 0.35, 0.90, 585.0, 660.0);
assert!(
(wide_pitch - 622.3).abs() < 18.0,
"{name}: RPN bend range stayed inert, got {wide_pitch} Hz"
);
let (plain, _) = render_case(
prog,
vec![
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 110,
},
),
(1.2, EvKind::NoteOff { ch: 0, key: 69 }),
],
1.5,
);
let (fine, _) = render_case(
prog,
vec![
(0.01, cc(101, 0)),
(0.01, cc(100, 1)),
(0.01, cc(6, 96)),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 110,
},
),
(1.2, EvKind::NoteOff { ch: 0, key: 69 }),
],
1.5,
);
let plain_pitch = peak(&plain, 0.35, 0.90, 420.0, 460.0);
let fine_pitch = peak(&fine, 0.35, 0.90, 440.0, 475.0);
let want_ratio = 2f32.powf(50.0 / 1200.0);
let ratio = fine_pitch / plain_pitch;
assert!(
(ratio - want_ratio).abs() < 0.012,
"{name}: RPN fine tune stayed inert, ratio {ratio}"
);
let (glide, stats) = render_case(
prog,
vec![
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(0.45, EvKind::NoteOff { ch: 0, key: 69 }),
(1.0, cc(65, 127)),
(1.0, cc(5, 109)),
(
2.0,
EvKind::NoteOn {
ch: 0,
key: 81,
vel: 110,
},
),
(3.7, EvKind::NoteOff { ch: 0, key: 81 }),
],
4.0,
);
assert_eq!(
stats.voices_spawned, 2,
"{name}: portamento should spawn a new voice"
);
let early = peak(&glide, 2.06, 2.18, 405.0, 485.0);
let late = peak(&glide, 3.25, 3.60, 820.0, 940.0);
assert!(
early < 500.0,
"{name}: glide did not start near old pitch: {early}"
);
assert!(
(late - 880.0).abs() < 35.0,
"{name}: glide did not settle to target: {late}"
);
let (pressed, _) = render_case(
prog,
vec![
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(0.10, EvKind::Aftertouch { ch: 0, val: 127 }),
(2.5, EvKind::NoteOff { ch: 0, key: 69 }),
],
2.7,
);
let (unpressed, _) = render_case(
prog,
vec![
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(2.5, EvKind::NoteOff { ch: 0, key: 69 }),
],
2.7,
);
let a = (1.25 * sr) as usize;
let b = (2.30 * sr) as usize;
let plain_spread = pitch_spread(&unpressed[a..b], sr, 405.0, 485.0);
let pressed_spread = pitch_spread(&pressed[a..b], sr, 405.0, 485.0);
assert!(
pressed_spread > 5.0 && pressed_spread > 1.5 * plain_spread,
"{name}: aftertouch vibrato inert, plain {plain_spread} Hz vs pressed {pressed_spread} Hz"
);
}
}
#[test]
fn portamento_glides_from_previous_pitch() {
let sr = 44100.0;
let cc = |num, val| EvKind::Cc { ch: 0, num, val };
let song = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 25 }),
(0.0, cc(93, 0)),
(0.0, cc(94, 0)),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
), (0.5, EvKind::NoteOff { ch: 0, key: 69 }),
(1.0, cc(65, 127)), (1.0, cc(5, 109)), (
2.5,
EvKind::NoteOn {
ch: 0,
key: 57,
vel: 110,
},
), (4.2, EvKind::NoteOff { ch: 0, key: 57 }),
],
4.5,
);
let (stereo, stats) = render(&song, &test_opts(sr));
assert_eq!(stats.voices_spawned, 2);
let mono = left(&stereo);
let win = |t0: f32, t1: f32| mean_freq(&mono[(t0 * sr) as usize..(t1 * sr) as usize], sr);
let early = win(2.55, 2.68); let late = crate::testutil::peak_locate(
&mono[(3.8 * sr) as usize..(4.1 * sr) as usize],
sr,
195.0,
245.0,
);
assert!(early > 320.0, "glide didn't start high: {early} Hz");
assert!(
(late - 220.0).abs() < 12.0,
"glide didn't reach target: {late} Hz"
);
}
fn render_pluck_resonance(cc71: u8) -> Vec<f32> {
let song = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 25 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 94,
val: 0,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 74,
val: 41,
},
), (
0.0,
EvKind::Cc {
ch: 0,
num: 71,
val: cc71,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 64,
vel: 110,
},
), (1.5, EvKind::NoteOff { ch: 0, key: 64 }),
],
1.6,
);
left(&render(&song, &test_opts(44100.0)).0)
}
#[test]
fn cc71_resonance_builds_a_peak() {
let sr = 44100.0;
let flat = render_pluck_resonance(0); let peaky = render_pluck_resonance(127); let (a, b) = ((0.3 * sr) as usize, (1.4 * sr) as usize);
let e_flat = band_frac(&flat[a..b], 990.0, 4.0, sr);
let e_peaky = band_frac(&peaky[a..b], 990.0, 4.0, sr);
assert!(
e_peaky > 1.5 * e_flat,
"resonance built no peak: flat {e_flat} vs peaky {e_peaky}"
);
}
fn render_bowed_aftertouch(at: Option<u8>) -> Vec<f32> {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 40 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 90,
},
),
];
if let Some(v) = at {
ev.push((0.1, EvKind::Aftertouch { ch: 0, val: v })); }
ev.push((2.4, EvKind::NoteOff { ch: 0, key: 69 }));
left(&render(&test_song(ev, 2.5), &test_opts(44100.0)).0)
}
#[test]
fn aftertouch_swells_gain_and_vibrato() {
let sr = 44100.0;
let plain = render_bowed_aftertouch(None);
let pressed = render_bowed_aftertouch(Some(127));
let (a, b) = ((1.5 * sr) as usize, (2.2 * sr) as usize);
let ratio = rms(&pressed[a..b]) / rms(&plain[a..b]).max(1e-9);
assert!(
(1.15..1.6).contains(&ratio),
"aftertouch gain bloom off: {ratio}x"
);
let sp_plain = cycle_freq_spread(&plain[a..b], sr);
let sp_press = cycle_freq_spread(&pressed[a..b], sr);
assert!(
sp_press > sp_plain + 3.0,
"no aftertouch vibrato: plain {sp_plain} Hz vs pressed {sp_press} Hz"
);
}
fn render_poly_at_chord(poly: bool) -> Vec<f32> {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 40 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 90,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 76,
vel: 90,
},
),
];
if poly {
ev.push((
0.1,
EvKind::PolyAftertouch {
ch: 0,
key: 69,
val: 127,
},
));
}
ev.push((2.4, EvKind::NoteOff { ch: 0, key: 69 }));
ev.push((2.4, EvKind::NoteOff { ch: 0, key: 76 }));
left(&render(&test_song(ev, 2.5), &test_opts(44100.0)).0)
}
#[test]
fn sampled_ride_does_not_machine_gun() {
let sr = 44100.0;
let opts = Options {
samples: true,
..test_opts(sr)
};
if !crate::embedded_samples_available() {
return; }
let gap = 1.6f64;
let mut ev = Vec::new();
for i in 0..5u32 {
let t = 0.05 + gap * i as f64;
ev.push((
t,
EvKind::NoteOn {
ch: 9,
key: 51,
vel: 100,
},
));
ev.push((t + gap - 0.1, EvKind::NoteOff { ch: 9, key: 51 }));
}
let mono = left(&render(&test_song(ev, 0.05 + gap * 5.0), &opts).0);
let hit = |i: usize| -> &[f32] {
let a = ((0.05 + gap as f32 * i as f32) * sr) as usize;
&mono[a..a + (0.06 * sr) as usize]
};
let rms = |x: &[f32]| (x.iter().map(|v| v * v).sum::<f32>() / x.len() as f32).sqrt();
for i in 0..5 {
assert!(
rms(hit(i)) > 1e-3,
"ride hit {i} is silent — sampled cymbal not routed?"
);
}
let h0 = hit(0);
let h4 = hit(4);
let diff: Vec<f32> = h0.iter().zip(h4).map(|(a, b)| a - b).collect();
let ratio = rms(&diff) / rms(h0).max(1e-9);
assert!(
ratio > 0.5,
"same-round-robin ride hits are nearly identical (diff/rms = {ratio:.3}) — \
MACHINE-GUN: the micro-variation is not decorrelating repeats"
);
}
#[test]
fn poly_aftertouch_targets_only_the_pressed_note() {
let sr = 44100.0;
let plain = render_poly_at_chord(false);
let pressed = render_poly_at_chord(true);
let (a, b) = ((1.5 * sr) as usize, (2.2 * sr) as usize);
let sp_a_plain = pitch_spread(&plain[a..b], sr, 405.0, 485.0);
let sp_a_press = pitch_spread(&pressed[a..b], sr, 405.0, 485.0);
assert!(
sp_a_press > 5.0 && sp_a_press > 1.5 * sp_a_plain,
"poly AT vibrato inert on the pressed note: plain {sp_a_plain} Hz vs pressed {sp_a_press} Hz"
);
let sp_e_plain = pitch_spread(&plain[a..b], sr, 610.0, 710.0);
let sp_e_press = pitch_spread(&pressed[a..b], sr, 610.0, 710.0);
assert!(
sp_e_press < 5.0f32.max(1.5 * sp_e_plain),
"poly AT leaked onto the chord-mate: plain {sp_e_plain} Hz vs pressed {sp_e_press} Hz"
);
}
fn render_flute_breath(cc2: Option<u8>) -> Vec<f32> {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 73 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
];
if let Some(v) = cc2 {
ev.push((
0.5,
EvKind::Cc {
ch: 0,
num: 2,
val: v,
},
)); }
ev.push((2.4, EvKind::NoteOff { ch: 0, key: 69 }));
left(&render(&test_song(ev, 2.5), &test_opts(44100.0)).0)
}
#[test]
fn cc2_breath_scales_wind_level() {
let sr = 44100.0;
let plain = render_flute_breath(None);
let soft = render_flute_breath(Some(40));
let (a, b) = ((1.5 * sr) as usize, (2.2 * sr) as usize);
let ratio = rms(&soft[a..b]) / rms(&plain[a..b]).max(1e-9);
assert!(
(0.01..0.4).contains(&ratio),
"CC2 breath backoff inert or overdone: {ratio}x"
);
}
#[test]
fn breath_and_poly_at_neutral_until_authored() {
let plain = render_flute_breath(None);
let full = render_flute_breath(Some(127));
assert_eq!(plain, full, "CC2=127 must be a bit-exact no-op");
let base = |extra: Option<EvKind>| {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 40 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 90,
},
),
];
if let Some(e) = extra {
ev.push((0.1, e));
}
ev.push((2.0, EvKind::NoteOff { ch: 0, key: 69 }));
left(&render(&test_song(ev, 2.2), &test_opts(44100.0)).0)
};
let untouched = base(None);
let missed = base(Some(EvKind::PolyAftertouch {
ch: 0,
key: 60, val: 127,
}));
assert_eq!(
untouched, missed,
"poly AT on a silent key must be a bit-exact no-op"
);
}
#[test]
fn cc66_sostenuto_holds_only_captured_notes() {
let sr = 44100.0;
let cc = |num, val| EvKind::Cc { ch: 0, num, val };
let captured = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(0.3, cc(66, 127)), (0.5, EvKind::NoteOff { ch: 0, key: 60 }), (2.0, cc(66, 0)), ],
3.5,
);
let after = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(0.05, cc(66, 127)), (
0.3,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(0.5, EvKind::NoteOff { ch: 0, key: 60 }), (2.5, cc(66, 0)),
],
3.5,
);
let cap = left(&render(&captured, &test_opts(sr)).0);
let aft = left(&render(&after, &test_opts(sr)).0);
let w = |sig: &[f32], t0: f32, t1: f32| rms(&sig[(t0 * sr) as usize..(t1 * sr) as usize]);
let cap_mid = w(&cap, 1.4, 1.9);
let aft_mid = w(&aft, 1.4, 1.9);
assert!(
cap_mid > 10.0 * aft_mid.max(1e-9),
"sostenuto didn't select: captured {cap_mid} vs after {aft_mid}"
);
let cap_after = w(&cap, 3.0, 3.4);
assert!(
cap_after < 0.05 * cap_mid,
"pedal-up didn't release: {cap_after} vs {cap_mid}"
);
}
fn dry_core() -> EngineCore {
EngineCore::new(CoreOptions::from_options(
&test_opts(44100.0),
false,
false,
false,
))
}
fn unreleased_driven(core: &EngineCore, ch: u8) -> usize {
core.active
.iter()
.filter(|a| a.ch == ch && needs_drive(a.program) && !a.voice.released())
.count()
}
#[test]
fn offline_polyphony_is_unbounded() {
let mut core = dry_core();
let mut spawned = 0usize;
'outer: for ch in 0u8..9 {
for key in 21u8..=108 {
core.handle_event(EvKind::NoteOn { ch, key, vel: 80 });
spawned += 1;
if spawned >= 300 {
break 'outer;
}
}
}
assert_eq!(
core.active_voice_count(),
spawned,
"offline polyphony was capped: {} of {spawned} voices survived",
core.active_voice_count()
);
}
#[test]
fn option_builders_clamp_every_knob_into_its_supported_range() {
let d = Options::default();
for rate in [0u32, 1, 7_999, u32::MAX, MAX_SAMPLE_RATE + 1] {
let got = Options::default().with_sample_rate(rate).sample_rate();
assert!(
(MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&got),
"rate {rate} survived as {got}"
);
}
assert_eq!(
Options::default().with_sample_rate(0).sample_rate(),
MIN_SAMPLE_RATE
);
assert_eq!(
Options::default().with_sample_rate(u32::MAX).sample_rate(),
MAX_SAMPLE_RATE
);
assert_eq!(Options::default().with_echo(100_000.0).echo(), MAX_ECHO_S);
assert_eq!(Options::default().with_echo(f32::MAX).echo(), MAX_ECHO_S);
assert_eq!(Options::default().with_echo(-1.0).echo(), 0.0);
assert_eq!(Options::default().with_tail(f32::MAX).tail(), MAX_TAIL_S);
assert_eq!(Options::default().with_tail(-5.0).tail(), 0.0);
assert_eq!(Options::default().with_reverb(9.0).reverb(), 1.0);
assert_eq!(Options::default().with_reverb(-9.0).reverb(), 0.0);
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert_eq!(Options::default().with_echo(bad).echo(), d.delay_s, "{bad}");
assert_eq!(Options::default().with_tail(bad).tail(), d.tail, "{bad}");
assert_eq!(Options::default().with_reverb(bad).reverb(), d.wet, "{bad}");
}
}
#[test]
fn option_builders_leave_shipping_values_untouched() {
for rate in [44_100u32, 48_000, 88_200, 96_000, 192_000] {
assert_eq!(
Options::default().with_sample_rate(rate).sample_rate(),
rate
);
}
for echo in [0.0f32, 0.20, 0.375, 0.62, 1.0] {
assert_eq!(Options::default().with_echo(echo).echo(), echo);
}
for tail in [0.0f32, 0.1, 0.2, 0.5, 1.0, 6.0, 8.0] {
assert_eq!(Options::default().with_tail(tail).tail(), tail);
}
for wet in [0.0f32, 0.25, 0.32, 1.0] {
assert_eq!(Options::default().with_reverb(wet).reverb(), wet);
}
}
#[test]
fn enforce_voice_cap_matches_the_quadratic_reference() {
fn reference(mut voices: Vec<(u8, bool)>, cap: usize) -> Vec<u8> {
while voices.len() > cap {
let victim = voices
.iter()
.position(|(_, released)| *released)
.unwrap_or(0);
voices.remove(victim);
}
voices.into_iter().map(|(key, _)| key).collect()
}
let mut checked = 0usize;
for population in [1usize, 2, 5, 12, 20] {
for pattern in 0u8..12 {
for cap in 0..=population {
let mut core = dry_core();
let keys: Vec<u8> = (0..population as u8).map(|i| 60 + i).collect();
for &key in &keys {
core.handle_event(EvKind::NoteOn {
ch: 0,
key,
vel: 100,
});
}
for (i, &key) in keys.iter().enumerate() {
let release = match pattern {
0 => false,
1 => true,
2 => i % 2 == 0,
3 => i % 2 == 1,
4 => i < population / 2,
5 => i >= population / 2,
6 => i % 3 == 0,
7 => i % 3 == 1,
8 => i == 0,
9 => i + 1 == population,
10 => i > 0 && i + 1 < population,
_ => (i * 7 + population) % 5 < 2,
};
if release {
core.handle_event(EvKind::NoteOff { ch: 0, key });
}
}
let before: Vec<(u8, bool)> = core
.active
.iter()
.map(|a| (a.key, a.voice.released()))
.collect();
assert_eq!(before.len(), population, "note-off removed a voice");
let expected = reference(before, cap);
core.enforce_voice_cap(cap);
let got: Vec<u8> = core.active.iter().map(|a| a.key).collect();
assert_eq!(
got, expected,
"population={population} pattern={pattern} cap={cap}"
);
checked += 1;
}
}
}
assert_eq!(checked, 540, "the configuration sweep shrank");
}
#[test]
fn enforce_voice_cap_steals_oldest_released_first() {
let mut core = dry_core();
for key in 60u8..66 {
core.handle_event(EvKind::NoteOn {
ch: 0,
key,
vel: 100,
});
}
assert_eq!(core.active_voice_count(), 6);
core.enforce_voice_cap(4);
let keys: Vec<u8> = core.active.iter().map(|a| a.key).collect();
assert_eq!(
keys,
vec![62, 63, 64, 65],
"oldest not stolen first: {keys:?}"
);
core.handle_event(EvKind::NoteOff { ch: 0, key: 65 });
core.enforce_voice_cap(3);
let keys: Vec<u8> = core.active.iter().map(|a| a.key).collect();
assert_eq!(
keys,
vec![62, 63, 64],
"released voice not stolen first: {keys:?}"
);
}
#[test]
fn driven_guitar_voice_limit_releases_oldest_across_programs_per_channel() {
let mut core = dry_core();
core.handle_event(EvKind::Prog { ch: 0, prog: 29 });
for key in 40..44 {
core.handle_event(EvKind::NoteOn {
ch: 0,
key,
vel: 100,
});
}
core.handle_event(EvKind::Prog { ch: 0, prog: 30 });
for key in 44..48 {
core.handle_event(EvKind::NoteOn {
ch: 0,
key,
vel: 100,
});
}
core.handle_event(EvKind::Prog { ch: 1, prog: 29 });
core.handle_event(EvKind::NoteOn {
ch: 1,
key: 52,
vel: 100,
});
assert_eq!(unreleased_driven(&core, 0), DRIVEN_GUITAR_VOICE_LIMIT);
core.handle_event(EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
});
assert_eq!(unreleased_driven(&core, 0), DRIVEN_GUITAR_VOICE_LIMIT);
assert_eq!(unreleased_driven(&core, 1), 1, "other channel was stolen");
let oldest = core
.active
.iter()
.find(|a| a.ch == 0 && a.key == 40)
.unwrap();
assert!(
oldest.voice.released(),
"oldest driven voice was not stolen"
);
let newest = core
.active
.iter()
.find(|a| a.ch == 0 && a.key == 60)
.unwrap();
assert!(!newest.voice.released(), "newest driven voice was stolen");
}
#[test]
fn driven_guitar_voice_limit_overrides_sustain_pedal() {
let mut core = dry_core();
core.handle_event(EvKind::Prog { ch: 0, prog: 29 });
core.handle_event(EvKind::Cc {
ch: 0,
num: 64,
val: 127,
});
for key in 40..48 {
core.handle_event(EvKind::NoteOn {
ch: 0,
key,
vel: 100,
});
core.handle_event(EvKind::NoteOff { ch: 0, key });
}
assert!(core.active.iter().filter(|a| a.ch == 0).all(|a| a.held));
core.handle_event(EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
});
let oldest = core
.active
.iter()
.find(|a| a.ch == 0 && a.key == 40)
.unwrap();
assert!(oldest.voice.released() && !oldest.held);
core.handle_event(EvKind::Cc {
ch: 0,
num: 64,
val: 0,
});
assert_eq!(unreleased_driven(&core, 0), 1);
assert!(core
.active
.iter()
.find(|a| a.ch == 0 && a.key == 60)
.is_some_and(|a| !a.voice.released()));
}
#[test]
fn driven_guitar_voice_limit_overrides_sostenuto_capture() {
let mut core = dry_core();
core.handle_event(EvKind::Prog { ch: 0, prog: 30 });
for key in 40..48 {
core.handle_event(EvKind::NoteOn {
ch: 0,
key,
vel: 100,
});
}
core.handle_event(EvKind::Cc {
ch: 0,
num: 66,
val: 127,
});
for key in 40..48 {
core.handle_event(EvKind::NoteOff { ch: 0, key });
}
assert!(core
.active
.iter()
.filter(|a| a.ch == 0)
.all(|a| a.sost && a.sost_held));
core.handle_event(EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
});
let oldest = core
.active
.iter()
.find(|a| a.ch == 0 && a.key == 40)
.unwrap();
assert!(oldest.voice.released() && !oldest.sost && !oldest.sost_held);
core.handle_event(EvKind::Cc {
ch: 0,
num: 66,
val: 0,
});
assert_eq!(unreleased_driven(&core, 0), 1);
assert!(core
.active
.iter()
.find(|a| a.ch == 0 && a.key == 60)
.is_some_and(|a| !a.voice.released()));
}
fn render_piano_una_corda(soft: bool) -> Vec<f32> {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 0 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
];
if soft {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 67,
val: 127,
},
)); }
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((3.0, EvKind::NoteOff { ch: 0, key: 60 }));
left(&render(&test_song(ev, 3.5), &test_opts(44100.0)).0)
}
#[cfg(feature = "embedded-samples")]
fn render_held_gm0(key: u8, vel: u8, samples: bool) -> Vec<f32> {
let song = test_song(
vec![
(0.0, EvKind::Prog { ch: 0, prog: 0 }),
(0.0, EvKind::NoteOn { ch: 0, key, vel }),
(0.75, EvKind::NoteOff { ch: 0, key }),
],
0.75,
);
let opt = Options {
samples,
tail: 0.0,
..test_opts(44_100.0)
};
left(&render(&song, &opt).0)
}
#[cfg(feature = "embedded-samples")]
fn held_note_rebound_db(signal: &[f32], sr: f32) -> (f32, Vec<f32>) {
let window = (0.050 * sr) as usize;
let levels: Vec<f32> = signal
.chunks_exact(window)
.map(crate::testutil::rms)
.collect();
let rebound_db = (3..9)
.map(|i| {
let later = levels[i + 1..14].iter().copied().fold(0.0f32, f32::max);
20.0 * (later / levels[i].max(1e-12)).log10()
})
.fold(f32::NEG_INFINITY, f32::max);
(rebound_db, levels)
}
#[cfg(feature = "embedded-samples")]
#[test]
fn gm0_model_stays_monotone_and_sample_only_body_survives() {
let sr = 44_100.0;
for vel in [50u8, 100] {
for key in 21u8..=108 {
let model = render_held_gm0(key, vel, false);
let (rebound_db, levels) = held_note_rebound_db(&model, sr);
assert!(
rebound_db <= 1.5,
"GM0 model key {key} vel {vel} rebounds {rebound_db:.2} dB through \
the engine after its 150 ms attack budget (50 ms RMS: {levels:?})"
);
let sampled = render_held_gm0(key, vel, true);
assert!(sampled.iter().all(|value| value.is_finite()));
let late =
crate::testutil::rms(&sampled[(0.550 * sr) as usize..(0.700 * sr) as usize]);
assert!(
late > 1e-5,
"GM0 sample-only key {key} vel {vel} lost its recorded body before 700 ms"
);
}
}
}
#[cfg(feature = "embedded-samples")]
#[test]
fn sample_only_gm0_is_seed_invariant_while_the_model_control_is_not() {
let sr = 44_100.0;
let render = |seed: u32, samples: bool| {
let mut voice = crate::voices::make(0, 72, 100, sr, seed, samples);
let mut out = vec![0.0; (0.700 * sr) as usize];
voice.render(&mut out);
out
};
let seeds = [0x1357_9BDFu32, 0x6A09_E667];
let a = render(seeds[0], true);
let a2 = render(seeds[0], true);
let b = render(seeds[1], true);
assert!(
a.iter().zip(&a2).all(|(x, y)| x.to_bits() == y.to_bits()),
"same engine seed must keep GM0 deterministic"
);
assert!(
a.iter().zip(&b).all(|(x, y)| x.to_bits() == y.to_bits()),
"sample-only GM0 still depends on the model seed"
);
let model_a = render(seeds[0], false);
let model_b = render(seeds[1], false);
let ncc = ncc_max(&model_a, &model_b, 128);
assert!(
ncc < 0.99,
"seed pair does not move the model control (NCC {ncc:.6})"
);
}
fn render_keyboard_una_corda(program: u8, soft: bool) -> Vec<f32> {
let mut ev = vec![
(
0.0,
EvKind::Prog {
ch: 0,
prog: program,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 93,
val: 0,
},
),
];
if soft {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 67,
val: 127,
},
));
}
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((1.0, EvKind::NoteOff { ch: 0, key: 60 }));
left(&render(&test_song(ev, 1.4), &test_opts(44100.0)).0)
}
#[test]
fn cc67_una_corda_softens_piano() {
let sr = 44100.0;
let normal = render_piano_una_corda(false);
let soft = render_piano_una_corda(true);
let (a, b) = ((0.06 * sr) as usize, (0.5 * sr) as usize);
let r_norm = rms(&normal[a..b]);
let r_soft = rms(&soft[a..b]);
assert!(
r_soft < 0.9 * r_norm,
"una corda not quieter: {r_soft} vs {r_norm}"
);
let hf_norm = energy_above(&normal[a..b], 3000.0, sr);
let hf_soft = energy_above(&soft[a..b], 3000.0, sr);
assert!(
hf_soft < 0.85 * hf_norm,
"una corda not duller: soft {hf_soft} vs normal {hf_norm}"
);
}
#[test]
fn keyboard_voices_una_corda_is_acoustic_piano_only() {
let sr = 44100.0;
let normal = render_keyboard_una_corda(0, false);
let soft = render_keyboard_una_corda(0, true);
let (a, b) = ((0.06 * sr) as usize, (0.5 * sr) as usize);
assert!(
rms(&soft[a..b]) < 0.9 * rms(&normal[a..b]),
"GM0 una corda positive control did not soften"
);
for program in 4u8..=7 {
let plain = render_keyboard_una_corda(program, false);
let soft = render_keyboard_una_corda(program, true);
assert!(
plain
.iter()
.zip(&soft)
.all(|(x, y)| x.to_bits() == y.to_bits()),
"GM{program} was still velocity-scaled by acoustic-piano CC67"
);
}
}
fn pitch_spread_cents(
sig: &[f32],
sr: f32,
t0: f32,
t1: f32,
n: usize,
flo: f32,
fhi: f32,
) -> f32 {
let span = (t1 - t0) / n as f32;
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
for i in 0..n {
let a = ((t0 + i as f32 * span) * sr) as usize;
let b = ((t0 + (i + 1) as f32 * span) * sr) as usize;
let f = crate::testutil::peak_locate(&sig[a..b], sr, flo, fhi);
lo = lo.min(f);
hi = hi.max(f);
}
1200.0 * (hi / lo).log2()
}
fn render_brass_cc1(cc1: Option<u8>) -> Vec<f32> {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 56 }),
(
0.0,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
];
if let Some(v) = cc1 {
ev.push((
0.5,
EvKind::Cc {
ch: 0,
num: 1,
val: v,
},
));
}
ev.push((3.0, EvKind::NoteOff { ch: 0, key: 69 }));
left(&render(&test_song(ev, 3.2), &test_opts(44100.0)).0)
}
#[test]
fn e55_1_cc1_vibrato_live_on_brass() {
let sr = 44100.0;
let plain = render_brass_cc1(None);
let modded = render_brass_cc1(Some(127));
let sp_plain = pitch_spread_cents(&plain, sr, 1.2, 1.92, 12, 380.0, 500.0);
let sp_mod = pitch_spread_cents(&modded, sr, 1.2, 1.92, 12, 380.0, 500.0);
assert!(
sp_mod >= 20.0,
"CC1 brass vibrato too shallow: {sp_mod:.1} cents"
);
assert!(
sp_mod >= 2.0 * sp_plain,
"CC1 vibrato not 2x the no-CC1 spread: mod {sp_mod:.1} vs plain {sp_plain:.1} cents"
);
}
fn brass_slur_song(cc68: u8) -> (Vec<f32>, Stats) {
let ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 57 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 68,
val: cc68,
},
),
(
0.0,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(
0.8,
EvKind::NoteOn {
ch: 0,
key: 71,
vel: 100,
},
),
(2.5, EvKind::NoteOff { ch: 0, key: 69 }),
(2.5, EvKind::NoteOff { ch: 0, key: 71 }),
];
let (out, stats) = render(&test_song(ev, 2.7), &test_opts(44100.0));
(left(&out), stats)
}
#[test]
fn e55_3_cc68_brass_slur_no_second_attack() {
let sr = 44100.0;
let (slur, slur_stats) = brass_slur_song(127);
let (_, tongued_stats) = brass_slur_song(0);
assert_eq!(
slur_stats.max_polyphony, 1,
"brass slur spawned a second voice"
);
assert_eq!(
tongued_stats.max_polyphony, 2,
"the tongued (CC68=0) phrase should be two voices"
);
let f = crate::testutil::peak_locate(
&slur[(1.6 * sr) as usize..(2.0 * sr) as usize],
sr,
400.0,
560.0,
);
let target = 493.883_f32; assert!(
(f / target - 1.0).abs() <= 0.005,
"slur target pitch off: {f:.1} Hz vs {target:.1} Hz"
);
let sus = rms(&slur[(1.6 * sr) as usize..(2.0 * sr) as usize]);
let early = rms(&slur[(0.3 * sr) as usize..(0.6 * sr) as usize]);
assert!(
sus > 0.5 * early,
"slurred brass voice decayed like a pluck: sustain ratio {:.3}",
sus / early
);
}
fn render_chord(prog: u8) -> Vec<f32> {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog })];
for &k in &[60u8, 64, 67] {
ev.push((
0.02,
EvKind::NoteOn {
ch: 0,
key: k,
vel: 100,
},
));
ev.push((3.0, EvKind::NoteOff { ch: 0, key: k }));
}
ev.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
render(&test_song(ev, 3.2), &test_opts(44100.0)).0
}
#[test]
fn e55_4a_section_chorus_decorrelates() {
let sr = 44100.0;
let (a, b) = ((0.5 * sr) as usize, (3.0 * sr) as usize);
let sec = render_chord(61);
let corr_sec = crate::testutil::inter_corr(&left(&sec)[a..b], &right(&sec)[a..b]);
let dry = render_chord(68);
let corr_dry = crate::testutil::inter_corr(&left(&dry)[a..b], &right(&dry)[a..b]);
assert!(
corr_sec <= 0.97,
"section chorus did not decorrelate: corr {corr_sec:.4}"
);
assert!(
corr_dry >= 0.999,
"dry reed chord not ~mono at centre: corr {corr_dry:.4}"
);
}
fn opts_delay(delay_s: f32) -> Options {
Options {
sr: 44100.0,
wet: 0.0,
tail: 1.0,
delay_s,
samples: false,
solo: 0xFFFF,
}
}
fn render_sax_echo(delay_s: f32) -> Vec<f32> {
let ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 65 }),
(
0.0,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(0.3, EvKind::NoteOff { ch: 0, key: 69 }),
];
crate::testutil::mono(&render(&test_song(ev, 1.2), &opts_delay(delay_s)).0)
}
#[test]
fn e55_4b_sax_echo_present() {
let sr = 44100.0;
let wet = render_sax_echo(0.25);
let dry = render_sax_echo(0.0);
let (a, b) = ((0.55 * sr) as usize, (0.65 * sr) as usize);
let d: Vec<f32> = wet[a..b]
.iter()
.zip(&dry[a..b])
.map(|(x, y)| x - y)
.collect();
let e = rms(&d);
const FLOOR: f32 = 1e-4; assert!(
e >= FLOOR,
"sax echo missing in the difference window: rms {e:.3e}"
);
}
fn mallet_echo_rms(prog: u8, authored_del: Option<u8>) -> f32 {
let sr = 44100.0;
let render_one = |delay_s: f32| {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog })];
if let Some(val) = authored_del {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 94,
val,
},
));
}
ev.push((
0.0,
EvKind::NoteOn {
ch: 0,
key: 76,
vel: 64,
},
));
ev.push((0.1, EvKind::NoteOff { ch: 0, key: 76 }));
crate::testutil::mono(&render(&test_song(ev, 1.6), &opts_delay(delay_s)).0)
};
let wet = render_one(0.30);
let dry = render_one(0.0);
let (a, b) = ((0.28 * sr) as usize, (1.20 * sr) as usize);
let d: Vec<f32> = wet[a..b]
.iter()
.zip(&dry[a..b])
.map(|(x, y)| x - y)
.collect();
rms(&d)
}
#[test]
fn mallet_bus_echo_is_off_for_glockenspiel_and_a_trace_for_its_neighbours() {
let glock = mallet_echo_rms(9, None);
assert!(
glock < 1e-9,
"GM 9 glockenspiel must take NO bus echo: difference rms {glock:.3e}"
);
const OLD_CC94: u8 = 19; let expect = 0.06 / (OLD_CC94 as f32 / 127.0);
for prog in [8u8, 10u8] {
let got = mallet_echo_rms(prog, None);
let reference = mallet_echo_rms(prog, Some(OLD_CC94));
assert!(
reference > 1e-4,
"GM {prog} reference echo too quiet to divide by: rms {reference:.3e}"
);
let ratio = got / reference;
assert!(
(ratio - expect).abs() < 0.02,
"GM {prog} echo send is not 0.06: measured ratio {ratio:.4} vs the \
expected {expect:.4} against an authored CC94={OLD_CC94}"
);
}
}
fn render_brass_cc11(ramp: bool) -> Vec<f32> {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 57 }),
(
0.0,
EvKind::NoteOn {
ch: 0,
key: 45,
vel: 100,
},
),
];
if ramp {
for i in 0..=20u32 {
let t = 0.1 + i as f64 * 0.09;
let v = (30 + i * (127 - 30) / 20) as u8;
ev.push((
t,
EvKind::Cc {
ch: 0,
num: 11,
val: v,
},
));
}
} else {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 11,
val: 127,
},
));
}
ev.push((2.5, EvKind::NoteOff { ch: 0, key: 45 }));
ev.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
left(&render(&test_song(ev, 2.7), &test_opts(44100.0)).0)
}
#[test]
fn br_o4_cc11_opens_brass_tone() {
let sr = 44100.0;
let cf = |sig: &[f32], t0: f32, t1: f32| {
crate::testutil::centroid(&sig[(t0 * sr) as usize..(t1 * sr) as usize], sr)
};
let rw = |sig: &[f32], t0: f32, t1: f32| rms(&sig[(t0 * sr) as usize..(t1 * sr) as usize]);
let ramp = render_brass_cc11(true);
let pinned = render_brass_cc11(false);
let ramp_c = cf(&ramp, 1.8, 2.3) / cf(&ramp, 0.3, 0.8);
let ramp_r = rw(&ramp, 1.8, 2.3) / rw(&ramp, 0.3, 0.8).max(1e-9);
let pin_c = cf(&pinned, 1.8, 2.3) / cf(&pinned, 0.3, 0.8);
assert!(
ramp_c >= 1.3,
"CC11 ramp didn't brighten: centroid ratio {ramp_c:.2}"
);
assert!(
ramp_r >= 2.0,
"CC11 ramp didn't swell: rms ratio {ramp_r:.2}"
);
assert!(
pin_c < 1.1,
"pinned CC11 shouldn't drift timbre: centroid ratio {pin_c:.2}"
);
}
#[test]
fn br_o14_bend_and_vibrato_liveness() {
let sr = 44100.0;
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 56 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 90,
},
),
(
0.02,
EvKind::NoteOn {
ch: 0,
key: 69,
vel: 100,
},
),
(0.5, EvKind::Bend { ch: 0, semis: 2.0 }),
];
for i in 0..=10u32 {
let t = 1.9 + i as f64 * 0.04;
let semis = 2.0 - i as f32 * 0.4;
ev.push((t, EvKind::Bend { ch: 0, semis }));
}
ev.push((2.0, EvKind::NoteOff { ch: 0, key: 69 }));
ev.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let m = left(&render(&test_song(ev, 2.6), &test_opts(sr)).0);
let target = 493.883_f32; let held = crate::testutil::peak_locate(
&m[(1.0 * sr) as usize..(1.5 * sr) as usize],
sr,
440.0,
540.0,
);
let cents = 1200.0 * (held / target).log2();
assert!(
cents.abs() <= 25.0,
"bent+vibratoed brass pitch off: {held:.1} Hz ({cents:.1} cents from B4)"
);
let tail = crate::testutil::peak_locate(
&m[(2.02 * sr) as usize..(2.16 * sr) as usize],
sr,
340.0,
520.0,
);
let tail_cents = 1200.0 * (tail / target).log2();
assert!(
tail_cents <= -80.0,
"release tail didn't track the fall: {tail:.1} Hz ({tail_cents:.1} cents from B4)"
);
}
#[test]
fn rd_o10_reed_slur_and_pitch_liveness() {
let sr = 44100.0;
let slur_song = |cc68: u8| {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 66 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 68,
val: cc68,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 64,
vel: 100,
},
),
(
0.4,
EvKind::NoteOn {
ch: 0,
key: 67,
vel: 100,
},
),
(1.5, EvKind::NoteOff { ch: 0, key: 64 }),
(1.5, EvKind::NoteOff { ch: 0, key: 67 }),
];
ev.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
render(&test_song(ev, 1.7), &test_opts(sr))
};
let (slur_out, slur_stats) = slur_song(127);
let (_, tongued_stats) = slur_song(0);
assert_eq!(
slur_stats.max_polyphony, 1,
"reed slur spawned a second voice"
);
assert_eq!(
tongued_stats.max_polyphony, 2,
"tongued reed should be 2 voices"
);
let m = left(&slur_out);
let g4 = 391.995_f32; let f = crate::testutil::peak_locate(
&m[(0.8 * sr) as usize..(1.3 * sr) as usize],
sr,
320.0,
460.0,
);
assert!(
(f / g4 - 1.0).abs() <= 0.01,
"reed slur target off: {f:.1} Hz vs {g4:.1} Hz"
);
let ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 71 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 1,
val: 96,
},
),
(
0.02,
EvKind::NoteOn {
ch: 0,
key: 62,
vel: 100,
},
),
(0.5, EvKind::Bend { ch: 0, semis: 2.0 }),
(2.0, EvKind::NoteOff { ch: 0, key: 62 }),
];
let m2 = left(&render(&test_song(ev, 2.2), &test_opts(sr)).0);
let target = 329.628_f32; let f2 = crate::testutil::peak_locate(
&m2[(1.0 * sr) as usize..(1.5 * sr) as usize],
sr,
280.0,
380.0,
);
assert!(
(f2 / target - 1.0).abs() <= 0.03,
"clarinet bent+vibrato pitch off: {f2:.1} Hz vs {target:.1} Hz"
);
}
fn bank_song(cc0: Option<u8>, prog: u8) -> Vec<(f64, EvKind)> {
let mut ev = Vec::new();
if let Some(v) = cc0 {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: v,
},
));
}
ev.push((0.0, EvKind::Prog { ch: 0, prog }));
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((1.5, EvKind::NoteOff { ch: 0, key: 60 }));
ev
}
#[test]
fn alt_bank_selects_distinct_voices() {
let sr = 44100.0;
for prog in [42u8, 48, 52] {
let alt = left(&render(&test_song(bank_song(Some(1), prog), 2.0), &test_opts(sr)).0);
let def = left(&render(&test_song(bank_song(None, prog), 2.0), &test_opts(sr)).0);
assert_eq!(alt.len(), def.len());
assert_ne!(
alt, def,
"prog {prog}: alt bank must produce a distinct render"
);
let diff: Vec<f32> = alt.iter().zip(&def).map(|(a, b)| a - b).collect();
let ratio = rms(&diff) / rms(&def).max(1e-9);
assert!(
ratio > 0.01,
"prog {prog}: alt render too close to default (diff/base = {ratio:.4})"
);
}
}
#[test]
fn alt_bank_latch_reset_and_delegation() {
let sr = 44100.0;
let def = left(&render(&test_song(bank_song(None, 48), 2.0), &test_opts(sr)).0);
let alt = left(&render(&test_song(bank_song(Some(1), 48), 2.0), &test_opts(sr)).0);
let rac = vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 48 }),
(
0.02,
EvKind::Cc {
ch: 0,
num: 121,
val: 0,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(1.5, EvKind::NoteOff { ch: 0, key: 60 }),
];
let after_rac = left(&render(&test_song(rac, 2.0), &test_opts(sr)).0);
assert_eq!(after_rac, alt, "CC121 must not clear the bank");
let reset = vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
),
(
0.02,
EvKind::Cc {
ch: 0,
num: 0,
val: 0,
},
),
(0.03, EvKind::Prog { ch: 0, prog: 48 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
),
(1.5, EvKind::NoteOff { ch: 0, key: 60 }),
];
let after_reset = left(&render(&test_song(reset, 2.0), &test_opts(sr)).0);
assert_eq!(after_reset, def, "CC0=0 must return to the default bank");
let alt_flute = left(&render(&test_song(bank_song(Some(1), 73), 2.0), &test_opts(sr)).0);
let def_flute = left(&render(&test_song(bank_song(None, 73), 2.0), &test_opts(sr)).0);
assert_eq!(
alt_flute, def_flute,
"alt bank must delegate non-orchestral programs to the default voice"
);
}
#[test]
fn cc32_undefined_bank_falls_back_byte_identical() {
let sr = 44100.0;
let song = |lsb: Option<u8>, prog: u8| {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog })];
if let Some(v) = lsb {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 32,
val: v,
},
));
}
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((1.0, EvKind::NoteOff { ch: 0, key: 60 }));
ev
};
for prog in [24u8, 0, 48, 30] {
let base = left(&render(&test_song(song(None, prog), 1.5), &test_opts(sr)).0);
let undef = left(&render(&test_song(song(Some(113), prog), 1.5), &test_opts(sr)).0);
assert_eq!(
base, undef,
"prog {prog}: an undefined CC32 bank must fall back to base GM byte-identically"
);
}
}
#[test]
fn cc32_defined_bank_selects_the_variation() {
let sr = 44100.0;
let variations: &[(u8, u8, u8)] = &[
(24, 96, 60), (25, 96, 60), (105, 98, 60), (116, 96, 45), (48, 3, 60), (30, 41, 45), (99, 19, 60), (33, 45, 40), (50, 65, 60), ];
let song = |lsb: Option<u8>, prog: u8, key: u8| {
let mut ev = vec![(0.0, EvKind::Prog { ch: 0, prog })];
if let Some(v) = lsb {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 32,
val: v,
},
));
}
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
));
ev.push((1.6, EvKind::NoteOff { ch: 0, key }));
ev
};
for &(prog, lsb, key) in variations {
let base = left(&render(&test_song(song(None, prog, key), 2.0), &test_opts(sr)).0);
let var = left(&render(&test_song(song(Some(lsb), prog, key), 2.0), &test_opts(sr)).0);
assert_eq!(base.len(), var.len());
assert_ne!(
base, var,
"prog {prog} + CC32={lsb}: the variation must render differently from the base"
);
let diff: Vec<f32> = base.iter().zip(&var).map(|(a, b)| a - b).collect();
let ratio = rms(&diff) / rms(&base).max(1e-9);
assert!(
ratio > 0.01,
"prog {prog} + CC32={lsb}: variation render too close to base (diff/base = {ratio:.4})"
);
}
}
#[test]
fn mandolin_variation_renders_at_the_played_pitch() {
let sr = 44100.0;
let key = 69u8; let f0 = key_freq(key);
let ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 25 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 32,
val: 96,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
),
(1.6, EvKind::NoteOff { ch: 0, key }),
];
let out = left(&render(&test_song(ev, 2.0), &test_opts(sr)).0);
let seg = &out[(0.15 * sr) as usize..(1.2 * sr) as usize];
let fund = crate::testutil::mag_at(seg, sr, f0);
let oct_down = crate::testutil::mag_at(seg, sr, f0 * 0.5);
assert!(
fund > 1e-3,
"mandolin A4 is silent or not at pitch (fundamental {fund:.5})"
);
assert!(
fund > 4.0 * oct_down,
"mandolin A4 has too much energy an octave down (fund {fund:.5} vs \
f0/2 {oct_down:.5}) — the MANDOLIN model is octave-broken"
);
}
#[test]
fn cc84_glides_from_source_without_porta_on_and_is_consumed_once() {
let sr = 44100.0;
let src = 48u8; let key = 60u8;
let f_src = key_freq(src);
let events = |cc84: bool| {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 0, prog: 80 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 5,
val: 110,
},
),
];
if cc84 {
ev.push((
0.01,
EvKind::Cc {
ch: 0,
num: 84,
val: src,
},
));
}
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
));
ev.push((0.90, EvKind::NoteOff { ch: 0, key }));
ev.push((
1.30,
EvKind::NoteOn {
ch: 0,
key,
vel: 100,
},
));
ev.push((2.10, EvKind::NoteOff { ch: 0, key }));
ev
};
let render_mono = |cc84: bool| {
crate::testutil::mono(&render(&test_song(events(cc84), 2.4), &test_opts(sr)).0)
};
let with_cc84 = render_mono(true);
let baseline = render_mono(false);
let e_src = |buf: &[f32], t0: f32, t1: f32| {
crate::testutil::mag_at(&buf[(t0 * sr) as usize..(t1 * sr) as usize], sr, f_src)
};
let n1_cc84 = e_src(&with_cc84, 0.05, 0.12);
let n1_base = e_src(&baseline, 0.05, 0.12);
let n2_cc84 = e_src(&with_cc84, 1.30, 1.37);
assert!(
n1_cc84 > 1e-3,
"note-1 onset should carry real energy: {n1_cc84:.6}"
);
assert!(
n1_cc84 > 5.0 * n1_base,
"CC84 must glide from key {src}: onset source-pitch energy cc84 {n1_cc84:.5} vs no-CC84 base {n1_base:.5}"
);
assert!(
n1_cc84 > 5.0 * n2_cc84,
"CC84 must be consumed once: note-1 source-pitch energy {n1_cc84:.5} vs note-2 {n2_cc84:.5}"
);
}
#[test]
fn xg_inert_effect_params_are_byte_identical() {
let sr = 44100.0;
let opts = Options {
sr,
wet: 0.3,
tail: 0.5,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
};
let phrase: Vec<(f64, EvKind)> = vec![
(0.0, EvKind::Prog { ch: 0, prog: 29 }),
(0.0, EvKind::Prog { ch: 1, prog: 48 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 45,
vel: 100,
},
),
(
0.05,
EvKind::NoteOn {
ch: 1,
key: 60,
vel: 90,
},
),
(0.9, EvKind::NoteOff { ch: 0, key: 45 }),
(0.9, EvKind::NoteOff { ch: 1, key: 60 }),
];
let mut with_inert_xg: Vec<(f64, EvKind)> = vec![
(0.0, EvKind::XgReset),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x00,
data: [0x02, 0x00],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x20,
data: [0x42, 0x00],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x40,
data: [0x4B, 0x11],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x42,
data: [0x00, 0x18],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x54,
data: [0x00, 0x64],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x5A,
data: [0x01, 0x00],
len: 1,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x5B,
data: [0x0E, 0x00],
len: 1,
},
),
];
with_inert_xg.extend(phrase.clone());
let base = render(&test_song(phrase, 1.6), &opts).0;
let xg = render(&test_song(with_inert_xg, 1.6), &opts).0;
assert_eq!(
base, xg,
"inert XG effect params must not perturb the render (byte-identity spine)"
);
}
#[test]
fn xg_effect_params_update_state() {
let sr = 44100.0;
let mut core = EngineCore::new(CoreOptions {
sr,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
});
for kind in [
EvKind::XgEffectParam {
addr_lo: 0x40,
data: [0x4B, 0x11],
len: 2,
},
EvKind::XgEffectParam {
addr_lo: 0x42,
data: [0x00, 0x18],
len: 2,
},
EvKind::XgEffectParam {
addr_lo: 0x54,
data: [0x00, 0x64],
len: 2,
},
EvKind::XgEffectParam {
addr_lo: 0x5A,
data: [0x00, 0x00],
len: 1,
},
EvKind::XgEffectParam {
addr_lo: 0x5B,
data: [0x0E, 0x00],
len: 1,
},
] {
core.handle_event(kind);
}
assert_eq!(core.xg.var_type_msb, 0x4B, "Amp Simulator type MSB");
assert_eq!(core.xg.var_drive, 24, "Drive 24");
assert_eq!(core.xg.var_drywet, 100, "Dry/Wet 100");
assert_eq!(core.xg.var_connection, 0, "INSERTION");
assert_eq!(core.xg.var_part, 14, "Part 15 = 0-based channel 14");
core.handle_event(EvKind::XgReset);
assert_eq!(core.xg.var_part, 127, "System On resets Part to OFF");
assert_eq!(core.xg.var_drive, 0, "System On resets Drive");
}
#[test]
fn xg_hall1_reconfigures_only_the_hall() {
let sr = 44100.0;
let mut core = EngineCore::new(CoreOptions {
sr,
wet: 0.3,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
});
let hall_before = core.reverb.debug_comb_feedback();
let drum_before = core.drum_room.debug_comb_feedback();
core.handle_event(EvKind::XgEffectParam {
addr_lo: 0x00,
data: HALL1_TYPE,
len: 2,
});
assert_eq!(
core.reverb.debug_comb_feedback(),
HALL1_ROOM,
"hall re-tuned to Hall 1"
);
assert_ne!(
core.reverb.debug_comb_feedback(),
hall_before,
"hall feedback actually changed"
);
assert_eq!(
core.drum_room.debug_comb_feedback(),
drum_before,
"drum room must be untouched"
);
core.handle_event(EvKind::XgReset);
assert_eq!(
core.reverb.debug_comb_feedback(),
DEFAULT_HALL_ROOM,
"System On restores the hall default"
);
}
#[test]
fn xg_hall1_changes_render_other_types_inert() {
let sr = 44100.0;
let opts = Options {
sr,
wet: 0.4,
tail: 0.8,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
};
let song = |reverb_type: Option<[u8; 2]>| {
let mut ev: Vec<(f64, EvKind)> = Vec::new();
if let Some(t) = reverb_type {
ev.push((
0.0,
EvKind::XgEffectParam {
addr_lo: 0x00,
data: t,
len: 2,
},
));
}
ev.push((0.0, EvKind::Prog { ch: 0, prog: 48 }));
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((0.5, EvKind::NoteOff { ch: 0, key: 60 }));
test_song(ev, 1.2)
};
let base = render(&song(None), &opts).0;
let hall1 = render(&song(Some(HALL1_TYPE)), &opts).0;
let other = render(&song(Some([0x02, 0x00])), &opts).0;
assert_ne!(base, hall1, "Hall 1 must change the hall render");
assert_eq!(base, other, "an unrecognized reverb type must be inert");
}
#[test]
fn chorus_reconfigure_changes_output_and_defaults_are_noop() {
let sr = 44100.0;
let n = (0.5 * sr) as usize;
let send: Vec<f32> = (0..n)
.map(|i| (TAU * 220.0 * i as f32 / sr).sin())
.collect();
let run = |reconfig: Option<(f32, f32, f32)>| {
let mut c = Chorus::new(sr);
if let Some((r, b, d)) = reconfig {
c.reconfigure(r, b, d);
}
let mut l = vec![0.0; n];
let mut r = vec![0.0; n];
c.process(&send, &mut l, &mut r);
(l, r)
};
let default = run(None);
let same = run(Some((
DEFAULT_CHORUS_RATE,
DEFAULT_CHORUS_BASE_S,
DEFAULT_CHORUS_DEPTH_S,
)));
let chorus1 = run(Some((CHORUS1_RATE, CHORUS1_BASE_S, CHORUS1_DEPTH_S)));
let rate_only = run(Some((
CHORUS1_RATE,
DEFAULT_CHORUS_BASE_S,
DEFAULT_CHORUS_DEPTH_S,
)));
assert_eq!(default, same, "reconfigure to the defaults must be a no-op");
assert_ne!(default, chorus1, "Chorus 1 params must change the output");
assert_ne!(default, rate_only, "the LFO rate must be wired");
}
#[test]
fn xg_chorus1_changes_render_other_types_inert() {
let sr = 44100.0;
let opts = Options {
sr,
wet: 0.0,
tail: 0.5,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
};
let song = |chorus_type: Option<[u8; 2]>| {
let mut ev: Vec<(f64, EvKind)> = Vec::new();
if let Some(t) = chorus_type {
ev.push((
0.0,
EvKind::XgEffectParam {
addr_lo: 0x20,
data: t,
len: 2,
},
));
}
ev.push((0.0, EvKind::Prog { ch: 0, prog: 90 }));
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((0.6, EvKind::NoteOff { ch: 0, key: 60 }));
test_song(ev, 1.2)
};
let base = render(&song(None), &opts).0;
let chorus1 = render(&song(Some(CHORUS1_TYPE)), &opts).0;
let other = render(&song(Some([0x00, 0x00])), &opts).0;
assert_ne!(base, chorus1, "Chorus 1 must change the chorus render");
assert_eq!(base, other, "an unrecognized chorus type must be inert");
}
fn xg_amp_sim_block(part: u8, drive: u8, drywet: u8, connection: u8) -> Vec<(f64, EvKind)> {
vec![
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x40,
data: [AMP_SIM_TYPE_MSB, 0x00],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x42,
data: [0x00, drive],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x54,
data: [0x00, drywet],
len: 2,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x5A,
data: [connection, 0x00],
len: 1,
},
),
(
0.0,
EvKind::XgEffectParam {
addr_lo: 0x5B,
data: [part, 0x00],
len: 1,
},
),
]
}
#[test]
fn xg_variation_routes_to_the_target_channel_only() {
let sr = 44100.0;
let opts = |solo: u16| Options {
sr,
wet: 0.0,
tail: 0.5,
delay_s: 0.0,
samples: false,
solo,
};
let song = |xg: Vec<(f64, EvKind)>| {
let mut ev = vec![
(0.0, EvKind::Prog { ch: 14, prog: 29 }),
(0.0, EvKind::Prog { ch: 0, prog: 29 }),
];
ev.extend(xg);
ev.push((
0.05,
EvKind::NoteOn {
ch: 14,
key: 45,
vel: 100,
},
));
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 52,
vel: 100,
},
));
ev.push((0.9, EvKind::NoteOff { ch: 14, key: 45 }));
ev.push((0.9, EvKind::NoteOff { ch: 0, key: 52 }));
test_song(ev, 1.4)
};
let insert = xg_amp_sim_block(14, 24, 100, XG_VAR_INSERTION);
let all = 0xFFFF;
let ch0_only = 1u16 << 0;
let base = render(&song(vec![]), &opts(all)).0;
let with_var = render(&song(insert.clone()), &opts(all)).0;
assert_ne!(
base, with_var,
"the Amp-Sim insert must change the target render"
);
let ch0_base = render(&song(vec![]), &opts(ch0_only)).0;
let ch0_var = render(&song(insert.clone()), &opts(ch0_only)).0;
assert_eq!(
ch0_base, ch0_var,
"a non-target channel must be byte-identical"
);
let system = render(&song(xg_amp_sim_block(14, 24, 100, 0x01)), &opts(all)).0;
assert_eq!(system, base, "SYSTEM connection must install no insert");
let off = render(
&song(xg_amp_sim_block(127, 24, 100, XG_VAR_INSERTION)),
&opts(all),
)
.0;
assert_eq!(off, base, "part OFF (127) must install no insert");
}
#[test]
fn xg_variation_replaces_not_stacks_the_program_drive() {
let build = |clear_drive: bool| {
let mut core = EngineCore::new(CoreOptions {
sr: 44100.0,
wet: 0.0,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
gtr_symp_on: true,
drum_room_on: true,
sitar_symp_on: true,
});
core.handle_event(EvKind::Prog { ch: 14, prog: 29 });
assert!(
core.strips[14].drive.is_some(),
"program 29 builds a program drive"
);
for (_, kind) in xg_amp_sim_block(14, 24, 100, XG_VAR_INSERTION) {
core.handle_event(kind);
}
assert!(
core.strips[14].xg_insert.is_some(),
"Amp-Sim insert must be installed"
);
if clear_drive {
core.strips[14].drive = None;
}
core.handle_event(EvKind::NoteOn {
ch: 14,
key: 45,
vel: 100,
});
let mut out = Vec::new();
let mut sink = vec![0f32; BLOCK * 2];
for _ in 0..30 {
sink.iter_mut().for_each(|x| *x = 0.0);
core.render_block_add(BLOCK, &mut sink);
out.extend_from_slice(&sink);
}
out
};
assert_eq!(
build(false),
build(true),
"the insert must REPLACE the program drive (it never runs when an insert is present)"
);
}
#[test]
fn xg_variation_drywet_blends_toward_full_wet() {
let sr = 44100.0;
let opts = Options {
sr,
wet: 0.0,
tail: 0.5,
delay_s: 0.0,
samples: false,
solo: 0xFFFF,
};
let render_dw = |drywet: u8| {
let mut ev = vec![(0.0, EvKind::Prog { ch: 14, prog: 29 })];
ev.extend(xg_amp_sim_block(14, 90, drywet, XG_VAR_INSERTION));
ev.push((
0.05,
EvKind::NoteOn {
ch: 14,
key: 45,
vel: 100,
},
));
ev.push((0.9, EvKind::NoteOff { ch: 14, key: 45 }));
left(&render(&test_song(ev, 1.4), &opts).0)
};
let full = render_dw(127);
let mid = render_dw(64);
let dry = render_dw(1);
let dist =
|a: &[f32], b: &[f32]| rms(&a.iter().zip(b).map(|(x, y)| x - y).collect::<Vec<_>>());
let d_dry = dist(&dry, &full);
let d_mid = dist(&mid, &full);
assert!(d_mid > 1e-6, "Dry/Wet 64 must differ from full wet");
assert!(
d_dry > d_mid * 1.3,
"lower Dry/Wet must move monotonically away from full wet: dry {d_dry:.6} mid {d_mid:.6}"
);
}
#[test]
fn amp_sim_higher_drive_adds_harmonics() {
let sr = 44100.0;
let f0 = 200.0;
let n = 8192;
let thd = |drive: u8| {
let mut buf: Vec<f32> = (0..n)
.map(|i| 0.3 * (TAU * f0 * i as f32 / sr).sin())
.collect();
Drive::amp_sim(sr, drive).process(&mut buf);
let fund = crate::testutil::mag_at(&buf, sr, f0).max(1e-9);
let h2 = crate::testutil::mag_at(&buf, sr, 2.0 * f0);
let h3 = crate::testutil::mag_at(&buf, sr, 3.0 * f0);
(h2 + h3) / fund
};
let mild = thd(20);
let hot = thd(110);
assert!(
hot > mild * 1.5,
"more XG Drive must add harmonics: mild {mild:.4} hot {hot:.4}"
);
}
#[test]
fn ch10_program_40_selects_brush() {
let sr = 44100.0;
let song = |progs: &[(f64, u8)]| {
let mut ev: Vec<(f64, EvKind)> = progs
.iter()
.map(|&(t, p)| (t, EvKind::Prog { ch: 9, prog: p }))
.collect();
ev.push((
0.3,
EvKind::NoteOn {
ch: 9,
key: 38,
vel: 100,
},
));
ev.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
test_song(ev, 1.0)
};
let r = |progs: &[(f64, u8)]| render(&song(progs), &test_opts(sr)).0;
let v3 = r(&[]);
let prog8 = r(&[(0.0, 8)]);
let brush = r(&[(0.0, 40)]);
let back = r(&[(0.0, 40), (0.1, 8)]);
assert_eq!(prog8, v3, "non-40 programs must keep the V3 kit");
assert_ne!(brush, v3, "prog 40 must select Brush, not V3");
assert_eq!(back, v3, "a later non-40 program must hand back to V3");
}
#[test]
fn alt_bank_gm14_tamtam_authored_only() {
let sr = 44100.0;
let base = vec![
(0.0, EvKind::Prog { ch: 0, prog: 14 }),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 43,
vel: 100,
},
),
];
let with_cc0 = |val: u8| {
let mut ev = vec![(0.0, EvKind::Cc { ch: 0, num: 0, val })];
ev.extend(base.iter().cloned());
ev
};
let no_cc = render(&test_song(base.clone(), 2.0), &test_opts(sr)).0;
let cc0_zero = render(&test_song(with_cc0(0), 2.0), &test_opts(sr)).0;
let cc0_alt = render(&test_song(with_cc0(8), 2.0), &test_opts(sr)).0;
assert_eq!(cc0_zero, no_cc, "CC0=0 must be a no-op (default bank)");
assert_ne!(cc0_alt, no_cc, "CC0!=0 on GM 14 must swap in the tam-tam");
let l = left(&cc0_alt);
let f = crate::testutil::peak_locate(
&l[(0.5 * sr) as usize..(1.8 * sr) as usize],
sr,
55.0,
140.0,
);
assert!(
(f - 98.0).abs() <= 3.0,
"alt GM 14 fundamental {f:.1} Hz is not the key-43 gong"
);
}
#[test]
fn gm19_bank_overlap_matches_split_channels() {
let sr = 44100.0;
let overlap_events = |split: bool, first_alt: bool| {
let first_ch = 0;
let second_ch = u8::from(split);
let mut events = vec![];
for (ch, alt) in [(first_ch, first_alt), (second_ch, !first_alt)] {
events.push((
0.0,
EvKind::Cc {
ch,
num: 0,
val: u8::from(alt),
},
));
events.push((0.0, EvKind::Prog { ch, prog: 19 }));
if !split {
break;
}
}
events.push((
0.05,
EvKind::NoteOn {
ch: first_ch,
key: 48,
vel: 100,
},
));
if !split {
events.push((
0.30,
EvKind::Cc {
ch: 0,
num: 0,
val: u8::from(!first_alt),
},
));
}
events.push((
0.35,
EvKind::NoteOn {
ch: second_ch,
key: 60,
vel: 100,
},
));
for (time, num, val) in [
(0.45, 91, 100),
(0.50, 1, 90),
(0.65, 93, 64),
(0.75, 94, 72),
] {
events.push((time, EvKind::Cc { ch: 0, num, val }));
if split {
events.push((time, EvKind::Cc { ch: 1, num, val }));
}
}
events.push((
1.00,
EvKind::Cc {
ch: 0,
num: 121,
val: 0,
},
));
if split {
events.push((
1.00,
EvKind::Cc {
ch: 1,
num: 121,
val: 0,
},
));
}
events.push((
1.50,
EvKind::NoteOff {
ch: first_ch,
key: 48,
},
));
events.push((
1.70,
EvKind::NoteOff {
ch: second_ch,
key: 60,
},
));
events
};
let mut opts = test_opts(sr);
opts.wet = 0.32;
opts.delay_s = 0.12;
opts.tail = 1.0;
for first_alt in [false, true] {
let overlap = render(&test_song(overlap_events(false, first_alt), 2.0), &opts).0;
let split = render(&test_song(overlap_events(true, first_alt), 2.0), &opts).0;
assert_eq!(overlap.len(), split.len());
let diff: Vec<f32> = overlap.iter().zip(&split).map(|(x, y)| x - y).collect();
let ratio = rms(&diff) / rms(&split).max(1e-9);
assert!(
ratio < 2e-4,
"held GM19 routes leaked with first_alt={first_alt}: diff/base {ratio:.6}"
);
}
}
#[test]
fn default_gm19_is_the_legacy_drawbar_voice() {
let events = |alt: bool| {
let mut ev = Vec::new();
if alt {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 1,
},
));
}
ev.push((0.0, EvKind::Prog { ch: 0, prog: 19 }));
ev.push((
0.2,
EvKind::Cc {
ch: 0,
num: 1,
val: 96,
},
));
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 60,
vel: 100,
},
));
ev.push((1.6, EvKind::NoteOff { ch: 0, key: 60 }));
ev
};
let mut opts = test_opts(44100.0);
opts.wet = 0.32;
opts.delay_s = 0.12;
opts.tail = 0.5;
let default_bank = render(&test_song(events(false), 2.0), &opts).0;
let legacy = render(&test_song(events(true), 2.0), &opts).0;
assert!(
default_bank
.iter()
.zip(&legacy)
.all(|(a, b)| a.to_bits() == b.to_bits()),
"default GM19 and CC0 legacy GM19 renders diverged"
);
}
#[test]
fn gm19_cc0_2_is_the_cathedral_organ() {
let events = |bank: u8| {
let mut ev = Vec::new();
if bank > 0 {
ev.push((
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: bank,
},
));
}
ev.push((0.0, EvKind::Prog { ch: 0, prog: 19 }));
ev.push((
0.05,
EvKind::NoteOn {
ch: 0,
key: 48,
vel: 100,
},
));
ev.push((1.2, EvKind::NoteOff { ch: 0, key: 48 }));
ev
};
let mut opts = test_opts(44100.0);
opts.wet = 0.32;
opts.tail = 1.0;
let default_bank = render(&test_song(events(0), 2.5), &opts).0;
let legacy = render(&test_song(events(1), 2.5), &opts).0;
let cathedral = render(&test_song(events(2), 2.5), &opts).0;
assert!(
default_bank
.iter()
.zip(&legacy)
.all(|(a, b)| a.to_bits() == b.to_bits()),
"default and CC0=1 GM19 must stay the identical Leslie drawbar"
);
assert!(
cathedral
.iter()
.zip(&legacy)
.any(|(a, b)| a.to_bits() != b.to_bits()),
"CC0=2 GM19 did not differ from the Leslie — cathedral voice not routed"
);
let sr = 44100usize;
let tail = |v: &[f32]| {
let seg = &v[v.len().saturating_sub((0.3 * sr as f32) as usize)..];
(seg.iter().map(|x| x * x).sum::<f32>() / seg.len().max(1) as f32).sqrt()
};
let (cat_tail, leg_tail) = (tail(&cathedral), tail(&legacy));
assert!(
cat_tail > leg_tail * 2.0 && cat_tail > 1e-4,
"cathedral reverb tail ({cat_tail:.2e}) should ring well past the Leslie's ({leg_tail:.2e})"
);
}
#[test]
#[ignore = "writes matched cathedral-send A/B WAVs to the OS temp directory"]
fn write_cathedral_send_hpf_matched_ab() {
fn render_variant(song: &Song, opt: &Options, highpass: bool) -> Vec<f32> {
let total = render_sample_count(song, opt);
let mut out = vec![0.0f32; total * 2];
drive_render(
song,
opt,
true,
true,
true,
&mut |_| {},
|core, block_start, n| {
core.cathedral_send_hpf_on = highpass;
core.render_block_add(n, &mut out[block_start * 2..(block_start + n) * 2]);
Ok::<(), std::convert::Infallible>(())
},
)
.expect("an infallible render sink cannot fail");
out
}
let song = test_song(
vec![
(
0.0,
EvKind::Cc {
ch: 0,
num: 0,
val: 2,
},
),
(0.0, EvKind::Prog { ch: 0, prog: 19 }),
(
0.0,
EvKind::Cc {
ch: 0,
num: 7,
val: 70,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 11,
val: 100,
},
),
(
0.0,
EvKind::Cc {
ch: 0,
num: 91,
val: 90,
},
),
(
0.05,
EvKind::NoteOn {
ch: 0,
key: 36,
vel: 84,
},
),
(
0.35,
EvKind::NoteOn {
ch: 0,
key: 43,
vel: 80,
},
),
(
0.65,
EvKind::NoteOn {
ch: 0,
key: 48,
vel: 76,
},
),
(3.35, EvKind::NoteOff { ch: 0, key: 48 }),
(3.50, EvKind::NoteOff { ch: 0, key: 43 }),
(3.65, EvKind::NoteOff { ch: 0, key: 36 }),
],
4.0,
);
let mut wet_options = test_opts(44_100.0);
wet_options.wet = 0.32;
wet_options.tail = 8.0;
let mut dry_options = wet_options.clone();
dry_options.wet = 0.0;
let dry = render_variant(&song, &dry_options, false);
let full_band_mix = render_variant(&song, &wet_options, false);
let highpass_mix = render_variant(&song, &wet_options, true);
let full_band_return: Vec<f32> = full_band_mix
.iter()
.zip(&dry)
.map(|(wet, dry)| wet - dry)
.collect();
let mut highpass_return: Vec<f32> = highpass_mix
.iter()
.zip(&dry)
.map(|(wet, dry)| wet - dry)
.collect();
let sr = wet_options.sr as u32;
let full_band_lufs = crate::loudness::integrated_lufs(&full_band_return, sr);
let highpass_lufs = crate::loudness::integrated_lufs(&highpass_return, sr);
assert!(
full_band_lufs.is_finite() && highpass_lufs.is_finite(),
"both isolated cathedral returns must have measurable loudness"
);
let match_gain_db = full_band_lufs - highpass_lufs;
let match_gain = 10f32.powf(match_gain_db / 20.0);
for sample in &mut highpass_return {
*sample *= match_gain;
}
let matched_lufs = crate::loudness::integrated_lufs(&highpass_return, sr);
assert!(
(full_band_lufs - matched_lufs).abs() < 0.05,
"matched return differs by {:.3} LU",
full_band_lufs - matched_lufs
);
let common_peak = full_band_return
.iter()
.chain(&highpass_return)
.copied()
.map(f32::abs)
.fold(0.0f32, f32::max);
assert!(common_peak > 1e-6, "cathedral returns are silent");
let common_gain = 0.80 / common_peak;
let output_dir = std::env::temp_dir().join("ferrosintesis-cathedral-send-hpf");
std::fs::create_dir_all(&output_dir).expect("create cathedral A/B directory");
for (name, samples) in [
("current-full-band-return.wav", &full_band_return),
("candidate-150hz-hpf-return.wav", &highpass_return),
] {
let pcm = dither_quantize(samples, common_gain);
let path = output_dir.join(name);
crate::wav::write_wav(&path, sr, &pcm)
.unwrap_or_else(|error| panic!("write {}: {error}", path.display()));
println!("{}", path.display());
}
println!(
"current {full_band_lufs:.3} LUFS; HPF {highpass_lufs:.3} LUFS raw, \
{matched_lufs:.3} LUFS matched; HPF match gain {match_gain_db:+.3} dB; \
common output gain {:+.3} dB",
20.0 * common_gain.log10()
);
}
}