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]
|
[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"]
|
||||||
|
|
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 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 },
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue