From 3d47e5b9ccbc74bc99d7196a89c4d2b4191e3356 Mon Sep 17 00:00:00 2001 From: pommicket Date: Fri, 30 Sep 2022 12:13:34 -0400 Subject: start presets --- .gitignore | 4 + src/main.rs | 3 +- src/soundfont.rs | 557 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 519 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..3ccdc79 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /target +tags +TAGS +*~ +*.out diff --git a/src/main.rs b/src/main.rs index 157533f..47bc913 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,7 +69,7 @@ fn playmidi_main() -> Result<(), String> { } fn main() { - let sf = match soundfont::SoundFont::open("/etc/alternatives/default-GM.sf2") { + let _sf = match soundfont::SoundFont::open("/etc/alternatives/default-GM.sf2") { Err(x) => { eprintln!("Error: {}", String::from(x)); return; @@ -77,7 +77,6 @@ fn main() { Ok(s) => s, }; - println!("{}",sf.name); // let result = playmidi_main(); // if let Err(s) = result { diff --git a/src/soundfont.rs b/src/soundfont.rs index acd96a6..79cd312 100644 --- a/src/soundfont.rs +++ b/src/soundfont.rs @@ -1,21 +1,113 @@ #![allow(unused)] // @TODO: delete me use std::fs::File; use std::io::{Read, Seek}; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +enum ZoneReference { + None, + SampleID(u16), // for instrument zones + Instrument(u16) // for preset zones +} + +impl ZoneReference { + fn is_none(&self) -> bool { + match &self { + ZoneReference::None => true, + _ => false, + } + } +} + +#[derive(Clone, Debug)] +struct Zone { + key_range: (u8, u8), + vel_range: (u8, u8), + inst: u32, + start_offset: i64, + end_offset: i64, + startloop_offset: i64, + endloop_offset: i64, + 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 + initial_attenuation: u16, // in centibels + tune: i32, // in cents + reference: ZoneReference, + loops: bool, + scale_tuning: u16, // 100 = normal tuning, 50 = each MIDI key is a *quarter* tone, etc. + force_root_key: i8, // -1 for no forced root key +} + +// Instrument and Preset implement this (and nothing else does) +trait SFObject { + fn add_zone(&mut self, zone: Zone); +} + +#[derive(Default)] +struct Instrument { + name: String, + zones: Vec +} + +impl SFObject for Instrument { + fn add_zone(&mut self, zone: Zone) { + self.zones.push(zone); + } +} + +#[derive(Default)] +pub struct Preset { + name: String, + zones: Vec +} + +impl SFObject for Preset { + fn add_zone(&mut self, zone: Zone) { + self.zones.push(zone); + } +} + + +#[derive(Debug)] +struct Sample { + offset: u32, + len: u32, + startloop: u32, + endloop: u32, + sample_rate: u32, + pitch: u8, + pitch_correction: i8, + data: Vec, +} + +const FILE_CACHE_CHUNK_SIZE: usize = 4096; pub struct SoundFont { file: Option, + presets: Vec, + instruments: Vec, + samples: Vec, + // maps (offset, len) to sample data + file_cache: HashMap<(u32, u32), Vec>, pub name: String } -pub enum Error { +pub enum OpenError { IO(std::io::Error), NotASoundFont, BadSoundFont(String), } -impl From<&Error> for String { - fn from(err: &Error) -> String { - use Error::*; +pub enum PresetError { + BadPreset, + IO(std::io::Error), +} + +impl From<&OpenError> for String { + fn from(err: &OpenError) -> String { + use OpenError::*; match err { IO(e) => format!("IO error: {}", e), NotASoundFont => "not a sound font".to_string(), @@ -24,15 +116,31 @@ impl From<&Error> for String { } } -impl From for String { - fn from(err: Error) -> String { +impl From for String { + fn from(err: OpenError) -> String { + String::from(&err) + } +} + +impl From<&PresetError> for String { + fn from(err: &PresetError) -> String { + use PresetError::*; + match err { + IO(e) => format!("IO error: {}", e), + BadPreset => format!("bad preset index"), + } + } +} + +impl From for String { + fn from(err: PresetError) -> String { String::from(&err) } } -impl From for Error { - fn from(err: std::io::Error) -> Error { - Error::IO(err) +impl From for OpenError { + fn from(err: std::io::Error) -> OpenError { + OpenError::IO(err) } } @@ -67,16 +175,16 @@ const fn fourcc(s: &str) -> FourCC { } } -fn read_fourcc(f: &mut File) -> Result { +fn read_fourcc(f: &mut File) -> Result { let mut bytes = [0; 4]; f.read_exact(&mut bytes); - FourCC::new(bytes[0], bytes[1], bytes[2], bytes[3]).ok_or(Error::NotASoundFont) + FourCC::new(bytes[0], bytes[1], bytes[2], bytes[3]).ok_or(OpenError::NotASoundFont) } -fn read_u32(f: &mut File) -> u32 { - let mut bytes = [0; 4]; +fn read_u8(f: &mut File) -> u8 { + let mut bytes = [0; 1]; f.read_exact(&mut bytes); - u32::from_le_bytes(bytes) + bytes[0] } fn read_u16(f: &mut File) -> u16 { @@ -85,16 +193,181 @@ fn read_u16(f: &mut File) -> u16 { u16::from_le_bytes(bytes) } -fn bad_sound_font(s: &str) -> Error { - Error::BadSoundFont(s.to_string()) +fn read_u32(f: &mut File) -> u32 { + let mut bytes = [0; 4]; + f.read_exact(&mut bytes); + u32::from_le_bytes(bytes) +} + +fn read_i8(f: &mut File) -> i8 { + read_u8(f) as i8 +} + +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 { + 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)) + ) +} + + +impl Zone { + fn new(inst: u32) -> Self { + Self { + key_range: (0, 127), + vel_range: (0, 127), + inst, + start_offset: 0, + end_offset: 0, + startloop_offset: 0, + endloop_offset: 0, + pan: 0, + is_global: false, + force_key: -1, + force_vel: -1, + initial_attenuation: 0, + tune: 0, + reference: ZoneReference::None, + loops: false, + scale_tuning: 100, + force_root_key: -1, + } + } +} + +fn read_gen_zone(file: &mut File, zone: &mut Zone, gen_count: u16) { + for _gen_ndx in 0..gen_count { + let gen_type = read_u16(file); + let amount_u16 = read_u16(file); + let amount_i16 = amount_u16 as i16; + let amount_range = ((amount_u16 >> 8) as u8, amount_u16 as u8); + + mod gen { + // generators + // these aren't all of the ones soundfont defines, + // just the ones I think are relevant+easy enough to implement. + pub const START_ADDRS_OFFSET: u16 = 0; + pub const END_ADDRS_OFFSET: u16 = 1; + pub const STARTLOOP_ADDRS_OFFSET: u16 = 2; + pub const ENDLOOP_ADDRS_OFFSET: u16 = 3; + pub const START_ADDRS_COARSE_OFFSET: u16 = 4; + pub const END_ADDRS_COARSE_OFFSET: u16 = 12; + pub const PAN: u16 = 17; + pub const INSTRUMENT: u16 = 41; + pub const KEY_RANGE: u16 = 43; + pub const VEL_RANGE: u16 = 44; + pub const STARTLOOP_ADDRS_COARSE_OFFSET: u16 = 45; + pub const KEYNUM: u16 = 46; + pub const VELOCITY: u16 = 47; + pub const INITIAL_ATTENUATION: u16 = 48; + pub const ENDLOOP_ADDRS_COARSE_OFFSET: u16 = 50; + pub const COARSE_TUNE: u16 = 51; + pub const FINE_TUNE: u16 = 52; + pub const SAMPLE_ID: u16 = 53; + pub const SAMPLE_MODES: u16 = 54; + pub const SCALE_TUNING: u16 = 56; + pub const OVERRIDING_ROOT_KEY: u16 = 58; + } + use gen::*; + + match gen_type { + START_ADDRS_OFFSET => zone.start_offset += amount_i16 as i64, + START_ADDRS_COARSE_OFFSET => zone.start_offset += (amount_i16 as i64) * 32768, + END_ADDRS_OFFSET => zone.end_offset += amount_i16 as i64, + END_ADDRS_COARSE_OFFSET => zone.end_offset += (amount_i16 as i64) * 32768, + STARTLOOP_ADDRS_OFFSET => zone.startloop_offset += amount_i16 as i64, + STARTLOOP_ADDRS_COARSE_OFFSET => zone.startloop_offset += (amount_i16 as i64) * 32768, + ENDLOOP_ADDRS_OFFSET => zone.endloop_offset += amount_i16 as i64, + ENDLOOP_ADDRS_COARSE_OFFSET => zone.endloop_offset += (amount_i16 as i64) * 32768, + PAN => zone.pan = amount_i16.clamp(-1000, 1000), + KEY_RANGE => zone.key_range = amount_range, + 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, + 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), + INSTRUMENT => zone.reference = ZoneReference::Instrument(amount_u16), + SAMPLE_MODES => zone.loops = (amount_u16 & 1) != 0, + SCALE_TUNING => zone.scale_tuning = amount_u16, + OVERRIDING_ROOT_KEY => zone.force_root_key = amount_i16.clamp(-1, 127) as i8, + //other => println!("OTHER: {}",other), + _ => {}, + } + + } +} + +// reads the ibag or pbag chunk of a soundfont +fn read_bag_chunk(file: &mut File, bag_indices: Vec) -> (Vec<(u32, u16)>, Vec<(u32, u16)>) { + let mut gen_indices = vec![]; + let mut mod_indices = vec![]; + for inst_ndx in 0..bag_indices.len() - 1 { + let start_ndx = bag_indices[inst_ndx]; + let end_ndx = bag_indices[inst_ndx+1]; + for i in start_ndx..end_ndx { + let gen_ndx = read_u16(file); + let mod_ndx = read_u16(file); + gen_indices.push((inst_ndx as u32, gen_ndx)); + mod_indices.push((inst_ndx as u32, mod_ndx)); + } + } + + { + // terminal zone + let item_ndx = bag_indices.len() as u32; + let gen_ndx = read_u16(file); + let mod_ndx = read_u16(file); + gen_indices.push((item_ndx, gen_ndx)); + mod_indices.push((item_ndx, mod_ndx)); + } + + (gen_indices, mod_indices) +} + +// read pgen or igen chunk +fn read_gen_zones(file: &mut File, items: &mut Vec, gen_indices: Vec<(u32, u16)>, mod_indices: Vec<(u32, u16)>) { + let mut prev_inst_ndx = u32::MAX; + let mut global_zone: Option = None; + for zone_ndx in 0..gen_indices.len()-1 { + let (inst_ndx, start_ndx) = gen_indices[zone_ndx]; + let (_, end_ndx) = gen_indices[zone_ndx+1]; + let mut zone = Zone::new(inst_ndx); + if inst_ndx == prev_inst_ndx { + if let Some(z) = &global_zone { + zone = z.clone(); + } + } else { + global_zone = None; + } + prev_inst_ndx = inst_ndx; + + read_gen_zone(file, &mut zone, end_ndx - start_ndx); + + if zone.reference.is_none() { + // this is a global zone + zone.is_global = true; + global_zone = Some(zone.clone()); + } else { + items[inst_ndx as usize].add_zone(zone); + } + } } impl SoundFont { /// Open a soundfont. /// Note: SoundFont keeps a handle to the file. - /// when you have loaded all the instruments you need, - /// you can call `close_file()` if you want to close it. - pub fn open(filename: &str) -> Result { + /// When you've loaded all the presets you need, you can call `close_file()` to close it. + pub fn open(filename: &str) -> Result { const RIFF: FourCC = fourcc("RIFF"); const SFBK: FourCC = fourcc("sfbk"); const LIST: FourCC = fourcc("LIST"); @@ -103,13 +376,20 @@ impl SoundFont { const SDTA: FourCC = fourcc("sdta"); const PDTA: FourCC = fourcc("pdta"); const INST: FourCC = fourcc("inst"); + const IBAG: FourCC = fourcc("ibag"); + const IGEN: FourCC = fourcc("igen"); + const IMOD: FourCC = fourcc("imod"); + const SHDR: FourCC = fourcc("shdr"); + const PHDR: FourCC = fourcc("phdr"); + const PGEN: FourCC = fourcc("pgen"); + const PBAG: FourCC = fourcc("pbag"); let mut file = File::open(filename)?; let riff = read_fourcc(&mut file)?; if riff != RIFF { // definitely not a soundfont - return Err(Error::NotASoundFont); + return Err(OpenError::NotASoundFont); } let _sfbk_size = read_u32(&mut file); @@ -117,7 +397,7 @@ impl SoundFont { let sfbk = read_fourcc(&mut file)?; if sfbk != SFBK { // could be a WAV file, for example. - return Err(Error::NotASoundFont); + return Err(OpenError::NotASoundFont); } // at this point, the file *should* be a soundfont. @@ -162,7 +442,8 @@ impl SoundFont { let list = read_fourcc(&mut file)?; let sdta_size = read_u32(&mut file); - let sdta_end = file.stream_position()? + sdta_size as u64; + let sdta_offset = file.stream_position()?; + let sdta_end = sdta_offset + sdta_size as u64; let sdta = read_fourcc(&mut file)?; @@ -180,44 +461,234 @@ impl SoundFont { return Err(bad_sound_font("no pdta chunk")); } - + struct Chunk { + offset: u64, + size: u32 + } + impl Chunk { + fn new() -> Self { + Chunk { offset: 0, size: 0 } + } + fn end(&self) -> u64 { + return self.offset + self.size as u64; + } + } + let mut inst = Chunk::new(); + let mut ibag = Chunk::new(); + let mut igen = Chunk::new(); + let mut imod = Chunk::new(); + let mut shdr = Chunk::new(); + let mut phdr = Chunk::new(); + let mut pbag = Chunk::new(); + let mut pgen = Chunk::new(); + // 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_end = file.stream_position()? + chunk_size as u64; - if chunk_type == INST { - while file.stream_position()? < chunk_end { - let mut name_buf = [0; 20]; - file.read_exact(&mut name_buf); - let mut name_vec = Vec::from(name_buf); - while !name_vec.is_empty() && name_vec[name_vec.len()-1] == 0 { - name_vec.pop(); - } - let name = match String::from_utf8(name_vec) { - Ok(x) => x, - Err(_) => return Err(bad_sound_font("invalid UTF-8 in inst name")), - }; - if name.is_empty() { - return Err(bad_sound_font("instrument with no name.")); - } - - let bag_idx = read_u16(&mut file); - println!("{:30} ---- {}",name, bag_idx); - } + let chunk = Chunk { + offset: file.stream_position()?, + size: chunk_size, + }; + + match chunk_type { + INST => inst = chunk, + IBAG => ibag = chunk, + IGEN => igen = chunk, + IMOD => imod = chunk, + SHDR => shdr = chunk, + PHDR => phdr = chunk, + PBAG => pbag = chunk, + PGEN => pgen = chunk, + _ => {}, } file.seek(std::io::SeekFrom::Start(chunk_end)); } + if inst.offset == 0 { + return Err(bad_sound_font("no inst chunk.")); + } + if ibag.offset == 0 { + return Err(bad_sound_font("no ibag chunk.")); + } + if igen.offset == 0 { + return Err(bad_sound_font("no igen chunk.")); + } + if imod.offset == 0 { + return Err(bad_sound_font("no imod chunk.")); + } + if shdr.offset == 0 { + return Err(bad_sound_font("no shdr chunk.")); + } + if phdr.offset == 0 { + return Err(bad_sound_font("no phdr chunk.")); + } + + + let mut instruments: Vec = vec![]; + let mut instrument_bag_indices: Vec = vec![]; + + // --- 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() { + return Err(bad_sound_font("instrument with no name.")); + } + + 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.")); + } + } + } + + + // --- read ibag chunk --- + 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 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)); + 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)); + 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 { + 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); + presets.push(Preset { name, ..Default::default() }); + preset_bag_indices.push(bag_ndx); + + } + + + // --- read pbag chunk --- + 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 pgen chunk --- + 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)); + let samples_count = shdr.size / 46; + 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")?; + 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); + /* + 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); + 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); + + // file offset + let offset = sdta_offset as u32 + 2 * start; + + let sample = Sample { + offset, + len: end - start, + startloop: startloop - start, + endloop: endloop - start, + sample_rate, + pitch: original_pitch, + pitch_correction, + data: vec![] + }; + samples.push(sample); + + } + + + instruments.pop(); // remove EOI + presets.pop(); // remove EOP + + Ok(SoundFont { file: Some(file), name: name_unwrapped, + instruments, + samples, + presets, + file_cache: HashMap::new(), }) } - + + pub fn preset_count(&self) -> usize { + self.presets.len() + } + + pub fn preset_name(&self, idx: usize) -> Option<&str> { + if idx >= self.presets.len() { + None + } else { + Some(&self.presets[idx].name) + } + } + + pub fn load_preset(&mut self, preset_idx: usize) -> Result { + if preset_idx >= self.presets.len() { + return Err(PresetError::BadPreset); + } + let preset = &self.presets[preset_idx]; + // @TODO + } + pub fn close_file(&mut self) { self.file = None; } } + +impl Preset { + /// adds own sample data to `samples`. + pub fn add_samples(&self, key: u8, vel: u8, hold_time: f64, samples: &mut [i16]) { + } +} -- cgit v1.2.3