From 0ca32c3afe2af629ccffc949296fef88e29bd23f Mon Sep 17 00:00:00 2001 From: MyBlackMIDIScore Date: Wed, 31 Jul 2024 18:34:13 +0300 Subject: [PATCH] Add documentation --- core/src/audio_pipe.rs | 14 +++- core/src/audio_stream.rs | 2 + core/src/buffered_renderer.rs | 28 +++++--- core/src/channel/event.rs | 22 +++++- core/src/channel/mod.rs | 33 +++++++-- core/src/channel/params.rs | 5 +- core/src/channel_group/events.rs | 6 ++ core/src/channel_group/mod.rs | 24 ++++++- core/src/effects.rs | 2 +- core/src/effects/filter.rs | 19 ++++- core/src/effects/limiter.rs | 3 + core/src/helpers.rs | 2 + core/src/lib.rs | 3 +- core/src/soundfont/audio.rs | 3 +- core/src/soundfont/mod.rs | 120 +++++++++++++++++++++++++++++-- core/src/voice.rs | 36 +++++++--- core/src/voice/envelopes.rs | 17 +++-- kdmapi/src/lib.rs | 2 +- realtime/src/config.rs | 25 +++++++ realtime/src/event_senders.rs | 11 ++- realtime/src/lib.rs | 4 +- realtime/src/realtime_synth.rs | 32 ++++++++- render/src/builder.rs | 12 +++- render/src/config.rs | 22 ++++++ render/src/lib.rs | 4 +- render/src/rendered.rs | 11 +++ soundfonts/src/lib.rs | 19 +++++ soundfonts/src/resample.rs | 2 + soundfonts/src/sf2/mod.rs | 4 ++ soundfonts/src/sfz/mod.rs | 14 ++-- soundfonts/src/sfz/parse.rs | 5 +- 31 files changed, 439 insertions(+), 67 deletions(-) diff --git a/core/src/audio_pipe.rs b/core/src/audio_pipe.rs index c71b5f6..c44dd98 100644 --- a/core/src/audio_pipe.rs +++ b/core/src/audio_pipe.rs @@ -1,16 +1,24 @@ use crate::AudioStreamParams; +/// An object to read audio samples from. pub trait AudioPipe { - /// The stream parameters of the audio pipe + /// The audio stream parameters of the audio pipe. fn stream_params(&self) -> &'_ AudioStreamParams; - /// Reads samples from the pipe + /// Reads samples from the pipe. + /// + /// When using in a MIDI synthesizer, the amount of samples determines the + /// time of the current active MIDI events. For example if we send a note + /// on event and read 44100 samples (with a 44.1kHz sample rate), then the + /// note will be audible for 1 second. If after reading those samples we + /// send a note off event for the same key, then on the next read the key + /// will be released. If we don't, then the note will keep playing. fn read_samples(&mut self, to: &mut [f32]) { assert!(to.len() as u32 % self.stream_params().channels.count() as u32 == 0); self.read_samples_unchecked(to); } - /// Reads samples from the pipe without checking the channel count of the output + /// Reads samples from the pipe without checking the channel count of the output. fn read_samples_unchecked(&mut self, to: &mut [f32]); } diff --git a/core/src/audio_stream.rs b/core/src/audio_stream.rs index 8033c54..f39c7f1 100644 --- a/core/src/audio_stream.rs +++ b/core/src/audio_stream.rs @@ -1,3 +1,4 @@ +/// Number of audio channels. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ChannelCount { Mono, @@ -28,6 +29,7 @@ impl From for ChannelCount { } } +/// Parameters of the output audio. #[derive(Debug, Clone, Copy)] pub struct AudioStreamParams { pub sample_rate: u32, diff --git a/core/src/buffered_renderer.rs b/core/src/buffered_renderer.rs index 00f060a..d870015 100644 --- a/core/src/buffered_renderer.rs +++ b/core/src/buffered_renderer.rs @@ -14,53 +14,57 @@ use crate::AudioStreamParams; use super::AudioPipe; +/// Holds the statistics for an instance of BufferedRenderer. #[derive(Debug, Clone)] -pub struct BufferedRendererStats { - /// The number of samples currently buffered. - /// Can be negative if the reader is waiting for more samples. +struct BufferedRendererStats { samples: Arc, - /// The number of samples that were in the buffer after the last read. last_samples_after_read: Arc, - /// The last number of samples last requested by the read command. last_request_samples: Arc, - /// The last 100 render time percentages (0 to 1) - /// of how long the render thread spent rendering, from the max allowed time. render_time: Arc>>, - /// The number of samples to render each iteration render_size: Arc, } +/// Reads the statistics of an instance of BufferedRenderer in a usable way. pub struct BufferedRendererStatsReader { stats: BufferedRendererStats, } impl BufferedRendererStatsReader { + /// The number of samples currently buffered. + /// Can be negative if the reader is waiting for more samples. pub fn samples(&self) -> i64 { self.stats.samples.load(Ordering::Relaxed) } + /// The number of samples that were in the buffer after the last read. pub fn last_samples_after_read(&self) -> i64 { self.stats.last_samples_after_read.load(Ordering::Relaxed) } + /// The last number of samples last requested by the read command. pub fn last_request_samples(&self) -> i64 { self.stats.last_request_samples.load(Ordering::Relaxed) } + /// The number of samples to render each iteration. pub fn render_size(&self) -> usize { self.stats.render_size.load(Ordering::Relaxed) } + /// The average render time percentages (0 to 1) + /// of how long the render thread spent rendering, from the max allowed time. pub fn average_renderer_load(&self) -> f64 { let queue = self.stats.render_time.read().unwrap(); let total = queue.len(); queue.iter().sum::() / total as f64 } + /// The last render time percentage (0 to 1) + /// of how long the render thread spent rendering, from the max allowed time. pub fn last_renderer_load(&self) -> f64 { let queue = self.stats.render_time.read().unwrap(); *queue.front().unwrap_or(&0.0) @@ -69,6 +73,7 @@ impl BufferedRendererStatsReader { /// The helper struct for deferred sample rendering. /// Helps avoid stutter when the render time is exceding the max time allowed by the audio driver. +/// /// Instead, it renders in a separate thread with much smaller sample sizes, causing a minimal impact on latency /// while allowing more time to render per sample. /// @@ -84,6 +89,7 @@ pub struct BufferedRenderer { /// Whether the render thread should be killed. killed: Arc>, + /// The thread handle to wait for at the end. thread_handle: Option>, @@ -91,6 +97,12 @@ pub struct BufferedRenderer { } impl BufferedRenderer { + /// Creates a new instance of BufferedRenderer + /// + /// - `render`: An object implementing the AudioPipe struct for BufferedRenderer to + /// read samples from + /// - `stream_params`: Parameters of the output audio + /// - `render_size`: The number of samples to render each iteration pub fn new( mut render: F, stream_params: AudioStreamParams, diff --git a/core/src/channel/event.rs b/core/src/channel/event.rs index d4a3d53..1c5853f 100644 --- a/core/src/channel/event.rs +++ b/core/src/channel/event.rs @@ -2,55 +2,71 @@ use std::sync::Arc; use crate::soundfont::SoundfontBase; +/// MIDI events for a single key in a channel. #[derive(Debug, Clone)] pub enum KeyNoteEvent { /// Starts a new note voice with a velocity On(u8), + /// Signals off to a note voice Off, + /// Signals off to all note voices AllOff, + /// Kills all note voices without decay AllKilled, } +/// Events to modify parameters of a channel. #[derive(Debug, Clone)] pub enum ChannelConfigEvent { /// Sets the soundfonts for the channel SetSoundfonts(Vec>), + /// Sets the layer count for the soundfont SetLayerCount(Option), } +/// MIDI events for a channel. #[derive(Debug, Clone)] pub enum ChannelAudioEvent { - /// Starts a new note vocice + /// Starts a new note voice NoteOn { key: u8, vel: u8 }, + /// Signals off to a note voice NoteOff { key: u8 }, + /// Signal off to all voices AllNotesOff, + /// Kill all voices without decay AllNotesKilled, + /// Resets all CC to their default values ResetControl, + /// Control event for the channel Control(ControlEvent), + /// Program change event ProgramChange(u8), } +/// Wrapper enum for various events for a channel. #[derive(Debug, Clone)] pub enum ChannelEvent { - /// Audio + /// Audio event Audio(ChannelAudioEvent), - /// Config event for the channel + /// Configuration event for the channel Config(ChannelConfigEvent), } +/// MIDI control events for a channel. #[derive(Debug, Clone)] pub enum ControlEvent { + /// A raw control change event Raw(u8, u8), /// The pitch bend strength, in tones diff --git a/core/src/channel/mod.rs b/core/src/channel/mod.rs index ca93342..cd54fc8 100644 --- a/core/src/channel/mod.rs +++ b/core/src/channel/mod.rs @@ -9,10 +9,7 @@ use crate::{ use soundfonts::FilterType; -use self::{ - key::KeyData, - params::{VoiceChannelParams, VoiceChannelStatsReader}, -}; +use self::{key::KeyData, params::VoiceChannelParams}; use super::AudioPipe; @@ -29,7 +26,9 @@ mod voice_spawner; mod event; pub use event::*; -pub struct ValueLerp { +pub use params::VoiceChannelStatsReader; + +pub(crate) struct ValueLerp { lerp_length: f32, step: f32, current: f32, @@ -121,9 +120,18 @@ impl ControlEventData { } } +/// Options for initializing a new VoiceChannel. #[derive(Debug, Clone, Copy)] pub struct ChannelInitOptions { + /// If set to true, the voices killed due to the voice limit will fade out. + /// If set to false, they will be killed immediately, usually causing clicking. + /// + /// Default: `false` pub fade_out_killing: bool, + + /// If set to true, the channel will only use drum patches. + /// + /// Default: `false` pub drums_only: bool, } @@ -137,6 +145,9 @@ impl Default for ChannelInitOptions { } } +/// Represents a single MIDI channel within XSynth. +/// +/// Keeps track and manages MIDI events and the active voices of a channel. pub struct VoiceChannel { key_voices: Vec, @@ -152,11 +163,17 @@ pub struct VoiceChannel { /// Processed control data, ready to feed to voices voice_control_data: VoiceControlData, - // Effects + /// Effects cutoff: MultiChannelBiQuad, } impl VoiceChannel { + /// Initializes a new voice channel. + /// + /// - `options`: Channel configuration + /// - `stream_params`: Parameters of the output audio + /// - `threadpool`: The thread-pool that will be used to render the voices concurrently. + /// If None is used, the voices of the channel will be rendered successively. pub fn new( options: ChannelInitOptions, stream_params: AudioStreamParams, @@ -292,6 +309,7 @@ impl VoiceChannel { } } + /// Sends a ControlEvent to the channel. pub fn process_control_event(&mut self, event: ControlEvent) { match event { ControlEvent::Raw(controller, value) => match controller { @@ -490,10 +508,12 @@ impl VoiceChannel { self.propagate_voice_controls(); } + /// Sends a ChannelEvent to the channel. pub fn process_event(&mut self, event: ChannelEvent) { self.push_events_iter(std::iter::once(event)); } + /// Sends multiple ChannelEvent items to the channel as an iterator. pub fn push_events_iter>(&mut self, iter: T) { for e in iter { match e { @@ -541,6 +561,7 @@ impl VoiceChannel { } } + /// Returns a reader for the VoiceChannel statistics. pub fn get_channel_stats(&self) -> VoiceChannelStatsReader { let stats = self.params.stats.clone(); VoiceChannelStatsReader::new(stats) diff --git a/core/src/channel/params.rs b/core/src/channel/params.rs index 15b8a1b..b09fb38 100644 --- a/core/src/channel/params.rs +++ b/core/src/channel/params.rs @@ -4,11 +4,13 @@ use crate::AudioStreamParams; use super::{channel_sf::ChannelSoundfont, ChannelConfigEvent}; +/// Holds the statistics for an instance of VoiceChannel. #[derive(Debug, Clone)] pub struct VoiceChannelStats { pub(super) voice_counter: Arc, } +/// Reads the statistics of an instance of VoiceChannel in a usable way. pub struct VoiceChannelStatsReader { stats: VoiceChannelStats, } @@ -63,10 +65,11 @@ impl VoiceChannelParams { } impl VoiceChannelStatsReader { - pub fn new(stats: VoiceChannelStats) -> Self { + pub(super) fn new(stats: VoiceChannelStats) -> Self { Self { stats } } + /// The active voice count of the VoiceChannel pub fn voice_count(&self) -> u64 { self.stats .voice_counter diff --git a/core/src/channel_group/events.rs b/core/src/channel_group/events.rs index c50f2c7..cb39a19 100644 --- a/core/src/channel_group/events.rs +++ b/core/src/channel_group/events.rs @@ -1,7 +1,13 @@ use crate::channel::{ChannelAudioEvent, ChannelConfigEvent}; +/// Wrapper enum for various events to be sent to a MIDI synthesizer. pub enum SynthEvent { + /// An audio event to be sent to the specified channel Channel(u32, ChannelAudioEvent), + + /// An audio event to be sent to all available channels AllChannels(ChannelAudioEvent), + + /// Configuration event for all channels ChannelConfig(ChannelConfigEvent), } diff --git a/core/src/channel_group/mod.rs b/core/src/channel_group/mod.rs index 0ac4720..5d99bfa 100644 --- a/core/src/channel_group/mod.rs +++ b/core/src/channel_group/mod.rs @@ -12,6 +12,9 @@ use rayon::prelude::*; const MAX_EVENT_CACHE_SIZE: u32 = 1024 * 1024; +/// Represents a MIDI synthesizer within XSynth. +/// +/// Manages multiple VoiceChannel objects at once. pub struct ChannelGroup { thread_pool: rayon::ThreadPool, cached_event_count: u32, @@ -21,15 +24,29 @@ pub struct ChannelGroup { audio_params: AudioStreamParams, } +/// Options for initializing a new ChannelGroup. pub struct ChannelGroupConfig { + /// Channel initialization options (same for all channels). pub channel_init_options: ChannelInitOptions, + + /// Amount of VoiceChannel objects to be created. + /// (Number of MIDI channels) pub channel_count: u32, + + /// A vector which specifies which of the created channels (indexes) will be used for drums. + /// For example in a conventional 16 MIDI channel setup where channel 10 is used for + /// drums, the vector would be set as \[9\] (counting from 0). pub drums_channels: Vec, + + /// Parameters of the output audio. pub audio_params: AudioStreamParams, + + /// Whether or not to use a threadpool to render voices. pub use_threadpool: bool, } impl ChannelGroup { + /// Creates a new ChannelGroup with the given configuration. pub fn new(config: ChannelGroupConfig) -> Self { let mut channels = Vec::new(); let mut channel_events_cache = Vec::new(); @@ -44,9 +61,7 @@ impl ChannelGroup { for i in 0..config.channel_count { let mut init = config.channel_init_options; - if config.drums_channels.clone().into_iter().any(|c| c == i) { - init.drums_only = true; - } + init.drums_only = config.drums_channels.clone().into_iter().any(|c| c == i); channels.push(VoiceChannel::new(init, config.audio_params, pool.clone())); channel_events_cache.push(Vec::new()); @@ -66,6 +81,8 @@ impl ChannelGroup { } } + /// Sends a SynthEvent to the ChannelGroup. + /// Please see the SynthEvent documentation for more information. pub fn send_event(&mut self, event: SynthEvent) { match event { SynthEvent::Channel(channel, event) => { @@ -136,6 +153,7 @@ impl ChannelGroup { }); } + /// Returns the active voice count of all the MIDI channels. pub fn voice_count(&self) -> u64 { self.channels .iter() diff --git a/core/src/effects.rs b/core/src/effects.rs index e884984..307ca3a 100644 --- a/core/src/effects.rs +++ b/core/src/effects.rs @@ -1,4 +1,4 @@ mod limiter; pub use limiter::*; -pub mod filter; +mod filter; pub use filter::*; diff --git a/core/src/effects/filter.rs b/core/src/effects/filter.rs index 8a78887..ea8acc6 100644 --- a/core/src/effects/filter.rs +++ b/core/src/effects/filter.rs @@ -4,7 +4,7 @@ use simdeez::prelude::*; use soundfonts::FilterType; #[derive(Clone)] -pub struct BiQuadFilter { +pub(crate) struct BiQuadFilter { filter: DirectForm1, } @@ -69,6 +69,13 @@ impl BiQuadFilter { } } +/// A multi-channel bi-quad audio filter. +/// +/// Supports single pole low pass filter and two pole low pass, high pass +/// and band pass filters. For more information please see the FilterType +/// documentation. +/// +/// Uses the `biquad` crate for signal processing. pub struct MultiChannelBiQuad { channels: Vec, fil_type: FilterType, @@ -78,6 +85,14 @@ pub struct MultiChannelBiQuad { } impl MultiChannelBiQuad { + /// Creates a new audio filter with the given parameters. + /// + /// - `channels`: Number of audio channels + /// - `fil_type`: Type of the audio filter. See FilterType docs + /// - `freq`: Cutoff frequency + /// - `sample_rate`: Sample rate of the audio to be processed + /// - `q`: The Q parameter of the cutoff filter. Use None for the default + /// Butterworth value. pub fn new( channels: usize, fil_type: FilterType, @@ -96,6 +111,7 @@ impl MultiChannelBiQuad { } } + /// Changes the type of the audio filter. pub fn set_filter_type(&mut self, fil_type: FilterType, freq: f32, q: Option) { self.value.set_end(freq); self.fil_type = fil_type; @@ -109,6 +125,7 @@ impl MultiChannelBiQuad { } } + /// Filters the audio of the given sample buffer. pub fn process(&mut self, sample: &mut [f32]) { let channel_count = self.channels.len(); for (i, s) in sample.iter_mut().enumerate() { diff --git a/core/src/effects/limiter.rs b/core/src/effects/limiter.rs index 5cd7231..00b991f 100644 --- a/core/src/effects/limiter.rs +++ b/core/src/effects/limiter.rs @@ -37,6 +37,7 @@ impl SingleChannelLimiter { } } +/// A multi-channel audio limiter. pub struct VolumeLimiter { channels: Vec, channel_count: usize, @@ -50,6 +51,7 @@ pub struct VolumeLimiterIter<'a, 'b, T: 'b + Iterator> { } impl VolumeLimiter { + /// Initializes a new audio limiter with a specified audio channel count. pub fn new(channel_count: u16) -> VolumeLimiter { let mut limiters = Vec::new(); for _ in 0..channel_count { @@ -61,6 +63,7 @@ impl VolumeLimiter { } } + /// Limits the audio of the given sample buffer. pub fn limit(&mut self, sample: &mut [f32]) { for (i, s) in sample.iter_mut().enumerate() { *s = self.channels[i % self.channel_count].limit(*s); diff --git a/core/src/helpers.rs b/core/src/helpers.rs index cba9c3c..1028466 100644 --- a/core/src/helpers.rs +++ b/core/src/helpers.rs @@ -17,10 +17,12 @@ pub fn prepapre_cache_vec(vec: &mut Vec, len: usize, default: T) { vec.fill(default); } +/// Converts a dB value to 0-1 amplitude pub fn db_to_amp(db: f32) -> f32 { 10f32.powf(db / 20.0) } +/// Checks if two `Arc` vecs are equal pub fn are_arc_vecs_equal(old: &[Arc], new: &[Arc]) -> bool { // First, check if the lengths are the same if old.len() != new.len() { diff --git a/core/src/lib.rs b/core/src/lib.rs index 8f1bc9a..0657929 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,8 +1,7 @@ #![allow(clippy::let_and_return)] #![allow(non_local_definitions)] -mod buffered_renderer; -pub use buffered_renderer::*; +pub mod buffered_renderer; pub mod channel; diff --git a/core/src/soundfont/audio.rs b/core/src/soundfont/audio.rs index 18f4a60..a165e6e 100644 --- a/core/src/soundfont/audio.rs +++ b/core/src/soundfont/audio.rs @@ -10,6 +10,7 @@ use crate::{AudioStreamParams, ChannelCount}; use soundfonts::resample::resample_vecs; use thiserror::Error; +/// Errors that can be generated when loading an audio file. #[derive(Debug, Error)] pub enum AudioLoadError { #[error("IO Error")] @@ -27,7 +28,7 @@ pub enum AudioLoadError { type ProcessedSample = (Arc<[Arc<[f32]>]>, u32); -pub fn load_audio_file( +pub(super) fn load_audio_file( path: &PathBuf, stream_params: AudioStreamParams, ) -> Result { diff --git a/core/src/soundfont/mod.rs b/core/src/soundfont/mod.rs index 3df5daf..a295d3b 100644 --- a/core/src/soundfont/mod.rs +++ b/core/src/soundfont/mod.rs @@ -10,11 +10,12 @@ use biquad::Q_BUTTERWORTH_F32; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use soundfonts::{ sf2::Sf2ParseError, - sfz::{parse::SfzParseError, AmpegEnvelopeParams, RegionParams}, + sfz::{AmpegEnvelopeParams, RegionParams, SfzParseError}, }; use thiserror::Error; -use self::audio::{load_audio_file, AudioLoadError}; +use self::audio::load_audio_file; +pub use self::audio::AudioLoadError; use super::{ voice::VoiceControlData, @@ -28,7 +29,7 @@ use crate::{ use soundfonts::{convert_sample_index, FilterType, LoopMode}; -pub mod audio; +mod audio; mod voice_spawners; use voice_spawners::*; @@ -55,14 +56,18 @@ pub trait SoundfontBase: Sync + Send + std::fmt::Debug { ) -> Vec>; } +/// Type of sample interpolator #[derive(Clone, PartialEq, Eq, Copy, Debug)] pub enum Interpolator { + /// Nearest neighbor Nearest, + + /// Linear Linear, } #[derive(Clone)] -pub struct LoopParams { +pub(super) struct LoopParams { pub mode: LoopMode, pub offset: u32, pub start: u32, @@ -99,12 +104,38 @@ impl SampleCache { } } +/// Options for initializing/loading a new sample soundfont. #[derive(Debug, Clone, Copy)] pub struct SoundfontInitOptions { + /// The bank number (0-128) to extract and use from the soundfont. + /// `None` means to use all available banks (bank 0 for SFZ). + /// + /// Default: `None` pub bank: Option, + + /// The preset number (0-127) to extract and use from the soundfont. + /// `None` means to use all available presets (preset 0 for SFZ). + /// + /// Default: `None` pub preset: Option, + + /// If set to true, the voices generated using this soundfont will + /// release using a linear function instead of convex. + /// + /// Default: `false` pub linear_release: bool, + + /// If set to true, the voices generated using this soundfont will + /// be able to use signal processing effects. Currently this option + /// only affects the cutoff filter. + /// + /// Default: `true` pub use_effects: bool, + + /// The type of interpolator to use for the new soundfont. See the + /// documentation of the Interpolator enum for available options. + /// + /// Default: `Nearest` pub interpolator: Interpolator, } @@ -128,12 +159,69 @@ fn cents_factor(cents: f32) -> f32 { 2.0f32.powf(cents / 1200.0) } -pub struct SoundfontInstrument { +pub(super) struct SoundfontInstrument { bank: u8, preset: u8, spawner_params_list: Vec>>, } +/// Represents a sample soundfont to be used within XSynth. +/// +/// Supports SFZ and SF2 soundfonts. +/// +/// ## SFZ specification support (opcodes) +/// - `lovel` & `hivel` +/// - `lokey` & `hikey` +/// - `pitch_keycenter` +/// - `volume` +/// - `pan` +/// - `sample` +/// - `default_path` +/// - `loop_mode` +/// - `loop_start` +/// - `loop_end` +/// - `offset` +/// - `cutoff` +/// - `resonance` +/// - `fil_veltrack` +/// - `fil_keycenter` +/// - `fil_keytrack` +/// - `filter_type` +/// - `tune` +/// - `ampeg_start` +/// - `ampeg_delay` +/// - `ampeg_attack` +/// - `ampeg_hold` +/// - `ampeg_decay` +/// - `ampeg_sustain` +/// - `ampeg_release` +/// +/// ## SF2 specification support +/// ### Generators +/// - `startAddrsOffset` +/// - `startloopAddrsOffset` +/// - `endloopAddrsOffset` +/// - `initialFilterFc` +/// - `initialFilterQ` +/// - `pan` +/// - `delayVolEnv` +/// - `attackVolEnv` +/// - `holdVolEnv` +/// - `decayVolEnv` +/// - `sustainVolEnv` +/// - `releaseVolEnv` +/// - `instrument` +/// - `keyRange` +/// - `velRange` +/// - `initialAttenuation` +/// - `coarseTune` +/// - `fineTune` +/// - `sampleID` +/// - `sampleModes` +/// - `overridingRootKey` +/// +/// ### Modulators +/// None pub struct SampleSoundfont { instruments: Vec, stream_params: AudioStreamParams, @@ -158,6 +246,7 @@ fn envelope_descriptor_from_region_params( } } +/// Errors that can be generated when loading an SFZ soundfont. #[derive(Debug, Error)] pub enum LoadSfzError { #[error("IO Error")] @@ -170,6 +259,8 @@ pub enum LoadSfzError { SfzParseError(#[from] SfzParseError), } +/// Errors that can be generated when loading a soundfont +/// of an unspecified format. #[derive(Debug, Error)] pub enum LoadSfError { #[error("Error loading the SFZ: {0}")] @@ -183,6 +274,13 @@ pub enum LoadSfError { } impl SampleSoundfont { + /// Loads a new sample soundfont of an unspecified type. + /// The type of the soundfont will be determined from the file extension. + /// + /// Parameters: + /// - `path`: The path of the soundfont + /// - `stream_params`: Parameters of the output audio + /// - `options`: The soundfont configuration pub fn new( path: impl Into, stream_params: AudioStreamParams, @@ -204,6 +302,12 @@ impl SampleSoundfont { } } + /// Loads a new SFZ soundfont + /// + /// Parameters: + /// - `path`: The path of the soundfont + /// - `stream_params`: Parameters of the output audio + /// - `options`: The soundfont configuration pub fn new_sfz( sfz_path: impl Into, stream_params: AudioStreamParams, @@ -354,6 +458,12 @@ impl SampleSoundfont { }) } + /// Loads a new SF2 soundfont + /// + /// Parameters: + /// - `path`: The path of the soundfont + /// - `stream_params`: Parameters of the output audio + /// - `options`: The soundfont configuration pub fn new_sf2( sf2_path: impl Into, stream_params: AudioStreamParams, diff --git a/core/src/voice.rs b/core/src/voice.rs index 29bc8f7..0655f11 100644 --- a/core/src/voice.rs +++ b/core/src/voice.rs @@ -1,48 +1,64 @@ mod envelopes; -pub use envelopes::*; +pub(crate) use envelopes::*; mod simd; -pub use simd::*; +pub(crate) use simd::*; mod simdvoice; -pub use simdvoice::*; +pub(crate) use simdvoice::*; mod base; -pub use base::*; +pub(crate) use base::*; mod squarewave; -pub use squarewave::*; +#[allow(unused_imports)] +pub(crate) use squarewave::*; mod channels; -pub use channels::*; +#[allow(unused_imports)] +pub(crate) use channels::*; mod constant; -pub use constant::*; +pub(crate) use constant::*; mod sampler; -pub use sampler::*; +pub(crate) use sampler::*; mod control; -pub use control::*; +pub(crate) use control::*; mod cutoff; -pub use cutoff::*; +pub(crate) use cutoff::*; +/// Options to modify the envelope of a voice #[derive(Copy, Clone)] pub struct EnvelopeControlData { + /// Controls the attack. Can take values from 0 to 128 + /// according to the MIDI CC spec. pub attack: Option, + + /// Controls the release. Can take values from 0 to 128 + /// according to the MIDI CC spec. pub release: Option, } +/// How a voice should be released #[derive(Copy, Clone, PartialEq)] pub enum ReleaseType { + /// Standard release. Uses the voice's envelope. Standard, + + /// Kills the voice with a fadeout of 1ms. Kill, } +/// Options to control the parameters of a voice. #[derive(Copy, Clone)] pub struct VoiceControlData { + /// Pitch multiplier pub voice_pitch_multiplier: f32, + + /// Envelope control pub envelope: EnvelopeControlData, } diff --git a/core/src/voice/envelopes.rs b/core/src/voice/envelopes.rs index d53658d..a47e3e6 100644 --- a/core/src/voice/envelopes.rs +++ b/core/src/voice/envelopes.rs @@ -215,6 +215,7 @@ pub struct EnvelopeDescriptor { } impl EnvelopeDescriptor { + #[allow(clippy::wrong_self_convention)] pub fn to_envelope_params( &self, samplerate: u32, @@ -310,7 +311,7 @@ impl EnvelopeParameters { }) } - pub fn get_stage_duration(&self, stage: EnvelopeStage) -> u32 { + pub fn get_stage_duration(&self, stage: EnvelopeStage) -> u32 { let stage_info = &self.parts[stage.as_usize()]; match stage_info { EnvelopePart::Lerp { @@ -322,7 +323,7 @@ impl EnvelopeParameters { } } - pub fn modify_stage_data(&mut self, part: usize, data: EnvelopePart) { + pub fn modify_stage_data(&mut self, part: usize, data: EnvelopePart) { self.parts[part] = data; } } @@ -441,9 +442,8 @@ impl SIMDVoiceEnvelope { } if let Some(attack) = envelope.attack { - let duration = - params.get_stage_duration::(EnvelopeStage::Attack) as f32 / sample_rate; - params.modify_stage_data::( + let duration = params.get_stage_duration(EnvelopeStage::Attack) as f32 / sample_rate; + params.modify_stage_data( 1, EnvelopePart::lerp( 1.0, @@ -452,9 +452,8 @@ impl SIMDVoiceEnvelope { ); } if let Some(release) = envelope.release { - let duration = - params.get_stage_duration::(EnvelopeStage::Release) as f32 / sample_rate; - params.modify_stage_data::( + let duration = params.get_stage_duration(EnvelopeStage::Release) as f32 / sample_rate; + params.modify_stage_data( 5, EnvelopePart::lerp_to_zero_curve( (calculate_curve(release, duration).max(0.02) * sample_rate) as u32, @@ -483,7 +482,7 @@ impl VoiceGeneratorBase for SIMDVoiceEnvelope { #[inline(always)] fn signal_release(&mut self, rel_type: ReleaseType) { if rel_type == ReleaseType::Kill { - self.params.modify_stage_data::( + self.params.modify_stage_data( 5, EnvelopePart::lerp(0.0, (0.001 * self.sample_rate) as u32), ); diff --git a/kdmapi/src/lib.rs b/kdmapi/src/lib.rs index 82cd7b9..88eafdf 100644 --- a/kdmapi/src/lib.rs +++ b/kdmapi/src/lib.rs @@ -13,7 +13,7 @@ use core::{ soundfont::{SampleSoundfont, SoundfontBase}, }; -use realtime::{config::XSynthRealtimeConfig, RealtimeEventSender, RealtimeSynth}; +use realtime::{RealtimeEventSender, RealtimeSynth, XSynthRealtimeConfig}; #[cfg(windows)] use winapi::{ diff --git a/realtime/src/config.rs b/realtime/src/config.rs index d6675e3..5d66cd0 100644 --- a/realtime/src/config.rs +++ b/realtime/src/config.rs @@ -1,12 +1,37 @@ use core::channel::ChannelInitOptions; use std::ops::RangeInclusive; +/// Options for initializing a new RealtimeSynth. pub struct XSynthRealtimeConfig { + /// Channel initialization options (same for all channels). pub channel_init_options: ChannelInitOptions, + + /// The length of the buffer reader in ms. + /// + /// Default: `10.0` pub render_window_ms: f64, + + /// nt of VoiceChannel objects to be created. + /// (Number of MIDI channels) + /// + /// Default: `16` pub channel_count: u32, + + /// A vector which specifies which of the created channels (indexes) will be used for drums. + /// For example in a conventional 16 MIDI channel setup where channel 10 is used for + /// drums, the vector would be set as \[9\] (counting from 0). + /// + /// Default: `[9]` pub drums_channels: Vec, + + /// Whether or not to use a threadpool to render voices. + /// + /// Default: `false` pub use_threadpool: bool, + + /// A range of velocities that will not be played. + /// + /// Default: `0..=0` pub ignore_range: RangeInclusive, } diff --git a/realtime/src/event_senders.rs b/realtime/src/event_senders.rs index a43012f..b1e2b2f 100644 --- a/realtime/src/event_senders.rs +++ b/realtime/src/event_senders.rs @@ -209,13 +209,14 @@ impl Clone for EventSender { } } +/// A helper object to send events to the realtime synthesizer. #[derive(Clone)] pub struct RealtimeEventSender { senders: Vec, } impl RealtimeEventSender { - pub fn new( + pub(super) fn new( senders: Vec>, max_nps: Arc, ignore_range: RangeInclusive, @@ -228,6 +229,9 @@ impl RealtimeEventSender { } } + /// Sends a SynthEvent to the realtime synthesizer. + /// + /// See the SynthEvent documentation for more information. pub fn send_event(&mut self, event: SynthEvent) { match event { SynthEvent::Channel(channel, event) => { @@ -246,10 +250,14 @@ impl RealtimeEventSender { } } + /// Sends a ChannelConfigEvent to the realtime synthesizer. + /// + /// See the ChannelConfigEvent documentation for more information. pub fn send_config(&mut self, event: ChannelConfigEvent) { self.send_event(SynthEvent::ChannelConfig(event)) } + /// Sends a MIDI event as raw bytes. pub fn send_event_u32(&mut self, event: u32) { let head = event & 0xFF; let channel = head & 0xF; @@ -308,6 +316,7 @@ impl RealtimeEventSender { } } + /// Resets all note and control change data of the realtime synthesizer. pub fn reset_synth(&mut self) { self.send_event(SynthEvent::AllChannels(ChannelAudioEvent::AllNotesKilled)); diff --git a/realtime/src/lib.rs b/realtime/src/lib.rs index 1fbedd7..57a6b5f 100644 --- a/realtime/src/lib.rs +++ b/realtime/src/lib.rs @@ -1,4 +1,6 @@ -pub mod config; +mod config; +pub use config::*; + mod util; pub use core::channel_group::SynthEvent; diff --git a/realtime/src/realtime_synth.rs b/realtime/src/realtime_synth.rs index 4ab7cf4..75550ae 100644 --- a/realtime/src/realtime_synth.rs +++ b/realtime/src/realtime_synth.rs @@ -14,16 +14,18 @@ use cpal::{ use crossbeam_channel::{bounded, unbounded}; use core::{ + buffered_renderer::{BufferedRenderer, BufferedRendererStatsReader}, channel::VoiceChannel, effects::VolumeLimiter, helpers::{prepapre_cache_vec, sum_simd}, - AudioPipe, AudioStreamParams, BufferedRenderer, BufferedRendererStatsReader, FunctionAudioPipe, + AudioPipe, AudioStreamParams, FunctionAudioPipe, }; use crate::{ config::XSynthRealtimeConfig, util::ReadWriteAtomicU64, RealtimeEventSender, SynthEvent, }; +/// Holds the statistics for an instance of RealtimeSynth. #[derive(Debug, Clone)] struct RealtimeSynthStats { voice_count: Arc, @@ -37,6 +39,7 @@ impl RealtimeSynthStats { } } +/// Reads the statistics of an instance of RealtimeSynth in a usable way. pub struct RealtimeSynthStatsReader { buffered_stats: BufferedRendererStatsReader, stats: RealtimeSynthStats, @@ -53,10 +56,14 @@ impl RealtimeSynthStatsReader { } } + /// Returns the active voice count of all the MIDI channels. pub fn voice_count(&self) -> u64 { self.stats.voice_count.load(Ordering::Relaxed) } + /// Returns the statistics of the buffered renderer used. + /// + /// See the BufferedRendererStatsReader documentation for more information. pub fn buffer(&self) -> &BufferedRendererStatsReader { &self.buffered_stats } @@ -70,6 +77,7 @@ struct RealtimeSynthThreadSharedData { event_senders: RealtimeEventSender, } +/// A realtime MIDI synthesizer using an audio device for output. pub struct RealtimeSynth { data: Option, join_handles: Vec>, @@ -80,6 +88,8 @@ pub struct RealtimeSynth { } impl RealtimeSynth { + /// Initializes a new realtime synthesizer using the default config and + /// the default audio output. pub fn open_with_all_defaults() -> Self { let host = cpal::default_host(); @@ -93,6 +103,8 @@ impl RealtimeSynth { RealtimeSynth::open(Default::default(), &device, stream_config) } + /// Initializes as new realtime synthesizer using a given config and + /// the default audio output. pub fn open_with_default_output(config: XSynthRealtimeConfig) -> Self { let host = cpal::default_host(); @@ -106,6 +118,10 @@ impl RealtimeSynth { RealtimeSynth::open(config, &device, stream_config) } + /// Initializes a new realtime synthesizer using a given config and a + /// specified audio output device. + /// + /// See the `cpal` crate for the `device` and `stream_config` parameters. pub fn open( config: XSynthRealtimeConfig, device: &Device, @@ -248,16 +264,27 @@ impl RealtimeSynth { } } + /// Sends a SynthEvent to the realtime synthesizer. + /// + /// See the SynthEvent documentation for more information. pub fn send_event(&mut self, event: SynthEvent) { let data = self.data.as_mut().unwrap(); data.event_senders.send_event(event); } + /// Returns the event sender of the realtime synthesizer. + /// + /// See the RealtimeEventSender documentation for more information + /// on how to use. pub fn get_senders(&self) -> RealtimeEventSender { let data = self.data.as_ref().unwrap(); data.event_senders.clone() } + /// Returns the statistics reader of the realtime synthesizer. + /// + /// See the RealtimeSynthStatsReader documentation for more information + /// on how to use. pub fn get_stats(&self) -> RealtimeSynthStatsReader { let data = self.data.as_ref().unwrap(); let buffered_stats = data.buffered_renderer.lock().unwrap().get_buffer_stats(); @@ -265,15 +292,18 @@ impl RealtimeSynth { RealtimeSynthStatsReader::new(self.stats.clone(), buffered_stats) } + /// Returns the parameters of the audio output device pub fn stream_params(&self) -> AudioStreamParams { self.stream_params } + /// Pauses the playback of the audio output device pub fn pause(&mut self) -> Result<(), PauseStreamError> { let data = self.data.as_mut().unwrap(); data.stream.pause() } + /// Resumes the playback of the audio output device pub fn resume(&mut self) -> Result<(), PlayStreamError> { let data = self.data.as_mut().unwrap(); data.stream.play() diff --git a/render/src/builder.rs b/render/src/builder.rs index 6be9b8f..99a5507 100644 --- a/render/src/builder.rs +++ b/render/src/builder.rs @@ -23,12 +23,18 @@ use midi_toolkit::{ }, }; +/// Statistics of an XSynthRender object. pub struct XSynthRenderStats { + /// The progress of the render in seconds. + /// For example, if two seconds of the MIDI are rendered the value will be `2.0`. pub progress: f64, + + /// The active voice count of the synthesizer. pub voice_count: u64, // pub render_time: f64, } +/// Errors that can be generated when rendering a MIDI. #[derive(Debug, Error)] pub enum XSynthRenderError { #[error("SF loading failed")] @@ -44,6 +50,9 @@ impl From for XSynthRenderError { } } +/// Helper struct to create an XSynthRender object and render a MIDI file. +/// +/// Initialize using the `xsynth_renderer` function. pub struct XSynthRenderBuilder<'a, StatsCallback: FnMut(XSynthRenderStats)> { config: XSynthRenderConfig, midi_path: &'a str, @@ -53,6 +62,7 @@ pub struct XSynthRenderBuilder<'a, StatsCallback: FnMut(XSynthRenderStats)> { stats_callback: StatsCallback, } +/// Initializes an XSynthRenderBuilder object. pub fn xsynth_renderer<'a>( midi_path: &'a str, out_path: &'a str, @@ -68,7 +78,6 @@ pub fn xsynth_renderer<'a>( } impl<'a, ProgressCallback: FnMut(XSynthRenderStats)> XSynthRenderBuilder<'a, ProgressCallback> { - // Config functions pub fn with_config(mut self, config: XSynthRenderConfig) -> Self { self.config = config; self @@ -116,6 +125,7 @@ impl<'a, ProgressCallback: FnMut(XSynthRenderStats)> XSynthRenderBuilder<'a, Pro self } + /// Sets a callback function to be used to update the render statistics. pub fn with_progress_callback( self, stats_callback: F, diff --git a/render/src/config.rs b/render/src/config.rs index d375a0d..edfd09c 100644 --- a/render/src/config.rs +++ b/render/src/config.rs @@ -1,20 +1,42 @@ use core::{channel::ChannelInitOptions, soundfont::SoundfontInitOptions}; +/// Supported audio formats of XSynthRender #[derive(PartialEq, Clone, Copy)] pub enum XSynthRenderAudioFormat { Wav, } +/// Options for initializing a new XSynthRender object. #[derive(Clone)] pub struct XSynthRenderConfig { + /// Channel initialization options (same for all channels). pub channel_init_options: ChannelInitOptions, + + /// Soundfont initialization options (same for all soundfonts). pub sf_init_options: SoundfontInitOptions, + + /// Amount of VoiceChannel objects to be created. + /// (Number of MIDI channels) pub channel_count: u32, + + /// A vector which specifies which of the created channels (indexes) will be used for drums. + /// For example in a conventional 16 MIDI channel setup where channel 10 is used for + /// drums, the vector would be set as \[9\] (counting from 0). pub drums_channels: Vec, + + /// Whether or not to use a threadpool to render voices. pub use_threadpool: bool, + + /// Whether or not to limit the output audio. pub use_limiter: bool, + + /// Audio output sample rate pub sample_rate: u32, + + /// Audio output audio channels pub audio_channels: u16, + + /// Audio output format pub audio_format: XSynthRenderAudioFormat, } diff --git a/render/src/lib.rs b/render/src/lib.rs index e75c270..4b20b69 100644 --- a/render/src/lib.rs +++ b/render/src/lib.rs @@ -1,10 +1,10 @@ -pub mod config; +mod config; pub use config::*; mod rendered; pub use rendered::*; -pub mod builder; +mod builder; pub use builder::*; mod writer; diff --git a/render/src/rendered.rs b/render/src/rendered.rs index aaf8d37..8a4fe92 100644 --- a/render/src/rendered.rs +++ b/render/src/rendered.rs @@ -13,6 +13,7 @@ struct BatchRenderElements { missed_samples: f64, } +/// Represents an XSynth MIDI synthesizer that renders a MIDI to a file. pub struct XSynthRender { config: XSynthRenderConfig, channel_group: ChannelGroup, @@ -23,6 +24,8 @@ pub struct XSynthRender { } impl XSynthRender { + /// Initializes a new XSynthRender object with the given configuration and + /// audio output path. pub fn new(config: XSynthRenderConfig, out_path: PathBuf) -> Self { let audio_params = AudioStreamParams::new(config.sample_rate, config.audio_channels.into()); let chgroup_config = ChannelGroupConfig { @@ -55,14 +58,20 @@ impl XSynthRender { } } + /// Returns the parameters of the output audio. pub fn get_params(&self) -> AudioStreamParams { self.audio_params } + /// Sends a SynthEvent to the XSynthRender object. + /// Please see the SynthEvent documentation for more information. pub fn send_event(&mut self, event: SynthEvent) { self.channel_group.send_event(event); } + /// Renders audio samples of the specified time to the audio output file. + /// + /// The time should be the delta time of the last sent events. pub fn render_batch(&mut self, event_time: f64) { if event_time > 10.0 { // If the time is too large, split it up @@ -95,6 +104,7 @@ impl XSynthRender { } } + /// Finishes the render and finalizes the audio file. pub fn finalize(mut self) { loop { self.render_elements @@ -118,6 +128,7 @@ impl XSynthRender { self.audio_writer.finalize(); } + /// Returns the active voice count of the MIDI synthesizer. pub fn voice_count(&self) -> u64 { self.channel_group.voice_count() } diff --git a/soundfonts/src/lib.rs b/soundfonts/src/lib.rs index f934a38..c44733d 100644 --- a/soundfonts/src/lib.rs +++ b/soundfonts/src/lib.rs @@ -2,24 +2,43 @@ pub mod resample; pub mod sf2; pub mod sfz; +/// Type of the audio filter used. #[derive(Debug, PartialEq, Clone, Copy, Default)] pub enum FilterType { + /// First order low pass filter LowPassPole, + + /// Second order low pass filter #[default] LowPass, + + /// Second order high pass filter HighPass, + + /// Second order band pass filter BandPass, } +/// Type of looping for a sample. #[derive(Debug, PartialEq, Clone, Copy, Default)] pub enum LoopMode { + /// Do not loop the sample #[default] NoLoop, + + /// Play once from start to finish ignoring note off and envelope OneShot, + + /// Play from start and loop the specified region continuously LoopContinuous, + + /// Play from start, loop the specified region continuously and + /// play the rest of the sample when released LoopSustain, } +/// Converts the sample index of an audio sample array when +/// it is resampled. pub fn convert_sample_index(idx: u32, old_sample_rate: u32, new_sample_rate: u32) -> u32 { (new_sample_rate as f32 * idx as f32 / old_sample_rate as f32).round() as u32 } diff --git a/soundfonts/src/resample.rs b/soundfonts/src/resample.rs index c268bfe..1a12089 100644 --- a/soundfonts/src/resample.rs +++ b/soundfonts/src/resample.rs @@ -3,6 +3,7 @@ use rubato::{ }; use std::sync::Arc; +/// Resample multiple audio sample vectors pub fn resample_vecs( vecs: Vec>, sample_rate: f32, @@ -13,6 +14,7 @@ pub fn resample_vecs( .collect() } +/// Resample a single audio sample vector pub fn resample_vec(vec: Vec, sample_rate: f32, new_sample_rate: f32) -> Arc<[f32]> { let params = SincInterpolationParameters { sinc_len: 32, diff --git a/soundfonts/src/sf2/mod.rs b/soundfonts/src/sf2/mod.rs index 94e4ede..394c683 100644 --- a/soundfonts/src/sf2/mod.rs +++ b/soundfonts/src/sf2/mod.rs @@ -8,6 +8,7 @@ mod preset; mod sample; mod zone; +/// Errors that can be generated when loading an SF2 file. #[derive(Error, Debug, Clone)] pub enum Sf2ParseError { #[error("Failed to read file: {0}")] @@ -17,6 +18,7 @@ pub enum Sf2ParseError { FailedToParseFile, } +/// Structure that holds the generator and modulator parameters of an SF2 region. #[derive(Clone)] pub struct Sf2Region { pub sample: Arc<[Arc<[f32]>]>, @@ -37,12 +39,14 @@ pub struct Sf2Region { pub coarse_tune: i16, } +/// Structure that holds the parameters of an SF2 preset. pub struct Sf2Preset { pub bank: u16, pub preset: u16, pub regions: Vec, } +/// Parses an SF2 file and returns its presets in a vector. pub fn load_soundfont( sf2_path: impl Into, sample_rate: u32, diff --git a/soundfonts/src/sfz/mod.rs b/soundfonts/src/sfz/mod.rs index dcf3795..e525dad 100644 --- a/soundfonts/src/sfz/mod.rs +++ b/soundfonts/src/sfz/mod.rs @@ -4,15 +4,15 @@ use std::{ path::{Path, PathBuf}, }; -use self::parse::{ - parse_tokens_resolved, SfzAmpegEnvelope, SfzGroupType, SfzOpcode, SfzParseError, SfzToken, -}; +use self::parse::{parse_tokens_resolved, SfzAmpegEnvelope, SfzGroupType, SfzOpcode, SfzToken}; use crate::{FilterType, LoopMode}; -pub mod grammar; -pub mod parse; +mod grammar; +mod parse; +pub use parse::{SfzParseError, SfzValidationError}; +/// Structure that holds the opcode parameters of the SFZ's AmpEG envelope. #[derive(Debug, Clone)] pub struct AmpegEnvelopeParams { pub ampeg_start: f32, @@ -53,7 +53,7 @@ impl AmpegEnvelopeParams { } #[derive(Debug, Clone)] -pub struct RegionParamsBuilder { +pub(crate) struct RegionParamsBuilder { lovel: u8, hivel: u8, lokey: i8, @@ -173,6 +173,7 @@ impl RegionParamsBuilder { } } +/// Structure that holds the opcode parameters of the SFZ file. #[derive(Debug, Clone)] pub struct RegionParams { pub velrange: RangeInclusive, @@ -263,6 +264,7 @@ fn parse_sf_root(tokens: impl Iterator, base_path: PathBuf) -> regions } +/// Parses an SFZ file and returns its regions in a vector. pub fn parse_soundfont(sfz_path: impl Into) -> Result, SfzParseError> { let sfz_path = sfz_path.into(); let sfz_path: PathBuf = sfz_path diff --git a/soundfonts/src/sfz/parse.rs b/soundfonts/src/sfz/parse.rs index dcbefab..1be930b 100644 --- a/soundfonts/src/sfz/parse.rs +++ b/soundfonts/src/sfz/parse.rs @@ -78,6 +78,7 @@ pub enum SfzTokenWithMeta { Define(String, String), } +/// Parameters of an error generated while validating an SFZ file. #[derive(Error, Debug, Clone)] pub struct SfzValidationError { pub pos: FileLocation, @@ -85,7 +86,8 @@ pub struct SfzValidationError { } impl SfzValidationError { - pub fn new(pos: FileLocation, message: String) -> Self { + #[allow(dead_code)] + pub(super) fn new(pos: FileLocation, message: String) -> Self { Self { pos, message } } } @@ -96,6 +98,7 @@ impl std::fmt::Display for SfzValidationError { } } +/// Errors that can be generated when parsing an SFZ file. #[derive(Error, Debug, Clone)] pub enum SfzParseError { #[error("Failed to parse SFZ file: {0}")]