CLI export opts

This commit is contained in:
Breval Ferrari 2025-05-19 17:52:29 +02:00
parent 42e52155e1
commit 0240602c19
2 changed files with 198 additions and 9 deletions

View file

@ -13,24 +13,32 @@ name = "bliplib"
[dependencies]
anyhow = { version = "1.0.98", optional = true }
cfg-if = "1.0.0"
clap = { version = "4.5.38", features = ["derive"], optional = true }
derive-new = "0.7.0"
derive_builder = "0.20.2"
derive_wrapper = "0.1.7"
fasteval = "0.2.4"
flacenc = "0.4.0"
hound = "3.5.1"
flacenc = { version = "0.4.0", optional = true }
hound = { version = "3.5.1", optional = true }
lazy_static = "1.5.0"
mp3lame-encoder = { version = "0.2.1", features = ["std"] }
mp3lame-encoder = { version = "0.2.1", features = ["std"], optional = true }
nom = "8.0.0"
nom_locate = "5.0.0"
raw_audio = "0.0.1"
raw_audio = { version = "0.0.1", optional = true }
strum = { version = "0.27.1", features = ["derive"] }
thiserror = "2.0.12"
[features]
binary-build = ["anyhow", "clap"]
default = ["bin", "all-formats"]
bin = ["anyhow", "clap"]
all-formats = ["mp3", "wav", "flac", "raw"]
mp3 = ["mp3lame-encoder"]
wav = ["hound"]
flac = ["flacenc"]
raw = ["raw_audio"]
[[bin]]
name = "blip"
path = "src/cli/main.rs"
required-features = ["binary-build"]
required-features = ["bin"]

View file

@ -1,15 +1,31 @@
use std::{
error::Error,
fmt::Debug,
fs::File,
io::{self},
ops::Not,
str::FromStr,
};
use clap::Parser;
use anyhow::{Context, anyhow};
use bliplib::parser::TokenParser;
use clap::{Parser, builder::EnumValueParser};
use derive_new::new;
use derive_wrapper::AsRef;
use fasteval::{Compiler, Instruction, Slab};
use hound::SampleFormat;
use mp3lame_encoder::{Bitrate, Quality};
use nom::{
AsBytes, Compare, Finish, Input, Offset, Parser as _,
branch::alt,
bytes::complete::tag,
character::complete::{char, u16, usize},
combinator::{opt, rest, success, value},
error::{ErrorKind, ParseError},
sequence::preceded,
};
use nom_locate::{LocatedSpan, position};
use strum::{Display, EnumDiscriminants, EnumString, IntoDiscriminant, IntoStaticStr, ToString};
use thiserror::Error;
const DEFAULT_INSTRUMENT: &str = "sin(2*PI*(442+442*((n+1)/N))*t)";
@ -226,7 +242,172 @@ impl FromStr for ClonableFile {
#[derive(Parser, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
struct ExportOpts;
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<'a>(input: &'a str) -> Result<AudioFormat, anyhow::Error> {
fn mp3<'a>() -> impl TokenParser<&'a str, AudioFormat> {
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 TokenParser<&'a str, AudioFormat> {
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 TokenParser<&'a str, AudioFormat> {
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 TokenParser<&'a str, AudioFormat> {
alt((
mp3(),
wav(),
flac(),
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))]
@ -234,7 +415,7 @@ enum Memo {
Syntax,
#[command(subcommand)]
Example(Example),
Format,
Formats,
}
#[derive(Parser, Clone)]