diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 78 | ||||
-rw-r--r-- | src/soundfont.rs | 430 |
2 files changed, 319 insertions, 189 deletions
diff --git a/src/main.rs b/src/main.rs index 4f6089e..b14f9e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate cpal; use std::io::Write; -use cpal::traits::{HostTrait, DeviceTrait, StreamTrait}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; mod midi_input; mod soundfont; @@ -70,7 +70,7 @@ fn playmidi_main() -> Result<(), String> { } fn main() { - let mut sf = match soundfont::SoundFont::open("/etc/alternatives/default-GM.sf2") { + let mut sf = match soundfont::SoundFont::open("/usr/share/sounds/sf2/FluidR3_GM.sf2") { Err(x) => { eprintln!("Error: {}", String::from(x)); return; @@ -78,17 +78,23 @@ fn main() { Ok(s) => s, }; - for i in 0..sf.preset_count() { - println!("{}. {}", i, sf.preset_name(i).unwrap()); - } + for i in 0..sf.preset_count() { + println!("{}. {}", i, sf.preset_name(i).unwrap()); + } + + //sf._debug_preset_zones(125); + //sf._debug_instrument_zones(148); // let result = playmidi_main(); // if let Err(s) = result { // eprintln!("Error: {}", s); // } let host = cpal::default_host(); - let device = host.default_output_device().expect("no output device available"); - let supported_configs = device.supported_output_configs() + let device = host + .default_output_device() + .expect("no output device available"); + let supported_configs = device + .supported_output_configs() .expect("error while querying configs"); let mut chosen_config = None; for config in supported_configs { @@ -101,39 +107,41 @@ fn main() { None => { eprintln!("Couldn't configure audio device to have 2 16-bit channels."); return; - }, + } Some(x) => x, }; - let supp_config: cpal::SupportedStreamConfig = chosen_config - .with_max_sample_rate() - .into(); - if supp_config.channels() != 2 { - } + let supp_config: cpal::SupportedStreamConfig = chosen_config.with_max_sample_rate().into(); + if supp_config.channels() != 2 {} let config = supp_config.into(); let mut time = 0.0; let mut key = 60; - let stream = 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; - match sf.add_samples_interlaced(126, 1.0, key, 60, time, data, sample_rate) { - Ok(false) => {println!("stop")}, - Err(e) => eprintln!("{}", e), - _ => {}, - } - time += (data.len() as f64) / (2.0 * sample_rate); - if time >= 1.0 { - time = 0.0; - key += 1; - } - }, - move |err| { - println!("audio stream error: {}", err); - }, - ).expect("couldn't build output stream"); + sf.load_samples_for_preset(125).expect("oh no"); + + let stream = 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; + match sf.add_samples_interlaced(125, 1.0, key, 60, time, data, sample_rate) { + Ok(false) => {} //{println!("stop")}, + Err(e) => eprintln!("{}", e), + _ => {} + } + time += (data.len() as f64) / (2.0 * sample_rate); + if time >= 1.0 { + println!("{}", sf.cache_size()); + time = 0.0; + key += 1; + } + }, + move |err| { + println!("audio stream error: {}", err); + }, + ) + .expect("couldn't build output stream"); stream.play().expect("couldn't play stream"); loop { diff --git a/src/soundfont.rs b/src/soundfont.rs index 9841a47..5bede73 100644 --- a/src/soundfont.rs +++ b/src/soundfont.rs @@ -13,10 +13,8 @@ IMPORTANT SOUNDFONT TERMINOLOGY: a SAMPLE is a block of audio data with some properties of how it should be played */ -#![allow(unused)] // @TODO: delete me -use std::collections::HashMap; use std::fs::File; -use std::io::{Read, Seek}; +use std::io::{Read, Seek, Write}; #[derive(Clone, Debug)] enum ZoneReference { @@ -25,12 +23,6 @@ enum ZoneReference { Instrument(u16), // for preset zones } -impl ZoneReference { - fn is_none(&self) -> bool { - matches!(self, ZoneReference::None) - } -} - #[derive(Clone, Debug)] struct Zone { key_range: (u8, u8), @@ -39,12 +31,11 @@ struct Zone { end_offset: i32, startloop_offset: i32, endloop_offset: i32, - pan: i16, // -1000 = full pan left, 1000 = full pan right - is_global: bool, - force_key: i8, // -1 for no forced key, otherwise input MIDI key is replaced with this - force_vel: i8, // -1 for no forced velocity + 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 - tune: i32, // in cents + tune: i32, // in cents reference: ZoneReference, loops: bool, scale_tuning: u16, // 100 = normal tuning, 50 = each MIDI key is a *quarter* tone, etc. @@ -58,6 +49,7 @@ trait SFObject { #[derive(Default)] struct Instrument { + #[allow(unused)] name: String, zones: Vec<Zone>, } @@ -80,7 +72,7 @@ impl SFObject for Preset { } } -#[derive(Debug, Clone)] +#[derive(Debug)] struct Sample { start: u32, len: u32, @@ -89,18 +81,15 @@ struct Sample { sample_rate: u32, root_key: u8, pitch_correction: i8, + data: Vec<i16>, } -const FILE_CACHE_CHUNK_SIZE: usize = 4096; - pub struct SoundFont { - file: File, + file: Option<File>, sdta_offset: u64, presets: Vec<Preset>, instruments: Vec<Instrument>, samples: Vec<Sample>, - // maps (offset, len) to sample data - file_cache: HashMap<(u32, u32), Vec<i16>>, pub name: String, } @@ -111,9 +100,10 @@ pub enum OpenError { } pub enum SampleError { + IO(std::io::Error), BadPreset, NoSamples, - IO(std::io::Error), + NoFile, } impl From<&OpenError> for String { @@ -145,6 +135,12 @@ impl std::fmt::Debug for OpenError { } } +impl From<std::io::Error> for SampleError { + fn from(err: std::io::Error) -> SampleError { + SampleError::IO(err) + } +} + impl From<&SampleError> for String { fn from(err: &SampleError) -> String { use SampleError::*; @@ -152,6 +148,7 @@ impl From<&SampleError> for String { IO(e) => format!("IO error: {}", e), BadPreset => "bad preset index".to_string(), NoSamples => "no samples".to_string(), + NoFile => "file is closed, but samples from it are needed".to_string(), } } } @@ -174,6 +171,37 @@ impl std::fmt::Debug for SampleError { } } +impl Sample { + // fills in the self.data field if necessary. + fn get_data( + &mut self, + maybe_file: &mut Option<File>, + sdta_offset: u64, + ) -> Result<(), SampleError> { + if self.data.is_empty() { + match maybe_file { + None => Err(SampleError::NoFile), + Some(file) => { + file.seek(std::io::SeekFrom::Start( + sdta_offset + 2 * self.start as u64, + ))?; + let len = self.len as usize; + let mut data8 = vec![0u8; 2 * len]; + file.read_exact(&mut data8)?; + + self.data = vec![0i16; len]; + for i in 0..len as usize { + self.data[i] = i16::from_le_bytes([data8[2 * i], data8[2 * i + 1]]); + } + Ok(()) + } + } + } else { + Ok(()) + } + } +} + #[derive(PartialEq, Eq)] struct FourCC(u8, u8, u8, u8); @@ -207,30 +235,30 @@ const fn fourcc(s: &str) -> FourCC { fn read_fourcc(f: &mut File) -> Result<FourCC, OpenError> { let mut bytes = [0; 4]; - f.read_exact(&mut bytes); + f.read_exact(&mut bytes)?; FourCC::new(bytes[0], bytes[1], bytes[2], bytes[3]).ok_or(OpenError::NotASoundFont) } -fn read_u8(f: &mut File) -> u8 { +fn read_u8(f: &mut File) -> std::io::Result<u8> { let mut bytes = [0; 1]; - f.read_exact(&mut bytes); - bytes[0] + f.read_exact(&mut bytes)?; + Ok(bytes[0]) } -fn read_u16(f: &mut File) -> u16 { +fn read_u16(f: &mut File) -> std::io::Result<u16> { let mut bytes = [0; 2]; - f.read_exact(&mut bytes); - u16::from_le_bytes(bytes) + f.read_exact(&mut bytes)?; + Ok(u16::from_le_bytes(bytes)) } -fn read_u32(f: &mut File) -> u32 { +fn read_u32(f: &mut File) -> std::io::Result<u32> { let mut bytes = [0; 4]; - f.read_exact(&mut bytes); - u32::from_le_bytes(bytes) + f.read_exact(&mut bytes)?; + Ok(u32::from_le_bytes(bytes)) } -fn read_i8(f: &mut File) -> i8 { - read_u8(f) as i8 +fn read_i8(f: &mut File) -> std::io::Result<i8> { + Ok(read_u8(f)? as i8) } fn bad_sound_font(s: &str) -> OpenError { @@ -239,7 +267,7 @@ fn bad_sound_font(s: &str) -> OpenError { fn read_utf8_fixed_len(file: &mut File, len: usize, what: &str) -> Result<String, OpenError> { let mut name_vec = vec![0; len]; - file.read_exact(&mut name_vec); + file.read_exact(&mut name_vec)?; while !name_vec.is_empty() && name_vec[name_vec.len() - 1] == 0 { name_vec.pop(); } @@ -257,7 +285,6 @@ impl Zone { startloop_offset: 0, endloop_offset: 0, pan: 0, - is_global: false, force_key: -1, force_vel: -1, initial_attenuation: 0, @@ -269,8 +296,14 @@ impl Zone { } } + fn is_global(&self) -> bool { + // a "global" zone is a template for all other zones. + // global zones have no instrument or sample ID. + matches!(self.reference, ZoneReference::None) + } + fn contains(&self, key: u8, vel: u8) -> bool { - if self.is_global { + if self.is_global() { return false; } @@ -279,8 +312,8 @@ impl Zone { && vel >= self.vel_range.0 && vel <= self.vel_range.1 } - - // the zone for a note is generated by adding + + // the zone for a note is generated by adding fn add(zone1: &Self, zone2: &Self) -> Self { fn add_forced(a: i8, b: i8) -> i8 { // the standard isn't really clear about this @@ -293,7 +326,7 @@ impl Zone { a + b } } - + let mut reference = ZoneReference::None; if let ZoneReference::SampleID(id) = zone1.reference { reference = ZoneReference::SampleID(id); @@ -301,11 +334,10 @@ 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 - is_global: false, // not relevant + key_range: (0, 0), // not relevant + vel_range: (0, 0), // not relevant start_offset: zone1.start_offset + zone2.start_offset, end_offset: zone1.end_offset + zone2.end_offset, startloop_offset: zone1.startloop_offset + zone2.startloop_offset, @@ -323,15 +355,18 @@ impl Zone { } } -fn read_gen_zone(file: &mut File, zone: &mut Zone, gen_count: u16) { +fn read_gen_zone(file: &mut File, zone: &mut Zone, gen_count: u16) -> Result<(), OpenError> { for _gen_ndx in 0..gen_count { - let gen_type = read_u16(file); - let amount_u16 = read_u16(file); + let gen_type = read_u16(file)?; + let amount_u16 = read_u16(file)?; let amount_i16 = amount_u16 as i16; let mut amount_range = ((amount_u16 >> 8) as u8, amount_u16 as u8); // "LS byte indicates the highest and the MS byte the lowest valid key." (soundfont ยง 8.1.2) // but "TimGM6mb.sf2" seems to disagree. maybe something something endianness. - amount_range = (u8::min(amount_range.0, amount_range.1), u8::max(amount_range.0, amount_range.1)); + amount_range = ( + u8::min(amount_range.0, amount_range.1), + u8::max(amount_range.0, amount_range.1), + ); mod gen { // generators @@ -387,6 +422,7 @@ fn read_gen_zone(file: &mut File, zone: &mut Zone, gen_count: u16) { _ => {} } } + Ok(()) } // reads the ibag or pbag chunk of a soundfont @@ -395,21 +431,25 @@ struct GenIndex { gen: u16, } +#[allow(unused)] struct ModIndex { obj: u16, r#mod: u16, } // read pbag or ibag -fn read_bag_chunk(file: &mut File, bag_indices: Vec<u16>) -> (Vec<GenIndex>, Vec<ModIndex>) { +fn read_bag_chunk( + file: &mut File, + bag_indices: Vec<u16>, +) -> Result<(Vec<GenIndex>, Vec<ModIndex>), OpenError> { let mut gen_indices = vec![]; let mut mod_indices = vec![]; for obj_ndx in 0..bag_indices.len() - 1 { let start_ndx = bag_indices[obj_ndx]; let end_ndx = bag_indices[obj_ndx + 1]; - for i in start_ndx..end_ndx { - let gen_ndx = read_u16(file); - let mod_ndx = read_u16(file); + for _i in start_ndx..end_ndx { + let gen_ndx = read_u16(file)?; + let mod_ndx = read_u16(file)?; gen_indices.push(GenIndex { obj: obj_ndx as u16, gen: gen_ndx, @@ -424,8 +464,8 @@ fn read_bag_chunk(file: &mut File, bag_indices: Vec<u16>) -> (Vec<GenIndex>, Vec { // terminal zone let obj_ndx = bag_indices.len(); - let gen_ndx = read_u16(file); - let mod_ndx = read_u16(file); + let gen_ndx = read_u16(file)?; + let mod_ndx = read_u16(file)?; gen_indices.push(GenIndex { obj: obj_ndx as u16, gen: gen_ndx, @@ -436,7 +476,7 @@ fn read_bag_chunk(file: &mut File, bag_indices: Vec<u16>) -> (Vec<GenIndex>, Vec }); } - (gen_indices, mod_indices) + Ok((gen_indices, mod_indices)) } // read pgen or igen chunk @@ -444,8 +484,8 @@ fn read_gen_zones<Item: SFObject>( file: &mut File, items: &mut [Item], gen_indices: Vec<GenIndex>, - mod_indices: Vec<ModIndex>, -) { + _mod_indices: Vec<ModIndex>, +) -> Result<(), OpenError> { let mut prev_inst_ndx = u16::MAX; let mut global_zone: Option<Zone> = None; for zone_ndx in 0..gen_indices.len() - 1 { @@ -462,21 +502,26 @@ fn read_gen_zones<Item: SFObject>( } prev_inst_ndx = inst_ndx; - read_gen_zone(file, &mut zone, end_gen - start_gen); + read_gen_zone(file, &mut zone, end_gen - start_gen)?; - if zone.reference.is_none() { - // this is a global zone - zone.is_global = true; + if zone.is_global() { + // this is a global zone. everyone should copy it. global_zone = Some(zone.clone()); } else { items[inst_ndx as usize].add_zone(zone); } } + Ok(()) } impl SoundFont { /// Open a soundfont. - /// Note: SoundFont keeps a handle to the file. + /// This does not load any sample data, since that would be slow + /// (soundfont files can be hundreds of megabytes large). + /// 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()`. pub fn open(filename: &str) -> Result<Self, OpenError> { const RIFF: FourCC = fourcc("RIFF"); const SFBK: FourCC = fourcc("sfbk"); @@ -502,7 +547,7 @@ impl SoundFont { return Err(OpenError::NotASoundFont); } - let _sfbk_size = read_u32(&mut file); + let _sfbk_size = read_u32(&mut file)?; let sfbk = read_fourcc(&mut file)?; if sfbk != SFBK { @@ -513,7 +558,7 @@ impl SoundFont { // at this point, the file *should* be a soundfont. let list = read_fourcc(&mut file)?; - let info_size = read_u32(&mut file); + let info_size = read_u32(&mut file)?; let info_end = file.stream_position()? + info_size as u64; let info = read_fourcc(&mut file)?; if list != LIST || info != INFO { @@ -525,13 +570,13 @@ impl SoundFont { // read INFO data while file.stream_position()? < info_end { let chunk_type = read_fourcc(&mut file)?; - let chunk_size = read_u32(&mut file); + let chunk_size = read_u32(&mut file)?; let chunk_end = file.stream_position()? + chunk_size as u64; if chunk_type == INAM { if chunk_size < 256 { let mut data = vec![0; chunk_size as usize]; - file.read(&mut data); + file.read(&mut data)?; data.pop(); // null terminator if let Ok(n) = String::from_utf8(data) { name = Some(n); @@ -542,7 +587,7 @@ impl SoundFont { } } - file.seek(std::io::SeekFrom::Start(chunk_end)); + file.seek(std::io::SeekFrom::Start(chunk_end))?; } let name_unwrapped = match name { @@ -551,7 +596,7 @@ impl SoundFont { }; let list = read_fourcc(&mut file)?; - let sdta_size = read_u32(&mut file); + let sdta_size = read_u32(&mut file)?; let sdta_offset = file.stream_position()?; let sdta_end = sdta_offset + sdta_size as u64; let sdta = read_fourcc(&mut file)?; @@ -560,10 +605,10 @@ impl SoundFont { return Err(bad_sound_font("no sdta chunk")); } - file.seek(std::io::SeekFrom::Start(sdta_end)); + file.seek(std::io::SeekFrom::Start(sdta_end))?; let list = read_fourcc(&mut file)?; - let pdta_size = read_u32(&mut file); + let pdta_size = read_u32(&mut file)?; let pdta_end = file.stream_position()? + pdta_size as u64; let pdta = read_fourcc(&mut file)?; if list != LIST || pdta != PDTA { @@ -594,7 +639,7 @@ impl SoundFont { // read pdta data while file.stream_position()? < pdta_end { let chunk_type = read_fourcc(&mut file)?; - let chunk_size = read_u32(&mut file); + let chunk_size = read_u32(&mut file)?; let chunk_end = file.stream_position()? + chunk_size as u64; let chunk = Chunk { @@ -614,7 +659,7 @@ impl SoundFont { _ => {} } - file.seek(std::io::SeekFrom::Start(chunk_end)); + file.seek(std::io::SeekFrom::Start(chunk_end))?; } if inst.offset == 0 { @@ -641,14 +686,14 @@ impl SoundFont { // --- read inst chunk --- { - file.seek(std::io::SeekFrom::Start(inst.offset)); + file.seek(std::io::SeekFrom::Start(inst.offset))?; loop { let name = read_utf8_fixed_len(&mut file, 20, "instrument name")?; if name.is_empty() { return Err(bad_sound_font("instrument with no name.")); } - let bag_ndx = read_u16(&mut file); + let bag_ndx = read_u16(&mut file)?; let is_eoi = name == "EOI"; @@ -670,41 +715,41 @@ impl SoundFont { } // --- read ibag chunk --- - file.seek(std::io::SeekFrom::Start(ibag.offset)); + file.seek(std::io::SeekFrom::Start(ibag.offset))?; // these are vecs of (instrument idx, gen idx) // and (instrument idx, mod idx) let (instrument_gen_indices, instrument_mod_indices) = - read_bag_chunk(&mut file, instrument_bag_indices); + read_bag_chunk(&mut file, instrument_bag_indices)?; // --- read igen chunk --- // annoyingly, the igen chunk appears after the imod chunk, even though you need it first // to figure out which modifiers are global. - file.seek(std::io::SeekFrom::Start(igen.offset)); + file.seek(std::io::SeekFrom::Start(igen.offset))?; read_gen_zones( &mut file, &mut instruments, instrument_gen_indices, instrument_mod_indices, - ); + )?; // --- read phdr chunk --- let mut presets = vec![]; let mut preset_bag_indices = vec![]; - file.seek(std::io::SeekFrom::Start(phdr.offset)); + file.seek(std::io::SeekFrom::Start(phdr.offset))?; if phdr.size < 38 * 2 || phdr.size % 38 != 0 { return Err(OpenError::BadSoundFont(format!( "Bad PHDR size: {}", phdr.size ))); } - for i in 0..phdr.size / 38 { + for _i in 0..phdr.size / 38 { let name = read_utf8_fixed_len(&mut file, 20, "preset name")?; - let preset = read_u16(&mut file); - let bank = read_u16(&mut file); - let bag_ndx = read_u16(&mut file); - let library = read_u32(&mut file); - let genre = read_u32(&mut file); - let morphology = read_u32(&mut file); + let _preset = read_u16(&mut file)?; + let _bank = read_u16(&mut file)?; + let bag_ndx = read_u16(&mut file)?; + let _library = read_u32(&mut file)?; + let _genre = read_u32(&mut file)?; + let _morphology = read_u32(&mut file)?; presets.push(Preset { name, ..Default::default() @@ -713,50 +758,49 @@ impl SoundFont { } // --- read pbag chunk --- - file.seek(std::io::SeekFrom::Start(pbag.offset)); + file.seek(std::io::SeekFrom::Start(pbag.offset))?; // these are vecs of (preset idx, gen idx) // and (preset idx, mod idx) let (preset_gen_indices, preset_mod_indices) = - read_bag_chunk(&mut file, preset_bag_indices); + read_bag_chunk(&mut file, preset_bag_indices)?; // --- read pgen chunk --- - file.seek(std::io::SeekFrom::Start(pgen.offset)); + file.seek(std::io::SeekFrom::Start(pgen.offset))?; read_gen_zones( &mut file, &mut presets, preset_gen_indices, preset_mod_indices, - ); + )?; // --- read shdr chunk --- - file.seek(std::io::SeekFrom::Start(shdr.offset)); + file.seek(std::io::SeekFrom::Start(shdr.offset))?; let samples_count = shdr.size / 46; let mut samples = Vec::with_capacity(samples_count as usize); - for i in 0..shdr.size / 46 { + for _i in 0..shdr.size / 46 { // a sample let sample_name = read_utf8_fixed_len(&mut file, 20, "sample name")?; if sample_name == "EOS" { break; } - let start = read_u32(&mut file); - let end = read_u32(&mut file); - let startloop = read_u32(&mut file); - let endloop = read_u32(&mut file); + let start = read_u32(&mut file)?; + let end = read_u32(&mut file)?; + let startloop = read_u32(&mut file)?; + let endloop = read_u32(&mut file)?; /* for sample rates: "If an illegal or impractical value is encountered, the nearest practical value should be used" */ - let sample_rate = read_u32(&mut file).clamp(400, 100000); - let mut original_pitch = read_u8(&mut file); + let sample_rate = read_u32(&mut file)?.clamp(400, 100000); + let mut original_pitch = read_u8(&mut file)?; if original_pitch == 255 { // unpitched instrument original_pitch = 60; } - let pitch_correction = read_i8(&mut file); - let _sample_link = read_u16(&mut file); - let _sample_type = read_u16(&mut file); - + let pitch_correction = read_i8(&mut file)?; + let _sample_link = read_u16(&mut file)?; + let _sample_type = read_u16(&mut file)?; let sample = Sample { start, @@ -766,6 +810,7 @@ impl SoundFont { sample_rate, root_key: original_pitch, pitch_correction, + data: vec![], }; samples.push(sample); } @@ -774,34 +819,62 @@ impl SoundFont { presets.pop(); // remove EOP Ok(SoundFont { - file, + file: Some(file), sdta_offset, name: name_unwrapped, instruments, samples, presets, - file_cache: HashMap::new(), }) } - pub fn clear_cache(&mut self) { - self.file_cache.clear(); + /// loads all sample data for the given preset into memory. + /// you can use `clear_cache()` to unload them. + #[allow(unused)] + pub fn load_samples_for_preset(&mut self, preset_idx: usize) -> Result<(), SampleError> { + if preset_idx >= self.presets.len() { + return Err(SampleError::BadPreset); + } + + let preset = &self.presets[preset_idx]; + for pzone in preset.zones.iter() { + 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.sdta_offset)?; + } + } + } + } + Ok(()) } - fn get_samples(&mut self, start: u32, len: u32) -> &[i16] { - if self.file_cache.get(&(start, len)).is_none() { - self.file.seek(std::io::SeekFrom::Start(self.sdta_offset + 2 * start as u64)); - let mut data8 = vec![0u8; 2 * (len as usize)]; - self.file.read_exact(&mut data8); + /// close the input file. + /// no more samples can be loaded. + /// before calling this function, call `load_samples_for_preset()` with the preset(s) you want to use. + /// do not call `clear_cache()` after this function, or you won't be able to get any samples. + #[allow(unused)] + pub fn close_file(&mut self) { + self.file = None; + } - let mut data16 = vec![0i16; len as usize]; - for i in 0..len as usize { - data16[i] = i16::from_le_bytes([data8[2 * i], data8[2 * i + 1]]); - } + /// clears any cached samples. + /// use with `cache_size()` if you really care about memory usage. + #[allow(unused)] + pub fn clear_cache(&mut self) { + for sample in self.samples.iter_mut() { + sample.data.clear(); + } + } - self.file_cache.insert((start, len), data16); + /// size of all cached samples in bytes. + #[allow(unused)] + pub fn cache_size(&self) -> usize { + let mut total = 0; + for sample in self.samples.iter() { + total += sample.data.len() * 2; } - self.file_cache.get(&(start, len)).unwrap() + total } fn get_zones(&self, preset: &Preset, key: u8, vel: u8) -> Vec<Zone> { @@ -818,9 +891,37 @@ impl SoundFont { } } } + + // @TODO: find closest zone if zones.len() == 0 + zones } + fn _debug_sample_to_file(&mut self, sample: &Sample) { + let filename = "raw_sample.out"; + println!("Exporting {}Hz sample to {}.", sample.sample_rate, filename); + let mut out = std::fs::File::create(filename).unwrap(); + + for s in sample.data.iter() { + let bytes = i16::to_le_bytes(*s); + out.write(&bytes).unwrap(); + } + } + + pub fn _debug_preset_zones(&self, preset_idx: usize) { + let preset = &self.presets[preset_idx]; + for pzone in preset.zones.iter() { + println!("{:?}", pzone); + } + } + + pub fn _debug_instrument_zones(&self, inst_idx: usize) { + let inst = &self.instruments[inst_idx]; + for izone in inst.zones.iter() { + println!("{:?}", izone); + } + } + /// 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. @@ -829,7 +930,7 @@ impl SoundFont { &mut self, preset_idx: usize, volume: f32, - // falloff: f32, @TODO + // falloff: f32, @TODO key: u8, vel: u8, hold_time: f64, @@ -840,71 +941,92 @@ impl SoundFont { return Err(SampleError::BadPreset); } - let zones = self - .get_zones(&self.presets[preset_idx], key, vel); + let zones = self.get_zones(&self.presets[preset_idx], key, vel); if zones.len() == 0 { return Err(SampleError::NoSamples); } let mut held = false; - + for zone in zones.iter() { let sample = match zone.reference { - ZoneReference::SampleID(id) => self.samples[id as usize].clone(), + ZoneReference::SampleID(id) => &mut self.samples[id as usize], _ => return Err(SampleError::NoSamples), }; - + sample.get_data(&mut self.file, self.sdta_offset)?; + let mut tune = zone.tune as i32; let root_key = if zone.force_root_key != -1 { - zone.force_root_key as u8 - } else { - sample.root_key - }; + zone.force_root_key as u8 + } else { + sample.root_key + }; let keynum = if zone.force_key != -1 { - zone.force_key as u8 - } else { - key - }; + zone.force_key as u8 + } else { + key + }; tune += (keynum as i32 - root_key as i32) * zone.scale_tuning as i32; tune += sample.pitch_correction as i32; - let freq_modulation = f64::powf(1.0005777895065548 /* 2 ^ (1/1200) */, tune as f64); - - let data = self.get_samples(sample.start, sample.len); - + let freq_modulation = + f64::powf(1.0005777895065548 /* 2 ^ (1/1200) */, tune as f64); + + // if key == 60 && hold_time == 0.0 { + // self._debug_sample_to_file(&sample); + // } + let mut this_held = true; - + let pan = zone.pan as f32 * 0.001 + 0.5; - + let velnum = if zone.force_vel != -1 { - zone.force_vel as u8 - } else { - vel - }; - let mut amplitude = volume * (velnum as f32) * (1.0 / 127.0); + zone.force_vel as u8 + } else { + vel + }; + let amplitude = 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 { + // 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 { + // uh this is bad + endloop = sample.len as usize; + } + if endloop <= startloop { + // uh this is bad + startloop = 0; + endloop = sample.len as usize; + } + /* - i've taken initial attenuation out because i dont want it (better to just control the volume). - if you want it back in: - //0.9885530946569389 = 0.1^0.005 - // initial attenuation is in cB, so why 0.005 instead of 0.1? - // i have no clue. if you do a -10dB gain on audacity, - // it decreases all the samples by a factor of sqrt(10), not 10. - // some bullshit audio thing or something. - amplitude *= f32::powf(0.9885530946569389, zone.initial_attenuation as f32); + //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) + // initial attenuation is in cB, so why 1/200 instead of 1/100? + // here's the key: audio samples measure voltage, not power + // P = V^2 / R + // 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[..]; for i in 0..samples.len() / 2 { let mut s = (t * freq_modulation * sample.sample_rate as f64) as usize; if zone.loops { - // @TODO alter s + while s >= endloop { + s = (s + startloop) - endloop; + } } if s >= data.len() { this_held = false; break; } let sample = data[s] as f32; - - samples[2*i] += (amplitude * sample * (1.0 - pan)) as i16; - samples[2*i+1] += (amplitude * sample * pan) as i16; + + samples[2 * i] += (amplitude * sample * (1.0 - pan)) as i16; + samples[2 * i + 1] += (amplitude * sample * pan) as i16; t += 1.0 / sample_rate; } held |= this_held; |