Skip to main content

xsynth_core/soundfont/
mod.rs

1#![allow(non_camel_case_types)]
2use std::{
3    collections::{HashMap, HashSet},
4    io,
5    path::PathBuf,
6    sync::Arc,
7};
8
9use biquad::Q_BUTTERWORTH_F32;
10use rayon::iter::{IntoParallelIterator, ParallelIterator};
11use thiserror::Error;
12use xsynth_soundfonts::{convert_sample_index, FilterType, LoopMode};
13
14use self::audio::load_audio_file;
15pub use self::audio::AudioLoadError;
16
17use super::{
18    voice::VoiceControlData,
19    voice::{EnvelopeParameters, Voice},
20};
21use crate::{helpers::db_to_amp, voice::EnvelopeDescriptor, AudioStreamParams, ChannelCount};
22
23pub use xsynth_soundfonts::{sf2::Sf2ParseError, sfz::SfzParseError};
24
25mod audio;
26mod config;
27mod utils;
28mod voice_spawners;
29use utils::*;
30use voice_spawners::*;
31
32pub use config::*;
33
34pub trait VoiceSpawner: Sync + Send {
35    fn spawn_voice(&self, control: &VoiceControlData) -> Box<dyn Voice>;
36    fn exclusive_class(&self) -> Option<u8> {
37        None
38    }
39}
40
41pub trait SoundfontBase: Sync + Send + std::fmt::Debug {
42    fn stream_params(&self) -> &'_ AudioStreamParams;
43
44    fn get_attack_voice_spawners_at(
45        &self,
46        bank: u8,
47        preset: u8,
48        key: u8,
49        vel: u8,
50    ) -> Vec<Box<dyn VoiceSpawner>>;
51    fn get_release_voice_spawners_at(
52        &self,
53        bank: u8,
54        preset: u8,
55        key: u8,
56        vel: u8,
57    ) -> Vec<Box<dyn VoiceSpawner>>;
58}
59
60#[derive(Clone)]
61pub(super) struct LoopParams {
62    pub mode: LoopMode,
63    pub offset: u32,
64    pub start: u32,
65    pub end: u32,
66    pub stop: Option<u32>,
67}
68
69struct SampleVoiceSpawnerParams {
70    volume: f32,
71    pan: f32,
72    speed_mult: f32,
73    cutoff: Option<f32>,
74    resonance: f32,
75    filter_type: FilterType,
76    loop_params: LoopParams,
77    envelope: Arc<EnvelopeParameters>,
78    sample: Arc<[Arc<[f32]>]>,
79    interpolator: Interpolator,
80    exclusive_class: Option<u8>,
81}
82
83pub(super) struct SoundfontInstrument {
84    bank: u8,
85    preset: u8,
86    spawner_params_list: Vec<Vec<Arc<SampleVoiceSpawnerParams>>>,
87}
88
89/// Represents a sample soundfont to be used within XSynth.
90///
91/// Supports SFZ and SF2 soundfonts.
92///
93/// ## SFZ specification support (opcodes)
94/// - `lovel` & `hivel`
95/// - `lokey` & `hikey`
96/// - `pitch_keycenter`
97/// - `volume`
98/// - `pan`
99/// - `sample`
100/// - `default_path`
101/// - `loop_mode`
102/// - `loop_start`
103/// - `loop_end`
104/// - `offset`
105/// - `cutoff`
106/// - `resonance`
107/// - `fil_veltrack`
108/// - `fil_keycenter`
109/// - `fil_keytrack`
110/// - `filter_type`
111/// - `tune`
112/// - `ampeg_start`
113/// - `ampeg_delay`
114/// - `ampeg_attack`
115/// - `ampeg_hold`
116/// - `ampeg_decay`
117/// - `ampeg_sustain`
118/// - `ampeg_release`
119///
120/// ## SF2 specification support
121/// ### Generators
122/// - `startAddrsOffset`
123/// - `endAddrsOffset`
124/// - `startloopAddrsOffset`
125/// - `endloopAddrsOffset`
126/// - `startAddrsCoarseOffset`
127/// - `endAddrsCoarseOffset`
128/// - `startloopAddrsCoarseOffset`
129/// - `endloopAddrsCoarseOffset`
130/// - `initialFilterFc`
131/// - `initialFilterQ`
132/// - `pan`
133/// - `delayVolEnv`
134/// - `attackVolEnv`
135/// - `holdVolEnv`
136/// - `decayVolEnv`
137/// - `sustainVolEnv`
138/// - `releaseVolEnv`
139/// - `instrument`
140/// - `keyRange`
141/// - `velRange`
142/// - `initialAttenuation`
143/// - `coarseTune`
144/// - `fineTune`
145/// - `sampleID`
146/// - `sampleModes`
147/// - `overridingRootKey`
148/// - `scaleTuning`
149/// - `exclusiveClass`
150/// - `keynum`
151/// - `velocity`
152/// - `keynumToVolEnvHold`
153/// - `keynumToVolEnvDecay`
154///
155/// ### Modulators
156/// XSynth intentionally supports a baked subset of SF2 modulators so note-on
157/// articulation can be resolved at soundfont load time without adding runtime
158/// voice stages. Currently supported:
159/// - sources: key number and note-on velocity
160/// - curves: linear, concave, convex, and switch
161/// - transforms: linear and absolute
162/// - destinations: attenuation, filter cutoff, pan, volume envelope timings,
163///   and static pitch offsets
164///
165/// The following SF2 features are intentionally not supported in the runtime
166/// engine because they would add significant hot-path and binary-size cost:
167/// - modulation envelope generators and destinations
168/// - modulation LFO / vibrato LFO generators and destinations
169/// - generic CC / aftertouch / pitch-wheel-driven SF2 modulators
170/// - chorus and reverb send behavior
171pub struct SampleSoundfont {
172    instruments: Vec<SoundfontInstrument>,
173    stream_params: AudioStreamParams,
174}
175
176/// Errors that can be generated when loading an SFZ soundfont.
177#[derive(Debug, Error)]
178pub enum LoadSfzError {
179    #[error("IO Error")]
180    IOError(#[from] io::Error),
181
182    #[error("Error loading samples")]
183    AudioLoadError(#[from] AudioLoadError),
184
185    #[error("Error parsing the SFZ: {0}")]
186    SfzParseError(#[from] SfzParseError),
187}
188
189/// Errors that can be generated when loading a soundfont
190/// of an unspecified format.
191#[derive(Debug, Error)]
192pub enum LoadSfError {
193    #[error("Error loading the SFZ: {0}")]
194    LoadSfzError(#[from] LoadSfzError),
195
196    #[error("Error loading the SF2: {0}")]
197    LoadSf2Error(#[from] Sf2ParseError),
198
199    #[error("Unsupported format")]
200    Unsupported,
201}
202
203impl SampleSoundfont {
204    /// Loads a new sample soundfont of an unspecified type.
205    /// The type of the soundfont will be determined from the file extension.
206    ///
207    /// Parameters:
208    /// - `path`: The path of the soundfont to be loaded.
209    /// - `stream_params`: Parameters of the output audio. See the `AudioStreamParams`
210    ///   documentation for the available options.
211    /// - `options`: The soundfont configuration. See the `SoundfontInitOptions`
212    ///   documentation for the available options.
213    pub fn new(
214        path: impl Into<PathBuf>,
215        stream_params: AudioStreamParams,
216        options: SoundfontInitOptions,
217    ) -> Result<Self, LoadSfError> {
218        let path: PathBuf = path.into();
219        if let Some(ext) = path.extension() {
220            match ext.to_str().unwrap_or("").to_lowercase().as_str() {
221                "sfz" => {
222                    Self::new_sfz(path, stream_params, options).map_err(LoadSfError::LoadSfzError)
223                }
224                "sf2" => {
225                    Self::new_sf2(path, stream_params, options).map_err(LoadSfError::LoadSf2Error)
226                }
227                _ => Err(LoadSfError::Unsupported),
228            }
229        } else {
230            Err(LoadSfError::Unsupported)
231        }
232    }
233
234    /// Loads a new SFZ soundfont
235    ///
236    /// Parameters:
237    /// - `path`: The path of the SFZ soundfont to be loaded.
238    /// - `stream_params`: Parameters of the output audio. See the `AudioStreamParams`
239    ///   documentation for the available options.
240    /// - `options`: The soundfont configuration. See the `SoundfontInitOptions`
241    ///   documentation for the available options.
242    pub fn new_sfz(
243        sfz_path: impl Into<PathBuf>,
244        stream_params: AudioStreamParams,
245        options: SoundfontInitOptions,
246    ) -> Result<Self, LoadSfzError> {
247        let regions = xsynth_soundfonts::sfz::parse_soundfont(sfz_path.into())?;
248
249        // Find the unique samples that we need to parse and convert
250        let unique_sample_params: HashSet<_> = regions
251            .iter()
252            .map(sample_cache_from_region_params)
253            .collect();
254
255        // Parse and convert them in parallel
256        let samples: Result<HashMap<_, _>, _> = unique_sample_params
257            .into_par_iter()
258            .map(|params| -> Result<(_, _), LoadSfzError> {
259                let sample = load_audio_file(&params.path, stream_params)?;
260                Ok((params, sample))
261            })
262            .collect();
263        let samples = samples?;
264
265        // Generate region params
266        let mut spawner_params_list = Vec::<Vec<Arc<SampleVoiceSpawnerParams>>>::new();
267        for _ in 0..(128 * 128) {
268            spawner_params_list.push(Vec::new());
269        }
270
271        // Write region params
272        for region in regions {
273            let params = sample_cache_from_region_params(&region);
274
275            // Key value -1 is used for CC triggered regions which are not supported by XSynth
276            if region.keyrange.contains(&-1) {
277                continue;
278            }
279
280            for key in region.keyrange.clone() {
281                for vel in region.velrange.clone() {
282                    let index = key_vel_to_index(key as u8, vel);
283                    let speed_mult =
284                        get_speed_mult_from_keys(key as u8, region.pitch_keycenter as u8)
285                            * cents_factor(region.tune as f32);
286
287                    let mut envelope = region.ampeg_envelope.clone();
288                    envelope.ampeg_release +=
289                        (vel as f32 / 127.0) * region.ampeg_envelope.ampeg_vel2release;
290                    let envelope_params = Arc::new(
291                        envelope_descriptor_from_region_params(&envelope).to_envelope_params(
292                            stream_params.sample_rate,
293                            options.vol_envelope_options,
294                        ),
295                    );
296
297                    let mut cutoff = None;
298                    if options.use_effects {
299                        if let Some(mut cutoff_t) = region.cutoff {
300                            if cutoff_t >= 1.0 {
301                                let cents = vel as f32 / 127.0 * region.fil_veltrack as f32
302                                    + (key as f32 - region.fil_keycenter as f32)
303                                        * region.fil_keytrack as f32;
304                                cutoff_t *= cents_factor(cents);
305                                cutoff = Some(
306                                    cutoff_t
307                                        .clamp(1.0, stream_params.sample_rate as f32 / 2.0 - 100.0),
308                                );
309                            }
310                        }
311                    }
312
313                    let pan_mult = vel as f32 / 127.0 * region.pan_veltrack
314                        + (key as f32 - region.pan_keycenter as f32) * region.pan_keytrack;
315                    let pan = (region.pan as f32 + pan_mult).clamp(-100.0, 100.0) / 100.0;
316                    let pan = (pan + 1.0) / 2.0;
317
318                    let vol_vel = {
319                        let a = region.amp_veltrack / 100.0;
320                        let aabs = a.abs();
321                        let vel = vel as f32;
322
323                        127.0 * (1.0 - aabs)
324                            + vel * (a + aabs) / 2.0
325                            + (127.0 - vel) * (aabs - a) / 2.0
326                    };
327                    let vol_mult = (vol_vel / 127.0).powi(2);
328                    let vol_db_add =
329                        (key as f32 - region.amp_keycenter as f32) * region.amp_keytrack;
330                    let vol_db = (region.volume as f32 + vol_db_add).clamp(-96.0, 12.0);
331                    let volume = vol_mult * db_to_amp(vol_db);
332
333                    let sample_rate = samples[&params].1;
334
335                    let loop_params = LoopParams {
336                        mode: if region.loop_start == region.loop_end {
337                            LoopMode::NoLoop
338                        } else {
339                            region.loop_mode
340                        },
341                        offset: convert_sample_index(
342                            region.offset,
343                            sample_rate,
344                            stream_params.sample_rate,
345                        ),
346                        start: convert_sample_index(
347                            region.loop_start,
348                            sample_rate,
349                            stream_params.sample_rate,
350                        ),
351                        end: convert_sample_index(
352                            region.loop_end,
353                            sample_rate,
354                            stream_params.sample_rate,
355                        ),
356                        stop: None,
357                    };
358
359                    let mut region_samples = samples[&params].0.clone();
360                    if stream_params.channels == ChannelCount::Stereo && region_samples.len() == 1 {
361                        region_samples =
362                            Arc::new([region_samples[0].clone(), region_samples[0].clone()]);
363                    }
364
365                    let spawner_params = Arc::new(SampleVoiceSpawnerParams {
366                        pan,
367                        volume,
368                        envelope: envelope_params,
369                        speed_mult,
370                        cutoff,
371                        resonance: db_to_amp(region.resonance) * Q_BUTTERWORTH_F32,
372                        filter_type: region.filter_type,
373                        interpolator: options.interpolator,
374                        loop_params,
375                        sample: region_samples,
376                        exclusive_class: None,
377                    });
378
379                    spawner_params_list[index].push(spawner_params.clone());
380                }
381            }
382        }
383
384        Ok(SampleSoundfont {
385            instruments: vec![SoundfontInstrument {
386                bank: options.bank.unwrap_or(0),
387                preset: options.preset.unwrap_or(0),
388                spawner_params_list,
389            }],
390            stream_params,
391        })
392    }
393
394    /// Loads a new SF2 soundfont
395    ///
396    /// Parameters:
397    /// - `path`: The path of the SF2 soundfont to be loaded.
398    /// - `stream_params`: Parameters of the output audio. See the `AudioStreamParams`
399    ///   documentation for the available options.
400    /// - `options`: The soundfont configuration. See the `SoundfontInitOptions`
401    ///   documentation for the available options.
402    pub fn new_sf2(
403        sf2_path: impl Into<PathBuf>,
404        stream_params: AudioStreamParams,
405        options: SoundfontInitOptions,
406    ) -> Result<Self, Sf2ParseError> {
407        let presets =
408            xsynth_soundfonts::sf2::load_soundfont(sf2_path.into(), stream_params.sample_rate)?;
409
410        let mut instruments = Vec::new();
411
412        for preset in presets {
413            if let Some(bank) = options.bank {
414                if bank != preset.bank as u8 {
415                    continue;
416                }
417            }
418            if let Some(presetn) = options.preset {
419                if presetn != preset.preset as u8 {
420                    continue;
421                }
422            }
423
424            let mut spawner_params_list = Vec::<Vec<Arc<SampleVoiceSpawnerParams>>>::new();
425            for _ in 0..(128 * 128) {
426                spawner_params_list.push(Vec::new());
427            }
428
429            let mut unique_envelope_params =
430                Vec::<(EnvelopeDescriptor, Arc<EnvelopeParameters>)>::new();
431
432            for region in preset.regions {
433                for key in region.keyrange.clone() {
434                    for vel in region.velrange.clone() {
435                        let index = key_vel_to_index(key, vel);
436                        let note_params = region.note_params(key, vel);
437                        let envelope =
438                            envelope_descriptor_from_region_params(&note_params.ampeg_envelope);
439                        let envelope_params = if let Some((_, params)) = unique_envelope_params
440                            .iter()
441                            .find(|(descriptor, _)| *descriptor == envelope)
442                        {
443                            params.clone()
444                        } else {
445                            let params = Arc::new(envelope.to_envelope_params(
446                                stream_params.sample_rate,
447                                options.vol_envelope_options,
448                            ));
449                            unique_envelope_params.push((envelope, params.clone()));
450                            params
451                        };
452                        let tuned_key_cents =
453                            (key as f32 - region.root_key as f32) * region.scale_tuning as f32;
454                        let speed_mult = cents_factor(
455                            tuned_key_cents
456                                + region.fine_tune as f32
457                                + region.coarse_tune as f32 * 100.0
458                                + note_params.tune_cents,
459                        );
460
461                        let mut cutoff = None;
462                        if options.use_effects {
463                            if let Some(cutoff_t) = note_params.cutoff {
464                                if cutoff_t >= 1.0 {
465                                    cutoff = Some(cutoff_t.clamp(
466                                        1.0,
467                                        stream_params.sample_rate as f32 / 2.0 - 100.0,
468                                    ));
469                                }
470                            }
471                        }
472
473                        let pan = ((note_params.pan as f32 / 500.0) + 1.0) / 2.0;
474
475                        let loop_params = LoopParams {
476                            mode: if region.loop_start == region.loop_end {
477                                LoopMode::NoLoop
478                            } else {
479                                region.loop_mode
480                            },
481                            offset: region.offset,
482                            start: region.loop_start,
483                            end: region.loop_end,
484                            stop: Some(region.sample_end),
485                        };
486
487                        let mut region_samples = region.sample.clone();
488                        if stream_params.channels == ChannelCount::Stereo
489                            && region_samples.len() == 1
490                        {
491                            region_samples =
492                                Arc::new([region_samples[0].clone(), region_samples[0].clone()]);
493                        }
494
495                        let spawner_params = Arc::new(SampleVoiceSpawnerParams {
496                            pan,
497                            volume: note_params.volume,
498                            envelope: envelope_params,
499                            speed_mult,
500                            cutoff,
501                            resonance: db_to_amp(note_params.resonance) * Q_BUTTERWORTH_F32,
502                            filter_type: FilterType::LowPass,
503                            interpolator: options.interpolator,
504                            loop_params,
505                            sample: region_samples,
506                            exclusive_class: region.exclusive_class,
507                        });
508
509                        spawner_params_list[index].push(spawner_params.clone());
510                    }
511                }
512            }
513
514            let new = SoundfontInstrument {
515                bank: preset.bank as u8,
516                preset: preset.preset as u8,
517                spawner_params_list,
518            };
519            instruments.push(new);
520        }
521
522        Ok(SampleSoundfont {
523            instruments,
524            stream_params,
525        })
526    }
527}
528
529impl std::fmt::Debug for SampleSoundfont {
530    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
531        write!(f, "SampleSoundfont")
532    }
533}
534
535impl SoundfontBase for SampleSoundfont {
536    fn stream_params(&self) -> &'_ AudioStreamParams {
537        &self.stream_params
538    }
539
540    fn get_attack_voice_spawners_at(
541        &self,
542        bank: u8,
543        preset: u8,
544        key: u8,
545        vel: u8,
546    ) -> Vec<Box<dyn VoiceSpawner>> {
547        use simdeez::*; // nuts
548
549        use simdeez::prelude::*;
550
551        simd_runtime_generate!(
552            fn get(
553                key: u8,
554                vel: u8,
555                sf: &SoundfontInstrument,
556                stream_params: &AudioStreamParams,
557            ) -> Vec<Box<dyn VoiceSpawner>> {
558                if sf.spawner_params_list.is_empty() {
559                    return Vec::new();
560                }
561
562                let index = key_vel_to_index(key, vel);
563                let mut vec = Vec::<Box<dyn VoiceSpawner>>::new();
564                for spawner in &sf.spawner_params_list[index] {
565                    match stream_params.channels {
566                        ChannelCount::Stereo => vec.push(Box::new(
567                            StereoSampledVoiceSpawner::<S>::new(spawner, vel, *stream_params),
568                        )),
569                        ChannelCount::Mono => vec.push(Box::new(
570                            MonoSampledVoiceSpawner::<S>::new(spawner, vel, *stream_params),
571                        )),
572                    }
573                }
574                vec
575            }
576        );
577
578        let empty = SoundfontInstrument {
579            bank: 0,
580            preset: 0,
581            spawner_params_list: Vec::new(),
582        };
583
584        let instrument = self
585            .instruments
586            .iter()
587            .find(|i| i.bank == bank && i.preset == preset)
588            .unwrap_or(&empty);
589
590        get(key, vel, instrument, self.stream_params())
591    }
592
593    fn get_release_voice_spawners_at(
594        &self,
595        _bank: u8,
596        _preset: u8,
597        _key: u8,
598        _vel: u8,
599    ) -> Vec<Box<dyn VoiceSpawner>> {
600        vec![]
601    }
602}