diff --git a/.gitignore b/.gitignore index 18fc7b7..27c53dc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ env.sh env.bat shell.nix +.vscode diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 6279580..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "(Windows) Launch", - "type": "cppvsdbg", - "request": "launch", - "program": "${workspaceRoot}/target/debug/examples/midi.exe", - "args": [], - "stopAtEntry": false, - "cwd": "${workspaceRoot}", - "environment": [], - "console": "integratedTerminal", - "preLaunchTask": "${defaultBuildTask}", - "env": { "RUST_BACKTRACE": "1" } - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ef8dddc..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "editor.tabSize": 4, - "rust-analyzer.check.overrideCommand": [ - "cargo", - "clippy", - "--all", - "--message-format=json", - "--all-targets" - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 201fdd0..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "cargo", - "command": "build", - "args": ["--example", "midi"], - "problemMatcher": ["$rustc"], - "group": { - "kind": "build", - "isDefault": true - }, - "label": "rust: cargo build" - } - ] -} diff --git a/core/src/channel_group/config.rs b/core/src/channel_group/config.rs new file mode 100644 index 0000000..09adba3 --- /dev/null +++ b/core/src/channel_group/config.rs @@ -0,0 +1,87 @@ +use crate::{channel::ChannelInitOptions, AudioStreamParams}; + +/// Defines the multithreading options for each task that supports it. +#[derive(Clone)] +pub enum ThreadCount { + /// No multithreading. Run everything on the same thread. + None, + + /// Run with multithreading, with an automatically determined thread count. + /// Please read + /// [this](https://docs.rs/rayon-core/1.5.0/rayon_core/struct.ThreadPoolBuilder.html#method.num_threads) + /// for more information about the thread count selection. + Auto, + + /// Run with multithreading, with the specified thread count. + Manual(usize), +} + +/// Options regarding which parts of the ChannelGroup should be multithreaded. +/// +/// Responsibilities of a channel: processing input events for the channel, +/// dispatching per-key rendering of audio, applying filters to the final channel's audio +/// +/// Responsibilities of a key: Rendering per-voice audio for all the voices stored in a +/// key for a channel. This is generally the most compute intensive part of the synth. +/// +/// Best practices: +/// - As there are often 16 channels in MIDI, per-key multithreading can balance out the +/// load more evenly between CPU cores. +/// - However, per-key multithreading adds some overhead, so if the synth is invoked to +/// render very small sample counts each time (e.g. sub 1 millisecond), not using per-key +/// multithreading becomes more efficient. +#[derive(Clone)] +pub struct ParallelismOptions { + /// Render the MIDI channels parallel in a threadpool with the specified + /// thread count. + pub channel: ThreadCount, + + /// Render the individisual keys of each channel parallel in a threadpool + /// with the specified thread count. + pub key: ThreadCount, +} + +impl ParallelismOptions { + pub const AUTO_PER_KEY: Self = ParallelismOptions { + channel: ThreadCount::Auto, + key: ThreadCount::Auto, + }; + + pub const AUTO_PER_CHANNEL: Self = ParallelismOptions { + channel: ThreadCount::Auto, + key: ThreadCount::None, + }; +} + +impl Default for ParallelismOptions { + fn default() -> Self { + Self::AUTO_PER_KEY + } +} + +/// Options for initializing a new ChannelGroup. +#[derive(Clone)] +pub struct ChannelGroupConfig { + /// Channel initialization options (same for all channels). + /// See the `ChannelInitOptions` documentation for more information. + pub channel_init_options: ChannelInitOptions, + + /// Amount of VoiceChannel objects to be created + /// (Number of MIDI channels) + /// The MIDI 1 spec uses 16 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 vec!\[9\] (counting from 0). + pub drums_channels: Vec, + + /// Parameters of the output audio. + /// See the `AudioStreamParams` documentation for more information. + pub audio_params: AudioStreamParams, + + /// Options about the `ChannelGroup` instance's parallelism. See the `ParallelismOptions` + /// documentation for more information. + pub parallelism: ParallelismOptions, +} diff --git a/core/src/channel_group/mod.rs b/core/src/channel_group/mod.rs index b726c08..2eb5bac 100644 --- a/core/src/channel_group/mod.rs +++ b/core/src/channel_group/mod.rs @@ -1,11 +1,13 @@ use std::sync::Arc; use crate::{ - channel::{ChannelAudioEvent, ChannelEvent, ChannelInitOptions, VoiceChannel}, + channel::{ChannelAudioEvent, ChannelEvent, VoiceChannel}, helpers::sum_simd, AudioPipe, AudioStreamParams, }; +mod config; +pub use config::*; mod events; pub use events::*; use rayon::prelude::*; @@ -16,7 +18,7 @@ const MAX_EVENT_CACHE_SIZE: u32 = 1024 * 1024; /// /// Manages multiple VoiceChannel objects at once. pub struct ChannelGroup { - thread_pool: rayon::ThreadPool, + thread_pool: Option, cached_event_count: u32, channel_events_cache: Box<[Vec]>, sample_cache_vecs: Box<[Vec]>, @@ -24,33 +26,6 @@ pub struct ChannelGroup { audio_params: AudioStreamParams, } -/// Options for initializing a new ChannelGroup. -pub struct ChannelGroupConfig { - /// Channel initialization options (same for all channels). - /// See the `ChannelInitOptions` documentation for more information. - pub channel_init_options: ChannelInitOptions, - - /// Amount of VoiceChannel objects to be created - /// (Number of MIDI channels) - /// The MIDI 1 spec uses 16 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 vec!\[9\] (counting from 0). - pub drums_channels: Vec, - - /// Parameters of the output audio. - /// See the `AudioStreamParams` documentation for more information. - pub audio_params: AudioStreamParams, - - /// Whether or not to use a threadpool to render individual keys' voices. - /// Regardless, each MIDI channel uses its own thread. This setting - /// adds more fine-grained threading per key rather than per channel. - pub use_threadpool: bool, -} - impl ChannelGroup { /// Creates a new ChannelGroup with the given configuration. /// See the `ChannelGroupConfig` documentation for the available options. @@ -60,26 +35,44 @@ impl ChannelGroup { let mut sample_cache_vecs = Vec::new(); // Thread pool for individual channels to split between keys - let pool = if config.use_threadpool { - Some(Arc::new(rayon::ThreadPoolBuilder::new().build().unwrap())) - } else { - None + let channel_pool = match config.parallelism.channel { + ThreadCount::None => None, + ThreadCount::Auto => Some(Arc::new(rayon::ThreadPoolBuilder::new().build().unwrap())), + ThreadCount::Manual(threads) => Some(Arc::new( + rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build() + .unwrap(), + )), + }; + + // Thread pool for splitting channels between threads + let group_pool = match config.parallelism.key { + ThreadCount::None => None, + ThreadCount::Auto => Some(rayon::ThreadPoolBuilder::new().build().unwrap()), + ThreadCount::Manual(threads) => Some( + rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build() + .unwrap(), + ), }; for i in 0..config.channel_count { let mut init = config.channel_init_options; init.drums_only = config.drums_channels.clone().into_iter().any(|c| c == i); - channels.push(VoiceChannel::new(init, config.audio_params, pool.clone())); + channels.push(VoiceChannel::new( + init, + config.audio_params, + channel_pool.clone(), + )); channel_events_cache.push(Vec::new()); sample_cache_vecs.push(Vec::new()); } - // Thread pool for splitting channels between threads - let thread_pool = rayon::ThreadPoolBuilder::new().build().unwrap(); - Self { - thread_pool, + thread_pool: group_pool, cached_event_count: 0, channel_events_cache: channel_events_cache.into_boxed_slice(), channels: channels.into_boxed_slice(), @@ -121,44 +114,75 @@ impl ChannelGroup { return; } - let thread_pool = &mut self.thread_pool; - let channels = &mut self.channels; - let channel_events_cache = &mut self.channel_events_cache; - - thread_pool.install(move || { - channels - .par_iter_mut() - .zip(channel_events_cache.par_iter_mut()) - .for_each(|(channel, events)| { - channel.push_events_iter(events.drain(..).map(ChannelEvent::Audio)); + match self.thread_pool.as_ref() { + Some(pool) => { + let channels = &mut self.channels; + let channel_events_cache = &mut self.channel_events_cache; + + pool.install(move || { + channels + .par_iter_mut() + .zip(channel_events_cache.par_iter_mut()) + .for_each(|(channel, events)| { + channel.push_events_iter(events.drain(..).map(ChannelEvent::Audio)); + }); }); - }); + } + None => { + for (channel, events) in self + .channels + .iter_mut() + .zip(self.channel_events_cache.iter_mut()) + { + channel.push_events_iter(events.drain(..).map(ChannelEvent::Audio)); + } + } + } self.cached_event_count = 0; } fn render_to(&mut self, buffer: &mut [f32]) { self.flush_events(); - - let thread_pool = &mut self.thread_pool; - let channels = &mut self.channels; - let sample_cache_vecs = &mut self.sample_cache_vecs; - buffer.fill(0.0); - thread_pool.install(move || { - channels - .par_iter_mut() - .zip(sample_cache_vecs.par_iter_mut()) - .for_each(|(channel, samples)| { - samples.resize(buffer.len(), 0.0); - channel.read_samples(samples.as_mut_slice()); + + match self.thread_pool.as_ref() { + Some(pool) => { + let channels = &mut self.channels; + let sample_cache_vecs = &mut self.sample_cache_vecs; + pool.install(move || { + channels + .par_iter_mut() + .zip(sample_cache_vecs.par_iter_mut()) + .for_each(|(channel, samples)| { + samples.resize(buffer.len(), 0.0); + channel.read_samples(samples.as_mut_slice()); + }); + + for vec in sample_cache_vecs.iter_mut() { + sum_simd(vec, buffer); + vec.clear(); + } }); + } + None => { + let len = buffer.len(); + + for (channel, samples) in self + .channels + .iter_mut() + .zip(self.sample_cache_vecs.iter_mut()) + { + samples.resize(len, 0.0); + channel.read_samples(samples.as_mut_slice()); + } - for vec in sample_cache_vecs.iter_mut() { - sum_simd(vec, buffer); - vec.clear(); + for vec in self.sample_cache_vecs.iter_mut() { + sum_simd(vec, buffer); + vec.clear(); + } } - }); + } } /// Returns the active voice count of the synthesizer. diff --git a/kdmapi/src/lib.rs b/kdmapi/src/lib.rs index 6990555..03f3c05 100644 --- a/kdmapi/src/lib.rs +++ b/kdmapi/src/lib.rs @@ -60,7 +60,6 @@ pub extern "C" fn GetVoiceCount() -> u64 //This entire function is custom to xsy pub extern "C" fn InitializeKDMAPIStream() -> i32 { let config = XSynthRealtimeConfig { render_window_ms: 5.0, - use_threadpool: true, ..Default::default() }; diff --git a/realtime/src/config.rs b/realtime/src/config.rs index a0a9194..12c2b03 100644 --- a/realtime/src/config.rs +++ b/realtime/src/config.rs @@ -1,5 +1,5 @@ use std::ops::RangeInclusive; -pub use xsynth_core::channel::ChannelInitOptions; +pub use xsynth_core::{channel::ChannelInitOptions, channel_group::ThreadCount}; /// Options for initializing a new RealtimeSynth. pub struct XSynthRealtimeConfig { @@ -25,12 +25,12 @@ pub struct XSynthRealtimeConfig { /// Default: `[9]` pub drums_channels: Vec, - /// Whether or not to use a threadpool to render individual keys' voices. - /// Regardless, each MIDI channel uses its own thread. This setting - /// adds more fine-grained threading per key rather than per channel. + /// Controls the multithreading used for rendering per-voice audio for all + /// the voices stored in a key for a channel. See the `ThreadCount` documentation + /// for the available options. /// - /// Default: `false` - pub use_threadpool: bool, + /// Default: `ThreadCount::None` + pub multithreading: ThreadCount, /// A range of velocities that will not be played. /// @@ -45,7 +45,7 @@ impl Default for XSynthRealtimeConfig { render_window_ms: 10.0, channel_count: 16, drums_channels: vec![9], - use_threadpool: false, + multithreading: ThreadCount::None, ignore_range: 0..=0, } } diff --git a/realtime/src/realtime_synth.rs b/realtime/src/realtime_synth.rs index 2c435c5..a659e71 100644 --- a/realtime/src/realtime_synth.rs +++ b/realtime/src/realtime_synth.rs @@ -22,7 +22,7 @@ use xsynth_core::{ }; use crate::{ - config::XSynthRealtimeConfig, util::ReadWriteAtomicU64, RealtimeEventSender, SynthEvent, + util::ReadWriteAtomicU64, RealtimeEventSender, SynthEvent, ThreadCount, XSynthRealtimeConfig, }; /// Holds the statistics for an instance of RealtimeSynth. @@ -137,10 +137,15 @@ impl RealtimeSynth { let sample_rate = stream_config.sample_rate().0; let stream_params = AudioStreamParams::new(sample_rate, stream_config.channels().into()); - let pool = if config.use_threadpool { - Some(Arc::new(rayon::ThreadPoolBuilder::new().build().unwrap())) - } else { - None + let pool = match config.multithreading { + ThreadCount::None => None, + ThreadCount::Auto => Some(Arc::new(rayon::ThreadPoolBuilder::new().build().unwrap())), + ThreadCount::Manual(threads) => Some(Arc::new( + rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build() + .unwrap(), + )), }; let (output_sender, output_receiver) = bounded::>(config.channel_count as usize); diff --git a/render/src/builder.rs b/render/src/builder.rs index b60740c..de185cd 100644 --- a/render/src/builder.rs +++ b/render/src/builder.rs @@ -23,6 +23,8 @@ use midi_toolkit::{ }, }; +pub use xsynth_core::channel_group::ParallelismOptions; + /// Statistics of an XSynthRender object. pub struct XSynthRenderStats { /// The progress of the render in seconds. @@ -64,11 +66,12 @@ pub struct XSynthRenderBuilder<'a, StatsCallback: FnMut(XSynthRenderStats)> { /// Initializes an XSynthRenderBuilder object. pub fn xsynth_renderer<'a>( + config: XSynthRenderConfig, midi_path: &'a str, out_path: &'a str, ) -> XSynthRenderBuilder<'a, impl FnMut(XSynthRenderStats)> { XSynthRenderBuilder { - config: XSynthRenderConfig::default(), + config, midi_path, soundfonts: vec![], layer_count: Some(4), @@ -84,12 +87,12 @@ impl<'a, ProgressCallback: FnMut(XSynthRenderStats)> XSynthRenderBuilder<'a, Pro } pub fn with_channel_count(mut self, channels: u32) -> Self { - self.config.channel_count = channels; + self.config.group_options.channel_count = channels; self } - pub fn use_threadpool(mut self, use_threadpool: bool) -> Self { - self.config.use_threadpool = use_threadpool; + pub fn with_parallelism(mut self, options: ParallelismOptions) -> Self { + self.config.group_options.parallelism = options; self } @@ -99,12 +102,12 @@ impl<'a, ProgressCallback: FnMut(XSynthRenderStats)> XSynthRenderBuilder<'a, Pro } pub fn with_sample_rate(mut self, sample_rate: u32) -> Self { - self.config.sample_rate = sample_rate; + self.config.group_options.audio_params.sample_rate = sample_rate; self } pub fn with_audio_channels(mut self, audio_channels: u16) -> Self { - self.config.audio_channels = audio_channels; + self.config.group_options.audio_params.channels = audio_channels.into(); self } diff --git a/render/src/config.rs b/render/src/config.rs index 686c6b5..06c23ff 100644 --- a/render/src/config.rs +++ b/render/src/config.rs @@ -1,4 +1,6 @@ -pub use xsynth_core::{channel::ChannelInitOptions, soundfont::SoundfontInitOptions}; +pub use xsynth_core::{ + channel_group::ChannelGroupConfig, soundfont::SoundfontInitOptions, AudioStreamParams, +}; /// Supported audio formats of XSynthRender. #[derive(PartialEq, Clone, Copy)] @@ -9,68 +11,14 @@ pub enum XSynthRenderAudioFormat { /// Options for initializing a new XSynthRender object. #[derive(Clone)] pub struct XSynthRenderConfig { - /// Channel initialization options (same for all channels). - /// See the `ChannelInitOptions` documentation for more information. - pub channel_init_options: ChannelInitOptions, - - /// Soundfont initialization options (same for all soundfonts). - /// See the `SoundfontInitOptions` documentation for more information. - pub sf_init_options: SoundfontInitOptions, - - /// Amount 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 individual keys' voices. - /// Regardless, each MIDI channel uses its own thread. This setting - /// adds more fine-grained threading per key rather than per channel. - /// - /// Default: `true` - pub use_threadpool: bool, + /// Synthesizer initialization options. + /// See the `ChannelGroupConfig` documentation for more information. + pub group_options: ChannelGroupConfig, /// If set to true, the rendered audio will be limited to 0dB using /// the `VolumeLimiter` effect from `core` to prevent clipping. - /// - /// Default: `true` pub use_limiter: bool, - /// Audio output sample rate. - /// - /// Default: `48000` - pub sample_rate: u32, - - /// Audio output audio channels. - /// - /// Default: `2` - pub audio_channels: u16, - - /// Audio output format. - /// - /// Default: `Wav` + /// Audio output format. Supported: WAV pub audio_format: XSynthRenderAudioFormat, } - -impl Default for XSynthRenderConfig { - fn default() -> Self { - Self { - channel_init_options: Default::default(), - sf_init_options: Default::default(), - channel_count: 16, - drums_channels: vec![9], - use_threadpool: true, - use_limiter: true, - sample_rate: 48000, - audio_channels: 2, - audio_format: XSynthRenderAudioFormat::Wav, - } - } -} diff --git a/render/src/rendered.rs b/render/src/rendered.rs index 7158d5e..10d7348 100644 --- a/render/src/rendered.rs +++ b/render/src/rendered.rs @@ -1,5 +1,5 @@ use xsynth_core::{ - channel_group::{ChannelGroup, ChannelGroupConfig, SynthEvent}, + channel_group::{ChannelGroup, SynthEvent}, effects::VolumeLimiter, AudioPipe, AudioStreamParams, }; @@ -18,7 +18,6 @@ pub struct XSynthRender { config: XSynthRenderConfig, channel_group: ChannelGroup, audio_writer: AudioFileWriter, - audio_params: AudioStreamParams, limiter: Option, render_elements: BatchRenderElements, } @@ -27,20 +26,14 @@ 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 { - channel_init_options: config.channel_init_options, - channel_count: config.channel_count, - drums_channels: config.drums_channels.clone(), - audio_params, - use_threadpool: config.use_threadpool, - }; - let channel_group = ChannelGroup::new(chgroup_config); + let channel_group = ChannelGroup::new(config.group_options.clone()); let audio_writer = AudioFileWriter::new(config.clone(), out_path); let limiter = if config.use_limiter { - Some(VolumeLimiter::new(config.audio_channels)) + Some(VolumeLimiter::new( + config.group_options.audio_params.channels.count(), + )) } else { None }; @@ -49,7 +42,6 @@ impl XSynthRender { config, channel_group, audio_writer, - audio_params, limiter, render_elements: BatchRenderElements { output_vec: vec![0.0], @@ -60,7 +52,7 @@ impl XSynthRender { /// Returns the parameters of the output audio. pub fn get_params(&self) -> AudioStreamParams { - self.audio_params + self.config.group_options.audio_params } /// Sends a SynthEvent to the XSynthRender object. @@ -86,10 +78,11 @@ impl XSynthRender { } } } else { - let samples = - self.config.sample_rate as f64 * event_time + self.render_elements.missed_samples; + let samples = self.config.group_options.audio_params.sample_rate as f64 * event_time + + self.render_elements.missed_samples; self.render_elements.missed_samples = samples % 1.0; - let samples = samples as usize * self.config.audio_channels as usize; + let samples = + samples as usize * self.config.group_options.audio_params.channels.count() as usize; self.render_elements.output_vec.resize(samples, 0.0); self.channel_group @@ -107,9 +100,10 @@ impl XSynthRender { /// Finishes the render and finalizes the audio file. pub fn finalize(mut self) { loop { - self.render_elements - .output_vec - .resize(self.config.sample_rate as usize, 0.0); + self.render_elements.output_vec.resize( + self.config.group_options.audio_params.sample_rate as usize, + 0.0, + ); self.channel_group .read_samples(&mut self.render_elements.output_vec); let mut is_empty = true; diff --git a/render/src/writer.rs b/render/src/writer.rs index 4fe984b..78e9ef8 100644 --- a/render/src/writer.rs +++ b/render/src/writer.rs @@ -14,8 +14,8 @@ impl AudioFileWriter { match config.audio_format { XSynthRenderAudioFormat::Wav => { let spec = WavSpec { - channels: config.audio_channels, - sample_rate: config.sample_rate, + channels: config.group_options.audio_params.channels.count(), + sample_rate: config.group_options.audio_params.sample_rate, bits_per_sample: 32, sample_format: hound::SampleFormat::Float, };