summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2022-10-02 23:31:01 -0400
committerpommicket <pommicket@gmail.com>2022-10-02 23:31:01 -0400
commit4d7d33298f6cebf54d9794a168bfc24d291dd08c (patch)
tree56720746e4fea912b59f14cd8e7005a8eab6eb4d /src
parent93b0d691522e48974bbdbc30c93d051d223f2f53 (diff)
playing some nicer notes
Diffstat (limited to 'src')
-rw-r--r--src/main.rs78
-rw-r--r--src/soundfont.rs430
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;