diff --git a/Cargo.lock b/Cargo.lock index 44fd9e1..fb4d52b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,12 +38,55 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "arrayvec" version = "0.7.2" @@ -241,28 +284,30 @@ dependencies = [ "bitflags 1.3.2", "clap_lex 0.2.4", "indexmap", - "strsim", + "strsim 0.10.0", "termcolor", "textwrap", ] [[package]] name = "clap" -version = "4.5.14" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c937d4061031a6d0c8da4b9a4f98a172fc2976dfb1c19213a9cf7d0d3c837e36" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.14" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85379ba512b21a328adf887e85f7742d12e96eb31f3ef077df4ffc26b506ffed" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ + "anstream", "anstyle", "clap_lex 0.7.2", + "strsim 0.11.1", ] [[package]] @@ -280,6 +325,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "combine" version = "4.6.6" @@ -354,7 +405,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.14", + "clap 4.5.16", "criterion-plot", "is-terminal", "itertools", @@ -583,6 +634,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -1289,6 +1346,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "symphonia" version = "0.5.4" @@ -1550,6 +1613,12 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.4" @@ -1987,11 +2056,11 @@ name = "xsynth-render" version = "0.2.0" dependencies = [ "atomic_float", + "clap 4.5.16", "crossbeam-channel", "hound", "midi-toolkit-rs", "rayon", - "serde", "spin_sleep", "thiserror", "xsynth-core", diff --git a/README.md b/README.md index 74e6835..919b6cc 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,11 @@ The real-time rendering module within XSynth. Currently it outputs audio using ` It uses an asynchronous event sending system for high performance and simple to use API. ### Rendered -A module for rendering audio to a file. -It takes in a MIDI file path and other XSynth parameters, and outputs an audio file. +A command line utility for rendering MIDIs to audio using XSynth. +It receives a MIDI file path and other parameters as arguments, and generates an audio file in WAV format. + +See available options using `cargo run -r -- --help` if you are compiling from source +or `xsynth-render --help` if you are using a pre-built binary. ### Soundfonts A module to parse different types of soundfonts to be used in XSynth. diff --git a/realtime/examples/midi2.rs b/realtime/examples/midi2.rs deleted file mode 100644 index c1a3aa0..0000000 --- a/realtime/examples/midi2.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::{ - sync::Arc, - thread, - time::{Duration, Instant}, -}; - -use xsynth_core::{ - channel::{ChannelAudioEvent, ChannelConfigEvent, ChannelEvent, ControlEvent}, - soundfont::{SampleSoundfont, SoundfontBase}, -}; - -use midi_toolkit::{ - events::{Event, MIDIEventEnum}, - io::MIDIFile, - pipe, - sequence::{ - event::{cancel_tempo_events, scale_event_time}, - unwrap_items, TimeCaster, - }, -}; -use xsynth_realtime::{RealtimeSynth, SynthEvent}; - -fn main() { - let args = std::env::args().collect::>(); - let (Some(midi), Some(sfz)) = ( - args.get(1) - .cloned() - .or_else(|| std::env::var("XSYNTH_EXAMPLE_MIDI").ok()), - args.get(2) - .cloned() - .or_else(|| std::env::var("XSYNTH_EXAMPLE_SF").ok()), - ) else { - println!( - "Usage: {} [midi] [sfz/sf2]", - std::env::current_exe() - .unwrap_or("example".into()) - .display() - ); - return; - }; - - let synth = RealtimeSynth::open_with_all_defaults(); - let mut sender = synth.get_senders(); - - let params = synth.stream_params(); - - let soundfonts: Vec> = vec![Arc::new( - SampleSoundfont::new(sfz, params, Default::default()).unwrap(), - )]; - - sender.send_event(SynthEvent::AllChannels(ChannelEvent::Config( - ChannelConfigEvent::SetSoundfonts(soundfonts), - ))); - - let midi = MIDIFile::open(&midi, None).unwrap(); - - let ppq = midi.ppq(); - let merged = pipe!( - midi.iter_all_events_merged() - |>TimeCaster::::cast_event_delta() - |>cancel_tempo_events(250000) - |>scale_event_time(1.0 / ppq as f64) - |>unwrap_items() - ); - - let collected = merged.collect::>(); - - let stats = synth.get_stats(); - thread::spawn(move || loop { - println!( - "Voice Count: {}\tBuffer: {}\tRender time: {}", - stats.voice_count(), - stats.buffer().last_samples_after_read(), - stats.buffer().average_renderer_load() - ); - thread::sleep(Duration::from_millis(10)); - }); - - let now = Instant::now(); - let mut time = 0.0; - for e in collected.into_iter() { - if e.delta != 0.0 { - time += e.delta; - let diff = time - now.elapsed().as_secs_f64(); - if diff > 0.0 { - spin_sleep::sleep(Duration::from_secs_f64(diff)); - } - } - - match e.as_event() { - Event::NoteOn(e) => { - sender.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::NoteOn { - key: e.key, - vel: e.velocity, - }), - )); - } - Event::NoteOff(e) => { - sender.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::NoteOff { key: e.key }), - )); - } - Event::ControlChange(e) => { - sender.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::Control(ControlEvent::Raw( - e.controller, - e.value, - ))), - )); - } - Event::PitchWheelChange(e) => { - sender.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::Control(ControlEvent::PitchBendValue( - e.pitch as f32 / 8192.0, - ))), - )); - } - Event::ProgramChange(e) => { - sender.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::ProgramChange(e.program)), - )); - } - _ => {} - } - } - - std::thread::sleep(Duration::from_secs(10000)); -} diff --git a/realtime/examples/test.rs b/realtime/examples/test.rs deleted file mode 100644 index d9c720c..0000000 --- a/realtime/examples/test.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{sync::Arc, time::Duration}; -use xsynth_core::{ - channel::{ChannelAudioEvent, ChannelConfigEvent, ChannelEvent}, - soundfont::{SampleSoundfont, SoundfontBase}, -}; - -use xsynth_realtime::{RealtimeSynth, SynthEvent}; - -fn main() { - let args = std::env::args().collect::>(); - let Some(sfz) = args - .get(1) - .cloned() - .or_else(|| std::env::var("XSYNTH_EXAMPLE_SFZ").ok()) - else { - println!( - "Usage: {} [sfz]", - std::env::current_exe() - .unwrap_or("example".into()) - .display() - ); - return; - }; - - let synth = RealtimeSynth::open_with_all_defaults(); - let mut sender = synth.get_senders(); - - let params = synth.stream_params(); - - let soundfonts: Vec> = vec![Arc::new( - SampleSoundfont::new(sfz, params, Default::default()).unwrap(), - )]; - - sender.send_event(SynthEvent::AllChannels(ChannelEvent::Config( - ChannelConfigEvent::SetSoundfonts(soundfonts), - ))); - - // for k in 0..127 { - // for c in 0..16 { - // for _ in 0..16 { - // synth.send_event(SynthEvent::Channel( - // c, - // ChannelEvent::NoteOn { key: k, vel: 5 }, - // )); - // } - // } - // } - sender.send_event(SynthEvent::Channel( - 0, - ChannelEvent::Audio(ChannelAudioEvent::NoteOn { key: 10, vel: 127 }), - )); - - std::thread::sleep(Duration::from_secs(10000)); -} diff --git a/realtime/examples/unload.rs b/realtime/examples/unload.rs deleted file mode 100644 index bcb6332..0000000 --- a/realtime/examples/unload.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use xsynth_core::{ - channel::{ChannelAudioEvent, ChannelConfigEvent, ChannelEvent}, - soundfont::{SampleSoundfont, SoundfontBase}, -}; - -use xsynth_realtime::{RealtimeSynth, SynthEvent}; - -fn main() { - let synth = RealtimeSynth::open_with_all_defaults(); - let mut sender = synth.get_senders(); - - let params = synth.stream_params(); - - let args = std::env::args().collect::>(); - let Some(sfz) = args - .get(1) - .cloned() - .or_else(|| std::env::var("XSYNTH_EXAMPLE_SF").ok()) - else { - println!( - "Usage: {} [sfz/sf2]", - std::env::current_exe() - .unwrap_or("example".into()) - .display() - ); - return; - }; - - println!("Loading Soundfont"); - let soundfonts: Vec> = vec![Arc::new( - SampleSoundfont::new(sfz, params, Default::default()).unwrap(), - )]; - println!("Loaded"); - - sender.send_event(SynthEvent::AllChannels(ChannelEvent::Config( - ChannelConfigEvent::SetSoundfonts(soundfonts), - ))); - - sender.send_event(SynthEvent::Channel( - 0, - ChannelEvent::Audio(ChannelAudioEvent::NoteOn { key: 64, vel: 127 }), - )); - - std::thread::sleep(Duration::from_secs(1)); - - println!("unloading"); - drop(sender); - drop(synth); - println!("unloaded"); - - std::thread::sleep(Duration::from_secs(10)); -} diff --git a/render/Cargo.toml b/render/Cargo.toml index 71e150f..130b3cc 100644 --- a/render/Cargo.toml +++ b/render/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xsynth-render" -description = "An XSynth module for rendering audio to a file." +description = "A command line utility for rendering MIDIs to audio using XSynth." authors.workspace = true version.workspace = true @@ -21,7 +21,4 @@ midi-toolkit-rs = "0.1.0" spin_sleep = "1.2.1" atomic_float = "1.0.0" thiserror = "1.0.63" -serde = { version = "1.0", optional = true, features = ["derive"] } - -[features] -serde = ["dep:serde", "xsynth-core/serde"] \ No newline at end of file +clap = { version = "4.5.16", features = ["cargo"] } \ No newline at end of file diff --git a/render/examples/render.rs b/render/examples/render.rs deleted file mode 100644 index 226f61f..0000000 --- a/render/examples/render.rs +++ /dev/null @@ -1,92 +0,0 @@ -use atomic_float::AtomicF64; -use std::{ - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, - thread, - time::{Duration, Instant}, -}; -use xsynth_core::{ - soundfont::{SampleSoundfont, SoundfontBase}, - AudioStreamParams, -}; -use xsynth_render::{ - xsynth_renderer, ChannelGroupConfig, XSynthRenderAudioFormat, XSynthRenderConfig, - XSynthRenderStats, -}; - -fn main() { - let args = std::env::args().collect::>(); - let (Some(midi), Some(sfz)) = ( - args.get(1) - .cloned() - .or_else(|| std::env::var("XSYNTH_EXAMPLE_MIDI").ok()), - args.get(2) - .cloned() - .or_else(|| std::env::var("XSYNTH_EXAMPLE_SF").ok()), - ) else { - println!( - "Usage: {} [midi] [sfz/sf2]", - std::env::current_exe() - .unwrap_or("example".into()) - .display() - ); - return; - }; - let out = "out.wav"; - - println!("\n--- STARTING RENDER ---"); - - let render_time = Instant::now(); - let position = Arc::new(AtomicF64::new(0.0)); - let voices = Arc::new(AtomicU64::new(0)); - - let max_voices = Arc::new(AtomicU64::new(0)); - - let callback = |stats: XSynthRenderStats| { - position.store(stats.progress, Ordering::Relaxed); - voices.store(stats.voice_count, Ordering::Relaxed); - if stats.voice_count > max_voices.load(Ordering::Relaxed) { - max_voices.store(stats.voice_count, Ordering::Relaxed); - } - }; - - let position_thread = position.clone(); - let voices_thread = voices.clone(); - - thread::spawn(move || loop { - thread::sleep(Duration::from_millis(100)); - let pos = position_thread.load(Ordering::Relaxed); - let time = Duration::from_secs_f64(pos); - println!( - "Progress: {:?}, Voice Count: {}", - time, - voices_thread.load(Ordering::Relaxed) - ); - }); - - let config = XSynthRenderConfig { - group_options: ChannelGroupConfig { - channel_init_options: Default::default(), - format: Default::default(), - audio_params: AudioStreamParams::new(48000, 2.into()), - parallelism: Default::default(), - }, - use_limiter: true, - audio_format: XSynthRenderAudioFormat::Wav, - }; - - let soundfonts: Vec> = vec![Arc::new( - SampleSoundfont::new(sfz, config.group_options.audio_params, Default::default()).unwrap(), - )]; - - xsynth_renderer(config, &midi, out) - .add_soundfonts(soundfonts) - .with_layer_count(Some(128)) - .with_progress_callback(callback) - .run() - .unwrap(); - - println!("Render time: {} seconds", render_time.elapsed().as_secs()); -} diff --git a/render/examples/render_dialog.rs b/render/examples/render_dialog.rs deleted file mode 100644 index fc49755..0000000 --- a/render/examples/render_dialog.rs +++ /dev/null @@ -1,170 +0,0 @@ -use xsynth_render::{ - xsynth_renderer, ChannelGroupConfig, ParallelismOptions, XSynthRenderAudioFormat, - XSynthRenderConfig, XSynthRenderStats, -}; - -use xsynth_core::{ - channel_group::ThreadCount, - soundfont::{SampleSoundfont, SoundfontBase}, - AudioStreamParams, -}; - -use midi_toolkit::{io::MIDIFile, sequence::event::get_channels_array_statistics}; - -use std::{ - io, - io::prelude::*, - io::Write, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, - thread, - time::Instant, -}; - -use atomic_float::AtomicF64; - -fn main() { - println!("--- FILE PATHS ---"); - let midi_path = read_input("Enter MIDI path"); - let sf_path = read_input("Enter SFZ/SF2 path"); - let out_path = read_input("Enter output path"); - - println!("\n--- RENDER OPTIONS ---"); - let sample_rate: u32 = read_input("Enter sample rate (in Hz)").parse().unwrap(); - let use_threadpool = read_input_bool("Use threadpool? (y/n)"); - let use_limiter = read_input_bool("Use audio limiter? (y/n)"); - let layers = match read_input("Enter layer count").parse::().unwrap() { - 0 => None, - voices => Some(voices), - }; - - io::stdout().lock().flush().unwrap(); - - println!("\n--- STARTING RENDER ---"); - - let render_time = Instant::now(); - let position = Arc::new(AtomicF64::new(0.0)); - let voices = Arc::new(AtomicU64::new(0)); - - let max_voices = Arc::new(AtomicU64::new(0)); - - let callback = |stats: XSynthRenderStats| { - position.store(stats.progress, Ordering::Relaxed); - voices.store(stats.voice_count, Ordering::Relaxed); - if stats.voice_count > max_voices.load(Ordering::Relaxed) { - max_voices.store(stats.voice_count, Ordering::Relaxed); - } - }; - - let position_thread = position.clone(); - let voices_thread = voices.clone(); - let length = get_midi_length(&midi_path); - - thread::spawn(move || loop { - let pos = position_thread.load(Ordering::Relaxed); - let progress = (pos / length) * 100.0 + 0.0004; - print!("\rProgress: ["); - let bars = progress as u8 / 5; - for _ in 0..bars { - print!("="); - } - for _ in 0..(20 - bars) { - print!(" "); - } - print!("] {progress:.3}% | "); - print!("Voice Count: {}", voices_thread.load(Ordering::Relaxed)); - if progress >= 100.0 { - break; - } - }); - - let config = XSynthRenderConfig { - group_options: ChannelGroupConfig { - channel_init_options: Default::default(), - format: Default::default(), - audio_params: AudioStreamParams::new(sample_rate, 2.into()), - parallelism: if use_threadpool { - Default::default() - } else { - ParallelismOptions { - channel: ThreadCount::None, - key: ThreadCount::None, - } - }, - }, - use_limiter: true, - audio_format: XSynthRenderAudioFormat::Wav, - }; - - let soundfonts: Vec> = vec![Arc::new( - SampleSoundfont::new( - sf_path, - config.group_options.audio_params, - Default::default(), - ) - .unwrap(), - )]; - - xsynth_renderer(config, &midi_path, &out_path) - .use_limiter(use_limiter) - .add_soundfonts(soundfonts) - .with_layer_count(layers) - .with_progress_callback(callback) - .run() - .unwrap(); - - println!( - "\n\n--- RENDER FINISHED ---\nRender time: {} seconds | Max Voice Count: {} voices", - render_time.elapsed().as_secs(), - max_voices.load(Ordering::Relaxed) - ); - pause(); -} - -fn read_input(prompt: &str) -> String { - let stdout = io::stdout(); - let reader = io::stdin(); - - let mut input = String::new(); - print!("{prompt}: "); - stdout.lock().flush().unwrap(); - reader.read_line(&mut input).unwrap(); - let string = input.trim(); - - string.to_string() -} - -fn read_input_bool(prompt: &str) -> bool { - let string = read_input(prompt); - match &string[..] { - "y" => true, - "n" => false, - _ => read_input_bool(prompt), - } -} - -fn get_midi_length(path: &str) -> f64 { - let midi = MIDIFile::open(path, None).unwrap(); - let parse_length_outer = Arc::new(AtomicF64::new(f64::NAN)); - let ppq = midi.ppq(); - let tracks = midi.iter_all_tracks().collect(); - let stats = get_channels_array_statistics(tracks); - if let Ok(stats) = stats { - parse_length_outer.store( - stats.calculate_total_duration(ppq).as_secs_f64(), - Ordering::Relaxed, - ); - } - - parse_length_outer.load(Ordering::Relaxed) -} - -fn pause() { - let mut stdin = io::stdin(); - let mut stdout = io::stdout(); - write!(stdout, "Press any key to continue...").unwrap(); - stdout.flush().unwrap(); - let _ = stdin.read(&mut [0u8]).unwrap(); -} diff --git a/render/src/builder.rs b/render/src/builder.rs deleted file mode 100644 index 6d8ee87..0000000 --- a/render/src/builder.rs +++ /dev/null @@ -1,237 +0,0 @@ -use crate::{ - config::{XSynthRenderAudioFormat, XSynthRenderConfig}, - XSynthRender, -}; - -use std::sync::Arc; - -use xsynth_core::{ - channel::{ChannelAudioEvent, ChannelConfigEvent, ChannelEvent, ControlEvent}, - channel_group::SynthEvent, - soundfont::{LoadSfError, SoundfontBase}, -}; - -use thiserror::Error; - -use midi_toolkit::{ - events::{Event, MIDIEventEnum}, - io::{MIDIFile, MIDILoadError}, - pipe, - sequence::{ - event::{cancel_tempo_events, scale_event_time}, - unwrap_items, TimeCaster, - }, -}; - -pub use xsynth_core::channel_group::{ParallelismOptions, SynthFormat}; - -/// 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")] - SfLoadingFailed(#[from] LoadSfError), - - #[error("MIDI loading failed")] - MidiLoadingFailed(MIDILoadError), -} - -impl From for XSynthRenderError { - fn from(e: MIDILoadError) -> Self { - XSynthRenderError::MidiLoadingFailed(e) - } -} - -/// 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, - soundfonts: Vec>, - layer_count: Option, - out_path: &'a str, - stats_callback: StatsCallback, -} - -/// 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, - midi_path, - soundfonts: vec![], - layer_count: Some(4), - out_path, - stats_callback: |_| {}, - } -} - -impl<'a, ProgressCallback: FnMut(XSynthRenderStats)> XSynthRenderBuilder<'a, ProgressCallback> { - pub fn with_config(mut self, config: XSynthRenderConfig) -> Self { - self.config = config; - self - } - - pub fn with_synth_format(mut self, format: SynthFormat) -> Self { - self.config.group_options.format = format; - self - } - - pub fn with_parallelism(mut self, options: ParallelismOptions) -> Self { - self.config.group_options.parallelism = options; - self - } - - pub fn use_limiter(mut self, use_limiter: bool) -> Self { - self.config.use_limiter = use_limiter; - self - } - - pub fn with_sample_rate(mut self, sample_rate: u32) -> Self { - self.config.group_options.audio_params.sample_rate = sample_rate; - self - } - - pub fn with_audio_channels(mut self, audio_channels: u16) -> Self { - self.config.group_options.audio_params.channels = audio_channels.into(); - self - } - - /// Unused because only WAV is supported - pub fn _with_audio_format(mut self, audio_format: XSynthRenderAudioFormat) -> Self { - self.config.audio_format = audio_format; - self - } - - pub fn with_layer_count(mut self, layers: Option) -> Self { - self.layer_count = layers; - self - } - - // Set up functions - pub fn add_soundfonts(mut self, soundfonts: Vec>) -> Self { - self.soundfonts.extend(soundfonts); - self - } - - /// Sets a callback function to be used to update the render statistics. - pub fn with_progress_callback( - self, - stats_callback: F, - ) -> XSynthRenderBuilder<'a, F> { - XSynthRenderBuilder { - config: self.config, - midi_path: self.midi_path, - soundfonts: self.soundfonts, - layer_count: self.layer_count, - out_path: self.out_path, - stats_callback, - } - } - - pub fn run(mut self) -> Result<(), XSynthRenderError> { - let mut synth = XSynthRender::new(self.config.clone(), self.out_path.into()); - - synth.send_event(SynthEvent::AllChannels(ChannelEvent::Config( - ChannelConfigEvent::SetSoundfonts( - self.soundfonts - .drain(..) - .collect::>>(), - ), - ))); - - synth.send_event(SynthEvent::AllChannels(ChannelEvent::Config( - ChannelConfigEvent::SetLayerCount(self.layer_count), - ))); - - let midi = MIDIFile::open(self.midi_path, None)?; - - let ppq = midi.ppq(); - let merged = pipe!( - midi.iter_all_track_events_merged_batches() - |>TimeCaster::::cast_event_delta() - |>cancel_tempo_events(250000) - |>scale_event_time(1.0 / ppq as f64) - |>unwrap_items() - ); - - let mut pos: f64 = 0.0; - - for batch in merged { - if batch.delta > 0.0 { - synth.render_batch(batch.delta); - pos += batch.delta; - } - for e in batch.iter_events() { - (self.stats_callback)(XSynthRenderStats { - progress: pos, - voice_count: synth.voice_count(), - }); - match e.as_event() { - Event::NoteOn(e) => { - synth.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::NoteOn { - key: e.key, - vel: e.velocity, - }), - )); - } - Event::NoteOff(e) => { - synth.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::NoteOff { key: e.key }), - )); - } - Event::ControlChange(e) => { - synth.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::Control(ControlEvent::Raw( - e.controller, - e.value, - ))), - )); - } - Event::PitchWheelChange(e) => { - synth.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::Control( - ControlEvent::PitchBendValue(e.pitch as f32 / 8192.0), - )), - )); - } - Event::ProgramChange(e) => { - synth.send_event(SynthEvent::Channel( - e.channel as u32, - ChannelEvent::Audio(ChannelAudioEvent::ProgramChange(e.program)), - )); - } - _ => {} - } - } - } - synth.send_event(SynthEvent::AllChannels(ChannelEvent::Audio( - ChannelAudioEvent::AllNotesOff, - ))); - synth.send_event(SynthEvent::AllChannels(ChannelEvent::Audio( - ChannelAudioEvent::ResetControl, - ))); - synth.finalize(); - - Ok(()) - } -} diff --git a/render/src/config.rs b/render/src/config.rs index e6f4a1c..5a16282 100644 --- a/render/src/config.rs +++ b/render/src/config.rs @@ -1,26 +1,192 @@ -pub use xsynth_core::{ - channel_group::ChannelGroupConfig, soundfont::SoundfontInitOptions, AudioStreamParams, +use crate::utils::*; +use clap::{command, Arg, ArgAction}; +use std::path::PathBuf; +use xsynth_core::{ + channel::ChannelInitOptions, + channel_group::{ChannelGroupConfig, ParallelismOptions, SynthFormat, ThreadCount}, + soundfont::{EnvelopeCurveType, EnvelopeOptions, Interpolator, SoundfontInitOptions}, + AudioStreamParams, ChannelCount, }; -/// Supported audio formats of XSynthRender. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum XSynthRenderAudioFormat { - Wav, -} - -/// Options for initializing a new XSynthRender object. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct XSynthRenderConfig { - /// 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. + pub sf_options: SoundfontInitOptions, + pub use_limiter: bool, +} + +#[derive(Clone, Debug)] +pub struct State { + pub config: XSynthRenderConfig, + pub layers: Option, + pub midi: PathBuf, + pub soundfonts: Vec, + pub output: PathBuf, +} + +impl State { + const THREADING_HELP: &'static str = + "Use \"none\" for no multithreading, \"auto\" for multithreading with\n\ + an automatically determined thread count or any number to specify the\n\ + amount of threads that should be used.\n\ + Default: \"auto\""; + + pub fn from_args() -> Self { + let matches = command!() + .args([ + Arg::new("midi") + .required(true) + .help("The path of the MIDI file to be converted."), + Arg::new("soundfonts") + .required(true) + .help( + "Paths of the soundfonts to be used.\n\ + Will be loaded in the order they are typed.", + ) + .action(ArgAction::Append), + Arg::new("output").short('o').long("output").help( + "The path of the output audio file.\n\ + Default: \"out.wav\"", + ), + Arg::new("sample rate") + .short('s') + .long("sample-rate") + .help( + "The sample rate of the output audio in Hz.\n\ + Default: 48000 (48kHz)", + ) + .value_parser(int_parser), + Arg::new("audio channels") + .short('c') + .long("audio-channels") + .help( + "The audio channel count of the output audio.\n\ + Supported: \"mono\" and \"stereo\"\n\ + Default: stereo", + ) + .value_parser(audio_channels_parser), + Arg::new("layer limit") + .short('l') + .long("layers") + .help( + "The layer limit for each channel. Use \"0\" for unlimited layers.\n\ + One layer is one voice per key per channel.\n\ + Default: 32", + ) + .value_parser(layers_parser), + Arg::new("channel threading") + .long("channel-threading") + .help("Per-channel multithreading options.\n".to_owned() + Self::THREADING_HELP) + .value_parser(threading_parser), + Arg::new("key threading") + .long("key-threading") + .help("Per-key multithreading options.\n".to_owned() + Self::THREADING_HELP) + .value_parser(threading_parser), + Arg::new("limiter") + .short('L') + .long("apply-limiter") + .help("Apply an audio limiter to the output audio to prevent clipping.") + .action(ArgAction::SetTrue), + Arg::new("disable fade out voice killing") + .long("disable-fade-out") + .help("Disables fade out when killing a voice. This may cause popping.") + .action(ArgAction::SetFalse), + Arg::new("linear envelope") + .long("linear-envelope") + .help("Use a linear decay and release phase in the volume envelope, in amplitude units.") + .action(ArgAction::SetTrue), + Arg::new("interpolation") + .short('I') + .long("interpolation") + .help( + "The interpolation algorithm to use. Available options are\n\ + \"none\" (no interpolation) and \"linear\" (linear interpolation).\n\ + Default: \"linear\"", + ) + .value_parser(interpolation_parser), + ]) + .get_matches(); + + let midi = matches + .get_one::("midi") + .cloned() + .unwrap_or_default(); + + let output = matches + .get_one::("output") + .cloned() + .unwrap_or("out.wav".to_owned()); + + let soundfonts = matches + .get_many::("soundfonts") + .unwrap_or_default() + .map(PathBuf::from) + .collect::>(); + + let config = XSynthRenderConfig { + group_options: ChannelGroupConfig { + channel_init_options: ChannelInitOptions { + fade_out_killing: matches + .get_one("disable fade out voice killing") + .copied() + .unwrap_or(true), + }, + format: SynthFormat::MidiSingle, + audio_params: AudioStreamParams::new( + matches.get_one("sample rate").copied().unwrap_or(48000), + matches + .get_one("audio channels") + .copied() + .unwrap_or(ChannelCount::Stereo), + ), + parallelism: ParallelismOptions { + channel: matches + .get_one("channel threading") + .copied() + .unwrap_or(ThreadCount::Auto), + key: matches + .get_one("key threading") + .copied() + .unwrap_or(ThreadCount::Auto), + }, + }, + sf_options: SoundfontInitOptions { + bank: None, + preset: None, + vol_envelope_options: if matches + .get_one("linear release") + .copied() + .unwrap_or_default() + { + EnvelopeOptions { + attack_curve: EnvelopeCurveType::Exponential, + decay_curve: EnvelopeCurveType::Exponential, + release_curve: EnvelopeCurveType::Exponential, + } + } else { + EnvelopeOptions { + attack_curve: EnvelopeCurveType::Exponential, + decay_curve: EnvelopeCurveType::Linear, + release_curve: EnvelopeCurveType::Linear, + } + }, + use_effects: true, + interpolator: matches + .get_one("interpolation") + .copied() + .unwrap_or(Interpolator::Linear), + }, + use_limiter: matches.get_one("limiter").copied().unwrap_or_default(), + }; - /// Audio output format. Supported: WAV - pub audio_format: XSynthRenderAudioFormat, + Self { + config, + layers: matches.get_one("layer limit").copied().unwrap_or(Some(32)), + midi: PathBuf::from(midi), + output: PathBuf::from(output), + soundfonts, + } + } } diff --git a/render/src/lib.rs b/render/src/lib.rs deleted file mode 100644 index 4b20b69..0000000 --- a/render/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod config; -pub use config::*; - -mod rendered; -pub use rendered::*; - -mod builder; -pub use builder::*; - -mod writer; diff --git a/render/src/main.rs b/render/src/main.rs new file mode 100644 index 0000000..5af3588 --- /dev/null +++ b/render/src/main.rs @@ -0,0 +1,161 @@ +mod config; +use config::*; + +mod rendered; +use rendered::*; + +mod utils; +use utils::get_midi_length; + +mod writer; + +use xsynth_core::{ + channel::{ChannelAudioEvent, ChannelConfigEvent, ChannelEvent, ControlEvent}, + channel_group::SynthEvent, + soundfont::{SampleSoundfont, SoundfontBase}, +}; + +use midi_toolkit::{ + events::{Event, MIDIEventEnum}, + io::MIDIFile, + pipe, + sequence::{ + event::{cancel_tempo_events, scale_event_time}, + unwrap_items, TimeCaster, + }, +}; + +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + thread, +}; + +use atomic_float::AtomicF64; + +fn main() { + let state = State::from_args(); + + let mut synth = XSynthRender::new(state.config.clone(), state.output.clone()); + + print!("Loading soundfonts..."); + synth.send_event(SynthEvent::AllChannels(ChannelEvent::Config( + ChannelConfigEvent::SetSoundfonts( + state + .soundfonts + .iter() + .map(|s| { + let sf: Arc = Arc::new( + SampleSoundfont::new(s, synth.get_params(), state.config.sf_options) + .unwrap(), + ); + sf + }) + .collect::>>(), + ), + ))); + + synth.send_event(SynthEvent::AllChannels(ChannelEvent::Config( + ChannelConfigEvent::SetLayerCount(state.layers), + ))); + + let length = get_midi_length(state.midi.to_str().unwrap()); + + let midi = MIDIFile::open(state.midi, None).unwrap(); + + let ppq = midi.ppq(); + let merged = pipe!( + midi.iter_all_track_events_merged_batches() + |>TimeCaster::::cast_event_delta() + |>cancel_tempo_events(250000) + |>scale_event_time(1.0 / ppq as f64) + |>unwrap_items() + ); + + let position = Arc::new(AtomicF64::new(0.0)); + let voices = Arc::new(AtomicU64::new(0)); + + { + let position = position.clone(); + let voices = voices.clone(); + + thread::spawn(move || loop { + let pos = position.load(Ordering::Relaxed); + let progress = (pos / length) * 100.0 + 0.0004; + print!("\rProgress: ["); + let bars = progress as u8 / 5; + for _ in 0..bars { + print!("="); + } + for _ in 0..(20 - bars) { + print!(" "); + } + print!("] {progress:.3}% | "); + print!("Voice Count: {}", voices.load(Ordering::Relaxed)); + if progress >= 100.0 { + println!(); + break; + } + }); + } + + for batch in merged { + if batch.delta > 0.0 { + synth.render_batch(batch.delta); + position.fetch_add(batch.delta, Ordering::Relaxed); + voices.store(synth.voice_count(), Ordering::Relaxed); + } + for e in batch.iter_events() { + match e.as_event() { + Event::NoteOn(e) => { + synth.send_event(SynthEvent::Channel( + e.channel as u32, + ChannelEvent::Audio(ChannelAudioEvent::NoteOn { + key: e.key, + vel: e.velocity, + }), + )); + } + Event::NoteOff(e) => { + synth.send_event(SynthEvent::Channel( + e.channel as u32, + ChannelEvent::Audio(ChannelAudioEvent::NoteOff { key: e.key }), + )); + } + Event::ControlChange(e) => { + synth.send_event(SynthEvent::Channel( + e.channel as u32, + ChannelEvent::Audio(ChannelAudioEvent::Control(ControlEvent::Raw( + e.controller, + e.value, + ))), + )); + } + Event::PitchWheelChange(e) => { + synth.send_event(SynthEvent::Channel( + e.channel as u32, + ChannelEvent::Audio(ChannelAudioEvent::Control( + ControlEvent::PitchBendValue(e.pitch as f32 / 8192.0), + )), + )); + } + Event::ProgramChange(e) => { + synth.send_event(SynthEvent::Channel( + e.channel as u32, + ChannelEvent::Audio(ChannelAudioEvent::ProgramChange(e.program)), + )); + } + _ => {} + } + } + } + synth.send_event(SynthEvent::AllChannels(ChannelEvent::Audio( + ChannelAudioEvent::AllNotesOff, + ))); + synth.send_event(SynthEvent::AllChannels(ChannelEvent::Audio( + ChannelAudioEvent::ResetControl, + ))); + synth.finalize(); +} diff --git a/render/src/utils.rs b/render/src/utils.rs new file mode 100644 index 0000000..58f2eef --- /dev/null +++ b/render/src/utils.rs @@ -0,0 +1,64 @@ +use atomic_float::AtomicF64; +use midi_toolkit::{io::MIDIFile, sequence::event::get_channels_array_statistics}; +use std::sync::{atomic::Ordering, Arc}; +use xsynth_core::{channel_group::ThreadCount, soundfont::Interpolator, ChannelCount}; + +#[inline(always)] +pub fn layers_parser(s: &str) -> Result, String> { + let l: usize = s.parse().map_err(|e| format!("{}", e))?; + match l { + 0 => Ok(None), + layers => Ok(Some(layers)), + } +} + +#[inline(always)] +pub fn threading_parser(s: &str) -> Result { + match s { + "none" => Ok(ThreadCount::None), + "auto" => Ok(ThreadCount::Auto), + n => { + let threads: usize = n.parse().map_err(|e| format!("{}", e))?; + Ok(ThreadCount::Manual(threads)) + } + } +} + +#[inline(always)] +pub fn audio_channels_parser(s: &str) -> Result { + match s { + "mono" => Ok(ChannelCount::Mono), + "stereo" => Ok(ChannelCount::Stereo), + _ => Err("Invalid channel count".to_string()), + } +} + +#[inline(always)] +pub fn int_parser(s: &str) -> Result { + s.parse().map_err(|e| format!("{}", e)) +} + +#[inline(always)] +pub fn interpolation_parser(s: &str) -> Result { + match s { + "none" => Ok(Interpolator::Nearest), + "linear" => Ok(Interpolator::Linear), + _ => Err("Invalid interpolation type".to_string()), + } +} + +pub fn get_midi_length(path: &str) -> f64 { + let midi = MIDIFile::open(path, None).unwrap(); + let parse_length_outer = Arc::new(AtomicF64::new(f64::NAN)); + let ppq = midi.ppq(); + let tracks = midi.iter_all_tracks().collect(); + let stats = get_channels_array_statistics(tracks); + if let Ok(stats) = stats { + parse_length_outer.store( + stats.calculate_total_duration(ppq).as_secs_f64(), + Ordering::Relaxed, + ); + } + + parse_length_outer.load(Ordering::Relaxed) +} diff --git a/render/src/writer.rs b/render/src/writer.rs index 78e9ef8..0b397fd 100644 --- a/render/src/writer.rs +++ b/render/src/writer.rs @@ -1,54 +1,33 @@ -use crate::config::{XSynthRenderAudioFormat, XSynthRenderConfig}; +use crate::config::XSynthRenderConfig; use std::{fs::File, io::BufWriter, path::PathBuf}; use hound::{WavSpec, WavWriter}; pub struct AudioFileWriter { - config: XSynthRenderConfig, - wav_writer: Option>>, + writer: WavWriter>, } impl AudioFileWriter { pub fn new(config: XSynthRenderConfig, path: PathBuf) -> Self { - match config.audio_format { - XSynthRenderAudioFormat::Wav => { - let spec = WavSpec { - 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, - }; - let writer = WavWriter::create(path, spec).unwrap(); + let spec = WavSpec { + 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, + }; + let writer = WavWriter::create(path, spec).unwrap(); - Self { - config, - wav_writer: Some(writer), - } - } - } + Self { writer } } pub fn write_samples(&mut self, samples: &mut Vec) { - match self.config.audio_format { - XSynthRenderAudioFormat::Wav => { - for s in samples.drain(0..) { - if let Some(writer) = &mut self.wav_writer { - writer.write_sample(s).unwrap(); - } - } - } + for s in samples.drain(0..) { + self.writer.write_sample(s).unwrap(); } } - pub fn finalize(mut self) { - match self.config.audio_format { - XSynthRenderAudioFormat::Wav => { - if let Some(writer) = self.wav_writer { - writer.finalize().unwrap(); - self.wav_writer = None; - } - } - } + pub fn finalize(self) { + self.writer.finalize().unwrap(); } }