replace total note length variable from l to L, swap fasteval log_2 with log(2,...), fix silence rendering, first compiler test

This commit is contained in:
brevalferrari 2025-05-28 16:44:32 +02:00
parent ce5a8760c1
commit 478ce49d8d
3 changed files with 71 additions and 52 deletions

View file

@ -16,7 +16,7 @@ modes:
-i EXPR, --instrument EXPR
signal expression (instrument) to generate music samples (you can use the floating-point number t for the current time in seconds, n for the indice of the currently playing note in the list of available notes starting with 0 and N for the number of available notes) [default: "sin(2*PI*(442+442*((n+1)/N))*t)"]
-l EXPR, --length EXPR
expression to generate note length in seconds [default: "2^(2-log_2(l))*(60/T)"]
expression to generate note length in seconds [default: "2^(2-log(2, l))*(60/T)"]
-v VARIABLE=VALUE, --variable VARIABLE=VALUE
add a variable named VARIABLE (an single letter) for the sheet music and all expressions and set its initial value to VALUE (you can override n, t, l and T which is the only way to set the initial tempo)
-m NAME:EXPANSION, --macro NAME:EXPANSION
@ -34,7 +34,7 @@ modes:
-i EXPR, --instrument EXPR
signal expression (instrument) to generate music samples (you can use the floating-point number t for the current time in seconds, n for the indice of the currently playing note in the list of available notes starting with 0 and N for the number of available notes) [default: "sin(2*PI*(442+442*((n+1)/N))*t)"]
-l EXPR, --length EXPR
expression to generate note length in seconds [default: "2^(2-log_2(l))*(60/T)"]
expression to generate note length in seconds [default: "2^(2-log(2, l))*(60/T)"]
-v VARIABLE=VALUE, --variable VARIABLE=VALUE
add a variable named VARIABLE (an single letter) for the sheet music and all expressions and set its initial value to VALUE (you can override n, t, l and T which is the only way to set the initial tempo)
-m NAME:EXPANSION, --macro NAME:EXPANSION

View file

@ -29,7 +29,7 @@ use strum::{EnumDiscriminants, EnumString, IntoDiscriminant, IntoStaticStr};
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)";
const DEFAULT_LENGTH: &str = "2^(2-log(2, l))*(60/T)";
fn main() {
let cli = Cli::parse();

View file

@ -66,7 +66,8 @@ impl_type!(Silence);
impl Token for Silence {
fn apply(&self, mut context: Context) -> Result<Context, CompilerError> {
context.render(None)?;
let mut next = context.render(None)?;
context.result.append(&mut next);
Ok(context)
}
}
@ -266,14 +267,17 @@ pub struct Context {
}
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq))]
#[error("variable not found: {0}")]
pub struct VariableNotFoundError(char);
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq))]
#[error("expression not found: {0}")]
pub struct SlopeNotFoundError(char);
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq))]
pub enum CompilerError {
#[error("expression evaluation: {0:?}")]
FastEval(#[from] fasteval::Error),
@ -284,19 +288,21 @@ pub enum CompilerError {
}
impl Context {
pub fn current_length(&mut self) -> Result<f64, CompilerError> {
*self = self
.get_slopes_for('L')
pub fn current_length(&self) -> Result<f64, CompilerError> {
self.get_slopes_for('L')
.map_err(CompilerError::from)?
.iter()
.try_fold(
self.clone(),
|mut acc, slope| -> Result<Context, CompilerError> {
// just in case next slopes use L too
*acc.get_mut('L')? = self.eval(slope).map_err(Into::<CompilerError>::into)?;
Ok(acc)
},
)?;
Ok(*self.get('L')?)
)?
.get('L')
.map_err(Into::into)
.cloned()
}
pub fn get(&self, name: char) -> Result<&f64, VariableNotFoundError> {
@ -323,39 +329,30 @@ impl Context {
}
fn tick(&mut self) -> Result<(), CompilerError> {
*self.get_mut('t')? += 1f64 / SAMPLE_RATE as f64;
{
let changes = self
.slopes
.iter()
.map(
|(
v,
Expression {
from: _,
instruction,
slab,
},
)|
-> Result<(char, f64), fasteval::Error> {
Ok((
*v,
instruction.eval(
slab,
&mut self
.variables
.iter()
.map(|(c, f)| (c.to_string(), *f))
.collect::<BTreeMap<String, f64>>(),
)?,
))
},
)
.collect::<Result<Vec<(char, f64)>, fasteval::Error>>()?;
for (variable, new_value) in changes {
*self.get_mut(variable)? = new_value;
}
}
*self.get_mut('t')? += 1f64 / (SAMPLE_RATE as f64);
*self = self.slopes.iter().try_fold(
self.clone(),
|mut acc,
(
v,
Expression {
from: _,
instruction,
slab,
},
)|
-> Result<Self, CompilerError> {
*acc.get_mut(*v)? = instruction.eval(
slab,
&mut acc
.variables
.iter()
.map(|(c, f)| (c.to_string(), *f))
.collect::<BTreeMap<String, f64>>(),
)?;
Ok(acc)
},
)?;
Ok(())
}
@ -398,10 +395,14 @@ impl Context {
}
Ok(result)
} else {
while (self.current_length()? * SAMPLE_RATE as f64) > *self.get('t')? - curr_t {
while (self.current_length()?) > *self.get('t')? - curr_t {
self.tick()?;
}
Ok(vec![0.0; (*self.get('t')? - curr_t) as usize])
Ok(vec![
0.0;
((*self.get('t')? - curr_t) * SAMPLE_RATE as f64)
as usize
])
}
}
@ -411,29 +412,34 @@ impl Context {
}
#[derive(From)]
#[cfg_attr(test, derive(Debug, PartialEq))]
struct Compiler(Context);
impl Compiler {
fn step(self, token: impl Token) -> Result<Self, CompilerError> {
pub fn step(self, token: impl Token) -> Result<Self, CompilerError> {
token.apply(self.0).map(Into::into)
}
fn compile_all(
fn apply_all(
self,
tokens: impl IntoIterator<Item = impl Token>,
) -> Result<Vec<f64>, CompilerError> {
) -> Result<Self, CompilerError> {
tokens
.into_iter()
.try_fold(self, |acc, token| acc.step(token))
.map(|c| c.0)
.map(Context::finalize)
}
pub fn compile_all(
self,
tokens: impl IntoIterator<Item = impl Token>,
) -> Result<Vec<f64>, CompilerError> {
self.apply_all(tokens).map(|c| c.0).map(Context::finalize)
}
}
#[cfg(test)]
mod tests {
use crate::compiler::Expression;
use crate::compiler::{Compiler, Expression, SAMPLE_RATE, Silence};
use super::Context;
use super::{CompilerError, Context};
#[test]
fn expression_is_clone() {
@ -453,7 +459,20 @@ mod tests {
('T', 60.0),
],
"sin(2*PI*(442+442*((n+1)/N))*t)".parse().unwrap(),
[('L', "2^(2-log_2(l))*(60/T)")].map(|(c, e)| (c, e.parse().unwrap())),
[('L', "2^(2-log(2, l))*(60/T)")].map(|(c, e)| (c, e.parse().unwrap())),
)
}
#[test]
fn silence() -> Result<(), CompilerError> {
assert_eq!(
SAMPLE_RATE as usize,
Compiler::from(context_generator())
.apply_all(vec![Silence])?
.0
.result
.len()
);
Ok(())
}
}