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]
|
[dependencies]
|
||||||
anyhow = { version = "1.0.98", optional = true }
|
anyhow = { version = "1.0.98", optional = true }
|
||||||
|
cfg-if = "1.0.0"
|
||||||
clap = { version = "4.5.38", features = ["derive"], optional = true }
|
clap = { version = "4.5.38", features = ["derive"], optional = true }
|
||||||
derive-new = "0.7.0"
|
derive-new = "0.7.0"
|
||||||
derive_builder = "0.20.2"
|
derive_builder = "0.20.2"
|
||||||
derive_wrapper = "0.1.7"
|
derive_wrapper = "0.1.7"
|
||||||
fasteval = "0.2.4"
|
fasteval = "0.2.4"
|
||||||
flacenc = "0.4.0"
|
flacenc = { version = "0.4.0", optional = true }
|
||||||
hound = "3.5.1"
|
hound = { version = "3.5.1", optional = true }
|
||||||
lazy_static = "1.5.0"
|
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 = "8.0.0"
|
||||||
nom_locate = "5.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"
|
thiserror = "2.0.12"
|
||||||
|
|
||||||
[features]
|
[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]]
|
[[bin]]
|
||||||
name = "blip"
|
name = "blip"
|
||||||
path = "src/cli/main.rs"
|
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::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
|
fmt::Debug,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self},
|
io::{self},
|
||||||
ops::Not,
|
ops::Not,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use anyhow::{Context, anyhow};
|
||||||
|
use bliplib::parser::TokenParser;
|
||||||
|
use clap::{Parser, builder::EnumValueParser};
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use derive_wrapper::AsRef;
|
use derive_wrapper::AsRef;
|
||||||
use fasteval::{Compiler, Instruction, Slab};
|
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;
|
use thiserror::Error;
|
||||||
|
|
||||||
const DEFAULT_INSTRUMENT: &str = "sin(2*PI*(442+442*((n+1)/N))*t)";
|
const DEFAULT_INSTRUMENT: &str = "sin(2*PI*(442+442*((n+1)/N))*t)";
|
||||||
|
@ -226,7 +242,172 @@ impl FromStr for ClonableFile {
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
#[derive(Parser, Clone)]
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
#[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)]
|
#[derive(Parser, Clone)]
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
|
@ -234,7 +415,7 @@ enum Memo {
|
||||||
Syntax,
|
Syntax,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Example(Example),
|
Example(Example),
|
||||||
Format,
|
Formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
#[derive(Parser, Clone)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue