use crate::dsp::{key_freq, vel_amp, vel_amp_sensed, Rng};
use crate::voices::Voice;
use std::sync::OnceLock;
pub struct Zone {
root: f32,
data: Vec<f32>,
#[cfg(feature = "embedded-samples")]
b1_tail: Option<B1Tail>,
sustain_loop: OnceLock<Option<(usize, usize)>>,
}
#[cfg(feature = "embedded-samples")]
struct B1Tail {
entry_frame: usize,
rate_divisor: usize,
source_frames: usize,
data: Vec<f32>,
}
#[cfg(feature = "embedded-samples")]
const B1_TAIL_END_FADE_FRAMES: f32 = 0.080 * 44_100.0;
type LoopFinder = fn(&[f32], f32) -> Option<(usize, usize)>;
impl Zone {
fn sustain_loop(&self, find: LoopFinder) -> Option<(usize, usize)> {
*self
.sustain_loop
.get_or_init(|| find(&self.data, self.root))
}
}
fn parse_wav(bytes: &[u8]) -> Vec<f32> {
if bytes.starts_with(b"fLaC") {
let pcm = ferrosintesis_flac::decode_mono16(bytes)
.unwrap_or_else(|error| panic!("embedded sample bank is not decodable: {error}"));
return pcm.into_iter().map(|s| f32::from(s) / 32768.0).collect();
}
parse_riff_wav(bytes)
}
fn parse_riff_wav(bytes: &[u8]) -> Vec<f32> {
assert!(&bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WAVE");
let mut pos = 12;
let mut data = Vec::new();
while pos + 8 <= bytes.len() {
let id = &bytes[pos..pos + 4];
let len = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().unwrap()) as usize;
let body = &bytes[pos + 8..(pos + 8 + len).min(bytes.len())];
if id == b"fmt " {
let channels = u16::from_le_bytes(body[2..4].try_into().unwrap());
let sr = u32::from_le_bytes(body[4..8].try_into().unwrap());
let bits = u16::from_le_bytes(body[14..16].try_into().unwrap());
assert!(
channels == 1 && sr == 44100 && bits == 16,
"sample bank must be 16-bit mono 44.1 kHz"
);
} else if id == b"data" {
data = body
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0)
.collect();
}
pos += 8 + len + (len & 1);
}
assert!(!data.is_empty(), "sample bank file has no data chunk");
data
}
#[cfg(feature = "embedded-samples")]
fn decode_b1_tail_code(code: u8) -> f32 {
let value = code as f64 / 127.5 - 1.0;
if value == 0.0 {
0.0
} else {
(value.signum() * (value.abs() * 256.0f64.ln()).exp_m1() / 255.0) as f32
}
}
#[cfg(feature = "embedded-samples")]
fn parse_b1_tail(bytes: &[u8], pcm_frames: usize) -> Result<B1Tail, &'static str> {
if bytes.len() < 12 || &bytes[..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return Err("B1 asset is not RIFF/WAVE");
}
let declared_end = 8usize
.checked_add(u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize)
.ok_or("B1 RIFF size overflow")?;
if declared_end != bytes.len() {
return Err("B1 RIFF size does not match the asset");
}
let mut pos = 12usize;
let mut fmt_count = 0usize;
let mut data_frames = None;
let mut tail = None;
while pos < declared_end {
let header_end = pos.checked_add(8).ok_or("B1 chunk header overflow")?;
if header_end > declared_end {
return Err("B1 RIFF has a truncated chunk header");
}
let chunk_len = u32::from_le_bytes(bytes[pos + 4..header_end].try_into().unwrap()) as usize;
let body_start = header_end;
let body_end = body_start
.checked_add(chunk_len)
.ok_or("B1 chunk body overflow")?;
let padded_end = body_end
.checked_add(chunk_len & 1)
.ok_or("B1 chunk padding overflow")?;
if padded_end > declared_end {
return Err("B1 RIFF has a truncated or oversized chunk");
}
if chunk_len & 1 != 0 && bytes[body_end] != 0 {
return Err("B1 RIFF has a nonzero pad byte");
}
match &bytes[pos..pos + 4] {
b"fmt " => {
fmt_count += 1;
let body = &bytes[body_start..body_end];
if body.len() < 16
|| u16::from_le_bytes(body[0..2].try_into().unwrap()) != 1
|| u16::from_le_bytes(body[2..4].try_into().unwrap()) != 1
|| u32::from_le_bytes(body[4..8].try_into().unwrap()) != 44_100
|| u16::from_le_bytes(body[12..14].try_into().unwrap()) != 2
|| u16::from_le_bytes(body[14..16].try_into().unwrap()) != 16
{
return Err("B1 body is not PCM16 mono 44.1 kHz");
}
}
b"data" => {
if chunk_len == 0 || chunk_len & 1 != 0 {
return Err("B1 PCM body is empty or has a partial frame");
}
if data_frames.replace(chunk_len / 2).is_some() {
return Err("B1 RIFF has duplicate PCM data chunks");
}
}
b"b1t " => {
if tail.is_some() {
return Err("B1 RIFF has duplicate natural tails");
}
if padded_end != declared_end {
return Err("B1 natural tail is not terminal");
}
let body = &bytes[body_start..body_end];
if body.len() < 13 {
return Err("B1 natural tail is truncated or empty");
}
let version = body[0];
let rate_divisor = body[1] as usize;
let reserved = u16::from_le_bytes(body[2..4].try_into().unwrap());
let entry_frame = u32::from_le_bytes(body[4..8].try_into().unwrap()) as usize;
let source_frames = u32::from_le_bytes(body[8..12].try_into().unwrap()) as usize;
let payload = &body[12..];
if version != 1 {
return Err("unsupported B1 natural-tail version");
}
if rate_divisor != 4 {
return Err("unsupported B1 natural-tail rate divisor");
}
if reserved != 0 {
return Err("nonzero B1 natural-tail reserved field");
}
if entry_frame >= pcm_frames {
return Err("B1 natural-tail entry is outside the PCM body");
}
if payload.len() != source_frames.div_ceil(rate_divisor) {
return Err("B1 natural-tail payload length mismatch");
}
tail = Some(B1Tail {
entry_frame,
rate_divisor,
source_frames,
data: payload.iter().copied().map(decode_b1_tail_code).collect(),
});
}
_ => {}
}
pos = padded_end;
}
if fmt_count != 1 || data_frames.is_none() {
return Err("B1 RIFF requires exactly one fmt and one data chunk");
}
if data_frames != Some(pcm_frames) {
return Err("B1 strict PCM body disagrees with the decoded body");
}
tail.ok_or("B1 RIFF is missing its natural tail")
}
#[cfg(feature = "embedded-samples")]
fn embedded_wav(name: &str) -> &'static [u8] {
ferrosintesis_samples_core::get(name)
.or_else(|| ferrosintesis_samples_orchestral::get(name))
.or_else(|| ferrosintesis_samples_orchestral2::get(name))
.or_else(|| ferrosintesis_samples_gong::get(name))
.or_else(|| ferrosintesis_samples_grand::get(name))
.or_else(|| ferrosintesis_samples_vcsl_steinway::get(name))
.or_else(|| ferrosintesis_samples_vcsl_kawai::get(name))
.or_else(|| ferrosintesis_samples_headroom::get(name))
.or_else(|| ferrosintesis_samples_musescore_grand::get(name))
.or_else(|| ferrosintesis_samples_ydp_grand::get(name))
.or_else(|| ferrosintesis_samples_honkytonk::get(name))
.or_else(|| ferrosintesis_samples_b1_upright::get(name))
.or_else(|| ferrosintesis_samples_clavinet::get(name))
.or_else(|| ferrosintesis_samples_musescore::get(name))
.or_else(|| ferrosintesis_samples_sax::get(name))
.or_else(|| ferrosintesis_samples_strings::get(name))
.or_else(|| ferrosintesis_samples_bass::get(name))
.or_else(|| ferrosintesis_samples_bottle::get(name))
.or_else(|| ferrosintesis_samples_ccby::get(name))
.or_else(|| ferrosintesis_samples_rain::get(name))
.or_else(|| ferrosintesis_samples_mandolin::get(name))
.or_else(|| ferrosintesis_samples_fretnoise::get(name))
.unwrap_or_else(|| panic!("embedded sample inventory is missing {name}"))
}
#[cfg(not(feature = "embedded-samples"))]
fn embedded_wav(name: &str) -> &'static [u8] {
panic!("sample {name} requested from a modeled-only ferrosintesis build")
}
macro_rules! init_once {
($cell:ident, $build:expr) => {
$cell.get_or_init(|| {
#[cfg(test)]
crate::sampler::record_cache_init(line!());
$build
})
};
}
#[cfg(feature = "embedded-samples")]
pub fn rain_loop() -> &'static [f32] {
static L: OnceLock<Vec<f32>> = OnceLock::new();
init_once!(L, parse_wav(embedded_wav("rain_loop.flac")))
}
#[cfg(test)]
pub(crate) static BANK_INITS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) static LOOP_SEARCHES: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) static TAIL_DECODES: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) static CACHE_INIT_SITES: std::sync::Mutex<std::collections::BTreeSet<u32>> =
std::sync::Mutex::new(std::collections::BTreeSet::new());
#[cfg(test)]
pub(crate) fn record_cache_init(line: u32) {
CACHE_INIT_SITES
.lock()
.expect("cache-site registry")
.insert(line);
}
macro_rules! bank {
($($file:literal => $root:expr),+ $(,)?) => {{
#[cfg(test)]
crate::sampler::BANK_INITS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
vec![$(Zone {
root: $root,
data: parse_wav(embedded_wav($file)),
#[cfg(feature = "embedded-samples")]
b1_tail: None,
sustain_loop: OnceLock::new(),
}),+]
}};
}
macro_rules! b1_bank {
($($file:literal => $root:expr),+ $(,)?) => {{
#[cfg(test)]
crate::sampler::BANK_INITS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
vec![$({
let bytes = embedded_wav($file);
let data = parse_wav(bytes);
#[cfg(feature = "embedded-samples")]
let b1_tail = parse_b1_tail(bytes, data.len())
.unwrap_or_else(|error| panic!("{}: {error}", $file));
#[cfg(test)]
crate::sampler::TAIL_DECODES.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Zone {
root: $root,
data,
#[cfg(feature = "embedded-samples")]
b1_tail: Some(b1_tail),
sustain_loop: OnceLock::new(),
}
}),+]
}};
}
fn violin_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"violin_G3_f.flac" => 196.00,
"violin_E4_f.flac" => 329.33,
"violin_C5_f.flac" => 519.94,
"violin_G5_f.flac" => 786.90,
"violin_C6_f.flac" => 1040.45,
"violin_E6_f.flac" => 1329.58,
)
})
}
fn violin_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"violin_G3_p.flac" => 195.11,
"violin_E4_p.flac" => 329.51,
"violin_C5_p.flac" => 521.61,
"violin_G5_p.flac" => 787.21,
"violin_C6_p.flac" => 1045.77,
"violin_E6_p.flac" => 1320.88,
)
})
}
fn flute() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"flute_C4.flac" => 523.23,
"flute_A4.flac" => 879.92,
"flute_E5.flac" => 1320.47,
"flute_A5.flac" => 1757.81,
"flute_C6.flac" => 2091.31,
)
})
}
fn piano_pp() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"piano_C2_pp.flac" => 65.05,
"piano_G2_pp.flac" => 97.77,
"piano_C3_pp.flac" => 130.68,
"piano_G3_pp.flac" => 195.31,
"piano_C4_pp.flac" => 261.04,
"piano_G4_pp.flac" => 393.15,
"piano_C5_pp.flac" => 523.65,
"piano_G5_pp.flac" => 784.41,
"piano_C6_pp.flac" => 1051.84,
)
})
}
fn piano_mf() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"piano_C2_mf.flac" => 65.17,
"piano_G2_mf.flac" => 98.10,
"piano_C3_mf.flac" => 130.94,
"piano_G3_mf.flac" => 196.23,
"piano_C4_mf.flac" => 261.25,
"piano_G4_mf.flac" => 393.58,
"piano_C5_mf.flac" => 524.52,
"piano_G5_mf.flac" => 785.35,
"piano_C6_mf.flac" => 1050.22,
)
})
}
fn piano_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"piano_C2_f.flac" => 65.52,
"piano_G2_f.flac" => 98.33,
"piano_C3_f.flac" => 131.19,
"piano_G3_f.flac" => 195.73,
"piano_C4_f.flac" => 261.74,
"piano_G4_f.flac" => 393.96,
"piano_C5_f.flac" => 525.21,
"piano_G5_f.flac" => 786.26,
"piano_C6_f.flac" => 1050.00,
)
})
}
fn piano_pp_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"piano_C2_pp.flac" => 65.05,
"piano_G2_pp.flac" => 97.77,
"piano_C3_pp_rr2.flac" => 130.60,
"piano_G3_pp_rr2.flac" => 194.91,
"piano_C4_pp_rr2.flac" => 261.00,
"piano_G4_pp_rr2.flac" => 392.77,
"piano_C5_pp_rr2.flac" => 523.95,
"piano_G5_pp_rr2.flac" => 784.04,
"piano_C6_pp_rr2.flac" => 1049.08,
)
})
}
fn piano_mf_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"piano_C2_mf_rr2.flac" => 65.51,
"piano_G2_mf_rr2.flac" => 98.28,
"piano_C3_mf_rr2.flac" => 131.12,
"piano_G3_mf_rr2.flac" => 196.23,
"piano_C4_mf_rr2.flac" => 261.29,
"piano_G4_mf_rr2.flac" => 393.76,
"piano_C5_mf_rr2.flac" => 524.92,
"piano_G5_mf_rr2.flac" => 785.18,
"piano_C6_mf_rr2.flac" => 1049.43,
)
})
}
fn piano_f_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"piano_C2_f_rr2.flac" => 65.58,
"piano_G2_f_rr2.flac" => 98.43,
"piano_C3_f_rr2.flac" => 131.20,
"piano_G3_f_rr2.flac" => 196.39,
"piano_C4_f_rr2.flac" => 261.73,
"piano_G4_f_rr2.flac" => 393.77,
"piano_C5_f_rr2.flac" => 525.19,
"piano_G5_f_rr2.flac" => 786.16,
"piano_C6_f_rr2.flac" => 1050.12,
)
})
}
fn trumpet_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"trumpet_F2_p.flac" => 174.88,
"trumpet_C3_p.flac" => 259.21,
"trumpet_G3_p.flac" => 392.78,
"trumpet_D4_p.flac" => 586.53,
"trumpet_A4_p.flac" => 877.64,
)
})
}
fn trumpet_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"trumpet_F2_f.flac" => 172.61,
"trumpet_C3_f.flac" => 261.15,
"trumpet_G3_f.flac" => 393.83,
"trumpet_D4_f.flac" => 588.04,
"trumpet_A4_f.flac" => 886.84,
)
})
}
fn mutetpt_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"mutetpt_A#2_p.flac" => 232.97,
"mutetpt_D3_p.flac" => 293.86,
"mutetpt_G3_p.flac" => 392.99,
"mutetpt_D4_p.flac" => 586.63,
"mutetpt_A4_p.flac" => 880.37,
)
})
}
fn mutetpt_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"mutetpt_A#2_f.flac" => 233.07,
"mutetpt_D3_f.flac" => 293.30,
"mutetpt_G3_f.flac" => 392.47,
"mutetpt_D4_f.flac" => 586.39,
"mutetpt_A4_f.flac" => 880.34,
)
})
}
fn trombone_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"trombone_F1_p.flac" => 87.34,
"trombone_A#1_p.flac" => 116.31,
"trombone_D2_p.flac" => 146.73,
"trombone_F2_p.flac" => 174.45,
"trombone_C3_p.flac" => 261.43,
"trombone_F3_p.flac" => 349.09,
)
})
}
fn trombone_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"trombone_F1_f.flac" => 87.21,
"trombone_A#1_f.flac" => 116.56,
"trombone_D2_f.flac" => 146.73,
"trombone_F2_f.flac" => 174.53,
"trombone_C3_f.flac" => 261.56,
"trombone_F3_f.flac" => 349.05,
)
})
}
fn tuba_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"tuba_A#0_p.flac" => 58.01,
"tuba_D#1_p.flac" => 78.25,
"tuba_A#1_p.flac" => 116.03,
"tuba_D2_p.flac" => 146.34,
"tuba_F2_p.flac" => 174.46,
"tuba_A#2_p.flac" => 231.99,
)
})
}
fn tuba_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"tuba_A#0_f.flac" => 58.35,
"tuba_D#1_f.flac" => 77.78,
"tuba_A#1_f.flac" => 116.34,
"tuba_D2_f.flac" => 145.88,
"tuba_F2_f.flac" => 174.42,
"tuba_A#2_f.flac" => 233.19,
)
})
}
fn horn_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"horn_A#1_p.flac" => 116.90,
"horn_D2_p.flac" => 148.36,
"horn_F2_p.flac" => 173.95,
"horn_A2_p.flac" => 219.46,
"horn_C3_p.flac" => 260.59,
"horn_D4_p.flac" => 604.11,
)
})
}
fn horn_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"horn_A#1_f.flac" => 116.81,
"horn_D2_f.flac" => 146.41,
"horn_F2_f.flac" => 173.93,
"horn_A2_f.flac" => 219.08,
"horn_C3_f.flac" => 260.84,
"horn_D4_f.flac" => 604.11,
)
})
}
fn oboe_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"oboe_D3_p.flac" => 293.50,
"oboe_F3_p.flac" => 347.64,
"oboe_A#3_p.flac" => 464.35,
"oboe_D4_p.flac" => 586.46,
"oboe_F4_p.flac" => 698.81,
"oboe_A#4_p.flac" => 935.72,
)
})
}
fn oboe_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"oboe_D3_f.flac" => 294.16,
"oboe_F3_f.flac" => 349.95,
"oboe_A#3_f.flac" => 466.12,
"oboe_D4_f.flac" => 588.05,
"oboe_F4_f.flac" => 698.94,
"oboe_A#4_f.flac" => 930.35,
)
})
}
fn bassoon_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"bassoon_A#0_p.flac" => 58.31,
"bassoon_F1_p.flac" => 87.26,
"bassoon_C2_p.flac" => 130.70,
"bassoon_G2_p.flac" => 195.98,
"bassoon_D#3_p.flac" => 312.70,
"bassoon_C4_p.flac" => 523.95,
)
})
}
fn bassoon_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"bassoon_A#0_f.flac" => 58.25,
"bassoon_F1_f.flac" => 87.29,
"bassoon_C2_f.flac" => 130.76,
"bassoon_G2_f.flac" => 195.94,
"bassoon_D#3_f.flac" => 311.06,
"bassoon_C4_f.flac" => 523.15,
)
})
}
fn clarinet_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"clarinet_A#2_p.flac" => 232.55,
"clarinet_D3_p.flac" => 293.36,
"clarinet_F3_p.flac" => 349.08,
"clarinet_A#3_p.flac" => 466.23,
"clarinet_D4_p.flac" => 586.91,
"clarinet_F4_p.flac" => 698.75,
)
})
}
fn clarinet_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"clarinet_A#2_f.flac" => 233.09,
"clarinet_D3_f.flac" => 292.97,
"clarinet_F3_f.flac" => 348.60,
"clarinet_A#3_f.flac" => 466.38,
"clarinet_D4_f.flac" => 587.88,
"clarinet_F4_f.flac" => 700.07,
)
})
}
fn steel() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"steel_E2.flac" => 81.98,
"steel_A#2.flac" => 116.33,
"steel_E3.flac" => 164.56,
"steel_A#3.flac" => 231.86,
"steel_E4.flac" => 327.92,
"steel_B4.flac" => 490.67,
"steel_F5.flac" => 693.98,
"steel_B5.flac" => 978.93,
)
})
}
fn eastpick() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"eastpick_E2.flac" => 83.56,
"eastpick_B2.flac" => 124.31,
"eastpick_E3.flac" => 166.21,
"eastpick_A#3.flac" => 233.27,
"eastpick_E4.flac" => 331.56,
"eastpick_A#4.flac" => 469.73,
"eastpick_F5.flac" => 702.02,
"eastpick_B5.flac" => 991.03,
)
})
}
fn eastpluck() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"eastpluck_E2.flac" => 83.33,
"eastpluck_A#2.flac" => 116.61,
"eastpluck_E3.flac" => 165.49,
"eastpluck_A#3.flac" => 232.21,
"eastpluck_E4.flac" => 330.31,
"eastpluck_B4.flac" => 495.27,
"eastpluck_F5.flac" => 699.35,
"eastpluck_B5.flac" => 987.59,
)
})
}
fn nylon() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"nylon_E2.flac" => 82.47,
"nylon_B2.flac" => 124.07,
"nylon_E3.flac" => 165.21,
"nylon_A#3.flac" => 234.63,
"nylon_E4.flac" => 326.69,
"nylon_A#4.flac" => 466.15,
"nylon_E5.flac" => 661.48,
)
})
}
fn strsec_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"celens_C1_p.flac" => 65.48,
"celens_G1_p.flac" => 97.46,
"celens_D2_p.flac" => 146.97,
"celens_A2_p.flac" => 219.85,
"celens_E3_p.flac" => 329.02,
"celens_B3_p.flac" => 493.92,
"vlnens_G2_p.flac" => 196.06,
"vlnens_D3_p.flac" => 293.66,
"vlnens_A3_p.flac" => 440.80,
"vlnens_E4_p.flac" => 660.20,
"vlnens_B4_p.flac" => 989.79,
"vlnens_D5_p.flac" => 1175.21,
)
})
}
fn strsec_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"celens_C1_f.flac" => 65.38,
"celens_G1_f.flac" => 97.71,
"celens_D2_f.flac" => 146.73,
"celens_A2_f.flac" => 219.17,
"celens_E3_f.flac" => 328.79,
"celens_B3_f.flac" => 493.91,
"vlnens_G2_f.flac" => 195.56,
"vlnens_D3_f.flac" => 292.71,
"vlnens_A3_f.flac" => 441.25,
"vlnens_E4_f.flac" => 657.60,
"vlnens_B4_f.flac" => 991.44,
"vlnens_D5_f.flac" => 1170.87,
)
})
}
fn contrabass_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"dbass_E1_p.flac" => 41.22,
"dbass_A#1_p.flac" => 57.95,
"dbass_E2_p.flac" => 82.31,
"dbass_A2_p.flac" => 108.96,
"dbass_C#3_p.flac" => 137.69,
"dbass_E3_p.flac" => 164.19,
"dbass_G#3_p.flac" => 207.89,
"dbass_B3_p.flac" => 244.89,
)
})
}
fn contrabass_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"dbass_E1_f.flac" => 41.15,
"dbass_A#1_f.flac" => 57.63,
"dbass_E2_f.flac" => 82.21,
"dbass_A2_f.flac" => 109.20,
"dbass_C#3_f.flac" => 138.30,
"dbass_E3_f.flac" => 164.89,
"dbass_G#3_f.flac" => 206.22,
"dbass_B3_f.flac" => 243.45,
)
})
}
pub fn contrabass_bank(vel: u8) -> &'static [Zone] {
if vel >= 80 {
contrabass_f()
} else {
contrabass_p()
}
}
fn pizzbass() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"pizzbass_E1.flac" => 41.09,
"pizzbass_G1.flac" => 48.60,
"pizzbass_A#1.flac" => 58.25,
"pizzbass_C2.flac" => 65.04,
"pizzbass_E2.flac" => 82.41,
"pizzbass_G#2.flac" => 102.12,
"pizzbass_A2.flac" => 112.51,
"pizzbass_G#3.flac" => 205.95,
)
})
}
pub fn pizzbass_bank() -> &'static [Zone] {
pizzbass()
}
fn finger_bass() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"fingerbass_E1.flac" => 41.22,
"fingerbass_F#1.flac" => 46.84,
"fingerbass_G#1.flac" => 52.12,
"fingerbass_A#1.flac" => 58.69,
"fingerbass_C2.flac" => 65.40,
"fingerbass_D2.flac" => 73.27,
)
})
}
pub fn finger_bass_bank() -> &'static [Zone] {
finger_bass()
}
fn pick_bass() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"pickbass_E1.flac" => 41.62,
"pickbass_F#1.flac" => 47.19,
"pickbass_G#1.flac" => 52.55,
"pickbass_A#1.flac" => 58.70,
"pickbass_C2.flac" => 65.83,
"pickbass_D2.flac" => 73.70,
"pickbass_E2.flac" => 82.13,
)
})
}
pub fn pick_bass_bank() -> &'static [Zone] {
pick_bass()
}
fn rhodes() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"rhodes_E1.flac" => 41.21,
"rhodes_A#1.flac" => 58.24,
"rhodes_C#2.flac" => 69.26,
"rhodes_E2.flac" => 82.35,
"rhodes_G#2.flac" => 104.18,
"rhodes_C#3.flac" => 138.59,
"rhodes_D#5.flac" => 622.77,
"rhodes_E5.flac" => 659.30,
"rhodes_G#5.flac" => 826.52,
"rhodes_A#5.flac" => 931.19,
"rhodes_C#6.flac" => 1106.02,
)
})
}
pub fn rhodes_bank() -> &'static [Zone] {
rhodes()
}
fn dulcimer_la() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"dulcimer_C#4.flac" => 280.62,
"dulcimer_D4.flac" => 295.17,
"dulcimer_E4.flac" => 330.95,
"dulcimer_F#4.flac" => 373.89,
"dulcimer_G4.flac" => 394.73,
"dulcimer_A4.flac" => 442.73,
"dulcimer_B4.flac" => 496.11,
"dulcimer_C5.flac" => 525.46,
"dulcimer_D5.flac" => 594.66,
)
})
}
pub fn dulcimer_bank() -> &'static [Zone] {
dulcimer_la()
}
fn musicbox() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"musicbox_E5.flac" => 668.51,
"musicbox_A5.flac" => 873.59,
"musicbox_B5.flac" => 983.03,
"musicbox_C6.flac" => 1049.43,
"musicbox_D6.flac" => 1190.81,
"musicbox_E6.flac" => 1328.91,
"musicbox_F6.flac" => 1412.39,
"musicbox_G#6.flac" => 1686.54,
"musicbox_A6.flac" => 1784.92,
"musicbox_B6.flac" => 2006.59,
"musicbox_C7.flac" => 2127.34,
)
})
}
pub fn musicbox_bank() -> &'static [Zone] {
musicbox()
}
fn cello_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"cellosolo_C2_p.flac" => 65.50,
"cellosolo_A2_p.flac" => 109.19,
"cellosolo_C3_p.flac" => 130.82,
"cellosolo_A3_p.flac" => 215.89,
"cellosolo_C4_p.flac" => 261.79,
"cellosolo_A4_p.flac" => 439.37,
"cellosolo_C5_p.flac" => 522.21,
"cellosolo_F#5_p.flac" => 739.94,
)
})
}
fn cello_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"cellosolo_C2_f.flac" => 65.29,
"cellosolo_A2_f.flac" => 109.39,
"cellosolo_C3_f.flac" => 130.50,
"cellosolo_A3_f.flac" => 220.00,
"cellosolo_C4_f.flac" => 261.55,
"cellosolo_A4_f.flac" => 439.31,
"cellosolo_C5_f.flac" => 521.47,
"cellosolo_F#5_f.flac" => 742.74,
)
})
}
pub fn cello_bank(vel: u8) -> &'static [Zone] {
if vel >= 80 {
cello_f()
} else {
cello_p()
}
}
pub fn piano_bank(vel: u8, rr2: bool) -> &'static [Zone] {
match (vel, rr2) {
(0..=51, false) => piano_pp(),
(0..=51, true) => piano_pp_rr2(),
(52..=95, false) => piano_mf(),
(52..=95, true) => piano_mf_rr2(),
(_, false) => piano_f(),
(_, true) => piano_f_rr2(),
}
}
fn grand_pp() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"grand_C2_pp.flac" => 65.23,
"grand_F#2_pp.flac" => 92.00,
"grand_C3_pp.flac" => 130.39,
"grand_F#3_pp.flac" => 184.74,
"grand_C4_pp.flac" => 261.50,
"grand_F#4_pp.flac" => 369.37,
"grand_C5_pp.flac" => 524.09,
"grand_F#5_pp.flac" => 741.81,
"grand_C6_pp.flac" => 1051.21,
)
})
}
fn grand_pp_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"grand_C2_pp_rr2.flac" => 65.34,
"grand_F#2_pp_rr2.flac" => 92.14,
"grand_C3_pp_rr2.flac" => 130.39,
"grand_F#3_pp_rr2.flac" => 184.80,
"grand_C4_pp_rr2.flac" => 261.52,
"grand_F#4_pp_rr2.flac" => 369.43,
"grand_C5_pp_rr2.flac" => 524.17,
"grand_F#5_pp_rr2.flac" => 741.84,
"grand_C6_pp_rr2.flac" => 1051.95,
)
})
}
fn grand_mf() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"grand_C2_mf.flac" => 65.62,
"grand_F#2_mf.flac" => 92.38,
"grand_C3_mf.flac" => 130.45,
"grand_F#3_mf.flac" => 185.07,
"grand_C4_mf.flac" => 262.03,
"grand_F#4_mf.flac" => 370.25,
"grand_C5_mf.flac" => 524.71,
"grand_F#5_mf.flac" => 741.85,
"grand_C6_mf.flac" => 1053.01,
)
})
}
fn grand_mf_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"grand_C2_mf_rr2.flac" => 65.65,
"grand_F#2_mf_rr2.flac" => 92.49,
"grand_C3_mf_rr2.flac" => 130.47,
"grand_F#3_mf_rr2.flac" => 185.08,
"grand_C4_mf_rr2.flac" => 262.08,
"grand_F#4_mf_rr2.flac" => 370.29,
"grand_C5_mf_rr2.flac" => 524.79,
"grand_F#5_mf_rr2.flac" => 741.87,
"grand_C6_mf_rr2.flac" => 1053.05,
)
})
}
fn grand_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"grand_C2_f.flac" => 65.72,
"grand_F#2_f.flac" => 92.85,
"grand_C3_f.flac" => 130.77,
"grand_F#3_f.flac" => 185.32,
"grand_C4_f.flac" => 262.90,
"grand_F#4_f.flac" => 370.52,
"grand_C5_f.flac" => 525.38,
"grand_F#5_f.flac" => 741.92,
"grand_C6_f.flac" => 1054.71,
)
})
}
fn grand_f_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"grand_C2_f_rr2.flac" => 65.68,
"grand_F#2_f_rr2.flac" => 92.97,
"grand_C3_f_rr2.flac" => 130.85,
"grand_F#3_f_rr2.flac" => 185.42,
"grand_C4_f_rr2.flac" => 262.81,
"grand_F#4_f_rr2.flac" => 370.56,
"grand_C5_f_rr2.flac" => 525.99,
"grand_F#5_f_rr2.flac" => 742.05,
"grand_C6_f_rr2.flac" => 1055.09,
)
})
}
pub fn grand_bank(vel: u8, rr2: bool) -> &'static [Zone] {
match (vel, rr2) {
(0..=51, false) => grand_pp(),
(0..=51, true) => grand_pp_rr2(),
(52..=95, false) => grand_mf(),
(52..=95, true) => grand_mf_rr2(),
(_, false) => grand_f(),
(_, true) => grand_f_rr2(),
}
}
fn steinwayb_pp() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"steinwayb_C2_pp.flac" => 65.88,
"steinwayb_F#2_pp.flac" => 92.54,
"steinwayb_C3_pp.flac" => 130.91,
"steinwayb_F#3_pp.flac" => 185.51,
"steinwayb_C4_pp.flac" => 262.25,
"steinwayb_F#4_pp.flac" => 371.37,
"steinwayb_C5_pp.flac" => 525.53,
"steinwayb_F#5_pp.flac" => 739.00,
"steinwayb_C6_pp.flac" => 1049.52,
)
})
}
fn steinwayb_pp_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"steinwayb_C2_pp_rr2.flac" => 65.79,
"steinwayb_F#2_pp_rr2.flac" => 92.40,
"steinwayb_C3_pp_rr2.flac" => 131.27,
"steinwayb_F#3_pp_rr2.flac" => 185.91,
"steinwayb_C4_pp_rr2.flac" => 262.92,
"steinwayb_F#4_pp_rr2.flac" => 372.55,
"steinwayb_C5_pp_rr2.flac" => 526.49,
"steinwayb_F#5_pp_rr2.flac" => 740.67,
"steinwayb_C6_pp_rr2.flac" => 1049.34,
)
})
}
fn steinwayb_mf() -> &'static [Zone] {
steinwayb_pp_rr2()
}
fn steinwayb_mf_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"steinwayb_C2_mf_rr2.flac" => 65.67,
"steinwayb_F#2_mf_rr2.flac" => 92.39,
"steinwayb_C3_mf_rr2.flac" => 131.39,
"steinwayb_F#3_mf_rr2.flac" => 185.95,
"steinwayb_C4_mf_rr2.flac" => 262.84,
"steinwayb_F#4_mf_rr2.flac" => 372.49,
"steinwayb_C5_mf_rr2.flac" => 526.67,
"steinwayb_F#5_mf_rr2.flac" => 743.63,
"steinwayb_C6_mf_rr2.flac" => 1049.46,
)
})
}
fn steinwayb_f() -> &'static [Zone] {
steinwayb_mf_rr2()
}
fn steinwayb_f_rr2() -> &'static [Zone] {
steinwayb_pp_rr2()
}
pub fn steinwayb_bank(vel: u8, rr2: bool) -> &'static [Zone] {
match (vel, rr2) {
(0..=51, false) => steinwayb_pp(),
(0..=51, true) => steinwayb_pp_rr2(),
(52..=95, false) => steinwayb_mf(),
(52..=95, true) => steinwayb_mf_rr2(),
(_, false) => steinwayb_f(),
(_, true) => steinwayb_f_rr2(),
}
}
fn kawai_pp() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"kawai_C2_pp.flac" => 64.82,
"kawai_A2_pp.flac" => 109.29,
"kawai_C3_pp.flac" => 130.04,
"kawai_A#3_pp.flac" => 231.94,
"kawai_C4_pp.flac" => 260.88,
"kawai_A#4_pp.flac" => 464.44,
"kawai_C5_pp.flac" => 522.09,
"kawai_C6_pp.flac" => 1046.16,
)
})
}
fn kawai_pp_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"kawai_C2_pp_rr2.flac" => 65.12,
"kawai_A2_pp_rr2.flac" => 109.50,
"kawai_C3_pp_rr2.flac" => 130.47,
"kawai_A#3_pp_rr2.flac" => 231.60,
"kawai_C4_pp_rr2.flac" => 261.05,
"kawai_A#4_pp_rr2.flac" => 464.18,
"kawai_C5_pp_rr2.flac" => 521.86,
"kawai_C6_pp_rr2.flac" => 1045.95,
)
})
}
fn kawai_mf() -> &'static [Zone] {
kawai_pp_rr2()
}
fn kawai_mf_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"kawai_C2_mf_rr2.flac" => 65.51,
"kawai_A2_mf_rr2.flac" => 109.95,
"kawai_C3_mf_rr2.flac" => 131.34,
"kawai_A#3_mf_rr2.flac" => 233.31,
"kawai_C4_mf_rr2.flac" => 261.90,
"kawai_A#4_mf_rr2.flac" => 466.39,
"kawai_C5_mf_rr2.flac" => 522.87,
"kawai_C6_mf_rr2.flac" => 1046.29,
)
})
}
fn kawai_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"kawai_C2_f.flac" => 65.64,
"kawai_A2_f.flac" => 110.15,
"kawai_C3_f.flac" => 131.01,
"kawai_A#3_f.flac" => 234.22,
"kawai_C4_f.flac" => 262.53,
"kawai_A#4_f.flac" => 467.69,
"kawai_C5_f.flac" => 524.03,
"kawai_C6_f.flac" => 1044.80,
)
})
}
fn kawai_f_rr2() -> &'static [Zone] {
kawai_mf_rr2()
}
pub fn kawai_bank(vel: u8, rr2: bool) -> &'static [Zone] {
match (vel, rr2) {
(0..=51, false) => kawai_pp(),
(0..=51, true) => kawai_pp_rr2(),
(52..=95, false) => kawai_mf(),
(52..=95, true) => kawai_mf_rr2(),
(_, false) => kawai_f(),
(_, true) => kawai_f_rr2(),
}
}
fn headroom_pp() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"headroom_C2_pp.flac" => 65.50,
"headroom_F#2_pp.flac" => 92.37,
"headroom_C3_pp.flac" => 131.18,
"headroom_F#3_pp.flac" => 185.59,
"headroom_C4_pp.flac" => 262.46,
"headroom_F#4_pp.flac" => 372.02,
"headroom_C5_pp.flac" => 526.16,
"headroom_F#5_pp.flac" => 744.53,
"headroom_C6_pp.flac" => 1054.19,
)
})
}
fn headroom_pp_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"headroom_C2_pp_rr2.flac" => 66.25,
"headroom_F#2_pp_rr2.flac" => 92.77,
"headroom_C3_pp_rr2.flac" => 131.60,
"headroom_F#3_pp_rr2.flac" => 185.93,
"headroom_C4_pp_rr2.flac" => 262.50,
"headroom_F#4_pp_rr2.flac" => 372.24,
"headroom_C5_pp_rr2.flac" => 526.29,
"headroom_F#5_pp_rr2.flac" => 745.36,
"headroom_C6_pp_rr2.flac" => 1054.02,
)
})
}
fn headroom_mf() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"headroom_C2_mf.flac" => 66.43,
"headroom_F#2_mf.flac" => 93.07,
"headroom_C3_mf.flac" => 132.09,
"headroom_F#3_mf.flac" => 186.21,
"headroom_C4_mf.flac" => 262.55,
"headroom_F#4_mf.flac" => 372.58,
"headroom_C5_mf.flac" => 526.52,
"headroom_F#5_mf.flac" => 745.43,
"headroom_C6_mf.flac" => 1054.45,
)
})
}
fn headroom_mf_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"headroom_C2_mf_rr2.flac" => 66.33,
"headroom_F#2_mf_rr2.flac" => 93.82,
"headroom_C3_mf_rr2.flac" => 132.37,
"headroom_F#3_mf_rr2.flac" => 186.66,
"headroom_C4_mf_rr2.flac" => 262.66,
"headroom_F#4_mf_rr2.flac" => 372.72,
"headroom_C5_mf_rr2.flac" => 526.85,
"headroom_F#5_mf_rr2.flac" => 745.73,
"headroom_C6_mf_rr2.flac" => 1055.27,
)
})
}
fn headroom_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"headroom_C2_f.flac" => 66.23,
"headroom_F#2_f.flac" => 94.08,
"headroom_C3_f.flac" => 132.59,
"headroom_F#3_f.flac" => 186.86,
"headroom_C4_f.flac" => 262.79,
"headroom_F#4_f.flac" => 372.89,
"headroom_C5_f.flac" => 527.10,
"headroom_F#5_f.flac" => 746.62,
"headroom_C6_f.flac" => 1055.91,
)
})
}
fn headroom_f_rr2() -> &'static [Zone] {
headroom_mf_rr2()
}
pub fn headroom_bank(vel: u8, rr2: bool) -> &'static [Zone] {
match (vel, rr2) {
(0..=51, false) => headroom_pp(),
(0..=51, true) => headroom_pp_rr2(),
(52..=95, false) => headroom_mf(),
(52..=95, true) => headroom_mf_rr2(),
(_, false) => headroom_f(),
(_, true) => headroom_f_rr2(),
}
}
fn musescoregrand_zones() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"musescoregrand_B1.flac" => 62.20,
"musescoregrand_D2.flac" => 74.09,
"musescoregrand_E2.flac" => 83.24,
"musescoregrand_G2.flac" => 98.56,
"musescoregrand_A2.flac" => 110.65,
"musescoregrand_B2.flac" => 124.31,
"musescoregrand_C3.flac" => 131.54,
"musescoregrand_D3.flac" => 147.97,
"musescoregrand_E3.flac" => 165.70,
"musescoregrand_G3.flac" => 196.77,
"musescoregrand_A3.flac" => 221.51,
"musescoregrand_B3.flac" => 248.60,
"musescoregrand_C4.flac" => 263.02,
"musescoregrand_D4.flac" => 295.29,
"musescoregrand_E4.flac" => 331.03,
"musescoregrand_G4.flac" => 393.37,
"musescoregrand_A4.flac" => 441.71,
"musescoregrand_B4.flac" => 496.71,
"musescoregrand_C5.flac" => 524.28,
"musescoregrand_E5.flac" => 661.14,
"musescoregrand_G5.flac" => 786.25,
"musescoregrand_G#5.flac" => 832.82,
"musescoregrand_B5.flac" => 990.56,
"musescoregrand_C#6.flac" => 1112.86,
"musescoregrand_D#6.flac" => 1254.21,
)
})
}
pub fn musescoregrand_bank(_vel: u8, _rr2: bool) -> &'static [Zone] {
musescoregrand_zones()
}
fn ydpgrand_zones() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"ydpgrand_C2.flac" => 66.38,
"ydpgrand_F#2.flac" => 93.06,
"ydpgrand_C3.flac" => 131.75,
"ydpgrand_F#3.flac" => 186.71,
"ydpgrand_C4.flac" => 263.56,
"ydpgrand_F#4.flac" => 372.67,
"ydpgrand_C5.flac" => 530.39,
"ydpgrand_F#5.flac" => 747.92,
"ydpgrand_C6.flac" => 1053.83,
)
})
}
pub fn ydpgrand_bank(_vel: u8, _rr2: bool) -> &'static [Zone] {
ydpgrand_zones()
}
fn honkytonk_zones() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"honkytonk_C2.flac" => 66.64,
"honkytonk_F2.flac" => 88.64,
"honkytonk_C3.flac" => 131.73,
"honkytonk_F#3.flac" => 186.74,
"honkytonk_C4.flac" => 264.42,
"honkytonk_F4.flac" => 350.12,
"honkytonk_C5.flac" => 524.27,
"honkytonk_F#5.flac" => 739.95,
"honkytonk_C6.flac" => 1041.02,
)
})
}
pub fn honkytonk_bank(_vel: u8, _rr2: bool) -> &'static [Zone] {
honkytonk_zones()
}
fn b1_normal_zones() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
b1_bank!(
"b1_normal_C1.wav" => 31.71,
"b1_normal_E1.wav" => 40.28,
"b1_normal_G1.wav" => 47.86,
"b1_normal_B1.wav" => 60.91,
"b1_normal_D2.wav" => 72.53,
"b1_normal_F2.wav" => 86.54,
"b1_normal_A2.wav" => 108.90,
"b1_normal_C3.wav" => 130.11,
"b1_normal_E3.wav" => 163.54,
"b1_normal_G3.wav" => 194.82,
"b1_normal_B3.wav" => 245.03,
"b1_normal_D4.wav" => 291.58,
"b1_normal_F4.wav" => 347.39,
"b1_normal_A4.wav" => 437.40,
"b1_normal_C5.wav" => 521.81,
"b1_normal_E5.wav" => 655.74,
"b1_normal_G5.wav" => 782.45,
"b1_normal_B5.wav" => 983.28,
"b1_normal_D6.wav" => 1172.50,
"b1_normal_F6.wav" => 1390.01,
"b1_normal_A6.wav" => 1762.43,
"b1_normal_C7.wav" => 2076.71,
"b1_normal_E7.wav" => 2671.42,
"b1_normal_G7.wav" => 3149.78,
"b1_normal_B7.wav" => 3972.30,
)
})
}
fn b1_hard_zones() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
b1_bank!(
"b1_hard_A0.wav" => 26.77,
"b1_hard_C1.wav" => 31.73,
"b1_hard_E1.wav" => 40.28,
"b1_hard_G1.wav" => 48.03,
"b1_hard_B1.wav" => 60.98,
"b1_hard_D2.wav" => 72.63,
"b1_hard_F2.wav" => 86.68,
"b1_hard_A2.wav" => 108.93,
"b1_hard_C3.wav" => 130.33,
"b1_hard_E3.wav" => 163.51,
"b1_hard_G3.wav" => 195.12,
"b1_hard_B3.wav" => 245.29,
"b1_hard_D4.wav" => 291.92,
"b1_hard_F4.wav" => 347.83,
"b1_hard_A4.wav" => 438.14,
"b1_hard_C5.wav" => 522.30,
"b1_hard_E5.wav" => 656.13,
"b1_hard_G5.wav" => 782.37,
"b1_hard_B5.wav" => 983.98,
"b1_hard_D6.wav" => 1171.69,
"b1_hard_F6.wav" => 1385.93,
"b1_hard_A6.wav" => 1758.61,
"b1_hard_C7.wav" => 2068.31,
"b1_hard_E7.wav" => 2672.11,
"b1_hard_G7.wav" => 3152.84,
"b1_hard_B7.wav" => 3951.07,
"b1_hard_C8.wav" => 4255.38,
)
})
}
pub fn b1upright_bank(vel: u8, _rr2: bool) -> &'static [Zone] {
if vel < 60 {
b1_normal_zones()
} else {
b1_hard_zones()
}
}
#[cfg(feature = "embedded-samples")]
struct B1Sampled {
data: &'static [f32],
tail: &'static B1Tail,
pos: f32,
base_step: f32,
bend: f32,
gain: f32,
rel_mul: f32,
env: f32,
releasing: bool,
}
#[cfg(feature = "embedded-samples")]
pub fn b1upright_sampled(
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
release_t60: f32,
) -> Option<Box<dyn Voice>> {
let f = key_freq(key);
let zone = nearest(zones, f);
let ratio = f / zone.root;
if !(0.5..=2.05).contains(&ratio) {
return None;
}
Some(Box::new(B1Sampled {
data: zone.data.as_slice(),
tail: zone
.b1_tail
.as_ref()
.expect("embedded B1 zone has no natural tail"),
pos: 0.0,
base_step: ratio * 44_100.0 / sr,
bend: 1.0,
gain: gain * vel_amp(vel),
rel_mul: 10f32.powf(-3.0 / (release_t60 * sr)),
env: 1.0,
releasing: false,
}))
}
#[cfg(not(feature = "embedded-samples"))]
pub fn b1upright_sampled(
_zones: &'static [Zone],
_key: u8,
_vel: u8,
_sr: f32,
_gain: f32,
_release_t60: f32,
) -> Option<Box<dyn Voice>> {
None
}
#[cfg(feature = "embedded-samples")]
impl Voice for B1Sampled {
fn render(&mut self, out: &mut [f32]) -> bool {
let step = self.base_step * self.bend;
let entry = self.tail.entry_frame as f32;
let natural_end = (self.tail.entry_frame + self.tail.source_frames) as f32;
let crossfade_frames = (self.data.len() - self.tail.entry_frame) as f32;
for o in out {
if self.pos >= natural_end {
return false;
}
let j = self.pos as usize;
let frac = self.pos - j as f32;
let head = if j < self.data.len() {
Some(crate::dsp::cubic4(
self.data[j.saturating_sub(1)],
self.data[j],
self.data[(j + 1).min(self.data.len() - 1)],
self.data[(j + 2).min(self.data.len() - 1)],
frac,
))
} else {
None
};
let tail = if self.pos >= entry {
let tail_pos = (self.pos - entry) / self.tail.rate_divisor as f32;
let tail_index = tail_pos as usize;
if tail_index >= self.tail.data.len() {
return false;
}
let tail_frac = tail_pos - tail_index as f32;
let a = self.tail.data[tail_index];
let b = self.tail.data[(tail_index + 1).min(self.tail.data.len() - 1)];
let remaining = natural_end - self.pos;
let taper = (remaining / B1_TAIL_END_FADE_FRAMES).clamp(0.0, 1.0);
Some((a + (b - a) * tail_frac) * taper)
} else {
None
};
let value = match (head, tail) {
(Some(head), Some(tail)) => {
let mix = ((self.pos - entry) / crossfade_frames).clamp(0.0, 1.0);
head * (1.0 - mix) + tail * mix
}
(Some(head), None) => head,
(None, Some(tail)) => tail,
(None, None) => return false,
};
*o += value * self.gain * self.env;
self.pos += step;
if self.releasing {
self.env *= self.rel_mul;
if self.env < 1e-4 {
return false;
}
}
}
true
}
fn note_off(&mut self) {
self.releasing = true;
}
fn released(&self) -> bool {
self.releasing
}
fn set_pitch(&mut self, mult: f32) {
self.bend = mult;
}
#[cfg(test)]
fn kind(&self) -> &'static str {
"b1sampled"
}
}
pub fn violin_bank(vel: u8) -> &'static [Zone] {
if vel >= 80 {
violin_f()
} else {
violin_p()
}
}
fn viola_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"viola_C3_f.flac" => 130.50,
"viola_G3_f.flac" => 194.96,
"viola_D4_f.flac" => 292.40,
"viola_A4_f.flac" => 441.08,
"viola_E5_f.flac" => 660.40,
"viola_B5_f.flac" => 984.52,
"viola_D6_f.flac" => 1172.20,
)
})
}
fn viola_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"viola_C3_p.flac" => 130.59,
"viola_G3_p.flac" => 196.06,
"viola_D4_p.flac" => 292.94,
"viola_A4_p.flac" => 438.77,
"viola_E5_p.flac" => 659.14,
"viola_B5_p.flac" => 989.91,
"viola_D6_p.flac" => 1172.65,
)
})
}
pub fn viola_bank(vel: u8) -> &'static [Zone] {
if vel >= 80 {
viola_f()
} else {
viola_p()
}
}
fn marimba() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"marimba_F1.flac" => 43.68,
"marimba_C2.flac" => 65.62,
"marimba_G2.flac" => 98.00,
"marimba_B2.flac" => 123.40,
"marimba_F3.flac" => 174.45,
"marimba_C4.flac" => 262.15,
"marimba_G4.flac" => 391.46,
"marimba_B4.flac" => 493.90,
"marimba_F5.flac" => 697.02,
"marimba_C6.flac" => 1047.88,
)
})
}
pub fn marimba_bank() -> &'static [Zone] {
marimba()
}
fn xylo() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"xylo_G3.flac" => 195.77,
"xylo_C4.flac" => 264.37,
"xylo_G4.flac" => 395.62,
"xylo_C5.flac" => 528.10,
"xylo_G5.flac" => 789.28,
"xylo_C6.flac" => 1056.14,
"xylo_G6.flac" => 1584.39,
"xylo_C7.flac" => 2116.63,
)
})
}
pub fn xylo_bank() -> &'static [Zone] {
xylo()
}
fn glock() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"glock_C5.flac" => 524.98,
"glock_G5.flac" => 773.51,
"glock_G6.flac" => 1582.22,
"glock_C7.flac" => 2121.85,
)
})
}
pub fn glock_bank() -> &'static [Zone] {
glock()
}
fn vibes() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"vibes_A2.flac" => 112.07,
"vibes_C3.flac" => 129.70,
"vibes_E3.flac" => 164.64,
"vibes_G3.flac" => 195.90,
"vibes_B3.flac" => 246.78,
"vibes_D4.flac" => 293.56,
"vibes_F4.flac" => 349.08,
"vibes_A4.flac" => 439.95,
"vibes_C5.flac" => 523.13,
"vibes_E5.flac" => 659.19,
)
})
}
pub fn vibraphone_bank() -> &'static [Zone] {
vibes()
}
fn tubular() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"tubular_C4.flac" => 261.63,
"tubular_D4.flac" => 293.66,
"tubular_E4.flac" => 334.83,
"tubular_F4.flac" => 356.35,
"tubular_G4.flac" => 393.95,
"tubular_A4.flac" => 441.19,
"tubular_B4.flac" => 501.37,
"tubular_C5.flac" => 528.56,
"tubular_D5.flac" => 583.52,
)
})
}
pub fn tubular_bank() -> &'static [Zone] {
tubular()
}
fn celesta() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"celesta_F#3.flac" => 184.77,
"celesta_C4.flac" => 261.14,
"celesta_F#4.flac" => 370.10,
"celesta_C5.flac" => 523.20,
"celesta_F#5.flac" => 739.76,
"celesta_C6.flac" => 1052.66,
"celesta_F#6.flac" => 1481.45,
"celesta_C7.flac" => 2098.53,
)
})
}
fn brass_section() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"brasssection_F2.flac" => 87.28,
"brasssection_C3.flac" => 130.93,
"brasssection_F#3.flac" => 185.54,
"brasssection_A#3.flac" => 234.59,
"brasssection_C4.flac" => 262.50,
"brasssection_F4.flac" => 350.88,
"brasssection_A4.flac" => 441.64,
"brasssection_C5.flac" => 524.46,
"brasssection_F5.flac" => 700.31,
"brasssection_C6.flac" => 1049.46,
)
})
}
pub fn celesta_bank() -> &'static [Zone] {
celesta()
}
pub fn brass_section_bank() -> &'static [Zone] {
brass_section()
}
pub fn flute_bank() -> &'static [Zone] {
flute()
}
pub fn brass_bank(program: u8, vel: u8) -> &'static [Zone] {
let f = vel >= 80;
match program {
57 => {
if f {
trombone_f()
} else {
trombone_p()
}
}
58 => {
if f {
tuba_f()
} else {
tuba_p()
}
}
59 => {
if f {
mutetpt_f()
} else {
mutetpt_p()
}
}
60 => {
if f {
horn_f()
} else {
horn_p()
}
}
_ => {
if f {
trumpet_f()
} else {
trumpet_p()
}
}
}
}
pub fn reed_bank(program: u8, vel: u8) -> &'static [Zone] {
let f = vel >= 80;
match program {
70 => {
if f {
bassoon_f()
} else {
bassoon_p()
}
}
71 => {
if f {
clarinet_f()
} else {
clarinet_p()
}
}
_ => {
if f {
oboe_f()
} else {
oboe_p()
}
}
}
}
fn sax_sop_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_sop_G#3_p.flac" => 209.75,
"sax_sop_C4_p.flac" => 261.55,
"sax_sop_E4_p.flac" => 330.82,
"sax_sop_G#4_p.flac" => 419.59,
"sax_sop_C5_p.flac" => 530.91,
"sax_sop_E5_p.flac" => 665.84,
"sax_sop_G#5_p.flac" => 843.37,
"sax_sop_C6_p.flac" => 1069.46,
"sax_sop_E6_p.flac" => 1329.38,
)
})
}
fn sax_sop_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_sop_G#3_f.flac" => 209.85,
"sax_sop_C4_f.flac" => 261.43,
"sax_sop_E4_f.flac" => 332.00,
"sax_sop_G#4_f.flac" => 419.58,
"sax_sop_C5_f.flac" => 529.13,
"sax_sop_E5_f.flac" => 667.07,
"sax_sop_G#5_f.flac" => 842.03,
"sax_sop_C6_f.flac" => 1069.69,
"sax_sop_E6_f.flac" => 1344.32,
)
})
}
fn sax_alt_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_alt_C#3_p.flac" => 139.90,
"sax_alt_F3_p.flac" => 173.47,
"sax_alt_A3_p.flac" => 219.58,
"sax_alt_C#4_p.flac" => 277.31,
"sax_alt_F4_p.flac" => 353.06,
"sax_alt_A4_p.flac" => 440.99,
"sax_alt_C#5_p.flac" => 560.01,
"sax_alt_F5_p.flac" => 707.98,
"sax_alt_A5_p.flac" => 890.68,
)
})
}
fn sax_alt_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_alt_C#3_f.flac" => 140.43,
"sax_alt_F3_f.flac" => 174.15,
"sax_alt_A3_f.flac" => 220.50,
"sax_alt_C#4_f.flac" => 279.95,
"sax_alt_F4_f.flac" => 355.08,
"sax_alt_A4_f.flac" => 444.03,
"sax_alt_C#5_f.flac" => 562.81,
"sax_alt_F5_f.flac" => 710.03,
"sax_alt_A5_f.flac" => 889.20,
)
})
}
fn sax_ten_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_ten_G#2_p.flac" => 104.63,
"sax_ten_C3_p.flac" => 131.41,
"sax_ten_E3_p.flac" => 166.22,
"sax_ten_G#3_p.flac" => 208.67,
"sax_ten_C4_p.flac" => 263.70,
"sax_ten_E4_p.flac" => 332.76,
"sax_ten_G#4_p.flac" => 419.05,
"sax_ten_C5_p.flac" => 528.68,
"sax_ten_E5_p.flac" => 673.20,
)
})
}
fn sax_ten_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_ten_G#2_f.flac" => 104.54,
"sax_ten_C3_f.flac" => 131.32,
"sax_ten_E3_f.flac" => 166.28,
"sax_ten_G#3_f.flac" => 209.25,
"sax_ten_C4_f.flac" => 263.37,
"sax_ten_E4_f.flac" => 331.10,
"sax_ten_G#4_f.flac" => 417.77,
"sax_ten_C5_f.flac" => 527.47,
"sax_ten_E5_f.flac" => 674.90,
)
})
}
fn sax_bar_p() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_bar_C2_p.flac" => 69.65,
"sax_bar_E2_p.flac" => 82.52,
"sax_bar_G#2_p.flac" => 103.47,
"sax_bar_C3_p.flac" => 130.04,
"sax_bar_E3_p.flac" => 163.27,
"sax_bar_C4_p.flac" => 263.75,
"sax_bar_E4_p.flac" => 335.01,
"sax_bar_G#4_p.flac" => 421.23,
"sax_bar_A4_p.flac" => 450.50,
)
})
}
fn sax_bar_f() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sax_bar_C2_f.flac" => 65.83,
"sax_bar_E2_f.flac" => 82.38,
"sax_bar_G#2_f.flac" => 103.46,
"sax_bar_C3_f.flac" => 130.22,
"sax_bar_E3_f.flac" => 162.90,
"sax_bar_C4_f.flac" => 261.13,
"sax_bar_E4_f.flac" => 334.08,
"sax_bar_G#4_f.flac" => 420.54,
"sax_bar_A4_f.flac" => 448.84,
)
})
}
pub fn sax_bank(program: u8, vel: u8) -> &'static [Zone] {
let f = vel >= 80;
match program {
64 => {
if f {
sax_sop_f()
} else {
sax_sop_p()
}
}
66 => {
if f {
sax_ten_f()
} else {
sax_ten_p()
}
}
67 => {
if f {
sax_bar_f()
} else {
sax_bar_p()
}
}
_ => {
if f {
sax_alt_f()
} else {
sax_alt_p()
}
}
}
}
pub fn strings_bank(vel: u8) -> &'static [Zone] {
if vel >= 80 {
strsec_f()
} else {
strsec_p()
}
}
pub fn guitar_bank() -> &'static [Zone] {
nylon()
}
pub fn steel_bank() -> &'static [Zone] {
steel()
}
pub fn eastman_picked_bank() -> &'static [Zone] {
eastpick()
}
pub fn eastman_plucked_bank() -> &'static [Zone] {
eastpluck()
}
fn harpsichord() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"harpsi_C2.flac" => 65.64,
"harpsi_F2.flac" => 87.44,
"harpsi_C3.flac" => 131.38,
"harpsi_F3.flac" => 175.14,
"harpsi_C4.flac" => 262.36,
"harpsi_F4.flac" => 349.68,
"harpsi_C5.flac" => 524.79,
"harpsi_F5.flac" => 699.37,
"harpsi_C6.flac" => 1045.81,
"harpsi_F6.flac" => 1394.14,
)
})
}
pub fn harpsichord_bank() -> &'static [Zone] {
harpsichord()
}
fn harp() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"harp_G1.flac" => 48.47,
"harp_D2.flac" => 73.35,
"harp_A2.flac" => 110.11,
"harp_E3.flac" => 164.84,
"harp_B3.flac" => 245.33,
"harp_F4.flac" => 348.62,
"harp_C5.flac" => 520.04,
"harp_G5.flac" => 779.98,
"harp_D6.flac" => 1165.80,
"harp_A6.flac" => 1739.41,
"harp_F7.flac" => 2744.97,
)
})
}
pub fn harp_bank() -> &'static [Zone] {
harp()
}
fn ocarina() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"ocarina_E4.flac" => 329.00,
"ocarina_G#4.flac" => 414.48,
"ocarina_C5.flac" => 519.23,
)
})
}
pub fn ocarina_bank() -> &'static [Zone] {
ocarina()
}
fn recorder() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"recorder_F3.flac" => 174.67,
"recorder_A#3.flac" => 232.62,
"recorder_E4.flac" => 329.88,
"recorder_A#4.flac" => 467.80,
"recorder_E5.flac" => 659.37,
"recorder_A#5.flac" => 937.94,
"recorder_C6.flac" => 1051.96,
)
})
}
pub fn recorder_bank() -> &'static [Zone] {
recorder()
}
fn timpani() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"timpani_A#1.flac" => 57.05,
"timpani_F2.flac" => 88.17,
"timpani_G#2.flac" => 105.64,
"timpani_D3.flac" => 142.91,
"timpani_F3.flac" => 173.12,
)
})
}
pub fn timpani_bank() -> &'static [Zone] {
timpani()
}
fn sitar() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"sitar_E3.flac" => 165.05,
"sitar_G3.flac" => 196.17,
"sitar_C4.flac" => 262.53,
"sitar_E4.flac" => 330.02,
"sitar_C5.flac" => 524.23,
"sitar_E5.flac" => 659.16,
"sitar_C6.flac" => 1049.01,
"sitar_G6.flac" => 1587.62,
)
})
}
pub fn sitar_bank() -> &'static [Zone] {
sitar()
}
fn banjo() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"banjo_D#3.flac" => 156.43,
"banjo_F3.flac" => 176.25,
"banjo_G3.flac" => 196.19,
"banjo_G#3.flac" => 209.28,
"banjo_A3.flac" => 221.81,
"banjo_A#3.flac" => 235.29,
"banjo_B3.flac" => 247.19,
"banjo_C4.flac" => 263.44,
"banjo_D4.flac" => 293.46,
"banjo_D#4.flac" => 314.67,
"banjo_E4.flac" => 330.58,
"banjo_F4.flac" => 351.26,
"banjo_F#4.flac" => 371.23,
"banjo_G4.flac" => 390.90,
"banjo_G#4.flac" => 414.57,
"banjo_A4.flac" => 440.86,
"banjo_A#4.flac" => 468.37,
"banjo_C5.flac" => 526.41,
"banjo_C#5.flac" => 560.81,
"banjo_D5.flac" => 593.49,
"banjo_D#5.flac" => 629.48,
"banjo_E5.flac" => 669.23,
"banjo_G5.flac" => 805.64,
"banjo_G#5.flac" => 850.14,
)
})
}
pub fn banjo_bank() -> &'static [Zone] {
banjo()
}
fn mandolin_rr1() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"mandolin_G3_rr1.flac" => 195.57,
"mandolin_C4_rr1.flac" => 264.95,
"mandolin_D4_rr1.flac" => 292.93,
"mandolin_G4_rr1.flac" => 394.37,
"mandolin_A4_rr1.flac" => 440.41,
"mandolin_D5_rr1.flac" => 591.60,
"mandolin_E5_rr1.flac" => 658.52,
"mandolin_A5_rr1.flac" => 883.17,
"mandolin_D6_rr1.flac" => 1182.91,
"mandolin_E6_rr1.flac" => 1328.92,
)
})
}
fn mandolin_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"mandolin_G3_rr2.flac" => 195.74,
"mandolin_C4_rr2.flac" => 264.82,
"mandolin_D4_rr2.flac" => 292.65,
"mandolin_G4_rr2.flac" => 395.11,
"mandolin_A4_rr2.flac" => 439.28,
"mandolin_D5_rr2.flac" => 591.17,
"mandolin_E5_rr2.flac" => 656.88,
"mandolin_A5_rr2.flac" => 883.72,
"mandolin_D6_rr2.flac" => 1181.88,
"mandolin_E6_rr2.flac" => 1328.13,
)
})
}
fn mandolin_rr3() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"mandolin_G3_rr3.flac" => 195.84,
"mandolin_C4_rr3.flac" => 264.59,
"mandolin_D4_rr3.flac" => 293.03,
"mandolin_G4_rr3.flac" => 394.68,
"mandolin_A4_rr3.flac" => 440.40,
"mandolin_D5_rr3.flac" => 591.36,
"mandolin_E5_rr3.flac" => 657.51,
"mandolin_A5_rr3.flac" => 882.60,
"mandolin_D6_rr3.flac" => 1181.48,
"mandolin_E6_rr3.flac" => 1327.97,
)
})
}
fn mandolin_rr4() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"mandolin_G3_rr4.flac" => 195.61,
"mandolin_C4_rr4.flac" => 264.64,
"mandolin_D4_rr4.flac" => 293.33,
"mandolin_G4_rr4.flac" => 394.98,
"mandolin_A4_rr4.flac" => 439.17,
"mandolin_D5_rr4.flac" => 590.84,
"mandolin_E5_rr4.flac" => 656.93,
"mandolin_A5_rr4.flac" => 883.22,
"mandolin_D6_rr4.flac" => 1181.42,
"mandolin_E6_rr4.flac" => 1327.72,
)
})
}
pub const MANDOLIN_ROUND_ROBINS: usize = 4;
pub fn mandolin_bank(rr: usize) -> &'static [Zone] {
match rr % MANDOLIN_ROUND_ROBINS {
0 => mandolin_rr1(),
1 => mandolin_rr2(),
2 => mandolin_rr3(),
_ => mandolin_rr4(),
}
}
fn panflute() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"panflute_F#3.flac" => 184.12,
"panflute_C4.flac" => 261.21,
"panflute_F#4.flac" => 368.91,
"panflute_C5.flac" => 523.07,
"panflute_F#5.flac" => 740.14,
"panflute_C6.flac" => 1046.07,
"panflute_F#6.flac" => 1478.67,
"panflute_C7.flac" => 2092.01,
)
})
}
pub fn panflute_bank() -> &'static [Zone] {
panflute()
}
fn bottle() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, bank!("bottle_C6.flac" => 1056.51))
}
pub fn bottle_bank() -> &'static [Zone] {
bottle()
}
fn shakuhachi() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, bank!("shakuhachi_C5.flac" => 521.06))
}
pub fn shakuhachi_bank() -> &'static [Zone] {
shakuhachi()
}
fn chanter() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"chanter_F4.flac" => 341.37,
"chanter_G4.flac" => 383.74,
"chanter_A4.flac" => 432.55,
"chanter_A#4.flac" => 454.57,
"chanter_B4.flac" => 481.39,
"chanter_C5.flac" => 512.01,
"chanter_C#5.flac" => 538.52,
"chanter_D5.flac" => 578.53,
"chanter_F#5.flac" => 716.27,
"chanter_G5.flac" => 771.47,
)
})
}
fn chanter_rr2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"chanter_A4_rr2.flac" => 431.28,
"chanter_A#4_rr2.flac" => 454.56,
"chanter_B4_rr2.flac" => 482.11,
"chanter_C5_rr2.flac" => 512.34,
"chanter_D5_rr2.flac" => 578.09,
)
})
}
fn drone_g2() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, bank!("drone_G2.flac" => 98.22))
}
fn drone_g3() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, bank!("drone_G3.flac" => 196.21))
}
pub fn chanter_bank() -> &'static [Zone] {
chanter()
}
pub fn drone_g2_bank() -> &'static [Zone] {
drone_g2()
}
pub fn drone_g3_bank() -> &'static [Zone] {
drone_g3()
}
pub struct LoopVoice {
data: &'static [f32],
pos: f64,
step: f64,
gain: f32,
env: crate::dsp::Adsr,
drift: f32,
drift_target: f32,
t: u32,
rng: crate::dsp::Rng,
#[cfg(test)]
name: &'static str,
}
impl LoopVoice {
#[allow(clippy::too_many_arguments)]
pub fn new(
zones: &'static [Zone],
target_hz: f32,
sr: f32,
gain: f32,
attack: f32,
release: f32,
seed: u32,
#[cfg_attr(not(test), allow(unused_variables))] name: &'static str,
) -> Self {
let zone = nearest(zones, target_hz);
let ratio = (target_hz / zone.root).clamp(0.5, 2.0);
let step = (ratio * 44100.0 / sr) as f64;
LoopVoice {
data: zone.data.as_slice(),
pos: 0.0,
step,
gain,
env: crate::dsp::Adsr::new(attack, 0.0, 1.0, release, sr),
drift: 0.0,
drift_target: 0.0,
t: 0,
rng: crate::dsp::Rng::new(seed ^ 0xBA6_71FE),
#[cfg(test)]
name,
}
}
}
impl Voice for LoopVoice {
fn render(&mut self, out: &mut [f32]) -> bool {
let n = self.data.len();
for o in out.iter_mut() {
let e = self.env.next();
let j = self.pos as usize;
let frac = (self.pos - j as f64) as f32;
let v = crate::dsp::cubic4(
self.data[(j + n - 1) % n],
self.data[j],
self.data[(j + 1) % n],
self.data[(j + 2) % n],
frac,
);
*o += v * self.gain * e;
if self.t.is_multiple_of(SAX_DRIFT_SAMP) {
self.drift_target = SAX_DRIFT_MAX * self.rng.white();
}
self.drift += 0.002 * (self.drift_target - self.drift);
self.t = self.t.wrapping_add(1);
self.pos += self.step * (1.0 + self.drift) as f64;
if self.pos >= n as f64 {
self.pos -= n as f64;
}
}
self.env.alive()
}
fn note_off(&mut self) {
self.env.release();
}
fn released(&self) -> bool {
self.env.released()
}
#[cfg(test)]
fn kind(&self) -> &'static str {
self.name
}
}
pub fn bagpipe_chanter_loop(key: u8, sr: f32, seed: u32) -> LoopVoice {
let bank = if seed & 1 == 1 {
chanter_rr2()
} else {
chanter()
};
LoopVoice::new(
bank,
key_freq(key),
sr,
BAGPIPE_CHANTER_GAIN,
0.010,
0.11,
seed,
"bagpipe_chanter",
)
}
pub const BAGPIPE_CHANTER_GAIN: f32 = 0.85;
pub const BAGPIPE_DRONE_GAIN: f32 = 0.30;
pub fn prewarm() {
if !crate::embedded_samples_available() {
return;
}
#[cfg(feature = "embedded-samples")]
{
ferrosintesis_samples_drumkit::prewarm();
ferrosintesis_samples_drumkit2::prewarm();
}
for &(vel, rr2) in &[
(1u8, false),
(1, true),
(80, false),
(80, true),
(127, false),
(127, true),
] {
let _ = piano_bank(vel, rr2);
let _ = grand_bank(vel, rr2);
let _ = steinwayb_bank(vel, rr2);
let _ = kawai_bank(vel, rr2);
let _ = headroom_bank(vel, rr2);
let _ = musescoregrand_bank(vel, rr2);
let _ = ydpgrand_bank(vel, rr2);
let _ = honkytonk_bank(vel, rr2);
let _ = b1upright_bank(vel, rr2);
}
for vel in [1u8, 127] {
let _ = violin_bank(vel);
let _ = viola_bank(vel);
let _ = cello_bank(vel);
let _ = contrabass_bank(vel);
let _ = strings_bank(vel);
}
for rr in 0..MANDOLIN_ROUND_ROBINS {
let _ = mandolin_bank(rr);
}
for program in 56..=60 {
let _ = brass_bank(program, 1);
let _ = brass_bank(program, 127);
}
let _ = brass_section_bank();
for program in [68, 70, 71] {
let _ = reed_bank(program, 1);
let _ = reed_bank(program, 127);
}
for program in 64..=67 {
let _ = sax_bank(program, 1);
let _ = sax_bank(program, 127);
}
let _ = marimba_bank();
let _ = xylo_bank();
let _ = glock_bank();
let _ = vibraphone_bank();
let _ = tubular_bank();
let _ = celesta_bank();
let _ = musicbox_bank();
let _ = flute_bank();
let _ = guitar_bank();
let _ = steel_bank();
let _ = eastman_picked_bank();
let _ = eastman_plucked_bank();
let _ = harp_bank();
let _ = ocarina_bank();
let _ = recorder_bank();
let _ = timpani_bank();
let _ = sitar_bank();
let _ = banjo_bank();
let _ = panflute_bank();
let _ = bottle_bank();
let _ = shakuhachi_bank();
let _ = chanter_bank();
let _ = drone_g2_bank();
let _ = drone_g3_bank();
let _ = clavinet_bank();
let _ = harpsichord_bank();
let _ = rhodes_bank();
let _ = dulcimer_bank();
let _ = pizzbass_bank();
let _ = finger_bass_bank();
let _ = pick_bass_bank();
#[cfg(feature = "embedded-samples")]
for zone in clavinet_bank() {
let _ = zone.sustain_loop(find_clavinet_loop);
}
for program in 64..=67 {
for vel in [1u8, 127] {
for zone in sax_bank(program, vel) {
let _ = zone.sustain_loop(find_sax_loop);
}
}
}
for zone in bottle_loop_bank() {
let _ = zone.sustain_loop(find_bottle_loop);
}
#[cfg(feature = "embedded-samples")]
{
let _ = chanter_rr2();
let _ = rain_loop();
let _ = gong_layers();
let _ = fret_noise_takes();
}
}
fn nearest(zones: &'static [Zone], f: f32) -> &'static Zone {
&zones[nearest_idx(zones, f)]
}
fn nearest_idx(zones: &[Zone], f: f32) -> usize {
zones
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
let da = (a.root / f).ln().abs();
let db = (b.root / f).ln().abs();
da.partial_cmp(&db).unwrap()
})
.map(|(i, _)| i)
.unwrap()
}
#[inline]
fn smooth(x: f32) -> f32 {
let x = x.clamp(0.0, 1.0);
x * x * (3.0 - 2.0 * x)
}
#[derive(Clone, Copy, Default)]
pub struct LaFx {
pub vel_sense: Option<f32>,
pub shelf: Option<(f32, f32)>,
pub detune: Option<(f32, f32, f32)>,
pub vel_lp: Option<f32>,
}
fn vel_lp_alpha(base_hz: f32, vel: u8, sr: f32) -> f32 {
if vel >= 100 {
return 0.0;
}
let vn = vel as f32 / 127.0;
let corner = base_hz * (0.10 + 1.30 * vn);
1.0 - (-std::f32::consts::TAU * corner / sr).exp()
}
#[derive(Clone, Copy)]
pub struct OnsetVar {
pub detune_strata_c: f32,
pub detune_white_c: f32,
pub onset_max_s: f32,
pub gain_frac: f32,
}
pub const GUITAR_VAR: OnsetVar = OnsetVar {
detune_strata_c: 10.0,
detune_white_c: 1.0,
onset_max_s: 0.006,
gain_frac: 0.06,
};
pub struct LaVoice {
sustain: Box<dyn Voice>,
zone: &'static Zone,
pos: f32,
step: f32,
base_step: f32,
base_ratio: f32,
gain: f32,
rel_gain: f32,
rel_mul: f32,
rel_t60_mul: f32,
t: usize,
fade_start: usize,
fade_end: usize,
transient_handover: Option<(usize, usize)>,
transient_handover_len: usize,
blend: LaBlend,
buf: Vec<f32>,
fx: LaFx,
pos2: f32,
pos3: f32,
shelf_lp: f32,
shelf_a: f32,
shelf_g: f32,
var_mult: f32,
start: usize,
vel_lp: f32,
vel_lp_a: f32,
end_taper: f32,
rr: Option<RoundRobin>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LaBlend {
Replacement,
Additive,
}
pub(crate) struct LaVoiceBuild {
pub(crate) voice: Box<dyn Voice>,
pub(crate) used_sample: bool,
}
#[derive(Clone, Copy)]
struct RoundRobin {
bank: fn(usize) -> &'static [Zone],
takes: usize,
zone_idx: usize,
idx: usize,
}
pub(crate) const DEFAULT_LA_RELEASE_T60: f32 = 0.06;
const LA_SLUR_HANDOVER_S: f32 = 0.06;
const DEFAULT_MAX_UP_RATIO: f32 = 2.05;
impl LaVoice {
pub fn wrap(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
) -> Box<dyn Voice> {
Self::wrap_classified(sustain, zones, key, vel, sr, gain, fade).voice
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn wrap_classified(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
) -> LaVoiceBuild {
Self::wrap_fx_classified(sustain, zones, key, vel, sr, gain, fade, LaFx::default())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn wrap_additive_limited(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
max_up_ratio: f32,
) -> Box<dyn Voice> {
match Self::build(
sustain,
zones,
key,
vel,
sr,
gain,
fade,
LaFx::default(),
DEFAULT_LA_RELEASE_T60,
LaBlend::Additive,
max_up_ratio,
) {
Ok(la) => Box::new(la),
Err(model) => model,
}
}
#[allow(clippy::too_many_arguments)] pub fn wrap_rr(
sustain: Box<dyn Voice>,
bank: fn(usize) -> &'static [Zone],
takes: usize,
rr: u8,
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
) -> Box<dyn Voice> {
match Self::build_rr(sustain, bank, takes, rr, key, vel, sr, gain, fade) {
Ok(la) => Box::new(la),
Err(model) => model,
}
}
#[allow(clippy::too_many_arguments)]
fn build_rr(
sustain: Box<dyn Voice>,
bank: fn(usize) -> &'static [Zone],
takes: usize,
rr: u8,
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
) -> Result<LaVoice, Box<dyn Voice>> {
let idx = (rr as usize) % takes.max(1);
let zones = bank(idx);
let zone_idx = nearest_idx(zones, key_freq(key));
let mut la = Self::build(
sustain,
zones,
key,
vel,
sr,
gain,
fade,
LaFx::default(),
DEFAULT_LA_RELEASE_T60,
LaBlend::Replacement,
DEFAULT_MAX_UP_RATIO,
)?;
if takes >= 2 {
la.rr = Some(RoundRobin {
bank,
takes,
zone_idx,
idx,
});
}
Ok(la)
}
#[allow(clippy::too_many_arguments)] pub fn wrap_fx(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
fx: LaFx,
) -> Box<dyn Voice> {
Self::wrap_fx_classified(sustain, zones, key, vel, sr, gain, fade, fx).voice
}
#[allow(clippy::too_many_arguments)]
fn wrap_fx_classified(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
fx: LaFx,
) -> LaVoiceBuild {
match Self::build(
sustain,
zones,
key,
vel,
sr,
gain,
fade,
fx,
DEFAULT_LA_RELEASE_T60,
LaBlend::Replacement,
DEFAULT_MAX_UP_RATIO,
) {
Ok(la) => LaVoiceBuild {
voice: Box::new(la),
used_sample: true,
},
Err(model) => LaVoiceBuild {
voice: model,
used_sample: false,
},
}
}
#[allow(clippy::too_many_arguments)]
pub fn wrap_release(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
release_t60: f32,
) -> Box<dyn Voice> {
match Self::build(
sustain,
zones,
key,
vel,
sr,
gain,
fade,
LaFx::default(),
release_t60,
LaBlend::Replacement,
DEFAULT_MAX_UP_RATIO,
) {
Ok(la) => Box::new(la),
Err(model) => model,
}
}
#[allow(clippy::too_many_arguments)] pub fn wrap_var(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
fx: LaFx,
var: OnsetVar,
seed: u32,
) -> Box<dyn Voice> {
Self::wrap_var_classified(sustain, zones, key, vel, sr, gain, fade, fx, var, seed).voice
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn wrap_var_classified(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
fx: LaFx,
var: OnsetVar,
seed: u32,
) -> LaVoiceBuild {
let mut rng = crate::dsp::Rng::new(seed ^ 0x00A1_51C5);
let strata = [-0.5f32, -0.25, 0.0, 0.25, 0.5];
let cents = strata[(seed >> 3) as usize % strata.len()] * var.detune_strata_c
+ rng.white() * var.detune_white_c;
let var_mult = (cents / 1200.0).exp2();
let gain = gain * (1.0 + var.gain_frac * rng.white());
let start = (var.onset_max_s * sr * (0.5 + 0.5 * rng.white())) as usize;
match Self::build(
sustain,
zones,
key,
vel,
sr,
gain,
fade,
fx,
DEFAULT_LA_RELEASE_T60,
LaBlend::Replacement,
DEFAULT_MAX_UP_RATIO,
) {
Ok(mut la) => {
la.var_mult = var_mult;
la.base_step *= var_mult;
la.step = la.base_step;
la.start = start;
la.end_taper = 0.005 * sr;
la.sustain.set_pitch(var_mult);
LaVoiceBuild {
voice: Box::new(la),
used_sample: true,
}
}
Err(model) => LaVoiceBuild {
voice: model,
used_sample: false,
},
}
}
#[allow(clippy::too_many_arguments)]
fn build(
sustain: Box<dyn Voice>,
zones: &'static [Zone],
key: u8,
vel: u8,
sr: f32,
gain: f32,
fade: (f32, f32),
fx: LaFx,
release_t60: f32,
blend: LaBlend,
max_up_ratio: f32,
) -> Result<LaVoice, Box<dyn Voice>> {
let f = key_freq(key);
let zone = nearest(zones, f);
let ratio = f / zone.root;
if ratio < 0.5 || ratio > max_up_ratio {
return Err(sustain);
}
let step = ratio * 44100.0 / sr;
let (shelf_g, shelf_a) = match fx.shelf {
Some((db, hz)) => (
10f32.powf(db / 20.0) - 1.0,
1.0 - (-std::f32::consts::TAU * hz / sr).exp(),
),
None => (0.0, 0.0),
};
let vel_lp_a = match fx.vel_lp {
Some(base_hz) => vel_lp_alpha(base_hz, vel, sr),
None => 0.0,
};
let vel_gain = match fx.vel_sense {
Some(sense) => vel_amp_sensed(vel, sense),
None => vel_amp(vel),
};
let fade_start = (fade.0 * sr) as usize;
let fade_end = (fade.1 * sr) as usize;
Ok(LaVoice {
sustain,
zone,
pos: 0.0,
step,
base_step: step,
base_ratio: ratio,
gain: gain * vel_gain,
rel_gain: 1.0,
rel_mul: 1.0,
rel_t60_mul: 10f32.powf(-3.0 / (release_t60 * sr)),
t: 0,
fade_start,
fade_end,
transient_handover: None,
transient_handover_len: (LA_SLUR_HANDOVER_S * sr) as usize,
blend,
buf: Vec::new(),
fx,
pos2: 220.0,
pos3: 397.0,
shelf_lp: 0.0,
shelf_a,
shelf_g,
var_mult: 1.0,
start: 0,
vel_lp: 0.0,
vel_lp_a,
end_taper: 0.0,
rr: None,
})
}
}
impl Voice for LaVoice {
fn render(&mut self, out: &mut [f32]) -> bool {
self.buf.resize(out.len(), 0.0);
self.buf.fill(0.0);
let sustain_alive = self.sustain.render(&mut self.buf);
let n = self.zone.data.len();
let fade_len = (self.fade_end - self.fade_start).max(1) as f32;
let mut sample_live = false;
for (i, o) in out.iter_mut().enumerate() {
let t = self.t + i;
let base_u = if self.blend == LaBlend::Additive {
smooth(t as f32 / self.fade_end.max(1) as f32)
} else {
smooth((t as f32 - self.fade_start as f32) / fade_len)
};
let base_model_weight = if self.blend == LaBlend::Additive {
1.0
} else {
base_u
};
let (u, model_weight) = if let Some((start, len)) = self.transient_handover {
let handover = smooth((t as f32 - start as f32) / len.max(1) as f32);
(
1.0 - (1.0 - base_u) * (1.0 - handover),
1.0 - (1.0 - base_model_weight) * (1.0 - handover),
)
} else {
(base_u, base_model_weight)
};
let mut s = self.buf[i] * model_weight;
let j = self.pos as usize;
if t >= self.start && t < self.fade_end && j + 1 < n && self.rel_gain > 0.0005 {
sample_live = true;
let frac = self.pos - j as f32;
let mut v = crate::dsp::cubic4(
self.zone.data[j.saturating_sub(1)],
self.zone.data[j],
self.zone.data[j + 1],
self.zone.data[(j + 2).min(n - 1)],
frac,
);
if let Some((ra, rb, mix)) = self.fx.detune {
let d = &self.zone.data;
let mut side = 0.0;
let k = self.pos2 as usize;
if k + 1 < n {
let fr = self.pos2 - k as f32;
side += crate::dsp::cubic4(
d[k.saturating_sub(1)],
d[k],
d[k + 1],
d[(k + 2).min(n - 1)],
fr,
);
}
self.pos2 += self.step * ra;
let k = self.pos3 as usize;
if k + 1 < n {
let fr = self.pos3 - k as f32;
side += crate::dsp::cubic4(
d[k.saturating_sub(1)],
d[k],
d[k + 1],
d[(k + 2).min(n - 1)],
fr,
);
}
self.pos3 += self.step * rb;
v += mix * side;
}
if self.fx.shelf.is_some() {
self.shelf_lp += (v - self.shelf_lp) * self.shelf_a;
v += self.shelf_g * (v - self.shelf_lp);
}
if self.vel_lp_a > 0.0 {
self.vel_lp += (v - self.vel_lp) * self.vel_lp_a;
v = self.vel_lp;
}
if self.end_taper > 0.0 {
let remaining_src = n as f32 - 2.0 - self.pos;
v *= (remaining_src / (self.end_taper * self.step.max(1e-6))).clamp(0.0, 1.0);
}
s += v * (1.0 - u) * self.gain * self.rel_gain;
self.rel_gain *= self.rel_mul;
self.pos += self.step;
}
*o += s;
}
self.t += out.len();
if self.t >= self.fade_end {
self.transient_handover = None;
}
sustain_alive || sample_live
}
fn note_off(&mut self) {
self.sustain.note_off();
self.rel_mul = self.rel_t60_mul;
}
fn released(&self) -> bool {
self.sustain.released()
}
fn set_pitch(&mut self, mult: f32) {
self.step = self.base_step * mult;
self.sustain.set_pitch(self.var_mult * mult);
}
fn legato_to(&mut self, key: u8, vel: u8) -> bool {
if self.sustain.legato_to(key, vel) {
if self.t < self.fade_end && self.transient_handover.is_none() {
self.transient_handover = Some((self.t, self.transient_handover_len));
}
true
} else {
false
}
}
fn retrigger(&mut self, key: u8, vel: u8) -> bool {
if let Some(rr) = self.rr {
if !self.sustain.retrigger(key, vel) {
return false;
}
let idx = rr.idx.wrapping_add(1);
let zones = (rr.bank)(idx % rr.takes);
let zone = &zones[rr.zone_idx];
let ratio = self.zone.root / zone.root;
let next_ratio = self.base_ratio * ratio;
if !(0.5..=2.05).contains(&next_ratio) {
self.rel_mul = self.rel_t60_mul;
return true;
}
self.rr = Some(RoundRobin { idx, ..rr });
self.zone = zone;
self.step *= ratio;
self.base_step *= ratio;
self.base_ratio = next_ratio;
self.t = 0;
self.transient_handover = None;
self.pos = 0.0;
self.pos2 = 220.0;
self.pos3 = 397.0;
self.rel_gain = 1.0;
self.rel_mul = 1.0;
self.shelf_lp = 0.0;
self.vel_lp = 0.0;
return true;
}
if self.sustain.retrigger(key, vel) {
self.rel_mul = self.rel_t60_mul;
true
} else {
false
}
}
fn rr_phase(&self) -> Option<usize> {
self.rr.map(|rr| rr.idx % rr.takes.max(1))
}
#[cfg(test)]
fn kind(&self) -> &'static str {
self.sustain.kind()
}
fn set_trem(&mut self, rate_hz: f32, depth: f32) {
self.sustain.set_trem(rate_hz, depth);
}
fn set_breath(&mut self, pressure: f32, growl: f32) {
self.sustain.set_breath(pressure, growl);
}
}
const SAX_LOOP_GAIN: [f32; 4] = [0.508, 0.377, 0.666, 0.812];
fn sax_program_gain(program: u8) -> f32 {
SAX_LOOP_GAIN[(program.saturating_sub(64)).min(3) as usize]
}
fn find_sax_loop(data: &[f32], root: f32) -> Option<(usize, usize)> {
#[cfg(test)]
LOOP_SEARCHES.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let sr = 44100.0f32;
if root <= 0.0 {
return None;
}
let period = sr / root;
if period < 4.0 {
return None;
}
let n = data.len();
const START_LO_S: f32 = 0.30; const START_HI_S: f32 = 0.40;
const MIN_LEN_S: f32 = 0.05; const MAX_LEN_S: f32 = 0.13;
const BODY_END_S: f32 = 0.50; let start_lo = (START_LO_S * sr) as usize;
let min_len = (MIN_LEN_S * sr) as usize;
let body_end = ((BODY_END_S * sr) as usize).min(n.saturating_sub(2));
if start_lo == 0 || body_end <= start_lo + min_len {
return None;
}
let start_hi = ((START_HI_S * sr) as usize).min(body_end - min_len);
let max_len = (MAX_LEN_S * sr) as usize;
let stride = (period / 8.0).max(1.0) as usize;
let imbalance = |ls: usize, le: usize| -> f32 {
let mid = (ls + le) / 2;
let rms = |a: usize, b: usize| {
(data[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a).max(1) as f32).sqrt()
};
let (r1, r2) = (rms(ls, mid), rms(mid, le));
(r1 - r2).abs() / (0.5 * (r1 + r2) + 1e-6)
};
let mut best: Option<(f32, usize, usize)> = None; let mut ls = start_lo;
while ls <= start_hi {
let mut k = ((min_len as f32 / period).ceil() as usize).max(1);
loop {
let l = (k as f32 * period).round() as usize;
if l > max_len {
break;
}
let le = ls + l;
if le + 1 >= body_end {
break;
}
let vseam = (data[le] - data[ls]).abs();
let sseam = ((data[le] - data[le - 1]) - (data[ls] - data[ls - 1])).abs();
let cost = vseam + 0.5 * sseam + 4.0 * imbalance(ls, le);
if best.is_none_or(|(bc, _, _)| cost < bc) {
best = Some((cost, ls, le));
}
k += 1;
}
ls += stride;
}
best.map(|(_, ls, le)| (ls, le))
}
const SAX_VIB_RATE_HZ: f32 = 5.4; const SAX_VIB_PITCH: f32 = 0.006; const SAX_VIB_AMP: f32 = 0.05; const SAX_VIB_DELAY_S: f32 = 0.20; const SAX_VIB_BLOOM_S: f32 = 0.35; const SAX_DRIFT_MAX: f32 = 0.0022; const SAX_DRIFT_SAMP: u32 = 1024; const SAX_GRAIN_BODY_START_S: f32 = 0.30;
const SAX_GRAIN_BODY_END_S: f32 = 0.50;
const SAX_GRAIN_MIN_S: f32 = 0.05;
const SAX_GRAIN_MAX_S: f32 = 0.09;
const SAX_GRAIN_CROSSFADE_S: f32 = 0.025;
const SAX_BREATH_FLOOR: f32 = 0.0025; const SAX_BREATH_CC: f32 = 0.012; const SAX_BREATH_HP_HZ: f32 = 1800.0; const SAX_BRIGHT_LO_HZ: f32 = 1400.0; const SAX_BRIGHT_HI_HZ: f32 = 7000.0;
pub struct SaxLoopVoice {
data: &'static [f32],
loop_start: usize,
loop_end: usize,
pos: f32,
next_pos: Option<f32>,
next_loop_start: usize,
next_grain_len: usize,
next_grain_gain: f32,
grain_len: usize,
grain_gain: f32,
grain_reference_rms: f32,
grain_body_start: usize,
grain_body_end: usize,
source_period: f32,
grain_crossfade: f32,
grain_motion: bool,
base_step: f32,
step: f32,
gain: f32,
env: crate::dsp::Adsr,
sr: f32,
t: u64,
vib_phase: f32,
vib_inc: f32,
drift: f32,
drift_target: f32,
rng: Rng,
breath_hp: crate::dsp::OnePole,
breath_amt: f32, growl: f32, bright: crate::dsp::OnePole,
bright_active: bool, }
impl SaxLoopVoice {
fn new(
zone: &'static Zone,
target_hz: f32,
vel: u8,
sr: f32,
base_gain: f32,
seed: u32,
grain_motion: bool,
) -> Option<Self> {
let ratio = target_hz / zone.root;
if !(0.5..=2.05).contains(&ratio) {
return None;
}
let (loop_start, loop_end) = zone.sustain_loop(find_sax_loop)?;
let base_step = ratio * 44100.0 / sr;
let grain_len = loop_end - loop_start;
let grain_body_start = (SAX_GRAIN_BODY_START_S * 44_100.0) as usize;
let grain_body_end =
((SAX_GRAIN_BODY_END_S * 44_100.0) as usize).min(zone.data.len().saturating_sub(2));
let grain_reference_rms = Self::grain_rms(zone.data.as_slice(), loop_start, grain_len);
Some(SaxLoopVoice {
data: zone.data.as_slice(),
loop_start,
loop_end,
pos: 0.0,
next_pos: None,
next_loop_start: loop_start,
next_grain_len: grain_len,
next_grain_gain: 1.0,
grain_len,
grain_gain: 1.0,
grain_reference_rms,
grain_body_start,
grain_body_end,
source_period: 44_100.0 / zone.root,
grain_crossfade: (SAX_GRAIN_CROSSFADE_S * 44_100.0).min(0.45 * grain_len as f32),
grain_motion,
base_step,
step: base_step,
gain: base_gain * vel_amp(vel),
env: crate::dsp::Adsr::new(0.004, 0.0, 1.0, 0.07, sr),
sr,
t: 0,
vib_phase: 0.0,
vib_inc: std::f32::consts::TAU * SAX_VIB_RATE_HZ / sr,
drift: 0.0,
drift_target: 0.0,
rng: Rng::new(seed ^ 0x5AC0_FFEE ^ ((zone.root as u32) << 3) ^ vel as u32),
breath_hp: crate::dsp::OnePole::lowpass(SAX_BREATH_HP_HZ, sr),
breath_amt: SAX_BREATH_FLOOR,
growl: 0.0,
bright: crate::dsp::OnePole::lowpass(SAX_BRIGHT_HI_HZ, sr),
bright_active: false,
})
}
fn grain_rms(data: &[f32], start: usize, len: usize) -> f32 {
let energy = data[start..start + len].iter().map(|&x| x * x).sum::<f32>();
(energy / len.max(1) as f32).sqrt()
}
fn phase_match_grain(&self, nominal: usize, grain_len: usize) -> usize {
let radius = (0.5 * self.source_period).round() as isize;
let fade_start = self.loop_end.saturating_sub(self.grain_crossfade as usize);
let compare_len = ((0.006 * 44_100.0) as usize)
.min(self.grain_crossfade as usize)
.min(self.loop_end.saturating_sub(fade_start));
let candidate_hi = self.grain_body_end.saturating_sub(grain_len);
let mut best = nominal;
let mut best_corr = f64::NEG_INFINITY;
for offset in (-radius..=radius).step_by(2) {
let candidate = (nominal as isize + offset)
.clamp(self.grain_body_start as isize, candidate_hi as isize)
as usize;
let mut dot = 0.0f64;
let mut a2 = 0.0f64;
let mut b2 = 0.0f64;
for i in 0..compare_len {
let a = self.data[fade_start + i] as f64;
let b = self.data[candidate + i] as f64;
dot += a * b;
a2 += a * a;
b2 += b * b;
}
let corr = dot / (a2 * b2).sqrt().max(1e-20);
if corr > best_corr {
best_corr = corr;
best = candidate;
}
}
best
}
fn choose_next_grain(&mut self) -> (usize, usize) {
let min_periods = ((SAX_GRAIN_MIN_S * 44_100.0) / self.source_period)
.ceil()
.max(1.0) as usize;
let max_periods = ((SAX_GRAIN_MAX_S * 44_100.0) / self.source_period)
.floor()
.max(min_periods as f32) as usize;
let length_unit = (0.5 * (self.rng.white() + 1.0)).clamp(0.0, 0.999_999);
let periods = min_periods + (length_unit * (max_periods - min_periods + 1) as f32) as usize;
let grain_len = (periods as f32 * self.source_period).round() as usize;
let grain_start_hi = self
.grain_body_end
.saturating_sub(grain_len)
.max(self.grain_body_start);
let origin = self.loop_start as f32;
let first = ((self.grain_body_start as f32 - origin) / self.source_period).ceil() as i32;
let last = ((grain_start_hi as f32 - origin) / self.source_period).floor() as i32;
if last < first {
return (self.grain_body_start, grain_len);
}
let count = (last - first + 1) as usize;
let unit = (0.5 * (self.rng.white() + 1.0)).clamp(0.0, 0.999_999);
let mut step = first + (unit * count as f32) as i32;
let mut start = (origin + step as f32 * self.source_period).round() as usize;
if start.abs_diff(self.loop_start) < (0.5 * self.source_period) as usize {
step = if step < last { step + 1 } else { first };
start = (origin + step as f32 * self.source_period).round() as usize;
}
let nominal = start.clamp(self.grain_body_start, grain_start_hi);
(self.phase_match_grain(nominal, grain_len), grain_len)
}
}
impl Voice for SaxLoopVoice {
fn render(&mut self, out: &mut [f32]) -> bool {
for o in out.iter_mut() {
let e = self.env.next();
let time = self.t as f32 / self.sr;
let bloom = ((time - SAX_VIB_DELAY_S) / SAX_VIB_BLOOM_S).clamp(0.0, 1.0);
let s = self.vib_phase.sin();
self.vib_phase += self.vib_inc;
if self.vib_phase >= std::f32::consts::TAU {
self.vib_phase -= std::f32::consts::TAU;
}
let depth = bloom * (1.0 + 2.0 * self.growl);
if self.t.is_multiple_of(SAX_DRIFT_SAMP as u64) {
self.drift_target = SAX_DRIFT_MAX * self.rng.white();
}
self.drift += 0.002 * (self.drift_target - self.drift);
let eff_step = self.step * (1.0 + SAX_VIB_PITCH * depth * s + self.drift);
let amp = 1.0 + SAX_VIB_AMP * depth * s;
let fade_start = self.loop_end as f32 - self.grain_crossfade;
if self.grain_motion && self.next_pos.is_none() && self.pos >= fade_start {
(self.next_loop_start, self.next_grain_len) = self.choose_next_grain();
let next_rms =
Self::grain_rms(self.data, self.next_loop_start, self.next_grain_len);
self.next_grain_gain =
(self.grain_reference_rms / next_rms.max(1e-6)).clamp(0.5, 2.0);
self.next_pos = Some(self.next_loop_start as f32);
}
let j = self.pos as usize;
let frac = self.pos - j as f32;
let a = self.data[j];
let b = if !self.grain_motion && j + 1 >= self.loop_end {
self.data[self.loop_start]
} else {
self.data[j + 1]
};
let mut tone = (a + (b - a) * frac) * self.grain_gain;
if let Some(next_pos) = self.next_pos {
let nj = next_pos as usize;
let nf = next_pos - nj as f32;
let next_tone = (self.data[nj] + (self.data[nj + 1] - self.data[nj]) * nf)
* self.next_grain_gain;
let x = ((self.pos - fade_start) / self.grain_crossfade).clamp(0.0, 1.0);
let smooth = x * x * (3.0 - 2.0 * x);
tone += (next_tone - tone) * smooth;
}
if self.bright_active {
tone = self.bright.process(tone);
}
let w = self.rng.white();
let breath = (w - self.breath_hp.process(w)) * self.breath_amt;
*o += (tone * amp + breath) * self.gain * e;
self.pos += eff_step;
if let Some(next_pos) = &mut self.next_pos {
*next_pos += eff_step;
}
if self.pos >= self.loop_end as f32 {
if self.grain_motion {
self.pos = self
.next_pos
.take()
.expect("sax grain crossfade must start before the current slice ends");
self.loop_start = self.next_loop_start;
self.grain_len = self.next_grain_len;
self.grain_gain = self.next_grain_gain;
self.loop_end = self.loop_start + self.grain_len;
} else {
self.pos -= self.grain_len as f32;
}
}
self.t += 1;
}
self.env.alive()
}
fn note_off(&mut self) {
self.env.release();
}
fn released(&self) -> bool {
self.env.released()
}
fn set_pitch(&mut self, mult: f32) {
self.step = self.base_step * mult;
}
fn set_breath(&mut self, pressure: f32, growl: f32) {
let p = pressure.clamp(0.0, 1.0);
self.bright_active = true;
self.bright.set_cutoff(
SAX_BRIGHT_LO_HZ + (SAX_BRIGHT_HI_HZ - SAX_BRIGHT_LO_HZ) * p,
self.sr,
);
self.breath_amt = SAX_BREATH_FLOOR + SAX_BREATH_CC * p;
self.growl = growl.clamp(0.0, 1.0);
}
#[cfg(test)]
fn kind(&self) -> &'static str {
"saxloop"
}
}
pub fn sax_loop_voice(program: u8, key: u8, vel: u8, sr: f32, seed: u32) -> Option<Box<dyn Voice>> {
let zones = sax_bank(program, vel);
let f = key_freq(key);
let zone = nearest(zones, f);
SaxLoopVoice::new(
zone,
f,
vel,
sr,
sax_program_gain(program),
seed,
program == 67,
)
.map(|v| Box::new(v) as Box<dyn Voice>)
}
const BOTTLE_LOOP_GAIN: f32 = 0.65;
fn bottle_loop_bank() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"bottleloop_G3.flac" => 205.0,
)
})
}
fn find_bottle_loop(data: &[f32], root: f32) -> Option<(usize, usize)> {
#[cfg(test)]
LOOP_SEARCHES.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let sr = 44100.0f32;
if root <= 0.0 {
return None;
}
let period = sr / root;
if period < 4.0 {
return None;
}
let n = data.len();
const START_LO_S: f32 = 0.45; const START_HI_S: f32 = 0.90;
const MIN_LEN_S: f32 = 0.05; const MAX_LEN_S: f32 = 0.15;
const BODY_END_S: f32 = 1.45; let start_lo = (START_LO_S * sr) as usize;
let min_len = (MIN_LEN_S * sr) as usize;
let body_end = ((BODY_END_S * sr) as usize).min(n.saturating_sub(2));
if start_lo == 0 || body_end <= start_lo + min_len {
return None;
}
let start_hi = ((START_HI_S * sr) as usize).min(body_end - min_len);
let max_len = (MAX_LEN_S * sr) as usize;
let stride = (period / 8.0).max(1.0) as usize;
let imbalance = |ls: usize, le: usize| -> f32 {
let mid = (ls + le) / 2;
let rms = |a: usize, b: usize| {
(data[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a).max(1) as f32).sqrt()
};
let (r1, r2) = (rms(ls, mid), rms(mid, le));
(r1 - r2).abs() / (0.5 * (r1 + r2) + 1e-6)
};
let mut best: Option<(f32, usize, usize)> = None; let mut ls = start_lo;
while ls <= start_hi {
let mut k = ((min_len as f32 / period).ceil() as usize).max(1);
loop {
let l = (k as f32 * period).round() as usize;
if l > max_len {
break;
}
let le = ls + l;
if le + 1 >= body_end {
break;
}
let vseam = (data[le] - data[ls]).abs();
let sseam = ((data[le] - data[le - 1]) - (data[ls] - data[ls - 1])).abs();
let cost = vseam + 0.5 * sseam + 4.0 * imbalance(ls, le);
if best.is_none_or(|(bc, _, _)| cost < bc) {
best = Some((cost, ls, le));
}
k += 1;
}
ls += stride;
}
best.map(|(_, ls, le)| (ls, le))
}
pub struct BottleLoopVoice {
data: &'static [f32],
loop_start: usize,
loop_end: usize,
pos: f32,
base_step: f32,
step: f32,
gain: f32,
env: crate::dsp::Adsr,
sr: f32,
t: u64,
vib_phase: f32,
vib_inc: f32,
drift: f32,
drift_target: f32,
rng: Rng,
breath_hp: crate::dsp::OnePole,
breath_amt: f32, growl: f32, bright: crate::dsp::OnePole,
bright_active: bool, }
impl BottleLoopVoice {
fn new(
zone: &'static Zone,
target_hz: f32,
vel: u8,
sr: f32,
base_gain: f32,
seed: u32,
) -> Option<Self> {
let ratio = target_hz / zone.root;
if !(0.5..=2.05).contains(&ratio) {
return None;
}
let (loop_start, loop_end) = zone.sustain_loop(find_bottle_loop)?;
let base_step = ratio * 44100.0 / sr;
Some(BottleLoopVoice {
data: zone.data.as_slice(),
loop_start,
loop_end,
pos: 0.0,
base_step,
step: base_step,
gain: base_gain * (0.55 + 0.45 * vel_amp(vel)),
env: crate::dsp::Adsr::new(0.004, 0.0, 1.0, 0.07, sr),
sr,
t: 0,
vib_phase: 0.0,
vib_inc: std::f32::consts::TAU * SAX_VIB_RATE_HZ / sr,
drift: 0.0,
drift_target: 0.0,
rng: Rng::new(seed ^ 0x5AC0_FFEE ^ ((zone.root as u32) << 3) ^ vel as u32),
breath_hp: crate::dsp::OnePole::lowpass(SAX_BREATH_HP_HZ, sr),
breath_amt: SAX_BREATH_FLOOR,
growl: 0.0,
bright: crate::dsp::OnePole::lowpass(SAX_BRIGHT_HI_HZ, sr),
bright_active: false,
})
}
}
impl Voice for BottleLoopVoice {
fn render(&mut self, out: &mut [f32]) -> bool {
let loop_len = (self.loop_end - self.loop_start) as f32;
for o in out.iter_mut() {
let e = self.env.next();
let time = self.t as f32 / self.sr;
let bloom = ((time - SAX_VIB_DELAY_S) / SAX_VIB_BLOOM_S).clamp(0.0, 1.0);
let s = self.vib_phase.sin();
self.vib_phase += self.vib_inc;
if self.vib_phase >= std::f32::consts::TAU {
self.vib_phase -= std::f32::consts::TAU;
}
let depth = bloom * (1.0 + 2.0 * self.growl);
if self.t.is_multiple_of(SAX_DRIFT_SAMP as u64) {
self.drift_target = SAX_DRIFT_MAX * self.rng.white();
}
self.drift += 0.002 * (self.drift_target - self.drift);
let eff_step = self.step * (1.0 + SAX_VIB_PITCH * depth * s + self.drift);
let amp = 1.0 + SAX_VIB_AMP * depth * s;
let j = self.pos as usize;
let frac = self.pos - j as f32;
let a = self.data[j];
let jn = j + 1;
let b = if jn >= self.loop_end {
self.data[self.loop_start]
} else {
self.data[jn]
};
let mut tone = a + (b - a) * frac;
if self.bright_active {
tone = self.bright.process(tone);
}
let w = self.rng.white();
let breath = (w - self.breath_hp.process(w)) * self.breath_amt;
*o += (tone * amp + breath) * self.gain * e;
self.pos += eff_step;
if self.pos >= self.loop_end as f32 {
self.pos -= loop_len;
}
self.t += 1;
}
self.env.alive()
}
fn note_off(&mut self) {
self.env.release();
}
fn released(&self) -> bool {
self.env.released()
}
fn set_pitch(&mut self, mult: f32) {
self.step = self.base_step * mult;
}
fn set_breath(&mut self, pressure: f32, growl: f32) {
let p = pressure.clamp(0.0, 1.0);
self.bright_active = true;
self.bright.set_cutoff(
SAX_BRIGHT_LO_HZ + (SAX_BRIGHT_HI_HZ - SAX_BRIGHT_LO_HZ) * p,
self.sr,
);
self.breath_amt = SAX_BREATH_FLOOR + SAX_BREATH_CC * p;
self.growl = growl.clamp(0.0, 1.0);
}
#[cfg(test)]
fn kind(&self) -> &'static str {
"bottleloop"
}
}
pub fn bottle_loop_voice(key: u8, vel: u8, sr: f32, seed: u32) -> Option<Box<dyn Voice>> {
let zones = bottle_loop_bank();
let f = key_freq(key);
let zone = nearest(zones, f);
BottleLoopVoice::new(zone, f, vel, sr, BOTTLE_LOOP_GAIN, seed)
.map(|v| Box::new(v) as Box<dyn Voice>)
}
#[cfg(feature = "embedded-samples")]
const DRUM_LEVEL: [(u8, f32); 21] = [
(35, 1.54), (36, 1.54), (37, 0.28), (38, 0.98), (40, 0.98), (41, 0.95), (42, 0.14), (43, 1.04), (44, 0.13), (45, 1.22), (46, 0.17), (47, 1.19), (48, 1.21), (49, 0.65), (50, 1.22), (51, 0.45), (52, 0.68), (53, 0.45), (55, 0.34), (57, 0.68), (59, 0.45), ];
#[cfg(feature = "embedded-samples")]
const TOM_LO_ROOT_HZ: f32 = 113.5;
#[cfg(feature = "embedded-samples")]
const TOM_HI_ROOT_HZ: f32 = 181.0;
#[cfg(feature = "embedded-samples")]
const ELECTRIC_SNARE_REPITCH: f32 = 1.15;
#[cfg(feature = "embedded-samples")]
const SECOND_CYMBAL_REPITCH: f32 = 1.059_463_1;
#[cfg(feature = "embedded-samples")]
struct DrumJitter {
rate: f32,
strata: &'static [f32],
gain: f32,
onset_s: f32,
}
#[cfg(feature = "embedded-samples")]
const DRUM_JITTER: DrumJitter = DrumJitter {
rate: 0.025,
strata: &[],
gain: 0.06,
onset_s: 0.0,
};
#[cfg(feature = "embedded-samples")]
const HAT_JITTER: DrumJitter = DrumJitter {
rate: 0.008,
strata: &[-0.08, -0.04, 0.0, 0.04, 0.08],
gain: 0.10,
onset_s: 0.001,
};
#[cfg(feature = "embedded-samples")]
pub struct SampledDrum {
data: &'static [i16],
pos: f32,
step: f32,
gain: f32,
env: f32,
fade_mul: f32,
choke_mul: f32,
delay: u32,
sub_phase: f32,
sub_incr: f32,
sub_incr_end: f32,
sub_pitch_mul: f32,
sub_amp: f32,
sub_decay: f32,
#[cfg(test)]
layer: usize,
#[cfg(test)]
rr: usize,
}
#[cfg(feature = "embedded-samples")]
#[derive(Clone, Copy)]
struct DrumHit {
vel: u8,
seed: u32,
hit_index: u8,
}
#[cfg(feature = "embedded-samples")]
impl SampledDrum {
fn new(
bank: &'static ferrosintesis_samples_drumkit::Bank,
level: f32,
hit: DrumHit,
sr: f32,
repitch: f32,
jit: &DrumJitter,
) -> Self {
let layer = bank.layer_for_velocity(hit.vel);
let rr = hit.hit_index as usize % bank.round_robins;
let mut rng = Rng::new(hit.seed ^ 0x00C1_4BA1);
let stratum = if jit.strata.is_empty() {
0.0
} else {
jit.strata[hit.hit_index as usize % jit.strata.len()]
};
let rate = 1.0 + stratum + jit.rate * rng.white();
let gjit = 1.0 + jit.gain * rng.white();
let delay = (jit.onset_s * sr * (0.5 + 0.5 * rng.white())) as u32;
SampledDrum {
data: bank.pcm(layer, rr),
pos: 0.0,
step: 44_100.0 / sr * rate * repitch,
gain: level * vel_amp(hit.vel) * gjit,
env: 1.0,
fade_mul: 1.0,
choke_mul: 10f32.powf(-3.0 / (0.015 * sr)),
delay,
sub_phase: 0.0,
sub_incr: 0.0,
sub_incr_end: 0.0,
sub_pitch_mul: 0.0,
sub_amp: 0.0,
sub_decay: 0.0,
#[cfg(test)]
layer,
#[cfg(test)]
rr,
}
}
fn with_kick_sub(mut self, vel: u8, sr: f32) -> Self {
let tau = std::f32::consts::TAU;
self.sub_incr = tau * KICK_SUB_START_HZ / sr;
self.sub_incr_end = tau * KICK_SUB_END_HZ / sr;
self.sub_pitch_mul = (-1.0 / (KICK_SUB_PITCH_S * sr)).exp();
self.sub_amp = KICK_SUB_LEVEL * vel_amp(vel);
self.sub_decay = 10f32.powf(-3.0 / (KICK_SUB_T60_S * sr));
self
}
}
#[cfg(feature = "embedded-samples")]
const KICK_SUB_START_HZ: f32 = 90.0;
#[cfg(feature = "embedded-samples")]
const KICK_SUB_END_HZ: f32 = 48.0;
#[cfg(feature = "embedded-samples")]
const KICK_SUB_PITCH_S: f32 = 0.020;
#[cfg(feature = "embedded-samples")]
const KICK_SUB_T60_S: f32 = 0.16;
#[cfg(feature = "embedded-samples")]
const KICK_SUB_LEVEL: f32 = 0.55;
#[cfg(feature = "embedded-samples")]
pub fn sampled_drum(key: u8, vel: u8, seed: u32, hit_index: u8, sr: f32) -> Option<Box<dyn Voice>> {
use ferrosintesis_samples_drumkit as kit;
use ferrosintesis_samples_drumkit2 as kit2;
let (bank, repitch): (&'static kit::Bank, f32) = match key {
35 | 36 => (&kit::KICK, 1.0),
37 => (&kit::SIDESTICK, 1.0),
38 => (&kit::SNARE, 1.0),
40 => (&kit::SNARE, ELECTRIC_SNARE_REPITCH), 41 => (&kit::TOM_LO, 100.0 / TOM_LO_ROOT_HZ),
43 => (&kit::TOM_LO, 140.0 / TOM_LO_ROOT_HZ),
45 => (&kit::TOM_HI, 190.0 / TOM_HI_ROOT_HZ),
47 => (&kit::TOM_HI, 240.0 / TOM_HI_ROOT_HZ),
48 => (&kit::TOM_HI, 293.0 / TOM_HI_ROOT_HZ),
50 => (&kit::TOM_HI, 352.0 / TOM_HI_ROOT_HZ),
42 => (&kit::HH_CLOSED, 1.0),
44 => (&kit::HH_PEDAL, 1.0),
46 => (&kit::HH_OPEN, 1.0),
49 => (&kit2::CRASH, 1.0),
57 => (&kit2::CRASH, SECOND_CYMBAL_REPITCH), 51 => (&kit::RIDE, 1.0),
59 => (&kit::RIDE, SECOND_CYMBAL_REPITCH), 53 => (&kit::RIDE_BELL, 1.0),
52 => (&kit2::CHINA, 1.0),
55 => (&kit2::SPLASH, 1.0),
_ => return None,
};
let jit = match key {
42 | 44 | 46 => &HAT_JITTER,
_ => &DRUM_JITTER,
};
let level = DRUM_LEVEL.iter().find(|(k, _)| *k == key).unwrap().1;
let hit = DrumHit {
vel,
seed,
hit_index,
};
let voice = SampledDrum::new(bank, level, hit, sr, repitch, jit);
let voice = if matches!(key, 35 | 36) {
voice.with_kick_sub(vel, sr)
} else {
voice
};
Some(Box::new(voice))
}
#[cfg(not(feature = "embedded-samples"))]
pub fn sampled_drum(
_key: u8,
_vel: u8,
_seed: u32,
_hit_index: u8,
_sr: f32,
) -> Option<Box<dyn Voice>> {
None
}
#[cfg(feature = "embedded-samples")]
impl Voice for SampledDrum {
fn render(&mut self, out: &mut [f32]) -> bool {
let n = self.data.len();
for o in out.iter_mut() {
if self.delay > 0 {
self.delay -= 1;
continue;
}
let j = self.pos as usize;
if j + 1 >= n || self.env < 1e-4 {
return false;
}
let frac = self.pos - j as f32;
let v = crate::dsp::cubic4(
self.data[j.saturating_sub(1)] as f32,
self.data[j] as f32,
self.data[j + 1] as f32,
self.data[(j + 2).min(n - 1)] as f32,
frac,
);
*o += v * (1.0 / 32768.0) * self.gain * self.env;
if self.sub_amp > 1e-6 {
*o += self.sub_amp * self.env * self.sub_phase.sin();
self.sub_phase += self.sub_incr;
self.sub_incr =
self.sub_incr_end + (self.sub_incr - self.sub_incr_end) * self.sub_pitch_mul;
self.sub_amp *= self.sub_decay;
}
self.env *= self.fade_mul;
self.pos += self.step;
}
true
}
fn note_off(&mut self) {}
fn released(&self) -> bool {
true
}
fn choke(&mut self) {
self.fade_mul = self.choke_mul;
}
#[cfg(test)]
fn kind(&self) -> &'static str {
"sampleddrum"
}
}
#[cfg(feature = "embedded-samples")]
const GONG_ROOT_HZ: f32 = 99.4;
#[cfg(feature = "embedded-samples")]
const GONG_LEVEL: f32 = 0.80;
#[cfg(feature = "embedded-samples")]
const GONG_LOUD_VEL: u8 = 84;
#[cfg(feature = "embedded-samples")]
static GONG_LAYERS: OnceLock<(Vec<f32>, Vec<f32>)> = OnceLock::new();
#[cfg(feature = "embedded-samples")]
fn gong_layers() -> &'static (Vec<f32>, Vec<f32>) {
init_once!(GONG_LAYERS, {
(
parse_wav(embedded_wav("gong_ageng_soft.flac")),
parse_wav(embedded_wav("gong_ageng_loud.flac")),
)
})
}
#[cfg(feature = "embedded-samples")]
fn gong_layer(vel: u8, layers: &'static (Vec<f32>, Vec<f32>)) -> &'static [f32] {
if vel >= GONG_LOUD_VEL {
layers.1.as_slice()
} else {
layers.0.as_slice()
}
}
#[cfg(feature = "embedded-samples")]
pub struct GongOneShot {
data: &'static [f32],
pos: f32,
step: f32,
gain: f32,
}
#[cfg(feature = "embedded-samples")]
pub fn gong_one_shot(key: u8, vel: u8, sr: f32, _seed: u32) -> Box<dyn Voice> {
let data = gong_layer(vel, gong_layers());
let repitch = key_freq(crate::voices::fold_key(key, 36, 47)) / GONG_ROOT_HZ;
Box::new(GongOneShot {
data,
pos: 0.0,
step: 44_100.0 / sr * repitch,
gain: GONG_LEVEL * vel_amp(vel),
})
}
#[cfg(not(feature = "embedded-samples"))]
pub fn gong_one_shot(_key: u8, _vel: u8, _sr: f32, _seed: u32) -> Box<dyn Voice> {
panic!("gong one-shot requested from a modeled-only ferrosintesis build")
}
#[cfg(feature = "embedded-samples")]
impl Voice for GongOneShot {
fn render(&mut self, out: &mut [f32]) -> bool {
let n = self.data.len();
for o in out.iter_mut() {
let j = self.pos as usize;
if j + 1 >= n {
return false;
}
let frac = self.pos - j as f32;
let v = crate::dsp::cubic4(
self.data[j.saturating_sub(1)],
self.data[j],
self.data[j + 1],
self.data[(j + 2).min(n - 1)],
frac,
);
*o += v * self.gain;
self.pos += self.step;
}
true
}
fn note_off(&mut self) {}
fn released(&self) -> bool {
true
}
#[cfg(test)]
fn kind(&self) -> &'static str {
"gongoneshot"
}
}
#[cfg(feature = "embedded-samples")]
const FRETNOISE_LEVEL: f32 = 0.23;
#[cfg(feature = "embedded-samples")]
static FRETNOISE_TAKES: OnceLock<Vec<Vec<f32>>> = OnceLock::new();
#[cfg(feature = "embedded-samples")]
pub(crate) fn fret_noise_round_robins() -> usize {
ferrosintesis_samples_fretnoise::ROUND_ROBINS
}
#[cfg(not(feature = "embedded-samples"))]
pub(crate) fn fret_noise_round_robins() -> usize {
0
}
#[cfg(feature = "embedded-samples")]
fn fret_noise_takes() -> &'static [Vec<f32>] {
init_once!(FRETNOISE_TAKES, {
(0..ferrosintesis_samples_fretnoise::ROUND_ROBINS)
.map(|rr| parse_wav(embedded_wav(ferrosintesis_samples_fretnoise::take_name(rr))))
.collect::<Vec<Vec<f32>>>()
})
.as_slice()
}
#[cfg(feature = "embedded-samples")]
pub struct FretNoiseOneShot {
data: &'static [f32],
rr: usize,
pos: f32,
step: f32,
gain: f32,
}
#[cfg(feature = "embedded-samples")]
pub fn sampled_fret_noise(vel: u8, sr: f32, rr: usize) -> Option<Box<dyn Voice>> {
let takes = fret_noise_takes();
if takes.is_empty() {
return None;
}
let rr = rr % takes.len();
Some(Box::new(FretNoiseOneShot {
data: takes[rr].as_slice(),
rr,
pos: 0.0,
step: 44_100.0 / sr,
gain: FRETNOISE_LEVEL * vel_amp(vel),
}))
}
#[cfg(not(feature = "embedded-samples"))]
pub fn sampled_fret_noise(_vel: u8, _sr: f32, _rr: usize) -> Option<Box<dyn Voice>> {
None
}
#[cfg(feature = "embedded-samples")]
impl Voice for FretNoiseOneShot {
fn render(&mut self, out: &mut [f32]) -> bool {
let n = self.data.len();
for o in out.iter_mut() {
let j = self.pos as usize;
if j + 1 >= n {
return false;
}
let frac = self.pos - j as f32;
let v = crate::dsp::cubic4(
self.data[j.saturating_sub(1)],
self.data[j],
self.data[j + 1],
self.data[(j + 2).min(n - 1)],
frac,
);
*o += v * self.gain;
self.pos += self.step;
}
true
}
fn note_off(&mut self) {}
fn released(&self) -> bool {
true
}
fn rr_phase(&self) -> Option<usize> {
Some(self.rr)
}
#[cfg(test)]
fn kind(&self) -> &'static str {
"fretnoise"
}
}
#[cfg(feature = "embedded-samples")]
const CLAVINET_LEVEL: f32 = 0.80;
#[cfg(feature = "embedded-samples")]
const CLAVINET_RELEASE_T60: f32 = 0.06;
#[cfg(feature = "embedded-samples")]
const CLAVINET_SOURCE_SR: f32 = 44_100.0;
#[cfg(feature = "embedded-samples")]
fn clavinet_body_t60(key: u8) -> f32 {
let x = ((key as f32 - 58.0) / 15.0).clamp(0.0, 1.0);
let u = x * x * (3.0 - 2.0 * x);
5.0 + (3.3 - 5.0) * u
}
#[cfg(feature = "embedded-samples")]
fn clavinet_baked_t60(root: f32) -> f32 {
let midi = 69.0 + 12.0 * (root / 440.0).log2();
let frac = ((midi - 31.0) / 60.0).clamp(0.0, 1.0);
2.4 - 1.5 * frac
}
#[cfg(feature = "embedded-samples")]
fn find_clavinet_loop(data: &[f32], root: f32) -> Option<(usize, usize)> {
#[cfg(test)]
LOOP_SEARCHES.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if root <= 0.0 {
return None;
}
let period = CLAVINET_SOURCE_SR / root;
if period < 4.0 {
return None;
}
let n = data.len();
const START_LO_S: f32 = 0.16;
const START_HI_S: f32 = 0.34;
const MIN_LEN_S: f32 = 0.035;
const MAX_LEN_S: f32 = 0.11;
const BODY_END_S: f32 = 1.45;
let start_lo = (START_LO_S * CLAVINET_SOURCE_SR) as usize;
let min_len = (MIN_LEN_S * CLAVINET_SOURCE_SR) as usize;
let body_end = ((BODY_END_S * CLAVINET_SOURCE_SR) as usize).min(n.saturating_sub(2));
if start_lo == 0 || body_end <= start_lo + min_len {
return None;
}
let start_hi = ((START_HI_S * CLAVINET_SOURCE_SR) as usize).min(body_end - min_len);
let max_len = (MAX_LEN_S * CLAVINET_SOURCE_SR) as usize;
let stride = (period / 8.0).max(1.0) as usize;
let baked_t60 = clavinet_baked_t60(root);
let makeup_step = 10f32.powf(3.0 / (baked_t60 * CLAVINET_SOURCE_SR));
let mut flat = vec![0.0f32; body_end + 1];
let mut energy = vec![0.0f64; body_end + 2];
let mut makeup = makeup_step.recip();
for i in start_lo - 1..=body_end {
flat[i] = data[i] * makeup;
energy[i + 1] = energy[i] + f64::from(flat[i]) * f64::from(flat[i]);
makeup *= makeup_step;
}
let imbalance = |ls: usize, le: usize| -> f32 {
let mid = (ls + le) / 2;
let rms = |a: usize, b: usize| -> f32 {
((energy[b] - energy[a]) / (b - a).max(1) as f64).sqrt() as f32
};
let (r1, r2) = (rms(ls, mid), rms(mid, le));
(r1 - r2).abs() / (0.5 * (r1 + r2) + 1e-6)
};
let mut best: Option<(f32, usize, usize)> = None;
let mut ls = start_lo;
let stride_makeup = makeup_step.powi(stride as i32);
let mut ls_makeup = 1.0f32;
while ls <= start_hi {
let mut k = ((min_len as f32 / period).ceil() as usize).max(1);
loop {
let l = (k as f32 * period).round() as usize;
if l > max_len {
break;
}
let le = ls + l;
if le + 1 >= body_end {
break;
}
let scale = ls_makeup.recip();
let a = flat[ls] * scale;
let b = flat[le] * scale;
let prev_a = flat[ls - 1] * scale;
let prev_b = flat[le - 1] * scale;
let vseam = (b - a).abs();
let sseam = ((b - prev_b) - (a - prev_a)).abs();
let cost = vseam + 0.5 * sseam + 4.0 * imbalance(ls, le);
if best.is_none_or(|(bc, _, _)| cost < bc) {
best = Some((cost, ls, le));
}
k += 1;
}
ls += stride;
ls_makeup *= stride_makeup;
}
best.map(|(_, ls, le)| (ls, le))
}
fn clavinet() -> &'static [Zone] {
static B: OnceLock<Vec<Zone>> = OnceLock::new();
init_once!(B, {
bank!(
"clavinet_G1.flac" => 49.00,
"clavinet_C2.flac" => 65.41,
"clavinet_G2.flac" => 98.00,
"clavinet_C3.flac" => 130.81,
"clavinet_G3.flac" => 196.00,
"clavinet_C4.flac" => 261.63,
"clavinet_G4.flac" => 392.00,
"clavinet_C5.flac" => 523.25,
"clavinet_G5.flac" => 783.99,
"clavinet_C6.flac" => 1046.50,
"clavinet_G6.flac" => 1567.98,
)
})
}
pub fn clavinet_bank() -> &'static [Zone] {
clavinet()
}
#[cfg(feature = "embedded-samples")]
pub struct ClavinetSampled {
data: &'static [f32],
loop_start: usize,
loop_end: usize,
pos: f32,
base_step: f32,
bend: f32,
gain: f32,
body_mul: f32,
body_env: f32,
baked_t60: f32,
baked_makeup: f32,
rel_mul: f32,
env: f32,
releasing: bool,
}
#[cfg(feature = "embedded-samples")]
pub fn clavinet_sampled(key: u8, vel: u8, sr: f32, _seed: u32) -> Box<dyn Voice> {
let f = key_freq(key);
let zone = nearest(clavinet_bank(), f);
let (loop_start, loop_end) = zone
.sustain_loop(find_clavinet_loop)
.expect("embedded clavinet zone must contain a sustain loop");
Box::new(ClavinetSampled {
data: zone.data.as_slice(),
loop_start,
loop_end,
pos: 0.0,
base_step: 44_100.0 / sr * (f / zone.root),
bend: 1.0,
gain: CLAVINET_LEVEL * vel_amp(vel),
body_mul: 10f32.powf(-3.0 / (clavinet_body_t60(key) * sr)),
body_env: 1.0,
baked_t60: clavinet_baked_t60(zone.root),
baked_makeup: 1.0,
rel_mul: 10f32.powf(-3.0 / (CLAVINET_RELEASE_T60 * sr)),
env: 1.0,
releasing: false,
})
}
#[cfg(not(feature = "embedded-samples"))]
pub fn clavinet_sampled(_key: u8, _vel: u8, _sr: f32, _seed: u32) -> Box<dyn Voice> {
panic!("clavinet sample requested from a modeled-only ferrosintesis build")
}
#[cfg(feature = "embedded-samples")]
impl ClavinetSampled {
fn sample_at(&self, idx: isize) -> f32 {
let n = self.data.len() as isize;
let loop_start = self.loop_start as isize;
let loop_len = (self.loop_end - self.loop_start) as isize;
let idx = if idx < loop_start {
idx.clamp(0, n - 1) as usize
} else {
self.loop_start + (idx - loop_start).rem_euclid(loop_len) as usize
};
self.data[idx]
}
}
#[cfg(feature = "embedded-samples")]
impl Voice for ClavinetSampled {
fn render(&mut self, out: &mut [f32]) -> bool {
let step = self.base_step * self.bend;
let loop_len = (self.loop_end - self.loop_start) as f32;
let baked_makeup_mul = 10f32.powf(3.0 * step / (self.baked_t60 * CLAVINET_SOURCE_SR));
for o in out.iter_mut() {
let j = self.pos as usize;
let frac = self.pos - j as f32;
let in_loop = self.pos >= self.loop_start as f32;
let v = crate::dsp::cubic4(
self.sample_at(j as isize - 1),
self.sample_at(j as isize),
self.sample_at(j as isize + 1),
self.sample_at(j as isize + 2),
frac,
) * if in_loop { self.baked_makeup } else { 1.0 };
*o += v * self.gain * self.body_env * self.env;
self.pos += step;
if in_loop {
self.baked_makeup *= baked_makeup_mul;
} else if self.pos >= self.loop_start as f32 {
let offset = self.pos - self.loop_start as f32;
self.baked_makeup =
10f32.powf(3.0 * offset / (self.baked_t60 * CLAVINET_SOURCE_SR));
}
while self.pos >= self.loop_end as f32 {
self.pos -= loop_len;
let offset = self.pos - self.loop_start as f32;
self.baked_makeup =
10f32.powf(3.0 * offset / (self.baked_t60 * CLAVINET_SOURCE_SR));
}
self.body_env *= self.body_mul;
if self.releasing {
self.env *= self.rel_mul;
if self.env < 1e-4 {
return false;
}
} else if self.body_env < 1e-5 {
return false;
}
}
true
}
fn note_off(&mut self) {
self.releasing = true;
}
fn released(&self) -> bool {
self.releasing
}
fn set_pitch(&mut self, mult: f32) {
self.bend = mult;
}
#[cfg(test)]
fn kind(&self) -> &'static str {
"clavinetsampled"
}
}
#[cfg(all(test, not(feature = "embedded-samples")))]
mod modeled_only_tests {
use crate::voices;
#[test]
fn requested_b1_samples_fall_back_to_the_same_model() {
let render = |samples| {
let mut voice = voices::make(0, 60, 60, 44_100.0, 0x6A09_E667, samples);
let mut out = vec![0.0; 44_100];
voice.render(&mut out);
out
};
assert_eq!(
render(true),
render(false),
"a modeled-only build changed GM0 when samples were requested"
);
}
}
#[cfg(all(test, feature = "embedded-samples"))]
mod tests {
use super::*;
use crate::dsp::OnePole;
use crate::voices;
use crate::voices::Voice;
fn render_held(mut voice: Box<dyn Voice>, sr: f32, secs: f32) -> (Vec<f32>, bool) {
let mut buf = vec![0.0; (secs * sr) as usize];
let mut alive = true;
for chunk in buf.chunks_mut(128) {
if alive {
alive = voice.render(chunk);
}
}
(buf, alive)
}
fn dies_within(mut voice: Box<dyn Voice>, sr: f32, secs: f32) -> bool {
let mut left = (secs * sr) as usize;
while left > 0 {
let n = left.min(128);
let mut scratch = vec![0.0; n];
if !voice.render(&mut scratch) {
return true;
}
left -= n;
}
false
}
fn window_rms(buf: &[f32], sr: f32, start_s: f32, len_s: f32) -> f32 {
let a = (start_s * sr) as usize;
let b = ((start_s + len_s) * sr) as usize;
crate::testutil::rms(&buf[a..b])
}
fn decay_t60_between(buf: &[f32], sr: f32, early_s: f32, late_s: f32) -> f32 {
let early = window_rms(buf, sr, early_s, 0.10).max(1e-12);
let late = window_rms(buf, sr, late_s, 0.10).max(1e-12);
let drop_db = 20.0 * (early / late).log10();
60.0 * (late_s - early_s) / drop_db.max(1e-6)
}
fn band_decay_t60_between(buf: &[f32], sr: f32, f: f32, early_s: f32, late_s: f32) -> f32 {
let band = |start_s: f32| {
let a = (start_s * sr) as usize;
let b = ((start_s + 0.12) * sr) as usize;
crate::testutil::band_rms(&buf[a..b], sr, f, 2.0).max(1e-12)
};
let drop_db = 20.0 * (band(early_s) / band(late_s)).log10();
60.0 * (late_s - early_s) / drop_db.max(1e-6)
}
fn gm7_reference_t60_target(key: u8) -> f32 {
let x = ((key as f32 - 58.0) / 15.0).clamp(0.0, 1.0);
let u = x * x * (3.0 - 2.0 * x);
5.0 + (3.3 - 5.0) * u
}
#[cfg(ferrosintesis_repository_tests)]
fn assert_gong_provenance_matches_velocity_boundary(provenance: &str, loud_vel: u8) {
let soft_max = loud_vel
.checked_sub(1)
.expect("the loud gong boundary must leave a soft velocity range");
let hard_switch = format!("hard switch at velocity {loud_vel}");
let soft_range = format!("soft through velocity {soft_max}");
assert!(
provenance.contains(&hard_switch),
"gong provenance must name the shipped {hard_switch}"
);
assert!(
provenance.contains(&soft_range),
"gong provenance must name the shipped {soft_range}"
);
assert!(
!provenance.contains("velocity-crossfades"),
"gong provenance must not claim that the two recordings are summed"
);
}
#[cfg(ferrosintesis_repository_tests)]
#[test]
fn gong_provenance_describes_the_shipped_velocity_boundary() {
let layers = gong_layers();
assert!(
std::ptr::eq(gong_layer(GONG_LOUD_VEL - 1, layers), layers.0.as_slice()),
"the velocity below GONG_LOUD_VEL must select only the soft gong take"
);
assert!(
std::ptr::eq(gong_layer(GONG_LOUD_VEL, layers), layers.1.as_slice()),
"GONG_LOUD_VEL must select only the loud gong take"
);
let provenance = include_str!("../../ferrosintesis-samples-gong/PROVENANCE.md");
assert_gong_provenance_matches_velocity_boundary(provenance, GONG_LOUD_VEL);
}
#[cfg(ferrosintesis_repository_tests)]
#[test]
#[should_panic(expected = "gong provenance must name the shipped hard switch")]
fn gong_provenance_guard_rejects_a_stale_velocity_boundary() {
let provenance = include_str!("../../ferrosintesis-samples-gong/PROVENANCE.md");
assert_gong_provenance_matches_velocity_boundary(provenance, GONG_LOUD_VEL + 1);
}
#[cfg(ferrosintesis_repository_tests)]
fn normalized_ws(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[cfg(ferrosintesis_repository_tests)]
fn assert_text_contains(name: &str, text: &str, phrase: &str) {
let text = normalized_ws(text);
let phrase = normalized_ws(phrase);
assert!(
text.contains(&phrase),
"{name} must contain the current sax routing phrase {phrase:?}"
);
}
#[cfg(ferrosintesis_repository_tests)]
fn assert_text_omits(name: &str, text: &str, phrase: &str) {
let text = normalized_ws(text);
let phrase = normalized_ws(phrase);
assert!(
!text.contains(&phrase),
"{name} still contains retired sax routing phrase {phrase:?}"
);
}
#[cfg(ferrosintesis_repository_tests)]
#[test]
fn sax_published_docs_describe_the_looped_recording_voice() {
let provenance = include_str!("../../ferrosintesis-samples-sax/PROVENANCE.md");
let readme = include_str!("../../ferrosintesis-samples-sax/README.md");
let rustdoc = include_str!("../../ferrosintesis-samples-sax/src/lib.rs");
let notice = include_str!("../../ferrosintesis-samples-sax/NOTICE");
let prepare = include_str!("../../../tools/ferrosintesis-samples/prepare.py");
assert_text_contains(
"sax provenance",
provenance,
"recorded attack plus looped recorded sustain bank",
);
assert_text_contains(
"sax provenance",
provenance,
"The modeled reed is only the `--no-samples` / unusable-loop fallback",
);
assert_text_contains(
"sax README",
readme,
"recorded attack plus looped recorded sustain",
);
assert_text_contains(
"sax rustdoc",
rustdoc,
"recorded attack plus looped recorded sustain",
);
assert_text_contains(
"sax NOTICE",
notice,
"trimmed to keep attack plus loopable recorded sustain",
);
assert_text_contains(
"prepare.py sax comments",
prepare,
"recorded attack plus early loopable sustain",
);
for (name, text) in [
("sax provenance", provenance),
("sax README", readme),
("sax rustdoc", rustdoc),
("sax NOTICE", notice),
("prepare.py sax comments", prepare),
] {
for stale in [
"Saxophone attack/body bank",
"The model carries the\nsustain",
"the model carries the sustain",
"sample layer over the modeled reed",
"LA sample layer for the GM 64-67",
"MTG.SoloSax LA layer",
"attack+body transients",
"ferrosintesis LA sample layer",
] {
assert_text_omits(name, text, stale);
}
}
}
#[test]
fn clavinet_sampled_hold_survives_the_baked_tail_and_decays_in_output_time() {
let sr = 44100.0;
for &key in &[48u8, 53, 58, 63, 68, 73] {
let (buf, alive) = render_held(
voices::make(7, key, 110, sr, 0x4300 + key as u32, true),
sr,
3.0,
);
assert!(
alive,
"GM7 sampled key {key} died before 3.0 s; this is the baked 1.6 s wall"
);
let body = window_rms(&buf, sr, 2.10, 0.12);
assert!(
body > 2.0e-5,
"GM7 sampled key {key} is effectively silent after the old tail: rms {body:.8}"
);
let t60 = decay_t60_between(&buf, sr, 0.55, 2.55);
let want = gm7_reference_t60_target(key);
assert!(
(t60 - want).abs() <= 0.55,
"GM7 sampled key {key} t60 {t60:.2}s, expected {want:.2}s"
);
}
}
#[test]
fn clavinet_sampled_sustain_loops_have_no_step_outlier() {
if !crate::embedded_samples_available() {
return;
}
for zone in clavinet_bank() {
let (loop_start, loop_end) = zone
.sustain_loop(find_clavinet_loop)
.expect("embedded clavinet zone must contain a sustain loop");
let baked_t60 = clavinet_baked_t60(zone.root);
let flatten = |i: usize| {
let dt = i.saturating_sub(loop_start) as f32 / CLAVINET_SOURCE_SR;
zone.data[i] * 10f32.powf(3.0 * dt / baked_t60)
};
let mut steps: Vec<f32> = (loop_start..loop_end - 1)
.map(|i| (flatten(i + 1) - flatten(i)).abs())
.collect();
steps.sort_by(|a, b| a.partial_cmp(b).unwrap());
let p99 = steps[steps.len() * 99 / 100].max(1e-7);
let wrap = (flatten(loop_start) - flatten(loop_end - 1)).abs();
assert!(
wrap <= p99,
"GM7 clavinet zone {:.2} Hz loop step {wrap:.6} exceeds its p99 \
adjacent step {p99:.6}; the runtime wrap would click",
zone.root
);
}
}
#[test]
fn clavinet_sampled_attack_and_release_stay_bounded() {
let sr = 44100.0;
let (buf, alive) = render_held(voices::make(7, 60, 110, sr, 0x4301, true), sr, 0.08);
assert!(alive, "GM7 sampled attack should still be alive at 80 ms");
let peak = buf.iter().copied().map(f32::abs).fold(0.0, f32::max);
assert!(
(0.06..=0.65).contains(&peak),
"GM7 sampled attack peak moved outside the calibrated band: {peak:.4}"
);
let mut v = voices::make(7, 60, 110, sr, 0x4302, true);
let mut attack = vec![0.0; (0.45 * sr) as usize];
assert!(
v.render(&mut attack),
"held GM7 should survive into note-off"
);
v.note_off();
assert!(
dies_within(v, sr, 0.35),
"GM7 sampled note-off release should still terminate promptly"
);
assert!(
dies_within(voices::make(7, 48, 110, sr, 0x4303, true), sr, 12.0),
"GM7 sampled held decay must not leave a permanently live voice"
);
}
#[test]
fn clavinet_modeled_fallback_uses_the_same_multi_second_decay_idiom() {
let sr = 44100.0;
for &key in &[48u8, 63, 73] {
let (buf, alive) = render_held(
voices::make(7, key, 110, sr, 0x4310 + key as u32, false),
sr,
2.4,
);
assert!(
alive,
"GM7 modeled fallback key {key} died before a held 2.4 s note"
);
let t60 = decay_t60_between(&buf, sr, 0.35, 1.85);
let bt60 = band_decay_t60_between(&buf, sr, key_freq(key), 0.35, 1.85);
let want = gm7_reference_t60_target(key);
assert!(
(0.55 * want..=1.55 * want).contains(&t60),
"GM7 modeled fallback key {key} t60 {t60:.2}s, expected broadly around {want:.2}s"
);
assert!(
(0.55 * want..=1.55 * want).contains(&bt60),
"GM7 modeled fallback key {key} fundamental-band t60 {bt60:.2}s, expected broadly around {want:.2}s"
);
}
}
fn exercise_every_public_bank() {
for &(vel, rr2) in &[
(1u8, false),
(1, true),
(80, false),
(80, true),
(127, false),
(127, true),
] {
let _ = piano_bank(vel, rr2);
let _ = grand_bank(vel, rr2);
let _ = steinwayb_bank(vel, rr2);
let _ = kawai_bank(vel, rr2);
let _ = headroom_bank(vel, rr2);
let _ = musescoregrand_bank(vel, rr2);
let _ = ydpgrand_bank(vel, rr2);
let _ = honkytonk_bank(vel, rr2);
let _ = b1upright_bank(vel, rr2);
}
for vel in 0u8..=127 {
let _ = violin_bank(vel);
let _ = viola_bank(vel);
let _ = cello_bank(vel);
let _ = contrabass_bank(vel);
let _ = strings_bank(vel);
}
for rr in 0..(MANDOLIN_ROUND_ROBINS * 2 + 1) {
let _ = mandolin_bank(rr);
}
for program in 0u8..=127 {
for vel in [0u8, 64, 127] {
let _ = brass_bank(program, vel);
let _ = reed_bank(program, vel);
let _ = sax_bank(program, vel);
}
}
let _ = banjo_bank();
let _ = bottle_bank();
let _ = brass_section_bank();
let _ = celesta_bank();
let _ = chanter_bank();
let _ = clavinet_bank();
let _ = drone_g2_bank();
let _ = drone_g3_bank();
let _ = dulcimer_bank();
let _ = finger_bass_bank();
let _ = flute_bank();
let _ = glock_bank();
let _ = guitar_bank();
let _ = harp_bank();
let _ = harpsichord_bank();
let _ = marimba_bank();
let _ = musicbox_bank();
let _ = ocarina_bank();
let _ = panflute_bank();
let _ = pick_bass_bank();
let _ = pizzbass_bank();
let _ = recorder_bank();
let _ = eastman_picked_bank();
let _ = eastman_plucked_bank();
let _ = rhodes_bank();
let _ = shakuhachi_bank();
let _ = sitar_bank();
let _ = steel_bank();
let _ = timpani_bank();
let _ = tubular_bank();
let _ = vibraphone_bank();
let _ = xylo_bank();
}
fn exercise_every_lazy_cache() {
exercise_every_public_bank();
let _ = chanter_rr2();
let _ = bottle_loop_bank();
let _ = rain_loop();
let _ = gong_layers();
let _ = fret_noise_takes();
}
fn declared_lazy_caches(src: &str) -> Vec<(u32, String)> {
let mut item = String::from("<module>");
let mut out = Vec::new();
for (i, l) in src.lines().enumerate() {
let t = l.trim_start();
if let Some(rest) = t
.strip_prefix("pub fn ")
.or_else(|| t.strip_prefix("pub(crate) fn "))
.or_else(|| t.strip_prefix("fn "))
{
if let Some(name) = rest.split('(').next() {
item = name.to_string();
}
}
if t.starts_with("static ") && t.contains(": OnceLock<") {
let label = if l.starts_with("static ") {
t.trim_start_matches("static ")
.split(':')
.next()
.unwrap_or("<static>")
.to_string()
} else {
item.clone()
};
out.push((i as u32 + 1, label));
}
}
out
}
fn sampler_source() -> String {
std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("sampler.rs"),
)
.expect("sampler.rs is readable from its own crate")
}
#[test]
fn no_lazy_cache_bypasses_init_once() {
let src = sampler_source();
let shipped = src
.split_once("#[cfg(all(test, feature = \"embedded-samples\"))]")
.expect("the test module marks the end of the shipped module")
.0;
let lines: Vec<&str> = shipped.lines().collect();
let offenders: Vec<(usize, &str)> = lines
.iter()
.enumerate()
.filter(|(_, l)| l.contains("get_or_init("))
.filter(|(i, l)| {
let t = l.trim_start();
let near = lines[i.saturating_sub(3)..*i].join(" ");
!t.starts_with("$cell.get_or_init(") && !near.contains(".sustain_loop")
})
.map(|(i, l)| (i + 1, l.trim()))
.collect();
assert!(
offenders.is_empty(),
"{} cache initialization(s) bypass the counted `init_once!` funnel, so the \
prewarm-coverage oracles cannot see them:\n {}\n\nUse `init_once!(CELL, \
<builder>)` instead of a bare `get_or_init`.",
offenders.len(),
offenders
.iter()
.map(|(n, l)| format!("sampler.rs:{n}: {l}"))
.collect::<Vec<_>>()
.join("\n ")
);
}
#[test]
fn prewarm_covers_every_lazy_cache() {
const PROBE: &str = "FERRO_PREWARM_PROBE";
const NAME: &str = "sampler::tests::prewarm_covers_every_lazy_cache";
if std::env::var_os(PROBE).is_none() {
let out = std::process::Command::new(
std::env::current_exe().expect("the test binary's own path"),
)
.args([NAME, "--exact", "--nocapture", "--test-threads=1"])
.env(PROBE, "1")
.output()
.expect("re-exec this test binary");
assert!(
out.status.success(),
"the pristine-process probe failed:\n{}\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
return;
}
let src = sampler_source();
let declared = declared_lazy_caches(&src);
assert!(
declared.len() > 100,
"found only {} lazy caches — the scan is not reading what it thinks it is",
declared.len()
);
let owner = |line: u32| {
declared
.iter()
.rfind(|(decl, _)| *decl <= line)
.map(|(decl, item)| (*decl, item.clone()))
.expect("an init site always follows its own static declaration")
};
let sites = || CACHE_INIT_SITES.lock().expect("registry").clone();
prewarm();
let after_prewarm = sites();
exercise_every_lazy_cache();
let after_exercise = sites();
let reached: std::collections::BTreeSet<u32> =
after_exercise.iter().map(|&l| owner(l).0).collect();
let unreached: Vec<String> = declared
.iter()
.filter(|(line, _)| !reached.contains(line))
.map(|(line, item)| format!("sampler.rs:{line} ({item})"))
.collect();
assert!(
unreached.is_empty(),
"{} of {} lazy cache(s) are never initialized by prewarm() + the exercise \
sweep, so the coverage check is blind to them:\n {}\n\nAdd each to \
exercise_every_lazy_cache().",
unreached.len(),
declared.len(),
unreached.join("\n ")
);
let late: Vec<String> = after_exercise
.difference(&after_prewarm)
.map(|&line| {
let (decl, item) = owner(line);
format!("sampler.rs:{decl} ({item})")
})
.collect();
assert!(
late.is_empty(),
"{} lazy cache(s) were still uninitialized after prewarm() and initialized on \
first use. In the realtime path that work happens inside fill_ring()'s \
deadline-bearing block, which is the dropout prewarm_samples() exists to \
prevent:\n {}\n\nAdd each to sampler::prewarm().",
late.len(),
late.join("\n ")
);
}
#[test]
fn every_public_bank_accessor_is_exercised() {
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("sampler.rs"),
)
.expect("sampler.rs is readable from its own crate");
let declared: Vec<String> = src
.lines()
.filter_map(|l| l.trim().strip_prefix("pub fn "))
.filter_map(|rest| rest.split('(').next())
.filter(|name| name.ends_with("_bank"))
.map(str::to_string)
.collect();
assert!(
declared.len() > 20,
"found only {} public bank accessors — the scan is not reading what it \
thinks it is",
declared.len()
);
let body = src
.split_once("fn exercise_every_public_bank() {")
.expect("exercise_every_public_bank must exist")
.1;
let body = body
.split_once("\n }")
.expect("its body must terminate")
.0;
let missing: Vec<&String> = declared
.iter()
.filter(|name| !body.contains(&format!("{name}(")))
.collect();
assert!(
missing.is_empty(),
"{} public bank accessor(s) are not exercised, so the prewarm-coverage \
oracle silently does not cover them:\n {}\n\nAdd each to \
exercise_every_public_bank().",
missing.len(),
missing
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("\n ")
);
}
#[test]
fn prewarm_leaves_no_bank_uninitialized() {
use std::sync::atomic::Ordering;
prewarm();
let after_prewarm = BANK_INITS.load(Ordering::SeqCst);
exercise_every_public_bank();
let after_exercise = BANK_INITS.load(Ordering::SeqCst);
assert_eq!(
after_prewarm,
after_exercise,
"{} sample bank(s) were still uninitialized after prewarm() and decoded on \
first use. In the realtime path that decode happens inside fill_ring()'s \
deadline-bearing block, which is the dropout prewarm_samples() exists to \
prevent. Add the missing bank(s) to sampler::prewarm().",
after_exercise - after_prewarm
);
}
#[test]
fn every_drumkit_package_cache_is_explicitly_prewarmed() {
let manifest = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"),
)
.expect("ferrosintesis Cargo.toml is readable");
let packages: Vec<&str> = manifest
.lines()
.filter_map(|line| line.split_once(" = {").map(|(name, _)| name))
.filter(|name| name.starts_with("ferrosintesis-samples-drumkit"))
.collect();
assert!(
packages.len() >= 2,
"found only {} drum-kit packages — the package scan proves nothing",
packages.len()
);
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("sampler.rs"),
)
.expect("sampler.rs is readable from its own crate");
let body = src
.split_once("pub fn prewarm() {")
.expect("prewarm must exist")
.1
.split_once("\n}")
.expect("its body must terminate")
.0;
let missing: Vec<&str> = packages
.iter()
.copied()
.filter(|package| {
let crate_name = package.replace('-', "_");
!body.contains(&format!("{crate_name}::prewarm();"))
})
.collect();
assert!(
missing.is_empty(),
"drum-kit package cache(s) {missing:?} are absent from sampler::prewarm(); \
the first routed NoteOn would decode them inside the realtime callback"
);
}
#[test]
fn prewarm_leaves_no_sustain_loop_unsearched() {
use std::sync::atomic::Ordering;
prewarm();
let searches_before = LOOP_SEARCHES.load(Ordering::SeqCst);
let banks_before = BANK_INITS.load(Ordering::SeqCst);
assert!(
searches_before > 0,
"prewarm() resolved no sustain loop at all — the counter is not wired to the \
searches, so this oracle proves nothing"
);
for key in [30u8, 40, 44, 48, 55, 60, 67, 68, 70, 100] {
let _ = bottle_loop_voice(key, 100, 44_100.0, 7);
}
for program in 64u8..=67 {
for vel in [1u8, 127] {
for key in [12u8, 21, 48, 60, 72, 108, 127] {
let _ = sax_loop_voice(program, key, vel, 44_100.0, 7);
}
}
}
let searched = LOOP_SEARCHES.load(Ordering::SeqCst) - searches_before;
let decoded = BANK_INITS.load(Ordering::SeqCst) - banks_before;
assert_eq!(
searched, 0,
"{searched} sustain-loop search(es) ran during voice construction after \
prewarm(). Each one scans static PCM inside the audio callback (67.4 M \
multiply-accumulates for the bottle) and is exactly the dropout \
prewarm_samples() exists to prevent. Resolve the zone's loop in \
sampler::prewarm() and read it through Zone::sustain_loop."
);
assert_eq!(
decoded, 0,
"{decoded} sample bank(s) decoded during looped-sustain voice construction \
after prewarm(). Add them to sampler::prewarm()."
);
}
#[test]
fn every_sustain_loop_search_is_memoized_and_prewarmed() {
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("sampler.rs"),
)
.expect("sampler.rs is readable from its own crate");
let declared: Vec<&str> = src
.lines()
.filter_map(|l| l.trim().strip_prefix("fn find_"))
.filter_map(|rest| rest.split('(').next())
.filter(|name| name.ends_with("_loop"))
.collect();
assert!(
declared.len() >= 2,
"found only {} sustain-loop searches ({declared:?}) — the scan is not reading \
what it thinks it is",
declared.len()
);
let bypasses: Vec<String> = src
.lines()
.enumerate()
.filter(|(_, l)| !l.trim_start().starts_with("//"))
.filter(|(_, l)| !l.trim_start().starts_with("fn find_"))
.filter(|(_, l)| {
declared
.iter()
.any(|n| l.contains(&format!("find_{n}(")) && !l.contains("sustain_loop("))
})
.map(|(i, l)| format!("sampler.rs:{}: {}", i + 1, l.trim()))
.collect();
assert!(
bypasses.is_empty(),
"{} sustain-loop call site(s) bypass Zone::sustain_loop and re-run the search \
on every call:\n {}\n\nCall it as zone.sustain_loop(find_..._loop) so the \
answer is computed once, off the realtime thread.",
bypasses.len(),
bypasses.join("\n ")
);
let body = src
.split_once("pub fn prewarm() {")
.expect("prewarm must exist")
.1;
let body = body.split_once("\n}").expect("its body must terminate").0;
let unwarmed: Vec<&&str> = declared
.iter()
.filter(|n| !body.contains(&format!("find_{n}")))
.collect();
assert!(
unwarmed.is_empty(),
"sustain-loop search(es) {unwarmed:?} are never forced by sampler::prewarm(), \
so the first NoteOn that needs one still runs it inside the audio callback. \
Warm every zone of the owning bank there."
);
}
use ferrosintesis_samples_drumkit as kitbank;
use ferrosintesis_samples_drumkit2 as kitbank2;
#[test]
fn bagpipe_loop_sustains_across_wraps() {
let sr = 44100.0;
let mut v = bagpipe_chanter_loop(69, sr, 0);
let mut buf = vec![0f32; (1.6 * sr) as usize];
assert!(v.render(&mut buf), "chanter loop finished early");
let rms = |s: &[f32]| {
(s.iter().map(|x| (x * x) as f64).sum::<f64>() / s.len() as f64).sqrt() as f32
};
let e = |t0: f32, t1: f32| rms(&buf[(t0 * sr) as usize..(t1 * sr) as usize]);
let (a, b, c) = (e(0.2, 0.4), e(0.7, 0.9), e(1.3, 1.5));
assert!(buf.iter().all(|x| x.is_finite()), "non-finite in the loop");
assert!(a > 1e-3, "loop is silent");
for (lbl, x) in [("mid", b), ("late", c)] {
assert!(
(x / a - 1.0).abs() < 0.2,
"loop level not flat at {lbl}: {a:.4} -> {x:.4} (a broken wrap \
would go silent or decay)"
);
}
}
#[test]
fn every_note_named_zone_root_matches_its_filename() {
const FAMILY_TOLERANCE_CENTS: f32 = 70.0;
const OCTAVE_TOLERANCE_CENTS: f32 = 90.0;
let source = include_str!("sampler.rs");
let notes = [
("C", 0),
("C#", 1),
("D", 2),
("D#", 3),
("E", 4),
("F", 5),
("F#", 6),
("G", 7),
("G#", 8),
("A", 9),
("A#", 10),
("B", 11),
];
let mut measured: Vec<(String, String, f32)> = Vec::new();
for (index, _) in source.match_indices("\" =>") {
let head = &source[..index];
let Some(open) = head.rfind('"') else {
continue;
};
let name = &head[open + 1..];
let Some(stem) = name
.strip_suffix(".flac")
.or_else(|| name.strip_suffix(".wav"))
else {
continue;
};
let Some((family, rest)) = stem.split_once('_') else {
continue;
};
let token = rest.split('_').next().unwrap_or(rest);
let Some((note, semitone)) = notes
.iter()
.filter(|(note, _)| token.starts_with(note))
.max_by_key(|(note, _)| note.len())
else {
continue;
};
let Ok(octave) = token[note.len()..].parse::<i32>() else {
continue;
};
let tail = &source[index + 4..];
let root: f32 = match tail
.trim_start()
.split(|c: char| !(c.is_ascii_digit() || c == '.'))
.next()
.unwrap_or("")
.parse()
{
Ok(value) => value,
Err(_) => continue,
};
if root <= 0.0 {
continue;
}
let midi = (octave + 1) * 12 + semitone;
let labelled = 440.0 * 2f32.powf((midi as f32 - 69.0) / 12.0);
let cents = 1200.0 * (root / labelled).log2();
measured.push((family.to_owned(), format!("{stem} ({root} Hz)"), cents));
}
assert!(
measured.len() > 600,
"the zone scan found only {} note-named roots; it stopped working",
measured.len()
);
let mut families: std::collections::BTreeMap<String, Vec<(String, f32)>> =
std::collections::BTreeMap::new();
for (family, label, cents) in measured {
families.entry(family).or_default().push((label, cents));
}
assert!(
families.len() > 40,
"only {} zone families were seen; the scan is not reaching the tables",
families.len()
);
let mut errors: Vec<String> = Vec::new();
for (family, zones) in &families {
let mut sorted: Vec<f32> = zones.iter().map(|(_, c)| *c).collect();
sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite cents"));
let median = sorted[sorted.len() / 2];
for (label, cents) in zones {
let octaves = (cents / 1200.0).round() * 1200.0;
if (cents - octaves).abs() > OCTAVE_TOLERANCE_CENTS {
errors.push(format!(
"{label} is {cents:.1} cents from {family}'s label — not the \
named pitch in any octave"
));
continue;
}
if zones.len() >= 4 && (cents - median).abs() > FAMILY_TOLERANCE_CENTS {
errors.push(format!(
"{label} is {:.1} cents off {family}'s median ({median:.1}) — a \
transposed digit or a row/filename mismatch",
cents - median
));
}
}
}
assert!(
errors.is_empty(),
"{} zone root(s) disagree with their filenames:\n {}",
errors.len(),
errors.join("\n ")
);
}
#[test]
fn mandolin_zone_roots_match_the_fretboard() {
const STEPS: [f32; 9] = [5.0, 2.0, 5.0, 2.0, 5.0, 2.0, 5.0, 5.0, 2.0];
let cents = |a: f32, b: f32| 1200.0 * (b / a).log2();
let sets: Vec<&'static [Zone]> = (0..MANDOLIN_ROUND_ROBINS).map(mandolin_bank).collect();
for i in 1..sets.len() {
assert!(
!std::ptr::eq(sets[0], sets[i]),
"round robin {i} must be a distinct take-set from rr1"
);
}
assert!(
std::ptr::eq(sets[0], mandolin_bank(MANDOLIN_ROUND_ROBINS)),
"the round-robin index must WRAP, so a strike counter can grow without bound"
);
for (i, zones) in sets.iter().enumerate() {
let label = format!("rr{}", i + 1);
assert_eq!(zones.len(), 10, "{label} take-set must have ten zones");
for (i, z) in zones.iter().enumerate() {
assert!(!z.data.is_empty(), "{label} zone {i} decoded empty");
assert!(
(170.0..=1450.0).contains(&z.root),
"{label} zone {i} root {} Hz is outside the G3-E6 bank",
z.root
);
}
for (i, w) in zones.windows(2).enumerate() {
let got = cents(w[0].root, w[1].root) / 100.0;
assert!(
(got - STEPS[i]).abs() < 0.6,
"{label} zones {i}->{} span {got:.2} semitones, expected {} \
(roots {:.2} -> {:.2} Hz) — an octave or transcription error",
i + 1,
STEPS[i],
w[0].root,
w[1].root
);
}
}
for z in 0..10 {
let roots: Vec<f32> = sets.iter().map(|s| s[z].root).collect();
let lo = roots.iter().cloned().fold(f32::INFINITY, f32::min);
let hi = roots.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let spread = cents(lo, hi);
assert!(
spread < 30.0,
"zone {z}: the four takes' roots span {spread:.1} cents ({lo:.2}..{hi:.2} Hz) \
— they are the same note played four times, so this is a bad root"
);
}
}
#[test]
fn mandolin_tremolo_rotates_takes_while_single_take_banks_still_suppress() {
let sr = 44100.0;
let key = 76u8; let model = || voices::make(25, key, 100, sr, 7, false);
let mut la = match LaVoice::build_rr(
model(),
mandolin_bank,
MANDOLIN_ROUND_ROBINS,
0,
key,
100,
sr,
0.20,
(0.05, 0.28),
) {
Ok(la) => la,
Err(_) => panic!("the mandolin bank must wrap at E5"),
};
assert!(
la.rr.is_some(),
"a 4-take bank must arm round-robin rotation"
);
let mut seen: Vec<*const Zone> = vec![la.zone as *const Zone];
for _ in 0..(2 * MANDOLIN_ROUND_ROBINS - 1) {
assert!(
la.retrigger(key, 100),
"a plucked voice must accept a restrike"
);
assert_eq!(la.t, 0, "restrike must restart the crossfade clock");
assert_eq!(la.pos, 0.0, "restrike must rewind the sample read head");
assert_eq!(la.rel_gain, 1.0, "restrike must undo the release decay");
assert_eq!(
la.rel_mul, 1.0,
"restrike must not leave the sample decaying"
);
seen.push(la.zone as *const Zone);
}
for i in 0..MANDOLIN_ROUND_ROBINS {
for j in (i + 1)..MANDOLIN_ROUND_ROBINS {
assert_ne!(
seen[i], seen[j],
"strokes {i} and {j} of one cycle must play DIFFERENT takes"
);
}
assert_eq!(
seen[i],
seen[i + MANDOLIN_ROUND_ROBINS],
"stroke {i} and stroke {} must play the SAME take — the takes were \
recorded in pick-direction order, so the cycle must be strict",
i + MANDOLIN_ROUND_ROBINS
);
}
let mut steel = match LaVoice::build(
model(),
steel_bank(),
key,
100,
sr,
0.20,
(0.05, 0.28),
LaFx::default(),
DEFAULT_LA_RELEASE_T60,
LaBlend::Replacement,
DEFAULT_MAX_UP_RATIO,
) {
Ok(la) => la,
Err(_) => panic!("the steel bank must wrap at E5"),
};
assert!(
steel.rr.is_none(),
"a single-take bank must NOT arm rotation"
);
let mut buf = vec![0f32; 512];
steel.render(&mut buf);
let (t_before, pos_before) = (steel.t, steel.pos);
assert!(steel.retrigger(key, 100));
assert_eq!(
steel.t, t_before,
"a single-take bank must not restart its crossfade clock on a restrike"
);
assert_eq!(
steel.pos, pos_before,
"a single-take bank must not rewind its sample read head on a restrike"
);
assert!(
steel.rel_mul < 1.0,
"a single-take bank must still RETIRE its sampled attack on a restrike"
);
}
#[test]
fn bagpipe_chanter_rr2_and_drift_decorrelate() {
let sr = 44100.0;
let render = |seed: u32| {
let mut v = bagpipe_chanter_loop(69, sr, seed);
let mut buf = vec![0f32; (sr * 1.5) as usize];
v.render(&mut buf);
buf
};
let a = render(0);
let b = render(1); let c = render(2); assert!(
a.iter().zip(&b).any(|(x, y)| x != y),
"RR2 bank not engaged"
);
let late = (sr * 1.0) as usize;
assert!(
a[late..].iter().zip(&c[late..]).any(|(x, y)| x != y),
"drift walk did not decorrelate same-bank renders"
);
let a2 = render(0);
assert_eq!(a, a2, "seeded render must stay deterministic");
}
#[test]
fn bagpipe_chanter_zone_coverage() {
let z = chanter();
assert_eq!(z.len(), 10, "chanter RR1 zones: got {}", z.len());
let mut roots: Vec<f32> = z.iter().map(|z| z.root).collect();
roots.sort_by(|a, b| a.partial_cmp(b).unwrap());
for w in roots.windows(2) {
assert!(
w[1] / w[0] <= 1.25,
"adjacent zone gap {} -> {} exceeds 1.25",
w[0],
w[1]
);
}
}
#[test]
fn looped_sustain_banks_are_loopable() {
let banks: [(&str, &'static [Zone]); 4] = [
("chanter", chanter_bank()),
("chanter_rr2", chanter_rr2()),
("drone_g2", drone_g2_bank()),
("drone_g3", drone_g3_bank()),
];
if !crate::embedded_samples_available() {
return; }
for (label, zones) in banks {
for z in zones {
let x = z.data.as_slice();
let n = x.len();
assert!(n > 512, "{label}: zone too short to loop ({n})");
let period = 44100.0 / z.root;
let cycles = n as f64 / period as f64;
let off = (cycles - cycles.round()).abs();
assert!(
off < 0.05,
"{label} @{:.1} Hz: loop spans {cycles:.3} periods — a \
fractional count wraps the harmonics out of phase",
z.root
);
let blk = (0.020 * 44100.0) as usize;
let nb = n / blk;
assert!(nb >= 3, "{label}: too short for a steadiness check");
let mut lo = f64::MAX;
let mut hi: f64 = 0.0;
let mut b_lo = f64::MAX;
let mut b_hi: f64 = 0.0;
for b in 0..nb {
let s = &x[b * blk..(b + 1) * blk];
let e: f64 = s.iter().map(|v| (*v as f64) * (*v as f64)).sum();
let hf: f64 = s
.windows(2)
.map(|w| {
let d = (w[1] - w[0]) as f64;
d * d
})
.sum();
assert!(e > 0.0, "{label}: silent block in a sustain loop");
lo = lo.min(e);
hi = hi.max(e);
let br = hf / e;
b_lo = b_lo.min(br);
b_hi = b_hi.max(br);
}
let spread_db = 10.0 * (hi / lo).log10();
assert!(
spread_db < 3.0,
"{label} @{:.1} Hz: level varies {spread_db:.2} dB across the \
loop — the wrap steps by that much every cycle",
z.root
);
let bright_db = 10.0 * (b_hi / b_lo).log10();
assert!(
bright_db < 4.0,
"{label} @{:.1} Hz: brightness varies {bright_db:.2} dB across \
the loop — the wrap steps in timbre every cycle",
z.root
);
}
}
}
#[test]
fn rain_loop_wraps_seamlessly() {
let s = rain_loop();
assert!(
s.len() as f32 > 44100.0,
"rain loop implausibly short: {} samples",
s.len()
);
let mut steps: Vec<f32> = s.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
steps.sort_by(|a, b| a.partial_cmp(b).unwrap());
let p99 = steps[steps.len() * 99 / 100];
let wrap = (s[s.len() - 1] - s[0]).abs();
assert!(
wrap <= p99,
"rain loop wrap discontinuity {wrap:.4} exceeds the p99 adjacent step \
{p99:.4} — the loop seam clicks (the tail->head crossfade broke)"
);
}
fn routed_banks() -> [&'static kitbank::Bank; 13] {
[
&kitbank2::CRASH,
&kitbank::RIDE,
&kitbank::RIDE_BELL,
&kitbank2::CHINA,
&kitbank2::SPLASH,
&kitbank::KICK,
&kitbank::SNARE,
&kitbank::SIDESTICK,
&kitbank::TOM_HI,
&kitbank::TOM_LO,
&kitbank::HH_CLOSED,
&kitbank::HH_OPEN,
&kitbank::HH_PEDAL,
]
}
#[test]
fn sampled_electric_snare_distinct_from_acoustic() {
let sr = 44100.0;
let render = |key: u8| {
let mut v = sampled_drum(key, 100, 7, 0, sr).expect("snare key has a sampled voice");
let mut buf = vec![0f32; (sr * 0.4) as usize];
v.render(&mut buf);
buf
};
let ac = render(38);
let el = render(40);
assert!(
ac != el,
"electric snare (40) renders identically to the acoustic snare (38) — same drum"
);
let win = |s: &[f32]| s[..(0.12 * sr) as usize].to_vec();
let c_ac = crate::testutil::spectral_centroid(&win(&ac), sr, 200.0, 12_000.0);
let c_el = crate::testutil::spectral_centroid(&win(&el), sr, 200.0, 12_000.0);
println!("snare centroid: acoustic(38)={c_ac:.0} Hz electric(40)={c_el:.0} Hz");
assert!(
c_el > c_ac * 1.08,
"electric snare (40) not brighter than acoustic (38): {c_el:.0} Hz vs {c_ac:.0} Hz"
);
}
#[test]
fn sampled_drum_round_robin_cycles() {
let sr = 44100.0;
let bank = &kitbank::RIDE;
let takes: Vec<(usize, usize)> = (0..5u8)
.map(|i| {
let seed = 0x9E37 ^ (i as u32).wrapping_mul(2654435761);
let v = SampledDrum::new(
bank,
0.4,
DrumHit {
vel: 100,
seed,
hit_index: i,
},
sr,
1.0,
&DRUM_JITTER,
);
(v.layer, v.rr)
})
.collect();
assert!(
takes.iter().all(|&(l, _)| l == takes[0].0),
"same key + velocity must stay in one layer"
);
let rrs: Vec<usize> = takes.iter().map(|&(_, r)| r).collect();
assert_eq!(rrs, vec![0, 1, 2, 3, 0], "clean rr cycle with wrap");
for w in rrs.windows(2) {
assert_ne!(w[0], w[1], "immediate round-robin repeat");
}
}
#[test]
fn sampled_kick_has_deep_sub() {
let sr = 44100.0;
let hit = DrumHit {
vel: 110,
seed: 7,
hit_index: 0,
};
let render = |sub: bool| -> Vec<f32> {
let mut v = SampledDrum::new(&kitbank::KICK, 1.0, hit, sr, 1.0, &DRUM_JITTER);
if sub {
v = v.with_kick_sub(hit.vel, sr);
}
let mut buf = vec![0f32; (0.3 * sr) as usize];
v.render(&mut buf);
buf
};
let mag50 = |x: &[f32]| -> f32 {
let w = std::f32::consts::TAU * 50.0 / sr;
let coeff = 2.0 * w.cos();
let (mut s1, mut s2) = (0.0f32, 0.0f32);
for &v in x {
let s0 = v + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
((s1 * s1 + s2 * s2 - coeff * s1 * s2).max(0.0)).sqrt() / x.len() as f32
};
let with = mag50(&render(true));
let without = mag50(&render(false));
eprintln!(
"kick 50 Hz: with-sub={with:.6} without={without:.6} ratio={:.2}",
with / without.max(1e-9)
);
assert!(
with > 1.8 * without.max(1e-9),
"kick sub-layer adds no deep 50 Hz content: with={with:.5} without={without:.5}"
);
}
#[test]
fn sampled_drum_velocity_selects_layer() {
let sr = 44100.0;
let bank = &kitbank::RIDE; let layer = |vel: u8| {
SampledDrum::new(
bank,
0.4,
DrumHit {
vel,
seed: 7,
hit_index: 0,
},
sr,
1.0,
&DRUM_JITTER,
)
.layer
};
assert_eq!(layer(30), 0);
assert_eq!(layer(42), 0);
assert_eq!(layer(43), 1);
assert_eq!(layer(85), 1);
assert_eq!(layer(86), 2);
assert_eq!(layer(120), 2);
let soft = SampledDrum::new(
bank,
0.4,
DrumHit {
vel: 30,
seed: 7,
hit_index: 0,
},
sr,
1.0,
&DRUM_JITTER,
);
let hard = SampledDrum::new(
bank,
0.4,
DrumHit {
vel: 120,
seed: 7,
hit_index: 0,
},
sr,
1.0,
&DRUM_JITTER,
);
assert!(
!std::ptr::eq(soft.data, hard.data),
"soft and hard hits must play different takes"
);
assert!(hard.gain > soft.gain * 2.0, "velocity gain not continuous");
}
#[test]
fn sampled_drum_choke_reaches_silence_within_20_ms() {
let sr = 44100.0;
let mut v = SampledDrum::new(
&kitbank2::CRASH,
0.4,
DrumHit {
vel: 110,
seed: 7,
hit_index: 0,
},
sr,
1.0,
&DRUM_JITTER,
);
let mut head = vec![0f32; (0.30 * sr) as usize];
assert!(v.render(&mut head));
let pre = head[head.len() - 441..]
.iter()
.fold(0f32, |m, &x| m.max(x.abs()));
assert!(pre > 1e-4, "crash should still ring at 0.3 s: {pre}");
v.choke();
let mut tail = vec![0f32; (0.05 * sr) as usize];
v.render(&mut tail);
let post = tail[(0.020 * sr) as usize..]
.iter()
.fold(0f32, |m, &x| m.max(x.abs()));
assert!(
post < pre * 2e-3,
"choked cymbal still audible after 20 ms: {post} vs pre {pre}"
);
let mut more = vec![0f32; 64];
assert!(!v.render(&mut more), "choked voice must terminate");
}
#[test]
fn sampled_drum_has_no_boundary_click() {
let sr = 44100.0;
for bank in routed_banks() {
for layer in 0..bank.layers() {
for rr in 0..bank.round_robins {
let pcm = bank.pcm(layer, rr);
let peak = pcm.iter().map(|&v| (v as i32).abs()).max().unwrap() as f32;
let head = pcm[..44].iter().map(|&v| (v as i32).abs()).max().unwrap() as f32;
let tail = pcm[pcm.len() - 44..]
.iter()
.map(|&v| (v as i32).abs())
.max()
.unwrap() as f32;
let name = bank.file_name(layer, rr);
assert!(
head < 0.05 * peak,
"{name}: hot first ms ({head} vs {peak})"
);
assert!(tail < 0.02 * peak, "{name}: hot last ms ({tail} vs {peak})");
}
}
let mut v = SampledDrum::new(
bank,
0.4,
DrumHit {
vel: 110,
seed: 7,
hit_index: 0,
},
sr,
1.0,
&DRUM_JITTER,
);
let mut buf = vec![0f32; (3.0 * sr) as usize];
let alive = v.render(&mut buf);
assert!(!alive, "3 s must outlast every bank take");
let peak = buf.iter().fold(0f32, |m, &x| m.max(x.abs()));
assert!(peak > 0.01, "{}: silent render", bank.name);
assert!(
buf[0].abs() < 0.05 * peak,
"{}: rendered onset steps ({} vs peak {peak})",
bank.name,
buf[0]
);
let last = buf.iter().rposition(|&x| x != 0.0).unwrap();
let end_peak = buf[last.saturating_sub(88)..=last]
.iter()
.fold(0f32, |m, &x| m.max(x.abs()));
assert!(
end_peak < 0.05 * peak,
"{}: rendered tail steps ({end_peak} vs peak {peak})",
bank.name
);
}
}
#[test]
fn banks_parse() {
for z in violin_f()
.iter()
.chain(violin_p())
.chain(flute())
.chain(grand_pp())
.chain(grand_mf())
.chain(grand_f())
.chain(grand_pp_rr2())
.chain(grand_mf_rr2())
.chain(grand_f_rr2())
.chain(trumpet_p())
.chain(trumpet_f())
.chain(mutetpt_p())
.chain(mutetpt_f())
.chain(trombone_p())
.chain(trombone_f())
.chain(tuba_p())
.chain(tuba_f())
.chain(horn_p())
.chain(horn_f())
.chain(oboe_p())
.chain(oboe_f())
.chain(bassoon_p())
.chain(bassoon_f())
.chain(clarinet_p())
.chain(clarinet_f())
.chain(nylon())
.chain(strsec_p())
.chain(strsec_f())
{
assert!(z.data.len() > 20_000, "zone too short: {}", z.data.len());
assert!((25.0..2500.0).contains(&z.root), "odd root {}", z.root);
let peak = z.data.iter().fold(0f32, |m, &v| m.max(v.abs()));
assert!(peak > 0.5, "zone not normalised: peak {peak}");
}
for z in piano_pp()
.iter()
.chain(piano_mf())
.chain(piano_f())
.chain(piano_pp_rr2())
.chain(piano_mf_rr2())
.chain(piano_f_rr2())
{
assert!(z.data.len() > 20_000, "zone too short: {}", z.data.len());
assert!((25.0..2500.0).contains(&z.root), "odd root {}", z.root);
let peak = z.data.iter().fold(0f32, |m, &v| m.max(v.abs()));
assert!(
(0.12..=0.91).contains(&peak),
"conditioned piano zone outside its shared-headroom range: peak {peak}"
);
}
}
#[test]
fn upright_round_robin_bank_only_aliases_declared_single_takes() {
if !crate::embedded_samples_available() {
return;
}
let pp1 = piano_pp();
let pp2 = piano_pp_rr2();
for zone in 0..2 {
assert_eq!(
pp1[zone].data, pp2[zone].data,
"quiet C2/G2 are the declared single-take cells"
);
}
for zone in 2..pp1.len() {
assert_ne!(
pp1[zone].data, pp2[zone].data,
"every other quiet cell must have a real second take"
);
}
for (first, second) in [(piano_mf(), piano_mf_rr2()), (piano_f(), piano_f_rr2())] {
for zone in 0..first.len() {
assert_ne!(
first[zone].data, second[zone].data,
"mf/f cells must have real second takes"
);
}
}
}
#[test]
fn kawai_logical_aliases_share_decoded_banks() {
if !crate::embedded_samples_available() {
return;
}
assert!(
std::ptr::eq(kawai_pp_rr2(), kawai_mf()),
"Kawai pp RR2 and mf RR1 are the same upstream velocity layer"
);
assert!(
std::ptr::eq(kawai_mf_rr2(), kawai_f_rr2()),
"Kawai mf RR2 and f RR2 are the same upstream velocity layer"
);
}
#[test]
fn steinway_logical_aliases_share_decoded_banks() {
if !crate::embedded_samples_available() {
return;
}
assert!(
std::ptr::eq(steinwayb_pp_rr2(), steinwayb_mf()),
"Steinway pp RR2 and mf RR1 are the same upstream velocity layer"
);
assert!(
std::ptr::eq(steinwayb_mf_rr2(), steinwayb_f()),
"Steinway mf RR2 and f RR1 are the same upstream velocity layer"
);
assert!(
std::ptr::eq(steinwayb_pp_rr2(), steinwayb_f_rr2()),
"Steinway pp RR2 and f RR2 are the same upstream velocity layer"
);
}
fn pitch_of(seg: &[f32], sr: f32) -> f32 {
let mut lp1 = OnePole::lowpass(600.0, sr);
let mut lp2 = OnePole::lowpass(600.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)
}
#[test]
fn la_pitch_a4() {
let sr = 44100.0;
let mut v = voices::make(40, 69, 100, sr, 5, true);
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
let hz = pitch_of(&buf[6615..24255], sr); assert!((hz - 440.0).abs() < 12.0, "measured {hz} Hz");
}
#[test]
fn la_brass_pitch_integrity() {
let sr = 44100.0;
for (program, key, name) in [
(56u8, 69u8, "trumpet"),
(57, 55, "trombone"),
(58, 40, "tuba"),
(59, 69, "muted-trumpet"),
(60, 62, "french-horn"),
(61, 69, "brass-section"),
] {
let f0 = crate::dsp::key_freq(key);
let mut v = voices::make(program, key, 100, sr, 5, true);
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
let hz = crate::testutil::peak_locate(&buf[6615..24255], sr, f0 * 0.8, f0 * 1.25);
let cents = 1200.0 * (hz / f0).log2();
assert!(
cents.abs() < 45.0,
"{name}: layered pitch {hz:.2} Hz vs nominal {f0:.2} Hz ({cents:.0} cents)"
);
}
}
#[test]
fn la_brass_attack_sharpness() {
let sr = 44100.0;
for (program, key, hf_gain, name) in [
(56u8, 69u8, 1.5f32, "trumpet"),
(57, 55, 1.3, "trombone"),
(58, 40, 2.0, "tuba"),
(59, 69, 1.3, "muted-trumpet"),
(60, 62, 0.0, "french-horn"),
] {
let early = |samples: bool| {
let mut v = voices::make(program, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (0.05 * sr) as usize];
v.render(&mut buf);
buf
};
let (on, off) = (early(true), early(false));
let diff: Vec<f32> = on.iter().zip(&off).map(|(a, b)| a - b).collect();
let (d, o) = (crate::testutil::rms(&diff), crate::testutil::rms(&off));
assert!(
d > 0.3 * o,
"{name}: onset barely changes with the layer (diff {d:.5} vs off {o:.5})"
);
let hf_frac = |buf: &[f32]| {
crate::testutil::hp_rms(buf, sr, 1500.0) / crate::testutil::rms(buf).max(1e-9)
};
let (r_on, r_off) = (hf_frac(&on), hf_frac(&off));
assert!(
r_on > r_off * hf_gain,
"{name}: attack not sharper: hf-frac on {r_on:.4} vs off {r_off:.4}"
);
}
}
#[test]
fn la_reed_pitch_integrity() {
let sr = 44100.0;
for (program, key, name) in [
(68u8, 76u8, "oboe"),
(69, 64, "english-horn"),
(70, 48, "bassoon"),
(71, 60, "clarinet"),
] {
let f0 = crate::dsp::key_freq(key);
let mut v = voices::make(program, key, 100, sr, 5, true);
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
let hz = crate::testutil::peak_locate(&buf[6615..24255], sr, f0 * 0.8, f0 * 1.25);
let cents = 1200.0 * (hz / f0).log2();
assert!(
cents.abs() < 45.0,
"{name}: layered pitch {hz:.2} Hz vs nominal {f0:.2} Hz ({cents:.0} cents)"
);
}
}
#[test]
fn la_reed_attack_sharpness() {
let sr = 44100.0;
for (program, key, name) in [
(68u8, 76u8, "oboe"),
(69, 64, "english-horn"),
(70, 48, "bassoon"),
(71, 60, "clarinet"),
] {
let early = |samples: bool| {
let mut v = voices::make(program, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (0.05 * sr) as usize];
v.render(&mut buf);
buf
};
let (on, off) = (early(true), early(false));
let diff: Vec<f32> = on.iter().zip(&off).map(|(a, b)| a - b).collect();
let (d, o) = (crate::testutil::rms(&diff), crate::testutil::rms(&off));
assert!(
d > 0.3 * o,
"{name}: onset barely changes with the layer (diff {d:.5} vs off {o:.5})"
);
let hf_frac = |buf: &[f32]| {
crate::testutil::hp_rms(buf, sr, 1500.0) / crate::testutil::rms(buf).max(1e-9)
};
let (r_on, r_off) = (hf_frac(&on), hf_frac(&off));
assert!(
r_on < r_off * 1.05,
"{name}: sampled attack hissier than the model: hf-frac on {r_on:.4} vs off {r_off:.4}"
);
}
}
#[test]
fn la_sax_pitch_integrity() {
let sr = 44100.0;
for (program, key, name) in [
(64u8, 72u8, "soprano-sax"),
(65, 65, "alto-sax"),
(66, 60, "tenor-sax"),
(67, 48, "baritone-sax"),
] {
let f0 = crate::dsp::key_freq(key);
let mut v = voices::make(program, key, 100, sr, 5, true);
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
let hz = crate::testutil::peak_locate(&buf[6615..24255], sr, f0 * 0.8, f0 * 1.25);
let cents = 1200.0 * (hz / f0).log2();
assert!(
cents.abs() < 45.0,
"{name}: layered pitch {hz:.2} Hz vs nominal {f0:.2} Hz ({cents:.0} cents)"
);
}
}
#[test]
fn la_sax_audible() {
let sr = 44100.0;
for (program, key, name) in [
(64u8, 72u8, "soprano-sax"),
(65, 65, "alto-sax"),
(66, 60, "tenor-sax"),
(67, 48, "baritone-sax"),
] {
let early = |samples: bool| {
let mut v = voices::make(program, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (0.05 * sr) as usize];
v.render(&mut buf);
buf
};
let (on, off) = (early(true), early(false));
let diff: Vec<f32> = on.iter().zip(&off).map(|(a, b)| a - b).collect();
let (d, o) = (crate::testutil::rms(&diff), crate::testutil::rms(&off));
assert!(
d > 0.3 * o,
"{name}: layer barely changes the onset (diff {d:.5} vs off {o:.5})"
);
}
}
#[cfg(test)]
fn la_engaged(program: u8, key: u8, sr: f32) -> bool {
let render = |samples: bool| {
let mut v = voices::make(program, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (0.03 * sr) as usize];
v.render(&mut buf);
buf
};
render(true) != render(false)
}
#[cfg(test)]
fn la_engaged_bank(program: u8, bank: u8, key: u8, sr: f32) -> bool {
let render = |samples: bool| {
let mut v = crate::altbank::make(program, bank, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (0.03 * sr) as usize];
v.render(&mut buf);
buf
};
render(true) != render(false)
}
#[test]
fn la_engagement_never_depends_on_output_rate() {
let keys = [24u8, 28, 33, 40, 52, 64, 76, 88];
let mut broken: Vec<String> = Vec::new();
for program in 0u8..=127 {
for &key in &keys {
let reference = la_engaged(program, key, 44_100.0);
for &sr in &[48_000.0f32, 96_000.0] {
if la_engaged(program, key, sr) != reference {
broken.push(format!(
"GM {program} key {key}: engages={reference} at 44100 Hz but \
{} at {sr:.0} Hz",
!reference
));
}
}
}
}
assert!(
broken.is_empty(),
"{} (program, key, rate) combination(s) change LA sample eligibility with the \
output rate. Eligibility must be decided on the pitch ratio \
`target_hz / zone.root`, and only then converted to the output clock by \
`* 44100 / sr`:\n {}",
broken.len(),
broken.join("\n ")
);
}
#[test]
fn la_bass_onset_engages_at_every_supported_rate() {
for (program, key, name) in [
(33u8, 28u8, "GM33 fingered bass E1"),
(33, 33, "GM33 fingered bass A1"),
(34, 40, "GM34 picked bass E2"),
(35, 28, "GM35 fretless bass E1"),
] {
for &sr in &[44_100.0f32, 48_000.0, 96_000.0] {
assert!(
la_engaged_bank(program, 1, key, sr),
"{name}: the sampled onset does not engage on the CC0=1 alt bank at \
{sr:.0} Hz — this note's repitch ratio is ~1.0, the most credible \
there is"
);
}
}
}
#[test]
fn sax_loop_level_parity_and_flat() {
let sr = 44100.0;
for (program, key, name) in [
(64u8, 72u8, "soprano"),
(65, 65, "alto"),
(66, 60, "tenor"),
(67, 48, "baritone"),
] {
assert_eq!(
voices::make(program, key, 100, sr, 5, true).kind(),
"saxloop",
"{name}: samples-on must engage the looped voice, not the modeled fallback"
);
let hold = |samples: bool| {
let mut v = voices::make(program, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (1.5 * sr) as usize];
v.render(&mut buf);
buf[(0.6 * sr) as usize..(1.4 * sr) as usize].to_vec()
};
let (loop_h, model_h) = (hold(true), hold(false));
let (rl, rm) = (
crate::testutil::rms(&loop_h),
crate::testutil::rms(&model_h),
);
let db = 20.0 * (rl / rm).log10();
assert!(
(-1.0..=4.5).contains(&db),
"{name}: looped hold {db:+.1} dB vs the modeled reed — out of the raw offset \
band (loop {rl:.4} vs model {rm:.4})"
);
let fl = (0.02 * sr) as usize;
let frames: Vec<f32> = loop_h
.chunks(fl)
.filter(|c| c.len() == fl)
.map(crate::testutil::rms)
.collect();
let mean = frames.iter().sum::<f32>() / frames.len() as f32;
let var = frames.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / frames.len() as f32;
let cov = var.sqrt() / (mean + 1e-9);
assert!(
(0.025..0.12).contains(&cov),
"{name}: hold liveness (frame-RMS CoV {cov:.3}) out of band — \
static (dead loop) below 0.025, or a decay-sawtooth pulse above 0.12"
);
let zones = sax_bank(program, 100);
let zone = nearest(zones, crate::dsp::key_freq(key));
let (ls, le) = zone
.sustain_loop(find_sax_loop)
.expect("a loop was found above");
let wrap = (zone.data[le] - zone.data[ls]).abs();
assert!(
wrap < 0.03,
"{name}: loop wrap discontinuity {wrap:.4} (loop {ls}..{le}) — would click"
);
}
}
#[test]
fn packaged_sax_takes_outside_the_zone_tables_are_documented() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/ferrosintesis has a parent");
let crate_dir = root.join("ferrosintesis-samples-sax");
let packaged: std::collections::BTreeSet<String> =
std::fs::read_dir(crate_dir.join("samples"))
.expect("sax samples directory")
.map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".wav") || n.ends_with(".flac"))
.collect();
let source = std::fs::read_to_string(root.join("ferrosintesis/src/sampler.rs"))
.expect("sampler.rs is readable");
let zoned: std::collections::BTreeSet<String> = source
.match_indices("\" =>")
.filter_map(|(end, _)| {
let head = &source[..end];
let start = head.rfind('"')?;
Some(head[start + 1..].to_owned())
})
.filter(|name| {
name.starts_with("sax_") && (name.ends_with(".wav") || name.ends_with(".flac"))
})
.collect();
assert!(
zoned.len() > 60,
"the zone-table scan found only {} sax entries; it stopped working",
zoned.len()
);
let excluded: Vec<&String> = packaged.difference(&zoned).collect();
let provenance = std::fs::read_to_string(crate_dir.join("PROVENANCE.md"))
.expect("sax PROVENANCE.md is readable");
for name in &excluded {
let stem = name.trim_end_matches(".flac").trim_end_matches(".wav");
assert!(
provenance.contains(stem),
"{name} is packaged but is not a runtime zone, and PROVENANCE.md never \
mentions it — a consumer is told every packaged take supplies the voice"
);
}
assert!(
provenance.contains(&format!(
"{} packaged samples and {} reachable zones",
packaged.len(),
zoned.len()
)),
"PROVENANCE.md does not state the {} packaged / {} reachable split",
packaged.len(),
zoned.len()
);
}
#[test]
fn baritone_sax_bank_rejects_the_rough_source_population_outlier() {
struct Reading {
name: String,
correlation: f64,
shipping: bool,
}
fn best_cycle_correlation(data: &[f32], root: f32) -> f64 {
let body = &data[(0.30 * 44_100.0) as usize..(0.50 * 44_100.0) as usize];
let period = (44_100.0 / root).round() as isize;
(-5..=5)
.map(|offset| {
let lag = (period + offset) as usize;
let (mut dot, mut a2, mut b2) = (0.0f64, 0.0f64, 0.0f64);
for (&a, &b) in body[..body.len() - lag].iter().zip(&body[lag..]) {
let (a, b) = (a as f64, b as f64);
dot += a * b;
a2 += a * a;
b2 += b * b;
}
dot / (a2 * b2).sqrt().max(1e-20)
})
.max_by(f64::total_cmp)
.expect("the lag sweep is non-empty")
}
let mut readings = Vec::new();
for (layer, zones) in [("p", sax_bar_p()), ("f", sax_bar_f())] {
for zone in zones {
readings.push(Reading {
name: format!("shipping {layer} {:.2} Hz", zone.root),
correlation: best_cycle_correlation(&zone.data, zone.root),
shipping: true,
});
}
}
let mut shipping_roughness: Vec<f64> = readings
.iter()
.map(|reading| 1.0 - reading.correlation)
.collect();
shipping_roughness.sort_by(f64::total_cmp);
let middle = shipping_roughness.len() / 2;
let median_roughness = if shipping_roughness.len().is_multiple_of(2) {
(shipping_roughness[middle - 1] + shipping_roughness[middle]) / 2.0
} else {
shipping_roughness[middle]
};
let outlier_bar = 10.0 * median_roughness;
for (name, root) in [
("sax_bar_G#3_p.flac", 208.95f32),
("sax_bar_G#3_f.flac", 209.52f32),
] {
let data = parse_wav(embedded_wav(name));
readings.push(Reading {
name: name.to_owned(),
correlation: best_cycle_correlation(&data, root),
shipping: false,
});
}
let outliers: Vec<&Reading> = readings
.iter()
.filter(|reading| 1.0 - reading.correlation > outlier_bar)
.collect();
let census: Vec<String> = readings
.iter()
.map(|reading| {
format!(
"{}={:.5}{}",
reading.name,
reading.correlation,
if reading.shipping { " [shipping]" } else { "" }
)
})
.collect();
assert_eq!(
outliers.len(),
1,
"expected one baritone-sax population outlier beyond the median-relative \
roughness bar {outlier_bar:.5}; census: {census:?}"
);
assert_eq!(
outliers[0].name, "sax_bar_G#3_f.flac",
"the unique rough source must remain the rejected forte G#3 take; \
census: {census:?}"
);
assert!(
!outliers[0].shipping,
"the rejected rough G#3 forte take was restored to a runtime bank; \
census: {census:?}"
);
}
#[test]
fn baritone_sax_hold_does_not_expose_the_source_loop_period() {
fn spectral_envelope_periodicity(signal: &[f32], sr: f32, f0: f32, lag_s: f32) -> f32 {
const HARMONICS: usize = 8;
let frame_len = 512usize;
let hop = 64usize;
let mut shapes = Vec::<[f32; HARMONICS]>::new();
for frame in signal.windows(frame_len).step_by(hop) {
let windowed: Vec<f32> = frame
.iter()
.enumerate()
.map(|(i, &x)| {
let w = 0.5
- 0.5
* (std::f32::consts::TAU * i as f32 / (frame_len - 1) as f32).cos();
x * w
})
.collect();
let mut shape = [0.0; HARMONICS];
for (h, value) in shape.iter_mut().enumerate() {
let hz = f0 * (h + 1) as f32;
*value = [0.98, 1.0, 1.02]
.iter()
.map(|&scale| crate::testutil::mag_at(&windowed, sr, hz * scale))
.sum::<f32>()
.max(1e-8)
.ln();
}
let frame_mean = shape.iter().sum::<f32>() / HARMONICS as f32;
shape.iter_mut().for_each(|x| *x -= frame_mean);
shapes.push(shape);
}
for h in 0..HARMONICS {
let mean = shapes.iter().map(|shape| shape[h]).sum::<f32>() / shapes.len() as f32;
shapes.iter_mut().for_each(|shape| shape[h] -= mean);
}
let correlation = |lag_s: f32| {
let lag = ((lag_s * sr) / hop as f32).round().max(1.0) as usize;
assert!(shapes.len() > lag + 8, "periodicity window is too short");
let mut covariance = 0.0f64;
let mut energy_a = 0.0f64;
let mut energy_b = 0.0f64;
for i in 0..shapes.len() - lag {
for (&a, &b) in shapes[i].iter().zip(&shapes[i + lag]) {
let a = a as f64;
let b = b as f64;
covariance += a * b;
energy_a += a * a;
energy_b += b * b;
}
}
covariance / (energy_a * energy_b).sqrt().max(1e-20)
};
let target = correlation(lag_s);
let mut decoys = [
correlation(0.73 * lag_s),
correlation(1.37 * lag_s),
correlation(1.91 * lag_s),
];
decoys.sort_by(f64::total_cmp);
(target - decoys[1]) as f32
}
let sr = 44_100.0;
for (key, vel) in [
(64u8, 72u8),
(64, 110),
(65, 72),
(65, 110),
(66, 72),
(66, 110),
(67, 72),
(67, 110),
(68, 72),
(68, 110),
(73, 72),
(73, 110),
] {
let f0 = crate::dsp::key_freq(key);
let zone = nearest(sax_bank(67, vel), f0);
let (loop_start, loop_end) = zone
.sustain_loop(find_sax_loop)
.expect("covered baritone-sax note must have a recorded sustain loop");
let source_step = (f0 / zone.root) * 44_100.0 / sr;
let loop_period_s = (loop_end - loop_start) as f32 / source_step / sr;
let seed = 0x1760_0000 + key as u32;
let mut voice = voices::make(67, key, vel, sr, seed, true);
assert_eq!(
voice.kind(),
"saxloop",
"key {key} velocity {vel} fell back to the model"
);
let mut rendered = vec![0.0; (1.35 * sr) as usize];
voice.render(&mut rendered);
let hold = &rendered[(0.55 * sr) as usize..(1.30 * sr) as usize];
let at_period = spectral_envelope_periodicity(hold, sr, f0, loop_period_s);
let at_twice = spectral_envelope_periodicity(hold, sr, f0, 2.0 * loop_period_s);
let exposed = at_period.max(at_twice);
let mut control =
SaxLoopVoice::new(zone, f0, vel, sr, sax_program_gain(67), seed, false)
.expect("covered baritone-sax note must engage the single-slice control");
let mut control_rendered = vec![0.0; (1.35 * sr) as usize];
control.render(&mut control_rendered);
let control_hold = &control_rendered[(0.55 * sr) as usize..(1.30 * sr) as usize];
let control_at_period =
spectral_envelope_periodicity(control_hold, sr, f0, loop_period_s);
let control_at_twice =
spectral_envelope_periodicity(control_hold, sr, f0, 2.0 * loop_period_s);
let control_exposed = control_at_period.max(control_at_twice);
assert!(
control_exposed > 0.40,
"GM67 key {key} velocity {vel}: the single-slice positive control did not \
expose the {loop_period_s:.4}s source loop (excess {control_exposed:.3}; \
1x {control_at_period:.3}, 2x {control_at_twice:.3})"
);
assert!(
exposed < 0.65 * control_exposed,
"GM67 key {key} velocity {vel}: spectral-envelope correlation excess \
{exposed:.3} exposes the {loop_period_s:.4}s source loop \
(1x {at_period:.3}, 2x {at_twice:.3}) versus single-slice control \
{control_exposed:.3}"
);
let frame_len = (0.02 * sr) as usize;
let levels: Vec<f32> = hold
.chunks_exact(frame_len)
.map(crate::testutil::rms)
.collect();
let mean = levels.iter().sum::<f32>() / levels.len() as f32;
let variance = levels
.iter()
.map(|&level| (level - mean).powi(2))
.sum::<f32>()
/ levels.len() as f32;
let level_cov = variance.sqrt() / mean.max(1e-9);
assert!(
level_cov < 0.12,
"GM67 key {key} velocity {vel}: multi-slice hold pulses in level \
(20ms frame-RMS CoV {level_cov:.3})"
);
}
}
#[test]
fn bottle_loop_level_parity_and_flat() {
let sr = 44100.0;
for key in [55u8, 60] {
assert_eq!(
voices::make(76, key, 100, sr, 5, true).kind(),
"bottleloop",
"key {key}: samples-on must engage the looped voice, not the modeled fallback"
);
let hold = |samples: bool| {
let mut v = voices::make(76, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (1.5 * sr) as usize];
v.render(&mut buf);
buf[(0.6 * sr) as usize..(1.4 * sr) as usize].to_vec()
};
let (loop_h, model_h) = (hold(true), hold(false));
let (rl, rm) = (
crate::testutil::rms(&loop_h),
crate::testutil::rms(&model_h),
);
let db = 20.0 * (rl / rm).log10();
assert!(
(-2.0..=4.0).contains(&db),
"key {key}: looped hold {db:+.1} dB vs the modeled bottle — out of the raw \
offset band (loop {rl:.4} vs model {rm:.4}). See BOTTLE_LOOP_GAIN."
);
let fl = (0.02 * sr) as usize;
let frames: Vec<f32> = loop_h
.chunks(fl)
.filter(|c| c.len() == fl)
.map(crate::testutil::rms)
.collect();
let mean = frames.iter().sum::<f32>() / frames.len() as f32;
let var = frames.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / frames.len() as f32;
let cov = var.sqrt() / (mean + 1e-9);
assert!(
(0.02..0.08).contains(&cov),
"key {key}: hold liveness (frame-RMS CoV {cov:.3}) out of band — \
static (dead loop) below 0.02, or a decay-sawtooth pulse above 0.08"
);
}
let zone = nearest(bottle_loop_bank(), crate::dsp::key_freq(60));
let (ls, le) = zone
.sustain_loop(find_bottle_loop)
.expect("a loop was found above");
let wrap = (zone.data[le] - zone.data[ls]).abs();
assert!(
wrap < 0.03,
"loop wrap discontinuity {wrap:.4} (loop {ls}..{le}) — would click"
);
}
#[test]
fn loop_voice_clocks_cross_u32_boundary_without_modulation_reset() {
let sr = 44100.0;
let near_start = u32::MAX as u64 - 511;
let period = SAX_DRIFT_SAMP as u64;
let settled_with_same_phase = |near: u64| {
let settled = (2.0 * sr) as u64;
settled + (near + period - settled % period) % period
};
let bottle_zone = nearest(bottle_loop_bank(), crate::dsp::key_freq(60));
let make_bottle = || {
BottleLoopVoice::new(
bottle_zone,
crate::dsp::key_freq(60),
100,
sr,
BOTTLE_LOOP_GAIN,
5,
)
.expect("representative bottle key must engage its loop")
};
let (mut bottle_near, mut bottle_reference) = (make_bottle(), make_bottle());
bottle_near.t = near_start;
bottle_reference.t = settled_with_same_phase(near_start);
let (mut bottle_actual, mut bottle_expected) = (vec![0.0; 1024], vec![0.0; 1024]);
bottle_near.render(&mut bottle_actual);
bottle_reference.render(&mut bottle_expected);
assert!(
bottle_near.t > u32::MAX as u64,
"bottle clock did not cross the old u32 boundary"
);
assert_eq!(
bottle_actual, bottle_expected,
"bottle modulation reset at the old u32 boundary"
);
let sax_zone = nearest(sax_bank(66, 100), crate::dsp::key_freq(60));
let make_sax = || {
SaxLoopVoice::new(
sax_zone,
crate::dsp::key_freq(60),
100,
sr,
sax_program_gain(66),
5,
false,
)
.expect("representative sax key must engage its loop")
};
let (mut sax_near, mut sax_reference) = (make_sax(), make_sax());
sax_near.t = near_start;
sax_reference.t = settled_with_same_phase(near_start);
let (mut sax_actual, mut sax_expected) = (vec![0.0; 1024], vec![0.0; 1024]);
sax_near.render(&mut sax_actual);
sax_reference.render(&mut sax_expected);
assert!(
sax_near.t > u32::MAX as u64,
"sax clock did not cross the old u32 boundary"
);
assert_eq!(
sax_actual, sax_expected,
"sax modulation reset at the old u32 boundary"
);
}
#[test]
fn bottle_loop_pitch_integrity() {
let sr = 44100.0;
for key in [48u8, 55, 60, 67] {
let f0 = crate::dsp::key_freq(key);
let mut v = voices::make(76, key, 100, sr, 5, true);
assert_eq!(
v.kind(),
"bottleloop",
"key {key}: sample must engage (else the pitch check is meaningless)"
);
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
let hz = crate::testutil::peak_locate(&buf[6615..24255], sr, f0 * 0.8, f0 * 1.25);
let cents = 1200.0 * (hz / f0).log2();
assert!(
cents.abs() < 45.0,
"bottle key {key}: located pitch {hz:.2} Hz vs nominal {f0:.2} Hz ({cents:.0} cents)"
);
}
}
#[test]
fn la_guitar_pitch_integrity() {
let sr = 44100.0;
for key in [40u8, 45, 52, 57, 64, 68, 79] {
let f0 = crate::dsp::key_freq(key);
let mut v = voices::make(24, key, 100, sr, 5, true);
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
let hz = crate::testutil::peak_locate(&buf[4410..17640], sr, f0 * 0.8, f0 * 1.25);
let cents = 1200.0 * (hz / f0).log2();
assert!(
cents.abs() < 45.0,
"nylon key {key}: layered pitch {hz:.2} Hz vs nominal {f0:.2} Hz ({cents:.0} cents)"
);
}
}
#[test]
fn la_guitar_attack_sharpness() {
let sr = 44100.0;
for key in [45u8, 52, 64] {
let early = |samples: bool| {
let mut v = voices::make(24, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (0.05 * sr) as usize];
v.render(&mut buf);
buf
};
let (on, off) = (early(true), early(false));
let diff: Vec<f32> = on.iter().zip(&off).map(|(a, b)| a - b).collect();
let (d, o) = (crate::testutil::rms(&diff), crate::testutil::rms(&off));
assert!(
d > 0.3 * o,
"nylon key {key}: onset barely changes with the layer (diff {d:.5} vs off {o:.5})"
);
let hf_frac = |buf: &[f32]| {
crate::testutil::hp_rms(buf, sr, 1500.0) / crate::testutil::rms(buf).max(1e-9)
};
let (r_on, r_off) = (hf_frac(&on), hf_frac(&off));
assert!(
r_on < r_off * 1.05,
"nylon key {key}: sampled attack hissier than the model: hf-frac on {r_on:.4} vs off {r_off:.4}"
);
}
}
#[test]
fn guitar_onset_variation_presence_and_determinism() {
let sr = 44100.0;
for program in [24u8, 25] {
let render = |seed: u32| {
let mut v = voices::make(program, 52, 100, sr, seed, true);
let mut buf = vec![0f32; 2205]; v.render(&mut buf);
buf
};
let (a, a2, b) = (render(5), render(5), render(21));
assert!(
a.iter().zip(&a2).all(|(x, y)| x.to_bits() == y.to_bits()),
"prog {program}: same seed must render bit-identically"
);
assert!(
a.iter().zip(&b).any(|(x, y)| x.to_bits() != y.to_bits()),
"prog {program}: different seeds replay identical onset PCM"
);
let dot = |x: &[f32], y: &[f32]| x.iter().zip(y).map(|(p, q)| p * q).sum::<f32>();
let ncc = dot(&a, &b) / (dot(&a, &a).sqrt() * dot(&b, &b).sqrt()).max(1e-12);
assert!(
ncc < 0.99,
"prog {program}: seeds {ncc:.4}-correlated — variation not reaching the onset"
);
}
}
#[test]
fn guitar_sample_layer_tracks_velocity_brightness() {
for vel in 100..=127 {
assert_eq!(super::vel_lp_alpha(5000.0, vel, 44100.0), 0.0);
}
assert!(super::vel_lp_alpha(5000.0, 99, 44100.0) > 0.0);
assert!(
super::vel_lp_alpha(5000.0, 32, 44100.0) < super::vel_lp_alpha(5000.0, 72, 44100.0),
"corner must open with velocity"
);
let sr = 44100.0;
for program in [24u8, 25] {
let hf_frac = |vel: u8| {
let mut v = voices::make(program, 52, vel, sr, 5, true);
let mut buf = vec![0f32; 2205];
v.render(&mut buf);
crate::testutil::hp_rms(&buf, sr, 1500.0) / crate::testutil::rms(&buf).max(1e-9)
};
let (soft, hard) = (hf_frac(32), hf_frac(110));
assert!(
soft < hard * 0.85,
"prog {program}: soft pick not darker (hf {soft:.4} vs {hard:.4})"
);
}
}
#[test]
fn guitar_low_velocity_seam_continuity() {
let sr = 44100.0;
for (program, key, vels) in [
(24u8, 52u8, &[40u8, 72][..]),
(24, 64, &[40, 72]),
(24, 76, &[56, 72, 86]),
(25, 52, &[40, 72]),
(25, 64, &[40, 72]),
] {
for &vel in vels {
let label = format!("gtr-lowvel prog {program} key {key} vel {vel}");
let fine = assert_wrap_seam(program, key, vel, sr, 0, &label);
assert_attack_is_peak(&fine, &label);
}
}
}
#[test]
fn guitar_zone_fade_budget() {
let (_, fade) = crate::voices::LA_GUITAR;
let need = fade.1 * 44100.0 * 2.05;
for (bank, name) in [
(guitar_bank(), "nylon"),
(steel_bank(), "steel"),
(eastman_picked_bank(), "eastpick"),
(eastman_plucked_bank(), "eastpluck"),
] {
for z in bank {
assert!(
(z.data.len() as f32) >= need,
"{name} zone (root {:.1} Hz): {} samples < fade budget {:.0}",
z.root,
z.data.len(),
need
);
}
}
}
#[test]
fn guitar_zone_dry_out_tapers_at_high_rate() {
assert_wrap_seam(25, 100, 100, 96000.0, 1, "steel key 100 @96k dry-out");
}
#[test]
fn non_guitar_la_render_signature_is_stable() {
let mut v = voices::make(56, 69, 100, 44100.0, 5, true);
let mut buf = vec![0f32; 22050];
v.render(&mut buf);
crate::testutil::assert_render_signature(
"GM56 non-guitar LA canary",
crate::testutil::render_signature(
&buf,
44100.0,
(0.0, 0.50),
(0.0, 0.10),
(0.35, 0.50),
),
crate::testutil::RenderSignature {
rms_db: -16.398,
centroid_hz: 1767.767,
late_early_db: 13.049,
},
);
}
#[test]
fn la_strings_pitch_integrity() {
let sr = 44100.0;
for (program, key, name) in [
(48u8, 48u8, "ensemble-low-cello"),
(48, 76, "ensemble-high-violin"),
(49, 55, "slow-strings-low"),
(49, 67, "slow-strings-mid"),
(50, 60, "synth-strings-1"),
(51, 64, "synth-strings-2"),
] {
let f0 = crate::dsp::key_freq(key);
let mut v = voices::make(program, key, 100, sr, 5, true);
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
let hz = crate::testutil::peak_locate(&buf[6615..24255], sr, f0 * 0.8, f0 * 1.25);
let cents = 1200.0 * (hz / f0).log2();
assert!(
cents.abs() < 45.0,
"{name}: layered pitch {hz:.2} Hz vs nominal {f0:.2} Hz ({cents:.0} cents)"
);
}
}
#[test]
fn synth_strings_50_51_skip_sample_layer() {
let sr = 44100.0;
let bits = |b: &[f32]| b.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
for prog in [50u8, 51] {
let render = |samples: bool| {
let mut v = voices::make(prog, 60, 100, sr, 6, samples);
let mut buf = vec![0f32; 22050];
v.render(&mut buf);
buf
};
assert_eq!(
bits(&render(true)),
bits(&render(false)),
"synth strings {prog} not sample-independent"
);
}
}
#[test]
fn brass_section_61_uses_its_own_sample_layer() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
let bank = brass_section_bank();
assert_eq!(bank.len(), 10);
assert_eq!(
bank.iter().map(|zone| zone.root).collect::<Vec<_>>(),
[87.28, 130.93, 185.54, 234.59, 262.50, 350.88, 441.64, 524.46, 700.31, 1049.46]
);
let render = |samples: bool| {
let mut v = voices::make(61, 69, 100, sr, 6, samples);
let mut buf = vec![0f32; (0.05 * sr) as usize];
v.render(&mut buf);
buf
};
let (on, off) = (render(true), render(false));
let diff: Vec<f32> = on.iter().zip(&off).map(|(a, b)| a - b).collect();
assert!(
crate::testutil::rms(&diff) > 0.3 * crate::testutil::rms(&off),
"brass-section ensemble onset is not materially audible"
);
}
#[test]
fn brass_section_recording_carries_the_high_early_body() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for key in [72u8, 76, 79] {
let render = |samples: bool| {
let mut voice = voices::make(61, key, 96, sr, 6, samples);
let mut buf = vec![0.0; (0.70 * sr) as usize];
voice.render(&mut buf);
buf
};
let (sampled, modeled) = (render(true), render(false));
let a = (0.45 * sr) as usize;
let b = (0.65 * sr) as usize;
let sampled = &sampled[a..b];
let modeled = &modeled[a..b];
let model_power = modeled.iter().map(|x| x * x).sum::<f32>().max(1e-12);
let align = sampled.iter().zip(modeled).map(|(x, y)| x * y).sum::<f32>() / model_power;
let residual: Vec<f32> = sampled
.iter()
.zip(modeled)
.map(|(x, y)| x - align * y)
.collect();
let ratio = crate::testutil::rms(&residual) / crate::testutil::rms(sampled).max(1e-9);
assert!(
ratio >= 0.20,
"GM61 key {key}: recorded high body has already vanished by 0.45 s \
(gain-aligned residual {ratio:.3})"
);
}
}
#[test]
fn brass_section_source_covers_the_full_handover_window() {
if !crate::embedded_samples_available() {
return;
}
let bank = brass_section_bank();
let fade_frames = 0.80 * 44100.0;
let baked_fade_frames = 0.20 * 44100.0;
for key in 0u8..=127 {
let target = crate::dsp::key_freq(key);
let zone = nearest(bank, target);
let ratio = target / zone.root;
if (0.5..=2.05).contains(&ratio) {
assert!(
zone.data.len() as f32 - baked_fade_frames > fade_frames * ratio + 2.0,
"key {key}, root {:.2}: {} unfaded source frames do not cover {:.0}",
zone.root,
zone.data.len() as f32 - baked_fade_frames,
fade_frames * ratio
);
}
}
}
#[test]
fn early_la_legato_has_no_transient_gap() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
let mut voice = voices::make(61, 69, 100, sr, 6, true);
let mut before = vec![0.0; (0.02 * sr) as usize];
voice.render(&mut before);
assert!(voice.legato_to(71, 100));
let mut after = vec![0.0; (0.02 * sr) as usize];
voice.render(&mut after);
assert!(voice.legato_to(72, 100));
let mut tail = vec![0.0; (0.08 * sr) as usize];
voice.render(&mut tail);
after.extend(tail);
let window = (0.01 * sr) as usize;
let before_rms = crate::testutil::rms(&before[before.len() - window..]);
let rms: Vec<f32> = after
.chunks_exact(window)
.map(crate::testutil::rms)
.collect();
let floor = rms.iter().copied().fold(f32::INFINITY, f32::min);
assert!(
floor > before_rms * 0.05,
"early legato dropped into a transient gap: before {before_rms:.5}, floor {floor:.5}"
);
for pair in rms.windows(2) {
let ratio = pair[0].max(pair[1]) / pair[0].min(pair[1]).max(1e-9);
assert!(
ratio < 2.4,
"early legato introduced a 10 ms level step of {ratio:.2}x"
);
}
let f0 = crate::dsp::key_freq(72);
let hz = crate::testutil::peak_locate(
&after[after.len() - (0.04 * sr) as usize..],
sr,
f0 * 0.8,
f0 * 1.25,
);
let cents = 1200.0 * (hz / f0).log2();
assert!(
cents.abs() < 45.0,
"old-pitch transient survived the slur handover: {hz:.2} Hz ({cents:.0} cents)"
);
}
#[test]
fn brass_section_staccato_is_smooth_at_handover_boundaries() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
let window = (0.005 * sr) as usize;
for off_s in [0.44f32, 0.46, 0.79, 0.81] {
let mut voice = voices::make(61, 60, 100, sr, 6, true);
let mut before = vec![0.0; (off_s * sr) as usize];
voice.render(&mut before);
voice.note_off();
let mut after = vec![0.0; (0.06 * sr) as usize];
voice.render(&mut after);
let boundary = before.len();
before.extend(after);
let start = boundary.saturating_sub(2 * window);
let end = (boundary + 4 * window).min(before.len());
let levels: Vec<f32> = before[start..end]
.chunks_exact(window)
.map(crate::testutil::rms)
.collect();
for pair in levels.windows(2) {
let ratio = pair[0].max(pair[1]) / pair[0].min(pair[1]).max(1e-9);
assert!(
ratio < 3.0,
"note-off at {off_s:.2}s introduced a 5 ms level step of {ratio:.2}x"
);
}
}
}
#[test]
fn la_wrap_forwards_breath() {
let sr = 44100.0;
let bits = |b: &[f32]| b.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
let render = |prog: u8, breath: Option<(f32, f32)>| {
let mut v = voices::make(prog, 69, 100, sr, 5, true);
if let Some((p, g)) = breath {
v.set_breath(p, g);
}
let mut buf = vec![0f32; 44100];
v.render(&mut buf);
buf
};
assert_eq!(
bits(&render(56, Some((1.0, 0.0)))),
bits(&render(56, None)),
"neutral set_breath(1.0, 0) must be a bit-exact no-op"
);
for prog in [56u8, 61, 68, 73] {
let (plain, blown) = (render(prog, None), render(prog, Some((0.4, 0.0))));
assert_ne!(
bits(&plain),
bits(&blown),
"prog {prog}: authored breath does not reach the wrapped model"
);
}
}
#[test]
fn la_strings_attack_audibility() {
let sr = 44100.0;
for (program, key, name) in [
(48u8, 48u8, "ensemble-low"),
(48, 60, "ensemble-mid"),
(48, 76, "ensemble-high"),
(49, 55, "slow-low"),
(49, 67, "slow-mid"),
] {
let early = |samples: bool| {
let mut v = voices::make(program, key, 100, sr, 5, samples);
let mut buf = vec![0f32; (0.05 * sr) as usize];
v.render(&mut buf);
buf
};
let (on, off) = (early(true), early(false));
let diff: Vec<f32> = on.iter().zip(&off).map(|(a, b)| a - b).collect();
let (d, o) = (crate::testutil::rms(&diff), crate::testutil::rms(&off));
assert!(
d > 0.3 * o,
"{name}: onset barely changes with the layer (diff {d:.5} vs off {o:.5})"
);
let hf_frac = |buf: &[f32]| {
crate::testutil::hp_rms(buf, sr, 1500.0) / crate::testutil::rms(buf).max(1e-9)
};
let (r_on, r_off) = (hf_frac(&on), hf_frac(&off));
assert!(
r_on < r_off * 1.6 && r_on > r_off * 0.5,
"{name}: attack hf-frac out of the measured band: on {r_on:.4} vs off {r_off:.4}"
);
}
}
#[test]
fn la_level_continuity() {
let sr = 44100.0;
for (program, key, name, struck) in [
(40u8, 69u8, "fiddle", false),
(110u8, 69, "fiddle-110", false),
(73u8, 69, "flute", false),
(0u8, 69, "piano", true),
(0u8, 48, "piano-low", true),
(56u8, 69, "trumpet", false),
(57u8, 55, "trombone", false),
(58u8, 40, "tuba", false),
(59u8, 69, "muted-trumpet", false),
(60u8, 62, "french-horn", false),
(61u8, 41, "brass-section-low", false),
(61u8, 60, "brass-section-mid", false),
(61u8, 84, "brass-section-high", false),
(68u8, 76, "oboe", false),
(69u8, 64, "english-horn", false),
(70u8, 48, "bassoon", false),
(71u8, 60, "clarinet", false),
(24u8, 45, "nylon-guitar-low", true),
(24u8, 52, "nylon-guitar", true),
(24u8, 64, "nylon-guitar-high", true),
(25u8, 45, "steel-guitar-low", true),
(25u8, 52, "steel-guitar", true),
(25u8, 64, "steel-guitar-high", true),
(24u8, 76, "nylon-guitar-top", true),
(6u8, 48, "harpsichord-low", true),
(6u8, 60, "harpsichord", true),
(6u8, 72, "harpsichord-high", true),
(15u8, 64, "dulcimer-low", true),
(15u8, 69, "dulcimer", true),
(15u8, 72, "dulcimer-high", true),
(48u8, 48, "string-ens-low", false),
(48u8, 76, "string-ens-high", false),
(49u8, 55, "slow-strings", false),
(42u8, 48, "cello-low", false),
(42u8, 69, "cello-high", false),
(43u8, 40, "contrabass", false),
] {
let fine = assert_wrap_seam(program, key, 100, sr, 0, name);
if struck {
assert_attack_is_peak(&fine, name);
}
}
}
#[test]
fn la_steel_high_key_level_parity() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for key in [76u8, 79, 83] {
for vel in [60u8, 100] {
let early = |samples: bool| {
let mut v = voices::make(25, key, vel, sr, 5, samples);
let mut buf = vec![0f32; (0.4 * sr) as usize];
v.render(&mut buf);
let (a, b) = ((0.05 * sr) as usize, (0.30 * sr) as usize);
(buf[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a) as f32).sqrt()
};
let ratio = early(true) / early(false).max(1e-12);
assert!(
(0.8..2.2).contains(&ratio),
"steel key {key} vel {vel}: wrapped/model early-RMS ratio {ratio:.2} outside 0.8–2.2"
);
}
}
}
#[test]
#[ignore = "calibration harness — run by hand"]
fn print_steel_wrap_level_ratios() {
let sr = 44100.0;
for key in [60u8, 64, 68, 72, 74, 76, 79, 83, 86, 90] {
for vel in [60u8, 100] {
let early = |samples: bool| {
let mut v = voices::make(25, key, vel, sr, 5, samples);
let mut buf = vec![0f32; (0.4 * sr) as usize];
v.render(&mut buf);
let (a, b) = ((0.05 * sr) as usize, (0.30 * sr) as usize);
(buf[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a) as f32).sqrt()
};
println!(
"steel key {key:3} vel {vel:3}: wrapped/model {:.2}",
early(true) / early(false).max(1e-12)
);
}
}
}
#[test]
#[ignore = "calibration harness — run by hand"]
fn print_ebass_wrap_level_ratios() {
let sr = 44100.0;
for program in [32u8, 33, 34, 35] {
for key in [
16u8, 20, 24, 28, 31, 34, 38, 40, 43, 46, 50, 52, 55, 58, 62, 64,
] {
for vel in [48u8, 72, 100, 120] {
let render = |seed: u32, samples: bool| {
let mut v = voices::bass_la_alt(program, key, vel, sr, seed, samples);
let mut buf = vec![0f32; (0.5 * sr) as usize];
v.render(&mut buf);
buf
};
let rms_win = |buf: &[f32], t0: f32, t1: f32| {
let (a, z) = ((t0 * sr) as usize, (t1 * sr) as usize);
(buf[a..z].iter().map(|&x| x * x).sum::<f32>() / (z - a) as f32).sqrt()
};
let seeds = [5u32, 21, 99];
let geo_combined = |t0: f32, t1: f32| {
let r = |s: u32| {
let (on, off) = (render(s, true), render(s, false));
rms_win(&on, t0, t1) / rms_win(&off, t0, t1).max(1e-12)
};
(seeds.iter().map(|&s| r(s).ln()).sum::<f32>() / seeds.len() as f32).exp()
};
let geo_sample = |t0: f32, t1: f32| {
let r = |s: u32| {
let (on, off) = (render(s, true), render(s, false));
let diff = diff_signal(&on, &off);
rms_win(&diff, t0, t1) / rms_win(&off, t0, t1).max(1e-12)
};
(seeds.iter().map(|&s| r(s).ln()).sum::<f32>() / seeds.len() as f32).exp()
};
let (c0, c1, all, s0, s1, tail) = (
geo_combined(0.0, 0.05),
geo_combined(0.05, 0.15),
geo_combined(0.0, 0.15),
geo_sample(0.0, 0.05),
geo_sample(0.05, 0.15),
geo_sample(0.151, 0.25),
);
let pk = |samples: bool| {
let mut v = voices::bass_la_alt(program, key, vel, sr, 5, samples);
let mut buf = vec![0f32; (0.5 * sr) as usize];
v.render(&mut buf);
buf[..(0.15 * sr) as usize]
.iter()
.fold(0f32, |m, x| m.max(x.abs()))
};
let (pk_on, pk_off) = (pk(true), pk(false));
let db = |g: f32| 20.0 * g.max(1e-12).log10();
println!(
"ebass gm{program} key {key:3} vel {vel:3}: \
combined[0,50ms] {c0:5.3} ({:+6.1} dB) | \
combined[50,150] {c1:5.3} ({:+6.1}) | all[0,150] {all:5.3} ({:+6.1}) | \
sample[0,50] {s0:5.3} sample[50,150] {s1:5.3} tail[151,250] {tail:5.3} | \
peak {pk_on:.3}/{pk_off:.3} = {:.2}x",
db(c0),
db(c1),
db(all),
pk_on / pk_off.max(1e-12)
);
}
}
}
}
fn render_bass_alt(
program: u8,
key: u8,
vel: u8,
seed: u32,
samples: bool,
secs: f32,
) -> Vec<f32> {
let sr = 44100.0;
let mut v = voices::bass_la_alt(program, key, vel, sr, seed, samples);
let mut buf = vec![0f32; (secs * sr) as usize];
v.render(&mut buf);
buf
}
fn win(buf: &[f32], sr: f32, t0: f32, t1: f32) -> &[f32] {
&buf[(t0 * sr) as usize..(t1 * sr) as usize]
}
fn rms_win(buf: &[f32], sr: f32, t0: f32, t1: f32) -> f32 {
crate::testutil::rms(win(buf, sr, t0, t1))
}
fn diff_signal(a: &[f32], b: &[f32]) -> Vec<f32> {
a.iter().zip(b).map(|(&x, &y)| x - y).collect()
}
#[test]
fn la_bass_alt_preserves_model_onset_and_tapers_sample_by_150ms() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for (program, key, name) in [(32u8, 43u8, "pizz"), (33, 40, "finger"), (34, 40, "pick")] {
let bare = render_bass_alt(program, key, 100, 5, false, 0.25);
let layered = render_bass_alt(program, key, 100, 5, true, 0.25);
let sample = diff_signal(&layered, &bare);
let low_bare =
crate::testutil::spectral_band_rms(win(&bare, sr, 0.0, 0.05), sr, 35.0, 120.0);
let low_layered =
crate::testutil::spectral_band_rms(win(&layered, sr, 0.0, 0.05), sr, 35.0, 120.0);
let low_db = 20.0 * (low_layered / low_bare.max(1e-12)).log10();
assert!(
low_db.abs() <= 1.0,
"GM{program} {name} key {key}: additive onset moved the modeled low kick by {low_db:+.2} dB"
);
let sample_onset = rms_win(&sample, sr, 0.0, 0.05);
let bare_onset = rms_win(&bare, sr, 0.0, 0.05);
assert!(
sample_onset >= 0.03 * bare_onset.max(1e-12),
"GM{program} {name} key {key}: sampled onset contribution vanished ({sample_onset:.6})"
);
let combined =
rms_win(&layered, sr, 0.0, 0.15) / rms_win(&bare, sr, 0.0, 0.15).max(1e-12);
assert!(
(0.80..=1.45).contains(&combined),
"GM{program} {name} key {key}: additive onset is not level-calibrated ({combined:.3}x model)"
);
let near_end = rms_win(&sample, sr, 0.145, 0.150);
assert!(
near_end <= 0.08 * sample_onset.max(1e-12),
"GM{program} {name} key {key}: sample taper still has {near_end:.6} RMS just before 150 ms"
);
let after_end = rms_win(&sample, sr, 0.151, 0.200);
assert!(
after_end <= 1e-7,
"GM{program} {name} key {key}: sampled layer still contributes after 150 ms ({after_end:.8})"
);
assert_eq!(
&layered[(0.151 * sr) as usize..(0.200 * sr) as usize],
&bare[(0.151 * sr) as usize..(0.200 * sr) as usize],
"GM{program} {name} key {key}: model is not bit-identical after the additive handover"
);
}
}
#[test]
fn la_bass_alt_limits_upward_repitch_to_five_semitones() {
if !crate::embedded_samples_available() {
return;
}
for (program, below_or_at, above, name) in [
(32u8, 60u8, 61u8, "pizz"),
(33, 42, 43, "finger"),
(34, 44, 45, "pick"),
(35, 42, 43, "fretless"),
] {
let engaged = render_bass_alt(program, below_or_at, 100, 5, true, 0.2);
let engaged_bare = render_bass_alt(program, below_or_at, 100, 5, false, 0.2);
assert!(
engaged != engaged_bare,
"GM{program} {name} key {below_or_at}: sample should engage at or below the five-semitone ceiling"
);
let fallback = render_bass_alt(program, above, 100, 5, true, 0.2);
let bare = render_bass_alt(program, above, 100, 5, false, 0.2);
assert!(
fallback == bare,
"GM{program} {name} key {above}: sample should fall back immediately above the five-semitone ceiling"
);
}
}
#[test]
fn la_bass_alt_note_off_releases_and_reaps() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for (program, key) in [(32u8, 43u8), (33, 40), (34, 40), (35, 40)] {
let mut voice = voices::bass_la_alt(program, key, 100, sr, 0x8500 + key as u32, true);
let mut onset = vec![0.0; (0.10 * sr) as usize];
assert!(
voice.render(&mut onset),
"GM{program} additive bass died during its onset"
);
voice.note_off();
assert!(
voice.released(),
"GM{program} additive bass did not forward note-off to its model"
);
assert!(
dies_within(voice, sr, 2.0),
"GM{program} additive bass stayed alive two seconds after note-off"
);
}
}
struct ConstantVoice(f32);
impl Voice for ConstantVoice {
fn render(&mut self, out: &mut [f32]) -> bool {
for x in out {
*x += self.0;
}
true
}
fn note_off(&mut self) {}
fn released(&self) -> bool {
false
}
fn kind(&self) -> &'static str {
"constant"
}
}
fn constant_zone() -> &'static [Zone] {
Box::leak(
vec![Zone {
root: key_freq(60),
data: vec![1.0; 44100],
#[cfg(feature = "embedded-samples")]
b1_tail: None,
sustain_loop: OnceLock::new(),
}]
.into_boxed_slice(),
)
}
#[test]
fn ordinary_la_wrap_keeps_sum_to_one_onset_ownership() {
let sr = 44100.0;
let mut v = LaVoice::wrap(
Box::new(ConstantVoice(2.0)),
constant_zone(),
60,
127,
sr,
1.0,
(0.05, 0.20),
);
let mut buf = vec![0f32; (0.25 * sr) as usize];
v.render(&mut buf);
assert!(
(buf[(0.010 * sr) as usize] - 1.0).abs() <= 1e-6,
"default LaVoice onset must be sample-owned, not additive"
);
assert!(
(buf[(0.225 * sr) as usize] - 2.0).abs() <= 1e-6,
"default LaVoice sustain must return to the model"
);
}
#[test]
fn la_ebass_additive_level_parity() {
let sr = 44100.0;
let seeds = [5u32, 21, 99];
for program in [32u8, 33, 34, 35] {
for key in [20u8, 24, 28, 34, 40, 46, 50] {
for vel in [48u8, 100, 120] {
let render = |seed: u32, samples: bool| {
let mut v = voices::bass_la_alt(program, key, vel, sr, seed, samples);
let mut buf = vec![0f32; (0.5 * sr) as usize];
v.render(&mut buf);
buf
};
let rms_win = |b: &[f32], t0: f32, t1: f32| {
let (a, z) = ((t0 * sr) as usize, (t1 * sr) as usize);
(b[a..z].iter().map(|&x| x * x).sum::<f32>() / (z - a) as f32).sqrt()
};
let engaged = render(5, true) != render(5, false);
if !engaged {
continue;
}
let ratio = |t0: f32, t1: f32| {
let r = |s: u32| {
rms_win(&render(s, true), t0, t1)
/ rms_win(&render(s, false), t0, t1).max(1e-12)
};
(seeds.iter().map(|&s| r(s).ln()).sum::<f32>() / seeds.len() as f32).exp()
};
let sample_ratio = |t0: f32, t1: f32| {
let r = |s: u32| {
let (on, off) = (render(s, true), render(s, false));
let sample = diff_signal(&on, &off);
rms_win(&sample, t0, t1) / rms_win(&off, t0, t1).max(1e-12)
};
(seeds.iter().map(|&s| r(s).ln()).sum::<f32>() / seeds.len() as f32).exp()
};
let sample_abs_max = |t0: f32, t1: f32| {
seeds.iter().fold(0.0f32, |m, &s| {
let (on, off) = (render(s, true), render(s, false));
let sample = diff_signal(&on, &off);
m.max(rms_win(&sample, t0, t1))
})
};
let onset = ratio(0.0, 0.05);
let decay = ratio(0.05, 0.15);
let all = ratio(0.0, 0.15);
let sample_onset = sample_ratio(0.0, 0.05);
let sample_tail = sample_abs_max(0.151, 0.25);
let peak = |samples: bool| {
render(5, samples)[..(0.15 * sr) as usize]
.iter()
.fold(0f32, |m, x| m.max(x.abs()))
};
let pk = peak(true) / peak(false).max(1e-12);
assert!(
(0.80..=1.45).contains(&onset),
"GM{program} key {key} vel {vel}: additive [0, 50 ms] level \
sits at {onset:.3}x the model ({:+.1} dB)",
20.0 * onset.max(1e-12).log10()
);
assert!(
(0.80..=1.45).contains(&decay) && (0.80..=1.45).contains(&all),
"GM{program} key {key} vel {vel}: additive window level is not \
calibrated ([50,150] {decay:.3}x, all[0,150] {all:.3}x)"
);
assert!(
(0.03..=0.35).contains(&sample_onset),
"GM{program} key {key} vel {vel}: sampled onset contribution \
{sample_onset:.3}x model is outside the additive accent band"
);
assert!(
sample_tail <= 1e-7,
"GM{program} key {key} vel {vel}: sampled layer still contributes \
after 150 ms ({sample_tail:.8})"
);
assert!(
pk <= 1.60,
"GM{program} key {key} vel {vel}: the wrapped peak is {pk:.2}x the \
model's. An RMS match is not a peak match — a taper must not buy \
level parity by shipping a transient spike."
);
}
}
}
}
#[test]
#[ignore = "calibration harness — run by hand"]
fn print_strings_wrap_level_ratios() {
let sr = 44100.0;
for program in [48u8, 49] {
for key in [
28u8, 32, 36, 40, 44, 48, 52, 55, 58, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96,
] {
for vel in [72u8, 110] {
let early = |seed: u32, samples: bool| {
let mut v = voices::make(program, key, vel, sr, seed, samples);
let mut buf = vec![0f32; (0.5 * sr) as usize];
v.render(&mut buf);
let (a, b) = ((0.10 * sr) as usize, (0.40 * sr) as usize);
(buf[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a) as f32).sqrt()
};
let r = |s: u32| early(s, true) / early(s, false).max(1e-12);
let seeds = [5u32, 21, 99];
let g =
(seeds.iter().map(|&s| r(s).ln()).sum::<f32>() / seeds.len() as f32).exp();
println!(
"strings gm{program} key {key:3} vel {vel:3}: geomean {g:5.2} ({:+5.1} dB) [s5 {:.2} s21 {:.2} s99 {:.2}]",
20.0 * g.max(1e-12).log10(),
r(5),
r(21),
r(99)
);
}
}
}
}
#[test]
fn la_strings_seam_level_parity() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
let seeds = [5u32, 21, 99];
let geo_ratio = |program: u8, key: u8, vel: u8| -> f32 {
let early = |seed: u32, samples: bool| {
let mut v = voices::make(program, key, vel, sr, seed, samples);
let mut buf = vec![0f32; (0.5 * sr) as usize];
v.render(&mut buf);
let (a, b) = ((0.10 * sr) as usize, (0.40 * sr) as usize);
(buf[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a) as f32).sqrt()
};
(seeds
.iter()
.map(|&s| (early(s, true) / early(s, false).max(1e-12)).ln())
.sum::<f32>()
/ seeds.len() as f32)
.exp()
};
for program in [48u8, 49] {
for key in [33u8, 48, 58, 68, 90] {
for vel in [40u8, 72, 110, 120] {
let g = geo_ratio(program, key, vel);
assert!(
(0.75..=1.30).contains(&g),
"strings gm{program} key {key} vel {vel}: 3-seed-geomean wrapped/model seam ratio {g:.2} outside 0.75-1.30"
);
}
}
}
}
#[test]
fn la_strings_onset_preserves_model_swell() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
let seeds = [5u32, 21, 99];
let geo_rise = |key: u8, vel: u8, samples: bool| -> f32 {
let one = |seed: u32| {
let mut v = voices::make(49, key, vel, sr, seed, samples);
let mut buf = vec![0f32; (1.3 * sr) as usize];
v.render(&mut buf);
let m = |t0: f32, t1: f32| {
let (a, b) = ((t0 * sr) as usize, (t1 * sr) as usize);
(buf[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a) as f32).sqrt()
};
m(0.8, 1.2) / m(0.0, 0.4).max(1e-12)
};
(seeds.iter().map(|&s| one(s).ln()).sum::<f32>() / seeds.len() as f32).exp()
};
let mut guarded = 0usize;
let mut worst: Option<(u8, u8, f32)> = None;
for key in [48u8, 55, 58, 64, 68, 72] {
for vel in [72u8, 110] {
let (w, m) = (geo_rise(key, vel, true), geo_rise(key, vel, false));
let wm = w / m;
println!("gm49 swell key {key} vel {vel}: body/onset wrapped {w:.2} model-only {m:.2} w/m {wm:.2}");
if m > 1.05 {
guarded += 1;
if worst.map(|(.., x)| wm < x).unwrap_or(true) {
worst = Some((key, vel, wm));
}
}
}
}
assert!(
guarded >= 6,
"non-vacuous guard broken: only {guarded} of 12 GM49 cases have a swelling model (m > 1.05) — the model or probe changed; re-derive before trusting this oracle"
);
let (key, vel, wm) = worst.expect("guarded >= 6 implies a worst case");
assert!(
wm >= 0.70,
"gm49 key {key} vel {vel}: sampled onset degrades the model's swell — wrapped/model-only body/onset ratio {wm:.2} < 0.70"
);
}
#[test]
fn la_strings_slow_swell_not_inverted() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
let seeds = [5u32, 21, 99];
let geo_rise = |key: u8, vel: u8, samples: bool| -> f32 {
let one = |seed: u32| {
let mut v = voices::make(49, key, vel, sr, seed, samples);
let mut buf = vec![0f32; (1.3 * sr) as usize];
v.render(&mut buf);
let m = |t0: f32, t1: f32| {
let (a, b) = ((t0 * sr) as usize, (t1 * sr) as usize);
(buf[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a) as f32).sqrt()
};
m(0.8, 1.2) / m(0.0, 0.4).max(1e-12)
};
(seeds.iter().map(|&s| one(s).ln()).sum::<f32>() / seeds.len() as f32).exp()
};
for vel in [72u8, 110] {
let k48_model = geo_rise(48, vel, false);
let k52_model = geo_rise(52, vel, false);
let k55_model = geo_rise(55, vel, false);
let k48_full = geo_rise(48, vel, true);
let k52_full = geo_rise(52, vel, true);
let k55_full = geo_rise(55, vel, true);
println!(
"gm49 low-swell vel {vel}: model k48 {k48_model:.2} k52 {k52_model:.2} k55 {k55_model:.2}; full k48 {k48_full:.2} k52 {k52_full:.2} k55 {k55_full:.2}"
);
assert!(
(2.0..=2.5).contains(&k48_model),
"GM49 key 48 vel {vel}: model body/onset {k48_model:.2} is outside the approved +6..+8 dB swell band"
);
assert!(
k48_full > 1.0,
"GM49 key 48 vel {vel}: full sampled voice still falls or stays flat, body/onset {k48_full:.2}"
);
assert!(
k52_model > k55_model && k52_model < k48_model,
"GM49 vel {vel}: key-52 model swell must taper smoothly between key 48 and the key-55 anchor, got k48 {k48_model:.2}, k52 {k52_model:.2}, k55 {k55_model:.2}"
);
assert!(
k52_full > 1.0,
"GM49 key 52 vel {vel}: full sampled voice still falls or stays flat, body/onset {k52_full:.2}"
);
assert!(
(1.90..=2.25).contains(&k55_model),
"GM49 key 55 vel {vel}: MM-BUG-KILN-00024 transition anchor moved, model body/onset {k55_model:.2}"
);
assert!(
k55_full > 1.0,
"GM49 key 55 vel {vel}: full sampled transition anchor must still rise, body/onset {k55_full:.2}"
);
}
}
fn assert_wrap_seam(
program: u8,
key: u8,
vel: u8,
sr: f32,
skip: usize,
label: &str,
) -> Vec<f32> {
assert_wrap_seam_seed(program, key, vel, sr, skip, label, 5)
}
#[allow(clippy::too_many_arguments)]
fn assert_wrap_seam_seed(
program: u8,
key: u8,
vel: u8,
sr: f32,
skip: usize,
label: &str,
seed: u32,
) -> Vec<f32> {
let win = |samples: bool| {
let mut v = voices::make(program, key, vel, sr, seed, samples);
let mut buf = vec![0f32; sr as usize]; v.render(&mut buf);
let w = (0.05 * sr) as usize;
let rms = |a: usize, b: usize| {
(buf[a..b].iter().map(|&x| x * x).sum::<f32>() / (b - a) as f32).sqrt()
};
let coarse: Vec<f32> = (0..9)
.map(|k| rms(w + k * 2 * w, w + (k + 1) * 2 * w))
.collect();
let fine: Vec<f32> = (0..19).map(|k| rms(k * w, (k + 1) * w)).collect();
(coarse, fine)
};
let ((wv, fine), (m, _)) = (win(true), win(false));
for (pw, pm) in wv.windows(2).zip(m.windows(2)).skip(skip) {
let (rw, rm) = (pw[0] / pw[1].max(1e-12), pm[0] / pm[1].max(1e-12));
let excess = (rw / rm).max(rm / rw);
assert!(
excess < 2.4,
"{label}: wrap-introduced level step across the crossfade \
({excess:.2}x beyond the model's own shape): wrapped {wv:?} \
vs model {m:?}"
);
}
fine
}
fn piano_release_reading(mut voice: Box<dyn Voice>, sr: f32) -> (f32, f32, bool) {
let hold_len = (0.3125 * sr) as usize;
let window = (0.020 * sr) as usize;
let mut held = vec![0.0; hold_len];
assert!(voice.render(&mut held), "piano died before note-off");
let before = crate::testutil::rms(&held[hold_len - window..]);
voice.note_off();
let mut release = vec![0.0; (2.0 * sr) as usize];
voice.render(&mut release);
let mut probe = [0.0; 128];
let alive = voice.render(&mut probe);
let gap_start = (0.0425 * sr) as usize;
let gap_end = (0.0625 * sr) as usize;
let gap = crate::testutil::rms(&release[gap_start..gap_end]);
let tail_start = (0.230 * sr) as usize;
let tail_end = (0.250 * sr) as usize;
let tail = crate::testutil::rms(&release[tail_start..tail_end]);
(
20.0 * (before / gap.max(1e-12)).log10(),
20.0 * (before / tail.max(1e-12)).log10(),
alive,
)
}
fn explicit_b1_voice(key: u8, vel: u8, sr: f32, seed: u32, samples: bool) -> Box<dyn Voice> {
let source = voices::GM0_SOURCES
.iter()
.find(|source| source.name == "B1 upright")
.expect("GM0 source table lost the B1 upright");
let bank = if samples {
(source.bank)(vel, seed & 1 == 0)
} else {
&[]
};
voices::acoustic_grand_with_bank(bank, key, vel, sr, seed, false, source.voicing())
}
#[cfg(feature = "embedded-samples")]
fn decode_parent_b1_c5_prefix(vel: u8) -> Vec<f32> {
let hex = if vel < 60 {
include_str!("../tests/fixtures/b1_parent_edcd116_normal_C5_first30.hex")
} else {
include_str!("../tests/fixtures/b1_parent_edcd116_hard_C5_first30.hex")
}
.trim();
assert_eq!(hex.len(), 1_324 * 4);
hex.as_bytes()
.chunks_exact(4)
.map(|digits| {
let text = std::str::from_utf8(digits).expect("parent fixture is ASCII hex");
u16::from_str_radix(text, 16).expect("parent fixture contains valid hex") as i16
as f32
/ 32768.0
})
.collect()
}
#[cfg(feature = "embedded-samples")]
#[test]
fn b1_natural_tail_codec_and_asset_metadata_are_pinned() {
let codes = [0u8, 32, 98, 128, 157, 223, 255];
let expected = [
-1.0,
-0.2456980837065721,
-0.010225303858321285,
8.621159565072071e-5,
0.010225303858321294,
0.2456980837065721,
1.0,
];
for (code, want) in codes.into_iter().zip(expected) {
assert!((decode_b1_tail_code(code) - want as f32).abs() < 1e-7);
}
let bytes = embedded_wav("b1_hard_A4.wav");
let body = parse_wav(bytes);
let tail = parse_b1_tail(bytes, body.len()).expect("hard A4 has a valid natural tail");
assert_eq!(tail.entry_frame, 59_535);
assert_eq!(tail.rate_divisor, 4);
assert_eq!(tail.source_frames, 292_987);
assert_eq!(tail.data.len(), tail.source_frames.div_ceil(4));
let mut truncated = bytes.to_vec();
truncated.pop();
assert!(parse_b1_tail(&truncated, body.len()).is_err());
let tail_start = bytes
.windows(4)
.rposition(|window| window == b"b1t ")
.expect("hard A4 tail FourCC");
let mut unsupported = bytes.to_vec();
unsupported[tail_start + 8] = 2;
assert!(parse_b1_tail(&unsupported, body.len()).is_err());
let data_start = bytes
.windows(4)
.position(|window| window == b"data")
.expect("hard A4 PCM data FourCC");
let data_len =
u32::from_le_bytes(bytes[data_start + 4..data_start + 8].try_into().unwrap());
let mut odd_pcm = bytes.to_vec();
odd_pcm[data_start + 4..data_start + 8].copy_from_slice(&(data_len - 1).to_le_bytes());
assert_eq!(
parse_b1_tail(&odd_pcm, body.len()).err(),
Some("B1 PCM body is empty or has a partial frame")
);
let mut empty_pcm = bytes.to_vec();
empty_pcm[data_start + 4..data_start + 8].copy_from_slice(&0u32.to_le_bytes());
assert_eq!(
parse_b1_tail(&empty_pcm, body.len()).err(),
Some("B1 PCM body is empty or has a partial frame")
);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn b1_natural_tail_extends_the_body_once_and_stops_at_the_recorded_end() {
let key = 69u8;
let vel = 100u8;
let zones = b1upright_bank(vel, false);
let zone = nearest(zones, key_freq(key));
let tail = zone.b1_tail.as_ref().expect("B1 zone has a natural tail");
let step = key_freq(key) / zone.root;
let body_end = (zone.data.len() as f32 / step).ceil() as usize;
let mut natural_end = 0usize;
let mut source_pos = 0.0f32;
while source_pos < (tail.entry_frame + tail.source_frames) as f32 {
natural_end += 1;
source_pos += step;
}
assert!(natural_end > body_end + 2 * 44_100);
let mut voice =
b1upright_sampled(zones, key, vel, 44_100.0, 1.0, 1.0).expect("covered B1 note");
let mut through_old_eof = vec![0.0; body_end + 44_100 / 4];
assert!(voice.render(&mut through_old_eof));
assert!(
crate::testutil::rms(&through_old_eof[body_end..]) > 1e-6,
"natural tail is silent after the old body EOF"
);
let mut whole =
b1upright_sampled(zones, key, vel, 44_100.0, 1.0, 1.0).expect("covered B1 note");
let mut rendered = vec![0.0; natural_end + 256];
assert!(!whole.render(&mut rendered));
assert_eq!(&rendered[natural_end + 8..], &[0.0; 248]);
assert!(
crate::testutil::rms(&rendered[natural_end - 44_100 / 20..natural_end])
< crate::testutil::rms(
&rendered[natural_end - 44_100 / 10..natural_end - 44_100 / 20]
),
"terminal taper does not reduce the final 50 ms"
);
}
#[cfg(feature = "embedded-samples")]
#[test]
fn b1_natural_tail_seams_stay_local_at_common_rates_and_bends() {
fn root_and_worst_keys(zones: &'static [Zone]) -> Vec<u8> {
let mut keys = std::collections::BTreeSet::new();
for zone in zones {
let key = (21u8..=108)
.min_by(|a, b| {
(key_freq(*a) / zone.root)
.log2()
.abs()
.partial_cmp(&(key_freq(*b) / zone.root).log2().abs())
.unwrap()
})
.unwrap();
keys.insert(key);
}
let worst = (21u8..=108)
.filter_map(|key| {
let zone = nearest(zones, key_freq(key));
let ratio = key_freq(key) / zone.root;
(0.5..=2.05)
.contains(&ratio)
.then_some((ratio.log2().abs(), key))
})
.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
.unwrap()
.1;
keys.insert(worst);
keys.into_iter().collect()
}
fn assert_local_seam(rendered: &[f32], seam: usize, window: usize, label: &str) {
let seam_step = (rendered[seam] - rendered[seam - 1]).abs();
let local_start = seam.saturating_sub(window).max(1);
let local_end = (seam + window).min(rendered.len());
let local_max = (local_start..local_end)
.filter(|i| i.abs_diff(seam) > 4)
.map(|i| (rendered[i] - rendered[i - 1]).abs())
.fold(0.0f32, f32::max);
let quantization_margin = (decode_b1_tail_code(128) - decode_b1_tail_code(127)).abs();
assert!(
seam_step <= local_max + quantization_margin,
"{label}: seam step {seam_step:.6} exceeds local maximum {local_max:.6} \
plus one near-zero tail-code step"
);
}
fn output_frame_for_source(source_frame: usize, step: f32) -> usize {
let mut position = 0.0f32;
let mut output_frame = 0usize;
while position < source_frame as f32 {
position += step;
output_frame += 1;
}
output_frame
}
for vel in [59u8, 60] {
let zones = b1upright_bank(vel, false);
for key in root_and_worst_keys(zones) {
let zone = nearest(zones, key_freq(key));
let tail = zone.b1_tail.as_ref().expect("B1 zone has a natural tail");
for sample_rate in [44_100.0f32, 48_000.0, 96_000.0] {
for bend_semitones in [-2.0f32, 0.0, 2.0] {
let bend = 2.0f32.powf(bend_semitones / 12.0);
let step = key_freq(key) / zone.root * 44_100.0 / sample_rate * bend;
let entry = output_frame_for_source(tail.entry_frame, step);
let body_end = output_frame_for_source(zone.data.len(), step);
let window = (0.020 * sample_rate) as usize;
let mut rendered = vec![0.0; body_end + window + 8];
let mut voice = b1upright_sampled(zones, key, vel, sample_rate, 1.0, 1.0)
.expect("covered B1 key");
voice.set_pitch(bend);
assert!(
voice.render(&mut rendered),
"velocity {vel} key {key} rate {sample_rate} bend {bend_semitones}: \
natural tail ended at the old body EOF"
);
assert!(
rendered.iter().all(|value| value.is_finite()),
"velocity {vel} key {key} rate {sample_rate} bend {bend_semitones}: \
natural tail produced a non-finite sample"
);
let label = format!(
"velocity {vel} key {key} rate {sample_rate} bend {bend_semitones}"
);
assert_local_seam(&rendered, entry, window, &format!("{label} entry"));
assert_local_seam(
&rendered,
body_end,
window,
&format!("{label} body EOF"),
);
}
}
}
}
}
#[cfg(feature = "embedded-samples")]
#[test]
fn b1_sample_only_preserves_the_exact_parent_first_30ms() {
const PARENT_GAIN: f32 = 5.20;
const EXPERIMENT_GAIN: f32 = 1.30;
const LAST_PARENT_FRAME: usize = (0.030 * 44_100.0) as usize;
const MAX_ERROR: f32 = 0.00025;
for vel in [59u8, 60] {
let zones = b1upright_bank(vel, false);
for key in [72u8, 73] {
let frequency = key_freq(key);
let zone = nearest(zones, frequency);
let step = frequency / zone.root;
let frame_count = (0..)
.take_while(|frame| {
((*frame as f32 * step).floor() as usize) + 2 <= LAST_PARENT_FRAME
})
.count();
let parent = decode_parent_b1_c5_prefix(vel);
let mut expected = Vec::with_capacity(frame_count);
let mut pos = 0.0f32;
for _ in 0..frame_count {
let j = pos.floor() as usize;
let frac = pos - j as f32;
expected.push(
crate::dsp::cubic4(
parent[j.saturating_sub(1)],
parent[j],
parent[j + 1],
parent[j + 2],
frac,
) * PARENT_GAIN
* vel_amp(vel),
);
pos += step;
}
assert!(
pos.floor() as usize + 2 > LAST_PARENT_FRAME,
"fixture did not stop at the cubic-support boundary"
);
let mut actual = vec![0.0; frame_count];
let mut voice = b1upright_sampled(zones, key, vel, 44_100.0, EXPERIMENT_GAIN, 1.0)
.expect("C5 controls are inside B1 coverage");
assert!(voice.render(&mut actual));
let max_error = actual
.iter()
.zip(&expected)
.map(|(actual, expected)| (actual - expected).abs())
.fold(0.0, f32::max);
assert!(
max_error <= MAX_ERROR,
"key {key} velocity {vel}: first-30ms parent parity error \
{max_error:.9} exceeds {MAX_ERROR}"
);
}
}
}
fn b1_release_curve_error(
mut released: Box<dyn Voice>,
mut held: Box<dyn Voice>,
sr: f32,
t60: f32,
) -> f32 {
let hold_len = (0.3125 * sr) as usize;
let mut released_prefix = vec![0.0; hold_len];
let mut held_prefix = vec![0.0; hold_len];
released.render(&mut released_prefix);
held.render(&mut held_prefix);
assert_eq!(released_prefix, held_prefix);
released.note_off();
let mut released_tail = vec![0.0; (0.250 * sr) as usize];
let mut held_tail = vec![0.0; released_tail.len()];
released.render(&mut released_tail);
held.render(&mut held_tail);
released_tail
.iter()
.zip(&held_tail)
.enumerate()
.filter(|(_, (_, held))| held.abs() > 1e-5)
.map(|(i, (&released, &held))| {
let expected = 10f32.powf(-3.0 * i as f32 / (t60 * sr));
(released / held - expected).abs()
})
.fold(0.0, f32::max)
}
#[test]
fn b1_upright_sample_path_is_recording_only_and_seed_invariant() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44_100.0;
let seeds = [0x1357_9BDFu32, 0x6A09_E667];
for key in [21u8, 60, 85, 108] {
for vel in [59u8, 60] {
let mut sampled_a = explicit_b1_voice(key, vel, sr, seeds[0], true);
let mut sampled_b = explicit_b1_voice(key, vel, sr, seeds[1], true);
assert_eq!(sampled_a.kind(), "b1sampled");
assert_eq!(sampled_b.kind(), "b1sampled");
let mut a = vec![0.0; sr as usize];
let mut b = vec![0.0; a.len()];
assert!(sampled_a.render(&mut a));
assert!(sampled_b.render(&mut b));
assert_eq!(
a, b,
"key {key} velocity {vel} still depends on the model seed"
);
}
}
let mut model_a = explicit_b1_voice(60, 60, sr, seeds[0], false);
let mut model_b = explicit_b1_voice(60, 60, sr, seeds[1], false);
let mut a = vec![0.0; (0.500 * sr) as usize];
let mut b = vec![0.0; a.len()];
model_a.render(&mut a);
model_b.render(&mut b);
let start = (0.100 * sr) as usize;
let diff: Vec<f32> = a[start..]
.iter()
.zip(&b[start..])
.map(|(x, y)| x - y)
.collect();
let normalized = crate::testutil::rms(&diff)
/ crate::testutil::rms(&a[start..]).max(crate::testutil::rms(&b[start..]));
assert!(
normalized >= 0.05,
"seed pair does not exercise model randomness: normalized RMS difference {normalized}"
);
}
const GAP_RATIO_MIN_MODEL: f32 = 0.80;
const GAP_RATIO_MIN_SAMPLED: f32 = 0.40;
const GAP_RATIO_MAX: f32 = 1.10;
const TAIL_RATIO_MIN: f32 = 0.75;
#[test]
fn gm0_fra_gap_release_bridges_then_clears() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for samples in [false, true] {
for key in [62u8, 66, 69] {
let mut rr_drops = Vec::new();
for seed in [1u32, 2] {
let voice = if samples {
let src = &voices::GM0_SOURCES[0];
voices::acoustic_grand_with_bank(
(src.bank)(104, seed == 2),
key,
104,
sr,
1,
false,
src.voicing(),
)
} else {
voices::make(0, key, 104, sr, seed, false)
};
let (gap_drop, tail_drop, alive) = piano_release_reading(voice, sr);
println!(
"GM0 key {key} seed {seed} samples={samples}: \
gap {gap_drop:.2} dB, 250ms {tail_drop:.2} dB"
);
let (t60, _) = voices::PianoDamper::Felt.t60_for(key);
let gap_expect = 60.0 * 0.0625 / t60;
let tail_expect = 60.0 * 0.25 / t60;
println!(
" key {key} t60 {t60:.3}s: gap expect {gap_expect:.2} \
got {gap_drop:.2} (x{:.2}), tail expect {tail_expect:.2} \
got {tail_drop:.2} (x{:.2})",
gap_drop / gap_expect,
tail_drop / tail_expect
);
if samples {
let error = b1_release_curve_error(
explicit_b1_voice(key, 104, sr, seed, true),
explicit_b1_voice(key, 104, sr, seed, true),
sr,
t60,
);
assert!(
error < 2e-4,
"GM0 key {key} seed {seed}: B1 release differs from the \
felt-damper exponential by {error}"
);
} else {
assert!(
gap_drop <= gap_expect * GAP_RATIO_MAX,
"GM0 key {key} seed {seed}: 62.5ms model gap drop \
{gap_drop:.2} dB exceeds {GAP_RATIO_MAX}x the {gap_expect:.2} dB \
its {t60:.3}s damper predicts — the note is cut, not damped"
);
}
let gap_min = if samples {
GAP_RATIO_MIN_SAMPLED
} else {
GAP_RATIO_MIN_MODEL
};
assert!(
gap_drop >= gap_expect * gap_min,
"GM0 key {key} seed {seed} samples={samples}: 62.5ms gap drop \
{gap_drop:.2} dB is under {gap_min}x the {gap_expect:.2} dB \
its {t60:.3}s damper predicts — the release barely engages"
);
assert!(
tail_drop >= tail_expect * TAIL_RATIO_MIN,
"GM0 key {key} seed {seed} samples={samples}: only {tail_drop:.2} dB \
down after 250 ms, under {TAIL_RATIO_MIN}x the {tail_expect:.2} dB \
its {t60:.3}s damper predicts"
);
assert!(
!alive,
"GM0 key {key} seed {seed} samples={samples}: \
voice remained alive 2 seconds after note-off"
);
rr_drops.push(gap_drop);
}
if samples {
assert!(
(rr_drops[0] - rr_drops[1]).abs() <= 2.0,
"GM0 key {key}: round-robin release spread {:.2} dB",
(rr_drops[0] - rr_drops[1]).abs()
);
}
}
}
}
#[test]
fn gm0_conditioned_bank_still_needs_the_release_override() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for (key, seed) in [(62u8, 1u32), (66, 2), (69, 1)] {
let legacy = voices::acoustic_grand_with_bank(
piano_bank(104, seed & 1 == 0),
key,
104,
sr,
seed,
false,
voices::LEGACY_VOICING,
);
let (gap_drop, _, _) = piano_release_reading(legacy, sr);
println!("conditioned GM0 key {key} legacy gap: {gap_drop:.2} dB");
assert!(
gap_drop > 10.0,
"GM0 key {key}: conditioned bank unexpectedly meets the gap target \
on legacy release ({gap_drop:.2} dB)"
);
}
}
#[test]
fn gm0_release_clears_across_the_piano_register() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
for samples in [false, true] {
for key in [36u8, 60, 84] {
let (_, tail_drop, alive) =
piano_release_reading(voices::make(0, key, 104, sr, 1, samples), sr);
let (t60, _) = voices::PianoDamper::Felt.t60_for(key);
let tail_expect = 60.0 * 0.25 / t60;
println!(
" key {key} samples={samples}: t60 {t60:.3}s, tail expect \
{tail_expect:.2} dB, got {tail_drop:.2} (x{:.2})",
tail_drop / tail_expect
);
assert!(
tail_drop >= tail_expect * TAIL_RATIO_MIN,
"GM0 key {key} samples={samples}: only {tail_drop:.2} dB down at 250 ms, \
under {TAIL_RATIO_MIN}x the {tail_expect:.2} dB its {t60:.3}s damper \
predicts"
);
assert!(
!alive,
"GM0 key {key} samples={samples}: alive after the 2 s reap probe"
);
}
}
}
#[test]
fn gm0_damper_reaches_gm0_alternates_and_stops_there() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44100.0;
let (default_drop, _, _) = piano_release_reading(voices::make(0, 66, 104, sr, 1, true), sr);
assert!(
default_drop <= 10.0,
"default GM0 did not opt into the calibrated release"
);
for (name, voice, control) in [
(
"GM0 Salamander alternate",
crate::altbank::make(0, 2, 66, 104, sr, 1, true),
{
let src = &voices::GM0_SOURCES[2];
voices::acoustic_grand_with_bank(
(src.bank)(104, false),
66,
104,
sr,
1,
false,
src.voicing(),
)
},
),
(
"GM0 undefined-CC0 fallback",
crate::altbank::make(0, 99, 66, 104, sr, 1, true),
voices::acoustic_grand_with_bank(
&[],
66,
104,
sr,
1,
false,
voices::GM0_SOURCES[0].voicing(),
),
),
(
"GM1 Bright",
voices::make(1, 66, 104, sr, 1, true),
voices::acoustic_grand_with_bank(
kawai_bank(104, false),
66,
104,
sr,
1,
true,
voices::GM1_VOICING,
),
),
] {
let (drop, _, _) = piano_release_reading(voice, sr);
let (control_drop, _, _) = piano_release_reading(control, sr);
assert!(
(drop - control_drop).abs() <= 0.01,
"{name}: release drifted from its explicit control \
({drop:.2} dB vs {control_drop:.2} dB)"
);
}
let (honky_drop, _, _) = piano_release_reading(voices::make(3, 66, 104, sr, 1, true), sr);
assert!(
(35.5..=36.0).contains(&honky_drop),
"GM3 honky-tonk release drifted from its legacy window ({honky_drop:.2} dB)"
);
let (nylon_drop, _, _) = piano_release_reading(voices::make(24, 52, 104, sr, 1, true), sr);
assert!(
(5.5..=6.5).contains(&nylon_drop),
"GM24 nylon LA release drifted from its legacy window ({nylon_drop:.2} dB)"
);
}
#[test]
fn gm0_conditioned_bank_is_continuous_across_keys_layers_and_round_robins() {
if !crate::embedded_samples_available() {
return;
}
struct Silent;
impl Voice for Silent {
fn render(&mut self, _out: &mut [f32]) -> bool {
true
}
fn note_off(&mut self) {}
fn released(&self) -> bool {
false
}
fn kind(&self) -> &'static str {
"silent"
}
}
let sr = 44100.0;
let keys = [
36u8, 39, 40, 43, 45, 46, 48, 51, 52, 55, 57, 58, 60, 63, 64, 67, 69, 70, 72, 75, 76,
79, 81, 82, 84,
];
let boundaries = [
(39u8, 40u8),
(45, 46),
(51, 52),
(57, 58),
(63, 64),
(69, 70),
(75, 76),
(81, 82),
];
for vel in [48u8, 80, 104] {
for seed in [1u32, 2] {
let mut readings = Vec::new();
for &key in &keys {
let (gain, _) = voices::LA_PIANO;
let bank = piano_bank(vel, seed & 1 == 0);
let step = key_freq(key) / nearest(bank, key_freq(key)).root * 44100.0 / sr;
let mut sample = LaVoice::wrap_release(
Box::new(Silent),
bank,
key,
vel,
sr,
gain,
(2.0, 3.0),
crate::voices::GM0_RELEASE_T60,
);
let mut buf = vec![0.0; sr as usize];
sample.render(&mut buf);
let attack = crate::testutil::rms(&buf[..(0.030 * sr / step) as usize]);
let body = crate::testutil::rms(
&buf[(0.140 * sr / step) as usize..(0.220 * sr / step) as usize],
);
assert!(
attack > 1e-4,
"GM0 key {key} vel {vel} seed {seed} is silent"
);
readings.push((key, body));
}
for &(left_key, right_key) in &boundaries {
let (_, left_body) = readings
.iter()
.copied()
.find(|&(key, _)| key == left_key)
.unwrap();
let (_, right_body) = readings
.iter()
.copied()
.find(|&(key, _)| key == right_key)
.unwrap();
let step_db = 20.0 * (left_body / right_body.max(1e-12)).log10();
assert!(
step_db.abs() <= 3.0,
"GM0 body jumps {step_db:+.2} dB across keys \
{left_key}/{right_key} at vel {vel} seed {seed}"
);
}
}
}
}
fn assert_attack_is_peak(fine: &[f32], label: &str) {
let attack_windows = 3;
let attack = fine[..attack_windows].iter().fold(0f32, |mx, &x| mx.max(x));
let late = fine[attack_windows..].iter().fold(0f32, |mx, &x| mx.max(x));
assert!(
late <= attack * 1.01,
"{label}: attack is not the peak — late window {late:.5} \
above attack {attack:.5} by more than 1% ({fine:?})"
);
}
#[test]
fn b1_natural_tail_is_bounded_and_pitch_bend_changes_its_read_rate() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44_100.0;
let seed = 0x6A09_E667;
let mut natural = explicit_b1_voice(60, 60, sr, seed, true);
let mut first_second = vec![0.0; sr as usize];
assert!(natural.render(&mut first_second));
assert!(crate::testutil::rms(&first_second[(0.8 * sr) as usize..]) > 1e-5);
let mut past_old_eof = vec![0.0; sr as usize];
assert!(natural.render(&mut past_old_eof));
assert!(crate::testutil::rms(&past_old_eof) > 1e-6);
let mut natural_tail = vec![0.0; (6.0 * sr) as usize];
assert!(!natural.render(&mut natural_tail));
let lifetime = |bend: f32| {
let mut voice = explicit_b1_voice(60, 60, sr, seed, true);
voice.set_pitch(bend);
let mut frames = 0usize;
loop {
let mut block = [0.0; 1024];
frames += block.len();
if !voice.render(&mut block) {
return frames;
}
assert!(frames < 12 * sr as usize, "B1 natural tail did not end");
}
};
let unbent_frames = lifetime(1.0);
let bent_frames = lifetime(2.0);
assert!(
(0.49..=0.51).contains(&(bent_frames as f32 / unbent_frames as f32)),
"octave-up bend lifetime {bent_frames} is not half of {unbent_frames}"
);
}
#[test]
fn b1_sample_only_note_off_applies_the_felt_damper() {
let sr = 44_100.0;
for vel in [59u8, 60] {
for hold_s in [0.100f32, 1.400, 1.700] {
let mut released = explicit_b1_voice(60, vel, sr, 0x6A09_E667, true);
let mut held = explicit_b1_voice(60, vel, sr, 0x6A09_E667, true);
let mut released_prefix = vec![0.0; (hold_s * sr) as usize];
let mut held_prefix = vec![0.0; released_prefix.len()];
released.render(&mut released_prefix);
held.render(&mut held_prefix);
assert_eq!(released_prefix, held_prefix);
released.note_off();
let mut released_tail = vec![0.0; (0.300 * sr) as usize];
let mut held_tail = vec![0.0; released_tail.len()];
released.render(&mut released_tail);
held.render(&mut held_tail);
let late = (0.200 * sr) as usize;
assert!(
crate::testutil::rms(&released_tail[late..])
< crate::testutil::rms(&held_tail[late..]) * 0.20,
"velocity {vel} hold {hold_s:.1}s: felt damper did not suppress the recording"
);
}
let (t60, _) = voices::PianoDamper::Felt.t60_for(60);
let error = b1_release_curve_error(
explicit_b1_voice(60, vel, sr, 0x6A09_E667, true),
explicit_b1_voice(60, vel, sr, 0x6A09_E667, true),
sr,
t60,
);
assert!(
error < 2e-4,
"velocity {vel}: release differs from the felt curve by {error}"
);
}
}
#[test]
#[ignore = "writes B1 dry-control WAVs to the OS temp directory for human audition"]
fn write_b1_sample_only_dry_controls() {
if !crate::embedded_samples_available() {
return;
}
let sr = 44_100.0;
let output_dir = std::env::temp_dir().join("ferrosintesis-b1-sample-only");
std::fs::create_dir_all(&output_dir).expect("create B1 diagnostic directory");
let cases = [
("dry-aligned-b1-normal-root-C5-v59.wav", 72u8, 59u8, None),
("dry-aligned-b1-hard-root-C5-v60.wav", 72, 60, None),
(
"dry-aligned-b1-normal-repitch-Csharp5-v59.wav",
73,
59,
None,
),
(
"dry-aligned-b1-normal-release-C5-v59.wav",
72,
59,
Some(0.100f32),
),
];
for (name, key, vel, release_at) in cases {
let mut voice = explicit_b1_voice(key, vel, sr, 0x6A09_E667, true);
let total = (4.0 * sr) as usize;
let release = release_at.map(|seconds| (seconds * sr) as usize);
let mut rendered = vec![0.0f32; total];
let mut offset = 0;
while offset < total {
if release == Some(offset) {
voice.note_off();
}
let next = release
.filter(|&at| at > offset)
.map_or((offset + 64).min(total), |at| at.min(offset + 64));
voice.render(&mut rendered[offset..next]);
offset = next;
}
let peak = rendered.iter().copied().map(f32::abs).fold(0.0, f32::max);
let runtime_scalar = 1.30 * vel_amp(vel);
let alignment_scalar = runtime_scalar.recip();
let mut stereo = Vec::with_capacity(rendered.len() * 2);
for value in rendered {
let aligned = value * alignment_scalar;
let pcm = (aligned.clamp(-1.0, 1.0) * i16::MAX as f32).round() as i16;
stereo.extend([pcm, pcm]);
}
let path = output_dir.join(name);
crate::wav::write_wav(&path, sr as u32, &stereo)
.unwrap_or_else(|error| panic!("write {}: {error}", path.display()));
println!(
"{}: runtime scalar {:.9}, alignment scalar {:.9}, aligned peak {:.6}",
path.display(),
runtime_scalar,
alignment_scalar,
peak * alignment_scalar
);
}
}
#[test]
fn clavinet_default_is_sampled_alt_is_modeled() {
let sr = 44100.0;
assert_eq!(
voices::make(7, 60, 100, sr, 5, true).kind(),
"clavinetsampled",
"GM7 default (samples on) must be the sampled clavinet"
);
assert_eq!(
voices::make(7, 60, 100, sr, 5, false).kind(),
"CLAVINET",
"GM7 --no-samples must be the modeled Pluck"
);
assert_eq!(
crate::altbank::make(7, 1, 60, 100, sr, 5, true).kind(),
"CLAVINET",
"GM7 CC0-nonzero alt bank must be the modeled Pluck"
);
let (mut def, mut alt) = (
voices::make(7, 60, 100, sr, 5, true),
crate::altbank::make(7, 1, 60, 100, sr, 5, true),
);
let (mut db, mut ab) = (vec![0f32; 22050], vec![0f32; 22050]);
def.render(&mut db);
alt.render(&mut ab);
assert_ne!(
db, ab,
"sampled default must differ from the modeled alt bank"
);
}
}