use alloc::string::String;
use alloc::vec::Vec;
use crate::envelope::AdsrConfig;
use crate::error::Result;
use crate::instrument::Instrument;
use crate::loop_mode::LoopMode;
use crate::sample::SampleId;
use crate::zone::{FilterMode, Zone};
#[must_use]
pub fn parse_note_or_number(s: &str) -> Option<u8> {
if let Ok(v) = s.parse::<u8>() {
return Some(v);
}
let bytes = s.as_bytes();
if bytes.is_empty() {
return None;
}
let note_base = match bytes[0].to_ascii_lowercase() {
b'c' => 0i32,
b'd' => 2,
b'e' => 4,
b'f' => 5,
b'g' => 7,
b'a' => 9,
b'b' => 11,
_ => return None,
};
let mut idx = 1;
let mut accidental = 0i32;
if idx < bytes.len() {
match bytes[idx] {
b'#' | b's' => {
accidental = 1;
idx += 1;
}
b'b' if idx + 1 < bytes.len() && bytes[idx + 1].is_ascii_digit() => {
accidental = -1;
idx += 1;
}
_ => {}
}
}
let octave_str = &s[idx..];
let octave: i32 = octave_str.parse().ok()?;
let midi = (octave + 1) * 12 + note_base + accidental;
if (0..=127).contains(&midi) {
Some(midi as u8)
} else {
None
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[must_use]
pub struct SfzRegion {
pub sample: Option<String>,
pub lokey: u8,
pub hikey: u8,
pub lovel: u8,
pub hivel: u8,
pub pitch_keycenter: u8,
pub tune: i32,
pub volume: f32,
pub pan: f32,
pub loop_mode: Option<String>,
pub loop_start: usize,
pub loop_end: usize,
pub group: u32,
pub ampeg_attack: f32,
pub ampeg_decay: f32,
pub ampeg_sustain: f32,
pub ampeg_release: f32,
pub cutoff: f32,
pub fil_veltrack: f32,
pub fileg_attack: f32,
pub fileg_decay: f32,
pub fileg_sustain: f32,
pub fileg_release: f32,
pub fileg_depth: f32,
pub transpose: i32,
pub offset: usize,
pub end: usize,
pub resonance: f32,
pub fil_type: Option<String>,
pub key: Option<u8>,
pub pitchlfo_freq: f32,
pub pitchlfo_depth: f32,
pub fillfo_freq: f32,
pub fillfo_depth: f32,
pub fil_keytrack: f32,
pub output: u8,
pub cc_modulations: Vec<(String, u8, f32)>,
}
impl Default for SfzRegion {
fn default() -> Self {
Self {
sample: None,
lokey: 0,
hikey: 127,
lovel: 1,
hivel: 127,
pitch_keycenter: 60,
tune: 0,
volume: 0.0,
pan: 0.0,
loop_mode: None,
loop_start: 0,
loop_end: 0,
group: 0,
ampeg_attack: 0.0,
ampeg_decay: 0.0,
ampeg_sustain: 100.0,
ampeg_release: 0.0,
cutoff: 0.0,
fil_veltrack: 0.0,
fileg_attack: 0.0,
fileg_decay: 0.0,
fileg_sustain: 100.0,
fileg_release: 0.0,
fileg_depth: 0.0,
transpose: 0,
offset: 0,
end: 0,
resonance: 0.0,
fil_type: None,
key: None,
pitchlfo_freq: 0.0,
pitchlfo_depth: 0.0,
fillfo_freq: 0.0,
fillfo_depth: 0.0,
fil_keytrack: 0.0,
output: 0,
cc_modulations: Vec::new(),
}
}
}
impl SfzRegion {
pub fn new() -> Self {
Self::default()
}
fn apply_opcode(&mut self, key: &str, value: &str) {
match key {
"sample" => self.sample = Some(String::from(value)),
"lokey" => {
if let Some(v) = parse_note_or_number(value) {
self.lokey = v;
}
}
"hikey" => {
if let Some(v) = parse_note_or_number(value) {
self.hikey = v;
}
}
"key" => {
if let Some(v) = parse_note_or_number(value) {
self.key = Some(v);
}
}
"lovel" => {
if let Ok(v) = value.parse::<u8>() {
self.lovel = v;
}
}
"hivel" => {
if let Ok(v) = value.parse::<u8>() {
self.hivel = v;
}
}
"pitch_keycenter" => {
if let Some(v) = parse_note_or_number(value) {
self.pitch_keycenter = v;
}
}
"tune" => {
if let Ok(v) = value.parse::<i32>() {
self.tune = v;
}
}
"volume" => {
if let Ok(v) = value.parse::<f32>() {
self.volume = v;
}
}
"pan" => {
if let Ok(v) = value.parse::<f32>() {
self.pan = v.clamp(-100.0, 100.0);
}
}
"loop_mode" | "loopmode" => self.loop_mode = Some(String::from(value)),
"loop_start" | "loopstart" => {
if let Ok(v) = value.parse::<usize>() {
self.loop_start = v;
}
}
"loop_end" | "loopend" => {
if let Ok(v) = value.parse::<usize>() {
self.loop_end = v;
}
}
"seq_position" => {
if let Ok(v) = value.parse::<u32>() {
self.group = v;
}
}
"group" => {
if let Ok(v) = value.parse::<u32>() {
self.group = v;
}
}
"ampeg_attack" => {
if let Ok(v) = value.parse::<f32>() {
self.ampeg_attack = v.max(0.0);
}
}
"ampeg_decay" => {
if let Ok(v) = value.parse::<f32>() {
self.ampeg_decay = v.max(0.0);
}
}
"ampeg_sustain" => {
if let Ok(v) = value.parse::<f32>() {
self.ampeg_sustain = v.clamp(0.0, 100.0);
}
}
"ampeg_release" => {
if let Ok(v) = value.parse::<f32>() {
self.ampeg_release = v.max(0.0);
}
}
"cutoff" => {
if let Ok(v) = value.parse::<f32>() {
self.cutoff = v.max(0.0);
}
}
"fil_veltrack" => {
if let Ok(v) = value.parse::<f32>() {
self.fil_veltrack = v;
}
}
"fileg_attack" => {
if let Ok(v) = value.parse::<f32>() {
self.fileg_attack = v.max(0.0);
}
}
"fileg_decay" => {
if let Ok(v) = value.parse::<f32>() {
self.fileg_decay = v.max(0.0);
}
}
"fileg_sustain" => {
if let Ok(v) = value.parse::<f32>() {
self.fileg_sustain = v.clamp(0.0, 100.0);
}
}
"fileg_release" => {
if let Ok(v) = value.parse::<f32>() {
self.fileg_release = v.max(0.0);
}
}
"fileg_depth" => {
if let Ok(v) = value.parse::<f32>() {
self.fileg_depth = v.clamp(-9600.0, 9600.0);
}
}
"transpose" => {
if let Ok(v) = value.parse::<i32>() {
self.transpose = v;
}
}
"offset" => {
if let Ok(v) = value.parse::<usize>() {
self.offset = v;
}
}
"end" => {
if let Ok(v) = value.parse::<usize>() {
self.end = v;
}
}
"resonance" | "fil_resonance" => {
if let Ok(v) = value.parse::<f32>() {
self.resonance = v.max(0.0);
}
}
"fil_type" | "filtype" => {
self.fil_type = Some(String::from(value));
}
"pitchlfo_freq" => {
if let Ok(v) = value.parse::<f32>() {
self.pitchlfo_freq = v.max(0.0);
}
}
"pitchlfo_depth" => {
if let Ok(v) = value.parse::<f32>() {
self.pitchlfo_depth = v;
}
}
"fillfo_freq" => {
if let Ok(v) = value.parse::<f32>() {
self.fillfo_freq = v.max(0.0);
}
}
"fillfo_depth" => {
if let Ok(v) = value.parse::<f32>() {
self.fillfo_depth = v;
}
}
"fil_keytrack" => {
if let Ok(v) = value.parse::<f32>() {
self.fil_keytrack = v.clamp(0.0, 1200.0);
}
}
"output" => {
if let Ok(v) = value.parse::<u8>() {
self.output = v;
}
}
_ if key.contains("_oncc") => {
if let Some(pos) = key.find("_oncc") {
let param = &key[..pos];
let cc_str = &key[pos + 5..];
if let (Ok(cc), Ok(depth)) = (cc_str.parse::<u8>(), value.parse::<f32>()) {
self.cc_modulations.push((String::from(param), cc, depth));
}
}
}
_ => {}
}
}
fn inherit_from(&mut self, parent: &SfzRegion) {
if self.sample.is_none() {
self.sample.clone_from(&parent.sample);
}
if self.lokey == 0 && parent.lokey != 0 {
self.lokey = parent.lokey;
}
if self.hikey == 127 && parent.hikey != 127 {
self.hikey = parent.hikey;
}
if self.lovel == 1 && parent.lovel != 1 {
self.lovel = parent.lovel;
}
if self.hivel == 127 && parent.hivel != 127 {
self.hivel = parent.hivel;
}
if self.pitch_keycenter == 60 && parent.pitch_keycenter != 60 {
self.pitch_keycenter = parent.pitch_keycenter;
}
if self.tune == 0 && parent.tune != 0 {
self.tune = parent.tune;
}
if self.volume == 0.0 && parent.volume != 0.0 {
self.volume = parent.volume;
}
if self.pan == 0.0 && parent.pan != 0.0 {
self.pan = parent.pan;
}
if self.loop_mode.is_none() {
self.loop_mode.clone_from(&parent.loop_mode);
}
if self.loop_start == 0 && parent.loop_start != 0 {
self.loop_start = parent.loop_start;
}
if self.loop_end == 0 && parent.loop_end != 0 {
self.loop_end = parent.loop_end;
}
if self.group == 0 && parent.group != 0 {
self.group = parent.group;
}
if self.ampeg_attack == 0.0 && parent.ampeg_attack != 0.0 {
self.ampeg_attack = parent.ampeg_attack;
}
if self.ampeg_decay == 0.0 && parent.ampeg_decay != 0.0 {
self.ampeg_decay = parent.ampeg_decay;
}
if self.ampeg_sustain == 100.0 && parent.ampeg_sustain != 100.0 {
self.ampeg_sustain = parent.ampeg_sustain;
}
if self.ampeg_release == 0.0 && parent.ampeg_release != 0.0 {
self.ampeg_release = parent.ampeg_release;
}
if self.cutoff == 0.0 && parent.cutoff != 0.0 {
self.cutoff = parent.cutoff;
}
if self.fil_veltrack == 0.0 && parent.fil_veltrack != 0.0 {
self.fil_veltrack = parent.fil_veltrack;
}
if self.fileg_attack == 0.0 && parent.fileg_attack != 0.0 {
self.fileg_attack = parent.fileg_attack;
}
if self.fileg_decay == 0.0 && parent.fileg_decay != 0.0 {
self.fileg_decay = parent.fileg_decay;
}
if self.fileg_sustain == 100.0 && parent.fileg_sustain != 100.0 {
self.fileg_sustain = parent.fileg_sustain;
}
if self.fileg_release == 0.0 && parent.fileg_release != 0.0 {
self.fileg_release = parent.fileg_release;
}
if self.fileg_depth == 0.0 && parent.fileg_depth != 0.0 {
self.fileg_depth = parent.fileg_depth;
}
if self.transpose == 0 && parent.transpose != 0 {
self.transpose = parent.transpose;
}
if self.offset == 0 && parent.offset != 0 {
self.offset = parent.offset;
}
if self.end == 0 && parent.end != 0 {
self.end = parent.end;
}
if self.resonance == 0.0 && parent.resonance != 0.0 {
self.resonance = parent.resonance;
}
if self.fil_type.is_none() {
self.fil_type.clone_from(&parent.fil_type);
}
if self.key.is_none() {
self.key = parent.key;
}
if self.pitchlfo_freq == 0.0 && parent.pitchlfo_freq != 0.0 {
self.pitchlfo_freq = parent.pitchlfo_freq;
}
if self.pitchlfo_depth == 0.0 && parent.pitchlfo_depth != 0.0 {
self.pitchlfo_depth = parent.pitchlfo_depth;
}
if self.fillfo_freq == 0.0 && parent.fillfo_freq != 0.0 {
self.fillfo_freq = parent.fillfo_freq;
}
if self.fillfo_depth == 0.0 && parent.fillfo_depth != 0.0 {
self.fillfo_depth = parent.fillfo_depth;
}
if self.fil_keytrack == 0.0 && parent.fil_keytrack != 0.0 {
self.fil_keytrack = parent.fil_keytrack;
}
if self.output == 0 && parent.output != 0 {
self.output = parent.output;
}
if self.cc_modulations.is_empty() && !parent.cc_modulations.is_empty() {
self.cc_modulations.clone_from(&parent.cc_modulations);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HeaderKind {
None,
Control,
Global,
Group,
Region,
Curve,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[must_use]
pub struct SfzFile {
pub global: SfzRegion,
pub groups: Vec<SfzRegion>,
pub regions: Vec<SfzRegion>,
group_indices: Vec<Option<usize>>,
pub default_path: Option<String>,
pub includes: Vec<String>,
}
impl SfzFile {
pub fn to_instrument(&self, name: &str, sample_rate: f32) -> (Instrument, Vec<String>) {
let zones_and_files = self.to_zones(sample_rate);
let mut inst = Instrument::new(name);
let mut sample_files: Vec<String> = Vec::new();
for (zone, filename) in zones_and_files {
let idx = sample_files
.iter()
.position(|f| f == &filename)
.unwrap_or_else(|| {
let i = sample_files.len();
sample_files.push(filename);
i
});
let mut z = zone;
z.sample_id = SampleId(idx as u32);
inst.add_zone(z);
}
(inst, sample_files)
}
#[must_use]
pub fn to_zones(&self, sample_rate: f32) -> Vec<(Zone, String)> {
let mut result = Vec::with_capacity(self.regions.len());
for (i, region) in self.regions.iter().enumerate() {
let mut merged = region.clone();
if let Some(Some(group_idx)) = self.group_indices.get(i)
&& let Some(group) = self.groups.get(*group_idx)
{
merged.inherit_from(group);
}
merged.inherit_from(&self.global);
if let Some(k) = merged.key {
if merged.lokey == 0 && merged.hikey == 127 {
merged.lokey = k;
merged.hikey = k;
}
if merged.pitch_keycenter == 60 {
merged.pitch_keycenter = k;
}
}
let mut filename = match merged.sample {
Some(ref f) => f.clone(),
None => continue,
};
if let Some(ref prefix) = self.default_path
&& !filename.starts_with(prefix.as_str())
{
let mut full = String::with_capacity(prefix.len() + filename.len());
full.push_str(prefix);
full.push_str(&filename);
filename = full;
}
let tune_cents = merged.tune as f32 + merged.transpose as f32 * 100.0;
let filter_type = map_fil_type(merged.fil_type.as_deref());
let mut zone = Zone::new(SampleId(i as u32))
.with_key_range(merged.lokey, merged.hikey)
.with_vel_range(merged.lovel, merged.hivel)
.with_root_note(merged.pitch_keycenter)
.with_tune(tune_cents)
.with_volume(merged.volume)
.with_pan(merged.pan / 100.0)
.with_loop(
map_loop_mode(merged.loop_mode.as_deref()),
merged.loop_start,
merged.loop_end,
)
.with_filter(merged.cutoff, map_fil_veltrack(merged.fil_veltrack))
.with_filter_type(filter_type)
.with_group(merged.group);
if merged.resonance > 0.0 {
zone = zone.with_filter_resonance(merged.resonance);
}
if merged.offset > 0 {
zone = zone.with_sample_offset(merged.offset);
}
if merged.end > 0 {
zone = zone.with_sample_end(merged.end);
}
let has_ampeg = merged.ampeg_attack != 0.0
|| merged.ampeg_decay != 0.0
|| merged.ampeg_sustain != 100.0
|| merged.ampeg_release != 0.0;
let zone = if has_ampeg {
let adsr = AdsrConfig::from_seconds(
merged.ampeg_attack,
merged.ampeg_decay,
merged.ampeg_sustain / 100.0,
merged.ampeg_release,
sample_rate,
);
zone.with_adsr(adsr)
} else {
zone
};
let has_fileg = merged.fileg_depth != 0.0
|| merged.fileg_attack != 0.0
|| merged.fileg_decay != 0.0
|| merged.fileg_sustain != 100.0
|| merged.fileg_release != 0.0;
let zone = if has_fileg {
let fileg = AdsrConfig::from_seconds(
merged.fileg_attack,
merged.fileg_decay,
merged.fileg_sustain / 100.0,
merged.fileg_release,
sample_rate,
);
zone.with_filter_envelope(fileg, merged.fileg_depth)
} else {
zone
};
let zone = if merged.pitchlfo_freq > 0.0 && merged.pitchlfo_depth != 0.0 {
zone.with_pitch_lfo(merged.pitchlfo_freq, merged.pitchlfo_depth)
} else {
zone
};
let zone = if merged.fillfo_freq > 0.0 && merged.fillfo_depth != 0.0 {
zone.with_filter_lfo(merged.fillfo_freq, merged.fillfo_depth)
} else {
zone
};
let zone = if merged.fil_keytrack > 0.0 {
zone.with_key_tracking(merged.fil_keytrack / 1200.0)
} else {
zone
};
let zone = if merged.output > 0 {
zone.with_output_bus(merged.output)
} else {
zone
};
result.push((zone, filename));
}
result
}
}
#[must_use]
#[inline]
fn map_loop_mode(mode: Option<&str>) -> LoopMode {
match mode {
Some("loop_continuous") => LoopMode::Forward,
Some("loop_sustain") => LoopMode::LoopSustain,
Some("one_shot") => LoopMode::OneShot,
Some("no_loop") | None => LoopMode::OneShot,
Some(_) => LoopMode::OneShot,
}
}
#[must_use]
#[inline]
fn map_fil_type(fil_type: Option<&str>) -> FilterMode {
match fil_type {
Some("hpf_1p") | Some("hpf_2p") => FilterMode::HighPass,
Some("bpf_2p") => FilterMode::BandPass,
Some("brf_2p") => FilterMode::Notch,
_ => FilterMode::LowPass,
}
}
#[must_use]
#[inline]
fn map_fil_veltrack(cents: f32) -> f32 {
(cents / 9600.0).clamp(0.0, 1.0)
}
pub fn parse(input: &str) -> Result<SfzFile> {
let mut global = SfzRegion::new();
let mut groups: Vec<SfzRegion> = Vec::new();
let mut regions: Vec<SfzRegion> = Vec::new();
let mut group_indices: Vec<Option<usize>> = Vec::new();
let mut current_header = HeaderKind::None;
let mut current_group_idx: Option<usize> = None;
let mut default_path: Option<String> = None;
let mut includes: Vec<String> = Vec::new();
for line in input.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") {
continue;
}
if line.starts_with("#include") {
let path = line.trim_start_matches("#include").trim().trim_matches('"');
if !path.is_empty() {
includes.push(String::from(path));
}
continue;
}
let tokens: Vec<&str> = line.split_whitespace().collect();
for token in &tokens {
if let Some(header) = parse_header(token) {
match header {
HeaderKind::Control => {
current_header = HeaderKind::Control;
}
HeaderKind::Global => {
current_header = HeaderKind::Global;
}
HeaderKind::Group => {
current_header = HeaderKind::Group;
groups.push(SfzRegion::new());
current_group_idx = Some(groups.len() - 1);
}
HeaderKind::Region => {
current_header = HeaderKind::Region;
regions.push(SfzRegion::new());
group_indices.push(current_group_idx);
}
HeaderKind::Curve => {
current_header = HeaderKind::Curve;
}
HeaderKind::None => {}
}
continue;
}
if let Some((key, value)) = split_opcode(token) {
match current_header {
HeaderKind::Control => {
if key == "default_path" {
default_path = Some(String::from(value));
}
}
HeaderKind::Global => global.apply_opcode(key, value),
HeaderKind::Group => {
if let Some(g) = groups.last_mut() {
g.apply_opcode(key, value);
}
}
HeaderKind::Region => {
if let Some(r) = regions.last_mut() {
r.apply_opcode(key, value);
}
}
HeaderKind::Curve => {
}
HeaderKind::None => {
global.apply_opcode(key, value);
}
}
}
}
}
Ok(SfzFile {
global,
groups,
regions,
group_indices,
default_path,
includes,
})
}
fn parse_header(token: &str) -> Option<HeaderKind> {
let trimmed = token.trim();
if trimmed.starts_with('<') && trimmed.ends_with('>') {
let name = &trimmed[1..trimmed.len() - 1];
match name {
"control" => Some(HeaderKind::Control),
"global" => Some(HeaderKind::Global),
"group" => Some(HeaderKind::Group),
"region" => Some(HeaderKind::Region),
"curve" => Some(HeaderKind::Curve),
_ => None,
}
} else {
None
}
}
fn split_opcode(token: &str) -> Option<(&str, &str)> {
let idx = token.find('=')?;
let key = &token[..idx];
let value = &token[idx + 1..];
if key.is_empty() || value.is_empty() {
return None;
}
Some((key, value))
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn parse_empty_file() {
let sfz = parse("").expect("should parse empty input");
assert!(sfz.regions.is_empty());
assert!(sfz.groups.is_empty());
}
#[test]
fn parse_single_region() {
let input = r#"
<region>
sample=piano_c4.wav
lokey=60 hikey=72
pitch_keycenter=66
lovel=1 hivel=100
"#;
let sfz = parse(input).expect("should parse single region");
assert_eq!(sfz.regions.len(), 1);
let r = &sfz.regions[0];
assert_eq!(r.sample.as_deref(), Some("piano_c4.wav"));
assert_eq!(r.lokey, 60);
assert_eq!(r.hikey, 72);
assert_eq!(r.pitch_keycenter, 66);
assert_eq!(r.lovel, 1);
assert_eq!(r.hivel, 100);
}
#[test]
fn parse_with_global_defaults() {
let input = r#"
<global>
ampeg_release=0.5
volume=-6
<region>
sample=test.wav
"#;
let sfz = parse(input).expect("should parse with globals");
assert_eq!(sfz.regions.len(), 1);
assert!((sfz.global.ampeg_release - 0.5).abs() < f32::EPSILON);
assert!((sfz.global.volume - -6.0).abs() < f32::EPSILON);
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 1);
let (zone, filename) = &zones[0];
assert_eq!(filename, "test.wav");
assert!((zone.volume_db - -6.0).abs() < f32::EPSILON);
}
#[test]
fn parse_with_group_inheritance() {
let input = r#"
<global>
ampeg_release=0.3
<group>
lokey=60 hikey=72
<region>
sample=soft.wav
lovel=1 hivel=80
<region>
sample=loud.wav
lovel=81 hivel=127
"#;
let sfz = parse(input).expect("should parse with groups");
assert_eq!(sfz.groups.len(), 1);
assert_eq!(sfz.regions.len(), 2);
assert_eq!(sfz.groups[0].lokey, 60);
assert_eq!(sfz.groups[0].hikey, 72);
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 2);
let (z0, f0) = &zones[0];
assert_eq!(f0, "soft.wav");
assert_eq!(z0.key_lo, 60);
assert_eq!(z0.key_hi, 72);
assert_eq!(z0.vel_lo, 1);
assert_eq!(z0.vel_hi, 80);
let (z1, f1) = &zones[1];
assert_eq!(f1, "loud.wav");
assert_eq!(z1.key_lo, 60);
assert_eq!(z1.key_hi, 72);
assert_eq!(z1.vel_lo, 81);
assert_eq!(z1.vel_hi, 127);
}
#[test]
fn round_trip_to_instrument() {
let input = r#"
<region>
sample=piano.wav
lokey=48 hikey=72
pitch_keycenter=60
lovel=1 hivel=127
tune=5
volume=-3
pan=50
"#;
let sfz = parse(input).expect("should parse");
let (inst, files) = sfz.to_instrument("test_piano", 44100.0);
assert_eq!(inst.name(), "test_piano");
assert_eq!(inst.zone_count(), 1);
assert_eq!(files.len(), 1);
assert_eq!(files[0], "piano.wav");
let zones = inst.zones();
let z = &zones[0];
assert_eq!(z.key_lo, 48);
assert_eq!(z.key_hi, 72);
assert_eq!(z.root_note, 60);
assert!((z.tune_cents - 5.0).abs() < f32::EPSILON);
assert!((z.volume_db - -3.0).abs() < f32::EPSILON);
assert!((z.pan - 0.5).abs() < f32::EPSILON);
}
#[test]
fn loop_mode_mapping() {
assert_eq!(map_loop_mode(None), LoopMode::OneShot);
assert_eq!(map_loop_mode(Some("no_loop")), LoopMode::OneShot);
assert_eq!(map_loop_mode(Some("one_shot")), LoopMode::OneShot);
assert_eq!(map_loop_mode(Some("loop_continuous")), LoopMode::Forward);
assert_eq!(map_loop_mode(Some("loop_sustain")), LoopMode::LoopSustain);
assert_eq!(map_loop_mode(Some("unknown_mode")), LoopMode::OneShot);
}
#[test]
fn invalid_opcode_ignored() {
let input = r#"
<region>
sample=test.wav
totally_fake_opcode=999
another_invalid=hello
lokey=60
"#;
let sfz = parse(input).expect("should parse despite unknown opcodes");
assert_eq!(sfz.regions.len(), 1);
assert_eq!(sfz.regions[0].lokey, 60);
assert_eq!(sfz.regions[0].sample.as_deref(), Some("test.wav"));
}
#[test]
fn comments_and_blank_lines_skipped() {
let input = r#"
// This is a comment
<region>
sample=test.wav
// Another comment
lokey=60 hikey=72
"#;
let sfz = parse(input).expect("should parse");
assert_eq!(sfz.regions.len(), 1);
assert_eq!(sfz.regions[0].lokey, 60);
}
#[test]
fn region_overrides_group_overrides_global() {
let input = r#"
<global>
volume=-10
pan=25
<group>
volume=-5
<region>
sample=test.wav
volume=-2
"#;
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 1);
let (z, _) = &zones[0];
assert!((z.volume_db - -2.0).abs() < f32::EPSILON);
assert!((z.pan - 0.25).abs() < f32::EPSILON);
}
#[test]
fn loop_mode_parsed_in_region() {
let input = r#"
<region>
sample=loop.wav
loop_mode=loop_continuous
loop_start=1000
loop_end=5000
"#;
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 1);
let (z, _) = &zones[0];
assert_eq!(z.loop_mode, LoopMode::Forward);
assert_eq!(z.loop_start, 1000);
assert_eq!(z.loop_end, 5000);
}
#[test]
fn filter_opcodes_parsed() {
let input = r#"
<region>
sample=test.wav
cutoff=5000
fil_veltrack=4800
"#;
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 1);
let (z, _) = &zones[0];
assert!((z.filter_cutoff - 5000.0).abs() < f32::EPSILON);
assert!((z.filter_vel_track - 0.5).abs() < f32::EPSILON);
}
#[test]
fn multiple_groups() {
let input = r#"
<group>
lokey=36 hikey=47
<region>
sample=bass.wav
<group>
lokey=48 hikey=72
<region>
sample=mid.wav
<region>
sample=mid2.wav
"#;
let sfz = parse(input).expect("should parse");
assert_eq!(sfz.groups.len(), 2);
assert_eq!(sfz.regions.len(), 3);
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 3);
assert_eq!(zones[0].0.key_lo, 36);
assert_eq!(zones[0].0.key_hi, 47);
assert_eq!(zones[0].1, "bass.wav");
assert_eq!(zones[1].0.key_lo, 48);
assert_eq!(zones[1].0.key_hi, 72);
assert_eq!(zones[2].0.key_lo, 48);
assert_eq!(zones[2].0.key_hi, 72);
}
#[test]
fn region_without_sample_skipped() {
let input = r#"
<region>
lokey=60 hikey=72
<region>
sample=valid.wav
"#;
let sfz = parse(input).expect("should parse");
assert_eq!(sfz.regions.len(), 2);
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 1);
assert_eq!(zones[0].1, "valid.wav");
}
#[test]
fn adsr_envelope_from_sfz() {
let input = r#"
<global>
ampeg_attack=0.01
ampeg_decay=0.1
ampeg_sustain=70
ampeg_release=0.5
<region>
sample=test.wav
"#;
let sfz = parse(input).expect("should parse");
assert!((sfz.global.ampeg_attack - 0.01).abs() < f32::EPSILON);
assert!((sfz.global.ampeg_decay - 0.1).abs() < f32::EPSILON);
assert!((sfz.global.ampeg_sustain - 70.0).abs() < f32::EPSILON);
assert!((sfz.global.ampeg_release - 0.5).abs() < f32::EPSILON);
}
#[test]
fn to_instrument_deduplicates_samples() {
let input = r#"
<region>
sample=shared.wav
lokey=60 hikey=66
<region>
sample=shared.wav
lokey=67 hikey=72
<region>
sample=other.wav
lokey=73 hikey=84
"#;
let sfz = parse(input).expect("should parse");
let (inst, files) = sfz.to_instrument("dedup_test", 44100.0);
assert_eq!(inst.zone_count(), 3);
assert_eq!(files.len(), 2);
assert_eq!(files[0], "shared.wav");
assert_eq!(files[1], "other.wav");
let zones = inst.zones();
assert_eq!(zones[0].sample_id(), SampleId(0));
assert_eq!(zones[1].sample_id(), SampleId(0));
assert_eq!(zones[2].sample_id(), SampleId(1));
}
#[test]
fn note_name_parsing() {
assert_eq!(parse_note_or_number("60"), Some(60));
assert_eq!(parse_note_or_number("c4"), Some(60));
assert_eq!(parse_note_or_number("C4"), Some(60));
assert_eq!(parse_note_or_number("f#3"), Some(54));
assert_eq!(parse_note_or_number("eb4"), Some(63));
assert_eq!(parse_note_or_number("b4"), Some(71));
assert_eq!(parse_note_or_number("c-1"), Some(0));
assert_eq!(parse_note_or_number("g9"), Some(127));
assert_eq!(parse_note_or_number(""), None);
assert_eq!(parse_note_or_number("xyz"), None);
}
#[test]
fn note_names_in_opcodes() {
let input = "<region>\nsample=test.wav\nlokey=c4 hikey=c5 pitch_keycenter=f#4\n";
let sfz = parse(input).expect("should parse note names");
assert_eq!(sfz.regions[0].lokey, 60);
assert_eq!(sfz.regions[0].hikey, 72);
assert_eq!(sfz.regions[0].pitch_keycenter, 66);
}
#[test]
fn key_shorthand_opcode() {
let input = "<region>\nsample=test.wav\nkey=60\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones.len(), 1);
let (z, _) = &zones[0];
assert_eq!(z.key_lo, 60);
assert_eq!(z.key_hi, 60);
assert_eq!(z.root_note, 60);
}
#[test]
fn control_header_default_path() {
let input = "<control>\ndefault_path=samples/piano/\n<region>\nsample=c4.wav\n";
let sfz = parse(input).expect("should parse");
assert_eq!(sfz.default_path.as_deref(), Some("samples/piano/"));
let zones = sfz.to_zones(44100.0);
assert_eq!(zones[0].1, "samples/piano/c4.wav");
}
#[test]
fn curve_header_does_not_break_parsing() {
let input = "<curve>\ncurve_index=1\nv000=0 v127=1\n<region>\nsample=test.wav\n";
let sfz = parse(input).expect("should parse with curve header");
assert_eq!(sfz.regions.len(), 1);
}
#[test]
fn transpose_adds_to_tune() {
let input = "<region>\nsample=test.wav\ntune=10 transpose=2\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert!((zones[0].0.tune_cents - 210.0).abs() < f32::EPSILON);
}
#[test]
fn fil_type_maps_to_filter_mode() {
use crate::zone::FilterMode;
let input = "<region>\nsample=test.wav\nfil_type=hpf_2p\ncutoff=1000\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones[0].0.filter_type(), FilterMode::HighPass);
}
#[test]
fn offset_and_end_opcodes() {
let input = "<region>\nsample=test.wav\noffset=100 end=5000\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones[0].0.sample_offset(), 100);
assert_eq!(zones[0].0.sample_end(), 5000);
}
#[test]
fn resonance_opcode() {
let input = "<region>\nsample=test.wav\ncutoff=2000 resonance=6.0\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert!((zones[0].0.filter_resonance() - 6.0).abs() < f32::EPSILON);
}
#[test]
fn fileg_opcodes_wired_to_zone() {
let input = "<region>\nsample=test.wav\ncutoff=2000\nfileg_attack=0.1 fileg_decay=0.2 fileg_sustain=50 fileg_release=0.3 fileg_depth=2400\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
let (z, _) = &zones[0];
assert!(z.fileg().is_some());
assert!((z.fileg_depth() - 2400.0).abs() < f32::EPSILON);
}
#[test]
fn ampeg_wired_to_zone_adsr() {
let input = "<region>\nsample=test.wav\nampeg_attack=0.05 ampeg_release=0.3\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
let (z, _) = &zones[0];
assert!(z.adsr().is_some());
let adsr = z.adsr().unwrap();
assert!(adsr.attack_samples > 0);
assert!(adsr.release_samples > 0);
}
#[test]
fn loop_sustain_mode_in_sfz() {
let input =
"<region>\nsample=test.wav\nloop_mode=loop_sustain\nloop_start=100 loop_end=500\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones[0].0.loop_mode(), LoopMode::LoopSustain);
}
#[test]
fn pitchlfo_opcodes_wired_to_zone() {
let input = "<region>\nsample=test.wav\npitchlfo_freq=5.0 pitchlfo_depth=50\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
let (z, _) = &zones[0];
assert!((z.pitchlfo_rate() - 5.0).abs() < f32::EPSILON);
assert!((z.pitchlfo_depth() - 50.0).abs() < f32::EPSILON);
}
#[test]
fn fillfo_opcodes_wired_to_zone() {
let input = "<region>\nsample=test.wav\ncutoff=2000\nfillfo_freq=3.0 fillfo_depth=600\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
let (z, _) = &zones[0];
assert!((z.fillfo_rate() - 3.0).abs() < f32::EPSILON);
assert!((z.fillfo_depth() - 600.0).abs() < f32::EPSILON);
}
#[test]
fn fil_keytrack_opcode() {
let input = "<region>\nsample=test.wav\ncutoff=2000\nfil_keytrack=600\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
let (z, _) = &zones[0];
assert!((z.fil_keytrack() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn include_directives_collected() {
let input =
"#include \"common.sfz\"\n<region>\nsample=test.wav\n#include \"velocities.sfz\"\n";
let sfz = parse(input).expect("should parse with includes");
assert_eq!(sfz.includes.len(), 2);
assert_eq!(sfz.includes[0], "common.sfz");
assert_eq!(sfz.includes[1], "velocities.sfz");
}
#[test]
fn cc_modulation_opcodes_parsed() {
let input = "<region>\nsample=test.wav\nvolume_oncc1=6 cutoff_oncc74=2400\n";
let sfz = parse(input).expect("should parse");
assert_eq!(sfz.regions[0].cc_modulations.len(), 2);
let (param, cc, depth) = &sfz.regions[0].cc_modulations[0];
assert_eq!(param, "volume");
assert_eq!(*cc, 1);
assert!((depth - 6.0).abs() < f32::EPSILON);
}
#[test]
fn output_opcode_wired_to_bus() {
let input = "<region>\nsample=test.wav\noutput=2\n";
let sfz = parse(input).expect("should parse");
let zones = sfz.to_zones(44100.0);
assert_eq!(zones[0].0.output_bus(), 2);
}
}