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#[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 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 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 pub fn get_info(&self) -> &SoundFontInfo {
96 &self.info
97 }
98
99 pub fn get_bits_per_sample(&self) -> i32 {
101 self.bits_per_sample
102 }
103
104 pub fn get_wave_data(&self) -> &[i16] {
106 &self.wave_data[..]
107 }
108
109 pub fn get_sample_headers(&self) -> &[SampleHeader] {
111 &self.sample_headers[..]
112 }
113
114 pub fn get_presets(&self) -> &[Preset] {
116 &self.presets[..]
117 }
118
119 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 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 #[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 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 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); 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 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 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); 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 #[cfg(feature = "sf3")]
310 #[test]
311 fn test_sf3_matches_sf2() {
312 use std::collections::HashMap;
313
314 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 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 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 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 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 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 #[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}