diff options
Diffstat (limited to 'src/soundfont.rs')
-rw-r--r-- | src/soundfont.rs | 291 |
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; } |