cli: play

This commit is contained in:
Breval Ferrari 2025-05-16 15:25:18 +02:00
parent 283c6a7dd6
commit 42e52155e1
3 changed files with 251 additions and 12 deletions

View file

@ -11,19 +11,26 @@ repository = "https://gitdab.com/breval/blip"
[lib] [lib]
name = "bliplib" name = "bliplib"
[[bin]]
name = "blip"
path = "src/cli/main.rs"
[dependencies] [dependencies]
anyhow = "1.0.98" anyhow = { version = "1.0.98", optional = true }
clap = { version = "4.5.38", features = ["derive"] } clap = { version = "4.5.38", features = ["derive"], optional = true }
derive-new = "0.7.0"
derive_builder = "0.20.2" 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" flacenc = "0.4.0"
hound = "3.5.1" hound = "3.5.1"
lazy_static = "1.5.0"
mp3lame-encoder = { version = "0.2.1", features = ["std"] } mp3lame-encoder = { version = "0.2.1", features = ["std"] }
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 = "0.0.1"
thiserror = "2.0.12" thiserror = "2.0.12"
[features]
binary-build = ["anyhow", "clap"]
[[bin]]
name = "blip"
path = "src/cli/main.rs"
required-features = ["binary-build"]

View file

@ -1,12 +1,245 @@
use std::{
error::Error,
fs::File,
io::{self},
ops::Not,
str::FromStr,
};
use clap::Parser; 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() { fn main() {
let name = Cli::parse().name; dbg!(Cli::parse());
println!("Hello, {name}!");
} }
#[derive(Parser)] #[derive(Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
#[command(version, author, about)] #[command(version, author, about)]
struct Cli { enum Cli {
name: String, /// 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<String>,
/// 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::<LetterString, Letter, Expression>)]
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<Self, Self::Err> {
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<Self, Self::Err> {
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<Self, Self::Err> {
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<const SEP: char, T, U>(
s: &str,
) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
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<T, U1, U2>(
s: &str,
) -> Result<(T, (U1, U2)), Box<dyn Error + Send + Sync + 'static>>
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<Self, Self::Err> {
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<ClonableFile>,
/// Use this sheet music instead of reading from a file or stdin
#[arg(short = 'c')]
sheet_music_string: Option<String>,
}
#[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<Self, Self::Err> {
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 },
} }

View file

@ -21,7 +21,6 @@ pub struct Marker;
pub struct Note(pub u8); pub struct Note(pub u8);
#[cfg_attr(debug_assertions, derive(Default))] #[cfg_attr(debug_assertions, derive(Default))]
#[derive(Clone)]
pub struct VariableChange(pub char, pub Instruction); pub struct VariableChange(pub char, pub Instruction);
#[cfg_attr(debug_assertions, derive(Default))] #[cfg_attr(debug_assertions, derive(Default))]