cli: play
This commit is contained in:
parent
283c6a7dd6
commit
42e52155e1
3 changed files with 251 additions and 12 deletions
21
Cargo.toml
21
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"]
|
||||
|
|
241
src/cli/main.rs
241
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<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 },
|
||||
}
|
||||
|
|
|
@ -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))]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue