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 -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)"] 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 -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 -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) 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 -m NAME:EXPANSION, --macro NAME:EXPANSION
@ -34,7 +34,7 @@ modes:
-i EXPR, --instrument EXPR -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)"] 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 -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 -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) 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 -m NAME:EXPANSION, --macro NAME:EXPANSION

View file

@ -29,7 +29,7 @@ use strum::{EnumDiscriminants, EnumString, IntoDiscriminant, IntoStaticStr};
use thiserror::Error; use thiserror::Error;
const DEFAULT_INSTRUMENT: &str = "sin(2*PI*(442+442*((n+1)/N))*t)"; 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() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();

View file

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