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"
|
||||
fasteval = "0.2"
|
||||
flacenc = { version = "0.4", optional = true }
|
||||
getset = "0.1.5"
|
||||
hound = { version = "3.5", optional = true }
|
||||
lazy_static = "1.5"
|
||||
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)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::Debug,
|
||||
fs::File,
|
||||
io::{self},
|
||||
ops::Not,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bliplib::compiler::Expression;
|
||||
mod cli;
|
||||
use clap::Parser;
|
||||
use derive_wrapper::AsRef;
|
||||
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)";
|
||||
use cli::Cli;
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(cli);
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[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))
|
||||
use Cli::*;
|
||||
match cli {
|
||||
Play(play_opts) => todo!(),
|
||||
Export(export_opts) => todo!(),
|
||||
Memo(memo) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[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