From 3712fdf2437d78706685e5e5121cd9a56eae7c21 Mon Sep 17 00:00:00 2001 From: pommicket Date: Tue, 4 Oct 2022 11:42:36 -0400 Subject: various improvements --- src/main.rs | 27 ++--- src/midi_input.rs | 101 ++++++++++--------- src/soundfont.rs | 291 ++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 296 insertions(+), 123 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 6c1dd9a..7be93f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -#![allow(unused_imports)] // @TODO: delete me extern crate cpal; use std::io::Write; @@ -56,7 +55,7 @@ fn midi_in_main() -> Result<(), String> { while device.is_connected() { while let Some(event) = device.read_event() { - println!("{:?}",event); + println!("{:?}", event); } std::thread::sleep(std::time::Duration::from_millis(140)); if let Some(err) = device.get_error() { @@ -109,7 +108,7 @@ fn soundfont_main() { } Some(x) => x, }; - let supp_config: cpal::SupportedStreamConfig = chosen_config.with_max_sample_rate().into(); + let supp_config: cpal::SupportedStreamConfig = chosen_config.with_max_sample_rate(); if supp_config.channels() != 2 {} let config = supp_config.into(); let mut time = 0.0; @@ -124,13 +123,20 @@ fn soundfont_main() { *x = 0; } let sample_rate = config.sample_rate.0 as f64; - match sf.add_samples_interlaced(125, 1.0, key, 60, time, data, sample_rate) { + for k in key..key+1 { + let mut request = sf.request(125, k, 60, 0.0).expect("ah"); + request.set_hold_time(time); + request.set_volume(0.5); + request.set_tune(((key % 2) * 50) as _); + request.set_falloff(0.0, 0.01); + match sf.add_samples_interlaced(&request, data, sample_rate) { Ok(false) => {} //{println!("stop")}, Err(e) => eprintln!("{}", e), _ => {} } + } time += (data.len() as f64) / (2.0 * sample_rate); - if time >= 1.0 { + if time >= 0.3 { println!("{}", sf.cache_size()); time = 0.0; key += 1; @@ -149,10 +155,9 @@ fn soundfont_main() { } fn main() { - match midi_in_main() { - Err(e) => println!("{}", e), - _ => {} - } - /* - */ + // match midi_in_main() { + // Err(e) => println!("{}", e), + // _ => {} + // } + soundfont_main(); } diff --git a/src/midi_input.rs b/src/midi_input.rs index 219a8eb..dd663b9 100644 --- a/src/midi_input.rs +++ b/src/midi_input.rs @@ -236,15 +236,19 @@ mod windows { pub const MMSYSERR_NOMEM: u32 = 7; pub const MMSYSERR_INVALFLAG: u32 = 10; pub const MMSYSERR_INVALPARAM: u32 = 11; - + pub const MM_MIM_DATA: u32 = 0x3C3; - + pub fn midi_in_dev_get_caps(device_id: u32) -> Option { let mut mic = MidiInCapsW { ..Default::default() }; let result = unsafe { - midiInGetDevCapsW(device_id, &mut mic as _, std::mem::size_of::() as u32) + midiInGetDevCapsW( + device_id, + &mut mic as _, + std::mem::size_of::() as u32, + ) }; if result == 0 { Some(mic) @@ -252,18 +256,17 @@ mod windows { None } } - + // returns u32::MAX on error. pub fn midi_in_get_id(hmi: HMidiIn) -> u32 { let mut device_id = u32::MAX; unsafe { midiInGetID(hmi, (&mut device_id) as *mut u32) }; device_id } - + // returns empty string on error. pub fn get_device_name(id: u32) -> String { if let Some(mic) = midi_in_dev_get_caps(id) { - let mut name_len = 0; while name_len < mic.pname.len() && mic.pname[name_len] != 0 { name_len += 1; @@ -273,7 +276,7 @@ mod windows { String::new() } } - + pub type MidiQueue = VecDeque; pub static MIDI_QUEUES: Mutex> = Mutex::new(vec![]); pub extern "C" fn midi_callback( @@ -287,7 +290,7 @@ mod windows { // don't care return; } - + if let Ok(mut queues) = MIDI_QUEUES.lock() { for (dev, queue) in queues.iter_mut() { if *dev == midi_in { @@ -303,12 +306,12 @@ mod windows { // events with 2 data bytes queue.push_back(data1); queue.push_back(data2); - }, + } 0b1100 | 0b1101 => { // events with 1 data byte queue.push_back(data1); - }, - _ => {}, + } + _ => {} } } } @@ -556,15 +559,8 @@ impl DeviceManager { { use windows::*; let mut hmi = 0 as HMidiIn; - let result = unsafe { - midiInOpen( - (&mut hmi) as _, - id.id, - midi_callback, - 0, - CALLBACK_FUNCTION, - ) - }; + let result = + unsafe { midiInOpen((&mut hmi) as _, id.id, midi_callback, 0, CALLBACK_FUNCTION) }; if result != 0 { let result_str = match result { @@ -582,15 +578,17 @@ impl DeviceManager { result_str ))); } - + let mut queues = MIDI_QUEUES .lock() .map_err(|_| DeviceOpenError::Other("failed to lock mutex".to_string()))?; queues.push((hmi, VecDeque::new())); drop(queues); - - unsafe { midiInStart(hmi); } - + + unsafe { + midiInStart(hmi); + } + Ok(Device::new(hmi)) } } @@ -607,7 +605,7 @@ impl Device { connected: true, } } - + #[cfg(windows)] fn new(hmi: windows::HMidiIn) -> Self { let id = windows::midi_in_get_id(hmi); @@ -619,7 +617,7 @@ impl Device { last_status: 0, } } - + fn read_raw(&mut self, buffer: &mut [u8]) -> usize { #[cfg(unix)] { @@ -686,14 +684,14 @@ impl Device { pub fn is_connected(&self) -> bool { #[cfg(unix)] { - self.connected + self.connected } #[cfg(windows)] { - // there's no way of telling when a device is connected, it seems. - // this is the best we can do. - let name_expected = windows::get_device_name(windows::midi_in_get_id(self.hmi)); - !name_expected.is_empty() && name_expected == self.name + // there's no way of telling when a device is connected, it seems. + // this is the best we can do. + let name_expected = windows::get_device_name(windows::midi_in_get_id(self.hmi)); + !name_expected.is_empty() && name_expected == self.name } } @@ -723,25 +721,28 @@ impl Device { // new status self.last_status = status; self.buffered_byte = None; - + if (status >> 4) == 0b1111 { return Some(Event::Other { status, data: None }); } - + byte = self.read_byte()?; if (byte & 0x80) != 0 { // no data provided for MIDI event which should have data. // what can we do... // returning None is bad because it stops the stream. - return Some(Event::Other { status, data: Some(byte & 0x7f) }); + return Some(Event::Other { + status, + data: Some(byte & 0x7f), + }); } } - + let channel = self.last_status & 0xf; - + // at this point we have a data byte assert!((byte & 0x80) == 0); - + match self.last_status >> 4 { 0b1000 | 0b1001 | 0b1010 | 0b1011 | 0b1110 => { // event with two bytes of data. @@ -755,19 +756,21 @@ impl Device { note: data1, vel: data2, }, - 0b1001 => if data2 == 0 { - Event::NoteOff { - channel, - note: data1, - vel: data2, + 0b1001 => { + if data2 == 0 { + Event::NoteOff { + channel, + note: data1, + vel: data2, + } + } else { + Event::NoteOn { + channel, + note: data1, + vel: data2, + } } - } else { - Event::NoteOn { - channel, - note: data1, - vel: data2, - } - }, + } 0b1010 => Event::NoteAftertouch { channel, note: data1, 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 +} + 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 { +fn read_utf8_fixed_len(file: &mut File, len: usize) -> Result { 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( 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 { + 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 { - 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; } -- cgit v1.2.3