CLI export opts
This commit is contained in:
parent
42e52155e1
commit
0240602c19
2 changed files with 198 additions and 9 deletions
20
Cargo.toml
20
Cargo.toml
|
@ -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"]
|
||||
|
|
187
src/cli/main.rs
187
src/cli/main.rs
|
@ -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)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue