use std::path::Path;
use std::sync::Arc;
use oxideav_core::{Error, Result};
use super::{Instrument, Voice};
pub const RIFF_MAGIC: &[u8; 4] = b"RIFF";
pub const SFBK_MAGIC: &[u8; 4] = b"sfbk";
pub const MAX_SAMPLE_FRAMES: usize = 256 * 1024 * 1024;
pub const MAX_PDTA_RECORDS: usize = 16 * 1024 * 1024;
pub const GEN_KEY_RANGE: u16 = 43;
pub const GEN_VEL_RANGE: u16 = 44;
pub const GEN_STARTLOOP_OFFSET: u16 = 2;
pub const GEN_ENDLOOP_OFFSET: u16 = 3;
pub const GEN_START_OFFSET: u16 = 0;
pub const GEN_END_OFFSET: u16 = 1;
pub const GEN_INSTRUMENT: u16 = 41;
pub const GEN_SAMPLE_ID: u16 = 53;
pub const GEN_SAMPLE_MODES: u16 = 54;
pub const GEN_COARSE_TUNE: u16 = 51;
pub const GEN_FINE_TUNE: u16 = 52;
pub const GEN_OVERRIDING_ROOT_KEY: u16 = 58;
pub const GEN_DELAY_VOL_ENV: u16 = 33;
pub const GEN_ATTACK_VOL_ENV: u16 = 34;
pub const GEN_HOLD_VOL_ENV: u16 = 35;
pub const GEN_DECAY_VOL_ENV: u16 = 36;
pub const GEN_SUSTAIN_VOL_ENV: u16 = 37;
pub const GEN_RELEASE_VOL_ENV: u16 = 38;
pub const GEN_INITIAL_ATTENUATION: u16 = 48;
pub const GEN_DELAY_MOD_ENV: u16 = 25;
pub const GEN_ATTACK_MOD_ENV: u16 = 26;
pub const GEN_HOLD_MOD_ENV: u16 = 27;
pub const GEN_DECAY_MOD_ENV: u16 = 28;
pub const GEN_SUSTAIN_MOD_ENV: u16 = 29;
pub const GEN_RELEASE_MOD_ENV: u16 = 30;
pub const GEN_MOD_ENV_TO_PITCH: u16 = 7;
pub const GEN_MOD_ENV_TO_FILTER_FC: u16 = 11;
pub const GEN_INITIAL_FILTER_FC: u16 = 8;
pub const GEN_INITIAL_FILTER_Q: u16 = 9;
pub const GEN_EXCLUSIVE_CLASS: u16 = 57;
#[derive(Clone, Debug)]
pub struct Sf2Bank {
pub info: Sf2Info,
pub presets: Vec<PresetHeader>,
pub instruments: Vec<InstrumentHeader>,
pub samples: Vec<SampleHeader>,
pub pbags: Vec<Bag>,
pub pgens: Vec<Generator>,
pub ibags: Vec<Bag>,
pub igens: Vec<Generator>,
pub sample_data: Arc<[i32]>,
}
pub mod sample_type_bits {
pub const MONO: u16 = 0x0001;
pub const RIGHT: u16 = 0x0002;
pub const LEFT: u16 = 0x0004;
pub const LINKED: u16 = 0x0008;
pub const ROM: u16 = 0x8000;
}
#[derive(Clone, Debug, Default)]
pub struct Sf2Info {
pub name: Option<String>,
pub engine: Option<String>,
pub version: Option<(u16, u16)>,
}
#[derive(Clone, Debug)]
pub struct PresetHeader {
pub name: String,
pub program: u16,
pub bank: u16,
pub pbag_start: u16,
}
#[derive(Clone, Debug)]
pub struct InstrumentHeader {
pub name: String,
pub ibag_start: u16,
}
#[derive(Clone, Debug)]
pub struct SampleHeader {
pub name: String,
pub start: u32,
pub end: u32,
pub start_loop: u32,
pub end_loop: u32,
pub sample_rate: u32,
pub original_key: u8,
pub pitch_correction: i8,
pub sample_link: u16,
pub sample_type: u16,
}
#[derive(Clone, Copy, Debug)]
pub struct Bag {
pub gen_start: u16,
pub mod_start: u16,
}
#[derive(Clone, Copy, Debug)]
pub struct Generator {
pub oper: u16,
pub amount: u16,
}
impl Generator {
pub fn amount_i16(self) -> i16 {
self.amount as i16
}
pub fn amount_lo_hi(self) -> (u8, u8) {
(self.amount as u8, (self.amount >> 8) as u8)
}
}
pub struct Sf2Instrument {
name: String,
bank: Sf2Bank,
}
impl Sf2Instrument {
pub fn open(path: &Path) -> Result<Self> {
let bytes = std::fs::read(path)?;
let bank = Sf2Bank::parse(&bytes)?;
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "sf2".to_string());
Ok(Self { name, bank })
}
pub fn from_bytes(name: impl Into<String>, bytes: &[u8]) -> Result<Self> {
let bank = Sf2Bank::parse(bytes)?;
Ok(Self {
name: name.into(),
bank,
})
}
pub fn bank(&self) -> &Sf2Bank {
&self.bank
}
}
impl Instrument for Sf2Instrument {
fn name(&self) -> &str {
&self.name
}
fn make_voice(
&self,
program: u8,
key: u8,
velocity: u8,
sample_rate: u32,
) -> Result<Box<dyn Voice>> {
let plan = self.bank.resolve(program, key, velocity).ok_or_else(|| {
Error::unsupported(format!(
"SF2 '{}': no preset matches program {program} key {key} velocity {velocity}",
self.name,
))
})?;
let voice =
Sf2Voice::from_plan(self.bank.sample_data.clone(), &plan, velocity, sample_rate);
Ok(Box::new(voice))
}
}
pub fn is_sf2(bytes: &[u8]) -> bool {
bytes.len() >= 12 && &bytes[0..4] == RIFF_MAGIC && &bytes[8..12] == SFBK_MAGIC
}
struct Cursor<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn remaining(&self) -> usize {
self.bytes.len().saturating_sub(self.pos)
}
fn at_end(&self) -> bool {
self.pos >= self.bytes.len()
}
fn read_tag(&mut self) -> Result<[u8; 4]> {
if self.remaining() < 4 {
return Err(Error::invalid("SF2: truncated chunk tag (needed 4 bytes)"));
}
let mut tag = [0u8; 4];
tag.copy_from_slice(&self.bytes[self.pos..self.pos + 4]);
self.pos += 4;
Ok(tag)
}
fn read_u32_le(&mut self) -> Result<u32> {
if self.remaining() < 4 {
return Err(Error::invalid("SF2: truncated u32"));
}
let v = u32::from_le_bytes(self.bytes[self.pos..self.pos + 4].try_into().unwrap());
self.pos += 4;
Ok(v)
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
if self.remaining() < n {
return Err(Error::invalid(format!(
"SF2: truncated payload (needed {n} bytes, {} remain)",
self.remaining(),
)));
}
let out = &self.bytes[self.pos..self.pos + n];
self.pos += n;
Ok(out)
}
}
fn read_chunk<'a>(c: &mut Cursor<'a>) -> Result<([u8; 4], &'a [u8])> {
let tag = c.read_tag()?;
let size = c.read_u32_le()? as usize;
if size > c.remaining() {
return Err(Error::invalid(format!(
"SF2: chunk '{}' length {size} exceeds {} bytes remaining",
tag_str(&tag),
c.remaining(),
)));
}
let payload = c.take(size)?;
if size % 2 == 1 && c.remaining() >= 1 {
c.pos += 1;
}
Ok((tag, payload))
}
fn tag_str(tag: &[u8; 4]) -> String {
if tag.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
String::from_utf8_lossy(tag).into_owned()
} else {
format!("{:02X}{:02X}{:02X}{:02X}", tag[0], tag[1], tag[2], tag[3])
}
}
impl Sf2Bank {
pub fn parse(bytes: &[u8]) -> Result<Self> {
if !is_sf2(bytes) {
return Err(Error::invalid(
"SF2: file does not start with RIFF/sfbk magic",
));
}
let mut outer = Cursor::new(bytes);
let _ = outer.read_tag()?; let total = outer.read_u32_le()? as usize;
if total + 8 > bytes.len() {
return Err(Error::invalid(format!(
"SF2: outer RIFF size {total} exceeds file size {}",
bytes.len() - 8,
)));
}
let body = &bytes[8..8 + total.min(bytes.len() - 8)];
let mut body_cur = Cursor::new(body);
let form = body_cur.read_tag()?;
if &form != SFBK_MAGIC {
return Err(Error::invalid(format!(
"SF2: outer form is '{}', expected 'sfbk'",
tag_str(&form),
)));
}
let mut info = Sf2Info::default();
let mut sdta_smpl: &[u8] = &[];
let mut sdta_sm24: &[u8] = &[];
let mut pdta_payload: &[u8] = &[];
while !body_cur.at_end() {
let (tag, payload) = read_chunk(&mut body_cur)?;
if &tag != b"LIST" {
continue;
}
if payload.len() < 4 {
return Err(Error::invalid("SF2: LIST chunk shorter than 4 bytes"));
}
let list_type = &payload[0..4];
let list_body = &payload[4..];
match list_type {
b"INFO" => parse_info(list_body, &mut info)?,
b"sdta" => {
let (smpl, sm24) = parse_sdta(list_body)?;
sdta_smpl = smpl;
sdta_sm24 = sm24;
}
b"pdta" => pdta_payload = list_body,
_ => {
}
}
}
if pdta_payload.is_empty() {
return Err(Error::invalid("SF2: missing 'pdta' LIST"));
}
let pdta = Pdta::parse(pdta_payload)?;
if sdta_smpl.len() % 2 != 0 {
return Err(Error::invalid(format!(
"SF2: sdta-smpl length {} is not a multiple of 2",
sdta_smpl.len(),
)));
}
let frame_count = sdta_smpl.len() / 2;
if frame_count > MAX_SAMPLE_FRAMES {
return Err(Error::invalid(format!(
"SF2: sdta-smpl frame count {frame_count} exceeds cap {MAX_SAMPLE_FRAMES}",
)));
}
let use_sm24 = !sdta_sm24.is_empty() && sdta_sm24.len() == frame_count;
let mut sample_data = Vec::with_capacity(frame_count);
for (i, chunk) in sdta_smpl.chunks_exact(2).enumerate() {
let hi = i16::from_le_bytes([chunk[0], chunk[1]]) as i32;
let lo = if use_sm24 { sdta_sm24[i] as i32 } else { 0 };
sample_data.push((hi << 8) | (lo & 0xFF));
}
for (i, sh) in pdta.shdr.iter().enumerate() {
if (sh.end as usize) > sample_data.len() || sh.start > sh.end {
return Err(Error::invalid(format!(
"SF2: shdr[{i}] '{}' range {}..{} is out of bounds (smpl has {} frames)",
sh.name,
sh.start,
sh.end,
sample_data.len(),
)));
}
if sh.start_loop > sh.end_loop || (sh.end_loop as usize) > sample_data.len() {
return Err(Error::invalid(format!(
"SF2: shdr[{i}] '{}' loop {}..{} is out of bounds",
sh.name, sh.start_loop, sh.end_loop,
)));
}
}
Ok(Self {
info,
presets: pdta.phdr,
instruments: pdta.inst,
samples: pdta.shdr,
pbags: pdta.pbag,
pgens: pdta.pgen,
ibags: pdta.ibag,
igens: pdta.igen,
sample_data: Arc::from(sample_data.into_boxed_slice()),
})
}
pub fn resolve(&self, program: u8, key: u8, velocity: u8) -> Option<SamplePlan> {
let preset_idx = self
.presets
.iter()
.position(|p| p.bank == 0 && p.program as u8 == program)
.or_else(|| self.presets.iter().position(|p| p.program as u8 == program))
.or(if self.presets.is_empty() {
None
} else {
Some(0)
})?;
let preset = &self.presets[preset_idx];
let next_pbag_end = self
.presets
.get(preset_idx + 1)
.map(|p| p.pbag_start as usize)
.unwrap_or_else(|| self.pbags.len().saturating_sub(1));
let pbag_lo = preset.pbag_start as usize;
let pbag_hi = next_pbag_end;
if pbag_hi > self.pbags.len().saturating_sub(1) || pbag_lo > pbag_hi {
return None;
}
for zone_idx in pbag_lo..pbag_hi {
let bag = self.pbags[zone_idx];
let next = self.pbags[zone_idx + 1];
let gens = self
.pgens
.get(bag.gen_start as usize..next.gen_start as usize)?;
let (klo, khi) = key_range(gens).unwrap_or((0, 127));
let (vlo, vhi) = vel_range(gens).unwrap_or((0, 127));
if key < klo || key > khi || velocity < vlo || velocity > vhi {
continue;
}
let inst_idx = gens
.iter()
.rev()
.find(|g| g.oper == GEN_INSTRUMENT)
.map(|g| g.amount as usize)?;
if inst_idx >= self.instruments.len() {
continue;
}
let inst = &self.instruments[inst_idx];
let next_ibag_end = self
.instruments
.get(inst_idx + 1)
.map(|i| i.ibag_start as usize)
.unwrap_or_else(|| self.ibags.len().saturating_sub(1));
let ilo = inst.ibag_start as usize;
let ihi = next_ibag_end;
if ihi > self.ibags.len().saturating_sub(1) || ilo > ihi {
continue;
}
for izone_idx in ilo..ihi {
let ibag = self.ibags[izone_idx];
let inext = self.ibags[izone_idx + 1];
let igens = self
.igens
.get(ibag.gen_start as usize..inext.gen_start as usize)?;
let (klo, khi) = key_range(igens).unwrap_or((0, 127));
let (vlo, vhi) = vel_range(igens).unwrap_or((0, 127));
if key < klo || key > khi || velocity < vlo || velocity > vhi {
continue;
}
let sample_idx = igens
.iter()
.rev()
.find(|g| g.oper == GEN_SAMPLE_ID)
.map(|g| g.amount as usize)?;
if sample_idx >= self.samples.len() {
continue;
}
let sample = &self.samples[sample_idx];
let mut plan = SamplePlan::from_zones(sample, igens, gens, key);
if (sample.sample_type & (sample_type_bits::LEFT | sample_type_bits::RIGHT)) != 0 {
let partner = sample.sample_link as usize;
if partner != sample_idx
&& partner < self.samples.len()
&& self.samples[partner].sample_link as usize == sample_idx
{
let p = &self.samples[partner];
plan.stereo_pair = Some(StereoPair {
start: p.start,
end: p.end,
start_loop: p.start_loop,
end_loop: p.end_loop,
sample_rate: p.sample_rate.max(1),
});
}
}
return Some(plan);
}
}
None
}
}
#[derive(Clone, Debug)]
pub struct SamplePlan {
pub start: u32,
pub end: u32,
pub start_loop: u32,
pub end_loop: u32,
pub sample_rate: u32,
pub loops: bool,
pub pitch_ratio: f64,
pub semitones: i32,
pub fine_cents: i32,
pub env: EnvParams,
pub mod_env: ModEnvParams,
pub mod_env_to_pitch_cents: i32,
pub mod_env_to_filter_cents: i32,
pub initial_filter_fc_cents: i32,
pub initial_filter_q_cb: i32,
pub initial_attenuation_cb: i32,
pub exclusive_class: u16,
pub stereo_pair: Option<StereoPair>,
}
#[derive(Clone, Copy, Debug)]
pub struct StereoPair {
pub start: u32,
pub end: u32,
pub start_loop: u32,
pub end_loop: u32,
pub sample_rate: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct ModEnvParams {
pub delay_tc: i32,
pub attack_tc: i32,
pub hold_tc: i32,
pub decay_tc: i32,
pub sustain_per_mille: i32,
pub release_tc: i32,
}
impl Default for ModEnvParams {
fn default() -> Self {
Self {
delay_tc: i32::MIN,
attack_tc: i32::MIN,
hold_tc: i32::MIN,
decay_tc: i32::MIN,
sustain_per_mille: 0,
release_tc: i32::MIN,
}
}
}
impl ModEnvParams {
pub fn from_generators(igens: &[Generator], pgens: &[Generator]) -> Self {
fn pick(igens: &[Generator], pgens: &[Generator], oper: u16) -> Option<i16> {
generator_amount(igens, oper)
.or_else(|| generator_amount(pgens, oper))
.map(|v| v as i16)
}
Self {
delay_tc: pick(igens, pgens, GEN_DELAY_MOD_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
attack_tc: pick(igens, pgens, GEN_ATTACK_MOD_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
hold_tc: pick(igens, pgens, GEN_HOLD_MOD_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
decay_tc: pick(igens, pgens, GEN_DECAY_MOD_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
sustain_per_mille: pick(igens, pgens, GEN_SUSTAIN_MOD_ENV)
.map(|v| v as i32)
.unwrap_or(0),
release_tc: pick(igens, pgens, GEN_RELEASE_MOD_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct EnvParams {
pub delay_tc: i32,
pub attack_tc: i32,
pub hold_tc: i32,
pub decay_tc: i32,
pub sustain_cb: i32,
pub release_tc: i32,
}
impl Default for EnvParams {
fn default() -> Self {
Self {
delay_tc: i32::MIN,
attack_tc: i32::MIN,
hold_tc: i32::MIN,
decay_tc: i32::MIN,
sustain_cb: 0,
release_tc: i32::MIN,
}
}
}
impl EnvParams {
pub fn from_generators(igens: &[Generator], pgens: &[Generator]) -> Self {
fn pick(igens: &[Generator], pgens: &[Generator], oper: u16) -> Option<i16> {
generator_amount(igens, oper)
.or_else(|| generator_amount(pgens, oper))
.map(|v| v as i16)
}
Self {
delay_tc: pick(igens, pgens, GEN_DELAY_VOL_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
attack_tc: pick(igens, pgens, GEN_ATTACK_VOL_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
hold_tc: pick(igens, pgens, GEN_HOLD_VOL_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
decay_tc: pick(igens, pgens, GEN_DECAY_VOL_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
sustain_cb: pick(igens, pgens, GEN_SUSTAIN_VOL_ENV)
.map(|v| v as i32)
.unwrap_or(0),
release_tc: pick(igens, pgens, GEN_RELEASE_VOL_ENV)
.map(i32::from)
.unwrap_or(i32::MIN),
}
}
}
pub fn timecents_to_seconds(tc: i32, fallback: f32) -> f32 {
if tc == i32::MIN {
return fallback;
}
if tc <= -12000 {
return fallback.min(0.001);
}
(2.0f64).powf(tc as f64 / 1200.0) as f32
}
pub fn centibels_to_gain(cb: i32) -> f32 {
let clamped = cb.clamp(0, 1440) as f32;
(10.0f32).powf(-clamped / 200.0)
}
impl SamplePlan {
fn from_zones(
sample: &SampleHeader,
igens: &[Generator],
pgens: &[Generator],
target_key: u8,
) -> Self {
let s_off = signed_offset(igens, GEN_START_OFFSET);
let e_off = signed_offset(igens, GEN_END_OFFSET);
let sl_off = signed_offset(igens, GEN_STARTLOOP_OFFSET);
let el_off = signed_offset(igens, GEN_ENDLOOP_OFFSET);
let start = (sample.start as i64 + s_off as i64).clamp(0, sample.end as i64) as u32;
let end = (sample.end as i64 + e_off as i64).clamp(start as i64, sample.end as i64 + 32_768)
as u32;
let start_loop =
(sample.start_loop as i64 + sl_off as i64).clamp(start as i64, end as i64) as u32;
let end_loop =
(sample.end_loop as i64 + el_off as i64).clamp(start_loop as i64, end as i64) as u32;
let mode = generator_amount(igens, GEN_SAMPLE_MODES)
.or_else(|| generator_amount(pgens, GEN_SAMPLE_MODES))
.unwrap_or(0);
let loops = mode == 1 || mode == 3;
let root = generator_amount(igens, GEN_OVERRIDING_ROOT_KEY)
.map(|v| v as u8)
.unwrap_or(sample.original_key)
.min(127);
let coarse = signed_amount(igens, GEN_COARSE_TUNE) as i32
+ signed_amount(pgens, GEN_COARSE_TUNE) as i32;
let fine = signed_amount(igens, GEN_FINE_TUNE) as i32
+ signed_amount(pgens, GEN_FINE_TUNE) as i32
+ sample.pitch_correction as i32;
let semitones = target_key as i32 - root as i32 + coarse;
let pitch_ratio = (2f64).powf(semitones as f64 / 12.0) * (2f64).powf(fine as f64 / 1200.0);
let env = EnvParams::from_generators(igens, pgens);
let mod_env = ModEnvParams::from_generators(igens, pgens);
let initial_attenuation_cb = signed_amount(igens, GEN_INITIAL_ATTENUATION) as i32
+ signed_amount(pgens, GEN_INITIAL_ATTENUATION) as i32;
let mod_env_to_pitch_cents = signed_amount(igens, GEN_MOD_ENV_TO_PITCH) as i32
+ signed_amount(pgens, GEN_MOD_ENV_TO_PITCH) as i32;
let mod_env_to_filter_cents = signed_amount(igens, GEN_MOD_ENV_TO_FILTER_FC) as i32
+ signed_amount(pgens, GEN_MOD_ENV_TO_FILTER_FC) as i32;
let initial_filter_fc_cents = generator_amount(igens, GEN_INITIAL_FILTER_FC)
.or_else(|| generator_amount(pgens, GEN_INITIAL_FILTER_FC))
.map(|v| v as i32)
.unwrap_or(13_500);
let initial_filter_q_cb = signed_amount(igens, GEN_INITIAL_FILTER_Q) as i32
+ signed_amount(pgens, GEN_INITIAL_FILTER_Q) as i32;
let exclusive_class = generator_amount(igens, GEN_EXCLUSIVE_CLASS).unwrap_or(0);
Self {
start,
end,
start_loop,
end_loop,
sample_rate: sample.sample_rate.max(1),
loops,
pitch_ratio,
semitones,
fine_cents: fine,
env,
mod_env,
mod_env_to_pitch_cents,
mod_env_to_filter_cents,
initial_filter_fc_cents,
initial_filter_q_cb,
initial_attenuation_cb,
exclusive_class,
stereo_pair: None,
}
}
}
fn key_range(gens: &[Generator]) -> Option<(u8, u8)> {
gens.iter().find(|g| g.oper == GEN_KEY_RANGE).map(|g| {
let (lo, hi) = g.amount_lo_hi();
(lo.min(127), hi.min(127).max(lo))
})
}
fn vel_range(gens: &[Generator]) -> Option<(u8, u8)> {
gens.iter().find(|g| g.oper == GEN_VEL_RANGE).map(|g| {
let (lo, hi) = g.amount_lo_hi();
(lo.min(127), hi.min(127).max(lo))
})
}
fn generator_amount(gens: &[Generator], oper: u16) -> Option<u16> {
gens.iter().rev().find(|g| g.oper == oper).map(|g| g.amount)
}
fn signed_amount(gens: &[Generator], oper: u16) -> i16 {
gens.iter()
.rev()
.find(|g| g.oper == oper)
.map(|g| g.amount_i16())
.unwrap_or(0)
}
fn signed_offset(gens: &[Generator], oper: u16) -> i16 {
signed_amount(gens, oper)
}
fn parse_info(body: &[u8], info: &mut Sf2Info) -> Result<()> {
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
match &tag {
b"ifil" if payload.len() >= 4 => {
let major = u16::from_le_bytes([payload[0], payload[1]]);
let minor = u16::from_le_bytes([payload[2], payload[3]]);
info.version = Some((major, minor));
}
b"INAM" => info.name = Some(zstring(payload)),
b"isng" => info.engine = Some(zstring(payload)),
_ => {}
}
}
Ok(())
}
fn zstring(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
fn parse_sdta(body: &[u8]) -> Result<(&[u8], &[u8])> {
let mut c = Cursor::new(body);
let mut smpl: &[u8] = &[];
let mut sm24: &[u8] = &[];
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
match &tag {
b"smpl" => smpl = payload,
b"sm24" => sm24 = payload,
_ => { }
}
}
Ok((smpl, sm24))
}
struct Pdta {
phdr: Vec<PresetHeader>,
pbag: Vec<Bag>,
pgen: Vec<Generator>,
inst: Vec<InstrumentHeader>,
ibag: Vec<Bag>,
igen: Vec<Generator>,
shdr: Vec<SampleHeader>,
}
const PHDR_RECORD: usize = 38;
const PBAG_RECORD: usize = 4;
const PMOD_RECORD: usize = 10;
const PGEN_RECORD: usize = 4;
const INST_RECORD: usize = 22;
#[allow(dead_code)]
const IBAG_RECORD: usize = 4;
const IMOD_RECORD: usize = 10;
#[allow(dead_code)]
const IGEN_RECORD: usize = 4;
const SHDR_RECORD: usize = 46;
impl Pdta {
fn parse(body: &[u8]) -> Result<Self> {
let mut c = Cursor::new(body);
let mut phdr_raw: &[u8] = &[];
let mut pbag_raw: &[u8] = &[];
let mut pgen_raw: &[u8] = &[];
let mut inst_raw: &[u8] = &[];
let mut ibag_raw: &[u8] = &[];
let mut igen_raw: &[u8] = &[];
let mut shdr_raw: &[u8] = &[];
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
match &tag {
b"phdr" => phdr_raw = payload,
b"pbag" => pbag_raw = payload,
b"pmod" => check_record(payload, PMOD_RECORD, "pmod")?,
b"pgen" => pgen_raw = payload,
b"inst" => inst_raw = payload,
b"ibag" => ibag_raw = payload,
b"imod" => check_record(payload, IMOD_RECORD, "imod")?,
b"igen" => igen_raw = payload,
b"shdr" => shdr_raw = payload,
_ => { }
}
}
let phdr = parse_phdr(phdr_raw)?;
let pbag = parse_bag(pbag_raw, "pbag")?;
let pgen = parse_gen(pgen_raw, "pgen")?;
let inst = parse_inst(inst_raw)?;
let ibag = parse_bag(ibag_raw, "ibag")?;
let igen = parse_gen(igen_raw, "igen")?;
let shdr = parse_shdr(shdr_raw)?;
for (i, p) in phdr.iter().enumerate() {
if (p.pbag_start as usize) >= pbag.len() {
return Err(Error::invalid(format!(
"SF2: phdr[{i}] '{}' pbag_start {} >= pbag.len() {}",
p.name,
p.pbag_start,
pbag.len(),
)));
}
}
for (i, b) in pbag.iter().enumerate() {
if (b.gen_start as usize) >= pgen.len() && !pgen.is_empty() {
return Err(Error::invalid(format!(
"SF2: pbag[{i}] gen_start {} >= pgen.len() {}",
b.gen_start,
pgen.len(),
)));
}
}
for (i, inst_h) in inst.iter().enumerate() {
if (inst_h.ibag_start as usize) >= ibag.len() {
return Err(Error::invalid(format!(
"SF2: inst[{i}] '{}' ibag_start {} >= ibag.len() {}",
inst_h.name,
inst_h.ibag_start,
ibag.len(),
)));
}
}
for (i, b) in ibag.iter().enumerate() {
if (b.gen_start as usize) >= igen.len() && !igen.is_empty() {
return Err(Error::invalid(format!(
"SF2: ibag[{i}] gen_start {} >= igen.len() {}",
b.gen_start,
igen.len(),
)));
}
}
Ok(Self {
phdr,
pbag,
pgen,
inst,
ibag,
igen,
shdr,
})
}
}
fn check_record(body: &[u8], record_size: usize, what: &str) -> Result<()> {
if body.len() % record_size != 0 {
return Err(Error::invalid(format!(
"SF2: '{what}' length {} is not a multiple of {record_size}",
body.len(),
)));
}
let n = body.len() / record_size;
if n > MAX_PDTA_RECORDS {
return Err(Error::invalid(format!(
"SF2: '{what}' record count {n} exceeds cap {MAX_PDTA_RECORDS}",
)));
}
Ok(())
}
fn parse_phdr(body: &[u8]) -> Result<Vec<PresetHeader>> {
check_record(body, PHDR_RECORD, "phdr")?;
let n = body.len() / PHDR_RECORD;
if n == 0 {
return Err(Error::invalid(
"SF2: 'phdr' is empty (need at least the EOP sentinel)",
));
}
let mut out = Vec::with_capacity(n);
for i in 0..n {
let r = &body[i * PHDR_RECORD..(i + 1) * PHDR_RECORD];
let name = zstring(&r[0..20]);
let program = u16::from_le_bytes([r[20], r[21]]);
let bank = u16::from_le_bytes([r[22], r[23]]);
let pbag_start = u16::from_le_bytes([r[24], r[25]]);
out.push(PresetHeader {
name,
program,
bank,
pbag_start,
});
}
out.pop();
Ok(out)
}
fn parse_inst(body: &[u8]) -> Result<Vec<InstrumentHeader>> {
check_record(body, INST_RECORD, "inst")?;
let n = body.len() / INST_RECORD;
if n == 0 {
return Err(Error::invalid("SF2: 'inst' is empty"));
}
let mut out = Vec::with_capacity(n);
for i in 0..n {
let r = &body[i * INST_RECORD..(i + 1) * INST_RECORD];
let name = zstring(&r[0..20]);
let ibag_start = u16::from_le_bytes([r[20], r[21]]);
out.push(InstrumentHeader { name, ibag_start });
}
out.pop(); Ok(out)
}
fn parse_bag(body: &[u8], what: &str) -> Result<Vec<Bag>> {
check_record(body, PBAG_RECORD, what)?;
let n = body.len() / PBAG_RECORD;
if n == 0 {
return Err(Error::invalid(format!("SF2: '{what}' is empty")));
}
let mut out = Vec::with_capacity(n);
for i in 0..n {
let r = &body[i * PBAG_RECORD..(i + 1) * PBAG_RECORD];
out.push(Bag {
gen_start: u16::from_le_bytes([r[0], r[1]]),
mod_start: u16::from_le_bytes([r[2], r[3]]),
});
}
Ok(out)
}
fn parse_gen(body: &[u8], what: &str) -> Result<Vec<Generator>> {
check_record(body, PGEN_RECORD, what)?;
let n = body.len() / PGEN_RECORD;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let r = &body[i * PGEN_RECORD..(i + 1) * PGEN_RECORD];
out.push(Generator {
oper: u16::from_le_bytes([r[0], r[1]]),
amount: u16::from_le_bytes([r[2], r[3]]),
});
}
Ok(out)
}
fn parse_shdr(body: &[u8]) -> Result<Vec<SampleHeader>> {
check_record(body, SHDR_RECORD, "shdr")?;
let n = body.len() / SHDR_RECORD;
if n == 0 {
return Err(Error::invalid("SF2: 'shdr' is empty"));
}
let mut out = Vec::with_capacity(n);
for i in 0..n {
let r = &body[i * SHDR_RECORD..(i + 1) * SHDR_RECORD];
let name = zstring(&r[0..20]);
let start = u32::from_le_bytes(r[20..24].try_into().unwrap());
let end = u32::from_le_bytes(r[24..28].try_into().unwrap());
let start_loop = u32::from_le_bytes(r[28..32].try_into().unwrap());
let end_loop = u32::from_le_bytes(r[32..36].try_into().unwrap());
let sample_rate = u32::from_le_bytes(r[36..40].try_into().unwrap());
let original_key = r[40];
let pitch_correction = r[41] as i8;
let sample_link = u16::from_le_bytes([r[42], r[43]]);
let sample_type = u16::from_le_bytes([r[44], r[45]]);
out.push(SampleHeader {
name,
start,
end,
start_loop,
end_loop,
sample_rate,
original_key: original_key.min(127),
pitch_correction,
sample_link,
sample_type,
});
}
out.pop(); Ok(out)
}
pub struct Sf2Voice {
sample_data: Arc<[i32]>,
#[allow(dead_code)]
start: u32,
end: u32,
start_loop: u32,
end_loop: u32,
loops: bool,
phase: f64,
stereo: Option<StereoState>,
base_phase_inc: f64,
phase_inc: f64,
amplitude: f32,
pressure_gain: f32,
pitch_bend_cents: i32,
elapsed: u32,
release_pos: Option<u32>,
release_start_level: f32,
delay_samples: u32,
attack_samples: u32,
hold_samples: u32,
decay_samples: u32,
release_samples: u32,
sustain_level: f32,
done: bool,
mod_env_delay: u32,
mod_env_attack: u32,
mod_env_hold: u32,
mod_env_decay: u32,
mod_env_release: u32,
mod_env_sustain_level: f32,
mod_env_release_start_level: f32,
mod_env_to_pitch_cents: i32,
mod_env_to_filter_cents: i32,
initial_filter_fc_cents: i32,
initial_filter_q_cb: i32,
filter: Option<BiquadState>,
exclusive_class: u16,
output_rate: f32,
}
struct StereoState {
end: u32,
start_loop: u32,
end_loop: u32,
phase: f64,
}
struct BiquadState {
last_cutoff_cents: i32,
a1: f32,
a2: f32,
b0: f32,
b1: f32,
b2: f32,
x1: [f32; 2],
x2: [f32; 2],
y1: [f32; 2],
y2: [f32; 2],
}
impl Sf2Voice {
fn from_plan(
sample_data: Arc<[i32]>,
plan: &SamplePlan,
velocity: u8,
output_rate: u32,
) -> Self {
let v = (velocity as f32 / 127.0).clamp(0.0, 1.0);
let attn = centibels_to_gain(plan.initial_attenuation_cb);
let amplitude = v * v * 0.5 * attn;
let phase_inc = plan.pitch_ratio * (plan.sample_rate as f64 / output_rate.max(1) as f64);
let sr = output_rate.max(1) as f32;
let delay_s = timecents_to_seconds(plan.env.delay_tc, 0.0);
let attack_s = timecents_to_seconds(plan.env.attack_tc, 0.005);
let hold_s = timecents_to_seconds(plan.env.hold_tc, 0.0);
let decay_s = timecents_to_seconds(plan.env.decay_tc, 0.100);
let release_s = timecents_to_seconds(plan.env.release_tc, 0.100);
let sustain_level = centibels_to_gain(plan.env.sustain_cb);
let mod_delay_s = timecents_to_seconds(plan.mod_env.delay_tc, 0.0);
let mod_attack_s = timecents_to_seconds(plan.mod_env.attack_tc, 0.0);
let mod_hold_s = timecents_to_seconds(plan.mod_env.hold_tc, 0.0);
let mod_decay_s = timecents_to_seconds(plan.mod_env.decay_tc, 0.0);
let mod_release_s = timecents_to_seconds(plan.mod_env.release_tc, 0.0);
let mod_sustain_level =
(1.0 - (plan.mod_env.sustain_per_mille as f32 / 1000.0)).clamp(0.0, 1.0);
let stereo = plan.stereo_pair.map(|p| StereoState {
end: p.end,
start_loop: p.start_loop,
end_loop: p.end_loop,
phase: p.start as f64,
});
let needs_filter =
plan.initial_filter_fc_cents < 13_000 || plan.mod_env_to_filter_cents.abs() > 200;
let filter = if needs_filter {
Some(BiquadState::new())
} else {
None
};
Self {
sample_data,
start: plan.start,
end: plan.end,
start_loop: plan.start_loop,
end_loop: plan.end_loop,
loops: plan.loops,
phase: plan.start as f64,
stereo,
base_phase_inc: phase_inc,
phase_inc,
amplitude,
pressure_gain: 1.0,
pitch_bend_cents: 0,
elapsed: 0,
release_pos: None,
release_start_level: 1.0,
delay_samples: (sr * delay_s) as u32,
attack_samples: (sr * attack_s).max(1.0) as u32,
hold_samples: (sr * hold_s) as u32,
decay_samples: (sr * decay_s).max(1.0) as u32,
release_samples: (sr * release_s).max(1.0) as u32,
sustain_level,
done: false,
mod_env_delay: (sr * mod_delay_s) as u32,
mod_env_attack: (sr * mod_attack_s).max(1.0) as u32,
mod_env_hold: (sr * mod_hold_s) as u32,
mod_env_decay: (sr * mod_decay_s).max(1.0) as u32,
mod_env_release: (sr * mod_release_s).max(1.0) as u32,
mod_env_sustain_level: mod_sustain_level,
mod_env_release_start_level: 1.0,
mod_env_to_pitch_cents: plan.mod_env_to_pitch_cents,
mod_env_to_filter_cents: plan.mod_env_to_filter_cents,
initial_filter_fc_cents: plan.initial_filter_fc_cents,
initial_filter_q_cb: plan.initial_filter_q_cb,
filter,
exclusive_class: plan.exclusive_class,
output_rate: sr,
}
}
fn envelope_at(&self, t: u32) -> f32 {
if let Some(rel_at) = self.release_pos {
let since = t.saturating_sub(rel_at);
if since >= self.release_samples {
return 0.0;
}
let x = since as f32 / self.release_samples.max(1) as f32;
let curve = (1.0 - x) * (1.0 - x);
return self.release_start_level * curve;
}
if t < self.delay_samples {
return 0.0;
}
let t = t - self.delay_samples;
if t < self.attack_samples {
return t as f32 / self.attack_samples.max(1) as f32;
}
let t = t - self.attack_samples;
if t < self.hold_samples {
return 1.0;
}
let t = t - self.hold_samples;
if t < self.decay_samples {
let x = t as f32 / self.decay_samples.max(1) as f32;
let drop = 1.0 - self.sustain_level;
let curve = 1.0 - (1.0 - x) * (1.0 - x);
return 1.0 - drop * curve;
}
self.sustain_level
}
fn envelope_run(&self, t0: u32, buf: &mut [f32]) {
if t0 as u64 + buf.len() as u64 > u32::MAX as u64 {
for (k, slot) in buf.iter_mut().enumerate() {
*slot = self.envelope_at(t0.wrapping_add(k as u32));
}
return;
}
if let Some(rel_at) = self.release_pos {
let den = self.release_samples.max(1) as f32;
let start = self.release_start_level;
for (k, slot) in buf.iter_mut().enumerate() {
let since = (t0 + k as u32).saturating_sub(rel_at);
*slot = if since >= self.release_samples {
0.0
} else {
let x = since as f32 / den;
start * ((1.0 - x) * (1.0 - x))
};
}
return;
}
let b_delay = self.delay_samples as u64;
let b_attack = b_delay + self.attack_samples as u64;
let b_hold = b_attack + self.hold_samples as u64;
let b_decay = b_hold + self.decay_samples as u64;
let att_den = self.attack_samples.max(1) as f32;
let dec_den = self.decay_samples.max(1) as f32;
let drop = 1.0 - self.sustain_level;
let n = buf.len();
let mut k = 0usize;
while k < n {
let t = t0 as u64 + k as u64;
if t < b_delay {
let run = (b_delay - t).min((n - k) as u64) as usize;
buf[k..k + run].fill(0.0);
k += run;
} else if t < b_attack {
let run = (b_attack - t).min((n - k) as u64) as usize;
let base = (t - b_delay) as u32;
for (j, slot) in buf[k..k + run].iter_mut().enumerate() {
*slot = (base + j as u32) as f32 / att_den;
}
k += run;
} else if t < b_hold {
let run = (b_hold - t).min((n - k) as u64) as usize;
buf[k..k + run].fill(1.0);
k += run;
} else if t < b_decay {
let run = (b_decay - t).min((n - k) as u64) as usize;
let base = (t - b_hold) as u32;
for (j, slot) in buf[k..k + run].iter_mut().enumerate() {
let x = (base + j as u32) as f32 / dec_den;
let curve = 1.0 - (1.0 - x) * (1.0 - x);
*slot = 1.0 - drop * curve;
}
k += run;
} else {
buf[k..n].fill(self.sustain_level);
k = n;
}
}
}
fn fetch(&self, phase: f64) -> f32 {
let i = phase.floor() as i64;
let frac = (phase - i as f64) as f32;
if i < 0 || (i as usize) + 1 >= self.sample_data.len() {
return 0.0;
}
let a = self.sample_data[i as usize] as f32;
let b = self.sample_data[i as usize + 1] as f32;
let mixed = a + (b - a) * frac;
mixed * (1.0 / 8_388_608.0)
}
fn mod_env_at(&self, t: u32) -> f32 {
if let Some(rel_at) = self.release_pos {
let since = t.saturating_sub(rel_at);
if since >= self.mod_env_release {
return 0.0;
}
let x = since as f32 / self.mod_env_release.max(1) as f32;
let curve = (1.0 - x) * (1.0 - x);
return self.mod_env_release_start_level * curve;
}
if t < self.mod_env_delay {
return 0.0;
}
let t = t - self.mod_env_delay;
if t < self.mod_env_attack {
return t as f32 / self.mod_env_attack.max(1) as f32;
}
let t = t - self.mod_env_attack;
if t < self.mod_env_hold {
return 1.0;
}
let t = t - self.mod_env_hold;
if t < self.mod_env_decay {
let x = t as f32 / self.mod_env_decay.max(1) as f32;
let drop = 1.0 - self.mod_env_sustain_level;
let curve = 1.0 - (1.0 - x) * (1.0 - x);
return 1.0 - drop * curve;
}
self.mod_env_sustain_level
}
fn update_filter_coeffs(&mut self, cutoff_cents: i32, output_rate: f32) {
let Some(filter) = self.filter.as_mut() else {
return;
};
let cents = cutoff_cents.clamp(1500, 13_500);
let cutoff_hz = 8.176_f32 * (2.0_f32).powf(cents as f32 / 1200.0);
let nyquist = output_rate * 0.5;
let cutoff_hz = cutoff_hz.min(nyquist * 0.99).max(20.0);
let q_lin = (10.0_f32).powf(self.initial_filter_q_cb as f32 / 200.0)
* std::f32::consts::FRAC_1_SQRT_2;
let q = q_lin.clamp(0.1, 16.0);
let omega = 2.0 * std::f32::consts::PI * cutoff_hz / output_rate;
let alpha = omega.sin() / (2.0 * q);
let cos_w = omega.cos();
let a0 = 1.0 + alpha;
let inv_a0 = 1.0 / a0;
filter.b0 = ((1.0 - cos_w) * 0.5) * inv_a0;
filter.b1 = (1.0 - cos_w) * inv_a0;
filter.b2 = ((1.0 - cos_w) * 0.5) * inv_a0;
filter.a1 = (-2.0 * cos_w) * inv_a0;
filter.a2 = (1.0 - alpha) * inv_a0;
filter.last_cutoff_cents = cutoff_cents;
}
fn filter_step(&mut self, channel: usize, x: f32) -> f32 {
let Some(filter) = self.filter.as_mut() else {
return x;
};
let y = filter.b0 * x + filter.b1 * filter.x1[channel] + filter.b2 * filter.x2[channel]
- filter.a1 * filter.y1[channel]
- filter.a2 * filter.y2[channel];
filter.x2[channel] = filter.x1[channel];
filter.x1[channel] = x;
filter.y2[channel] = filter.y1[channel];
filter.y1[channel] = y;
y
}
}
impl BiquadState {
fn new() -> Self {
Self {
last_cutoff_cents: i32::MIN,
a1: 0.0,
a2: 0.0,
b0: 1.0,
b1: 0.0,
b2: 0.0,
x1: [0.0; 2],
x2: [0.0; 2],
y1: [0.0; 2],
y2: [0.0; 2],
}
}
}
const ENV_RUN: usize = 256;
impl Voice for Sf2Voice {
fn render(&mut self, out: &mut [f32]) -> usize {
if self.done {
return 0;
}
let total = out.len();
let mut env_buf = [0.0f32; ENV_RUN];
let mut base = 0usize;
while base < total {
let n = (total - base).min(ENV_RUN);
self.envelope_run(self.elapsed, &mut env_buf[..n]);
let chunk = &mut out[base..base + n];
for (j, (slot, &env)) in chunk.iter_mut().zip(&env_buf[..n]).enumerate() {
if self.release_pos.is_some() && env <= 0.0 {
self.done = true;
return base + j;
}
let mod_lvl = if self.mod_env_to_pitch_cents != 0 || self.filter.is_some() {
self.mod_env_at(self.elapsed)
} else {
0.0
};
if self.mod_env_to_pitch_cents != 0 {
let pitch_cents = self.pitch_bend_cents
+ (mod_lvl * self.mod_env_to_pitch_cents as f32) as i32;
let bend_ratio = (2.0f64).powf(pitch_cents as f64 / 1200.0);
self.phase_inc = self.base_phase_inc * bend_ratio;
}
if self.filter.is_some() {
let target = self.initial_filter_fc_cents
+ (mod_lvl * self.mod_env_to_filter_cents as f32) as i32;
let last = self
.filter
.as_ref()
.map(|f| f.last_cutoff_cents)
.unwrap_or(i32::MIN);
if target.saturating_sub(last).saturating_abs() > 50 {
self.update_filter_coeffs(target, self.output_rate);
}
}
if self.phase >= self.end as f64 {
if self.loops {
let over = self.phase - self.end_loop as f64;
let loop_len = (self.end_loop as f64 - self.start_loop as f64).max(1.0);
let wrapped = over.rem_euclid(loop_len);
self.phase = self.start_loop as f64 + wrapped;
} else {
self.done = true;
return base + j;
}
} else if self.loops && self.phase >= self.end_loop as f64 {
let over = self.phase - self.end_loop as f64;
let loop_len = (self.end_loop as f64 - self.start_loop as f64).max(1.0);
let wrapped = over.rem_euclid(loop_len);
self.phase = self.start_loop as f64 + wrapped;
}
let mut s = self.fetch(self.phase);
if self.filter.is_some() {
s = self.filter_step(0, s);
}
*slot = s * env * self.amplitude * self.pressure_gain;
self.phase += self.phase_inc;
self.elapsed = self.elapsed.wrapping_add(1);
}
base += n;
}
total
}
fn release(&mut self) {
if self.release_pos.is_none() {
self.release_start_level = self.envelope_at(self.elapsed).max(0.0);
self.mod_env_release_start_level = self.mod_env_at(self.elapsed).max(0.0);
self.release_pos = Some(self.elapsed);
}
}
fn done(&self) -> bool {
self.done
}
fn set_pitch_bend_cents(&mut self, cents: i32) {
self.pitch_bend_cents = cents;
let bend_ratio = (2.0f64).powf(cents as f64 / 1200.0);
self.phase_inc = self.base_phase_inc * bend_ratio;
}
fn set_pressure(&mut self, pressure: f32) {
let p = pressure.clamp(0.0, 1.0);
self.pressure_gain = 1.0 + 0.5 * p; }
fn is_stereo(&self) -> bool {
self.stereo.is_some()
}
fn render_stereo(&mut self, out_l: &mut [f32], out_r: &mut [f32]) -> usize {
debug_assert_eq!(out_l.len(), out_r.len());
if self.stereo.is_none() {
let n = self.render(out_l);
out_r[..n].copy_from_slice(&out_l[..n]);
return n;
}
if self.done {
return 0;
}
for i in 0..out_l.len() {
let env = self.envelope_at(self.elapsed);
if self.release_pos.is_some() && env <= 0.0 {
self.done = true;
return i;
}
let mod_lvl = if self.mod_env_to_pitch_cents != 0 || self.filter.is_some() {
self.mod_env_at(self.elapsed)
} else {
0.0
};
if self.mod_env_to_pitch_cents != 0 {
let pitch_cents =
self.pitch_bend_cents + (mod_lvl * self.mod_env_to_pitch_cents as f32) as i32;
let bend_ratio = (2.0f64).powf(pitch_cents as f64 / 1200.0);
self.phase_inc = self.base_phase_inc * bend_ratio;
}
if self.filter.is_some() {
let target = self.initial_filter_fc_cents
+ (mod_lvl * self.mod_env_to_filter_cents as f32) as i32;
let last = self
.filter
.as_ref()
.map(|f| f.last_cutoff_cents)
.unwrap_or(i32::MIN);
if target.saturating_sub(last).saturating_abs() > 50 {
self.update_filter_coeffs(target, self.output_rate);
}
}
if self.phase >= self.end as f64 {
if self.loops {
let over = self.phase - self.end_loop as f64;
let loop_len = (self.end_loop as f64 - self.start_loop as f64).max(1.0);
self.phase = self.start_loop as f64 + over.rem_euclid(loop_len);
} else {
self.done = true;
return i;
}
} else if self.loops && self.phase >= self.end_loop as f64 {
let over = self.phase - self.end_loop as f64;
let loop_len = (self.end_loop as f64 - self.start_loop as f64).max(1.0);
self.phase = self.start_loop as f64 + over.rem_euclid(loop_len);
}
let (partner_phase_to_fetch, advanced_partner_phase) = {
let st = self.stereo.as_mut().expect("stereo voice");
if st.phase >= st.end as f64 {
if self.loops {
let over = st.phase - st.end_loop as f64;
let loop_len = (st.end_loop as f64 - st.start_loop as f64).max(1.0);
st.phase = st.start_loop as f64 + over.rem_euclid(loop_len);
} else {
self.done = true;
return i;
}
} else if self.loops && st.phase >= st.end_loop as f64 {
let over = st.phase - st.end_loop as f64;
let loop_len = (st.end_loop as f64 - st.start_loop as f64).max(1.0);
st.phase = st.start_loop as f64 + over.rem_euclid(loop_len);
}
let p = st.phase;
(p, p + self.phase_inc)
};
let mut sl = self.fetch(self.phase);
let mut sr = self.fetch(partner_phase_to_fetch);
if self.filter.is_some() {
sl = self.filter_step(0, sl);
sr = self.filter_step(1, sr);
}
let amp = env * self.amplitude * self.pressure_gain;
out_l[i] = sl * amp;
out_r[i] = sr * amp;
self.phase += self.phase_inc;
if let Some(st) = self.stereo.as_mut() {
st.phase = advanced_partner_phase;
}
self.elapsed = self.elapsed.wrapping_add(1);
}
out_l.len()
}
fn exclusive_class(&self) -> u16 {
self.exclusive_class
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_riff_sfbk_magic() {
let mut blob = vec![0u8; 32];
blob[0..4].copy_from_slice(b"RIFF");
blob[4..8].copy_from_slice(&24u32.to_le_bytes());
blob[8..12].copy_from_slice(b"sfbk");
assert!(is_sf2(&blob));
}
#[test]
fn rejects_wrong_magic() {
assert!(!is_sf2(b""));
assert!(!is_sf2(b"RIFF\x00\x00\x00\x00WAVE...."));
}
fn build_minimal_sf2() -> Vec<u8> {
let mut samples: Vec<i16> = Vec::with_capacity(20);
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16;
samples.push(v);
}
let mut smpl_bytes = Vec::with_capacity(samples.len() * 2);
for s in &samples {
smpl_bytes.extend_from_slice(&s.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes()); v.extend_from_slice(&4u16.to_le_bytes()); v
});
push_chunk(&mut info, b"INAM", b"Test Bank\0\0");
push_chunk(&mut info, b"isng", b"EMU8000\0");
let mut info_list = Vec::new();
info_list.extend_from_slice(b"INFO");
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl_bytes);
let mut sdta_list = Vec::new();
sdta_list.extend_from_slice(b"sdta");
sdta_list.extend_from_slice(&sdta);
let pdta = build_pdta();
let mut pdta_list = Vec::new();
pdta_list.extend_from_slice(b"pdta");
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::new();
body.extend_from_slice(b"sfbk");
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::new();
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
fn build_pdta() -> Vec<u8> {
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("Test Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst = Vec::new();
inst.extend_from_slice(&inst_record("Test Inst", 0));
inst.extend_from_slice(&inst_record("EOI", 1));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(1, 0));
let imod = vec![0u8; IMOD_RECORD];
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record("Ramp", 0, 20, 5, 15, 22050, 60, 0, 0, 1));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut out = Vec::new();
push_chunk(&mut out, b"phdr", &phdr);
push_chunk(&mut out, b"pbag", &pbag);
push_chunk(&mut out, b"pmod", &pmod);
push_chunk(&mut out, b"pgen", &pgen);
push_chunk(&mut out, b"inst", &inst);
push_chunk(&mut out, b"ibag", &ibag);
push_chunk(&mut out, b"imod", &imod);
push_chunk(&mut out, b"igen", &igen);
push_chunk(&mut out, b"shdr", &shdr);
out
}
fn push_chunk(out: &mut Vec<u8>, tag: &[u8; 4], payload: &[u8]) {
out.extend_from_slice(tag);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
if payload.len() % 2 == 1 {
out.push(0);
}
}
fn name20(s: &str) -> [u8; 20] {
let mut buf = [0u8; 20];
let bytes = s.as_bytes();
let n = bytes.len().min(19);
buf[..n].copy_from_slice(&bytes[..n]);
buf
}
fn phdr_record(name: &str, program: u16, bank: u16, pbag_start: u16) -> [u8; PHDR_RECORD] {
let mut r = [0u8; PHDR_RECORD];
r[0..20].copy_from_slice(&name20(name));
r[20..22].copy_from_slice(&program.to_le_bytes());
r[22..24].copy_from_slice(&bank.to_le_bytes());
r[24..26].copy_from_slice(&pbag_start.to_le_bytes());
r
}
fn inst_record(name: &str, ibag_start: u16) -> [u8; INST_RECORD] {
let mut r = [0u8; INST_RECORD];
r[0..20].copy_from_slice(&name20(name));
r[20..22].copy_from_slice(&ibag_start.to_le_bytes());
r
}
fn bag_record(gen_start: u16, mod_start: u16) -> [u8; PBAG_RECORD] {
let mut r = [0u8; PBAG_RECORD];
r[0..2].copy_from_slice(&gen_start.to_le_bytes());
r[2..4].copy_from_slice(&mod_start.to_le_bytes());
r
}
fn gen_record(oper: u16, amount: u16) -> [u8; PGEN_RECORD] {
let mut r = [0u8; PGEN_RECORD];
r[0..2].copy_from_slice(&oper.to_le_bytes());
r[2..4].copy_from_slice(&amount.to_le_bytes());
r
}
#[allow(clippy::too_many_arguments)]
fn shdr_record(
name: &str,
start: u32,
end: u32,
start_loop: u32,
end_loop: u32,
sample_rate: u32,
original_key: u8,
pitch_correction: i8,
sample_link: u16,
sample_type: u16,
) -> [u8; SHDR_RECORD] {
let mut r = [0u8; SHDR_RECORD];
r[0..20].copy_from_slice(&name20(name));
r[20..24].copy_from_slice(&start.to_le_bytes());
r[24..28].copy_from_slice(&end.to_le_bytes());
r[28..32].copy_from_slice(&start_loop.to_le_bytes());
r[32..36].copy_from_slice(&end_loop.to_le_bytes());
r[36..40].copy_from_slice(&sample_rate.to_le_bytes());
r[40] = original_key;
r[41] = pitch_correction as u8;
r[42..44].copy_from_slice(&sample_link.to_le_bytes());
r[44..46].copy_from_slice(&sample_type.to_le_bytes());
r
}
#[test]
fn parse_minimal_sf2() {
let blob = build_minimal_sf2();
let bank = Sf2Bank::parse(&blob).expect("parse");
assert_eq!(bank.presets.len(), 1);
assert_eq!(bank.presets[0].name, "Test Preset");
assert_eq!(bank.instruments.len(), 1);
assert_eq!(bank.samples.len(), 1);
assert_eq!(bank.samples[0].name, "Ramp");
assert_eq!(bank.samples[0].sample_rate, 22050);
assert_eq!(bank.samples[0].original_key, 60);
assert_eq!(bank.sample_data.len(), 20);
assert_eq!(bank.info.name.as_deref(), Some("Test Bank"));
assert_eq!(bank.info.version, Some((2, 4)));
}
#[test]
fn resolve_finds_sample_for_program_zero() {
let blob = build_minimal_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 60, 100).expect("resolve");
assert_eq!(plan.start, 0);
assert_eq!(plan.end, 20);
assert!((plan.pitch_ratio - 1.0).abs() < 1e-12);
assert!(!plan.loops);
}
#[test]
fn resolve_pitch_octave_up() {
let blob = build_minimal_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 72, 100).unwrap();
assert!((plan.pitch_ratio - 2.0).abs() < 1e-9);
}
#[test]
fn voice_renders_pcm_at_native_rate() {
let blob = build_minimal_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 127, 22_050).unwrap();
let mut out = vec![0.0f32; 20];
let n = voice.render(&mut out);
assert_eq!(n, 20);
let nonzero = out.iter().filter(|s| s.abs() > 0.0).count();
assert!(nonzero > 5, "expected non-silent output, got {nonzero}");
}
#[test]
fn voice_finishes_after_release() {
let blob = build_minimal_looping_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 100, 48_000).unwrap();
let mut buf = [0.0f32; 256];
voice.render(&mut buf);
voice.release();
let mut total = 0;
for _ in 0..50 {
let n = voice.render(&mut buf);
total += n;
if voice.done() {
break;
}
}
assert!(voice.done(), "voice should be done after release");
assert!(total > 0);
}
fn build_minimal_looping_sf2() -> Vec<u8> {
let mut samples: Vec<i16> = Vec::with_capacity(20);
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16;
samples.push(v);
}
let mut smpl_bytes = Vec::with_capacity(samples.len() * 2);
for s in &samples {
smpl_bytes.extend_from_slice(&s.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
push_chunk(&mut info, b"INAM", b"Test Bank\0\0");
let mut info_list = Vec::new();
info_list.extend_from_slice(b"INFO");
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl_bytes);
let mut sdta_list = Vec::new();
sdta_list.extend_from_slice(b"sdta");
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("Test Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("Test Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(2, 0));
let imod = vec![0u8; IMOD_RECORD];
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_SAMPLE_MODES, 1));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record("RampLoop", 0, 20, 5, 15, 22050, 60, 0, 0, 1));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::new();
pdta_list.extend_from_slice(b"pdta");
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::new();
body.extend_from_slice(b"sfbk");
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::new();
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn looping_voice_keeps_producing_audio() {
let blob = build_minimal_looping_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 60, 100).unwrap();
assert!(plan.loops, "sampleModes=1 should set loops=true");
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 127, 22_050).unwrap();
let mut buf = [0.0f32; 4096];
let n = voice.render(&mut buf);
assert_eq!(n, 4096, "looping voice should fill the whole buffer");
assert!(!voice.done(), "looping voice must not finish on its own");
}
#[test]
fn voice_runs_off_end_of_non_looping_sample() {
let blob = build_minimal_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 100, 1).unwrap();
let mut buf = [0.0f32; 64];
let _ = voice.render(&mut buf);
let mut got_done = false;
for _ in 0..4 {
let n = voice.render(&mut buf);
if n == 0 || voice.done() {
got_done = true;
break;
}
}
assert!(
got_done,
"voice should be done after walking off the sample"
);
}
#[test]
fn rejects_truncated_riff() {
let mut blob = build_minimal_sf2();
blob.truncate(20);
let err = Sf2Bank::parse(&blob).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn rejects_chunk_size_overflow() {
let mut blob = Vec::new();
blob.extend_from_slice(b"RIFF");
blob.extend_from_slice(&100u32.to_le_bytes());
blob.extend_from_slice(b"sfbk");
blob.extend_from_slice(b"LIST");
blob.extend_from_slice(&(1_000_000_000u32).to_le_bytes());
let err = Sf2Bank::parse(&blob).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn rejects_phdr_with_wrong_record_size() {
let body = vec![0u8; 19];
let err = parse_phdr(&body).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn rejects_oversized_sample_block() {
let mut blob = build_minimal_sf2();
Sf2Bank::parse(&blob).unwrap();
blob.push(0);
let _ = Sf2Bank::parse(&blob); }
#[test]
fn name_truncation_at_nul() {
assert_eq!(zstring(b"hi\0\0\0"), "hi");
assert_eq!(zstring(b"abc"), "abc");
assert_eq!(zstring(b""), "");
}
#[test]
fn timecents_to_seconds_round_trip() {
assert!((timecents_to_seconds(0, 0.0) - 1.0).abs() < 1e-6);
assert!((timecents_to_seconds(1200, 0.0) - 2.0).abs() < 1e-4);
assert!((timecents_to_seconds(-1200, 0.0) - 0.5).abs() < 1e-4);
assert!((timecents_to_seconds(i32::MIN, 0.05) - 0.05).abs() < 1e-9);
}
#[test]
fn centibels_to_gain_known_values() {
assert!((centibels_to_gain(0) - 1.0).abs() < 1e-6);
assert!((centibels_to_gain(200) - 0.1).abs() < 1e-3);
assert!(centibels_to_gain(1000) < 1e-4);
}
#[test]
fn voice_pitch_bend_changes_phase_inc() {
let blob = build_minimal_looping_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 100, 22_050).unwrap();
let mut buf = vec![0.0f32; 1024];
voice.render(&mut buf);
let energy_centre: f32 = buf.iter().map(|s| s * s).sum();
voice.set_pitch_bend_cents(1200);
let mut buf2 = vec![0.0f32; 1024];
voice.render(&mut buf2);
let energy_bent: f32 = buf2.iter().map(|s| s * s).sum();
assert!(energy_bent > 0.001, "bent voice silent: {energy_bent}");
assert!(
energy_centre > 0.001,
"centre voice silent: {energy_centre}",
);
}
#[test]
fn voice_pressure_boosts_amplitude() {
let blob = build_minimal_looping_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut a = inst.make_voice(0, 60, 100, 22_050).unwrap();
let mut b = inst.make_voice(0, 60, 100, 22_050).unwrap();
b.set_pressure(1.0);
let mut buf_a = vec![0.0f32; 1024];
let mut buf_b = vec![0.0f32; 1024];
a.render(&mut buf_a);
b.render(&mut buf_b);
let peak_a: f32 = buf_a[200..1000].iter().map(|s| s.abs()).fold(0.0, f32::max);
let peak_b: f32 = buf_b[200..1000].iter().map(|s| s.abs()).fold(0.0, f32::max);
assert!(
peak_b > peak_a * 1.2,
"pressure didn't boost: a={peak_a}, b={peak_b}"
);
}
#[test]
fn envelope_release_starts_from_current_level() {
let blob = build_minimal_looping_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 127, 48_000).unwrap();
let mut buf = vec![0.0f32; 16];
voice.render(&mut buf);
voice.release();
let mut total = 0;
let mut peak: f32 = 0.0;
for _ in 0..1024 {
let n = voice.render(&mut buf);
for s in &buf[..n] {
peak = peak.max(s.abs());
}
total += n;
if voice.done() {
break;
}
}
assert!(voice.done(), "voice should finish after release");
assert!(
peak < 0.5,
"release start must not jump to full amplitude: peak={peak}",
);
assert!(total > 0);
}
#[test]
fn envelope_full_dahdsr_overrides_via_generators() {
let blob = build_envelope_override_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "test".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 127, 44_100).unwrap();
let mut buf = vec![0.0f32; 4096];
voice.render(&mut buf);
let early = buf[80..120].iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let late = buf[2400..2600]
.iter()
.map(|s| s.abs())
.fold(0.0f32, f32::max);
assert!(
late > early * 2.0,
"long attack should ramp gradually: early={early}, late={late}",
);
}
fn build_envelope_override_sf2() -> Vec<u8> {
let mut samples: Vec<i16> = Vec::with_capacity(20);
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16;
samples.push(v);
}
let mut smpl_bytes = Vec::with_capacity(samples.len() * 2);
for s in &samples {
smpl_bytes.extend_from_slice(&s.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::new();
info_list.extend_from_slice(b"INFO");
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl_bytes);
let mut sdta_list = Vec::new();
sdta_list.extend_from_slice(b"sdta");
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("Test Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("Test Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(4, 0));
let imod = vec![0u8; IMOD_RECORD];
let attack_tc: i16 = -4660;
let sustain_cb: i16 = 200;
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_ATTACK_VOL_ENV, attack_tc as u16));
igen.extend_from_slice(&gen_record(GEN_SUSTAIN_VOL_ENV, sustain_cb as u16));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_MODES, 1));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record("RampLoop", 0, 20, 5, 15, 22050, 60, 0, 0, 1));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::new();
pdta_list.extend_from_slice(b"pdta");
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::new();
body.extend_from_slice(b"sfbk");
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::new();
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn generator_amount_signed_decoding() {
let g = Generator {
oper: GEN_FINE_TUNE,
amount: 0xFFFF,
};
assert_eq!(g.amount_i16(), -1);
let (lo, hi) = Generator {
oper: GEN_KEY_RANGE,
amount: 0x4321,
}
.amount_lo_hi();
assert_eq!((lo, hi), (0x21, 0x43));
}
fn build_sm24_sf2() -> Vec<u8> {
let mut smpl: Vec<u8> = Vec::with_capacity(40);
let mut sm24: Vec<u8> = Vec::with_capacity(20);
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16;
smpl.extend_from_slice(&v.to_le_bytes());
sm24.push(0x10 + i as u8);
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
push_chunk(&mut sdta, b"sm24", &sm24);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let pdta = build_pdta();
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn sm24_combines_into_24_bit_samples() {
let blob = build_sm24_sf2();
let bank = Sf2Bank::parse(&blob).expect("parse sm24 fixture");
let v0 = bank.sample_data[0];
let expected_v0 = ((-8000_i32) << 8) | 0x10;
assert_eq!(v0, expected_v0, "sm24 lower byte not combined: got {v0:#X}");
let v5 = bank.sample_data[5];
let expected_v5 = ((-4000_i32) << 8) | 0x15;
assert_eq!(v5, expected_v5);
}
#[test]
fn missing_sm24_falls_back_to_16_bit() {
let blob = build_minimal_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
for (i, &v) in bank.sample_data.iter().enumerate() {
let i16_val = (i as i32) * 800 - 8000;
assert_eq!(
v,
i16_val << 8,
"frame {i}: i16 {i16_val} did not widen cleanly: got {v:#X}",
);
assert_eq!(v & 0xFF, 0, "lower byte must be 0 without sm24");
}
}
#[test]
fn sm24_chunk_with_wrong_length_is_silently_ignored() {
let blob = build_sm24_with_short_sm24(10);
let bank = Sf2Bank::parse(&blob).expect("parse should not reject");
for &v in bank.sample_data.iter() {
assert_eq!(v & 0xFF, 0, "wrong-length sm24 should be ignored");
}
}
fn build_sm24_with_short_sm24(sm24_frames: usize) -> Vec<u8> {
let mut smpl: Vec<u8> = Vec::new();
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16;
smpl.extend_from_slice(&v.to_le_bytes());
}
let sm24: Vec<u8> = (0..sm24_frames).map(|i| 0x10 + i as u8).collect();
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
push_chunk(&mut sdta, b"sm24", &sm24);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let pdta = build_pdta();
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
fn build_stereo_sf2() -> Vec<u8> {
let mut smpl: Vec<u8> = Vec::with_capacity(80);
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16; smpl.extend_from_slice(&v.to_le_bytes());
}
for i in 0i32..20 {
let v = -(i * 800 - 8000) as i16; smpl.extend_from_slice(&v.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("Stereo Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("Stereo Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(2, 0));
let imod = vec![0u8; IMOD_RECORD];
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_SAMPLE_MODES, 1));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record(
"Left",
0,
20,
5,
15,
22050,
60,
0,
1,
sample_type_bits::LEFT,
));
shdr.extend_from_slice(&shdr_record(
"Right",
20,
40,
25,
35,
22050,
60,
0,
0,
sample_type_bits::RIGHT,
));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn stereo_sf2_resolves_with_partner() {
let blob = build_stereo_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
assert_eq!(bank.samples.len(), 2);
assert_eq!(bank.samples[0].sample_type, sample_type_bits::LEFT);
assert_eq!(bank.samples[1].sample_type, sample_type_bits::RIGHT);
let plan = bank.resolve(0, 60, 100).expect("resolve");
let pair = plan.stereo_pair.expect("stereo pair must be linked");
assert_eq!(pair.start, 20);
assert_eq!(pair.end, 40);
}
#[test]
fn stereo_voice_writes_distinct_l_r() {
let blob = build_stereo_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "stereo".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 127, 22_050).unwrap();
assert!(voice.is_stereo(), "voice should report is_stereo=true");
let mut l = vec![0.0f32; 1024];
let mut r = vec![0.0f32; 1024];
let n = voice.render_stereo(&mut l, &mut r);
assert_eq!(n, 1024);
let mut differing = 0;
for i in 200..1000 {
if (l[i] - r[i]).abs() > 0.001 {
differing += 1;
}
}
assert!(
differing > 100,
"L/R should differ from each other; got {differing} differing samples",
);
}
#[test]
fn stereo_voice_is_routed_through_mixer_stereo_path() {
use crate::mixer::Mixer;
let blob = build_stereo_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "stereo".into(),
bank,
};
let voice = inst.make_voice(0, 60, 127, 22_050).unwrap();
let mut mixer = Mixer::new();
mixer.note_on(0, 60, 127, voice);
let mut l = vec![0.0f32; 1024];
let mut r = vec![0.0f32; 1024];
let active = mixer.mix_stereo(&mut l, &mut r);
assert_eq!(active, 1);
let l_energy: f32 = l.iter().map(|s| s * s).sum();
let r_energy: f32 = r.iter().map(|s| s * s).sum();
assert!(l_energy > 0.0, "left silent");
assert!(r_energy > 0.0, "right silent");
}
#[test]
fn stereo_link_self_reference_falls_back_to_mono() {
let blob = build_self_linked_stereo_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 60, 100).unwrap();
assert!(plan.stereo_pair.is_none(), "self-link must not pair");
}
fn build_self_linked_stereo_sf2() -> Vec<u8> {
let mut blob = build_stereo_sf2();
let mut smpl: Vec<u8> = Vec::with_capacity(80);
for i in 0i32..40 {
let v = (i * 200 - 4000) as i16;
smpl.extend_from_slice(&v.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("Self Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("Self Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(2, 0));
let imod = vec![0u8; IMOD_RECORD];
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_SAMPLE_MODES, 1));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record(
"Self",
0,
20,
5,
15,
22050,
60,
0,
0,
sample_type_bits::LEFT,
));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
blob.clear();
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
fn build_filter_sf2() -> Vec<u8> {
let mut smpl: Vec<u8> = Vec::with_capacity(80);
for i in 0i32..40 {
let v = if i % 2 == 0 { 16000_i16 } else { -16000_i16 };
smpl.extend_from_slice(&v.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("Filter Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("Filter Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(4, 0));
let imod = vec![0u8; IMOD_RECORD];
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_INITIAL_FILTER_FC, 6500));
igen.extend_from_slice(&gen_record(GEN_INITIAL_FILTER_Q, 0));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_MODES, 1));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record("FilterRamp", 0, 40, 0, 40, 44100, 60, 0, 0, 1));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn filter_attenuates_high_frequency() {
let blob_filt = build_filter_sf2();
let bank_filt = Sf2Bank::parse(&blob_filt).unwrap();
let plan = bank_filt.resolve(0, 60, 100).unwrap();
assert_eq!(plan.initial_filter_fc_cents, 6500);
let inst = Sf2Instrument {
name: "filt".into(),
bank: bank_filt,
};
let mut voice = inst.make_voice(0, 60, 127, 44_100).unwrap();
let mut buf = vec![0.0f32; 1024];
voice.render(&mut buf);
voice.render(&mut buf);
let peak: f32 = buf[200..1000].iter().map(|s| s.abs()).fold(0.0, f32::max);
assert!(
peak < 0.05,
"filter should attenuate HF: peak {peak} (expected < 0.05)",
);
}
#[test]
fn filter_default_cutoff_is_bypass() {
let blob = build_minimal_looping_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 60, 100).unwrap();
assert_eq!(plan.initial_filter_fc_cents, 13_500);
}
fn build_mod_env_sf2() -> Vec<u8> {
let mut smpl: Vec<u8> = Vec::with_capacity(80);
for i in 0i32..40 {
let v = if i % 2 == 0 { 16000_i16 } else { -16000_i16 };
smpl.extend_from_slice(&v.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("ModEnv Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("ModEnv Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(6, 0));
let imod = vec![0u8; IMOD_RECORD];
let attack_tc: i16 = -4660;
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_INITIAL_FILTER_FC, 4500));
igen.extend_from_slice(&gen_record(GEN_MOD_ENV_TO_FILTER_FC, 6000));
igen.extend_from_slice(&gen_record(GEN_ATTACK_MOD_ENV, attack_tc as u16));
igen.extend_from_slice(&gen_record(GEN_SUSTAIN_MOD_ENV, 0)); igen.extend_from_slice(&gen_record(GEN_SAMPLE_MODES, 1));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record(
"ModEnvSquare",
0,
40,
0,
40,
44100,
60,
0,
0,
1,
));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn mod_env_to_filter_routes_correctly() {
let blob = build_mod_env_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 60, 100).unwrap();
assert_eq!(plan.initial_filter_fc_cents, 4500);
assert_eq!(plan.mod_env_to_filter_cents, 6000);
let attack_s = timecents_to_seconds(plan.mod_env.attack_tc, 0.0);
assert!(
(0.04..0.07).contains(&attack_s),
"mod-env attack should be ~50 ms, got {attack_s}",
);
}
#[test]
fn mod_env_brightens_filter_over_attack() {
let blob = build_mod_env_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "modenv".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 127, 44_100).unwrap();
let mut buf = vec![0.0f32; 4096];
voice.render(&mut buf);
let early: f32 = buf[300..600].iter().map(|s| s.abs()).fold(0.0, f32::max);
let late: f32 = buf[3000..3500].iter().map(|s| s.abs()).fold(0.0, f32::max);
assert!(
late > early * 1.5,
"mod-env should brighten the filter: early={early}, late={late}",
);
}
fn build_exclusive_class_sf2() -> Vec<u8> {
let mut smpl: Vec<u8> = Vec::with_capacity(80);
for i in 0i32..40 {
let v = (i * 400 - 8000) as i16;
smpl.extend_from_slice(&v.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("ExClass Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("ExClass Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(3, 0));
let imod = vec![0u8; IMOD_RECORD];
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_EXCLUSIVE_CLASS, 7));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_MODES, 1));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record("Drum", 0, 40, 0, 40, 22050, 60, 0, 0, 1));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn exclusive_class_propagates_to_voice() {
let blob = build_exclusive_class_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 60, 100).unwrap();
assert_eq!(plan.exclusive_class, 7);
let inst = Sf2Instrument {
name: "ex".into(),
bank,
};
let voice = inst.make_voice(0, 60, 127, 22_050).unwrap();
assert_eq!(voice.exclusive_class(), 7);
}
#[test]
fn exclusive_class_cuts_prior_voice_in_same_class() {
use crate::mixer::Mixer;
let blob = build_exclusive_class_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let inst = Sf2Instrument {
name: "ex".into(),
bank,
};
let mut mixer = Mixer::new();
let v1 = inst.make_voice(0, 60, 127, 22_050).unwrap();
mixer.note_on(0, 60, 127, v1);
assert_eq!(mixer.live_voice_count(), 1);
let v2 = inst.make_voice(0, 64, 127, 22_050).unwrap();
mixer.note_on(0, 64, 127, v2);
assert_eq!(
mixer.live_voice_count(),
1,
"second exclusive-class note must cut the first",
);
}
#[test]
fn overriding_root_key_changes_pitch_ratio() {
let blob = build_overriding_root_key_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let plan = bank.resolve(0, 60, 100).unwrap();
assert!(
(plan.pitch_ratio - 0.5).abs() < 1e-9,
"expected ratio 0.5 with overriding_root_key=72, got {}",
plan.pitch_ratio,
);
}
fn build_overriding_root_key_sf2() -> Vec<u8> {
let mut smpl: Vec<u8> = Vec::new();
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16;
smpl.extend_from_slice(&v.to_le_bytes());
}
let mut info = Vec::new();
push_chunk(&mut info, b"ifil", &{
let mut v = Vec::new();
v.extend_from_slice(&2u16.to_le_bytes());
v.extend_from_slice(&4u16.to_le_bytes());
v
});
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_chunk(&mut sdta, b"smpl", &smpl);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
let mut phdr = Vec::new();
phdr.extend_from_slice(&phdr_record("Root Preset", 0, 0, 0));
phdr.extend_from_slice(&phdr_record("EOP", 0, 0, 1));
let mut pbag = Vec::new();
pbag.extend_from_slice(&bag_record(0, 0));
pbag.extend_from_slice(&bag_record(1, 0));
let pmod = vec![0u8; PMOD_RECORD];
let mut pgen = Vec::new();
pgen.extend_from_slice(&gen_record(GEN_INSTRUMENT, 0));
pgen.extend_from_slice(&gen_record(0, 0));
let mut inst_chunk = Vec::new();
inst_chunk.extend_from_slice(&inst_record("Root Inst", 0));
inst_chunk.extend_from_slice(&inst_record("EOI", 2));
let mut ibag = Vec::new();
ibag.extend_from_slice(&bag_record(0, 0));
ibag.extend_from_slice(&bag_record(2, 0));
let imod = vec![0u8; IMOD_RECORD];
let mut igen = Vec::new();
igen.extend_from_slice(&gen_record(GEN_OVERRIDING_ROOT_KEY, 72));
igen.extend_from_slice(&gen_record(GEN_SAMPLE_ID, 0));
igen.extend_from_slice(&gen_record(0, 0));
let mut shdr = Vec::new();
shdr.extend_from_slice(&shdr_record("Root", 0, 20, 0, 20, 22050, 60, 0, 0, 1));
shdr.extend_from_slice(&shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0));
let mut pdta = Vec::new();
push_chunk(&mut pdta, b"phdr", &phdr);
push_chunk(&mut pdta, b"pbag", &pbag);
push_chunk(&mut pdta, b"pmod", &pmod);
push_chunk(&mut pdta, b"pgen", &pgen);
push_chunk(&mut pdta, b"inst", &inst_chunk);
push_chunk(&mut pdta, b"ibag", &ibag);
push_chunk(&mut pdta, b"imod", &imod);
push_chunk(&mut pdta, b"igen", &igen);
push_chunk(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_chunk(&mut body, b"LIST", &info_list);
push_chunk(&mut body, b"LIST", &sdta_list);
push_chunk(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn mod_env_default_sustain_is_full_when_zero() {
let m = ModEnvParams::default();
assert_eq!(m.sustain_per_mille, 0);
}
#[test]
fn voice_24_bit_quantisation_resolution() {
let blob = build_sm24_sf2();
let bank = Sf2Bank::parse(&blob).unwrap();
let nonzero_lsb = bank
.sample_data
.iter()
.filter(|&&v| (v & 0xFF) != 0)
.count();
assert_eq!(
nonzero_lsb,
bank.sample_data.len(),
"every frame should have a non-zero sm24 LSB",
);
let inst = Sf2Instrument {
name: "sm24".into(),
bank,
};
let mut voice = inst.make_voice(0, 60, 127, 22_050).unwrap();
let mut buf = vec![0.0f32; 4];
voice.render(&mut buf);
assert!(buf.iter().any(|s| s.abs() > 0.0), "voice rendered silence");
}
#[test]
fn envelope_run_matches_envelope_at_per_sample() {
let plan = SamplePlan {
start: 0,
end: 20,
start_loop: 0,
end_loop: 20,
sample_rate: 22_050,
loops: true,
pitch_ratio: 1.0,
semitones: 0,
fine_cents: 0,
env: EnvParams {
delay_tc: -7973,
attack_tc: -4500,
hold_tc: -6000,
decay_tc: -2000,
sustain_cb: 200,
release_tc: -3000,
},
mod_env: ModEnvParams::default(),
mod_env_to_pitch_cents: 0,
mod_env_to_filter_cents: 0,
initial_filter_fc_cents: 13_500,
initial_filter_q_cb: 0,
initial_attenuation_cb: 0,
exclusive_class: 0,
stereo_pair: None,
};
let data: Arc<[i32]> = Arc::from(vec![0i32; 32].into_boxed_slice());
let mut v = Sf2Voice::from_plan(data, &plan, 100, 44_100);
let mut buf = [0.0f32; 173];
let mut check_span = |v: &Sf2Voice, from: u32, to: u32| {
let mut t = from;
while t < to {
let n = buf.len().min((to - t) as usize);
v.envelope_run(t, &mut buf[..n]);
for (k, &got) in buf[..n].iter().enumerate() {
let want = v.envelope_at(t + k as u32);
assert_eq!(
got.to_bits(),
want.to_bits(),
"held/released env diverged at t={}",
t + k as u32
);
}
t += n as u32;
}
};
check_span(&v, 0, 40_000);
v.elapsed = 9_000;
v.release();
check_span(&v, 9_000, 20_000);
let t0 = u32::MAX - 50;
v.envelope_run(t0, &mut buf);
for (k, &got) in buf.iter().enumerate() {
let want = v.envelope_at(t0.wrapping_add(k as u32));
assert_eq!(got.to_bits(), want.to_bits(), "wrap env diverged at k={k}");
}
}
}