diff options
-rw-r--r-- | build.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 187 | ||||
-rw-r--r-- | src/soundfont.rs | 129 |
3 files changed, 211 insertions, 109 deletions
@@ -1,7 +1,5 @@ extern crate cc; fn main() { - cc::Build::new() - .file("src/vorbis.c") - .compile("stb_vorbis"); + cc::Build::new().file("src/vorbis.c").compile("stb_vorbis"); } diff --git a/src/main.rs b/src/main.rs index 47a476b..bce1b12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,33 @@ extern crate cpal; -use std::io::Write; - use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use std::io::Write; +use std::sync::Mutex; mod midi_input; mod soundfont; -#[allow(unused)] -fn midi_in_main() -> Result<(), String> { +struct Note { + key: u8, + req: soundfont::SamplesRequest, + down: bool, + kill: bool, // only used briefly +} + +struct NoteInfo { + pitch_bend: i16, // in cents + pedal_down: bool, + notes: Vec<Note>, +} + +static NOTE_INFO: Mutex<NoteInfo> = Mutex::new(NoteInfo { + pitch_bend: 0, + notes: vec![], + pedal_down: false, +}); +static SOUNDFONT: Mutex<Option<soundfont::SoundFont>> = Mutex::new(None); + +fn playmidi_main() -> Result<(), String> { let mut device_mgr = midi_input::DeviceManager::new()?; device_mgr.set_quiet(true); let devices = device_mgr.list()?; @@ -49,33 +68,11 @@ fn midi_in_main() -> Result<(), String> { } } } - let mut device = device_mgr + let mut midi_device = device_mgr .open(device_id) .expect("error opening MIDI device"); - while device.is_connected() { - while let Some(event) = device.read_event() { - println!("{:?}", event); - } - std::thread::sleep(std::time::Duration::from_millis(140)); - if let Some(err) = device.get_error() { - eprintln!("Error: {}", err); - device.clear_error(); - } - } - Ok(()) -} - -#[allow(unused)] -fn soundfont_main() { - let mut sf = match soundfont::SoundFont::open("/etc/alternatives/default-GM.sf3") { - ///usr/share/sounds/sf2/FluidR3_GM.sf2") { - Err(x) => { - eprintln!("Error: {}", String::from(x)); - return; - } - Ok(s) => s, - }; + let mut sf = soundfont::SoundFont::open("/etc/alternatives/default-GM.sf3")?; for i in 0..sf.preset_count() { println!("{}. {}", i, sf.preset_name(i).unwrap()); @@ -89,10 +86,10 @@ fn soundfont_main() { // eprintln!("Error: {}", s); // } let host = cpal::default_host(); - let device = host + let audio_device = host .default_output_device() .expect("no output device available"); - let supported_configs = device + let supported_configs = audio_device .supported_output_configs() .expect("error while querying configs"); let mut chosen_config = None; @@ -104,49 +101,48 @@ fn soundfont_main() { } let chosen_config = match chosen_config { None => { - eprintln!("Couldn't configure audio device to have 2 16-bit channels."); - return; + return Err("Couldn't configure audio device to have 2 16-bit channels.".to_string()) } Some(x) => x, }; 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; - let mut key = 60; let preset = 299; - + { use std::time::Instant; let now = Instant::now(); sf.load_samples_for_preset(preset).expect("oh no"); println!("Loaded in {:?}", now.elapsed()); } - let stream = device + + { + let mut sflock = SOUNDFONT.lock().expect("couldn't lock soundfont."); + *sflock = Some(sf); + } + + let stream = audio_device .build_output_stream( &config, move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { - for x in data.iter_mut() { - *x = 0; - } - let sample_rate = config.sample_rate.0 as f64; - for k in key..key + 1 { - let mut request = sf.request(preset, k, 60, 0.0).expect("ah"); - request.set_hold_time(time); - request.set_volume(0.5); - request.set_tune(0); - request.set_falloff(0.0, 0.01); - match sf.add_samples_interlaced(&request, data, sample_rate) { - Ok(false) => {} //{println!("stop")}, - Err(e) => eprintln!("{}", e), - _ => {} + let mut note_info = NOTE_INFO.lock().expect("couldn't lock notes."); + let mut maybe_sf = SOUNDFONT.lock().expect("couldn't lock soundfont."); + if let Some(sf) = maybe_sf.as_mut() { + let sample_rate = config.sample_rate.0 as f64; + for x in data.iter_mut() { + *x = 0; } - } - time += (data.len() as f64) / (2.0 * sample_rate); - if time >= 0.3 { - println!("{}", sf.cache_size()); - time = 0.0; - key += 1; + let pitch_bend = note_info.pitch_bend; + for note in note_info.notes.iter_mut() { + note.req.set_tune(pitch_bend as i32); + match sf.add_samples_interlaced(&mut note.req, data, sample_rate) { + Ok(true) => {} + Ok(false) => note.kill = true, + Err(e) => eprintln!("{}", e), + } + } + note_info.notes.retain(|note| !note.kill); } }, move |err| { @@ -156,15 +152,80 @@ fn soundfont_main() { .expect("couldn't build output stream"); stream.play().expect("couldn't play stream"); - loop { - std::thread::sleep(std::time::Duration::from_millis(100)); + let note_falloff = 0.1; // falloff when note is released + while midi_device.is_connected() { + while let Some(event) = midi_device.read_event() { + let mut note_info = NOTE_INFO + .lock() + .map_err(|_| "couldn't lock notes".to_string())?; + use midi_input::Event::*; + match event { + NoteOn { note, vel, .. } => { + let mut maybe_sf = SOUNDFONT.lock().expect("couldn't lock soundfont."); + note_info.notes.retain(|n| n.key != note); + if let Some(sf) = maybe_sf.as_mut() { + match sf.request(preset, note, vel) { + Ok(mut req) => { + req.set_volume(0.1); + note_info.notes.push(Note { + key: note, + req, + down: true, + kill: false, + }); + } + Err(e) => eprintln!("get samples error: {}", e), + } + } + } + NoteOff { note, .. } => { + let pedal_down = note_info.pedal_down; + if let Some(n) = note_info.notes.iter_mut().find(|n| n.key == note) { + n.down = false; + if !pedal_down { + n.req.set_falloff(note_falloff); + } + } + } + PitchBend { amount, .. } => { + note_info.pitch_bend = amount / 128; + } + ControlChange { + controller, value, .. + } => { + if controller == 64 { + // oddly, a value of 0 means "down" + note_info.pedal_down = value < 127; + if note_info.pedal_down { + // disable falloff for all notes + for note in note_info.notes.iter_mut() { + note.req.set_falloff(1.0); + } + } else { + // start falloff for all non-down notes + for note in note_info.notes.iter_mut() { + if !note.down { + note.req.set_falloff(note_falloff); + } + } + } + } + } + _ => {} + } + } + if let Some(err) = midi_device.get_error() { + eprintln!("Error: {}", err); + midi_device.clear_error(); + } + std::thread::sleep(std::time::Duration::from_millis(5)); } + + Ok(()) } fn main() { - // match midi_in_main() { - // Err(e) => println!("{}", e), - // _ => {} - // } - soundfont_main(); + if let Err(e) = playmidi_main() { + eprintln!("Error: {:?}", e); + } } diff --git a/src/soundfont.rs b/src/soundfont.rs index 1189a15..a671d1c 100644 --- a/src/soundfont.rs +++ b/src/soundfont.rs @@ -1,3 +1,5 @@ +/// SOUNDFONT PARSER. +/// supports .sf2 and .sf3 files. /* IMPORTANT SOUNDFONT TERMINOLOGY: a PRESET is a source you can play from, e.g. "Piano", "Harpsichord", "Choir" @@ -18,8 +20,11 @@ use std::fs::File; use std::io::{Read, Seek, Write}; #[derive(Clone, Copy, Debug)] +/// type of soundfont pub enum FileType { + /// .sf2 files, as detailed in the SoundFont 2 spec: <http://www.synthfont.com/sfspec24.pdf> SF2, + /// .sf3 files (musescore's vorbis-encoded soundfont format) SF3, } @@ -41,7 +46,7 @@ struct Zone { pan: i16, // -1000 = full pan left, 1000 = full pan right force_key: i8, // -1 for no forced key, otherwise input MIDI key is replaced with this force_vel: i8, // -1 for no forced velocity - initial_attenuation: u16, // in centibels + initial_attenuation: i16, // in centibels. the spec seems to think this is unsigned, but musescore disagrees. tune: i32, // in cents reference: ZoneReference, loops: bool, @@ -91,6 +96,26 @@ struct Sample { data: Vec<i16>, } + +/// basic usage: +/// ``` +/// let sf = SoundFont::open("soundfont.sf2"); +/// for i in 0..sf.preset_count() { +/// println!("{}: {}", i, sf.preset_name(i).unwrap()) +/// } +/// sf.load_samples_for_preset(106).expect("oh no"); +/// let mut request = sf.request(106, 60, 127); +/// loop { +/// let samples = [0i16; 4096]; +/// match sf.add_samples_interlaced(&mut request, &mut samples, 44100.0) { +/// Ok(true) => {}, +/// Ok(false) => break, +/// Err(e) => eprintln!("{}", e), +/// } +/// (play samples) +/// } +/// ... +/// ``` pub struct SoundFont { file: Option<File>, file_type: FileType, @@ -119,7 +144,6 @@ pub struct SamplesRequest { hold_time: f64, key: u8, vel: u8, - falloff_start: f64, falloff: f32, tune: i32, volume: f32, @@ -131,19 +155,32 @@ mod vorbis { extern crate libc; // stb_vorbis.h seems about 3x faster than libvorbis + // (maybe i was using libvorbis wrong?) // and much easier to use. #[link(name = "stb_vorbis")] extern "C" { - fn stb_vorbis_decode_memory(mem: *const c_uchar, len: c_int, channels: *mut c_int, sample_rate: *mut c_int, output: *mut *mut i16) -> c_int; + fn stb_vorbis_decode_memory( + mem: *const c_uchar, + len: c_int, + channels: *mut c_int, + sample_rate: *mut c_int, + output: *mut *mut i16, + ) -> c_int; } - + /// decode vorbis data into PCM pub fn decode(data: &[u8]) -> Result<Vec<i16>, String> { let mut channels: c_int = 0; let mut sample_rate: c_int = 0; let mut output: *mut i16 = 0 as _; let samples = unsafe { - stb_vorbis_decode_memory(&data[0] as _, data.len() as _, (&mut channels) as _, (&mut sample_rate) as _, (&mut output) as _) + stb_vorbis_decode_memory( + &data[0] as _, + data.len() as _, + (&mut channels) as _, + (&mut sample_rate) as _, + (&mut output) as _, + ) }; if samples < 0 { Err("bad vorbis file".to_string()) @@ -156,7 +193,6 @@ mod vorbis { unsafe { libc::free(output as _) }; Ok(vec) } - } } @@ -252,7 +288,7 @@ impl Sample { for i in 0..len as usize { self.data[i] = i16::from_le_bytes([data8[2 * i], data8[2 * i + 1]]); } - }, + } FileType::SF3 => { self.data = vorbis::decode(&data8).map_err(SampleError::Vorbis)?; } @@ -412,7 +448,6 @@ impl Zone { if let ZoneReference::SampleID(id) = zone2.reference { reference = ZoneReference::SampleID(id); } - Self { key_range: (0, 0), // not relevant vel_range: (0, 0), // not relevant @@ -488,7 +523,7 @@ fn read_gen_zone(file: &mut File, zone: &mut Zone, gen_count: u16) -> Result<(), VEL_RANGE => zone.vel_range = amount_range, KEYNUM => zone.force_key = amount_i16.clamp(-1, 127) as i8, VELOCITY => zone.force_vel = amount_i16.clamp(-1, 127) as i8, - INITIAL_ATTENUATION => zone.initial_attenuation = amount_u16, + INITIAL_ATTENUATION => zone.initial_attenuation = amount_i16, COARSE_TUNE => zone.tune += (amount_i16 as i32) * 100, FINE_TUNE => zone.tune += amount_i16 as i32, SAMPLE_ID => zone.reference = ZoneReference::SampleID(amount_u16), @@ -595,6 +630,7 @@ fn read_gen_zones<Item: SFObject>( /// request for sound font samples. impl SamplesRequest { /// set amount of time note has been playing for + #[allow(unused)] pub fn set_hold_time(&mut self, t: f64) { self.hold_time = t; } @@ -609,11 +645,10 @@ impl SamplesRequest { 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) { + /// every t seconds, volume will be multiplied by amount ^ t + /// e.g. when a user lets go of a piano key, you might call `set_falloff(0.01)` + pub fn set_falloff(&mut self, amount: f32) { self.falloff = amount; - self.falloff_start = start; } } @@ -624,7 +659,7 @@ impl SoundFont { /// Instead, a handle to the file is kept open, and whenever samples are needed, /// they are loaded from the file, and cached into memory. /// If you're only dealing with a few presets, you may want to call - /// `load_samples_for_preset()` after opening to avoid lag when calling `get_samples()`. + /// `load_samples_for_preset()` after opening to avoid lag when getting samples. pub fn open(filename: &str) -> Result<Self, OpenError> { let file_type = if filename.ends_with(".sf3") { FileType::SF3 @@ -633,7 +668,9 @@ impl SoundFont { }; Self::open_file(File::open(filename)?, file_type) } - + + + /// Like `open()` but takes a file instead of a file name. pub fn open_file(mut file: File, file_type: FileType) -> Result<Self, OpenError> { const RIFF: FourCC = fourcc("RIFF"); const SFBK: FourCC = fourcc("sfbk"); @@ -715,15 +752,15 @@ impl SoundFont { if list != LIST || sdta != SDTA { return Err(bad_sound_font("no sdta chunk")); } - + let smpl = read_fourcc(&mut file)?; let _smpl_size = read_u32(&mut file)?; let smpl_offset = file.stream_position()?; - + if smpl != SMPL { return Err(bad_sound_font("no smpl chunk")); } - + file.seek(std::io::SeekFrom::Start(sdta_end))?; let list = read_fourcc(&mut file)?; @@ -949,8 +986,8 @@ impl SoundFont { let sample = Sample { start, file_len: end - start, - startloop: startloop, - endloop: endloop, + startloop, + endloop, sample_rate, root_key: original_pitch, pitch_correction, @@ -1001,8 +1038,8 @@ impl SoundFont { /// loads all sample data for the given preset into memory. /// you can use `clear_cache()` to unload them. - /// this can take a while -- musescore's sf3 file has 100MB of piano samples (when decoded) - /// in that case, there's really no good option, since loading them as needed is also slow + /// this can take a while -- musescore's sf3 file has 100MB of Grand Piano samples (when decoded). + /// in that case, there's really no good option, since loading them as needed is also slow #[allow(unused)] pub fn load_samples_for_preset(&mut self, preset_idx: usize) -> Result<(), SampleError> { if preset_idx >= self.presets.len() { @@ -1014,7 +1051,11 @@ impl SoundFont { if let ZoneReference::Instrument(inst) = pzone.reference { for izone in self.instruments[inst as usize].zones.iter() { if let ZoneReference::SampleID(sample) = izone.reference { - self.samples[sample as usize].get_data(&mut self.file, self.file_type, self.smpl_offset)?; + self.samples[sample as usize].get_data( + &mut self.file, + self.file_type, + self.smpl_offset, + )?; } } } @@ -1085,7 +1126,7 @@ impl SoundFont { if let ZoneReference::SampleID(_) = izone.reference { // seems like musescore's sf3 sets sampleType = mono even when that's not the case // (kinda makes sense since you might want to use the same sample for both L+R) - // so we'll use the pan instead + // so we'll use the pan instead let pan = pzone.pan + izone.pan; if pan < -100 { if dist < closest_l_dist { @@ -1097,11 +1138,9 @@ impl SoundFont { closest_r_dist = dist; closest_r = Some(Zone::add(pzone, izone)); } - } else { - if dist < closest_m_dist { - closest_m_dist = dist; - closest_m = Some(Zone::add(pzone, izone)); - } + } else if dist < closest_m_dist { + closest_m_dist = dist; + closest_m = Some(Zone::add(pzone, izone)); } } } @@ -1146,16 +1185,12 @@ impl SoundFont { } /// 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. + /// this struct is passed to & updated by `add_samples_interlaced()` 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); @@ -1171,20 +1206,19 @@ impl SoundFont { key, vel, tune: 0, - hold_time, + hold_time: 0.0, 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 + /// volume (0 to 1) = volume of max velocity note. + /// returns `Ok(true)` if the note should still be held. increments `request.hold_time` as needed. pub fn add_samples_interlaced( &mut self, - request: &SamplesRequest, + request: &mut SamplesRequest, samples: &mut [i16], sample_rate: f64, ) -> Result<bool, SampleError> { @@ -1200,7 +1234,7 @@ impl SoundFont { }; sample.get_data(&mut self.file, self.file_type, self.smpl_offset)?; let sample_len = sample.data.len(); - + let mut tune = zone.tune as i32; let root_key = if zone.force_root_key != -1 { zone.force_root_key as u8 @@ -1264,7 +1298,7 @@ impl SoundFont { 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 mut falloff = 1.0; // falloff accrued from these samples let falloff_mul = f32::powf(request.falloff, t_inc as f32); for i in 0..samples.len() / 2 { let mut s = (t * tmul) as u64; @@ -1278,22 +1312,31 @@ impl SoundFont { 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 += t_inc; } + + request.volume *= falloff; + if request.volume < 1.0 / 32767.0 { + this_held = false; + } + held |= this_held; } + request.hold_time += samples.len() as f64 / (2.0 * sample_rate); Ok(held) } + /// get the number of presets in this soundfont. + /// essentially, a preset is an instrument (but instrument + /// already means something in soundfont terminology...) pub fn preset_count(&self) -> usize { self.presets.len() } + /// get the name of the given preset. pub fn preset_name(&self, idx: usize) -> Option<&str> { if idx >= self.presets.len() { None |