Skip to main content

rustysynth_ext/
soundfont.rs

1use std::io::Read;
2
3use crate::LoopMode;
4use crate::binary_reader::BinaryReader;
5use crate::error::SoundFontError;
6use crate::four_cc::FourCC;
7use crate::instrument::Instrument;
8use crate::preset::Preset;
9use crate::sample_header::SampleHeader;
10use crate::soundfont_info::SoundFontInfo;
11use crate::soundfont_parameters::SoundFontParameters;
12use crate::soundfont_sampledata::SoundFontSampleData;
13
14/// Represents a SoundFont.
15#[derive(Debug)]
16#[non_exhaustive]
17pub struct SoundFont {
18    pub(crate) info: SoundFontInfo,
19    pub(crate) bits_per_sample: i32,
20    pub(crate) wave_data: Vec<i16>,
21    pub(crate) sample_headers: Vec<SampleHeader>,
22    pub(crate) presets: Vec<Preset>,
23    pub(crate) instruments: Vec<Instrument>,
24}
25
26impl SoundFont {
27    /// Loads a SoundFont from the stream.
28    ///
29    /// # Arguments
30    ///
31    /// * `reader` - The data stream used to load the SoundFont.
32    pub fn new<R: Read>(reader: &mut R) -> Result<Self, SoundFontError> {
33        let chunk_id = BinaryReader::read_four_cc(reader)?;
34        if chunk_id != b"RIFF" {
35            return Err(SoundFontError::RiffChunkNotFound);
36        }
37
38        let _size = BinaryReader::read_i32(reader)?;
39
40        let form_type = BinaryReader::read_four_cc(reader)?;
41        if form_type != b"sfbk" {
42            return Err(SoundFontError::InvalidRiffChunkType {
43                expected: FourCC::from_bytes(*b"sfbk"),
44                actual: form_type,
45            });
46        }
47
48        let info = SoundFontInfo::new(reader)?;
49        let sample_bytes = SoundFontSampleData::read(reader)?;
50        let (parameters, sample_data) = SoundFontParameters::load(reader, sample_bytes)?;
51
52        let sound_font = Self {
53            info,
54            bits_per_sample: sample_data.bits_per_sample,
55            wave_data: sample_data.wave_data,
56            sample_headers: parameters.sample_headers,
57            presets: parameters.presets,
58            instruments: parameters.instruments,
59        };
60
61        sound_font.sanity_check()?;
62
63        Ok(sound_font)
64    }
65
66    fn sanity_check(&self) -> Result<(), SoundFontError> {
67        // https://github.com/sinshu/rustysynth/issues/22
68        // https://github.com/sinshu/rustysynth/issues/33
69        // https://github.com/sinshu/rustysynth/pull/51
70        for instrument in &self.instruments {
71            for region in &instrument.regions {
72                let start = region.get_sample_start();
73                let end = region.get_sample_end();
74                let start_loop = region.get_sample_start_loop();
75                let end_loop = region.get_sample_end_loop();
76                let loop_mode = region.get_sample_modes();
77
78                if start < 0
79                    || start_loop < 0
80                    || end as usize >= self.wave_data.len()
81                    || end_loop as usize >= self.wave_data.len()
82                    || end <= start
83                    || end_loop < start_loop
84                    || (loop_mode != LoopMode::NoLoop && start_loop >= end_loop)
85                {
86                    return Err(SoundFontError::SanityCheckFailed);
87                }
88            }
89        }
90
91        Ok(())
92    }
93
94    /// Gets the information of the SoundFont.
95    pub fn get_info(&self) -> &SoundFontInfo {
96        &self.info
97    }
98
99    /// Gets the bits per sample of the sample data.
100    pub fn get_bits_per_sample(&self) -> i32 {
101        self.bits_per_sample
102    }
103
104    /// Gets the sample data.
105    pub fn get_wave_data(&self) -> &[i16] {
106        &self.wave_data[..]
107    }
108
109    /// Gets the samples of the SoundFont.
110    pub fn get_sample_headers(&self) -> &[SampleHeader] {
111        &self.sample_headers[..]
112    }
113
114    /// Gets the presets of the SoundFont.
115    pub fn get_presets(&self) -> &[Preset] {
116        &self.presets[..]
117    }
118
119    /// Gets the instruments of the SoundFont.
120    pub fn get_instruments(&self) -> &[Instrument] {
121        &self.instruments[..]
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    use std::{fs::File, path::PathBuf};
130
131    fn samples_dir_path() -> PathBuf {
132        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
133            .parent()
134            .unwrap()
135            .join("samples")
136    }
137
138    // Snapshots a preset region in the same field order (and loop-mode
139    // encoding) used by rustysynth_test::preset_util::check.
140    fn preset_region_values(region: &crate::PresetRegion) -> Vec<f64> {
141        vec![
142            region.get_modulation_lfo_to_pitch() as f64,
143            region.get_vibrato_lfo_to_pitch() as f64,
144            region.get_modulation_envelope_to_pitch() as f64,
145            region.get_initial_filter_cutoff_frequency() as f64,
146            region.get_initial_filter_q() as f64,
147            region.get_modulation_lfo_to_filter_cutoff_frequency() as f64,
148            region.get_modulation_envelope_to_filter_cutoff_frequency() as f64,
149            region.get_modulation_lfo_to_volume() as f64,
150            region.get_chorus_effects_send() as f64,
151            region.get_reverb_effects_send() as f64,
152            region.get_pan() as f64,
153            region.get_delay_modulation_lfo() as f64,
154            region.get_frequency_modulation_lfo() as f64,
155            region.get_delay_vibrato_lfo() as f64,
156            region.get_frequency_vibrato_lfo() as f64,
157            region.get_delay_modulation_envelope() as f64,
158            region.get_attack_modulation_envelope() as f64,
159            region.get_hold_modulation_envelope() as f64,
160            region.get_decay_modulation_envelope() as f64,
161            region.get_sustain_modulation_envelope() as f64,
162            region.get_release_modulation_envelope() as f64,
163            region.get_key_number_to_modulation_envelope_hold() as f64,
164            region.get_key_number_to_modulation_envelope_decay() as f64,
165            region.get_delay_volume_envelope() as f64,
166            region.get_attack_volume_envelope() as f64,
167            region.get_hold_volume_envelope() as f64,
168            region.get_decay_volume_envelope() as f64,
169            region.get_sustain_volume_envelope() as f64,
170            region.get_release_volume_envelope() as f64,
171            region.get_key_number_to_volume_envelope_hold() as f64,
172            region.get_key_number_to_volume_envelope_decay() as f64,
173            region.get_key_range_start() as f64,
174            region.get_key_range_end() as f64,
175            region.get_velocity_range_start() as f64,
176            region.get_velocity_range_end() as f64,
177            region.get_initial_attenuation() as f64,
178            region.get_coarse_tune() as f64,
179            region.get_fine_tune() as f64,
180            region.get_scale_tuning() as f64,
181        ]
182    }
183
184    #[cfg(not(feature = "sf3"))]
185    #[test]
186    fn test_load_reject_sf3() {
187        let path = samples_dir_path().join("dummy.sf3");
188        let mut file = File::open(&path).unwrap();
189        assert!(matches!(
190            SoundFont::new(&mut file),
191            Err(SoundFontError::UnsupportedSampleFormat)
192        ));
193    }
194
195    #[cfg(feature = "sf3")]
196    #[test]
197    fn test_load_sf3_dummy() {
198        let path = samples_dir_path().join("dummy.sf3");
199        let mut file = File::open(&path).unwrap();
200        let sound_font = SoundFont::new(&mut file).unwrap();
201        assert_eq!(sound_font.get_bits_per_sample(), 16);
202        assert!(!sound_font.get_wave_data().is_empty());
203        assert!(!sound_font.get_sample_headers().is_empty());
204        assert!(!sound_font.get_presets().is_empty());
205    }
206
207    // Loads the larger FluidR3Mono_GM.SF2 sample font.
208    #[test]
209    fn test_load_sf2_fluid() {
210        let path = samples_dir_path().join("FluidR3Mono_GM.sf2");
211        let mut file = File::open(&path).unwrap();
212        let sound_font = SoundFont::new(&mut file).unwrap();
213
214        assert_eq!(sound_font.get_bits_per_sample(), 16);
215        assert!(!sound_font.get_wave_data().is_empty());
216        assert!(!sound_font.get_sample_headers().is_empty());
217        assert!(!sound_font.get_presets().is_empty());
218        assert!(!sound_font.get_instruments().is_empty());
219
220        // Every sample header must reference valid PCM data.
221        let wave_len = sound_font.get_wave_data().len();
222        for sample in sound_font.get_sample_headers() {
223            assert!(sample.get_start() >= 0);
224            assert!(sample.get_start() < sample.get_end());
225            assert!(sample.get_end() as usize <= wave_len);
226            assert!(sample.get_end_loop() as usize <= wave_len);
227        }
228
229        // Rendering a note must produce audible output.
230        use std::sync::Arc;
231
232        use crate::{Synthesizer, SynthesizerSettings};
233
234        let sound_font = Arc::new(sound_font);
235        let settings = SynthesizerSettings::new(44100);
236        let mut synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
237        synthesizer.note_on(0, 60, 100); // grand piano, middle C
238
239        let mut left = vec![0_f32; 44100];
240        let mut right = vec![0_f32; 44100];
241        synthesizer.render(&mut left, &mut right);
242
243        let peak = left
244            .iter()
245            .fold(0_f32, |peak, sample| peak.max(sample.abs()));
246        assert!(
247            peak > 0.01,
248            "the rendered note was effectively silent (peak {peak})"
249        );
250    }
251
252    #[cfg(feature = "sf3")]
253    #[test]
254    fn test_load_sf3_fluid() {
255        let path = samples_dir_path().join("FluidR3Mono_GM.sf3");
256        let mut file = File::open(&path).unwrap();
257        let sound_font = SoundFont::new(&mut file).unwrap();
258
259        assert_eq!(sound_font.get_bits_per_sample(), 16);
260        assert!(!sound_font.get_wave_data().is_empty());
261        assert!(!sound_font.get_sample_headers().is_empty());
262        assert!(!sound_font.get_presets().is_empty());
263        assert!(!sound_font.get_instruments().is_empty());
264
265        // Every decoded sample header must reference valid PCM data.
266        let wave_len = sound_font.get_wave_data().len();
267        for sample in sound_font.get_sample_headers() {
268            assert!(sample.get_start() >= 0);
269            assert!(sample.get_start() < sample.get_end());
270            assert!(sample.get_end() as usize <= wave_len);
271            assert!(sample.get_end_loop() as usize <= wave_len);
272        }
273
274        // Rendering a note must produce audible output; this guards against
275        // decoding the samples after the instrument regions are built.
276        use std::sync::Arc;
277
278        use crate::{Synthesizer, SynthesizerSettings};
279
280        let sound_font = Arc::new(sound_font);
281        let settings = SynthesizerSettings::new(44100);
282        let mut synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
283        synthesizer.note_on(0, 60, 100); // grand piano, middle C
284
285        let mut left = vec![0_f32; 44100];
286        let mut right = vec![0_f32; 44100];
287        synthesizer.render(&mut left, &mut right);
288
289        let peak = left
290            .iter()
291            .fold(0_f32, |peak, sample| peak.max(sample.abs()));
292        assert!(
293            peak > 0.01,
294            "the rendered note was effectively silent (peak {peak})"
295        );
296    }
297
298    // The .sf3 version must agree with the .sf2 version everywhere the two
299    // fonts represent the same musical data. The conversion tool REORDERED
300    // the samples, instruments and presets, and even cross-linked one sample
301    // name ('PalmMuted Guitar Bb') to two different samples, so nothing here
302    // is compared by index.
303    //
304    // The two builds are not byte-identical: the SF3 conversion started from
305    // a slightly longer source, so 317 samples decode to a few thousand
306    // extra frames. Samples are therefore matched by name and compared with
307    // a frame tolerance; structure that is exactly shared (sample identity
308    // fields, instrument and preset definitions) is compared exactly.
309    #[cfg(feature = "sf3")]
310    #[test]
311    fn test_sf3_matches_sf2() {
312        use std::collections::HashMap;
313
314        /// ~46 ms at 44.1 kHz: permitted frame difference between the two
315        /// builds for lengths and loop points (the observed source-build
316        /// drift is at most ~1000 frames).
317        const TOLERANCE: i32 = 2048;
318
319        fn load(file_name: &str) -> SoundFont {
320            let path = samples_dir_path().join(file_name);
321            let mut file = File::open(&path).unwrap();
322            SoundFont::new(&mut file).unwrap()
323        }
324
325        fn grouped(sf: &SoundFont) -> HashMap<&str, Vec<&crate::SampleHeader>> {
326            let mut groups: HashMap<&str, Vec<&SampleHeader>> = HashMap::new();
327            for header in sf.get_sample_headers() {
328                groups.entry(header.get_name()).or_default().push(header);
329            }
330            groups
331        }
332
333        fn by_name_i(sf: &SoundFont) -> HashMap<&str, &crate::Instrument> {
334            sf.get_instruments()
335                .iter()
336                .map(|i| (i.get_name(), i))
337                .collect()
338        }
339
340        fn find_preset(sf: &SoundFont, bank: i32, patch: i32) -> Option<&crate::Preset> {
341            sf.get_presets()
342                .iter()
343                .find(|p| p.get_bank_number() == bank && p.get_patch_number() == patch)
344        }
345
346        fn rel_loop(header: &crate::SampleHeader, field: fn(&crate::SampleHeader) -> i32) -> i32 {
347            field(header) - header.get_start()
348        }
349
350        let sf2 = load("FluidR3Mono_GM.sf2");
351        let sf3 = load("FluidR3Mono_GM.sf3");
352
353        // ---------------- samples ----------------
354        let headers_2 = sf2.get_sample_headers();
355        let headers_3 = sf3.get_sample_headers();
356        assert_eq!(
357            headers_2.len(),
358            headers_3.len(),
359            "the two builds must contain the same number of samples"
360        );
361
362        let grouped_2 = grouped(&sf2);
363        let grouped_3 = grouped(&sf3);
364        assert_eq!(
365            grouped_2.len(),
366            grouped_3.len(),
367            "the distinct sample names differ"
368        );
369
370        for (name, occurrences_2) in &grouped_2 {
371            let occurrences_3 = grouped_3
372                .get(name)
373                .unwrap_or_else(|| panic!("sample '{name}' is missing in the sf3 build"));
374            assert_eq!(
375                occurrences_2.len(),
376                occurrences_3.len(),
377                "the number of samples named '{name}' differs"
378            );
379
380            // Pair the occurrences (there is one duplicated name) by their
381            // relative loop points, which are order-independent within a font.
382            let mut occurrences_2 = occurrences_2.clone();
383            let mut occurrences_3 = occurrences_3.clone();
384            let key = |header: &&crate::SampleHeader| {
385                (
386                    header.get_start_loop() - header.get_start(),
387                    header.get_end_loop() - header.get_start(),
388                )
389            };
390            occurrences_2.sort_by_key(key);
391            occurrences_3.sort_by_key(key);
392
393            for (a, b) in occurrences_2.iter().zip(&occurrences_3) {
394                assert_eq!(a.get_sample_rate(), b.get_sample_rate(), "sample '{name}'");
395                assert_eq!(
396                    a.get_original_pitch(),
397                    b.get_original_pitch(),
398                    "sample '{name}'"
399                );
400                assert_eq!(
401                    a.get_pitch_correction(),
402                    b.get_pitch_correction(),
403                    "sample '{name}'"
404                );
405                assert!(
406                    (rel_loop(a, crate::SampleHeader::get_start_loop)
407                        - rel_loop(b, crate::SampleHeader::get_start_loop))
408                    .abs()
409                        <= TOLERANCE,
410                    "sample '{name}' loop start differs by more than {TOLERANCE} frames"
411                );
412                assert!(
413                    (rel_loop(a, crate::SampleHeader::get_end_loop)
414                        - rel_loop(b, crate::SampleHeader::get_end_loop))
415                    .abs()
416                        <= TOLERANCE,
417                    "sample '{name}' loop end differs by more than {TOLERANCE} frames"
418                );
419                assert!(
420                    ((a.get_end() - a.get_start()) - (b.get_end() - b.get_start())).abs()
421                        <= TOLERANCE,
422                    "sample '{name}' length differs by more than {TOLERANCE} frames"
423                );
424                assert!(
425                    b.get_sample_type() & 0x10 != 0,
426                    "sf3 sample '{name}' must be flagged as compressed"
427                );
428            }
429        }
430
431        // The decoded PCM pools may differ by at most one tolerance per sample
432        // (each font's exact pool size is pinned by its own info snapshot).
433        let pool_diff = (sf3.get_wave_data().len() as i64 - sf2.get_wave_data().len() as i64).abs();
434        assert!(
435            pool_diff <= headers_3.len() as i64 * TOLERANCE as i64,
436            "decoded PCM pool lengths differ by {pool_diff} frames"
437        );
438
439        // ---------------- instruments ----------------
440        let instruments_2 = sf2.get_instruments();
441        let instruments_3 = sf3.get_instruments();
442        assert_eq!(instruments_2.len(), instruments_3.len());
443
444        let mut names_2: Vec<&str> = instruments_2.iter().map(|i| i.get_name()).collect();
445        let mut names_3: Vec<&str> = instruments_3.iter().map(|i| i.get_name()).collect();
446        names_2.sort_unstable();
447        names_3.sort_unstable();
448        assert_eq!(names_2, names_3, "instrument name sets differ");
449
450        let by_name_i2 = by_name_i(&sf2);
451        let by_name_i3 = by_name_i(&sf3);
452        assert_eq!(by_name_i2.len(), by_name_i3.len());
453        assert_eq!(
454            by_name_i2.len(),
455            instruments_2.len(),
456            "duplicate instrument names"
457        );
458
459        for (name, a) in &by_name_i2 {
460            let b = by_name_i3
461                .get(name)
462                .unwrap_or_else(|| panic!("instrument '{name}' is missing in the sf3 build"));
463            assert_eq!(
464                a.get_regions().len(),
465                b.get_regions().len(),
466                "instrument '{name}'"
467            );
468        }
469
470        // ---------------- presets ----------------
471        let presets_2 = sf2.get_presets();
472        let presets_3 = sf3.get_presets();
473        assert_eq!(presets_2.len(), presets_3.len());
474
475        let mut count_2 = 0;
476        for preset in presets_2 {
477            let bank = preset.get_bank_number();
478            let patch = preset.get_patch_number();
479            let counterpart = find_preset(&sf3, bank, patch).unwrap_or_else(|| {
480                panic!("preset (bank {bank}, patch {patch}) is missing in the sf3 build")
481            });
482            assert_eq!(preset.get_name(), counterpart.get_name());
483            assert_eq!(preset.get_regions().len(), counterpart.get_regions().len());
484            for (ra, rb) in preset.get_regions().iter().zip(counterpart.get_regions()) {
485                assert_eq!(preset_region_values(ra), preset_region_values(rb));
486            }
487            count_2 += 1;
488        }
489        assert_eq!(count_2, presets_2.len());
490    }
491
492    // smpl sub-chunk exists, but is zero-length.
493    #[test]
494    fn test_load_empty_samples() {
495        let path = samples_dir_path().join("test_empty_samples.sf2");
496        let mut file = File::open(&path).unwrap();
497        assert!(matches!(
498            SoundFont::new(&mut file),
499            Err(SoundFontError::SampleDataNotFound)
500        ));
501    }
502}