split cli, cli utilities
This commit is contained in:
parent
1074adb9e7
commit
c0e3478ae0
3 changed files with 453 additions and 413 deletions
|
@ -20,6 +20,7 @@ derive_builder = "0.20"
|
||||||
derive_wrapper = "0.1"
|
derive_wrapper = "0.1"
|
||||||
fasteval = "0.2"
|
fasteval = "0.2"
|
||||||
flacenc = { version = "0.4", optional = true }
|
flacenc = { version = "0.4", optional = true }
|
||||||
|
getset = "0.1.5"
|
||||||
hound = { version = "3.5", optional = true }
|
hound = { version = "3.5", optional = true }
|
||||||
lazy_static = "1.5"
|
lazy_static = "1.5"
|
||||||
mp3lame-encoder = { version = "0.2", features = ["std"], optional = true }
|
mp3lame-encoder = { version = "0.2", features = ["std"], optional = true }
|
||||||
|
|
445
src/cli/cli.rs
Normal file
445
src/cli/cli.rs
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt::Debug,
|
||||||
|
fs::File,
|
||||||
|
io::{self, Cursor, Read, stdin},
|
||||||
|
ops::Not,
|
||||||
|
str::{Bytes, FromStr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use bliplib::compiler::Expression;
|
||||||
|
use clap::Parser;
|
||||||
|
use derive_wrapper::AsRef;
|
||||||
|
use getset::Getters;
|
||||||
|
use hound::SampleFormat;
|
||||||
|
use mp3lame_encoder::{Bitrate, Quality};
|
||||||
|
use nom::{
|
||||||
|
Finish, Parser as P,
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::tag,
|
||||||
|
character::complete::{char, u16, usize},
|
||||||
|
combinator::{rest, success, value},
|
||||||
|
error::ErrorKind,
|
||||||
|
sequence::preceded,
|
||||||
|
};
|
||||||
|
use nom_locate::{LocatedSpan, position};
|
||||||
|
use strum::{EnumDiscriminants, EnumString, IntoDiscriminant, IntoStaticStr};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const DEFAULT_INSTRUMENT: &str = "sin(2*pi()*(442+442*((n+1)/N))*t)";
|
||||||
|
const DEFAULT_LENGTH: &str = "2^(2-log(2, l))*(60/T)";
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
#[command(version, author, about)]
|
||||||
|
pub(super) enum Cli {
|
||||||
|
/// Play a song
|
||||||
|
Play(PlayOpts),
|
||||||
|
/// Export a song to an audio file or stdout
|
||||||
|
Export(ExportOpts),
|
||||||
|
/// Memo menu for examples and general help about syntax and supported audio formats
|
||||||
|
#[command(subcommand)]
|
||||||
|
Memo(Memo),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Clone, Getters)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
#[getset(get)]
|
||||||
|
pub(super) struct PlayOpts {
|
||||||
|
/// Use this sheet music [default: stdin]
|
||||||
|
#[command(flatten)]
|
||||||
|
input: InputGroup,
|
||||||
|
/// Set available notes ("a,b,c" for example)
|
||||||
|
#[arg(short, long)]
|
||||||
|
notes: Vec<String>,
|
||||||
|
/// Set the signal expression (instrument) used to generate music samples
|
||||||
|
#[arg(short, long, default_value = DEFAULT_INSTRUMENT)]
|
||||||
|
instrument: Expression,
|
||||||
|
/// Set the expression used to generate note lengths in seconds
|
||||||
|
#[arg(short, long, default_value = DEFAULT_LENGTH)]
|
||||||
|
length: Expression,
|
||||||
|
/// Add a variable named VARIABLE (a single letter) and set its initial value to VALUE
|
||||||
|
#[arg(short, long = "variable", value_name = "VARIABLE=VALUE", value_parser = parse_key_val::<'=', Letter, f64>)]
|
||||||
|
#[getset(skip)]
|
||||||
|
variables: Vec<(Letter, f64)>,
|
||||||
|
/// Add a macro named NAME (single character, not alphanumeric) which expands to EXPANSION when called in sheet music
|
||||||
|
#[arg(short, long = "macro", value_name = "NAME:EXPANSION", value_parser = parse_key_val::<':', NotALetter, String>)]
|
||||||
|
#[getset(skip)]
|
||||||
|
macros: Vec<(NotALetter, String)>,
|
||||||
|
/// Add a slope expression named NAME which mutates the VARIABLE with the result of EXPR each frame
|
||||||
|
#[arg(short, long = "slope", value_name = "NAME:VARIABLE=EXPR", value_parser = parse_key_tuple::<LetterString, Letter, Expression>)]
|
||||||
|
#[getset(skip)]
|
||||||
|
slopes: Vec<(String, (LetterString, Expression))>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayOpts {
|
||||||
|
pub(super) fn variables(&self) -> impl Iterator<Item = (&char, &f64)> {
|
||||||
|
self.variables.iter().map(|(c, f)| (c.as_ref(), f))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn macros(&self) -> impl Iterator<Item = (&char, &String)> {
|
||||||
|
self.macros.iter().map(|(c, s)| (c.as_ref(), s))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn slopes(&self) -> impl Iterator<Item = (&String, (&String, &Expression))> {
|
||||||
|
self.slopes.iter().map(|(id, (s, e))| (id, (s.as_ref(), e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputGroup {
|
||||||
|
pub(super) fn get<'a>(&'a self) -> Box<dyn Read> {
|
||||||
|
self.input
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| Box::new(i.clone().0) as Box<dyn Read>)
|
||||||
|
.or(self
|
||||||
|
.sheet_music_string
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| Box::new(Cursor::new(s.clone())) as Box<dyn Read>))
|
||||||
|
.unwrap_or(Box::new(stdin()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, AsRef)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
struct Letter(char);
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum LetterError {
|
||||||
|
#[error("the character '{0}' should be a letter")]
|
||||||
|
Char(char),
|
||||||
|
#[error("no characters found")]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Letter {
|
||||||
|
type Err = LetterError;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let c = s.chars().next().ok_or(Self::Err::Empty)?;
|
||||||
|
c.is_alphabetic()
|
||||||
|
.then_some(c)
|
||||||
|
.map(Self)
|
||||||
|
.ok_or(Self::Err::Char(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, AsRef)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
struct NotALetter(char);
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum NotALetterError {
|
||||||
|
#[error("the character '{0}' should not be a letter")]
|
||||||
|
Char(char),
|
||||||
|
#[error("no characters found")]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NotALetter {
|
||||||
|
type Err = NotALetterError;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let c = s.chars().next().ok_or(Self::Err::Empty)?;
|
||||||
|
c.is_alphabetic()
|
||||||
|
.not()
|
||||||
|
.then_some(c)
|
||||||
|
.map(Self)
|
||||||
|
.ok_or(Self::Err::Char(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, AsRef)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
struct LetterString(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum LetterStringError {
|
||||||
|
#[error("the string \"{0}\" should be only letters")]
|
||||||
|
String(String),
|
||||||
|
#[error("no characters found")]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for LetterString {
|
||||||
|
type Err = LetterStringError;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
s.is_empty()
|
||||||
|
.not()
|
||||||
|
.then_some(
|
||||||
|
s.chars()
|
||||||
|
.all(|c| c.is_alphabetic())
|
||||||
|
.then_some(s)
|
||||||
|
.map(str::to_string)
|
||||||
|
.map(Self)
|
||||||
|
.ok_or(Self::Err::String(s.to_string())),
|
||||||
|
)
|
||||||
|
.ok_or(Self::Err::Empty)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a single key-value pair
|
||||||
|
///
|
||||||
|
/// From https://github.com/clap-rs/clap/blob/6b12a81bafe7b9d013b06981f520ab4c70da5510/examples/typed-derive.rs
|
||||||
|
fn parse_key_val<const SEP: char, T, U>(
|
||||||
|
s: &str,
|
||||||
|
) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
|
||||||
|
where
|
||||||
|
T: std::str::FromStr,
|
||||||
|
T::Err: Error + Send + Sync + 'static,
|
||||||
|
U: std::str::FromStr,
|
||||||
|
U::Err: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let pos = s
|
||||||
|
.find(SEP)
|
||||||
|
.ok_or_else(|| format!("invalid KEY{SEP}value: no `{SEP}` found in `{s}`"))?;
|
||||||
|
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_key_tuple<T, U1, U2>(
|
||||||
|
s: &str,
|
||||||
|
) -> Result<(T, (U1, U2)), Box<dyn Error + Send + Sync + 'static>>
|
||||||
|
where
|
||||||
|
T: std::str::FromStr,
|
||||||
|
T::Err: Error + Send + Sync + 'static,
|
||||||
|
U1: std::str::FromStr,
|
||||||
|
U1::Err: Error + Send + Sync + 'static,
|
||||||
|
U2: std::str::FromStr,
|
||||||
|
U2::Err: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let pos = s
|
||||||
|
.find(':')
|
||||||
|
.ok_or_else(|| format!("invalid KEY:value, no `:` found in `{s}`"))?;
|
||||||
|
Ok((
|
||||||
|
s[..pos].parse()?,
|
||||||
|
parse_key_val::<'=', _, _>(&s[pos + 1..])?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Clone)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
#[group(required = false, multiple = false)]
|
||||||
|
pub(super) struct InputGroup {
|
||||||
|
/// Set the path to your sheet music file [default: stdin]
|
||||||
|
input: Option<ClonableFile>,
|
||||||
|
/// Use this sheet music instead of reading from a file or stdin
|
||||||
|
#[arg(short = 'c')]
|
||||||
|
sheet_music_string: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ClonableFile(File);
|
||||||
|
|
||||||
|
impl Clone for ClonableFile {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.try_clone().expect("cloning file handle"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ClonableFile {
|
||||||
|
type Err = io::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
File::open(s).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Clone)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
pub(super) struct ExportOpts {
|
||||||
|
#[command(flatten)]
|
||||||
|
playopts: PlayOpts,
|
||||||
|
/// Audio format to use
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "mp3",
|
||||||
|
arg(default_value = "mp3 --bitrate 128 --quality best")
|
||||||
|
)]
|
||||||
|
#[cfg_attr(
|
||||||
|
not(feature = "mp3"),
|
||||||
|
cfg_attr(feature = "raw", arg(default_value = "raw mulaw"))
|
||||||
|
)]
|
||||||
|
#[arg(short, long, value_parser = audio_format_parser)]
|
||||||
|
format: AudioFormat,
|
||||||
|
/// Output file [default: stdout]
|
||||||
|
output: Option<ClonableFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, EnumDiscriminants)]
|
||||||
|
#[strum_discriminants(derive(EnumString, IntoStaticStr), strum(serialize_all = "lowercase"))]
|
||||||
|
enum AudioFormat {
|
||||||
|
Mp3 {
|
||||||
|
bitrate: Bitrate,
|
||||||
|
quality: Quality,
|
||||||
|
},
|
||||||
|
Wav {
|
||||||
|
bps: u16,
|
||||||
|
sample_format: SampleFormat,
|
||||||
|
},
|
||||||
|
Flac {
|
||||||
|
bps: usize,
|
||||||
|
},
|
||||||
|
Raw(RawAudioFormat),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
impl Debug for AudioFormat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Raw(r) => f.debug_tuple("Raw").field(r).finish(),
|
||||||
|
_ => self.discriminant().fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AudioFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
AudioFormat::Raw(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audio_format_parser(input: &str) -> Result<AudioFormat, anyhow::Error> {
|
||||||
|
fn mp3<'a>() -> impl P<
|
||||||
|
LocatedSpan<&'a str>,
|
||||||
|
Output = AudioFormat,
|
||||||
|
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
||||||
|
> {
|
||||||
|
preceded(
|
||||||
|
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
||||||
|
AudioFormatDiscriminants::Mp3.into(),
|
||||||
|
),
|
||||||
|
u16.or(success(320))
|
||||||
|
.and(
|
||||||
|
alt((
|
||||||
|
value(Quality::Best, tag("b")),
|
||||||
|
value(Quality::SecondBest, tag("sb")),
|
||||||
|
value(Quality::NearBest, tag("nb")),
|
||||||
|
value(Quality::VeryNice, tag("v")),
|
||||||
|
value(Quality::Nice, tag("n")),
|
||||||
|
value(Quality::Good, tag("g")),
|
||||||
|
value(Quality::Decent, tag("d")),
|
||||||
|
value(Quality::Ok, tag("o")),
|
||||||
|
value(Quality::SecondWorst, tag("sw")),
|
||||||
|
value(Quality::Worst, tag("w")),
|
||||||
|
))
|
||||||
|
.or(success(Quality::Good)),
|
||||||
|
)
|
||||||
|
.and(position)
|
||||||
|
.map_res(|((b, q), p)| {
|
||||||
|
Ok(AudioFormat::Mp3 {
|
||||||
|
bitrate: match b {
|
||||||
|
8 => Bitrate::Kbps8,
|
||||||
|
16 => Bitrate::Kbps16,
|
||||||
|
24 => Bitrate::Kbps24,
|
||||||
|
32 => Bitrate::Kbps32,
|
||||||
|
40 => Bitrate::Kbps40,
|
||||||
|
48 => Bitrate::Kbps48,
|
||||||
|
64 => Bitrate::Kbps64,
|
||||||
|
80 => Bitrate::Kbps80,
|
||||||
|
96 => Bitrate::Kbps96,
|
||||||
|
112 => Bitrate::Kbps112,
|
||||||
|
128 => Bitrate::Kbps128,
|
||||||
|
160 => Bitrate::Kbps160,
|
||||||
|
192 => Bitrate::Kbps192,
|
||||||
|
224 => Bitrate::Kbps224,
|
||||||
|
256 => Bitrate::Kbps256,
|
||||||
|
320 => Bitrate::Kbps320,
|
||||||
|
_ => return Err(nom::error::Error::new(p, ErrorKind::Verify)),
|
||||||
|
},
|
||||||
|
quality: q,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wav<'a>() -> impl P<
|
||||||
|
LocatedSpan<&'a str>,
|
||||||
|
Output = AudioFormat,
|
||||||
|
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
||||||
|
> {
|
||||||
|
preceded(
|
||||||
|
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
||||||
|
AudioFormatDiscriminants::Wav.into(),
|
||||||
|
),
|
||||||
|
u16.or(success(320))
|
||||||
|
.and(value(SampleFormat::Float, char('f')).or(success(SampleFormat::Int)))
|
||||||
|
.map(|(bps, sample_format)| AudioFormat::Wav { bps, sample_format }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn flac<'a>() -> impl P<
|
||||||
|
LocatedSpan<&'a str>,
|
||||||
|
Output = AudioFormat,
|
||||||
|
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
||||||
|
> {
|
||||||
|
preceded(
|
||||||
|
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
||||||
|
AudioFormatDiscriminants::Flac.into(),
|
||||||
|
),
|
||||||
|
usize
|
||||||
|
.or(success(320000))
|
||||||
|
.map(|bps| AudioFormat::Flac { bps }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn parser<'a>() -> impl P<
|
||||||
|
LocatedSpan<&'a str>,
|
||||||
|
Output = AudioFormat,
|
||||||
|
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
||||||
|
> {
|
||||||
|
alt((
|
||||||
|
mp3::<'a>(),
|
||||||
|
wav::<'a>(),
|
||||||
|
flac::<'a>(),
|
||||||
|
rest.map_res(|r: LocatedSpan<&'a str>| {
|
||||||
|
Ok::<AudioFormat, nom::error::Error<LocatedSpan<&'a str>>>(AudioFormat::Raw(
|
||||||
|
RawAudioFormat::try_from(*r)
|
||||||
|
.map_err(|_| nom::error::Error::new(r, ErrorKind::Verify))?,
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
parser()
|
||||||
|
.parse_complete(LocatedSpan::new(input))
|
||||||
|
.finish()
|
||||||
|
.map(|(_, o)| o)
|
||||||
|
.map_err(|e| anyhow!("{e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Clone, EnumString, Default)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
enum RawAudioFormat {
|
||||||
|
ALaw,
|
||||||
|
F32Be,
|
||||||
|
F32Le,
|
||||||
|
F64Be,
|
||||||
|
F64Le,
|
||||||
|
#[default]
|
||||||
|
MuLaw,
|
||||||
|
S8,
|
||||||
|
S16Be,
|
||||||
|
S16Le,
|
||||||
|
S24Be,
|
||||||
|
S24Le,
|
||||||
|
S32Be,
|
||||||
|
S32Le,
|
||||||
|
U8,
|
||||||
|
U16Be,
|
||||||
|
U16Le,
|
||||||
|
U24Be,
|
||||||
|
U24Le,
|
||||||
|
U32Be,
|
||||||
|
U32Le,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Clone)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
pub(super) enum Memo {
|
||||||
|
Syntax,
|
||||||
|
#[command(subcommand)]
|
||||||
|
Example(Example),
|
||||||
|
Formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Clone)]
|
||||||
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
pub(super) enum Example {
|
||||||
|
List,
|
||||||
|
N { id: u8 },
|
||||||
|
}
|
420
src/cli/main.rs
420
src/cli/main.rs
|
@ -1,419 +1,13 @@
|
||||||
#![allow(dead_code)]
|
mod cli;
|
||||||
|
|
||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
fmt::Debug,
|
|
||||||
fs::File,
|
|
||||||
io::{self},
|
|
||||||
ops::Not,
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use bliplib::compiler::Expression;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use derive_wrapper::AsRef;
|
use cli::Cli;
|
||||||
use hound::SampleFormat;
|
|
||||||
use mp3lame_encoder::{Bitrate, Quality};
|
|
||||||
use nom::{
|
|
||||||
Finish, Parser as P,
|
|
||||||
branch::alt,
|
|
||||||
bytes::complete::tag,
|
|
||||||
character::complete::{char, u16, usize},
|
|
||||||
combinator::{rest, success, value},
|
|
||||||
error::ErrorKind,
|
|
||||||
sequence::preceded,
|
|
||||||
};
|
|
||||||
use nom_locate::{LocatedSpan, position};
|
|
||||||
use strum::{EnumDiscriminants, EnumString, IntoDiscriminant, IntoStaticStr};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
const DEFAULT_INSTRUMENT: &str = "sin(2*pi()*(442+442*((n+1)/N))*t)";
|
|
||||||
const DEFAULT_LENGTH: &str = "2^(2-log(2, l))*(60/T)";
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
#[cfg(debug_assertions)]
|
use Cli::*;
|
||||||
dbg!(cli);
|
match cli {
|
||||||
}
|
Play(play_opts) => todo!(),
|
||||||
|
Export(export_opts) => todo!(),
|
||||||
#[derive(Parser)]
|
Memo(memo) => todo!(),
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
#[command(version, author, about)]
|
|
||||||
enum Cli {
|
|
||||||
/// Play a song
|
|
||||||
Play(PlayOpts),
|
|
||||||
/// Export a song to an audio file or stdout
|
|
||||||
Export(ExportOpts),
|
|
||||||
/// Memo menu for examples and general help about syntax and supported audio formats
|
|
||||||
#[command(subcommand)]
|
|
||||||
Memo(Memo),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
struct PlayOpts {
|
|
||||||
/// Use this sheet music [default: stdin]
|
|
||||||
#[command(flatten)]
|
|
||||||
input: InputGroup,
|
|
||||||
/// Set available notes ("a,b,c" for example)
|
|
||||||
#[arg(short, long)]
|
|
||||||
notes: Vec<String>,
|
|
||||||
/// Set the signal expression (instrument) used to generate music samples
|
|
||||||
#[arg(short, long, default_value = DEFAULT_INSTRUMENT)]
|
|
||||||
instrument: Expression,
|
|
||||||
/// Set the expression used to generate note lengths in seconds
|
|
||||||
#[arg(short, long, default_value = DEFAULT_LENGTH)]
|
|
||||||
length: Expression,
|
|
||||||
/// Add a variable named VARIABLE (a single letter) and set its initial value to VALUE
|
|
||||||
#[arg(short, long = "variable", value_name = "VARIABLE=VALUE", value_parser = parse_key_val::<'=', Letter, f64>)]
|
|
||||||
variables: Vec<(Letter, f64)>,
|
|
||||||
/// Add a macro named NAME (single character, not alphanumeric) which expands to EXPANSION when called in sheet music
|
|
||||||
#[arg(short, long = "macro", value_name = "NAME:EXPANSION", value_parser = parse_key_val::<':', NotALetter, String>)]
|
|
||||||
macros: Vec<(NotALetter, String)>,
|
|
||||||
/// Add a slope expression named NAME which mutates the VARIABLE with the result of EXPR each frame
|
|
||||||
#[arg(short, long = "slope", value_name = "NAME:VARIABLE=EXPR", value_parser = parse_key_tuple::<LetterString, Letter, Expression>)]
|
|
||||||
slopes: Vec<(String, (LetterString, Expression))>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, AsRef)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
struct Letter(char);
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
enum LetterError {
|
|
||||||
#[error("the character '{0}' should be a letter")]
|
|
||||||
Char(char),
|
|
||||||
#[error("no characters found")]
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Letter {
|
|
||||||
type Err = LetterError;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let c = s.chars().next().ok_or(Self::Err::Empty)?;
|
|
||||||
c.is_alphabetic()
|
|
||||||
.then_some(c)
|
|
||||||
.map(Self)
|
|
||||||
.ok_or(Self::Err::Char(c))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, AsRef)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
struct NotALetter(char);
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
enum NotALetterError {
|
|
||||||
#[error("the character '{0}' should not be a letter")]
|
|
||||||
Char(char),
|
|
||||||
#[error("no characters found")]
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for NotALetter {
|
|
||||||
type Err = NotALetterError;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let c = s.chars().next().ok_or(Self::Err::Empty)?;
|
|
||||||
c.is_alphabetic()
|
|
||||||
.not()
|
|
||||||
.then_some(c)
|
|
||||||
.map(Self)
|
|
||||||
.ok_or(Self::Err::Char(c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, AsRef)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
struct LetterString(String);
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
enum LetterStringError {
|
|
||||||
#[error("the string \"{0}\" should be only letters")]
|
|
||||||
String(String),
|
|
||||||
#[error("no characters found")]
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for LetterString {
|
|
||||||
type Err = LetterStringError;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
s.is_empty()
|
|
||||||
.not()
|
|
||||||
.then_some(
|
|
||||||
s.chars()
|
|
||||||
.all(|c| c.is_alphabetic())
|
|
||||||
.then_some(s)
|
|
||||||
.map(str::to_string)
|
|
||||||
.map(Self)
|
|
||||||
.ok_or(Self::Err::String(s.to_string())),
|
|
||||||
)
|
|
||||||
.ok_or(Self::Err::Empty)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a single key-value pair
|
|
||||||
///
|
|
||||||
/// From https://github.com/clap-rs/clap/blob/6b12a81bafe7b9d013b06981f520ab4c70da5510/examples/typed-derive.rs
|
|
||||||
fn parse_key_val<const SEP: char, T, U>(
|
|
||||||
s: &str,
|
|
||||||
) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
|
|
||||||
where
|
|
||||||
T: std::str::FromStr,
|
|
||||||
T::Err: Error + Send + Sync + 'static,
|
|
||||||
U: std::str::FromStr,
|
|
||||||
U::Err: Error + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let pos = s
|
|
||||||
.find(SEP)
|
|
||||||
.ok_or_else(|| format!("invalid KEY{SEP}value: no `{SEP}` found in `{s}`"))?;
|
|
||||||
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_key_tuple<T, U1, U2>(
|
|
||||||
s: &str,
|
|
||||||
) -> Result<(T, (U1, U2)), Box<dyn Error + Send + Sync + 'static>>
|
|
||||||
where
|
|
||||||
T: std::str::FromStr,
|
|
||||||
T::Err: Error + Send + Sync + 'static,
|
|
||||||
U1: std::str::FromStr,
|
|
||||||
U1::Err: Error + Send + Sync + 'static,
|
|
||||||
U2: std::str::FromStr,
|
|
||||||
U2::Err: Error + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let pos = s
|
|
||||||
.find(':')
|
|
||||||
.ok_or_else(|| format!("invalid KEY:value, no `:` found in `{s}`"))?;
|
|
||||||
Ok((
|
|
||||||
s[..pos].parse()?,
|
|
||||||
parse_key_val::<'=', _, _>(&s[pos + 1..])?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
#[group(required = false, multiple = false)]
|
|
||||||
struct InputGroup {
|
|
||||||
/// Set the path to your sheet music file [default: stdin]
|
|
||||||
input: Option<ClonableFile>,
|
|
||||||
/// Use this sheet music instead of reading from a file or stdin
|
|
||||||
#[arg(short = 'c')]
|
|
||||||
sheet_music_string: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ClonableFile(File);
|
|
||||||
|
|
||||||
impl Clone for ClonableFile {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.0.try_clone().expect("cloning file handle"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ClonableFile {
|
|
||||||
type Err = io::Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
File::open(s).map(Self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
struct ExportOpts {
|
|
||||||
#[command(flatten)]
|
|
||||||
playopts: PlayOpts,
|
|
||||||
/// Audio format to use
|
|
||||||
#[cfg_attr(
|
|
||||||
feature = "mp3",
|
|
||||||
arg(default_value = "mp3 --bitrate 128 --quality best")
|
|
||||||
)]
|
|
||||||
#[cfg_attr(
|
|
||||||
not(feature = "mp3"),
|
|
||||||
cfg_attr(feature = "raw", arg(default_value = "raw mulaw"))
|
|
||||||
)]
|
|
||||||
#[arg(short, long, value_parser = audio_format_parser)]
|
|
||||||
format: AudioFormat,
|
|
||||||
/// Output file [default: stdout]
|
|
||||||
output: Option<ClonableFile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, EnumDiscriminants)]
|
|
||||||
#[strum_discriminants(derive(EnumString, IntoStaticStr), strum(serialize_all = "lowercase"))]
|
|
||||||
enum AudioFormat {
|
|
||||||
Mp3 {
|
|
||||||
bitrate: Bitrate,
|
|
||||||
quality: Quality,
|
|
||||||
},
|
|
||||||
Wav {
|
|
||||||
bps: u16,
|
|
||||||
sample_format: SampleFormat,
|
|
||||||
},
|
|
||||||
Flac {
|
|
||||||
bps: usize,
|
|
||||||
},
|
|
||||||
Raw(RawAudioFormat),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
impl Debug for AudioFormat {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Raw(r) => f.debug_tuple("Raw").field(r).finish(),
|
|
||||||
_ => self.discriminant().fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AudioFormat {
|
|
||||||
fn default() -> Self {
|
|
||||||
AudioFormat::Raw(Default::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio_format_parser(input: &str) -> Result<AudioFormat, anyhow::Error> {
|
|
||||||
fn mp3<'a>() -> impl P<
|
|
||||||
LocatedSpan<&'a str>,
|
|
||||||
Output = AudioFormat,
|
|
||||||
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
|
||||||
> {
|
|
||||||
preceded(
|
|
||||||
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
|
||||||
AudioFormatDiscriminants::Mp3.into(),
|
|
||||||
),
|
|
||||||
u16.or(success(320))
|
|
||||||
.and(
|
|
||||||
alt((
|
|
||||||
value(Quality::Best, tag("b")),
|
|
||||||
value(Quality::SecondBest, tag("sb")),
|
|
||||||
value(Quality::NearBest, tag("nb")),
|
|
||||||
value(Quality::VeryNice, tag("v")),
|
|
||||||
value(Quality::Nice, tag("n")),
|
|
||||||
value(Quality::Good, tag("g")),
|
|
||||||
value(Quality::Decent, tag("d")),
|
|
||||||
value(Quality::Ok, tag("o")),
|
|
||||||
value(Quality::SecondWorst, tag("sw")),
|
|
||||||
value(Quality::Worst, tag("w")),
|
|
||||||
))
|
|
||||||
.or(success(Quality::Good)),
|
|
||||||
)
|
|
||||||
.and(position)
|
|
||||||
.map_res(|((b, q), p)| {
|
|
||||||
Ok(AudioFormat::Mp3 {
|
|
||||||
bitrate: match b {
|
|
||||||
8 => Bitrate::Kbps8,
|
|
||||||
16 => Bitrate::Kbps16,
|
|
||||||
24 => Bitrate::Kbps24,
|
|
||||||
32 => Bitrate::Kbps32,
|
|
||||||
40 => Bitrate::Kbps40,
|
|
||||||
48 => Bitrate::Kbps48,
|
|
||||||
64 => Bitrate::Kbps64,
|
|
||||||
80 => Bitrate::Kbps80,
|
|
||||||
96 => Bitrate::Kbps96,
|
|
||||||
112 => Bitrate::Kbps112,
|
|
||||||
128 => Bitrate::Kbps128,
|
|
||||||
160 => Bitrate::Kbps160,
|
|
||||||
192 => Bitrate::Kbps192,
|
|
||||||
224 => Bitrate::Kbps224,
|
|
||||||
256 => Bitrate::Kbps256,
|
|
||||||
320 => Bitrate::Kbps320,
|
|
||||||
_ => return Err(nom::error::Error::new(p, ErrorKind::Verify)),
|
|
||||||
},
|
|
||||||
quality: q,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn wav<'a>() -> impl P<
|
|
||||||
LocatedSpan<&'a str>,
|
|
||||||
Output = AudioFormat,
|
|
||||||
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
|
||||||
> {
|
|
||||||
preceded(
|
|
||||||
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
|
||||||
AudioFormatDiscriminants::Wav.into(),
|
|
||||||
),
|
|
||||||
u16.or(success(320))
|
|
||||||
.and(value(SampleFormat::Float, char('f')).or(success(SampleFormat::Int)))
|
|
||||||
.map(|(bps, sample_format)| AudioFormat::Wav { bps, sample_format }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn flac<'a>() -> impl P<
|
|
||||||
LocatedSpan<&'a str>,
|
|
||||||
Output = AudioFormat,
|
|
||||||
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
|
||||||
> {
|
|
||||||
preceded(
|
|
||||||
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
|
||||||
AudioFormatDiscriminants::Flac.into(),
|
|
||||||
),
|
|
||||||
usize
|
|
||||||
.or(success(320000))
|
|
||||||
.map(|bps| AudioFormat::Flac { bps }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn parser<'a>() -> impl P<
|
|
||||||
LocatedSpan<&'a str>,
|
|
||||||
Output = AudioFormat,
|
|
||||||
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
|
||||||
> {
|
|
||||||
alt((
|
|
||||||
mp3::<'a>(),
|
|
||||||
wav::<'a>(),
|
|
||||||
flac::<'a>(),
|
|
||||||
rest.map_res(|r: LocatedSpan<&'a str>| {
|
|
||||||
Ok::<AudioFormat, nom::error::Error<LocatedSpan<&'a str>>>(AudioFormat::Raw(
|
|
||||||
RawAudioFormat::try_from(*r)
|
|
||||||
.map_err(|_| nom::error::Error::new(r, ErrorKind::Verify))?,
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
parser()
|
|
||||||
.parse_complete(LocatedSpan::new(input))
|
|
||||||
.finish()
|
|
||||||
.map(|(_, o)| o)
|
|
||||||
.map_err(|e| anyhow!("{e:?}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Clone, EnumString, Default)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
#[strum(ascii_case_insensitive)]
|
|
||||||
enum RawAudioFormat {
|
|
||||||
ALaw,
|
|
||||||
F32Be,
|
|
||||||
F32Le,
|
|
||||||
F64Be,
|
|
||||||
F64Le,
|
|
||||||
#[default]
|
|
||||||
MuLaw,
|
|
||||||
S8,
|
|
||||||
S16Be,
|
|
||||||
S16Le,
|
|
||||||
S24Be,
|
|
||||||
S24Le,
|
|
||||||
S32Be,
|
|
||||||
S32Le,
|
|
||||||
U8,
|
|
||||||
U16Be,
|
|
||||||
U16Le,
|
|
||||||
U24Be,
|
|
||||||
U24Le,
|
|
||||||
U32Be,
|
|
||||||
U32Le,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
enum Memo {
|
|
||||||
Syntax,
|
|
||||||
#[command(subcommand)]
|
|
||||||
Example(Example),
|
|
||||||
Formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
|
||||||
enum Example {
|
|
||||||
List,
|
|
||||||
N { id: u8 },
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue