rustysynth/
soundfont.rs

1#![allow(dead_code)]
2
3use std::io::Read;
4
5use crate::binary_reader::BinaryReader;
6use crate::error::SoundFontError;
7use crate::four_cc::FourCC;
8use crate::instrument::Instrument;
9use crate::preset::Preset;
10use crate::sample_header::SampleHeader;
11use crate::soundfont_info::SoundFontInfo;
12use crate::soundfont_parameters::SoundFontParameters;
13use crate::soundfont_sampledata::SoundFontSampleData;
14use crate::LoopMode;
15
16/// Reperesents a SoundFont.
17#[derive(Debug)]
18#[non_exhaustive]
19pub struct SoundFont {
20    pub(crate) info: SoundFontInfo,
21    pub(crate) bits_per_sample: i32,
22    pub(crate) wave_data: Vec<i16>,
23    pub(crate) sample_headers: Vec<SampleHeader>,
24    pub(crate) presets: Vec<Preset>,
25    pub(crate) instruments: Vec<Instrument>,
26}
27
28impl SoundFont {
29    /// Loads a SoundFont from the stream.
30    ///
31    /// # Arguments
32    ///
33    /// * `reader` - The data stream used to load the SoundFont.
34    pub fn new<R: Read>(reader: &mut R) -> Result<Self, SoundFontError> {
35        let chunk_id = BinaryReader::read_four_cc(reader)?;
36        if chunk_id != b"RIFF" {
37            return Err(SoundFontError::RiffChunkNotFound);
38        }
39
40        let _size = BinaryReader::read_i32(reader);
41
42        let form_type = BinaryReader::read_four_cc(reader)?;
43        if form_type != b"sfbk" {
44            return Err(SoundFontError::InvalidRiffChunkType {
45                expected: FourCC::from_bytes(*b"sfbk"),
46                actual: form_type,
47            });
48        }
49
50        let info = SoundFontInfo::new(reader)?;
51        let sample_data = SoundFontSampleData::new(reader)?;
52        let parameters = SoundFontParameters::new(reader)?;
53
54        let sound_font = Self {
55            info,
56            bits_per_sample: sample_data.bits_per_sample,
57            wave_data: sample_data.wave_data,
58            sample_headers: parameters.sample_headers,
59            presets: parameters.presets,
60            instruments: parameters.instruments,
61        };
62
63        sound_font.sanity_check()?;
64
65        Ok(sound_font)
66    }
67
68    fn sanity_check(&self) -> Result<(), SoundFontError> {
69        // https://github.com/sinshu/rustysynth/issues/22
70        // https://github.com/sinshu/rustysynth/issues/33
71        // https://github.com/sinshu/rustysynth/pull/51
72        for instrument in &self.instruments {
73            for region in &instrument.regions {
74                let start = region.get_sample_start();
75                let end = region.get_sample_end();
76                let start_loop = region.get_sample_start_loop();
77                let end_loop = region.get_sample_end_loop();
78                let loop_mode = region.get_sample_modes();
79
80                if start < 0
81                    || start_loop < 0
82                    || end as usize >= self.wave_data.len()
83                    || end_loop as usize >= self.wave_data.len()
84                    || end <= start
85                    || end_loop < start_loop
86                    || (loop_mode != LoopMode::NoLoop && start_loop >= end_loop)
87                {
88                    return Err(SoundFontError::SanityCheckFailed);
89                }
90            }
91        }
92
93        Ok(())
94    }
95
96    /// Gets the information of the SoundFont.
97    pub fn get_info(&self) -> &SoundFontInfo {
98        &self.info
99    }
100
101    /// Gets the bits per sample of the sample data.
102    pub fn get_bits_per_sample(&self) -> i32 {
103        self.bits_per_sample
104    }
105
106    /// Gets the sample data.
107    pub fn get_wave_data(&self) -> &[i16] {
108        &self.wave_data[..]
109    }
110
111    /// Gets the samples of the SoundFont.
112    pub fn get_sample_headers(&self) -> &[SampleHeader] {
113        &self.sample_headers[..]
114    }
115
116    /// Gets the presets of the SoundFont.
117    pub fn get_presets(&self) -> &[Preset] {
118        &self.presets[..]
119    }
120
121    /// Gets the instruments of the SoundFont.
122    pub fn get_instruments(&self) -> &[Instrument] {
123        &self.instruments[..]
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    use std::{fs::File, path::PathBuf};
132
133    fn samples_dir_path() -> PathBuf {
134        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
135            .parent()
136            .unwrap()
137            .join("samples")
138    }
139
140    #[test]
141    fn test_load_reject_sf3() {
142        let path = samples_dir_path().join("dummy.sf3");
143        let mut file = File::open(&path).unwrap();
144        assert!(matches!(
145            SoundFont::new(&mut file),
146            Err(SoundFontError::UnsupportedSampleFormat)
147        ));
148    }
149
150    // smpl sub-chunk exists, but is zero-length.
151    #[test]
152    fn test_load_empty_samples() {
153        let path = samples_dir_path().join("test_empty_samples.sf2");
154        let mut file = File::open(&path).unwrap();
155        assert!(matches!(
156            SoundFont::new(&mut file),
157            Err(SoundFontError::SampleDataNotFound)
158        ));
159    }
160}