diff --git a/Cargo.toml b/Cargo.toml index 1001ccb..2162327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,26 @@ repository = "https://gitdab.com/breval/blip" [lib] name = "bliplib" -[[bin]] -name = "blip" -path = "src/cli/main.rs" - [dependencies] -anyhow = "1.0.98" -clap = { version = "4.5.38", features = ["derive"] } +anyhow = { version = "1.0.98", optional = true } +clap = { version = "4.5.38", features = ["derive"], optional = true } +derive-new = "0.7.0" derive_builder = "0.20.2" -fasteval = { git = "https://github.com/brevalferrari/fasteval" } +derive_wrapper = "0.1.7" +fasteval = "0.2.4" flacenc = "0.4.0" hound = "3.5.1" +lazy_static = "1.5.0" mp3lame-encoder = { version = "0.2.1", features = ["std"] } nom = "8.0.0" nom_locate = "5.0.0" raw_audio = "0.0.1" thiserror = "2.0.12" + +[features] +binary-build = ["anyhow", "clap"] + +[[bin]] +name = "blip" +path = "src/cli/main.rs" +required-features = ["binary-build"] diff --git a/src/cli/main.rs b/src/cli/main.rs index b5a5910..2af8729 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -1,12 +1,245 @@ +use std::{ + error::Error, + fs::File, + io::{self}, + ops::Not, + str::FromStr, +}; + use clap::Parser; +use derive_new::new; +use derive_wrapper::AsRef; +use fasteval::{Compiler, Instruction, Slab}; +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)"; fn main() { - let name = Cli::parse().name; - println!("Hello, {name}!"); + dbg!(Cli::parse()); } #[derive(Parser)] +#[cfg_attr(debug_assertions, derive(Debug))] #[command(version, author, about)] -struct Cli { - name: String, +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, + /// 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::)] + 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 { + 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 { + 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 { + 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( + s: &str, +) -> Result<(T, U), Box> +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( + s: &str, +) -> Result<(T, (U1, U2)), Box> +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(new)] +#[cfg_attr(debug_assertions, derive(Debug))] +struct Expression { + instruction: Instruction, + slab: Slab, +} + +impl Clone for Expression { + fn clone(&self) -> Self { + unimplemented!() + } +} + +impl FromStr for Expression { + type Err = fasteval::Error; + fn from_str(s: &str) -> Result { + let mut slab = Slab::new(); + let instruction = fasteval::Parser::new() + .parse(s, &mut slab.ps)? + .from(&slab.ps) + .compile(&slab.ps, &mut slab.cs); + Ok(Expression::new(instruction, slab)) + } +} + +#[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, + /// Use this sheet music instead of reading from a file or stdin + #[arg(short = 'c')] + sheet_music_string: Option, +} + +#[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 { + File::open(s).map(Self) + } +} + +#[derive(Parser, Clone)] +#[cfg_attr(debug_assertions, derive(Debug))] +struct ExportOpts; + +#[derive(Parser, Clone)] +#[cfg_attr(debug_assertions, derive(Debug))] +enum Memo { + Syntax, + #[command(subcommand)] + Example(Example), + Format, +} + +#[derive(Parser, Clone)] +#[cfg_attr(debug_assertions, derive(Debug))] +enum Example { + List, + N { id: u8 }, } diff --git a/src/compiler.rs b/src/compiler.rs index 046f42e..7929a26 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -21,7 +21,6 @@ pub struct Marker; pub struct Note(pub u8); #[cfg_attr(debug_assertions, derive(Default))] -#[derive(Clone)] pub struct VariableChange(pub char, pub Instruction); #[cfg_attr(debug_assertions, derive(Default))]