summaryrefslogtreecommitdiff
path: root/src/soundfont.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/soundfont.rs')
-rw-r--r--src/soundfont.rs291
1 files changed, 228 insertions, 63 deletions
diff --git a/src/soundfont.rs b/src/soundfont.rs
index 4cddb0b..7329641 100644
--- a/src/soundfont.rs
+++ b/src/soundfont.rs
@@ -11,6 +11,8 @@ IMPORTANT SOUNDFONT TERMINOLOGY:
a preset zone refers to an instrument
an object zone refers to a sample
a SAMPLE is a block of audio data with some properties of how it should be played
+
+modulators are not currently supported
*/
use std::fs::File;
use std::io::{Read, Seek, Write};
@@ -71,8 +73,16 @@ impl SFObject for Preset {
}
}
+#[derive(Debug, Clone, Copy)]
+enum SampleType {
+ Mono,
+ Left,
+ Right,
+}
+
#[derive(Debug)]
struct Sample {
+ r#type: SampleType,
start: u32,
len: u32,
startloop: u32,
@@ -105,6 +115,17 @@ pub enum SampleError {
NoFile,
}
+pub struct SamplesRequest {
+ hold_time: f64,
+ key: u8,
+ vel: u8,
+ falloff_start: f64,
+ falloff: f32,
+ tune: i32,
+ volume: f32,
+ zones: Vec<Zone>
+}
+
impl From<&OpenError> for String {
fn from(err: &OpenError) -> String {
use OpenError::*;
@@ -264,14 +285,13 @@ fn bad_sound_font(s: &str) -> OpenError {
OpenError::BadSoundFont(s.to_string())
}
-fn read_utf8_fixed_len(file: &mut File, len: usize, what: &str) -> Result<String, OpenError> {
+fn read_utf8_fixed_len(file: &mut File, len: usize) -> Result<String, OpenError> {
let mut name_vec = vec![0; len];
file.read_exact(&mut name_vec)?;
while !name_vec.is_empty() && name_vec[name_vec.len() - 1] == 0 {
name_vec.pop();
}
- String::from_utf8(name_vec)
- .map_err(|_| OpenError::BadSoundFont(format!("invalid UTF-8 in {}", what)))
+ Ok(String::from_utf8_lossy(&name_vec).to_string())
}
impl Zone {
@@ -311,6 +331,21 @@ impl Zone {
&& vel >= self.vel_range.0
&& vel <= self.vel_range.1
}
+
+ fn distance_to(&self, key: u8, vel: u8) -> u32 {
+ let key = key as i32;
+ let vel = vel as i32;
+ let key0 = self.key_range.0 as i32;
+ let key1 = self.key_range.1 as i32;
+ let vel0 = self.vel_range.0 as i32;
+ let vel1 = self.vel_range.1 as i32;
+ use std::cmp::min;
+ let key_dist = min((key - key0).abs(), (key - key1).abs()) as u32;
+ let vel_dist = min((vel - vel0).abs(), (vel - vel1).abs()) as u32;
+
+ // key matters more than velocity.
+ key_dist * 100 + vel_dist
+ }
// the zone for a note is generated by adding
fn add(zone1: &Self, zone2: &Self) -> Self {
@@ -513,6 +548,31 @@ fn read_gen_zones<Item: SFObject>(
Ok(())
}
+/// request for sound font samples.
+impl SamplesRequest {
+ /// set amount of time note has been playing for
+ pub fn set_hold_time(&mut self, t: f64) {
+ self.hold_time = t;
+ }
+
+ /// `tune` is in cents
+ pub fn set_tune(&mut self, tune: i32) {
+ self.tune = tune;
+ }
+
+ /// 0 = silent, 1 = max volume.
+ pub fn set_volume(&mut self, volume: f32) {
+ self.volume = volume;
+ }
+
+ /// amplitude will be multiplied by amount ^ (t - start).
+ /// e.g. when a user lets go of a piano key, you might call `set_falloff(release_time, 0.01)`
+ pub fn set_falloff(&mut self, start: f64, amount: f32) {
+ self.falloff = amount;
+ self.falloff_start = start;
+ }
+}
+
impl SoundFont {
/// Open a soundfont.
/// This does not load any sample data, since that would be slow
@@ -575,7 +635,7 @@ impl SoundFont {
if chunk_type == INAM {
if chunk_size < 256 {
let mut data = vec![0; chunk_size as usize];
- file.read(&mut data)?;
+ let _nread = file.read(&mut data)?;
data.pop(); // null terminator
if let Ok(n) = String::from_utf8(data) {
name = Some(n);
@@ -622,9 +682,6 @@ impl SoundFont {
fn new() -> Self {
Chunk { offset: 0, size: 0 }
}
- fn end(&self) -> u64 {
- self.offset + self.size as u64
- }
}
let mut inst = Chunk::new();
let mut ibag = Chunk::new();
@@ -686,30 +743,29 @@ impl SoundFont {
// --- read inst chunk ---
{
file.seek(std::io::SeekFrom::Start(inst.offset))?;
- loop {
- let name = read_utf8_fixed_len(&mut file, 20, "instrument name")?;
- if name.is_empty() {
+ let inst_count = inst.size / 22;
+ if inst_count < 2 || inst.size % 22 != 0 {
+ return Err(OpenError::BadSoundFont(format!("bad INST chunk size ({} should be at least 44, and a multiple of 22)", inst.size)));
+ }
+ for i in 0..inst_count {
+ println!("{:x}",file.stream_position()?);
+ let name = read_utf8_fixed_len(&mut file, 20)?;
+ if name.is_empty() && i != inst_count - 1 {
return Err(bad_sound_font("instrument with no name."));
}
+
+ // oddly, musescore's sf3 file has an empty name for the terminal record.
+ if i == inst_count - 1 && !name.is_empty() && name != "EOI" {
+ return Err(bad_sound_font("no terminal instrument."));
+ }
let bag_ndx = read_u16(&mut file)?;
- let is_eoi = name == "EOI";
-
instruments.push(Instrument {
name,
..Default::default()
});
instrument_bag_indices.push(bag_ndx);
-
- if is_eoi {
- // terminal instrument.
- break;
- }
-
- if file.stream_position()? >= inst.end() {
- return Err(bad_sound_font("No terminal instrument."));
- }
}
}
@@ -742,7 +798,7 @@ impl SoundFont {
)));
}
for _i in 0..phdr.size / 38 {
- let name = read_utf8_fixed_len(&mut file, 20, "preset name")?;
+ let name = read_utf8_fixed_len(&mut file, 20)?;
let _preset = read_u16(&mut file)?;
let _bank = read_u16(&mut file)?;
let bag_ndx = read_u16(&mut file)?;
@@ -778,7 +834,7 @@ impl SoundFont {
let mut samples = Vec::with_capacity(samples_count as usize);
for _i in 0..shdr.size / 46 {
// a sample
- let sample_name = read_utf8_fixed_len(&mut file, 20, "sample name")?;
+ let sample_name = read_utf8_fixed_len(&mut file, 20)?;
if sample_name == "EOS" {
break;
}
@@ -799,9 +855,17 @@ impl SoundFont {
}
let pitch_correction = read_i8(&mut file)?;
let _sample_link = read_u16(&mut file)?;
- let _sample_type = read_u16(&mut file)?;
-
+ let sample_type = read_u16(&mut file)?;
+ let r#type = match sample_type {
+ 2 => SampleType::Left,
+ 4 => SampleType::Right,
+ _ => SampleType::Mono // meh
+ };
+
+ println!("{} {} {}", sample_name,start, startloop);
+
let sample = Sample {
+ r#type: r#type,
start,
len: end - start,
startloop: startloop - start,
@@ -817,6 +881,32 @@ impl SoundFont {
instruments.pop(); // remove EOI
presets.pop(); // remove EOP
+ // check instrument & sample indices
+ for p in presets.iter() {
+ for zone in p.zones.iter() {
+ if let ZoneReference::Instrument(inst) = zone.reference {
+ if inst as usize >= instruments.len() {
+ return Err(OpenError::BadSoundFont(format!(
+ "preset zone references instrument {}, but there are only {} instruments.",
+ inst, instruments.len())));
+ }
+ }
+ }
+ }
+ for i in instruments.iter() {
+ for zone in i.zones.iter() {
+ if let ZoneReference::SampleID(sample) = zone.reference {
+ if sample as usize >= samples.len() {
+ return Err(OpenError::BadSoundFont(format!(
+ "instrument zone references sample {}, but there are only {} samples.",
+ sample,
+ samples.len()
+ )));
+ }
+ }
+ }
+ }
+
Ok(SoundFont {
file: Some(file),
sdta_offset,
@@ -884,14 +974,63 @@ impl SoundFont {
let inst = &self.instruments[i as usize];
for izone in inst.zones.iter() {
if izone.contains(key, vel) {
- zones.push(Zone::add(pzone, izone));
+ if let ZoneReference::SampleID(_) = izone.reference {
+ zones.push(Zone::add(pzone, izone));
+ }
}
}
}
}
}
- // @TODO: find closest zone if zones.len() == 0
+ if zones.is_empty() {
+ // that didn't work. try finding closest zone(s).
+ let mut closest_l = None;
+ let mut closest_l_dist = u32::MAX;
+ let mut closest_m = None;
+ let mut closest_m_dist = u32::MAX;
+ let mut closest_r = None;
+ let mut closest_r_dist = u32::MAX;
+ for pzone in preset.zones.iter() {
+ let d1 = pzone.distance_to(key, vel);
+ if let ZoneReference::Instrument(i) = pzone.reference {
+ let inst = &self.instruments[i as usize];
+ for izone in inst.zones.iter() {
+ let d2 = izone.distance_to(key, vel);
+ let dist = d1 + d2;
+ if let ZoneReference::SampleID(sample) = izone.reference {
+ match self.samples[sample as usize].r#type {
+ SampleType::Mono =>
+ if dist < closest_m_dist {
+ closest_m_dist = dist;
+ closest_m = Some(Zone::add(pzone, izone));
+ },
+ SampleType::Left =>
+ if dist < closest_l_dist {
+ closest_l_dist = dist;
+ closest_l = Some(Zone::add(pzone, izone));
+ },
+ SampleType::Right =>
+ if dist < closest_r_dist {
+ closest_r_dist = dist;
+ closest_r = Some(Zone::add(pzone, izone));
+ },
+ }
+ }
+ }
+ }
+ }
+
+
+ if let Some(m) = closest_m {
+ zones.push(m);
+ } else if let Some(l) = closest_l {
+ if let Some(r) = closest_r {
+ zones.push(l);
+ zones.push(r);
+ }
+ }
+ }
zones
}
@@ -903,7 +1042,7 @@ impl SoundFont {
for s in sample.data.iter() {
let bytes = i16::to_le_bytes(*s);
- out.write(&bytes).unwrap();
+ let _nwritten = out.write(&bytes).unwrap();
}
}
@@ -920,33 +1059,50 @@ impl SoundFont {
println!("{:?}", izone);
}
}
-
+
+ /// create a new sample request.
+ /// this struct is passed to `add_samples_interlaced()`
+ /// you might get slightly better performance if you create one request, and reuse it
+ /// (calling `set_hold_time()` each time), rather than creating a request
+ /// every time you need samples. you're probably fine either way.
+ pub fn request(&self, preset_idx: usize, key: u8, vel: u8, hold_time: f64) -> Result<SamplesRequest, SampleError> {
+ if preset_idx >= self.presets.len() {
+ return Err(SampleError::BadPreset);
+ }
+
+ let zones = self.get_zones(&self.presets[preset_idx], key, vel);
+ if zones.is_empty() {
+ return Err(SampleError::NoSamples);
+ }
+
+ Ok(SamplesRequest {
+ volume: 1.0,
+ key,
+ vel,
+ tune: 0,
+ hold_time,
+ falloff: 1.0,
+ falloff_start: 0.0,
+ zones
+ })
+ }
+
/// adds sample data to `samples` which is an i16 slice containing samples LRLRLRLR...
/// (`samples` should have even length)
/// volume in (0,1) = volume of max velocity note.
/// returns Ok(true) if the note should still be held
pub fn add_samples_interlaced(
&mut self,
- preset_idx: usize,
- volume: f32,
- // falloff: f32, @TODO
- key: u8,
- vel: u8,
- hold_time: f64,
+ request: &SamplesRequest,
samples: &mut [i16],
sample_rate: f64,
) -> Result<bool, SampleError> {
- if preset_idx >= self.presets.len() {
- return Err(SampleError::BadPreset);
- }
-
- let zones = self.get_zones(&self.presets[preset_idx], key, vel);
- if zones.len() == 0 {
- return Err(SampleError::NoSamples);
- }
+ let key = request.key;
+ let vel = request.vel;
+
let mut held = false;
- for zone in zones.iter() {
+ for zone in request.zones.iter() {
let sample = match zone.reference {
ZoneReference::SampleID(id) => &mut self.samples[id as usize],
_ => return Err(SampleError::NoSamples),
@@ -966,6 +1122,7 @@ impl SoundFont {
};
tune += (keynum as i32 - root_key as i32) * zone.scale_tuning as i32;
tune += sample.pitch_correction as i32;
+ tune += request.tune;
let freq_modulation =
f64::powf(1.0005777895065548 /* 2 ^ (1/1200) */, tune as f64);
@@ -982,24 +1139,26 @@ impl SoundFont {
} else {
vel
};
- let amplitude = volume * (velnum as f32) * (1.0 / 127.0);
+ let amplitude = request.volume * (velnum as f32) * (1.0 / 127.0);
- let mut startloop = (zone.startloop_offset as i64 + (sample.startloop as i64)) as usize;
- if startloop > sample.len as usize {
+ let mut startloop = (zone.startloop_offset as i64 + (sample.startloop as i64)) as u64;
+ if startloop > sample.len as u64 {
// uh this is bad
startloop = 0;
}
- let mut endloop = (zone.endloop_offset as i64 + (sample.endloop as i64)) as usize;
- if endloop > sample.len as usize {
+ let mut endloop = (zone.endloop_offset as i64 + (sample.endloop as i64)) as u64;
+ if endloop > sample.len as u64 {
// uh this is bad
- endloop = sample.len as usize;
+ endloop = sample.len as u64;
}
if endloop <= startloop {
// uh this is bad
startloop = 0;
- endloop = sample.len as usize;
+ endloop = sample.len as u64;
}
-
+ let data_start = zone.start_offset.clamp(0, sample.len as _);
+ let data_end = (sample.len as i32 + zone.end_offset).clamp(0, sample.len as _);
+
/*
//i've taken initial attenuation out because i dont want it (better to just control the volume).
//if you want it back in, multiply amplitude by 0.9885530946569389^attenuation = 0.1^(attenuation/200)
@@ -1009,24 +1168,30 @@ impl SoundFont {
// so a 10x larger sample will have 100x the power (and will be 20dB, not 10dB louder).
*/
- let mut t = hold_time;
- let data = &sample.data[..];
+ let mut t = request.hold_time;
+ let t_inc = 1.0 / sample_rate;
+ let data = &sample.data[data_start as usize..data_end as usize];
+ let tmul = freq_modulation * (sample.sample_rate as f64);
+ let mut falloff = f32::powf(request.falloff, (t - request.falloff_start) as f32);
+ let falloff_mul = f32::powf(request.falloff, t_inc as f32);
for i in 0..samples.len() / 2 {
- let mut s = (t * freq_modulation * sample.sample_rate as f64) as usize;
- if zone.loops {
- while s >= endloop {
- s = (s + startloop) - endloop;
- }
+ let mut s = (t * tmul) as u64;
+ if zone.loops && s >= startloop {
+ s = (s - startloop) % (endloop - startloop) + startloop;
}
- if s >= data.len() {
+ if s as usize >= data.len() {
this_held = false;
break;
}
- let sample = data[s] as f32;
-
+ let mut sample = data[s as usize] as f32;
+
+ sample *= falloff;
+ // in theory, this might cause problems because of floating-point precision
+ // in practice, it seems fine. and f32::pow is slow.
+ falloff *= falloff_mul;
samples[2 * i] += (amplitude * sample * (1.0 - pan)) as i16;
samples[2 * i + 1] += (amplitude * sample * pan) as i16;
- t += 1.0 / sample_rate;
+ t += t_inc;
}
held |= this_held;
}