Skip to main content

rustysynth_ext/
soundfont.rs

1use std::io::Read;
2
3use crate::binary_reader::BinaryReader;
4use crate::error::SoundFontError;
5use crate::four_cc::FourCC;
6use crate::instrument::Instrument;
7use crate::preset::Preset;
8use crate::sample_header::SampleHeader;
9use crate::soundfont_info::SoundFontInfo;
10use crate::soundfont_parameters::SoundFontParameters;
11use crate::soundfont_sampledata::SoundFontSampleData;
12
13/// Represents a SoundFont.
14#[derive(Debug)]
15#[non_exhaustive]
16pub struct SoundFont {
17    pub(crate) info: SoundFontInfo,
18    pub(crate) bits_per_sample: i32,
19    pub(crate) wave_data: Vec<i16>,
20    pub(crate) sample_headers: Vec<SampleHeader>,
21    pub(crate) presets: Vec<Preset>,
22    pub(crate) instruments: Vec<Instrument>,
23}
24
25impl SoundFont {
26    /// Loads a SoundFont from the stream.
27    ///
28    /// # Arguments
29    ///
30    /// * `reader` - The data stream used to load the SoundFont.
31    pub fn new<R: Read>(reader: &mut R) -> Result<Self, SoundFontError> {
32        let chunk_id = BinaryReader::read_four_cc(reader)?;
33        if chunk_id != b"RIFF" {
34            return Err(SoundFontError::RiffChunkNotFound);
35        }
36
37        let _size = BinaryReader::read_i32(reader)?;
38
39        let form_type = BinaryReader::read_four_cc(reader)?;
40        if form_type != b"sfbk" {
41            return Err(SoundFontError::InvalidRiffChunkType {
42                expected: FourCC::from_bytes(*b"sfbk"),
43                actual: form_type,
44            });
45        }
46
47        let info = SoundFontInfo::new(reader)?;
48        let sample_bytes = SoundFontSampleData::read(reader)?;
49        let (parameters, sample_data) = SoundFontParameters::load(reader, sample_bytes)?;
50
51        let sound_font = Self {
52            info,
53            bits_per_sample: sample_data.bits_per_sample,
54            wave_data: sample_data.wave_data,
55            sample_headers: parameters.sample_headers,
56            presets: parameters.presets,
57            instruments: parameters.instruments,
58        };
59
60        sound_font.sanity_check()?;
61
62        Ok(sound_font)
63    }
64
65    fn sanity_check(&self) -> Result<(), SoundFontError> {
66        // https://github.com/sinshu/rustysynth/issues/22
67        // https://github.com/sinshu/rustysynth/issues/33
68        // https://github.com/sinshu/rustysynth/pull/51
69        for instrument in &self.instruments {
70            for region in &instrument.regions {
71                let start = region.get_sample_start();
72                let end = region.get_sample_end();
73                let start_loop = region.get_sample_start_loop();
74                let end_loop = region.get_sample_end_loop();
75
76                // A reversed or empty loop range no longer rejects the whole
77                // file: the oscillator plays such a region once without
78                // looping.
79                // https://github.com/sinshu/rustysynth/issues/55
80                //
81                // Loop points outside the sample data are a different matter.
82                // They mean the sample data is truncated, so the region cannot
83                // be played at all and the file stays rejected.
84                if start < 0
85                    || start_loop < 0
86                    || end as usize >= self.wave_data.len()
87                    || end_loop as usize >= self.wave_data.len()
88                    || end <= start
89                {
90                    return Err(SoundFontError::SanityCheckFailed);
91                }
92            }
93        }
94
95        Ok(())
96    }
97
98    /// Gets the information of the SoundFont.
99    pub fn get_info(&self) -> &SoundFontInfo {
100        &self.info
101    }
102
103    /// Gets the bits per sample of the sample data.
104    pub fn get_bits_per_sample(&self) -> i32 {
105        self.bits_per_sample
106    }
107
108    /// Gets the sample data.
109    pub fn get_wave_data(&self) -> &[i16] {
110        &self.wave_data[..]
111    }
112
113    /// Gets the samples of the SoundFont.
114    pub fn get_sample_headers(&self) -> &[SampleHeader] {
115        &self.sample_headers[..]
116    }
117
118    /// Gets the presets of the SoundFont.
119    pub fn get_presets(&self) -> &[Preset] {
120        &self.presets[..]
121    }
122
123    /// Gets the instruments of the SoundFont.
124    pub fn get_instruments(&self) -> &[Instrument] {
125        &self.instruments[..]
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    use std::{fs::File, path::PathBuf};
134
135    use crate::test_font::*;
136
137    fn samples_dir_path() -> PathBuf {
138        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
139            .parent()
140            .unwrap()
141            .join("samples")
142    }
143
144    // Snapshots a preset region in the same field order (and loop-mode
145    // encoding) used by rustysynth_test::preset_util::check. Only used by the
146    // sf3-vs-sf2 parity test, so gate it with the same feature.
147    #[cfg(feature = "sf3")]
148    fn preset_region_values(region: &crate::PresetRegion) -> Vec<f64> {
149        vec![
150            region.get_modulation_lfo_to_pitch() as f64,
151            region.get_vibrato_lfo_to_pitch() as f64,
152            region.get_modulation_envelope_to_pitch() as f64,
153            region.get_initial_filter_cutoff_frequency() as f64,
154            region.get_initial_filter_q() as f64,
155            region.get_modulation_lfo_to_filter_cutoff_frequency() as f64,
156            region.get_modulation_envelope_to_filter_cutoff_frequency() as f64,
157            region.get_modulation_lfo_to_volume() as f64,
158            region.get_chorus_effects_send() as f64,
159            region.get_reverb_effects_send() as f64,
160            region.get_pan() as f64,
161            region.get_delay_modulation_lfo() as f64,
162            region.get_frequency_modulation_lfo() as f64,
163            region.get_delay_vibrato_lfo() as f64,
164            region.get_frequency_vibrato_lfo() as f64,
165            region.get_delay_modulation_envelope() as f64,
166            region.get_attack_modulation_envelope() as f64,
167            region.get_hold_modulation_envelope() as f64,
168            region.get_decay_modulation_envelope() as f64,
169            region.get_sustain_modulation_envelope() as f64,
170            region.get_release_modulation_envelope() as f64,
171            region.get_key_number_to_modulation_envelope_hold() as f64,
172            region.get_key_number_to_modulation_envelope_decay() as f64,
173            region.get_delay_volume_envelope() as f64,
174            region.get_attack_volume_envelope() as f64,
175            region.get_hold_volume_envelope() as f64,
176            region.get_decay_volume_envelope() as f64,
177            region.get_sustain_volume_envelope() as f64,
178            region.get_release_volume_envelope() as f64,
179            region.get_key_number_to_volume_envelope_hold() as f64,
180            region.get_key_number_to_volume_envelope_decay() as f64,
181            region.get_key_range_start() as f64,
182            region.get_key_range_end() as f64,
183            region.get_velocity_range_start() as f64,
184            region.get_velocity_range_end() as f64,
185            region.get_initial_attenuation() as f64,
186            region.get_coarse_tune() as f64,
187            region.get_fine_tune() as f64,
188            region.get_scale_tuning() as f64,
189        ]
190    }
191
192    #[cfg(not(feature = "sf3"))]
193    #[test]
194    fn test_load_reject_sf3() {
195        let path = samples_dir_path().join("dummy.sf3");
196        let mut file = File::open(&path).unwrap();
197        assert!(matches!(
198            SoundFont::new(&mut file),
199            Err(SoundFontError::UnsupportedSampleFormat)
200        ));
201    }
202
203    #[cfg(feature = "sf3")]
204    #[test]
205    fn test_load_sf3_dummy() {
206        let path = samples_dir_path().join("dummy.sf3");
207        let mut file = File::open(&path).unwrap();
208        let sound_font = SoundFont::new(&mut file).unwrap();
209        assert_eq!(sound_font.get_bits_per_sample(), 16);
210        assert!(!sound_font.get_wave_data().is_empty());
211        assert!(!sound_font.get_sample_headers().is_empty());
212        assert!(!sound_font.get_presets().is_empty());
213    }
214
215    // Loads the larger FluidR3Mono_GM.SF2 sample font.
216    #[test]
217    fn test_load_sf2_fluid() {
218        let path = samples_dir_path().join("FluidR3Mono_GM.sf2");
219        let mut file = File::open(&path).unwrap();
220        let sound_font = SoundFont::new(&mut file).unwrap();
221
222        assert_eq!(sound_font.get_bits_per_sample(), 16);
223        assert!(!sound_font.get_wave_data().is_empty());
224        assert!(!sound_font.get_sample_headers().is_empty());
225        assert!(!sound_font.get_presets().is_empty());
226        assert!(!sound_font.get_instruments().is_empty());
227
228        // Every sample header must reference valid PCM data.
229        let wave_len = sound_font.get_wave_data().len();
230        for sample in sound_font.get_sample_headers() {
231            assert!(sample.get_start() >= 0);
232            assert!(sample.get_start() < sample.get_end());
233            assert!(sample.get_end() as usize <= wave_len);
234            assert!(sample.get_end_loop() as usize <= wave_len);
235        }
236
237        // Rendering a note must produce audible output.
238        use std::sync::Arc;
239
240        use crate::{Synthesizer, SynthesizerSettings};
241
242        let sound_font = Arc::new(sound_font);
243        let settings = SynthesizerSettings::new(44100);
244        let mut synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
245        synthesizer.note_on(0, 60, 100); // grand piano, middle C
246
247        let mut left = vec![0_f32; 44100];
248        let mut right = vec![0_f32; 44100];
249        synthesizer.render(&mut left, &mut right);
250
251        let peak = left
252            .iter()
253            .fold(0_f32, |peak, sample| peak.max(sample.abs()));
254        assert!(
255            peak > 0.01,
256            "the rendered note was effectively silent (peak {peak})"
257        );
258    }
259
260    #[cfg(feature = "sf3")]
261    #[test]
262    fn test_load_sf3_fluid() {
263        let path = samples_dir_path().join("FluidR3Mono_GM.sf3");
264        let mut file = File::open(&path).unwrap();
265        let sound_font = SoundFont::new(&mut file).unwrap();
266
267        assert_eq!(sound_font.get_bits_per_sample(), 16);
268        assert!(!sound_font.get_wave_data().is_empty());
269        assert!(!sound_font.get_sample_headers().is_empty());
270        assert!(!sound_font.get_presets().is_empty());
271        assert!(!sound_font.get_instruments().is_empty());
272
273        // Every decoded sample header must reference valid PCM data.
274        let wave_len = sound_font.get_wave_data().len();
275        for sample in sound_font.get_sample_headers() {
276            assert!(sample.get_start() >= 0);
277            assert!(sample.get_start() < sample.get_end());
278            assert!(sample.get_end() as usize <= wave_len);
279            assert!(sample.get_end_loop() as usize <= wave_len);
280        }
281
282        // Rendering a note must produce audible output; this guards against
283        // decoding the samples after the instrument regions are built.
284        use std::sync::Arc;
285
286        use crate::{Synthesizer, SynthesizerSettings};
287
288        let sound_font = Arc::new(sound_font);
289        let settings = SynthesizerSettings::new(44100);
290        let mut synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
291        synthesizer.note_on(0, 60, 100); // grand piano, middle C
292
293        let mut left = vec![0_f32; 44100];
294        let mut right = vec![0_f32; 44100];
295        synthesizer.render(&mut left, &mut right);
296
297        let peak = left
298            .iter()
299            .fold(0_f32, |peak, sample| peak.max(sample.abs()));
300        assert!(
301            peak > 0.01,
302            "the rendered note was effectively silent (peak {peak})"
303        );
304    }
305
306    // The .sf3 version must agree with the .sf2 version everywhere the two
307    // fonts represent the same musical data. The conversion tool REORDERED
308    // the samples, instruments and presets, and even cross-linked one sample
309    // name ('PalmMuted Guitar Bb') to two different samples, so nothing here
310    // is compared by index.
311    //
312    // The two builds are not byte-identical: the SF3 conversion started from
313    // a slightly longer source, so 317 samples decode to a few thousand
314    // extra frames. Samples are therefore matched by name and compared with
315    // a frame tolerance; structure that is exactly shared (sample identity
316    // fields, instrument and preset definitions) is compared exactly.
317    #[cfg(feature = "sf3")]
318    #[test]
319    fn test_sf3_matches_sf2() {
320        use std::collections::HashMap;
321
322        /// ~46 ms at 44.1 kHz: permitted frame difference between the two
323        /// builds for lengths and loop points (the observed source-build
324        /// drift is at most ~1000 frames).
325        const TOLERANCE: i32 = 2048;
326
327        fn load(file_name: &str) -> SoundFont {
328            let path = samples_dir_path().join(file_name);
329            let mut file = File::open(&path).unwrap();
330            SoundFont::new(&mut file).unwrap()
331        }
332
333        fn grouped(sf: &SoundFont) -> HashMap<&str, Vec<&crate::SampleHeader>> {
334            let mut groups: HashMap<&str, Vec<&SampleHeader>> = HashMap::new();
335            for header in sf.get_sample_headers() {
336                groups.entry(header.get_name()).or_default().push(header);
337            }
338            groups
339        }
340
341        fn by_name_i(sf: &SoundFont) -> HashMap<&str, &crate::Instrument> {
342            sf.get_instruments()
343                .iter()
344                .map(|i| (i.get_name(), i))
345                .collect()
346        }
347
348        fn find_preset(sf: &SoundFont, bank: i32, patch: i32) -> Option<&crate::Preset> {
349            sf.get_presets()
350                .iter()
351                .find(|p| p.get_bank_number() == bank && p.get_patch_number() == patch)
352        }
353
354        fn rel_loop(header: &crate::SampleHeader, field: fn(&crate::SampleHeader) -> i32) -> i32 {
355            field(header) - header.get_start()
356        }
357
358        let sf2 = load("FluidR3Mono_GM.sf2");
359        let sf3 = load("FluidR3Mono_GM.sf3");
360
361        // ---------------- samples ----------------
362        let headers_2 = sf2.get_sample_headers();
363        let headers_3 = sf3.get_sample_headers();
364        assert_eq!(
365            headers_2.len(),
366            headers_3.len(),
367            "the two builds must contain the same number of samples"
368        );
369
370        let grouped_2 = grouped(&sf2);
371        let grouped_3 = grouped(&sf3);
372        assert_eq!(
373            grouped_2.len(),
374            grouped_3.len(),
375            "the distinct sample names differ"
376        );
377
378        for (name, occurrences_2) in &grouped_2 {
379            let occurrences_3 = grouped_3
380                .get(name)
381                .unwrap_or_else(|| panic!("sample '{name}' is missing in the sf3 build"));
382            assert_eq!(
383                occurrences_2.len(),
384                occurrences_3.len(),
385                "the number of samples named '{name}' differs"
386            );
387
388            // Pair the occurrences (there is one duplicated name) by their
389            // relative loop points, which are order-independent within a font.
390            let mut occurrences_2 = occurrences_2.clone();
391            let mut occurrences_3 = occurrences_3.clone();
392            let key = |header: &&crate::SampleHeader| {
393                (
394                    header.get_start_loop() - header.get_start(),
395                    header.get_end_loop() - header.get_start(),
396                )
397            };
398            occurrences_2.sort_by_key(key);
399            occurrences_3.sort_by_key(key);
400
401            for (a, b) in occurrences_2.iter().zip(&occurrences_3) {
402                assert_eq!(a.get_sample_rate(), b.get_sample_rate(), "sample '{name}'");
403                assert_eq!(
404                    a.get_original_pitch(),
405                    b.get_original_pitch(),
406                    "sample '{name}'"
407                );
408                assert_eq!(
409                    a.get_pitch_correction(),
410                    b.get_pitch_correction(),
411                    "sample '{name}'"
412                );
413                assert!(
414                    (rel_loop(a, crate::SampleHeader::get_start_loop)
415                        - rel_loop(b, crate::SampleHeader::get_start_loop))
416                    .abs()
417                        <= TOLERANCE,
418                    "sample '{name}' loop start differs by more than {TOLERANCE} frames"
419                );
420                assert!(
421                    (rel_loop(a, crate::SampleHeader::get_end_loop)
422                        - rel_loop(b, crate::SampleHeader::get_end_loop))
423                    .abs()
424                        <= TOLERANCE,
425                    "sample '{name}' loop end differs by more than {TOLERANCE} frames"
426                );
427                assert!(
428                    ((a.get_end() - a.get_start()) - (b.get_end() - b.get_start())).abs()
429                        <= TOLERANCE,
430                    "sample '{name}' length differs by more than {TOLERANCE} frames"
431                );
432                assert!(
433                    b.get_sample_type() & 0x10 != 0,
434                    "sf3 sample '{name}' must be flagged as compressed"
435                );
436            }
437        }
438
439        // The decoded PCM pools may differ by at most one tolerance per sample
440        // (each font's exact pool size is pinned by its own info snapshot).
441        let pool_diff = (sf3.get_wave_data().len() as i64 - sf2.get_wave_data().len() as i64).abs();
442        assert!(
443            pool_diff <= headers_3.len() as i64 * TOLERANCE as i64,
444            "decoded PCM pool lengths differ by {pool_diff} frames"
445        );
446
447        // ---------------- instruments ----------------
448        let instruments_2 = sf2.get_instruments();
449        let instruments_3 = sf3.get_instruments();
450        assert_eq!(instruments_2.len(), instruments_3.len());
451
452        let mut names_2: Vec<&str> = instruments_2.iter().map(|i| i.get_name()).collect();
453        let mut names_3: Vec<&str> = instruments_3.iter().map(|i| i.get_name()).collect();
454        names_2.sort_unstable();
455        names_3.sort_unstable();
456        assert_eq!(names_2, names_3, "instrument name sets differ");
457
458        let by_name_i2 = by_name_i(&sf2);
459        let by_name_i3 = by_name_i(&sf3);
460        assert_eq!(by_name_i2.len(), by_name_i3.len());
461        assert_eq!(
462            by_name_i2.len(),
463            instruments_2.len(),
464            "duplicate instrument names"
465        );
466
467        for (name, a) in &by_name_i2 {
468            let b = by_name_i3
469                .get(name)
470                .unwrap_or_else(|| panic!("instrument '{name}' is missing in the sf3 build"));
471            assert_eq!(
472                a.get_regions().len(),
473                b.get_regions().len(),
474                "instrument '{name}'"
475            );
476        }
477
478        // ---------------- presets ----------------
479        let presets_2 = sf2.get_presets();
480        let presets_3 = sf3.get_presets();
481        assert_eq!(presets_2.len(), presets_3.len());
482
483        let mut count_2 = 0;
484        for preset in presets_2 {
485            let bank = preset.get_bank_number();
486            let patch = preset.get_patch_number();
487            let counterpart = find_preset(&sf3, bank, patch).unwrap_or_else(|| {
488                panic!("preset (bank {bank}, patch {patch}) is missing in the sf3 build")
489            });
490            assert_eq!(preset.get_name(), counterpart.get_name());
491            assert_eq!(preset.get_regions().len(), counterpart.get_regions().len());
492            for (ra, rb) in preset.get_regions().iter().zip(counterpart.get_regions()) {
493                assert_eq!(preset_region_values(ra), preset_region_values(rb));
494            }
495            count_2 += 1;
496        }
497        assert_eq!(count_2, presets_2.len());
498    }
499
500    // smpl sub-chunk exists, but is zero-length.
501    #[test]
502    fn test_load_empty_samples() {
503        let path = samples_dir_path().join("test_empty_samples.sf2");
504        let mut file = File::open(&path).unwrap();
505        assert!(matches!(
506            SoundFont::new(&mut file),
507            Err(SoundFontError::SampleDataNotFound)
508        ));
509    }
510
511    // A second smpl sub-chunk used to replace the first one silently.
512    #[test]
513    fn test_load_rejects_duplicate_sample_data_chunks() {
514        let bytes = build_sf2(&sample_spec(), 2);
515        let mut reader = std::io::Cursor::new(bytes);
516        assert!(matches!(
517            SoundFont::new(&mut reader),
518            Err(SoundFontError::InvalidSampleData)
519        ));
520    }
521
522    // The declared size of the `smpl` chunk is not trusted for the
523    // allocation either: a corrupt font can claim gigabytes and then end (#57).
524    #[test]
525    fn test_load_does_not_trust_the_declared_sample_size() {
526        let mut bytes = build_sf2(&sample_spec(), 1);
527
528        // The `smpl` chunk is the only one with a sample-sized body, so it is
529        // the chunk whose size field is rewritten.
530        let position = bytes
531            .windows(4)
532            .position(|window| window == b"smpl")
533            .unwrap();
534        bytes[position + 4..position + 8].copy_from_slice(&u32::MAX.to_le_bytes());
535
536        let mut reader = std::io::Cursor::new(bytes);
537        assert!(matches!(
538            SoundFont::new(&mut reader),
539            Err(SoundFontError::IoError(ref error))
540                if error.kind() == std::io::ErrorKind::UnexpectedEof
541        ));
542    }
543
544    // A looping region with a reversed loop range used to fail the sanity
545    // check, which rejected the whole SoundFont. It must load now, keeping the
546    // loop points as they are (#55).
547    #[test]
548    fn test_load_accepts_an_invalid_loop_range() {
549        let mut spec = sample_spec();
550        spec.start_loop = 96;
551        spec.end_loop = 32;
552
553        let bytes = build_sf2(&spec, 1);
554        let mut reader = std::io::Cursor::new(bytes);
555        let sound_font = SoundFont::new(&mut reader).unwrap();
556
557        let region = &sound_font.get_instruments()[0].get_regions()[0];
558        assert_eq!(region.get_sample_start_loop(), 96);
559        assert_eq!(region.get_sample_end_loop(), 32);
560        assert_eq!(region.get_sample_modes(), crate::LoopMode::Continuous);
561    }
562
563    // The oscillator plays a region with a reversed loop range once instead of
564    // looping it, so the sound must stop while a looping region keeps sounding.
565    #[test]
566    fn test_invalid_loop_range_plays_once() {
567        let mut looping = sample_spec();
568        looping.end_loop = 96;
569        let mut reversed = sample_spec();
570        reversed.start_loop = 96;
571        reversed.end_loop = 32;
572
573        let looping_peak = render_tail_peak(build_sf2(&looping, 1));
574        let reversed_peak = render_tail_peak(build_sf2(&reversed, 1));
575
576        assert!(
577            looping_peak > 0.01,
578            "the looping region stopped sounding (tail peak {looping_peak})"
579        );
580        assert!(
581            reversed_peak < 1e-6,
582            "the reversed loop region kept sounding (tail peak {reversed_peak})"
583        );
584    }
585
586    // RIFF pads a chunk of an odd size with a byte that the chunk size does
587    // not count. A reader that does not skip the padding loses sync and takes
588    // the padding for the first byte of the next chunk id (#56).
589    #[test]
590    fn test_load_skips_padding_after_an_odd_sized_chunk() {
591        let builder = FontBuilder {
592            info_chunks: vec![
593                chunk(b"ifil", &[2, 0, 0, 0]),
594                padded_chunk(b"INAM", b"Odd"),
595                padded_chunk(b"ICMT", b"Odd comment"),
596            ],
597            pmod_bytes: Some(vec![0_u8; 6]),
598            ..FontBuilder::default()
599        };
600
601        let mut reader = std::io::Cursor::new(builder.build());
602        let sound_font = SoundFont::new(&mut reader).unwrap();
603
604        let info = sound_font.get_info();
605        assert_eq!(info.get_bank_name(), "Odd");
606        assert_eq!(info.get_comments(), "Odd comment");
607    }
608
609    // The version chunks used to read their two version numbers and leave the
610    // rest of the declared size in the reader, so the following chunk was read
611    // from the wrong offset (#57).
612    #[test]
613    fn test_load_reads_past_the_version_numbers() {
614        let builder = FontBuilder {
615            info_chunks: vec![
616                chunk(b"ifil", &[2, 0, 1, 0, 0x41, 0x41, 0x41, 0x41]),
617                chunk(b"INAM", b"Bank"),
618            ],
619            ..FontBuilder::default()
620        };
621
622        let mut reader = std::io::Cursor::new(builder.build());
623        let sound_font = SoundFont::new(&mut reader).unwrap();
624
625        let info = sound_font.get_info();
626        assert_eq!(info.get_version().get_major(), 2);
627        assert_eq!(info.get_version().get_minor(), 1);
628        assert_eq!(info.get_bank_name(), "Bank");
629    }
630
631    // A zone span past the end of the zone list used to index out of bounds
632    // instead of being reported as a broken instrument (#57).
633    #[test]
634    fn test_load_rejects_an_instrument_zone_past_the_zone_list() {
635        let builder = FontBuilder {
636            instrument_zone_end: 10,
637            ..FontBuilder::default()
638        };
639
640        let mut reader = std::io::Cursor::new(builder.build());
641        assert!(matches!(
642            SoundFont::new(&mut reader),
643            Err(SoundFontError::InvalidInstrument(0))
644        ));
645    }
646
647    // The same for presets (#57).
648    #[test]
649    fn test_load_rejects_a_preset_zone_past_the_zone_list() {
650        let builder = FontBuilder {
651            preset_zone_end: 10,
652            ..FontBuilder::default()
653        };
654
655        let mut reader = std::io::Cursor::new(builder.build());
656        assert!(matches!(
657            SoundFont::new(&mut reader),
658            Err(SoundFontError::InvalidPreset(0))
659        ));
660    }
661
662    // Renders a short note and returns the peak of its tail, by which time a
663    // region that stops after a single pass has fallen silent.
664    fn render_tail_peak(sound_font: Vec<u8>) -> f32 {
665        use std::sync::Arc;
666
667        use crate::{Synthesizer, SynthesizerSettings};
668
669        let mut reader = std::io::Cursor::new(sound_font);
670        let sound_font = Arc::new(SoundFont::new(&mut reader).unwrap());
671
672        let mut settings = SynthesizerSettings::new(44100);
673        settings.enable_reverb_and_chorus = false;
674        let mut synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
675        synthesizer.note_on(0, 60, 100);
676
677        // The sample is 128 frames long, so a region played once is already
678        // over by the tail of this buffer.
679        const FRAMES: usize = 4096;
680        let mut left = vec![0_f32; FRAMES];
681        let mut right = vec![0_f32; FRAMES];
682        synthesizer.render(&mut left, &mut right);
683
684        left[FRAMES - 1024..]
685            .iter()
686            .fold(0_f32, |peak, sample| peak.max(sample.abs()))
687    }
688}