xsynth-render 0.3.4

A command line utility for rendering MIDIs to audio using XSynth.
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,
};

#[derive(Clone, Debug, PartialEq)]
pub struct XSynthRenderConfig {
    pub group_options: ChannelGroupConfig,

    pub sf_options: SoundfontInitOptions,

    pub use_limiter: bool,
}

#[derive(Clone, Debug)]
pub struct State {
    pub config: XSynthRenderConfig,
    pub layers: Option<usize>,
    pub midi: PathBuf,
    pub soundfonts: Vec<PathBuf>,
    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::<String>("midi")
            .cloned()
            .unwrap_or_default();

        let output = matches
            .get_one::<String>("output")
            .cloned()
            .unwrap_or("out.wav".to_owned());

        let soundfonts = matches
            .get_many::<String>("soundfonts")
            .unwrap_or_default()
            .map(PathBuf::from)
            .collect::<Vec<_>>();

        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::Midi,
                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(),
        };

        Self {
            config,
            layers: matches.get_one("layer limit").copied().unwrap_or(Some(32)),
            midi: PathBuf::from(midi),
            output: PathBuf::from(output),
            soundfonts,
        }
    }
}