diff --git a/Cargo.toml b/Cargo.toml index 2162327..8d3455f 100644 --- a/Cargo.toml +++ b/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"] diff --git a/src/cli/main.rs b/src/cli/main.rs index 2af8729..22a619b 100644 --- a/src/cli/main.rs +++ b/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, +} + +#[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 { + fn mp3<'a>() -> impl TokenParser<&'a str, AudioFormat> { + preceded( + tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error>>( + 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>>( + 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>>( + 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::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)]