diff --git a/doc/cli-design.txt b/doc/cli-design.txt index 2319de7..ab3bdc8 100644 --- a/doc/cli-design.txt +++ b/doc/cli-design.txt @@ -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 diff --git a/src/cli/main.rs b/src/cli/main.rs index 2c48f6e..eb1d56b 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -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(); diff --git a/src/compiler.rs b/src/compiler.rs index 72eecbe..cb54b78 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -66,7 +66,8 @@ impl_type!(Silence); impl Token for Silence { fn apply(&self, mut context: Context) -> Result { - 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 { - *self = self - .get_slopes_for('L') + pub fn current_length(&self) -> Result { + self.get_slopes_for('L') .map_err(CompilerError::from)? .iter() .try_fold( self.clone(), |mut acc, slope| -> Result { + // just in case next slopes use L too *acc.get_mut('L')? = self.eval(slope).map_err(Into::::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::>(), - )?, - )) - }, - ) - .collect::, 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 { + *acc.get_mut(*v)? = instruction.eval( + slab, + &mut acc + .variables + .iter() + .map(|(c, f)| (c.to_string(), *f)) + .collect::>(), + )?; + 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 { + pub fn step(self, token: impl Token) -> Result { token.apply(self.0).map(Into::into) } - fn compile_all( + fn apply_all( self, tokens: impl IntoIterator, - ) -> Result, CompilerError> { + ) -> Result { 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, + ) -> Result, 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(()) + } }