From c9ca70d8ee4c5322c6b935dff750ae2150f77f60 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Tue, 3 Feb 2026 16:48:02 +0000 Subject: [PATCH 01/21] separate cycle tests --- src/tidal/cycle.rs | 1027 +------------------------------------- src/tidal/cycle/tests.rs | 1025 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1026 insertions(+), 1026 deletions(-) create mode 100644 src/tidal/cycle/tests.rs diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 7d7c891..d268c75 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -2150,1029 +2150,4 @@ impl Cycle { // ------------------------------------------------------------------------------------------------- #[cfg(test)] -mod test { - use super::*; - - use pretty_assertions::assert_eq; - - fn assert_cycles(input: &str, outputs: Vec>>) -> Result<(), String> { - let mut cycle = Cycle::from(input)?; - for out in outputs { - assert_eq!(cycle.generate()?, out, "with input: '{}'", input); - } - Ok(()) - } - - fn assert_cycle_equality(a: &str, b: &str) -> Result<(), String> { - let seed = rand::rng().random(); - assert_eq!( - Cycle::from(a)?.with_seed(seed).generate()?, - Cycle::from(b)?.with_seed(seed).generate()?, - ); - Ok(()) - } - - fn assert_cycle_advancing(input: &str) -> Result<(), String> { - let seed = rand::rng().random(); - for number_of_runs in 1..9 { - let mut cycle1 = Cycle::from(input)?.with_seed(seed); - let mut cycle2 = Cycle::from(input)?.with_seed(seed); - for _ in 0..number_of_runs { - let _ = cycle1.generate()?; - cycle2.advance(); - } - assert_eq!(cycle1.generate()?, cycle2.generate()?); - } - Ok(()) - } - - #[test] - fn span() -> Result<(), String> { - assert!(Span::new(Fraction::new(0, 1), Fraction::new(1, 1)) - .includes(&Span::new(Fraction::new(1, 2), Fraction::new(2, 1)))); - Ok(()) - } - - #[test] - fn parse() -> Result<(), String> { - assert!(Cycle::from("a b c [d").is_err()); - assert!(Cycle::from("a b/ c [d").is_err()); - assert!(Cycle::from("a b--- c [d").is_err()); - assert!(Cycle::from("*a b c [d").is_err()); - assert!(Cycle::from("a {{{}}").is_err()); - assert!(Cycle::from("] a z [").is_err()); - assert!(Cycle::from("->err").is_err()); - assert!(Cycle::from("(a, b)").is_err()); - assert!(Cycle::from("#(12, 32)").is_err()); - assert!(Cycle::from("#c $").is_err()); - - assert!(Cycle::from("c44'mode").is_err()); - assert!(Cycle::from("c4'!mode").is_err()); - assert!(Cycle::from("y'mode").is_err()); - assert!(Cycle::from("c4'mo'de").is_err()); - assert!(Cycle::from("_names_cannot_start_with_underscore").is_err()); - - assert!(Cycle::from("c4'mode").is_ok()); - assert!(Cycle::from("c'm7#^-").is_ok()); - assert!(Cycle::from("[[[[[[[[]]]]]][[[[[]][[[]]]]]][[[][[[]]]]][[[[]]]]]]").is_ok()); - - Ok(()) - } - - #[test] - fn generate() -> Result<(), String> { - assert_eq!( - Cycle::from("[0x0] [0x1A] [0XA] [-0X5] [-0XA0] [-0Xaa]")?.generate()?, - [[ - Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0x0), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(0x1a), - Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_int(0xa), - Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_int(-0x5), - Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_int(-0xa0), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_int(-0xaa), - ]] - ); - - assert_eq!( - Cycle::from("[0] [1] [1.01] [0.01] [0.] [.01]")?.generate()?, - [[ - Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(1), - Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(1.01), - Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(0.01), - Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_float(0.0), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_float(0.01), - ]] - ); - - let empty_events: Vec> = vec![]; - assert_eq!(Cycle::from("a*[]")?.generate()?, empty_events); - assert_eq!(Cycle::from("[c d]/0")?.generate()?, empty_events); - assert_eq!(Cycle::from("[c d]*0")?.generate()?, empty_events); - - assert!(Cycle::from("[c d]/1000000000000").is_err()); // too large for fraction - assert_eq!( - Cycle::from("[c d]/1000000")?.generate()?, - [[Event::at(Fraction::from(0), Fraction::new(1, 1)).with_note(0, 4)]] - ); - - assert_eq!( - Cycle::from("a b c d")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_note(9, 4), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_note(11, 4), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(0, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(2, 4), - ]] - ); - assert_eq!( - Cycle::from("\ta\r\n\tb\nc\n d\n\n")?.generate()?, - Cycle::from("a b c d")?.generate()? - ); - assert_eq!( - Cycle::from("a b [ c d ]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_note(9, 4), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(11, 4), - Event::at(Fraction::new(2, 3), Fraction::new(1, 6)).with_note(0, 4), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(2, 4), - ]] - ); - assert_eq!( - Cycle::from("[a a] [b4 b5 b6] [c0 d1 c2 d3]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 4), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_note(9, 4), - Event::at(Fraction::new(3, 9), Fraction::new(1, 9)).with_note(11, 4), - Event::at(Fraction::new(4, 9), Fraction::new(1, 9)).with_note(11, 5), - Event::at(Fraction::new(5, 9), Fraction::new(1, 9)).with_note(11, 6), - Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(0, 0), - Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(2, 1), - Event::at(Fraction::new(10, 12), Fraction::new(1, 12)).with_note(0, 2), - Event::at(Fraction::new(11, 12), Fraction::new(1, 12)).with_note(2, 3), - ]] - ); - assert_eq!( - Cycle::from("[a0 [bb1 [b2 c3]]] c#4 [[[d5 D#6] E7 ] F8]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 0), - Event::at(Fraction::new(1, 6), Fraction::new(1, 12)).with_note(10, 1), - Event::at(Fraction::new(3, 12), Fraction::new(1, 24)).with_note(11, 2), - Event::at(Fraction::new(7, 24), Fraction::new(1, 24)).with_note(0, 3), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(1, 4), - Event::at(Fraction::new(2, 3), Fraction::new(1, 24)).with_note(2, 5), - Event::at(Fraction::new(17, 24), Fraction::new(1, 24)).with_note(3, 6), - Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(4, 7), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(5, 8), - ]] - ); - assert_eq!( - Cycle::from("[R [e [n o]]] , [[[i s] e ] _]")?.generate()?, - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("R"), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)).with_note(4, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 8)).with_name("n"), - Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("o"), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("i"), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_name("s"), - Event::at(Fraction::new(1, 4), Fraction::new(3, 4)).with_note(4, 4), - ], - ] - ); - - assert_cycles( - "", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(2, 4) - ]], - ], - )?; - - assert_cycles( - " >", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 4), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4), - ]], - vec![vec![ - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(0, 4) - ]], - vec![vec![ - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 0), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(2, 4), - ]], - ], - )?; - - assert_cycles( - "< b, >", - vec![ - vec![ - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4)], - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], - ], - vec![ - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4)], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(2, 4), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(4, 4), - ], - ], - vec![ - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 8)], - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], - ], - ], - )?; - - assert_cycles( - "{-3 -2 -1 0 1 2 3}%4", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-3), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(-1), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(0), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(-3), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-2), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(0), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(1), - ]], - ], - )?; - - assert_cycles( - "{<0 0 d#8:test> 1 :0xB [<.5 0.95> 1.]}%3", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(1), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) - .with_note(0, 4) - .with_target(Target::from_index(0xB)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 6)).with_float(0.5), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_float(1.0), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)) - .with_note(2, 4) - .with_target(Target::from_index(0xB)), - Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(0.95), - Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(1.0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) - .with_note(3, 8) - .with_target(Target::from_name("test".into())), - ]], - ], - )?; - - assert_eq!( - Cycle::from("[1 middle _] {}%42 [] <>")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 12)).with_int(1), - Event::at(Fraction::new(1, 12), Fraction::new(1, 6)).with_name("middle"), - Event::at(Fraction::new(1, 4), Fraction::new(3, 4)), - ]] - ); - - assert_eq!( - Cycle::from("[1 __ 2] 3")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(3, 8)).with_int(1), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(2), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(3), - ]] - ); - - assert_cycles( - "", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_name("some_name") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_name("another_one") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "chord") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "-^7") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_name("c6a_name") - ]], - ], - )?; - - assert_cycles( - "[1 2] [3 4,[5 6]:42]", - vec![vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(4), - ], - vec![ - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_int(5) - .with_target(Target::from_index(42)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(6) - .with_target(Target::from_index(42)), - ], - ]], - )?; - - assert_eq!( - Cycle::from("1 second*2 eb3*3 [32 32]*4")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("second"), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("second"), - Event::at(Fraction::new(6, 12), Fraction::new(1, 12)).with_note(3, 3), - Event::at(Fraction::new(7, 12), Fraction::new(1, 12)).with_note(3, 3), - Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(3, 3), - Event::at(Fraction::new(24, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(25, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(26, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(27, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(28, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(29, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(30, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(31, 32), Fraction::new(1, 32)).with_int(32), - ]] - ); - - assert_cycles( - "tresillo(6,8), outside(4,11)", - vec![vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(4, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(5, 8), Fraction::new(1, 8)), - Event::at(Fraction::new(6, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("tresillo"), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(1, 11), Fraction::new(2, 11)), - Event::at(Fraction::new(3, 11), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(4, 11), Fraction::new(2, 11)), - Event::at(Fraction::new(6, 11), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(7, 11), Fraction::new(2, 11)), - Event::at(Fraction::new(9, 11), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(10, 11), Fraction::new(1, 11)), - ], - ]], - )?; - - assert_cycles( - "[<1 10> <2 20>:a](2,5)", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(1), - Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) - .with_int(2) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), - Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(1), - Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) - .with_int(2) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(10), - Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) - .with_int(20) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), - Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(10), - Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) - .with_int(20) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), - ]], - ], - )?; - - assert_eq!( - Cycle::from("1!2 3 [4!3 5]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(12, 16), Fraction::new(1, 16)).with_int(4), - Event::at(Fraction::new(13, 16), Fraction::new(1, 16)).with_int(4), - Event::at(Fraction::new(14, 16), Fraction::new(1, 16)).with_int(4), - Event::at(Fraction::new(15, 16), Fraction::new(1, 16)).with_int(5), - ]] - ); - - assert_cycles( - "[0 1]!2 !2", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(9, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(9, 4), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(11, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(11, 4), - ]], - ], - )?; - - assert_cycles( - "[0 1]/2", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(0) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(1) - ]], - ], - )?; - - assert_cycles( - "[0 1]*2.5", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(0), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(1), - ]], - ], - )?; - - assert_eq!( - Cycle::from("a:1 b:target")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_name("target".into())) - ]] - ); - - assert_cycles( - "a:<1 2>", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(2))]], - ], - )?; - - assert_cycles( - "a:1:2:Target", - vec![vec![vec![Event::at( - Fraction::from(0), - Fraction::new(1, 1), - ) - .with_note(9, 4) - .with_targets(vec![ - Target::from_index(1), - Target::from_name("Target".into()), - ])]]], - )?; - - assert_cycles( - "[a:1:2]:<3 4>", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - ], - )?; - - // target expression preserves the structure from the left side - assert_eq!( - Cycle::from("[a b c d]:[1 2 3]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_note(0, 4) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(2, 4) - .with_target(Target::from_index(3)), - ]] - ); - - // when using ~ as a target, it's possible selectively skip overriding the outer target from within - assert_cycles( - "[a [b:<~ 7> b:<8 9>]]:[1 [2 3], 4]", - vec![ - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - // this iteration lets the outer context set the target - .with_target(Target::from_index(2)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(8)), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(8)), - ], - ], - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(7)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(9)), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(7)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(9)), - ], - ], - ], - )?; - - assert_cycles( - "[a b]:<1 target>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(1)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_name("target".into())), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_name("target".into())), - ]], - ], - )?; - - assert_cycles( - "[a:1 b]:<3 4>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(3)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(4)), - ]], - ], - )?; - - assert_eq!( - Cycle::from("a:1 b:v0.1:v1.0:p1.0:g100.0")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_targets(vec![ - Target::Named("v".into(), Some(0.1)), - // second v should not be applied - Target::Named("p".into(), Some(1.0)), - Target::Named("g".into(), Some(100.0)), - ]) - ]] - ); - - // outer instrument values shouldn't override inner ones - assert_eq!( - Cycle::from("[a:#2 b]:#3")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(3)), - ]] - ); - - assert_eq!( - Cycle::from("a:1:#1 b:#1:1")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::Index(1)), - ]] - ); - - assert_eq!( - Cycle::from("c(3,8,9)")?.generate()?, - [[ - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_note(0, 4), - Event::at(Fraction::new(3, 8), Fraction::new(1, 4)), - Event::at(Fraction::new(5, 8), Fraction::new(1, 8)).with_note(0, 4), - Event::at(Fraction::new(6, 8), Fraction::new(1, 8)), - Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_note(0, 4), - ]] - ); - - assert_cycle_equality("a? b?", "a?0.5 b?0.5")?; - assert_cycle_equality("[a b c](3,8,9)", "[a b c](3,8,1)")?; - assert_cycle_equality("[a b c](3,8,7)", "[a b c](3,8,-1)")?; - assert_cycle_equality("[a a a a]", "[a ! ! !]")?; - assert_cycle_equality("[! ! a !]", "[~ ~ a a]")?; - assert_cycle_equality("a ~ ~ ~", "a - - -")?; - assert_cycle_equality("[a b] ! ! !", "[a b] [a b] [a b] ")?; - assert_cycle_equality("{a b!2 c}%3", "{a b b c}%3")?; - assert_cycle_equality("a b, {c d e}%2", "{a b, c d e}")?; - assert_cycle_equality("0..3", "0 1 2 3")?; - assert_cycle_equality("-5..-8", "-5 -6 -7 -8")?; - assert_cycle_equality("a b . c d", "[a b] [c d]")?; - assert_cycle_equality( - "a b . c d e . f g h i [j k . l m]", - "[a b] [c d e] [f g h i [[j k] [l m]]]", - )?; - assert_cycle_equality( - "a b . c d e , f g h i . j k, l m", - "[a b] [c d e], [[f g h i] [j k]], [l m]", - )?; - assert_cycle_equality("{a b . c d . f g h}%2", "{[a b] [c d] [f g h]}%2")?; - assert_cycle_equality("", "<[a b] [c d] [f g h]>")?; - - assert_cycles( - "[0 1 2]/2", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::new(1, 3), Fraction::new(2, 3)).with_int(2) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - ], - )?; - - assert_cycles( - "0*<1 2>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_int(0) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(0), - ]], - ], - )?; - - assert_eq!( - Cycle::from("0*[4 3]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(0), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(0), - ]] - ); - - assert_cycles( - "{0 1 2 3}%<2 3>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(3), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - ], - )?; - - // TODO test random outputs // parse_with_debug("[a b c d]?0.5"); - - Ok(()) - } - - #[test] - fn expression_chains() -> Result<(), String> { - assert_cycle_equality("a*3/2", "a*1.5")?; - assert_cycle_equality("[a b c d]*2*4", "[a b c d]*8")?; - assert_cycle_equality("a/2/3/4/5", "a/120")?; - assert_cycle_equality( - "[a b c d e f]:[[v.2 v.5]*3]", - "[a b c d e f]:[v.2 v.5 v.2 v.5 v.2 v.5]", - )?; - assert_cycle_equality( - "[a:0 b:0 c:1 d:1 e:2 f:2 g:3 h:3]/2*4", - "[a b c d e f g h]:[0 1 2 3]/2*4", - )?; - assert_cycle_equality("[a:0 b:0 c:1 d:1]/2", "[a b c d]/2:<0 1>")?; - - assert_eq!( - Cycle::from("[0 1]*2:[1 2 3 4]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(0) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_int(0) - .with_target(Target::from_index(3)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::from_index(4)), - ]] - ); - - assert_cycles( - "[a b c:v.2 d]:p.5:[v.1 v.3]:v.8/4", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(9, 4) - .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.1)), - ])]], - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(11, 4) - .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.1)), - ])]], - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(0, 4) - .with_targets(vec![ - Target::Named("v".into(), Some(0.2)), - Target::Named("p".into(), Some(0.5)), - ])]], - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(2, 4) - .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.3)), - ])]], - ], - )?; - Ok(()) - } - - #[test] - fn event_limit() -> Result<(), String> { - assert!(Cycle::from("[[a b c d]*100]*100")?.generate().is_err()); - assert!(Cycle::from("[[a b c d]*100]*100")? - .with_event_limit(0x10000) - .generate() - .is_ok()); - Ok(()) - } - - #[test] - fn stacks() -> Result<(), String> { - assert_eq!( - Cycle::from("bd [bd, cp], ~ hh")?.generate()?, - [ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("bd"), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("bd") - ], - vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("cp")], - vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("hh")] - ] - ); - Ok(()) - } - - #[test] - fn advancing() -> Result<(), String> { - assert_cycle_advancing("[a b c d]")?; // stateless - assert_cycle_advancing("[a b], [c d]")?; - assert_cycle_advancing("{a b}%2 {a b}*5")?; // stateful - assert_cycle_advancing("[a b]*5 [a b]/5")?; - assert_cycle_advancing("[a b c d]")?; - assert_cycle_advancing("a ")?; - assert_cycle_advancing("[a b? c d]|[c? d?]")?; - assert_cycle_advancing("[{a b}/2 c d], e? {a b}*2")?; - Ok(()) - } - - #[test] - fn target_assign() -> Result<(), String> { - assert_cycle_equality( - "[a b c [d e f g]]:[v.5 v.3 v.2 v.1]:[p.5 p.25 p.1 p.9]", - "[a b c [d e f g]]:v=[.5 .3 .2 .1]:p=[.5 .25 .1 .9]", - )?; - assert_eq!( - Cycle::from("[1 2 3 4]:p=[.1 .2 .3 .4]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::Named("p".into(), Some(0.1))), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(2) - .with_target(Target::Named("p".into(), Some(0.2))), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_int(3) - .with_target(Target::Named("p".into(), Some(0.3))), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(4) - .with_target(Target::Named("p".into(), Some(0.4))), - ]] - ); - assert_eq!( - Cycle::from("[1 2 3 4]:#=[1 c4 3 0.2]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::Index(1)), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(2) - .with_target(Target::Index(48)), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_int(3) - .with_target(Target::Index(3)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(4) - .with_target(Target::Index(0)), - ]] - ); - - assert_eq!( - Cycle::from("[1 2 3 4]:long=[1 _ ~ 0.2]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::Named("long".into(), Some(1.0))), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(2) - .with_target(Target::Named("long".into(), Some(1.0))), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(4) - .with_target(Target::Named("long".into(), Some(0.2))), - ]] - ); - - assert_cycles( - "[1 2 3 4 5 6 7 8]/4:d=<.1 .2 .3 .4>:v=[<.3 .2 .1>*2/3]", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(1) - .with_targets(vec![ - Target::Named("d".into(), Some(0.1)), - Target::Named("v".into(), Some(0.3)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(2) - .with_targets(vec![ - Target::Named("d".into(), Some(0.1)), - Target::Named("v".into(), Some(0.3)), - ]), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(3) - .with_targets(vec![ - Target::Named("d".into(), Some(0.2)), - Target::Named("v".into(), Some(0.3)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(4) - .with_targets(vec![ - Target::Named("d".into(), Some(0.2)), - Target::Named("v".into(), Some(0.2)), - ]), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(5) - .with_targets(vec![ - Target::Named("d".into(), Some(0.3)), - Target::Named("v".into(), Some(0.2)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(6) - .with_targets(vec![ - Target::Named("d".into(), Some(0.3)), - Target::Named("v".into(), Some(0.2)), - ]), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(7) - .with_targets(vec![ - Target::Named("d".into(), Some(0.4)), - Target::Named("v".into(), Some(0.1)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(8) - .with_targets(vec![ - Target::Named("d".into(), Some(0.4)), - Target::Named("v".into(), Some(0.1)), - ]), - ]], - ], - )?; - - assert_cycle_equality( - "[1 2 3 4]:v=[0.2 0.3 0.4 p.8]", - "[1 2 3 4]:[v0.2 v0.3 v0.4 p.8]", - )?; - - assert_cycle_equality( - "[1 2 3 4]:g=[0.1 10.]", - "[1 2 3 4]:[g0.1 g10.]", - )?; - - Ok(()) - } -} +mod tests; diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs new file mode 100644 index 0000000..8d94768 --- /dev/null +++ b/src/tidal/cycle/tests.rs @@ -0,0 +1,1025 @@ +use rand::Rng; + +type Fraction = num_rational::Rational32; + +use super::*; + +use pretty_assertions::assert_eq; + +fn assert_cycles(input: &str, outputs: Vec>>) -> Result<(), String> { + let mut cycle = Cycle::from(input)?; + for out in outputs { + assert_eq!(cycle.generate()?, out, "with input: '{}'", input); + } + Ok(()) +} + +fn assert_cycle_equality(a: &str, b: &str) -> Result<(), String> { + let seed = rand::rng().random(); + assert_eq!( + Cycle::from(a)?.with_seed(seed).generate()?, + Cycle::from(b)?.with_seed(seed).generate()?, + ); + Ok(()) +} + +fn assert_cycle_advancing(input: &str) -> Result<(), String> { + let seed = rand::rng().random(); + for number_of_runs in 1..9 { + let mut cycle1 = Cycle::from(input)?.with_seed(seed); + let mut cycle2 = Cycle::from(input)?.with_seed(seed); + for _ in 0..number_of_runs { + let _ = cycle1.generate()?; + cycle2.advance(); + } + assert_eq!(cycle1.generate()?, cycle2.generate()?); + } + Ok(()) +} + +#[test] +fn span() -> Result<(), String> { + assert!(Span::new(Fraction::new(0, 1), Fraction::new(1, 1)) + .includes(&Span::new(Fraction::new(1, 2), Fraction::new(2, 1)))); + Ok(()) +} + +#[test] +fn parse() -> Result<(), String> { + assert!(Cycle::from("a b c [d").is_err()); + assert!(Cycle::from("a b/ c [d").is_err()); + assert!(Cycle::from("a b--- c [d").is_err()); + assert!(Cycle::from("*a b c [d").is_err()); + assert!(Cycle::from("a {{{}}").is_err()); + assert!(Cycle::from("] a z [").is_err()); + assert!(Cycle::from("->err").is_err()); + assert!(Cycle::from("(a, b)").is_err()); + assert!(Cycle::from("#(12, 32)").is_err()); + assert!(Cycle::from("#c $").is_err()); + + assert!(Cycle::from("c44'mode").is_err()); + assert!(Cycle::from("c4'!mode").is_err()); + assert!(Cycle::from("y'mode").is_err()); + assert!(Cycle::from("c4'mo'de").is_err()); + assert!(Cycle::from("_names_cannot_start_with_underscore").is_err()); + + assert!(Cycle::from("c4'mode").is_ok()); + assert!(Cycle::from("c'm7#^-").is_ok()); + assert!(Cycle::from("[[[[[[[[]]]]]][[[[[]][[[]]]]]][[[][[[]]]]][[[[]]]]]]").is_ok()); + + Ok(()) +} + +#[test] +fn generate() -> Result<(), String> { + assert_eq!( + Cycle::from("[0x0] [0x1A] [0XA] [-0X5] [-0XA0] [-0Xaa]")?.generate()?, + [[ + Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0x0), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(0x1a), + Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_int(0xa), + Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_int(-0x5), + Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_int(-0xa0), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_int(-0xaa), + ]] + ); + + assert_eq!( + Cycle::from("[0] [1] [1.01] [0.01] [0.] [.01]")?.generate()?, + [[ + Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(1), + Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(1.01), + Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(0.01), + Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_float(0.0), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_float(0.01), + ]] + ); + + let empty_events: Vec> = vec![]; + assert_eq!(Cycle::from("a*[]")?.generate()?, empty_events); + assert_eq!(Cycle::from("[c d]/0")?.generate()?, empty_events); + assert_eq!(Cycle::from("[c d]*0")?.generate()?, empty_events); + + assert!(Cycle::from("[c d]/1000000000000").is_err()); // too large for fraction + assert_eq!( + Cycle::from("[c d]/1000000")?.generate()?, + [[Event::at(Fraction::from(0), Fraction::new(1, 1)).with_note(0, 4)]] + ); + + assert_eq!( + Cycle::from("a b c d")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_note(9, 4), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_note(11, 4), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(0, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(2, 4), + ]] + ); + assert_eq!( + Cycle::from("\ta\r\n\tb\nc\n d\n\n")?.generate()?, + Cycle::from("a b c d")?.generate()? + ); + assert_eq!( + Cycle::from("a b [ c d ]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_note(9, 4), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(11, 4), + Event::at(Fraction::new(2, 3), Fraction::new(1, 6)).with_note(0, 4), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(2, 4), + ]] + ); + assert_eq!( + Cycle::from("[a a] [b4 b5 b6] [c0 d1 c2 d3]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 4), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_note(9, 4), + Event::at(Fraction::new(3, 9), Fraction::new(1, 9)).with_note(11, 4), + Event::at(Fraction::new(4, 9), Fraction::new(1, 9)).with_note(11, 5), + Event::at(Fraction::new(5, 9), Fraction::new(1, 9)).with_note(11, 6), + Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(0, 0), + Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(2, 1), + Event::at(Fraction::new(10, 12), Fraction::new(1, 12)).with_note(0, 2), + Event::at(Fraction::new(11, 12), Fraction::new(1, 12)).with_note(2, 3), + ]] + ); + assert_eq!( + Cycle::from("[a0 [bb1 [b2 c3]]] c#4 [[[d5 D#6] E7 ] F8]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 0), + Event::at(Fraction::new(1, 6), Fraction::new(1, 12)).with_note(10, 1), + Event::at(Fraction::new(3, 12), Fraction::new(1, 24)).with_note(11, 2), + Event::at(Fraction::new(7, 24), Fraction::new(1, 24)).with_note(0, 3), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(1, 4), + Event::at(Fraction::new(2, 3), Fraction::new(1, 24)).with_note(2, 5), + Event::at(Fraction::new(17, 24), Fraction::new(1, 24)).with_note(3, 6), + Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(4, 7), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(5, 8), + ]] + ); + assert_eq!( + Cycle::from("[R [e [n o]]] , [[[i s] e ] _]")?.generate()?, + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("R"), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)).with_note(4, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 8)).with_name("n"), + Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("o"), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("i"), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_name("s"), + Event::at(Fraction::new(1, 4), Fraction::new(3, 4)).with_note(4, 4), + ], + ] + ); + + assert_cycles( + "", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(2, 4) + ]], + ], + )?; + + assert_cycles( + " >", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 4), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4), + ]], + vec![vec![ + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(0, 4) + ]], + vec![vec![ + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 0), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(2, 4), + ]], + ], + )?; + + assert_cycles( + "< b, >", + vec![ + vec![ + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4)], + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], + ], + vec![ + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4)], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(2, 4), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(4, 4), + ], + ], + vec![ + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 8)], + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], + ], + ], + )?; + + assert_cycles( + "{-3 -2 -1 0 1 2 3}%4", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-3), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(-1), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(0), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(-3), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-2), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(0), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(1), + ]], + ], + )?; + + assert_cycles( + "{<0 0 d#8:test> 1 :0xB [<.5 0.95> 1.]}%3", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(1), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) + .with_note(0, 4) + .with_target(Target::from_index(0xB)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 6)).with_float(0.5), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_float(1.0), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)) + .with_note(2, 4) + .with_target(Target::from_index(0xB)), + Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(0.95), + Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(1.0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) + .with_note(3, 8) + .with_target(Target::from_name("test".into())), + ]], + ], + )?; + + assert_eq!( + Cycle::from("[1 middle _] {}%42 [] <>")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 12)).with_int(1), + Event::at(Fraction::new(1, 12), Fraction::new(1, 6)).with_name("middle"), + Event::at(Fraction::new(1, 4), Fraction::new(3, 4)), + ]] + ); + + assert_eq!( + Cycle::from("[1 __ 2] 3")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(3, 8)).with_int(1), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(2), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(3), + ]] + ); + + assert_cycles( + "", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_name("some_name") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_name("another_one") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "chord") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "-^7") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_name("c6a_name") + ]], + ], + )?; + + assert_cycles( + "[1 2] [3 4,[5 6]:42]", + vec![vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(4), + ], + vec![ + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_int(5) + .with_target(Target::from_index(42)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(6) + .with_target(Target::from_index(42)), + ], + ]], + )?; + + assert_eq!( + Cycle::from("1 second*2 eb3*3 [32 32]*4")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("second"), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("second"), + Event::at(Fraction::new(6, 12), Fraction::new(1, 12)).with_note(3, 3), + Event::at(Fraction::new(7, 12), Fraction::new(1, 12)).with_note(3, 3), + Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(3, 3), + Event::at(Fraction::new(24, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(25, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(26, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(27, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(28, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(29, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(30, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(31, 32), Fraction::new(1, 32)).with_int(32), + ]] + ); + + assert_cycles( + "tresillo(6,8), outside(4,11)", + vec![vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(4, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(5, 8), Fraction::new(1, 8)), + Event::at(Fraction::new(6, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("tresillo"), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(1, 11), Fraction::new(2, 11)), + Event::at(Fraction::new(3, 11), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(4, 11), Fraction::new(2, 11)), + Event::at(Fraction::new(6, 11), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(7, 11), Fraction::new(2, 11)), + Event::at(Fraction::new(9, 11), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(10, 11), Fraction::new(1, 11)), + ], + ]], + )?; + + assert_cycles( + "[<1 10> <2 20>:a](2,5)", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(1), + Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) + .with_int(2) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), + Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(1), + Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) + .with_int(2) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(10), + Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) + .with_int(20) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), + Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(10), + Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) + .with_int(20) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), + ]], + ], + )?; + + assert_eq!( + Cycle::from("1!2 3 [4!3 5]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(12, 16), Fraction::new(1, 16)).with_int(4), + Event::at(Fraction::new(13, 16), Fraction::new(1, 16)).with_int(4), + Event::at(Fraction::new(14, 16), Fraction::new(1, 16)).with_int(4), + Event::at(Fraction::new(15, 16), Fraction::new(1, 16)).with_int(5), + ]] + ); + + assert_cycles( + "[0 1]!2 !2", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(9, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(9, 4), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(11, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(11, 4), + ]], + ], + )?; + + assert_cycles( + "[0 1]/2", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(0) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(1) + ]], + ], + )?; + + assert_cycles( + "[0 1]*2.5", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(0), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(1), + ]], + ], + )?; + + assert_eq!( + Cycle::from("a:1 b:target")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_name("target".into())) + ]] + ); + + assert_cycles( + "a:<1 2>", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(2))]], + ], + )?; + + assert_cycles( + "a:1:2:Target", + vec![vec![vec![Event::at( + Fraction::from(0), + Fraction::new(1, 1), + ) + .with_note(9, 4) + .with_targets(vec![ + Target::from_index(1), + Target::from_name("Target".into()), + ])]]], + )?; + + assert_cycles( + "[a:1:2]:<3 4>", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + ], + )?; + + // target expression preserves the structure from the left side + assert_eq!( + Cycle::from("[a b c d]:[1 2 3]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_note(0, 4) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(2, 4) + .with_target(Target::from_index(3)), + ]] + ); + + // when using ~ as a target, it's possible selectively skip overriding the outer target from within + assert_cycles( + "[a [b:<~ 7> b:<8 9>]]:[1 [2 3], 4]", + vec![ + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + // this iteration lets the outer context set the target + .with_target(Target::from_index(2)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(8)), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(8)), + ], + ], + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(7)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(9)), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(7)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(9)), + ], + ], + ], + )?; + + assert_cycles( + "[a b]:<1 target>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(1)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_name("target".into())), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_name("target".into())), + ]], + ], + )?; + + assert_cycles( + "[a:1 b]:<3 4>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(3)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(4)), + ]], + ], + )?; + + assert_eq!( + Cycle::from("a:1 b:v0.1:v1.0:p1.0:g100.0")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_targets(vec![ + Target::Named("v".into(), Some(0.1)), + // second v should not be applied + Target::Named("p".into(), Some(1.0)), + Target::Named("g".into(), Some(100.0)), + ]) + ]] + ); + + // outer instrument values shouldn't override inner ones + assert_eq!( + Cycle::from("[a:#2 b]:#3")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(3)), + ]] + ); + + assert_eq!( + Cycle::from("a:1:#1 b:#1:1")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::Index(1)), + ]] + ); + + assert_eq!( + Cycle::from("c(3,8,9)")?.generate()?, + [[ + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_note(0, 4), + Event::at(Fraction::new(3, 8), Fraction::new(1, 4)), + Event::at(Fraction::new(5, 8), Fraction::new(1, 8)).with_note(0, 4), + Event::at(Fraction::new(6, 8), Fraction::new(1, 8)), + Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_note(0, 4), + ]] + ); + + assert_cycle_equality("a? b?", "a?0.5 b?0.5")?; + assert_cycle_equality("[a b c](3,8,9)", "[a b c](3,8,1)")?; + assert_cycle_equality("[a b c](3,8,7)", "[a b c](3,8,-1)")?; + assert_cycle_equality("[a a a a]", "[a ! ! !]")?; + assert_cycle_equality("[! ! a !]", "[~ ~ a a]")?; + assert_cycle_equality("a ~ ~ ~", "a - - -")?; + assert_cycle_equality("[a b] ! ! !", "[a b] [a b] [a b] ")?; + assert_cycle_equality("{a b!2 c}%3", "{a b b c}%3")?; + assert_cycle_equality("a b, {c d e}%2", "{a b, c d e}")?; + assert_cycle_equality("0..3", "0 1 2 3")?; + assert_cycle_equality("-5..-8", "-5 -6 -7 -8")?; + assert_cycle_equality("a b . c d", "[a b] [c d]")?; + assert_cycle_equality( + "a b . c d e . f g h i [j k . l m]", + "[a b] [c d e] [f g h i [[j k] [l m]]]", + )?; + assert_cycle_equality( + "a b . c d e , f g h i . j k, l m", + "[a b] [c d e], [[f g h i] [j k]], [l m]", + )?; + assert_cycle_equality("{a b . c d . f g h}%2", "{[a b] [c d] [f g h]}%2")?; + assert_cycle_equality("", "<[a b] [c d] [f g h]>")?; + + assert_cycles( + "[0 1 2]/2", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::new(1, 3), Fraction::new(2, 3)).with_int(2) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + ], + )?; + + assert_cycles( + "0*<1 2>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_int(0) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(0), + ]], + ], + )?; + + assert_eq!( + Cycle::from("0*[4 3]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(0), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(0), + ]] + ); + + assert_cycles( + "{0 1 2 3}%<2 3>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(3), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + ], + )?; + + // TODO test random outputs // parse_with_debug("[a b c d]?0.5"); + + Ok(()) +} + +#[test] +fn expression_chains() -> Result<(), String> { + assert_cycle_equality("a*3/2", "a*1.5")?; + assert_cycle_equality("[a b c d]*2*4", "[a b c d]*8")?; + assert_cycle_equality("a/2/3/4/5", "a/120")?; + assert_cycle_equality( + "[a b c d e f]:[[v.2 v.5]*3]", + "[a b c d e f]:[v.2 v.5 v.2 v.5 v.2 v.5]", + )?; + assert_cycle_equality( + "[a:0 b:0 c:1 d:1 e:2 f:2 g:3 h:3]/2*4", + "[a b c d e f g h]:[0 1 2 3]/2*4", + )?; + assert_cycle_equality("[a:0 b:0 c:1 d:1]/2", "[a b c d]/2:<0 1>")?; + + assert_eq!( + Cycle::from("[0 1]*2:[1 2 3 4]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(0) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_int(0) + .with_target(Target::from_index(3)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::from_index(4)), + ]] + ); + + assert_cycles( + "[a b c:v.2 d]:p.5:[v.1 v.3]:v.8/4", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(9, 4) + .with_targets(vec![ + Target::Named("p".into(), Some(0.5)), + Target::Named("v".into(), Some(0.1)), + ])]], + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(11, 4) + .with_targets(vec![ + Target::Named("p".into(), Some(0.5)), + Target::Named("v".into(), Some(0.1)), + ])]], + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(0, 4) + .with_targets(vec![ + Target::Named("v".into(), Some(0.2)), + Target::Named("p".into(), Some(0.5)), + ])]], + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(2, 4) + .with_targets(vec![ + Target::Named("p".into(), Some(0.5)), + Target::Named("v".into(), Some(0.3)), + ])]], + ], + )?; + Ok(()) +} + +#[test] +fn event_limit() -> Result<(), String> { + assert!(Cycle::from("[[a b c d]*100]*100")?.generate().is_err()); + assert!(Cycle::from("[[a b c d]*100]*100")? + .with_event_limit(0x10000) + .generate() + .is_ok()); + Ok(()) +} + +#[test] +fn stacks() -> Result<(), String> { + assert_eq!( + Cycle::from("bd [bd, cp], ~ hh")?.generate()?, + [ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("bd"), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("bd") + ], + vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("cp")], + vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("hh")] + ] + ); + Ok(()) +} + +#[test] +fn advancing() -> Result<(), String> { + assert_cycle_advancing("[a b c d]")?; // stateless + assert_cycle_advancing("[a b], [c d]")?; + assert_cycle_advancing("{a b}%2 {a b}*5")?; // stateful + assert_cycle_advancing("[a b]*5 [a b]/5")?; + assert_cycle_advancing("[a b c d]")?; + assert_cycle_advancing("a ")?; + assert_cycle_advancing("[a b? c d]|[c? d?]")?; + assert_cycle_advancing("[{a b}/2 c d], e? {a b}*2")?; + Ok(()) +} + +#[test] +fn target_assign() -> Result<(), String> { + assert_cycle_equality( + "[a b c [d e f g]]:[v.5 v.3 v.2 v.1]:[p.5 p.25 p.1 p.9]", + "[a b c [d e f g]]:v=[.5 .3 .2 .1]:p=[.5 .25 .1 .9]", + )?; + assert_eq!( + Cycle::from("[1 2 3 4]:p=[.1 .2 .3 .4]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::Named("p".into(), Some(0.1))), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(2) + .with_target(Target::Named("p".into(), Some(0.2))), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_int(3) + .with_target(Target::Named("p".into(), Some(0.3))), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(4) + .with_target(Target::Named("p".into(), Some(0.4))), + ]] + ); + assert_eq!( + Cycle::from("[1 2 3 4]:#=[1 c4 3 0.2]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::Index(1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(2) + .with_target(Target::Index(48)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_int(3) + .with_target(Target::Index(3)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(4) + .with_target(Target::Index(0)), + ]] + ); + + assert_eq!( + Cycle::from("[1 2 3 4]:long=[1 _ ~ 0.2]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::Named("long".into(), Some(1.0))), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(2) + .with_target(Target::Named("long".into(), Some(1.0))), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(4) + .with_target(Target::Named("long".into(), Some(0.2))), + ]] + ); + + assert_cycles( + "[1 2 3 4 5 6 7 8]/4:d=<.1 .2 .3 .4>:v=[<.3 .2 .1>*2/3]", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(1) + .with_targets(vec![ + Target::Named("d".into(), Some(0.1)), + Target::Named("v".into(), Some(0.3)), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(2) + .with_targets(vec![ + Target::Named("d".into(), Some(0.1)), + Target::Named("v".into(), Some(0.3)), + ]), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(3) + .with_targets(vec![ + Target::Named("d".into(), Some(0.2)), + Target::Named("v".into(), Some(0.3)), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(4) + .with_targets(vec![ + Target::Named("d".into(), Some(0.2)), + Target::Named("v".into(), Some(0.2)), + ]), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(5) + .with_targets(vec![ + Target::Named("d".into(), Some(0.3)), + Target::Named("v".into(), Some(0.2)), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(6) + .with_targets(vec![ + Target::Named("d".into(), Some(0.3)), + Target::Named("v".into(), Some(0.2)), + ]), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(7) + .with_targets(vec![ + Target::Named("d".into(), Some(0.4)), + Target::Named("v".into(), Some(0.1)), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(8) + .with_targets(vec![ + Target::Named("d".into(), Some(0.4)), + Target::Named("v".into(), Some(0.1)), + ]), + ]], + ], + )?; + + assert_cycle_equality( + "[1 2 3 4]:v=[0.2 0.3 0.4 p.8]", + "[1 2 3 4]:[v0.2 v0.3 v0.4 p.8]", + )?; + + assert_cycle_equality("[1 2 3 4]:g=[0.1 10.]", "[1 2 3 4]:[g0.1 g10.]")?; + + Ok(()) +} From 9c640cc642a36510ba360404f2a0abb18350c865 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sat, 7 Feb 2026 18:43:11 +0000 Subject: [PATCH 02/21] make replicate and weight dynamic --- src/tidal/cycle.rs | 213 ++++++++++++++++++++++++--------------- src/tidal/cycle/tests.rs | 7 ++ 2 files changed, 138 insertions(+), 82 deletions(-) diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index d268c75..ad0cf09 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -285,6 +285,8 @@ enum Step { Stack(Stack), Choices(Choices), SpeedExpression(SpeedExpression), + ReplicateExpression(ReplicateExpression), + WeightExpression(WeightExpression), TargetExpression(TargetExpression), Degrade(Degrade), Bjorklund(Bjorklund), @@ -302,6 +304,8 @@ impl Step { Step::Choices(cs) => cs.choices.iter().collect(), Step::Stack(st) => st.stack.iter().collect(), Step::SpeedExpression(e) => vec![&e.left, &e.right], + Step::WeightExpression(e) => vec![&e.left], + Step::ReplicateExpression(e) => vec![&e.left], Step::Degrade(e) => vec![&e.step], Step::TargetExpression(e) => vec![&e.left, &e.right], Step::Bjorklund(b) => { @@ -313,7 +317,6 @@ impl Step { } Step::Static(s) => match s { Static::Repeat => vec![], - Static::Expression(e) => vec![&e.left], Static::Range(_) => vec![], }, } @@ -325,6 +328,8 @@ impl Step { Step::Alternating(a) => a.steps.iter_mut().collect(), Step::Subdivision(sd) => sd.steps.iter_mut().collect(), Step::SpeedExpression(e) => vec![&mut e.left], + Step::WeightExpression(e) => vec![&mut e.left], + Step::ReplicateExpression(e) => vec![&mut e.left], Step::Choices(cs) => cs.choices.iter_mut().collect(), Step::Polymeter(pm) => pm.steps.as_mut().inner_steps_mut(), Step::Stack(st) => st.stack.iter_mut().collect(), @@ -334,7 +339,6 @@ impl Step { Step::Static(s) => match s { Static::Repeat => vec![], Static::Range(_) => vec![], - Static::Expression(_) => vec![], }, } } @@ -352,6 +356,13 @@ impl Step { } } + fn length(&self) -> Fraction { + match self { + Step::ReplicateExpression(re) => re.count.to_fraction().unwrap_or(Fraction::ONE), + _ => Fraction::ONE, + } + } + fn rest() -> Self { Self::Single(Single::default()) } @@ -371,7 +382,6 @@ impl Step { #[derive(Clone, Debug, PartialEq)] enum Static { - Expression(StaticExpression), Range(Range), Repeat, } @@ -408,12 +418,17 @@ struct Polymeter { } impl Polymeter { - fn length(&self) -> usize { + fn length(&self) -> Fraction { if let Step::Subdivision(s) = self.steps.as_ref() { - s.steps.len() + let l = s + .steps + .iter() + .fold(Fraction::ZERO, |a: Fraction, v: &Step| a + v.length()); + l } else { - 1 - } // unreachable + // unreachable + Fraction::ONE + } } } @@ -433,16 +448,11 @@ enum SpeedOp { Slow(), // / } -#[derive(Clone, Debug, PartialEq)] -enum StaticOp { - Replicate(), // ! - Weight(), // @ -} - #[derive(Clone, Debug, PartialEq)] enum Operator { - Static(StaticOp), Speed(SpeedOp), + Replicate(), // ! + Weight(), // @ Target(), // : Bjorklund(), // (p,s,r) Degrade(), // ? @@ -452,8 +462,8 @@ impl Operator { fn parse(pair: Pair) -> Result { match pair.as_rule() { Rule::op_degrade => Ok(Self::Degrade()), - Rule::op_replicate => Ok(Self::Static(StaticOp::Replicate())), - Rule::op_weight => Ok(Self::Static(StaticOp::Weight())), + Rule::op_replicate => Ok(Self::Replicate()), + Rule::op_weight => Ok(Self::Weight()), Rule::op_fast => Ok(Self::Speed(SpeedOp::Fast())), Rule::op_slow => Ok(Self::Speed(SpeedOp::Slow())), Rule::op_target => Ok(Self::Target()), @@ -471,10 +481,15 @@ struct SpeedExpression { } #[derive(Clone, Debug, PartialEq)] -struct StaticExpression { - op: StaticOp, +struct WeightExpression { + left: Box, + weight: Value, +} + +#[derive(Clone, Debug, PartialEq)] +struct ReplicateExpression { left: Box, - right: Value, + count: Value, } #[derive(Clone, Debug, PartialEq)] @@ -678,6 +693,10 @@ impl Value { Value::Name(_n) => None, } } + + fn to_fraction(&self) -> Option { + self.to_float().and_then(Fraction::from_f64) + } } impl Span { @@ -893,6 +912,22 @@ impl Events { } } + fn set_length(&mut self, length: Fraction) { + match self { + Events::Single(s) => s.length = length, + Events::Multi(m) => m.length = length, + Events::Poly(p) => p.length = length, + } + } + + fn first(&self) -> Option { + match self { + Events::Single(s) => Some(s.clone()), + Events::Multi(m) => m.events.first().and_then(Self::first), + Events::Poly(p) => p.channels.first().and_then(Self::first), + } + } + fn get_span(&self) -> Span { match self { Events::Single(s) => s.span.clone(), @@ -1292,31 +1327,6 @@ impl CycleParser { let repeat = steps.last().cloned().unwrap_or(Step::rest()); steps.push(repeat) } - Static::Expression(e) => match e.op { - StaticOp::Replicate() => { - steps.push(e.left.as_ref().clone()); - if let Some(repeats) = e.right.to_integer() { - if repeats > 0 { - for _i in 1..repeats { - steps.push(e.left.as_ref().clone()) - } - } - } - } - StaticOp::Weight() => { - steps.push(e.left.as_ref().clone()); - if let Some(repeats) = e.right.to_integer() { - if repeats > 0 { - for _i in 1..repeats { - steps.push(Step::Single(Single { - value: Value::Hold, - string: Rc::from("_"), - })) - } - } - } - } - }, Static::Range(r) => { let range = if r.start <= r.end { Box::new(r.start..=r.end) as Box> @@ -1562,22 +1572,33 @@ impl CycleParser { String::from("unreachable: missing right hand side from op_pair, error in grammar!") } - fn static_expression(left: Step, op: StaticOp, op_pair: Pair) -> Result { - let right = if let Some(right_pair) = op_pair.into_inner().next() { - right_pair - .into_inner() - .next() - .ok_or_else(Self::invalid_right_hand) - .and_then(Self::value)? + // TODO allow for a pattern on the right for weight and replicate + // with the current pest setup, this seems impossible if we want to support optional parameter here + // at least it is impossible without major rearrangement of the grammar and parsing + fn weight_expression(left: Step, op_pair: Pair) -> Result { + let weight = if let Some(pair) = op_pair.into_inner().next() { + Self::value(pair)? } else { - Value::Integer(2) + Value::Float(2.0) }; - Ok(Step::Static(Static::Expression(StaticExpression { + Ok(Step::WeightExpression(WeightExpression { left: Box::new(left), - right, - op, - }))) + weight, + })) + } + + fn replicate_expression(left: Step, op_pair: Pair) -> Result { + let count = if let Some(pair) = op_pair.into_inner().next() { + Self::value(pair)? + } else { + Value::Float(2.0) + }; + + Ok(Step::ReplicateExpression(ReplicateExpression { + left: Box::new(left), + count, + })) } fn degrade_expression(step: Step, op_pair: Pair) -> Result { @@ -1633,7 +1654,8 @@ impl CycleParser { // Loop over operators and parameters, creating a nested expression if multiple pairs are present for op_pair in inner { left = match Operator::parse(op_pair.clone())? { - Operator::Static(op) => Self::static_expression(left, op, op_pair)?, + Operator::Replicate() => Self::replicate_expression(left, op_pair)?, + Operator::Weight() => Self::weight_expression(left, op_pair)?, Operator::Speed(op) => Self::speed_expression(left, op, op_pair)?, Operator::Target() => Self::target_expression(left, op_pair)?, Operator::Degrade() => Self::degrade_expression(left, op_pair)?, @@ -1733,33 +1755,25 @@ impl Cycle { // helper to calculate the right multiplier for polymeter and speed expressions fn step_multiplier(step: &Step, value: &Value) -> Fraction { match step { - Step::Polymeter(pm) => { - let length = pm.length() as f64; - let count = value.to_float().unwrap_or(0.0); - Fraction::from_f64(count).unwrap_or(Fraction::ZERO) - / Fraction::from_f64(length).unwrap_or(Fraction::ONE) - } + Step::Polymeter(pm) => value.to_fraction().unwrap_or(Fraction::ZERO) / pm.length(), Step::SpeedExpression(e) => match e.op { - SpeedOp::Fast() => { - if let Some(right) = value.to_float() { - Fraction::from_f64(right).unwrap_or(Fraction::ZERO) - } else { - Fraction::ZERO - } - } - SpeedOp::Slow() => { - if let Some(right) = value.to_float() { - if right != 0.0 { - Fraction::from_f64(1.0 / right).unwrap_or(Fraction::ZERO) + SpeedOp::Fast() => value.to_fraction().unwrap_or(Fraction::ZERO), + SpeedOp::Slow() => value + .to_float() + .and_then(|div| { + if div != 0.0 { + Fraction::from_f64(1.0 / div) } else { - Fraction::ZERO + None } - } else { - Fraction::from(0) - } - } + }) + .unwrap_or(Fraction::ZERO), }, _ => Fraction::from(1), + // _ => value + // .to_float() + // .and_then(Fraction::from_f64) + // .unwrap_or(Fraction::ONE), } } @@ -1968,6 +1982,42 @@ impl Cycle { }) } } + Step::WeightExpression(we) => { + // TODO if the right side could be a step like alternating, we could evaluate like so + // let right_events = Self::output(we.weight.as_ref(), state, cycle, limit, overlap)?; + // let weight = right_events + // .first() + // .and_then(|e| e.value.to_fraction()) + // .unwrap_or(Fraction::ONE); + + let mut events = Self::output(we.left.as_ref(), state, cycle, limit, overlap)?; + let weight = we.weight.to_fraction().unwrap_or(Fraction::ONE); + events.set_length(weight); + events + } + Step::ReplicateExpression(we) => { + let count = we.count.to_float().unwrap_or(2.0); + let ceil = count.ceil(); + let len = if ceil == 0.0 { 1 } else { ceil as usize }; + let mult = count / ceil; + + let steps = vec![we.left.as_ref().clone(); len]; + let sub = Step::subdivision(steps); + + // TODO cache this if the right side is static + let step = Step::SpeedExpression(SpeedExpression { + op: SpeedOp::Fast(), + left: Box::from(sub), + right: Box::from(Step::Single(Single { + value: Value::Float(mult), + string: Rc::from(""), + })), + }); + + let mut events = Self::output(&step, state, cycle, limit, overlap)?; + events.set_length(Fraction::from_f64(count).unwrap_or(Fraction::ONE)); + events + } Step::Alternating(a) => { if a.steps.is_empty() { Events::empty() @@ -2111,13 +2161,12 @@ impl Cycle { Step::Choices(cs) => format!("Choices |{}|", cs.choices.len()), Step::Stack(st) => format!("Stack ({})", st.stack.len()), Step::SpeedExpression(e) => format!("Speed Expression {:?}", e.op), + Step::WeightExpression(we) => format!("Weight Expression {:?}", we.weight), + Step::ReplicateExpression(we) => format!("Replicate Expression {:?}", we.count), Step::TargetExpression(_e) => String::from("Target Expression"), Step::Static(s) => match s { Static::Repeat => "Repeat".to_string(), Static::Range(r) => format!("Range {}..{}", r.start, r.end), - Static::Expression(e) => { - format!("Static Expression {:?} : {:?}", e.op, e.right) - } }, Step::Degrade(d) => format!("Degrade ? {:?}", d.chance), Step::Bjorklund(_b) => format!("Bjorklund {}", ""), diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index 8d94768..bc6611d 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -44,6 +44,13 @@ fn span() -> Result<(), String> { Ok(()) } +#[test] +fn weight_and_replicate() -> Result<(), String> { + let mut cycle = Cycle::from("a!1.5 b")?; + let events = cycle.generate()?; + Ok(()) +} + #[test] fn parse() -> Result<(), String> { assert!(Cycle::from("a b c [d").is_err()); From def6f01d337a929cd9f44a3301430ab77c455f65 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sun, 8 Feb 2026 15:36:50 +0000 Subject: [PATCH 03/21] allow single value for weight, replicate and degrade --- src/tidal/cycle.pest | 6 +++--- src/tidal/cycle.rs | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index b722fd3..fe7fe3e 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -59,9 +59,9 @@ parameter = _{ single | group } single_parameter = _{ single } /// static operators -op_replicate = ${ "!" ~ number } -op_weight = ${ "@" ~ number? } -op_degrade = ${ "?" ~ number? } +op_replicate = ${ "!" ~ single_parameter } +op_weight = ${ "@" ~ single_parameter? } +op_degrade = ${ "?" ~ single_parameter? } /// dynamic operators op_fast = { "*" ~ parameter } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index ad0cf09..231886c 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -1231,7 +1231,7 @@ impl CycleParser { /// recursively parse a pair as a Step fn step(pair: Pair) -> Result { match pair.as_rule() { - Rule::single => Self::single(pair), + Rule::single => Ok(Step::Single(Self::single(pair)?)), Rule::repeat => Ok(Step::Static(Static::Repeat)), Rule::subdivision | Rule::mini => Self::group(pair, Step::subdivision), Rule::alternating => Self::group(pair, Step::alternating), @@ -1306,16 +1306,16 @@ impl CycleParser { } } - fn single(pair: Pair) -> Result { + fn single(pair: Pair) -> Result { pair.clone() .into_inner() .next() .ok_or_else(|| format!("empty single {}", pair)) .and_then(|value_pair| { - Ok(Step::Single(Single { + Ok(Single { string: Rc::from(value_pair.as_str()), value: Self::value(value_pair)?, - })) + }) }) } @@ -1577,7 +1577,7 @@ impl CycleParser { // at least it is impossible without major rearrangement of the grammar and parsing fn weight_expression(left: Step, op_pair: Pair) -> Result { let weight = if let Some(pair) = op_pair.into_inner().next() { - Self::value(pair)? + Self::single(pair)?.value } else { Value::Float(2.0) }; @@ -1590,7 +1590,7 @@ impl CycleParser { fn replicate_expression(left: Step, op_pair: Pair) -> Result { let count = if let Some(pair) = op_pair.into_inner().next() { - Self::value(pair)? + Self::single(pair)?.value } else { Value::Float(2.0) }; @@ -1607,7 +1607,8 @@ impl CycleParser { .into_inner() .next() .ok_or_else(Self::invalid_right_hand) - .and_then(Self::value)? + .and_then(Self::single)? + .value } else { Value::Float(0.5) }; From 21d9913ceac0946e5b223d59aee7af2282b7566f Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sun, 8 Feb 2026 17:42:50 +0000 Subject: [PATCH 04/21] variable values in cycle --- src/lib.rs | 3 +- src/tidal.rs | 2 +- src/tidal/cycle.pest | 4 +- src/tidal/cycle.rs | 444 ++++++++++++++++++++++++--------------- src/tidal/cycle/tests.rs | 13 ++ 5 files changed, 294 insertions(+), 172 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1153b96..7356010 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,8 @@ pub use crate::{ rhythm::{Rhythm, RhythmEvent}, sequence::Sequence, tidal::{ - Cycle, Event as CycleEvent, Span as CycleSpan, Target as CycleTarget, Value as CycleValue, + Constant as CycleValue, Cycle, Event as CycleEvent, Span as CycleSpan, + Target as CycleTarget, }, time::{ BeatTimeBase, BeatTimeStep, ExactSampleTime, SampleTime, SampleTimeBase, SampleTimeDisplay, diff --git a/src/tidal.rs b/src/tidal.rs index 2b73d0b..6e11783 100644 --- a/src/tidal.rs +++ b/src/tidal.rs @@ -1,4 +1,4 @@ //! Tidal mini parser and event generator, used as `Emitter`. mod cycle; -pub use cycle::{Cycle, Event, Span, Target, Value}; +pub use cycle::{Constant, Cycle, Event, Span, Target}; diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index fe7fe3e..eecba1d 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -21,6 +21,8 @@ target = ${ ("#" ~ integer) | (ASCII_ALPHA ~ float) } target_name = ${ "#" | name } target_assign = { target_name ~ "=" ~ parameter } +variable = ${ "$" ~ name} + /// chord as pitch with mode string, separated via "'" mode = ${ (ASCII_ALPHANUMERIC | "#" | "-" | "+" | "^")+ } chord = ${ pitch ~ "'" ~ mode } @@ -37,7 +39,7 @@ name = @{ ASCII_ALPHANUMERIC ~ (ASCII_ALPHANUMERIC | "_")* } repeat = { "!" } /// possible literals for single steps -single = { hold | rest | chord | target | pitch | number | name } +single = { hold | rest | chord | target | pitch | number | name | variable } choice_op = {"|"} stack_op = {","} diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 231886c..8787bde 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{collections::HashMap, rc::Rc}; #[cfg(test)] use std::fmt::Display; @@ -20,6 +20,8 @@ const OVERFLOW_ERROR: &str = "Internal error: integer overflow in cycle"; // ------------------------------------------------------------------------------------------------- +type Vars = HashMap, Constant>; + /// Tidal cycle mini notation parser and event generator. #[derive(Debug, Clone, PartialEq)] pub struct Cycle { @@ -29,6 +31,7 @@ pub struct Cycle { source: Option, seed: Option, state: CycleState, + vars: Option, } impl Cycle { @@ -58,6 +61,7 @@ impl Cycle { let seed = None; let source = None; let event_limit = Self::EVENT_LIMIT_DEFAULT; + let vars = None; let cycle = Self { input, seed, @@ -65,6 +69,7 @@ impl Cycle { root, state, event_limit, + vars, }; #[cfg(test)] { @@ -112,6 +117,28 @@ impl Cycle { } } + /// Rebuild/configure cycle to use an table to map variables from + pub fn with_vars(self, vars: Vars) -> Self { + Self { + vars: Some(vars), + ..self + } + } + + pub fn update_vars(&mut self, vars: Vars) { + self.vars = Some(vars) + } + + pub fn set_var(&mut self, name: &str, constant: Constant) { + if let Some(vars) = self.vars.as_mut() { + vars.insert(name.into(), constant); + } else { + let mut vars = HashMap::new(); + vars.insert(name.into(), constant); + self.vars = Some(vars); + } + } + /// Check if a cycle may give different outputs between cycles. pub fn is_stateful(&self) -> bool { // TODO improve: * and / can change the output, <1> does not etc.. @@ -133,7 +160,14 @@ impl Cycle { if let Some(seed) = self.seed { self.state.rng = Xoshiro256PlusPlus::seed_from_u64(seed.wrapping_add(cycle as u64)); } - let mut events = Self::output(&self.root, &mut self.state, cycle, self.event_limit, false)?; + let mut events = Self::output( + &self.root, + &mut self.state, + cycle, + self.event_limit, + false, + self.vars.as_ref(), + )?; self.state.iteration += 1; events.transform_spans(&Span::default()); Ok(events.export()) @@ -156,7 +190,7 @@ impl Cycle { pub struct Event { length: Fraction, span: Span, - value: Value, + value: Constant, string: Rc, targets: Vec, } @@ -166,7 +200,7 @@ impl Default for Event { Self { length: Fraction::default(), span: Span::default(), - value: Value::default(), + value: Constant::default(), string: Rc::from("~"), targets: vec![], } @@ -175,7 +209,7 @@ impl Default for Event { impl Event { /// The step's original parsed value. - pub fn value(&self) -> &Value { + pub fn value(&self) -> &Constant { &self.value } @@ -230,7 +264,7 @@ impl Span { /// Possible types of values that are emitted by a [`Cycle`]. #[derive(Clone, Debug, Default, PartialEq)] -pub enum Value { +pub enum Constant { #[default] Rest, Hold, @@ -356,9 +390,9 @@ impl Step { } } - fn length(&self) -> Fraction { + fn length(&self, vars: Option<&Vars>) -> Fraction { match self { - Step::ReplicateExpression(re) => re.count.to_fraction().unwrap_or(Fraction::ONE), + Step::ReplicateExpression(re) => re.count.to_fraction(vars).unwrap_or(Fraction::ONE), _ => Fraction::ONE, } } @@ -386,6 +420,59 @@ enum Static { Repeat, } +#[derive(Clone, Debug, PartialEq)] +enum Value { + Constant(Constant), + Variable(Rc), +} + +impl Default for Value { + fn default() -> Self { + Self::Constant(Constant::default()) + } +} + +impl Value { + fn to_float(&self, vars: Option<&Vars>) -> Option { + match self { + Self::Constant(l) => l.to_float(), + Self::Variable(_) => self.to_literal(vars).to_float(), + } + } + fn to_integer(&self, vars: Option<&Vars>) -> Option { + match self { + Self::Constant(l) => l.to_integer(), + Self::Variable(_) => self.to_literal(vars).to_integer(), + } + } + fn to_chance(&self, vars: Option<&Vars>) -> Option { + match self { + Self::Constant(l) => l.to_chance(), + Self::Variable(_) => self.to_literal(vars).to_chance(), + } + } + fn to_fraction(&self, vars: Option<&Vars>) -> Option { + match self { + Self::Constant(l) => l.to_fraction(), + Self::Variable(_) => self.to_literal(vars).to_fraction(), + } + } + fn to_target(&self, string: &Rc, vars: Option<&Vars>) -> Option { + match self { + Self::Constant(l) => l.to_target(string), + Self::Variable(_) => self.to_literal(vars).to_target(string), + } + } + fn to_literal(&self, vars: Option<&Vars>) -> Constant { + match self { + Self::Constant(l) => l.clone(), + Self::Variable(rc) => vars + .and_then(|vars| vars.get(rc).cloned()) + .unwrap_or(Constant::Name(Rc::clone(rc))), + } + } +} + #[derive(Clone, Debug, PartialEq)] struct Single { value: Value, @@ -395,7 +482,7 @@ struct Single { impl Default for Single { fn default() -> Self { Single { - value: Value::Rest, + value: Value::Constant(Constant::Rest), string: Rc::from("~"), } } @@ -418,12 +505,12 @@ struct Polymeter { } impl Polymeter { - fn length(&self) -> Fraction { + fn length(&self, vars: Option<&Vars>) -> Fraction { if let Step::Subdivision(s) = self.steps.as_ref() { let l = s .steps .iter() - .fold(Fraction::ZERO, |a: Fraction, v: &Step| a + v.length()); + .fold(Fraction::ZERO, |a: Fraction, v: &Step| a + v.length(vars)); l } else { // unreachable @@ -521,19 +608,6 @@ struct Range { // ------------------------------------------------------------------------------------------------- impl Target { - fn parse(value: &Value, value_string: &Rc) -> Option { - match value { - Value::Rest | Value::Hold => None, - Value::Integer(i) => Some(Self::from_index(*i)), - Value::Name(name) => Some(Self::from_name(Rc::clone(name))), - Value::Target(t) => Some(t.clone()), - Value::Float(_) | Value::Pitch(_) | Value::Chord(_, _) => { - // pass unexpected values as raw string and let clients deal with conversions or errors - Some(Self::from_name(Rc::clone(value_string))) - } - } - } - fn from_index(index: i32) -> Self { Self::Index(index) } @@ -621,7 +695,7 @@ impl Display for Pitch { } } -impl Value { +impl Constant { fn parse_integer(str: &str) -> Result { if let Some(hex) = str.strip_prefix("0x").or(str.strip_prefix("0X")) { i32::from_str_radix(hex, 16).map_err(|err| err.to_string()) @@ -638,65 +712,78 @@ impl Value { str.parse::().map_err(|err| err.to_string()) } - fn from_float(str: &str) -> Result { + fn from_float(str: &str) -> Result { Self::parse_float(str).map(Self::Float) } - fn from_integer(str: &str) -> Result { + fn from_integer(str: &str) -> Result { Self::parse_integer(str).map(Self::Integer) } fn to_integer(&self) -> Option { match &self { - Value::Rest => None, - Value::Hold => None, - Value::Integer(i) => Some(*i), - Value::Float(f) => Some(*f as i32), - Value::Pitch(n) => Some(n.midi_note() as i32), - Value::Chord(p, _m) => Some(p.midi_note() as i32), - Value::Target(t) => match t { + Constant::Rest => None, + Constant::Hold => None, + Constant::Integer(i) => Some(*i), + Constant::Float(f) => Some(*f as i32), + Constant::Pitch(n) => Some(n.midi_note() as i32), + Constant::Chord(p, _m) => Some(p.midi_note() as i32), + Constant::Target(t) => match t { Target::Index(i) => Some(*i), Target::Named(_, v) => v.map(|f| f as i32), }, - Value::Name(_n) => None, + Constant::Name(_n) => None, } } fn to_float(&self) -> Option { match &self { - Value::Rest => None, - Value::Hold => None, - Value::Integer(i) => Some(*i as f64), - Value::Float(f) => Some(*f), - Value::Pitch(n) => Some(n.midi_note() as f64), - Value::Chord(n, _m) => Some(n.midi_note() as f64), - Value::Target(t) => match t { + Constant::Rest => None, + Constant::Hold => None, + Constant::Integer(i) => Some(*i as f64), + Constant::Float(f) => Some(*f), + Constant::Pitch(n) => Some(n.midi_note() as f64), + Constant::Chord(n, _m) => Some(n.midi_note() as f64), + Constant::Target(t) => match t { Target::Index(i) => Some(*i as f64), Target::Named(_, v) => *v, }, - Value::Name(_n) => None, + Constant::Name(_n) => None, } } fn to_chance(&self) -> Option { match &self { - Value::Rest => None, - Value::Hold => None, - Value::Integer(i) => Some((*i as f64).clamp(0.0, 100.0) / 100.0), - Value::Float(f) => Some(f.clamp(0.0, 1.0)), - Value::Pitch(p) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), - Value::Chord(p, _m) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), - Value::Target(t) => match t { + Constant::Rest => None, + Constant::Hold => None, + Constant::Integer(i) => Some((*i as f64).clamp(0.0, 100.0) / 100.0), + Constant::Float(f) => Some(f.clamp(0.0, 1.0)), + Constant::Pitch(p) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), + Constant::Chord(p, _m) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), + Constant::Target(t) => match t { Target::Index(i) => Some(*i as f64), Target::Named(_, v) => v.map(|f| f.clamp(0.0, 1.0)), }, - Value::Name(_n) => None, + Constant::Name(_n) => None, } } fn to_fraction(&self) -> Option { self.to_float().and_then(Fraction::from_f64) } + + fn to_target(&self, string: &Rc) -> Option { + match self { + Self::Rest | Constant::Hold => None, + Self::Integer(i) => Some(Target::from_index(*i)), + Self::Name(name) => Some(Target::from_name(Rc::clone(name))), + Self::Target(t) => Some(t.clone()), + Self::Float(_) | Constant::Pitch(_) | Constant::Chord(_, _) => { + // pass unexpected values as raw string and let clients deal with conversions or errors + Some(Target::from_name(Rc::clone(string))) + } + } + } } impl Span { @@ -770,7 +857,7 @@ impl Event { end: start + length, }, string: Rc::from("~"), - value: Value::Rest, + value: Constant::Rest, targets: vec![], } } @@ -779,7 +866,7 @@ impl Event { fn with_note(&self, note: u8, octave: u8) -> Self { let pitch = Pitch { note, octave }; Self { - value: Value::Pitch(pitch.clone()), + value: Constant::Pitch(pitch.clone()), string: Rc::from(pitch.to_string()), ..self.clone() } @@ -789,7 +876,7 @@ impl Event { fn with_chord(&self, note: u8, octave: u8, mode: &str) -> Self { let pitch = Pitch { note, octave }; Self { - value: Value::Chord(pitch.clone(), Rc::from(mode)), + value: Constant::Chord(pitch.clone(), Rc::from(mode)), string: Rc::from(format!("{}'{}", pitch, mode)), ..self.clone() } @@ -798,7 +885,7 @@ impl Event { #[cfg(test)] fn with_int(&self, i: i32) -> Self { Self { - value: Value::Integer(i), + value: Constant::Integer(i), string: Rc::from(i.to_string()), ..self.clone() } @@ -807,7 +894,7 @@ impl Event { #[cfg(test)] fn with_name(&self, n: &'static str) -> Self { Self { - value: Value::Name(Rc::from(n)), + value: Constant::Name(Rc::from(n)), string: Rc::from(n.to_string()), ..self.clone() } @@ -816,7 +903,7 @@ impl Event { #[cfg(test)] fn with_float(&self, f: f64) -> Self { Self { - value: Value::Float(f), + value: Constant::Float(f), string: Rc::from(f.to_string()), ..self.clone() } @@ -891,7 +978,7 @@ impl Events { length: Fraction::ONE, span: Span::default(), string: Rc::from("~"), - value: Value::Rest, + value: Constant::Rest, targets: vec![], }) } @@ -920,14 +1007,6 @@ impl Events { } } - fn first(&self) -> Option { - match self { - Events::Single(s) => Some(s.clone()), - Events::Multi(m) => m.events.first().and_then(Self::first), - Events::Poly(p) => p.channels.first().and_then(Self::first), - } - } - fn get_span(&self) -> Span { match self { Events::Single(s) => s.span.clone(), @@ -1120,11 +1199,11 @@ impl Events { // filter out holds while extending preceding events fn merge_holds(events: &mut Vec) { - if events.iter().any(|e| e.value == Value::Hold) { + if events.iter().any(|e| e.value == Constant::Hold) { let mut result: Vec = Vec::with_capacity(events.len()); for e in events.iter() { match e.value { - Value::Hold => { + Constant::Hold => { if let Some(last) = result.last_mut() { last.extend(e) } @@ -1140,14 +1219,14 @@ impl Events { // so any remaining rest can be converted to a note-off later // rests at the beginning of a pattern also get dropped fn merge_rests(events: &mut Vec) { - if events.iter().any(|e| e.value == Value::Rest) { + if events.iter().any(|e| e.value == Constant::Rest) { let mut result: Vec = Vec::with_capacity(events.len()); for e in events.iter() { match e.value { - Value::Rest => { + Constant::Rest => { if let Some(last) = result.last_mut() { match last.value { - Value::Rest => last.extend(e), + Constant::Rest => last.extend(e), _ => result.push(e.clone()), } } @@ -1248,61 +1327,69 @@ impl CycleParser { /// parse a pair inside a single as a value fn value(pair: Pair) -> Result { - match pair.as_rule() { - Rule::integer => Value::from_integer(pair.as_str()), - Rule::float => Value::from_float(pair.as_str()), - Rule::number => { - if let Some(n) = pair.into_inner().next() { - match n.as_rule() { - Rule::integer => Value::from_integer(n.as_str()), - Rule::float => Value::from_float(n.as_str()), - _ => Err(format!("unrecognized number\n{:?}", n)), + if pair.as_rule() == Rule::variable { + pair.into_inner() + .next() + .ok_or("error in grammar, missing variable name".to_string()) + .map(|name_pair| Value::Variable(Rc::from(name_pair.as_str()))) + } else { + let constant = match pair.as_rule() { + Rule::integer => Constant::from_integer(pair.as_str()), + Rule::float => Constant::from_float(pair.as_str()), + Rule::number => { + if let Some(n) = pair.into_inner().next() { + match n.as_rule() { + Rule::integer => Constant::from_integer(n.as_str()), + Rule::float => Constant::from_float(n.as_str()), + _ => Err(format!("unrecognized number\n{:?}", n)), + } + } else { + Err("empty single".to_string()) } - } else { - Err("empty single".to_string()) } - } - Rule::hold => Ok(Value::Hold), - Rule::rest => Ok(Value::Rest), - Rule::pitch => Ok(Value::Pitch(Pitch::parse(pair))), - Rule::chord => { - let mut pitch = Pitch { note: 0, octave: 4 }; - let mut mode = ""; - for p in pair.into_inner() { - match p.as_rule() { - Rule::pitch => { - pitch = Pitch::parse(p); - } - Rule::mode => { - mode = p.as_str(); + Rule::hold => Ok(Constant::Hold), + Rule::rest => Ok(Constant::Rest), + Rule::pitch => Ok(Constant::Pitch(Pitch::parse(pair))), + Rule::chord => { + let mut pitch = Pitch { note: 0, octave: 4 }; + let mut mode = ""; + for p in pair.into_inner() { + match p.as_rule() { + Rule::pitch => { + pitch = Pitch::parse(p); + } + Rule::mode => { + mode = p.as_str(); + } + _ => (), } - _ => (), } + Ok(Constant::Chord(pitch, Rc::from(mode))) } - Ok(Value::Chord(pitch, Rc::from(mode))) - } - Rule::target => { - let name = pair.as_str().get(0..1).ok_or(format!( - "error in grammar, missing target key in pair\n{:?}", - pair - ))?; - let value = pair.clone().into_inner().next().ok_or(format!( - "error in grammar, missing target value in pair\n{:?}", - pair - ))?; - - match name.as_bytes() { - b"#" => Ok(Value::Target(Target::Index(Value::parse_integer( - value.as_str(), - )?))), - _ => Ok(Value::Target(Target::Named( - Rc::from(name), - Some(Value::parse_float(value.as_str())?), - ))), + Rule::target => { + let name = pair.as_str().get(0..1).ok_or(format!( + "error in grammar, missing target key in pair\n{:?}", + pair + ))?; + let value = pair.clone().into_inner().next().ok_or(format!( + "error in grammar, missing target value in pair\n{:?}", + pair + ))?; + + match name.as_bytes() { + b"#" => Ok(Constant::Target(Target::Index(Constant::parse_integer( + value.as_str(), + )?))), + _ => Ok(Constant::Target(Target::Named( + Rc::from(name), + Some(Constant::parse_float(value.as_str())?), + ))), + } } - } - Rule::name => Ok(Value::Name(Rc::from(pair.as_str()))), - _ => Err(format!("unrecognized target value\n{:?}", pair)), + Rule::name => Ok(Constant::Name(Rc::from(pair.as_str()))), + _ => Err(format!("unrecognized target value\n{:?}", pair)), + }?; + Ok(Value::Constant(constant)) } } @@ -1335,7 +1422,7 @@ impl CycleParser { }; for i in range { steps.push(Step::Single(Single { - value: Value::Integer(i), + value: Value::Constant(Constant::Integer(i)), string: Rc::from(i.to_string()), })) } @@ -1499,7 +1586,7 @@ impl CycleParser { if stack.len() > 1 && count > 0 { let count = Step::Single(Single { - value: Value::Integer(count as i32), + value: Value::Constant(Constant::Integer(count as i32)), string: Rc::from(count.to_string()), }); // if there is a stack but no count, the first section will determine the count of the rest @@ -1579,7 +1666,7 @@ impl CycleParser { let weight = if let Some(pair) = op_pair.into_inner().next() { Self::single(pair)?.value } else { - Value::Float(2.0) + Value::Constant(Constant::Float(2.0)) }; Ok(Step::WeightExpression(WeightExpression { @@ -1592,7 +1679,7 @@ impl CycleParser { let count = if let Some(pair) = op_pair.into_inner().next() { Self::single(pair)?.value } else { - Value::Float(2.0) + Value::Constant(Constant::Float(2.0)) }; Ok(Step::ReplicateExpression(ReplicateExpression { @@ -1610,7 +1697,7 @@ impl CycleParser { .and_then(Self::single)? .value } else { - Value::Float(0.5) + Value::Constant(Constant::Float(0.5)) }; Ok(Step::Degrade(Degrade { @@ -1677,19 +1764,22 @@ impl CycleParser { let mut pattern = Self::step(p)?; let mut key = k.into_inner(); if let Some(name) = key.next() { + // FIX parse variables for targets pattern.mutate_singles(&mut |single: &mut Single| { - if let Some(f) = single.value.to_float() { - if !matches!(single.value, Value::Target(_)) { - single.value = - Value::Target(Target::Named(Rc::from(name.as_str()), Some(f))); + if let Some(f) = single.value.to_float(None) { + if !matches!(single.value, Value::Constant(Constant::Target(_))) { + single.value = Value::Constant(Constant::Target(Target::Named( + Rc::from(name.as_str()), + Some(f), + ))); } } }); } else { pattern.mutate_singles(&mut |single: &mut Single| { - if let Some(i) = single.value.to_integer() { - if !matches!(single.value, Value::Target(_)) { - single.value = Value::Target(Target::Index(i)); + if let Some(i) = single.value.to_integer(None) { + if !matches!(single.value, Value::Constant(Constant::Target(_))) { + single.value = Value::Constant(Constant::Target(Target::Index(i))); } } }); @@ -1715,6 +1805,7 @@ impl Cycle { span: &Span, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { let range = span.whole_range(); let mut cycles = Vec::with_capacity(range.clone().count()); @@ -1723,7 +1814,7 @@ impl Cycle { Fraction::from_u32(cycle).ok_or(OVERFLOW_ERROR)?, Fraction::from_u32(cycle + 1).ok_or(OVERFLOW_ERROR)?, ); - let mut events = Self::output(step, state, cycle, limit, overlap)?; + let mut events = Self::output(step, state, cycle, limit, overlap, vars)?; events.transform_spans(&span); cycles.push(events) } @@ -1743,20 +1834,21 @@ impl Cycle { mult: Fraction, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { let span = Span::new( Fraction::from_u32(cycle).ok_or(OVERFLOW_ERROR)? * mult, Fraction::from_u32(cycle + 1).ok_or(OVERFLOW_ERROR)? * mult, ); - let mut events = Self::output_span(step, state, &span, limit, overlap)?; + let mut events = Self::output_span(step, state, &span, limit, overlap, vars)?; events.normalize_spans(&span); Ok(events) } // helper to calculate the right multiplier for polymeter and speed expressions - fn step_multiplier(step: &Step, value: &Value) -> Fraction { + fn step_multiplier(step: &Step, value: &Constant, vars: Option<&Vars>) -> Fraction { match step { - Step::Polymeter(pm) => value.to_fraction().unwrap_or(Fraction::ZERO) / pm.length(), + Step::Polymeter(pm) => value.to_fraction().unwrap_or(Fraction::ZERO) / pm.length(vars), Step::SpeedExpression(e) => match e.op { SpeedOp::Fast() => value.to_fraction().unwrap_or(Fraction::ZERO), SpeedOp::Slow() => value @@ -1781,7 +1873,7 @@ impl Cycle { // overlay two lists of events and apply the targets from the second to the first fn apply_targets(events: &mut [Event], target_events: &[Event]) { for target_event in target_events.iter() { - if let Some(target) = Target::parse(&target_event.value, &target_event.string) { + if let Some(target) = target_event.value.to_target(&target_event.string) { for event in events.iter_mut() { if event.span.overlaps(&target_event.span) && !{ @@ -1818,8 +1910,9 @@ impl Cycle { state: &mut CycleState, cycle: u32, limit: usize, + vars: Option<&Vars>, ) -> Result<(Vec>, Span), String> { - let mut events = Self::output(step, state, cycle, limit, true)?; + let mut events = Self::output(step, state, cycle, limit, true, vars)?; events.transform_spans(&events.get_span()); let mut channels = vec![]; events.flatten(&mut channels, &mut 0); @@ -1835,12 +1928,13 @@ impl Cycle { cycle: u32, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { match right { // multiply with single values to avoid generating events Step::Single(single) => { - let mut events = Self::output(left, state, cycle, limit, overlap)?; - if let Some(target) = Target::parse(&single.value, &single.string) { + let mut events = Self::output(left, state, cycle, limit, overlap, vars)?; + if let Some(target) = single.value.to_target(&single.string, vars) { events.mutate_events(&mut |event: &mut Event| { if !{ let this = &event; @@ -1855,8 +1949,9 @@ impl Cycle { } _ => { // generate all the events as flat vecs from both the left and right side of the expression - let (left_channels, left_span) = Self::output_flat(left, state, cycle, limit)?; - let (target_channels, _) = Self::output_flat(right, state, cycle, limit)?; + let (left_channels, left_span) = + Self::output_flat(left, state, cycle, limit, vars)?; + let (target_channels, _) = Self::output_flat(right, state, cycle, limit, vars)?; // iterate over channels from both sides to create necessary new stacks if the right side is polyphonic let mut channel_events: Vec = Vec::with_capacity(target_channels.len()); @@ -1889,6 +1984,7 @@ impl Cycle { cycle: u32, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { let left = match step { Step::Polymeter(pm) => pm.steps.as_ref(), @@ -1899,14 +1995,14 @@ impl Cycle { // multiply with single values to avoid generating events Step::Single(single) => { // apply multiplier - let multiplier = Self::step_multiplier(step, &single.value); + let multiplier = Self::step_multiplier(step, &single.value.to_literal(vars), vars); Ok(Self::output_multiplied( - left, state, cycle, multiplier, limit, overlap, + left, state, cycle, multiplier, limit, overlap, vars, )?) } _ => { // generate and flatten the events for the right side of the expression - let events = Self::output(right, state, cycle, limit, overlap)?; + let events = Self::output(right, state, cycle, limit, overlap, vars)?; let channels = events.export(); // extract a float to use as mult from each event and output the step with it @@ -1915,9 +2011,9 @@ impl Cycle { let mut multi_events: Vec = Vec::with_capacity(channel.len()); for event in channel { // apply multiplier - let multiplier = Self::step_multiplier(step, &event.value); + let multiplier = Self::step_multiplier(step, &event.value, vars); let mut partial_events = Self::output_multiplied( - left, state, cycle, multiplier, limit, overlap, + left, state, cycle, multiplier, limit, overlap, vars, )?; // crop and push to multi events partial_events.crop(&event.span, overlap); @@ -1947,6 +2043,7 @@ impl Cycle { cycle: u32, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { let events = match step { Step::Single(s) => { @@ -1961,7 +2058,7 @@ impl Cycle { length: Fraction::ONE, span: Span::default(), string: Rc::clone(&s.string), - value: s.value.clone(), + value: s.value.to_literal(vars), targets: vec![], }) } @@ -1971,7 +2068,7 @@ impl Cycle { } else { let mut events = Vec::with_capacity(sd.steps.len()); for s in &sd.steps { - let e = Self::output(s, state, cycle, limit, overlap)?; + let e = Self::output(s, state, cycle, limit, overlap, vars)?; events.push(e) } @@ -1991,13 +2088,14 @@ impl Cycle { // .and_then(|e| e.value.to_fraction()) // .unwrap_or(Fraction::ONE); - let mut events = Self::output(we.left.as_ref(), state, cycle, limit, overlap)?; - let weight = we.weight.to_fraction().unwrap_or(Fraction::ONE); + let mut events = + Self::output(we.left.as_ref(), state, cycle, limit, overlap, vars)?; + let weight = we.weight.to_fraction(vars).unwrap_or(Fraction::ONE); events.set_length(weight); events } Step::ReplicateExpression(we) => { - let count = we.count.to_float().unwrap_or(2.0); + let count = we.count.to_float(vars).unwrap_or(2.0); let ceil = count.ceil(); let len = if ceil == 0.0 { 1 } else { ceil as usize }; let mult = count / ceil; @@ -2010,12 +2108,12 @@ impl Cycle { op: SpeedOp::Fast(), left: Box::from(sub), right: Box::from(Step::Single(Single { - value: Value::Float(mult), + value: Value::Constant(Constant::Float(mult)), string: Rc::from(""), })), }); - let mut events = Self::output(&step, state, cycle, limit, overlap)?; + let mut events = Self::output(&step, state, cycle, limit, overlap, vars)?; events.set_length(Fraction::from_f64(count).unwrap_or(Fraction::ONE)); events } @@ -2027,7 +2125,7 @@ impl Cycle { let current = cycle % length; a.steps .get(current as usize) - .map(|step| Self::output(step, state, cycle / length, limit, overlap)) + .map(|step| Self::output(step, state, cycle / length, limit, overlap, vars)) .unwrap_or( Ok(Events::empty()), // unreachable )? @@ -2035,18 +2133,24 @@ impl Cycle { } Step::Choices(cs) => { let choice = state.rng.random_range(0..cs.choices.len()); - Self::output(&cs.choices[choice], state, cycle, limit, overlap)? - } - Step::Polymeter(pm) => { - Self::output_with_speed(pm.count.as_ref(), step, state, cycle, limit, overlap)? + Self::output(&cs.choices[choice], state, cycle, limit, overlap, vars)? } + Step::Polymeter(pm) => Self::output_with_speed( + pm.count.as_ref(), + step, + state, + cycle, + limit, + overlap, + vars, + )?, Step::Stack(st) => { if st.stack.is_empty() { Events::empty() } else { let mut channels = Vec::with_capacity(st.stack.len()); for s in &st.stack { - channels.push(Self::output(s, state, cycle, limit, overlap)?) + channels.push(Self::output(s, state, cycle, limit, overlap, vars)?) } Events::maybe_poly(PolyEvents { span: Span::default(), @@ -2056,11 +2160,11 @@ impl Cycle { } } Step::Degrade(d) => { - let mut out = Self::output(d.step.as_ref(), state, cycle, limit, overlap)?; + let mut out = Self::output(d.step.as_ref(), state, cycle, limit, overlap, vars)?; out.mutate_events(&mut |event: &mut Event| { - if let Some(chance) = d.chance.to_chance() { + if let Some(chance) = d.chance.to_chance(vars) { if chance < state.rng.random_range(0.0..1.0) { - event.value = Value::Rest + event.value = Constant::Rest } } }); @@ -2073,9 +2177,10 @@ impl Cycle { cycle, limit, overlap, + vars, )?, Step::SpeedExpression(e) => { - Self::output_with_speed(e.right.as_ref(), step, state, cycle, limit, overlap)? + Self::output_with_speed(e.right.as_ref(), step, state, cycle, limit, overlap, vars)? } Step::Bjorklund(b) => { let mut events = vec![]; @@ -2089,13 +2194,13 @@ impl Cycle { None => None, Some(r) => match r.as_ref() { Step::Single(rotation_single) => { - rotation_single.value.to_integer() + rotation_single.value.to_integer(vars) } _ => None, // TODO support something other than Step::Single as rotation }, }; - if let Some(steps) = steps_single.value.to_integer() { - if let Some(pulses) = pulses_single.value.to_integer() { + if let Some(steps) = steps_single.value.to_integer(vars) { + if let Some(pulses) = pulses_single.value.to_integer(vars) { events.reserve(pulses as usize); let out = Self::output( b.left.as_ref(), @@ -2103,6 +2208,7 @@ impl Cycle { cycle, limit, overlap, + vars, )?; for pulse in euclidean( steps.max(0) as u32, @@ -2153,12 +2259,12 @@ impl Cycle { fn print_steps(step: &Step, level: usize) { let name = match step { Step::Single(s) => match &s.value { - Value::Pitch(_p) => format!("{:?} {}", s.value, s.string), + Value::Constant(Constant::Pitch(_p)) => format!("{:?} {}", s.value, s.string), _ => format!("{:?} {:?}", s.value, s.string), }, Step::Subdivision(sd) => format!("Subdivision [{}]", sd.steps.len()), Step::Alternating(a) => format!("Alternating <{}>", a.steps.len()), - Step::Polymeter(pm) => format!("Polymeter {{{}}}", pm.length()), //, pm.count), + Step::Polymeter(pm) => format!("Polymeter {{{:?}}}", pm.count), Step::Choices(cs) => format!("Choices |{}|", cs.choices.len()), Step::Stack(st) => format!("Stack ({})", st.stack.len()), Step::SpeedExpression(e) => format!("Speed Expression {:?}", e.op), diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index bc6611d..d3c30c3 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -51,6 +51,19 @@ fn weight_and_replicate() -> Result<(), String> { Ok(()) } +#[test] +fn variables() -> Result<(), String> { + let note = Constant::Pitch(Pitch { note: 0, octave: 4 }); + let mut cycle = Cycle::from("a b $note d")?; + cycle.set_var("note", note); + assert_eq!(cycle.generate(), Cycle::from("a b c d")?.generate()); + + // unset variables convert into named + let mut cycle = Cycle::from("a b $note d")?; + assert_eq!(cycle.generate(), Cycle::from("a b note d")?.generate()); + Ok(()) +} + #[test] fn parse() -> Result<(), String> { assert!(Cycle::from("a b c [d").is_err()); From 4623bffbd4685c961e3384825ebb898128ebbf69 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sun, 8 Feb 2026 18:43:16 +0000 Subject: [PATCH 05/21] variable targets in cycle --- src/tidal/cycle.pest | 2 +- src/tidal/cycle.rs | 186 ++++++++++++++++++++++++--------------- src/tidal/cycle/tests.rs | 33 ++++++- 3 files changed, 149 insertions(+), 72 deletions(-) diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index eecba1d..e03b568 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -16,7 +16,7 @@ note = ${ (^"a"|^"b"|^"c"|^"d"|^"e"|^"f"|^"g") } pitch = ${ note ~ mark? ~ octave? ~ !name} /// target properties such as volume "v0.1" (pattrns extension) -target = ${ ("#" ~ integer) | (ASCII_ALPHA ~ float) } +target = ${ ("#" ~ (integer | variable)) | (ASCII_ALPHA ~ (float | variable)) } /// patterns for assigning target keys with pattern on the right side target_name = ${ "#" | name } target_assign = { target_name ~ "=" ~ parameter } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 8787bde..83f621c 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -424,6 +424,7 @@ enum Static { enum Value { Constant(Constant), Variable(Rc), + VariableTarget(Rc, Target), } impl Default for Value { @@ -436,39 +437,55 @@ impl Value { fn to_float(&self, vars: Option<&Vars>) -> Option { match self { Self::Constant(l) => l.to_float(), - Self::Variable(_) => self.to_literal(vars).to_float(), + Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_float(), } } fn to_integer(&self, vars: Option<&Vars>) -> Option { match self { Self::Constant(l) => l.to_integer(), - Self::Variable(_) => self.to_literal(vars).to_integer(), + Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_integer(), } } fn to_chance(&self, vars: Option<&Vars>) -> Option { match self { Self::Constant(l) => l.to_chance(), - Self::Variable(_) => self.to_literal(vars).to_chance(), + Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_chance(), } } fn to_fraction(&self, vars: Option<&Vars>) -> Option { match self { Self::Constant(l) => l.to_fraction(), - Self::Variable(_) => self.to_literal(vars).to_fraction(), + Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_fraction(), } } fn to_target(&self, string: &Rc, vars: Option<&Vars>) -> Option { match self { Self::Constant(l) => l.to_target(string), - Self::Variable(_) => self.to_literal(vars).to_target(string), + Self::Variable(_) | Self::VariableTarget(_, _) => { + self.to_constant(vars).to_target(string) + } } } - fn to_literal(&self, vars: Option<&Vars>) -> Constant { + fn resolve_var(var: Rc, vars: Option<&Vars>) -> Constant { + vars.and_then(|vars| vars.get(&var).cloned()) + .unwrap_or(Constant::Name(Rc::clone(&var))) + } + fn to_constant(&self, vars: Option<&Vars>) -> Constant { match self { Self::Constant(l) => l.clone(), - Self::Variable(rc) => vars - .and_then(|vars| vars.get(rc).cloned()) - .unwrap_or(Constant::Name(Rc::clone(rc))), + Self::Variable(rc) => Self::resolve_var(Rc::clone(rc), vars), + Self::VariableTarget(rc, target) => { + let constant = Self::resolve_var(Rc::clone(rc), vars); + match target { + Target::Index(_) => constant + .to_integer() + .map(|i| Constant::Target(Target::Index(i))) + .unwrap_or(constant), + Target::Named(n, _) => { + Constant::Target(Target::Named(Rc::clone(n), constant.to_float())) + } + } + } } } } @@ -1325,71 +1342,94 @@ impl CycleParser { } } + fn variable_identifier(pair: Pair) -> Result, String> { + pair.into_inner() + .next() + .ok_or("error in grammar, missing variable name".to_string()) + .map(|name_pair| Rc::from(name_pair.as_str())) + } + + fn variable(pair: Pair) -> Result { + Self::variable_identifier(pair).map(Value::Variable) + } + /// parse a pair inside a single as a value fn value(pair: Pair) -> Result { - if pair.as_rule() == Rule::variable { - pair.into_inner() - .next() - .ok_or("error in grammar, missing variable name".to_string()) - .map(|name_pair| Value::Variable(Rc::from(name_pair.as_str()))) - } else { - let constant = match pair.as_rule() { - Rule::integer => Constant::from_integer(pair.as_str()), - Rule::float => Constant::from_float(pair.as_str()), - Rule::number => { - if let Some(n) = pair.into_inner().next() { - match n.as_rule() { - Rule::integer => Constant::from_integer(n.as_str()), - Rule::float => Constant::from_float(n.as_str()), - _ => Err(format!("unrecognized number\n{:?}", n)), - } - } else { - Err("empty single".to_string()) - } + match pair.as_rule() { + Rule::variable => Self::variable(pair), + Rule::target => { + let name = pair.as_str().get(0..1).ok_or(format!( + "error in grammar, missing target key in pair\n{:?}", + pair + ))?; + let value = pair.clone().into_inner().next().ok_or(format!( + "error in grammar, missing target value in pair\n{:?}", + pair + ))?; + + match name.as_bytes() { + b"#" => match value.as_rule() { + Rule::integer => Ok(Value::Constant(Constant::Target(Target::Index( + Constant::parse_integer(value.as_str())?, + )))), + Rule::variable => Ok(Value::VariableTarget( + Self::variable_identifier(value)?, + Target::Index(0), + )), + _ => Err("error in grammar, unexpected rule for target index".to_string()), + }, + _ => match value.as_rule() { + Rule::float => Ok(Value::Constant(Constant::Target(Target::Named( + Rc::from(name), + Some(Constant::parse_float(value.as_str())?), + )))), + Rule::variable => Ok(Value::VariableTarget( + Self::variable_identifier(value)?, + Target::Named(Rc::from(name), None), + )), + _ => Err("error in grammar, unexpected rule for target float".to_string()), + }, } - Rule::hold => Ok(Constant::Hold), - Rule::rest => Ok(Constant::Rest), - Rule::pitch => Ok(Constant::Pitch(Pitch::parse(pair))), - Rule::chord => { - let mut pitch = Pitch { note: 0, octave: 4 }; - let mut mode = ""; - for p in pair.into_inner() { - match p.as_rule() { - Rule::pitch => { - pitch = Pitch::parse(p); - } - Rule::mode => { - mode = p.as_str(); + } + _ => { + let constant = match pair.as_rule() { + Rule::integer => Constant::from_integer(pair.as_str()), + Rule::float => Constant::from_float(pair.as_str()), + Rule::number => { + if let Some(n) = pair.into_inner().next() { + match n.as_rule() { + Rule::integer => Constant::from_integer(n.as_str()), + Rule::float => Constant::from_float(n.as_str()), + _ => Err(format!("unrecognized number\n{:?}", n)), } - _ => (), + } else { + Err("empty single".to_string()) } } - Ok(Constant::Chord(pitch, Rc::from(mode))) - } - Rule::target => { - let name = pair.as_str().get(0..1).ok_or(format!( - "error in grammar, missing target key in pair\n{:?}", - pair - ))?; - let value = pair.clone().into_inner().next().ok_or(format!( - "error in grammar, missing target value in pair\n{:?}", - pair - ))?; - - match name.as_bytes() { - b"#" => Ok(Constant::Target(Target::Index(Constant::parse_integer( - value.as_str(), - )?))), - _ => Ok(Constant::Target(Target::Named( - Rc::from(name), - Some(Constant::parse_float(value.as_str())?), - ))), + Rule::hold => Ok(Constant::Hold), + Rule::rest => Ok(Constant::Rest), + Rule::pitch => Ok(Constant::Pitch(Pitch::parse(pair))), + Rule::chord => { + let mut pitch = Pitch { note: 0, octave: 4 }; + let mut mode = ""; + for p in pair.into_inner() { + match p.as_rule() { + Rule::pitch => { + pitch = Pitch::parse(p); + } + Rule::mode => { + mode = p.as_str(); + } + _ => (), + } + } + Ok(Constant::Chord(pitch, Rc::from(mode))) } - } - Rule::name => Ok(Constant::Name(Rc::from(pair.as_str()))), - _ => Err(format!("unrecognized target value\n{:?}", pair)), - }?; - Ok(Value::Constant(constant)) + Rule::name => Ok(Constant::Name(Rc::from(pair.as_str()))), + _ => Err(format!("unrecognized target value\n{:?}", pair)), + }?; + Ok(Value::Constant(constant)) + } } } @@ -1764,7 +1804,6 @@ impl CycleParser { let mut pattern = Self::step(p)?; let mut key = k.into_inner(); if let Some(name) = key.next() { - // FIX parse variables for targets pattern.mutate_singles(&mut |single: &mut Single| { if let Some(f) = single.value.to_float(None) { if !matches!(single.value, Value::Constant(Constant::Target(_))) { @@ -1773,6 +1812,11 @@ impl CycleParser { Some(f), ))); } + } else if let Value::Variable(rc) = &single.value { + single.value = Value::VariableTarget( + Rc::clone(rc), + Target::Named(Rc::from(name.as_str()), None), + ) } }); } else { @@ -1781,6 +1825,8 @@ impl CycleParser { if !matches!(single.value, Value::Constant(Constant::Target(_))) { single.value = Value::Constant(Constant::Target(Target::Index(i))); } + } else if let Value::Variable(rc) = &single.value { + single.value = Value::VariableTarget(Rc::clone(rc), Target::Index(0)) } }); } @@ -1995,7 +2041,7 @@ impl Cycle { // multiply with single values to avoid generating events Step::Single(single) => { // apply multiplier - let multiplier = Self::step_multiplier(step, &single.value.to_literal(vars), vars); + let multiplier = Self::step_multiplier(step, &single.value.to_constant(vars), vars); Ok(Self::output_multiplied( left, state, cycle, multiplier, limit, overlap, vars, )?) @@ -2058,7 +2104,7 @@ impl Cycle { length: Fraction::ONE, span: Span::default(), string: Rc::clone(&s.string), - value: s.value.to_literal(vars), + value: s.value.to_constant(vars), targets: vec![], }) } diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index d3c30c3..9f2da30 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -47,7 +47,7 @@ fn span() -> Result<(), String> { #[test] fn weight_and_replicate() -> Result<(), String> { let mut cycle = Cycle::from("a!1.5 b")?; - let events = cycle.generate()?; + let _events = cycle.generate()?; Ok(()) } @@ -61,6 +61,37 @@ fn variables() -> Result<(), String> { // unset variables convert into named let mut cycle = Cycle::from("a b $note d")?; assert_eq!(cycle.generate(), Cycle::from("a b note d")?.generate()); + + let index = Constant::Integer(12); + let mut cycle = Cycle::from("a:$index")?; + cycle.set_var("index", index); + assert_eq!(cycle.generate(), Cycle::from("a:12")?.generate()); + + let float = Constant::Float(0.9); + let mut cycle = Cycle::from("a:p$float")?; + cycle.set_var("float", float); + assert_eq!(cycle.generate(), Cycle::from("a:p0.9")?.generate()); + + let f1 = Constant::Float(0.5); + let f2 = Constant::Float(0.9); + let mut cycle = Cycle::from("[a b c d]:p=[$f1 $f2]")?; + cycle.set_var("f1", f1); + cycle.set_var("f2", f2); + assert_eq!( + cycle.generate(), + Cycle::from("[a b c d]:p=[0.5 0.9]")?.generate() + ); + + let mult = Constant::Float(2.0); + let mut cycle = Cycle::from("a*$mult")?; + cycle.set_var("mult", mult); + assert_eq!(cycle.generate(), Cycle::from("a*2")?.generate()); + + let length = Constant::Float(3.0); + let mut cycle = Cycle::from("a@$length b")?; + cycle.set_var("length", length); + assert_eq!(cycle.generate(), Cycle::from("a@3 b")?.generate()); + Ok(()) } From ea98082a111a00e8b5668e8d499ea8fdcd406f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20M=C3=BCller?= Date: Thu, 19 Feb 2026 15:18:28 +0100 Subject: [PATCH 06/21] inject parameter values into `Cycle` as vars - do so in scripted and unscripted CycleEmitters - add basic test to verify parameter -> cycle value conversions - add basic mapping tests as well and verify interaction with parameters - add new `as_str` impl to CycleEvent to fetch resolved name variables --- src/emitter/cycle.rs | 240 +++++++++++++++++++++++++++++++++- src/emitter/scripted_cycle.rs | 21 ++- src/tidal/cycle.rs | 15 ++- 3 files changed, 260 insertions(+), 16 deletions(-) diff --git a/src/emitter/cycle.rs b/src/emitter/cycle.rs index 056bfce..4c3da95 100644 --- a/src/emitter/cycle.rs +++ b/src/emitter/cycle.rs @@ -1,15 +1,16 @@ -use std::{collections::HashMap, ops::RangeBounds}; +use std::{collections::HashMap, ops::RangeBounds, rc::Rc}; type Fraction = num_rational::Rational32; use crate::{ event::new_note, BeatTimeBase, Chord, Cycle, CycleEvent, CycleTarget, CycleValue, Emitter, - EmitterEvent, Event, InstrumentId, Note, NoteEvent, ParameterSet, RhythmEvent, + EmitterEvent, Event, InstrumentId, Note, NoteEvent, Parameter, ParameterSet, ParameterType, + RhythmEvent, }; // ------------------------------------------------------------------------------------------------- -/// Default conversion of a CycleValue into a note stack. +/// Try converting a [`CycleValue`] into a note event stack. /// /// Returns an error when resolving chord modes failed. impl TryFrom<&CycleValue> for Vec> { @@ -44,6 +45,22 @@ impl TryFrom<&CycleValue> for Vec> { // ------------------------------------------------------------------------------------------------- +/// Convert a [`Parameter`] value to a [`CycleValue`]. +impl From<&Parameter> for CycleValue { + fn from(value: &Parameter) -> Self { + match value.parameter_type() { + ParameterType::Boolean => CycleValue::Integer((value.value() >= 0.5) as i32), + ParameterType::Float => CycleValue::Float(value.value()), + ParameterType::Integer => CycleValue::Integer(value.value().round() as i32), + ParameterType::Enum => CycleValue::Name(Rc::from( + value.value_strings()[value.value().round() as usize].clone(), + )), + } + } +} + +// ------------------------------------------------------------------------------------------------- + // Conversion helpers for cycle targets fn float_value_in_range( maybe_float: &Option, @@ -244,14 +261,20 @@ impl CycleNoteEvents { #[derive(Clone, Debug)] pub struct CycleEmitter { cycle: Cycle, + parameters: ParameterSet, mappings: HashMap>>, } impl CycleEmitter { /// Create a new cycle emitter from the given precompiled cycle. pub(crate) fn new(cycle: Cycle) -> Self { + let parameters = vec![]; let mappings = HashMap::new(); - Self { cycle, mappings } + Self { + cycle, + parameters, + mappings, + } } /// Try creating a new cycle emitter from the given mini notation string. @@ -284,7 +307,7 @@ impl CycleEmitter { /// Generate a note event from a single cycle event, applying mappings if necessary fn map_note_event(&mut self, event: CycleEvent) -> Result>, String> { let mut note_events = { - if let Some(note_events) = self.mappings.get(event.string()) { + if let Some(note_events) = self.mappings.get(event.as_str().as_ref()) { // apply custom note mappings note_events.clone() } else { @@ -300,6 +323,11 @@ impl CycleEmitter { /// Generate next batch of events from the next cycle run. /// Converts cycle events to note events and flattens channels into note columns. fn generate(&mut self) -> Vec { + // inject parameter values into the cycle as variables + for parameter_ref in &self.parameters { + let parameter = parameter_ref.borrow(); + self.cycle.set_var(parameter.id(), (&*parameter).into()); + } // run the cycle event generator let events = { match self.cycle.generate() { @@ -343,8 +371,8 @@ impl Emitter for CycleEmitter { // nothing to do } - fn set_parameters(&mut self, _parameters: ParameterSet) { - // nothing to do + fn set_parameters(&mut self, parameters: ParameterSet) { + self.parameters = parameters; } fn run(&mut self, _pulse: RhythmEvent, emit_event: bool) -> Option> { @@ -379,3 +407,201 @@ pub fn new_cycle_emitter(input: &str) -> Result { pub fn new_cycle_emitter_with_seed(input: &str, seed: u64) -> Result { CycleEmitter::from_mini_with_seed(input, seed) } + +// ------------------------------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::Parameter; + use std::{cell::RefCell, rc::Rc}; + + use pretty_assertions::assert_eq; + + fn run_emitter( + emitter: &mut CycleEmitter, + ) -> Result, Box> { + emitter + .run( + RhythmEvent { + value: 1.0, + step_time: 1.0, + }, + true, + ) + .ok_or("No events emitted".into()) + } + + #[test] + fn parameter_conversion() -> Result<(), Box> { + // floats + let param = Rc::new(RefCell::new(Parameter::with_float( + "velocity", + "", + "", + 0.0..=1.0, + 0.8, + ))); + let mut variable = new_cycle_emitter("a:v$velocity")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("a:v0.8")?; + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + // integers + let param = Rc::new(RefCell::new(Parameter::with_integer( + "steps", + "", + "", + 1..=8, + 3, + ))); + let mut variable = new_cycle_emitter("a*$steps")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("a*3")?; + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + // enum + let param = Rc::new(RefCell::new(Parameter::with_boolean( + "enabled", "", "", true, + ))); + let mut variable = new_cycle_emitter("a*$enabled")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("a*1")?; + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + // enum + let param = Rc::new(RefCell::new(Parameter::with_enum( + "sample", + "", + "", + vec!["kick".to_string(), "snare".to_string()], + "snare".to_string(), + ))); + let mut variable = new_cycle_emitter("$sample")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("snare")?; + // NB: both will be `None` here as there are no mappings... + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + Ok(()) + } + + #[test] + fn parameter_value_changes() -> Result<(), Box> { + let param = Rc::new(RefCell::new(Parameter::with_float( + "velocity", + "", + "", + 0.0..=1.0, + 0.8, + ))); + let mut emitter = new_cycle_emitter("a:v$velocity")?; + emitter.set_parameters(vec![Rc::clone(¶m)]); + + // initial value + let mut expected = new_cycle_emitter("a:v0.8")?; + assert_eq!(run_emitter(&mut emitter)?, run_emitter(&mut expected)?); + + // changed parameter value + param.borrow_mut().set_value(0.5); + let mut expected = new_cycle_emitter("a:v0.5")?; + assert_eq!(run_emitter(&mut emitter)?, run_emitter(&mut expected)?); + + Ok(()) + } + + #[test] + fn note_mappings() -> Result<(), Box> { + // single note mapping + let mut emitter = new_cycle_emitter("bd sd")?.with_mappings(&[ + ("bd", vec![new_note(Note::C4)]), + ("sd", vec![new_note(Note::D4)]), + ]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 2); + assert_eq!(events[0].event, Event::NoteEvents(vec![new_note(Note::C4)])); + assert_eq!(events[1].event, Event::NoteEvents(vec![new_note(Note::D4)])); + + // multi note mapping + let mut emitter = new_cycle_emitter("x")? + .with_mappings(&[("x", vec![new_note(Note::C4), new_note(Note::E4)])]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 1); + assert_eq!( + events[0].event, + Event::NoteEvents(vec![new_note(Note::C4), new_note(Note::E4)]) + ); + + // mapping with rest (None = empty note) + let mut emitter = new_cycle_emitter("a b")? + .with_mappings(&[("a", vec![new_note(Note::C4)]), ("b", vec![None])]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 2); + assert_eq!(events[0].event, Event::NoteEvents(vec![new_note(Note::C4)])); + assert_eq!(events[1].event, Event::NoteEvents(vec![None])); + + // mapping combined with targets + let mut emitter = + new_cycle_emitter("bd:v0.5")?.with_mappings(&[("bd", vec![new_note(Note::C4)])]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 1); + if let Event::NoteEvents(notes) = &events[0].event { + let event = notes[0].clone(); + assert_eq!(event, new_note((Note::C4, None, None, 0.5))); + } else { + panic!("expected NoteEvents"); + } + + Ok(()) + } + + #[test] + fn note_mappings_with_parameters() -> Result<(), Box> { + let param = Rc::new(RefCell::new(Parameter::with_float( + "vel", + "", + "", + 0.0..=1.0, + 0.7, + ))); + let mut emitter = + new_cycle_emitter("bd:v$vel")?.with_mappings(&[("bd", vec![new_note(Note::C4)])]); + emitter.set_parameters(vec![Rc::clone(¶m)]); + let events = run_emitter(&mut emitter)?; + if let Event::NoteEvents(notes) = &events[0].event { + let event = notes[0].clone(); + assert_eq!(event, new_note((Note::C4, None, None, 0.7))); + } else { + panic!("expected NoteEvents"); + } + + // change parameter value + param.borrow_mut().set_value(0.3); + let events = run_emitter(&mut emitter)?; + if let Event::NoteEvents(notes) = &events[0].event { + let event = notes[0].clone(); + assert_eq!(event, new_note((Note::C4, None, None, 0.3))); + } else { + panic!("expected NoteEvents"); + } + + // mapped enum strings + let param = Rc::new(RefCell::new(Parameter::with_enum( + "sample", + "", + "", + vec!["kick".to_string(), "snare".to_string()], + "snare".to_string(), + ))); + let mut emitter = + new_cycle_emitter("$sample")?.with_mappings(&[("snare", vec![new_note(Note::C4)])]); + emitter.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = + new_cycle_emitter("snare")?.with_mappings(&[("snare", vec![new_note(Note::C4)])]); + let emitter_result = run_emitter(&mut emitter)?; + let expected_result = run_emitter(&mut expected)?; + assert_eq!(emitter_result, expected_result); + + Ok(()) + } +} diff --git a/src/emitter/scripted_cycle.rs b/src/emitter/scripted_cycle.rs index 23200ae..3c6d55c 100644 --- a/src/emitter/scripted_cycle.rs +++ b/src/emitter/scripted_cycle.rs @@ -26,6 +26,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct ScriptedCycleEmitter { cycle: Cycle, + parameters: ParameterSet, mappings: HashMap>>, mapping_callback: Option, timeout_hook: Option, @@ -35,12 +36,14 @@ pub struct ScriptedCycleEmitter { impl ScriptedCycleEmitter { /// Return a new cycle with the given value mappings applied. pub fn with_mappings(cycle: Cycle, mappings: Vec<(String, Vec>)>) -> Self { + let parameters = vec![]; let mappings = mappings.into_iter().collect(); let mapping_callback = None; let timeout_hook = None; let channel_steps = vec![]; Self { cycle, + parameters, mappings, mapping_callback, timeout_hook, @@ -58,6 +61,7 @@ impl ScriptedCycleEmitter { // create a new timeout_hook instance and reset it before calling the function let mut timeout_hook = timeout_hook.clone(); timeout_hook.reset(); + let parameters = vec![]; let mappings = HashMap::new(); // initialize emitter context for the function let mut mapping_callback = mapping_callback; @@ -75,6 +79,7 @@ impl ScriptedCycleEmitter { let channel_steps = vec![]; Ok(Self { cycle, + parameters, mappings, mapping_callback: Some(mapping_callback), timeout_hook: Some(timeout_hook), @@ -99,9 +104,9 @@ impl ScriptedCycleEmitter { step_length, )?; // call mapping function - let result = mapping_callback.call_with_arg(event.string())?; + let result = mapping_callback.call_with_arg(event.as_str().as_ref())?; note_events_from_value(&result, None)? - } else if let Some(note_events) = self.mappings.get(event.string()) { + } else if let Some(note_events) = self.mappings.get(event.as_str().as_ref()) { // apply custom note mapping note_events.clone() } else { @@ -116,7 +121,7 @@ impl ScriptedCycleEmitter { { return Err(LuaError::runtime(format!( "invalid/unknown identifier in cycle: '{}'. please check for typos or add a custom mapping for it.", - event.string() + event.as_str() ))); } // apply note properties from targets @@ -129,6 +134,11 @@ impl ScriptedCycleEmitter { /// Generate next batch of events from the next cycle run. /// Converts cycle events to note events and flattens channels into note columns. fn generate(&mut self) -> Vec { + // inject parameter values into cycle as variables + for parameter_ref in &self.parameters { + let parameter = parameter_ref.borrow(); + self.cycle.set_var(parameter.id(), (&*parameter).into()); + } // run the cycle event generator let events = { match self.cycle.generate() { @@ -238,7 +248,7 @@ impl ScriptedCycleEmitter { return; } // call mapping function - if let Err(err) = mapping_callback.call_with_arg(event.string()) { + if let Err(err) = mapping_callback.call_with_arg(event.as_str().as_ref()) { mapping_callback.handle_error(&err); return; } @@ -285,6 +295,9 @@ impl Emitter for ScriptedCycleEmitter { } fn set_parameters(&mut self, parameters: ParameterSet) { + // store parameters, so we can inject them in generate() + self.parameters = parameters.clone(); + // and pass them to the mapping callback context if let Some(timeout_hook) = &mut self.timeout_hook { timeout_hook.reset(); } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 83f621c..c3083de 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -208,16 +208,21 @@ impl Default for Event { } impl Event { + /// The step's value as string representation: Either the value name's original string value + /// or the original value string, unparsed as fallback. + pub fn as_str(&self) -> Rc { + match &self.value { + // prefer `Name` over self.string, as the string may contain unresolved variables + Constant::Name(name) => Rc::clone(name), + _ => Rc::clone(&self.string), + } + } + /// The step's original parsed value. pub fn value(&self) -> &Constant { &self.value } - /// The step's original value string, unparsed. - pub fn string(&self) -> &str { - &self.string - } - /// The step's time span. pub fn span(&self) -> &Span { &self.span From 90177262db08573223d69a9769b6101d4d1cc023 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sat, 21 Feb 2026 22:26:33 +0000 Subject: [PATCH 07/21] separate target tests --- src/tidal/cycle/tests.rs | 622 ++++++++++++++++++++------------------- 1 file changed, 317 insertions(+), 305 deletions(-) diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index 9f2da30..454f8c4 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -121,6 +121,314 @@ fn parse() -> Result<(), String> { Ok(()) } +#[test] +fn targets() -> Result<(), String> { + assert_eq!( + Cycle::from("a:v=0.5")?.generate()?, + [[Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::Named("v".into(), Some(0.5)))]] + ); + + assert_eq!( + Cycle::from("a:1 b:target")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_name("target".into())) + ]] + ); + + assert_cycles( + "a:<1 2>", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(2))]], + ], + )?; + + assert_cycles( + "a:1:2:Target", + vec![vec![vec![Event::at( + Fraction::from(0), + Fraction::new(1, 1), + ) + .with_note(9, 4) + .with_targets(vec![ + Target::from_index(1), + Target::from_name("Target".into()), + ])]]], + )?; + + assert_cycles( + "[a:1:2]:<3 4>", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + ], + )?; + + // target expression preserves the structure from the left side + assert_eq!( + Cycle::from("[a b c d]:[1 2 3]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_note(0, 4) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(2, 4) + .with_target(Target::from_index(3)), + ]] + ); + + assert_cycles( + "{<0 0 d#8:test> 1 :0xB [<.5 0.95> 1.]}%3", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(1), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) + .with_note(0, 4) + .with_target(Target::from_index(0xB)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 6)).with_float(0.5), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_float(1.0), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)) + .with_note(2, 4) + .with_target(Target::from_index(0xB)), + Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(0.95), + Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(1.0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) + .with_note(3, 8) + .with_target(Target::from_name("test".into())), + ]], + ], + )?; + + assert_cycles( + "[1 2] [3 4,[5 6]:42]", + vec![vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(4), + ], + vec![ + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_int(5) + .with_target(Target::from_index(42)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(6) + .with_target(Target::from_index(42)), + ], + ]], + )?; + + assert_cycles( + "[<1 10> <2 20>:a](2,5)", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(1), + Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) + .with_int(2) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), + Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(1), + Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) + .with_int(2) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(10), + Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) + .with_int(20) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), + Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(10), + Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) + .with_int(20) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), + ]], + ], + )?; + + // when using ~ as a target, it's possible selectively skip overriding the outer target from within + assert_cycles( + "[a [b:<~ 7> b:<8 9>]]:[1 [2 3], 4]", + vec![ + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + // this iteration lets the outer context set the target + .with_target(Target::from_index(2)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(8)), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(8)), + ], + ], + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(7)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(9)), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(7)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(9)), + ], + ], + ], + )?; + + assert_cycles( + "[a b]:<1 target>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(1)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_name("target".into())), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_name("target".into())), + ]], + ], + )?; + + assert_cycle_equality( + "[1 2 3 4]:v=[0.2 0.3 0.4 p.8]", + "[1 2 3 4]:[v0.2 v0.3 v0.4 p.8]", + )?; + + assert_cycle_equality("[1 2 3 4]:g=[0.1 10.]", "[1 2 3 4]:[g0.1 g10.]")?; + + assert_cycles( + "[a:1 b]:<3 4>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(3)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(4)), + ]], + ], + )?; + + assert_eq!( + Cycle::from("a:1 b:v0.1:v1.0:p1.0:g100.0")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_targets(vec![ + Target::Named("v".into(), Some(0.1)), + // second v should not be applied + Target::Named("p".into(), Some(1.0)), + Target::Named("g".into(), Some(100.0)), + ]) + ]] + ); + + // outer instrument values shouldn't override inner ones + assert_eq!( + Cycle::from("[a:#2 b]:#3")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(3)), + ]] + ); + + assert_eq!( + Cycle::from("a:1:#1 b:#1:1")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::Index(1)), + ]] + ); + + Ok(()) +} + #[test] fn generate() -> Result<(), String> { assert_eq!( @@ -308,35 +616,6 @@ fn generate() -> Result<(), String> { ], )?; - assert_cycles( - "{<0 0 d#8:test> 1 :0xB [<.5 0.95> 1.]}%3", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(1), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) - .with_note(0, 4) - .with_target(Target::from_index(0xB)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 6)).with_float(0.5), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_float(1.0), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)) - .with_note(2, 4) - .with_target(Target::from_index(0xB)), - Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(0.95), - Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(1.0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) - .with_note(3, 8) - .with_target(Target::from_name("test".into())), - ]], - ], - )?; - assert_eq!( Cycle::from("[1 middle _] {}%42 [] <>")?.generate()?, [[ @@ -365,35 +644,15 @@ fn generate() -> Result<(), String> { Event::at(Fraction::from(0), Fraction::from(1)).with_name("another_one") ]], vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "chord") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "-^7") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_name("c6a_name") - ]], - ], - )?; - - assert_cycles( - "[1 2] [3 4,[5 6]:42]", - vec![vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(4), - ], - vec![ - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_int(5) - .with_target(Target::from_index(42)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(6) - .with_target(Target::from_index(42)), - ], - ]], + Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "chord") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "-^7") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_name("c6a_name") + ]], + ], )?; assert_eq!( @@ -442,36 +701,6 @@ fn generate() -> Result<(), String> { ]], )?; - assert_cycles( - "[<1 10> <2 20>:a](2,5)", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(1), - Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) - .with_int(2) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), - Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(1), - Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) - .with_int(2) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(10), - Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) - .with_int(20) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), - Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(10), - Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) - .with_int(20) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), - ]], - ], - )?; - assert_eq!( Cycle::from("1!2 3 [4!3 5]")?.generate()?, [[ @@ -539,216 +768,6 @@ fn generate() -> Result<(), String> { ], )?; - assert_eq!( - Cycle::from("a:1 b:target")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_name("target".into())) - ]] - ); - - assert_cycles( - "a:<1 2>", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(2))]], - ], - )?; - - assert_cycles( - "a:1:2:Target", - vec![vec![vec![Event::at( - Fraction::from(0), - Fraction::new(1, 1), - ) - .with_note(9, 4) - .with_targets(vec![ - Target::from_index(1), - Target::from_name("Target".into()), - ])]]], - )?; - - assert_cycles( - "[a:1:2]:<3 4>", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - ], - )?; - - // target expression preserves the structure from the left side - assert_eq!( - Cycle::from("[a b c d]:[1 2 3]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_note(0, 4) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(2, 4) - .with_target(Target::from_index(3)), - ]] - ); - - // when using ~ as a target, it's possible selectively skip overriding the outer target from within - assert_cycles( - "[a [b:<~ 7> b:<8 9>]]:[1 [2 3], 4]", - vec![ - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - // this iteration lets the outer context set the target - .with_target(Target::from_index(2)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(8)), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(8)), - ], - ], - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(7)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(9)), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(7)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(9)), - ], - ], - ], - )?; - - assert_cycles( - "[a b]:<1 target>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(1)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_name("target".into())), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_name("target".into())), - ]], - ], - )?; - - assert_cycles( - "[a:1 b]:<3 4>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(3)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(4)), - ]], - ], - )?; - - assert_eq!( - Cycle::from("a:1 b:v0.1:v1.0:p1.0:g100.0")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_targets(vec![ - Target::Named("v".into(), Some(0.1)), - // second v should not be applied - Target::Named("p".into(), Some(1.0)), - Target::Named("g".into(), Some(100.0)), - ]) - ]] - ); - - // outer instrument values shouldn't override inner ones - assert_eq!( - Cycle::from("[a:#2 b]:#3")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(3)), - ]] - ); - - assert_eq!( - Cycle::from("a:1:#1 b:#1:1")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::Index(1)), - ]] - ); - assert_eq!( Cycle::from("c(3,8,9)")?.generate()?, [[ @@ -1065,12 +1084,5 @@ fn target_assign() -> Result<(), String> { ], )?; - assert_cycle_equality( - "[1 2 3 4]:v=[0.2 0.3 0.4 p.8]", - "[1 2 3 4]:[v0.2 v0.3 v0.4 p.8]", - )?; - - assert_cycle_equality("[1 2 3 4]:g=[0.1 10.]", "[1 2 3 4]:[g0.1 g10.]")?; - Ok(()) } From 6ac4f3700f602fb2b3ffd2fb25952b795fd40ef1 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sat, 21 Feb 2026 22:27:10 +0000 Subject: [PATCH 08/21] remove parse-time target assignment --- src/tidal/cycle.rs | 261 ++++++++++++++++++++++----------------------- 1 file changed, 125 insertions(+), 136 deletions(-) diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index c3083de..bdbfa09 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -288,6 +288,13 @@ pub enum Target { Named(Rc, Option), } +/// The kind of target used for target assignments +#[derive(Clone, Debug, PartialEq)] +pub enum TargetKind { + Index, + Named(Rc), +} + impl Target { pub fn equal_key(&self, other: &Self) -> bool { match (self, other) { @@ -346,7 +353,7 @@ impl Step { Step::WeightExpression(e) => vec![&e.left], Step::ReplicateExpression(e) => vec![&e.left], Step::Degrade(e) => vec![&e.step], - Step::TargetExpression(e) => vec![&e.left, &e.right], + Step::TargetExpression(e) => vec![&e.step, &e.target], Step::Bjorklund(b) => { if let Some(rotation) = &b.rotation { vec![&b.left, &b.steps, &b.pulses, &**rotation] @@ -361,40 +368,6 @@ impl Step { } } - fn inner_steps_mut(&mut self) -> Vec<&mut Step> { - match self { - Step::Single(_s) => vec![], - Step::Alternating(a) => a.steps.iter_mut().collect(), - Step::Subdivision(sd) => sd.steps.iter_mut().collect(), - Step::SpeedExpression(e) => vec![&mut e.left], - Step::WeightExpression(e) => vec![&mut e.left], - Step::ReplicateExpression(e) => vec![&mut e.left], - Step::Choices(cs) => cs.choices.iter_mut().collect(), - Step::Polymeter(pm) => pm.steps.as_mut().inner_steps_mut(), - Step::Stack(st) => st.stack.iter_mut().collect(), - Step::Degrade(e) => vec![&mut e.step], - Step::TargetExpression(e) => vec![&mut e.left], - Step::Bjorklund(b) => vec![&mut b.left], - Step::Static(s) => match s { - Static::Repeat => vec![], - Static::Range(_) => vec![], - }, - } - } - - fn mutate_singles(&mut self, fun: &mut F) - where - F: FnMut(&mut Single), - { - match self { - Self::Single(s) => fun(s), - _ => self - .inner_steps_mut() - .iter_mut() - .for_each(|s| s.mutate_singles(fun)), - } - } - fn length(&self, vars: Option<&Vars>) -> Fraction { match self { Step::ReplicateExpression(re) => re.count.to_fraction(vars).unwrap_or(Fraction::ONE), @@ -463,11 +436,28 @@ impl Value { Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_fraction(), } } - fn to_target(&self, string: &Rc, vars: Option<&Vars>) -> Option { + fn to_target( + &self, + string: &Rc, + kind: Option<&TargetKind>, + vars: Option<&Vars>, + ) -> Option { + kind.and_then(|kind| self.to_target_with_kind(kind, vars)) + .or(self.to_target_without_kind(string, vars)) + } + fn to_target_without_kind(&self, string: &Rc, vars: Option<&Vars>) -> Option { match self { - Self::Constant(l) => l.to_target(string), + Self::Constant(l) => l.to_target_without_kind(string), Self::Variable(_) | Self::VariableTarget(_, _) => { - self.to_constant(vars).to_target(string) + self.to_constant(vars).to_target_without_kind(string) + } + } + } + fn to_target_with_kind(&self, kind: &TargetKind, vars: Option<&Vars>) -> Option { + match self { + Self::Constant(l) => l.to_target_with_kind(kind), + Self::Variable(_) | Self::VariableTarget(_, _) => { + self.to_constant(vars).to_target_with_kind(kind) } } } @@ -609,8 +599,9 @@ struct Degrade { #[derive(Clone, Debug, PartialEq)] struct TargetExpression { - left: Box, - right: Box, + step: Box, + kind: Option, + target: Box, } #[derive(Clone, Debug, PartialEq)] @@ -734,59 +725,59 @@ impl Constant { str.parse::().map_err(|err| err.to_string()) } - fn from_float(str: &str) -> Result { + fn from_float(str: &str) -> Result { Self::parse_float(str).map(Self::Float) } - fn from_integer(str: &str) -> Result { + fn from_integer(str: &str) -> Result { Self::parse_integer(str).map(Self::Integer) } fn to_integer(&self) -> Option { match &self { - Constant::Rest => None, - Constant::Hold => None, - Constant::Integer(i) => Some(*i), - Constant::Float(f) => Some(*f as i32), - Constant::Pitch(n) => Some(n.midi_note() as i32), - Constant::Chord(p, _m) => Some(p.midi_note() as i32), - Constant::Target(t) => match t { + Self::Rest => None, + Self::Hold => None, + Self::Integer(i) => Some(*i), + Self::Float(f) => Some(*f as i32), + Self::Pitch(n) => Some(n.midi_note() as i32), + Self::Chord(p, _m) => Some(p.midi_note() as i32), + Self::Target(t) => match t { Target::Index(i) => Some(*i), Target::Named(_, v) => v.map(|f| f as i32), }, - Constant::Name(_n) => None, + Self::Name(_n) => None, } } fn to_float(&self) -> Option { match &self { - Constant::Rest => None, - Constant::Hold => None, - Constant::Integer(i) => Some(*i as f64), - Constant::Float(f) => Some(*f), - Constant::Pitch(n) => Some(n.midi_note() as f64), - Constant::Chord(n, _m) => Some(n.midi_note() as f64), - Constant::Target(t) => match t { + Self::Float(f) => Some(*f), + Self::Integer(i) => Some(*i as f64), + Self::Pitch(n) => Some(n.midi_note() as f64), + Self::Chord(n, _m) => Some(n.midi_note() as f64), + Self::Target(t) => match t { Target::Index(i) => Some(*i as f64), Target::Named(_, v) => *v, }, - Constant::Name(_n) => None, + Self::Rest => None, + Self::Hold => None, + Self::Name(_n) => None, } } fn to_chance(&self) -> Option { match &self { - Constant::Rest => None, - Constant::Hold => None, - Constant::Integer(i) => Some((*i as f64).clamp(0.0, 100.0) / 100.0), - Constant::Float(f) => Some(f.clamp(0.0, 1.0)), - Constant::Pitch(p) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), - Constant::Chord(p, _m) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), - Constant::Target(t) => match t { + Self::Rest => None, + Self::Hold => None, + Self::Integer(i) => Some((*i as f64).clamp(0.0, 100.0) / 100.0), + Self::Float(f) => Some(f.clamp(0.0, 1.0)), + Self::Pitch(p) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), + Self::Chord(p, _m) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), + Self::Target(t) => match t { Target::Index(i) => Some(*i as f64), Target::Named(_, v) => v.map(|f| f.clamp(0.0, 1.0)), }, - Constant::Name(_n) => None, + Self::Name(_n) => None, } } @@ -794,18 +785,37 @@ impl Constant { self.to_float().and_then(Fraction::from_f64) } - fn to_target(&self, string: &Rc) -> Option { + fn to_target_without_kind(&self, string: &Rc) -> Option { match self { - Self::Rest | Constant::Hold => None, + Self::Target(t) => Some(t.clone()), Self::Integer(i) => Some(Target::from_index(*i)), Self::Name(name) => Some(Target::from_name(Rc::clone(name))), - Self::Target(t) => Some(t.clone()), - Self::Float(_) | Constant::Pitch(_) | Constant::Chord(_, _) => { + Self::Float(_) | Self::Pitch(_) | Self::Chord(_, _) => { // pass unexpected values as raw string and let clients deal with conversions or errors Some(Target::from_name(Rc::clone(string))) } + Self::Rest | Self::Hold => None, + } + } + + fn to_target_with_kind(&self, kind: &TargetKind) -> Option { + match self { + // inner targets override outer target + Self::Target(t) => Some(t.clone()), + // // TODO allow string values for target outputs as per #94 + // Self::Name(_name) => None, + _ => match kind { + TargetKind::Index => self.to_integer().map(Target::from_index), + TargetKind::Named(name) => self + .to_float() + .map(|f| Target::Named(Rc::clone(name), Some(f))), + }, } } + fn to_target(&self, string: &Rc, kind: Option<&TargetKind>) -> Option { + kind.and_then(|kind| self.to_target_with_kind(kind)) + .or(self.to_target_without_kind(string)) + } } impl Span { @@ -1338,7 +1348,6 @@ impl CycleParser { Rule::alternating => Self::group(pair, Step::alternating), Rule::polymeter => Self::polymeter(pair), Rule::range => Self::range(pair), - Rule::target_assign => Self::target_assign(pair), Rule::expression => Self::expression(pair), _ => Err(format!( "unexpected rule, this is a bug in the parser\n{:?}", @@ -1768,11 +1777,35 @@ impl CycleParser { let right = op_pair .into_inner() .next() - .ok_or_else(Self::invalid_right_hand) - .and_then(Self::step)?; + .ok_or_else(Self::invalid_right_hand)?; + + let (kind, target) = match right.as_rule() { + Rule::target_assign => { + let mut right = right.into_inner(); + + let target_name_pair = + right.next().ok_or("error in grammar, missing target key")?; + if target_name_pair.as_rule() != Rule::target_name { + return Err("error in grammar, expected target_name".to_string()); + } + + let p = right.next().ok_or("missing step pattern")?; + let mut target_name = target_name_pair.into_inner(); + if let Some(target_name) = target_name.next() { + // target name was specified + (Some(TargetKind::Named(Rc::from(target_name.as_str()))), p) + } else { + // # was used as indexed target + (Some(TargetKind::Index), p) + } + } + _ => (None, right), + }; + Ok(Step::TargetExpression(TargetExpression { - left: Box::new(left), - right: Box::new(right), + step: Box::new(left), + kind, + target: Box::new(Self::step(target)?), })) } @@ -1797,47 +1830,6 @@ impl CycleParser { } Ok(left) } - fn target_assign(pair: Pair) -> Result { - let mut inner = pair.into_inner(); - - let k = inner.next().ok_or("error in grammar, missing target key")?; - if k.as_rule() != Rule::target_name { - return Err("error in grammar, expected target_name".to_string()); - } - - let p = inner.next().ok_or("missing step pattern")?; - let mut pattern = Self::step(p)?; - let mut key = k.into_inner(); - if let Some(name) = key.next() { - pattern.mutate_singles(&mut |single: &mut Single| { - if let Some(f) = single.value.to_float(None) { - if !matches!(single.value, Value::Constant(Constant::Target(_))) { - single.value = Value::Constant(Constant::Target(Target::Named( - Rc::from(name.as_str()), - Some(f), - ))); - } - } else if let Value::Variable(rc) = &single.value { - single.value = Value::VariableTarget( - Rc::clone(rc), - Target::Named(Rc::from(name.as_str()), None), - ) - } - }); - } else { - pattern.mutate_singles(&mut |single: &mut Single| { - if let Some(i) = single.value.to_integer(None) { - if !matches!(single.value, Value::Constant(Constant::Target(_))) { - single.value = Value::Constant(Constant::Target(Target::Index(i))); - } - } else if let Value::Variable(rc) = &single.value { - single.value = Value::VariableTarget(Rc::clone(rc), Target::Index(0)) - } - }); - } - - Ok(pattern) - } } // ------------------------------------------------------------------------------------------------- @@ -1922,9 +1914,9 @@ impl Cycle { } // overlay two lists of events and apply the targets from the second to the first - fn apply_targets(events: &mut [Event], target_events: &[Event]) { + fn apply_targets(events: &mut [Event], target_events: &[Event], kind: Option<&TargetKind>) { for target_event in target_events.iter() { - if let Some(target) = target_event.value.to_target(&target_event.string) { + if let Some(target) = target_event.value.to_target(&target_event.string, kind) { for event in events.iter_mut() { if event.span.overlaps(&target_event.span) && !{ @@ -1973,19 +1965,21 @@ impl Cycle { // generate events from Target expressions fn output_with_target( - left: &Step, - right: &Step, + exp: &TargetExpression, state: &mut CycleState, cycle: u32, limit: usize, overlap: bool, vars: Option<&Vars>, ) -> Result { - match right { - // multiply with single values to avoid generating events + let (step, target_kind, target_step) = + (exp.step.as_ref(), exp.kind.as_ref(), exp.target.as_ref()); + + match target_step { + // assign single value to avoid generating events Step::Single(single) => { - let mut events = Self::output(left, state, cycle, limit, overlap, vars)?; - if let Some(target) = single.value.to_target(&single.string, vars) { + let mut events = Self::output(step, state, cycle, limit, overlap, vars)?; + if let Some(target) = single.value.to_target(&single.string, target_kind, vars) { events.mutate_events(&mut |event: &mut Event| { if !{ let this = &event; @@ -2001,15 +1995,16 @@ impl Cycle { _ => { // generate all the events as flat vecs from both the left and right side of the expression let (left_channels, left_span) = - Self::output_flat(left, state, cycle, limit, vars)?; - let (target_channels, _) = Self::output_flat(right, state, cycle, limit, vars)?; + Self::output_flat(step, state, cycle, limit, vars)?; + let (target_channels, _) = + Self::output_flat(target_step, state, cycle, limit, vars)?; // iterate over channels from both sides to create necessary new stacks if the right side is polyphonic let mut channel_events: Vec = Vec::with_capacity(target_channels.len()); for channel in target_channels.into_iter() { for left_channel in left_channels.iter() { let mut cloned_left = left_channel.clone(); - Self::apply_targets(&mut cloned_left, &channel); + Self::apply_targets(&mut cloned_left, &channel, target_kind); channel_events.push(Events::Multi(MultiEvents { length: left_span.length(), span: left_span.clone(), @@ -2221,15 +2216,9 @@ impl Cycle { }); out } - Step::TargetExpression(e) => Self::output_with_target( - e.left.as_ref(), - e.right.as_ref(), - state, - cycle, - limit, - overlap, - vars, - )?, + Step::TargetExpression(e) => { + Self::output_with_target(e, state, cycle, limit, overlap, vars)? + } Step::SpeedExpression(e) => { Self::output_with_speed(e.right.as_ref(), step, state, cycle, limit, overlap, vars)? } From 6323bba3d88e1df174a5147701aa6d6abb0ae540 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sat, 21 Feb 2026 23:43:58 +0000 Subject: [PATCH 09/21] parse enum parameters as constants --- src/emitter/cycle.rs | 50 +++++++--- src/emitter/scripted_cycle.rs | 17 ++-- src/tidal/cycle.pest | 6 +- src/tidal/cycle.rs | 174 +++++++++++++++++++--------------- src/tidal/cycle/tests.rs | 25 +++++ 5 files changed, 172 insertions(+), 100 deletions(-) diff --git a/src/emitter/cycle.rs b/src/emitter/cycle.rs index 4c3da95..f1a4479 100644 --- a/src/emitter/cycle.rs +++ b/src/emitter/cycle.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, ops::RangeBounds, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, ops::RangeBounds, rc::Rc}; type Fraction = num_rational::Rational32; @@ -46,17 +46,36 @@ impl TryFrom<&CycleValue> for Vec> { // ------------------------------------------------------------------------------------------------- /// Convert a [`Parameter`] value to a [`CycleValue`]. -impl From<&Parameter> for CycleValue { - fn from(value: &Parameter) -> Self { - match value.parameter_type() { - ParameterType::Boolean => CycleValue::Integer((value.value() >= 0.5) as i32), - ParameterType::Float => CycleValue::Float(value.value()), - ParameterType::Integer => CycleValue::Integer(value.value().round() as i32), - ParameterType::Enum => CycleValue::Name(Rc::from( - value.value_strings()[value.value().round() as usize].clone(), - )), +pub type ParameterWithValues = (Rc>, Vec>); +impl Parameter { + pub fn into_var(&self, values: &[Option]) -> Option { + match self.parameter_type() { + ParameterType::Boolean => Some(CycleValue::Integer((self.value() >= 0.5) as i32)), + ParameterType::Float => Some(CycleValue::Float(self.value())), + ParameterType::Integer => Some(CycleValue::Integer(self.value().round() as i32)), + ParameterType::Enum => values[self.value().round() as usize].clone(), } } + + pub fn set_with_values(parameters: &ParameterSet) -> Vec { + parameters + .iter() + .map(|p| { + ( + Rc::clone(p), + if p.borrow().parameter_type() == ParameterType::Enum { + p.borrow() + .value_strings() + .iter() + .map(|s| Cycle::constant_from(s).ok()) + .collect() + } else { + Vec::default() + }, + ) + }) + .collect() + } } // ------------------------------------------------------------------------------------------------- @@ -261,7 +280,7 @@ impl CycleNoteEvents { #[derive(Clone, Debug)] pub struct CycleEmitter { cycle: Cycle, - parameters: ParameterSet, + parameters: Vec, mappings: HashMap>>, } @@ -324,9 +343,12 @@ impl CycleEmitter { /// Converts cycle events to note events and flattens channels into note columns. fn generate(&mut self) -> Vec { // inject parameter values into the cycle as variables - for parameter_ref in &self.parameters { + for (parameter_ref, enum_values) in &self.parameters { let parameter = parameter_ref.borrow(); - self.cycle.set_var(parameter.id(), (&*parameter).into()); + self.cycle.set_var( + parameter.id(), + parameter.into_var(enum_values).unwrap_or_default(), + ); } // run the cycle event generator let events = { @@ -372,7 +394,7 @@ impl Emitter for CycleEmitter { } fn set_parameters(&mut self, parameters: ParameterSet) { - self.parameters = parameters; + self.parameters = Parameter::set_with_values(¶meters); } fn run(&mut self, _pulse: RhythmEvent, emit_event: bool) -> Option> { diff --git a/src/emitter/scripted_cycle.rs b/src/emitter/scripted_cycle.rs index 3c6d55c..15ea4ac 100644 --- a/src/emitter/scripted_cycle.rs +++ b/src/emitter/scripted_cycle.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, rc::Rc}; use num_traits::ToPrimitive; @@ -9,9 +9,9 @@ use crate::{ add_lua_callback_error, note_events_from_value, ContextPlaybackState, LuaCallback, LuaTimeoutHook, }, - emitter::cycle::{apply_cycle_note_properties, CycleNoteEvents}, + emitter::cycle::{apply_cycle_note_properties, CycleNoteEvents, ParameterWithValues}, BeatTimeBase, Cycle, CycleEvent, CycleValue, Emitter, EmitterEvent, Event, NoteEvent, - ParameterSet, RhythmEvent, + Parameter, ParameterSet, RhythmEvent, }; // ------------------------------------------------------------------------------------------------- @@ -26,7 +26,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct ScriptedCycleEmitter { cycle: Cycle, - parameters: ParameterSet, + parameters: Vec, mappings: HashMap>>, mapping_callback: Option, timeout_hook: Option, @@ -135,9 +135,12 @@ impl ScriptedCycleEmitter { /// Converts cycle events to note events and flattens channels into note columns. fn generate(&mut self) -> Vec { // inject parameter values into cycle as variables - for parameter_ref in &self.parameters { + for (parameter_ref, enum_values) in &self.parameters { let parameter = parameter_ref.borrow(); - self.cycle.set_var(parameter.id(), (&*parameter).into()); + self.cycle.set_var( + parameter.id(), + parameter.into_var(enum_values).unwrap_or_default(), + ); } // run the cycle event generator let events = { @@ -296,7 +299,7 @@ impl Emitter for ScriptedCycleEmitter { fn set_parameters(&mut self, parameters: ParameterSet) { // store parameters, so we can inject them in generate() - self.parameters = parameters.clone(); + self.parameters = Parameter::set_with_values(¶meters); // and pass them to the mapping callback context if let Some(timeout_hook) = &mut self.timeout_hook { timeout_hook.reset(); diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index e03b568..0ff4cfe 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -39,7 +39,8 @@ name = @{ ASCII_ALPHANUMERIC ~ (ASCII_ALPHANUMERIC | "_")* } repeat = { "!" } /// possible literals for single steps -single = { hold | rest | chord | target | pitch | number | name | variable } +constant = _{ hold | rest | chord | target | pitch | number | name } +single = { constant | variable } choice_op = {"|"} stack_op = {","} @@ -83,3 +84,6 @@ section = _{ ( expression | range | single | repeat | group)+ } /// the root of the cycle mini = { SOI ~ sections? ~ EOI } + +/// parser for a single constant +constant_literal = { SOI ~ constant ~ EOI } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index bdbfa09..055c663 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -43,46 +43,52 @@ impl Cycle { /// /// Returns a parse error, when the given string is not a valid mini notation expression. pub fn from(input: &str) -> Result { - match CycleParser::parse(Rule::mini, input) { - Ok(mut tree) => { - if let Some(mini) = tree.next() { - #[cfg(test)] - { - println!("\nTREE"); - Self::print_pairs(&mini, 0); - } - let input = input.to_string(); - let root = CycleParser::step(mini)?; - let state = CycleState { - events: 0, - iteration: 0, - rng: Xoshiro256PlusPlus::from_seed(rng().random()), - }; - let seed = None; - let source = None; - let event_limit = Self::EVENT_LIMIT_DEFAULT; - let vars = None; - let cycle = Self { - input, - seed, - source, - root, - state, - event_limit, - vars, - }; - #[cfg(test)] - { - println!("\nCYCLE"); - cycle.print(); - } - Ok(cycle) - } else { - Err("couldn't parse input".to_string()) + CycleParser::parse_from_rule(Rule::mini, input).and_then(|root_pair| { + let root = CycleParser::step(root_pair)?; + let input = input.to_string(); + let state = CycleState { + events: 0, + iteration: 0, + rng: Xoshiro256PlusPlus::from_seed(rng().random()), + }; + let seed = None; + let source = None; + let event_limit = Self::EVENT_LIMIT_DEFAULT; + let vars = None; + let cycle = Self { + input, + seed, + source, + root, + state, + event_limit, + vars, + }; + #[cfg(test)] + { + println!("\nCYCLE"); + cycle.print(); + } + Ok(cycle) + }) + } + + pub fn constant_from(input: &str) -> Result { + CycleParser::parse_from_rule(Rule::constant_literal, input).and_then(|root| { + let string = root.as_str(); + let single = CycleParser::single(root); + if let Ok(single) = single { + match single.value { + Value::Constant(constant) => Ok(constant), + _ => Err(format!( + "variable {}found where constant was expected", + string + )), } + } else { + Err(format!("single constant expected, found {}", string)) } - Err(err) => Err(format!("{}", err)), - } + }) } /// Rebuild/configure a newly created cycle to use the given custom seed. @@ -125,10 +131,6 @@ impl Cycle { } } - pub fn update_vars(&mut self, vars: Vars) { - self.vars = Some(vars) - } - pub fn set_var(&mut self, name: &str, constant: Constant) { if let Some(vars) = self.vars.as_mut() { vars.insert(name.into(), constant); @@ -1337,8 +1339,39 @@ impl Events { #[grammar = "tidal/cycle.pest"] struct CycleParser {} -/// the errors here should be unreachable unless there is a bug in the pest grammar impl CycleParser { + fn parse_from_rule(rule: Rule, input: &'_ str) -> Result, String> { + match Self::parse(rule, input) { + Ok(mut tree) => { + if let Some(step_pair) = tree.next() { + #[cfg(test)] + { + println!("\nTREE"); + Self::print_pairs(&step_pair, 0); + } + Ok(step_pair) + } else { + Err("couldn't parse input".to_string()) + } + } + Err(err) => Err(format!("{}", err)), + } + } + + #[cfg(test)] + fn print_pairs(pair: &Pair, level: usize) { + println!( + "{} {:?} {:?}", + indent_lines(level), + pair.as_rule(), + pair.as_str() + ); + for p in pair.clone().into_inner() { + Self::print_pairs(&p, level + 1) + } + } + + /// the errors here should be unreachable unless there is a bug in the pest grammar /// recursively parse a pair as a Step fn step(pair: Pair) -> Result { match pair.as_rule() { @@ -1451,7 +1484,7 @@ impl CycleParser { pair.clone() .into_inner() .next() - .ok_or_else(|| format!("empty single {}", pair)) + .ok_or(format!("empty single {}", pair)) .and_then(|value_pair| { Ok(Single { string: Rc::from(value_pair.as_str()), @@ -1508,9 +1541,9 @@ impl CycleParser { if p.as_rule() == Rule::choice_op { is_choice = true; } else if is_choice { - let last = choiced_pairs.last_mut().ok_or_else(|| { - "this can never happen as '|' can never start a section".to_string() - })?; + let last = choiced_pairs + .last_mut() + .ok_or("this can never happen as '|' can never start a section")?; last.push(p); is_choice = false } else { @@ -1636,7 +1669,7 @@ impl CycleParser { let count = stack .first() .map(Vec::len) - .ok_or_else(|| format!("empty stack {:?}", stack))?; + .ok_or(format!("empty stack {:?}", stack))?; if stack.len() > 1 && count > 0 { let count = Step::Single(Single { @@ -1666,7 +1699,7 @@ impl CycleParser { let mut inner = pair.clone().into_inner(); let start_pair = inner .next() - .ok_or_else(|| format!("empty expression\n{:?}", pair))?; + .ok_or(format!("empty expression\n{:?}", pair))?; let start = start_pair.as_str().parse::().map_err(|_| { format!( "range expected integer on the left side, got '{}'", @@ -1674,9 +1707,7 @@ impl CycleParser { ) })?; - let end_pair = inner - .next() - .ok_or_else(|| "range expression has no right side".to_string())?; + let end_pair = inner.next().ok_or("range expression has no right side")?; let end = end_pair.as_str().parse::().map_err(|_| { format!( "range expected integer on the right side, got '{}'", @@ -1691,12 +1722,12 @@ impl CycleParser { let steps = inner .next() - .ok_or_else(|| format!("no steps in bjorklund\n{:?}", op_pair)) + .ok_or(format!("no steps in bjorklund\n{:?}", op_pair)) .and_then(Self::step)?; let pulses = inner .next() - .ok_or_else(|| format!("no pulse in bjorklund\n{:?}", op_pair)) + .ok_or(format!("no pulse in bjorklund\n{:?}", op_pair)) .and_then(Self::step)?; let rotate = inner.next().map(Self::step).transpose()?; @@ -1815,7 +1846,7 @@ impl CycleParser { let mut left = Self::step( inner .next() - .ok_or_else(|| format!("empty expression\n{:?}", pair))?, + .ok_or(format!("empty expression\n{:?}", pair))?, )?; // Loop over operators and parameters, creating a nested expression if multiple pairs are present for op_pair in inner { @@ -2286,15 +2317,6 @@ impl Cycle { Ok(events) } - #[cfg(test)] - fn indent_lines(level: usize) -> String { - let mut lines = String::new(); - for i in 0..level { - lines += [" │", " |"][i % 2]; - } - lines - } - #[cfg(test)] fn print_steps(step: &Step, level: usize) { let name = match step { @@ -2318,25 +2340,12 @@ impl Cycle { Step::Degrade(d) => format!("Degrade ? {:?}", d.chance), Step::Bjorklund(_b) => format!("Bjorklund {}", ""), }; - println!("{} {}", Self::indent_lines(level), name); + println!("{} {}", indent_lines(level), name); for step in step.inner_steps() { Self::print_steps(step, level + 1) } } - #[cfg(test)] - fn print_pairs(pair: &Pair, level: usize) { - println!( - "{} {:?} {:?}", - Self::indent_lines(level), - pair.as_rule(), - pair.as_str() - ); - for p in pair.clone().into_inner() { - Self::print_pairs(&p, level + 1) - } - } - #[cfg(test)] fn print(&self) { Self::print_steps(&self.root, 0); @@ -2345,5 +2354,14 @@ impl Cycle { // ------------------------------------------------------------------------------------------------- +#[cfg(test)] +fn indent_lines(level: usize) -> String { + let mut lines = String::new(); + for i in 0..level { + lines += [" │", " |"][i % 2]; + } + lines +} + #[cfg(test)] mod tests; diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index 454f8c4..61a7b74 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -95,6 +95,31 @@ fn variables() -> Result<(), String> { Ok(()) } +#[test] +fn constant_literals() -> Result<(), String> { + assert_eq!( + Cycle::constant_from("c3")?, + Constant::Pitch(Pitch { note: 0, octave: 3 }) + ); + assert_eq!( + Cycle::constant_from("v0.5")?, + Constant::Target(Target::Named("v".into(), Some(0.5))) + ); + assert_eq!(Cycle::constant_from("1.0")?, Constant::Float(1.0)); + assert_eq!(Cycle::constant_from("3.75")?, Constant::Float(3.75)); + assert_eq!(Cycle::constant_from("8")?, Constant::Integer(8)); + assert_eq!(Cycle::constant_from("~")?, Constant::Rest); + assert_eq!(Cycle::constant_from("_")?, Constant::Hold); + assert_eq!(Cycle::constant_from("name")?, Constant::Name("name".into())); + + assert!(Cycle::constant_from("$param").is_err()); + assert!(Cycle::constant_from("[1 2 3]").is_err()); + assert!(Cycle::constant_from("<1 2 3>").is_err()); + assert!(Cycle::constant_from("{{a b c}}%2").is_err()); + assert!(Cycle::constant_from("2 _ 3").is_err()); + Ok(()) +} + #[test] fn parse() -> Result<(), String> { assert!(Cycle::from("a b c [d").is_err()); From e1e98840294a92d86e06fda1e0f70930e11e4734 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sat, 21 Feb 2026 23:55:16 +0000 Subject: [PATCH 10/21] clear import --- src/emitter/scripted_cycle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emitter/scripted_cycle.rs b/src/emitter/scripted_cycle.rs index 15ea4ac..fcc646b 100644 --- a/src/emitter/scripted_cycle.rs +++ b/src/emitter/scripted_cycle.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use num_traits::ToPrimitive; From 1a7694177b6a678c88fbe4f6ad140c09d93e745c Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sun, 22 Feb 2026 00:23:45 +0000 Subject: [PATCH 11/21] cleanup error --- src/tidal/cycle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 055c663..23a1ba5 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -81,7 +81,7 @@ impl Cycle { match single.value { Value::Constant(constant) => Ok(constant), _ => Err(format!( - "variable {}found where constant was expected", + "variable {:?} found where constant was expected", string )), } From 2486027cf275790c5ea0570490448a10d5803277 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Mon, 23 Feb 2026 22:49:00 +0000 Subject: [PATCH 12/21] revert back to lazy errors --- src/tidal/cycle.pest | 2 +- src/tidal/cycle.rs | 28 +++++++++++++--------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index 0ff4cfe..e7cd14d 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -39,7 +39,7 @@ name = @{ ASCII_ALPHANUMERIC ~ (ASCII_ALPHANUMERIC | "_")* } repeat = { "!" } /// possible literals for single steps -constant = _{ hold | rest | chord | target | pitch | number | name } +constant = _{ hold | rest | chord | target | pitch | number | name } single = { constant | variable } choice_op = {"|"} diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 23a1ba5..ba71b55 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -1392,7 +1392,7 @@ impl CycleParser { fn variable_identifier(pair: Pair) -> Result, String> { pair.into_inner() .next() - .ok_or("error in grammar, missing variable name".to_string()) + .ok_or_else(|| "error in grammar, missing variable name".to_string()) .map(|name_pair| Rc::from(name_pair.as_str())) } @@ -1405,14 +1405,12 @@ impl CycleParser { match pair.as_rule() { Rule::variable => Self::variable(pair), Rule::target => { - let name = pair.as_str().get(0..1).ok_or(format!( - "error in grammar, missing target key in pair\n{:?}", - pair - ))?; - let value = pair.clone().into_inner().next().ok_or(format!( - "error in grammar, missing target value in pair\n{:?}", - pair - ))?; + let name = pair.as_str().get(0..1).ok_or_else(|| { + format!("error in grammar, missing target key in pair\n{:?}", pair) + })?; + let value = pair.clone().into_inner().next().ok_or_else(|| { + format!("error in grammar, missing target value in pair\n{:?}", pair) + })?; match name.as_bytes() { b"#" => match value.as_rule() { @@ -1484,7 +1482,7 @@ impl CycleParser { pair.clone() .into_inner() .next() - .ok_or(format!("empty single {}", pair)) + .ok_or_else(|| format!("empty single {}", pair)) .and_then(|value_pair| { Ok(Single { string: Rc::from(value_pair.as_str()), @@ -1669,7 +1667,7 @@ impl CycleParser { let count = stack .first() .map(Vec::len) - .ok_or(format!("empty stack {:?}", stack))?; + .ok_or_else(|| format!("empty stack {:?}", stack))?; if stack.len() > 1 && count > 0 { let count = Step::Single(Single { @@ -1699,7 +1697,7 @@ impl CycleParser { let mut inner = pair.clone().into_inner(); let start_pair = inner .next() - .ok_or(format!("empty expression\n{:?}", pair))?; + .ok_or_else(|| format!("empty expression\n{:?}", pair))?; let start = start_pair.as_str().parse::().map_err(|_| { format!( "range expected integer on the left side, got '{}'", @@ -1722,12 +1720,12 @@ impl CycleParser { let steps = inner .next() - .ok_or(format!("no steps in bjorklund\n{:?}", op_pair)) + .ok_or_else(|| format!("no steps in bjorklund\n{:?}", op_pair)) .and_then(Self::step)?; let pulses = inner .next() - .ok_or(format!("no pulse in bjorklund\n{:?}", op_pair)) + .ok_or_else(|| format!("no pulse in bjorklund\n{:?}", op_pair)) .and_then(Self::step)?; let rotate = inner.next().map(Self::step).transpose()?; @@ -1846,7 +1844,7 @@ impl CycleParser { let mut left = Self::step( inner .next() - .ok_or(format!("empty expression\n{:?}", pair))?, + .ok_or_else(|| format!("empty expression\n{:?}", pair))?, )?; // Loop over operators and parameters, creating a nested expression if multiple pairs are present for op_pair in inner { From cf1c70bddb304120a3cfc645eea5364b94b49ab5 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Tue, 24 Feb 2026 03:40:52 +0000 Subject: [PATCH 13/21] variables as pure subcycles --- src/emitter/cycle.rs | 52 ++- src/lib.rs | 2 +- src/tidal.rs | 2 +- src/tidal/cycle.pest | 10 +- src/tidal/cycle.rs | 745 ++++++++++++++++++++++----------------- src/tidal/cycle/tests.rs | 107 +++--- 6 files changed, 517 insertions(+), 401 deletions(-) diff --git a/src/emitter/cycle.rs b/src/emitter/cycle.rs index f1a4479..a9b2f79 100644 --- a/src/emitter/cycle.rs +++ b/src/emitter/cycle.rs @@ -3,9 +3,9 @@ use std::{cell::RefCell, collections::HashMap, ops::RangeBounds, rc::Rc}; type Fraction = num_rational::Rational32; use crate::{ - event::new_note, BeatTimeBase, Chord, Cycle, CycleEvent, CycleTarget, CycleValue, Emitter, - EmitterEvent, Event, InstrumentId, Note, NoteEvent, Parameter, ParameterSet, ParameterType, - RhythmEvent, + event::new_note, BeatTimeBase, Chord, Cycle, CycleEvent, CycleSubCycle, CycleTarget, + CycleValue, Emitter, EmitterEvent, Event, InstrumentId, Note, NoteEvent, Parameter, + ParameterSet, ParameterType, RhythmEvent, }; // ------------------------------------------------------------------------------------------------- @@ -18,6 +18,7 @@ impl TryFrom<&CycleValue> for Vec> { fn try_from(value: &CycleValue) -> Result { match value { + CycleValue::Null => Ok(vec![None]), CycleValue::Hold => Ok(vec![None]), CycleValue::Rest => Ok(vec![new_note(Note::OFF)]), CycleValue::Float(_f) => Ok(vec![None]), @@ -46,13 +47,13 @@ impl TryFrom<&CycleValue> for Vec> { // ------------------------------------------------------------------------------------------------- /// Convert a [`Parameter`] value to a [`CycleValue`]. -pub type ParameterWithValues = (Rc>, Vec>); +pub type ParameterWithValues = (Rc>, Vec>); impl Parameter { - pub fn into_var(&self, values: &[Option]) -> Option { + pub fn into_var(&self, values: &[Option]) -> Option { match self.parameter_type() { - ParameterType::Boolean => Some(CycleValue::Integer((self.value() >= 0.5) as i32)), - ParameterType::Float => Some(CycleValue::Float(self.value())), - ParameterType::Integer => Some(CycleValue::Integer(self.value().round() as i32)), + ParameterType::Boolean => Some(CycleSubCycle::integer((self.value() >= 0.5) as i32)), + ParameterType::Float => Some(CycleSubCycle::float(self.value())), + ParameterType::Integer => Some(CycleSubCycle::integer(self.value().round() as i32)), ParameterType::Enum => values[self.value().round() as usize].clone(), } } @@ -67,7 +68,7 @@ impl Parameter { p.borrow() .value_strings() .iter() - .map(|s| Cycle::constant_from(s).ok()) + .map(|s| CycleSubCycle::from(s).ok()) .collect() } else { Vec::default() @@ -81,27 +82,19 @@ impl Parameter { // ------------------------------------------------------------------------------------------------- // Conversion helpers for cycle targets -fn float_value_in_range( - maybe_float: &Option, - name: &'static str, - range: Range, -) -> Result +fn float_value_in_range(float: &f64, name: &'static str, range: Range) -> Result where Range: RangeBounds + std::fmt::Debug, { - maybe_float - .map(|v| v as f32) - .ok_or_else(|| format!("{} property must be a number value", name)) - .and_then(|v| { - if range.contains(&v) { - Ok(v) - } else { - Err(format!( - "{} property must be in range [{:?}] but is '{}'", - name, range, v - )) - } - }) + let v = *float as f32; + if range.contains(&v) { + Ok(v) + } else { + Err(format!( + "{} property must be in range [{:?}] but is '{}'", + name, range, v + )) + } } fn integer_value_in_range( @@ -141,7 +134,7 @@ pub(crate) fn apply_cycle_note_properties( note_event.instrument = Some(instrument); } } - CycleTarget::Named(name, value) => match name.as_bytes() { + CycleTarget::NamedFloat(name, value) => match name.as_bytes() { b"v" => { let volume = float_value_in_range(value, "volume", 0.0..=1.0)?; for note_event in note_events.iter_mut().flatten() { @@ -174,6 +167,9 @@ pub(crate) fn apply_cycle_note_properties( + "prefixes here."); } }, + CycleTarget::Named(name) => { + return Err(format!("{} property must be a number value", name)) + } } } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 7356010..9bae68c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ pub use crate::{ sequence::Sequence, tidal::{ Constant as CycleValue, Cycle, Event as CycleEvent, Span as CycleSpan, - Target as CycleTarget, + SubCycle as CycleSubCycle, Target as CycleTarget, }, time::{ BeatTimeBase, BeatTimeStep, ExactSampleTime, SampleTime, SampleTimeBase, SampleTimeDisplay, diff --git a/src/tidal.rs b/src/tidal.rs index 6e11783..83c24c3 100644 --- a/src/tidal.rs +++ b/src/tidal.rs @@ -1,4 +1,4 @@ //! Tidal mini parser and event generator, used as `Emitter`. mod cycle; -pub use cycle::{Constant, Cycle, Event, Span, Target}; +pub use cycle::{Constant, Cycle, Event, Span, SubCycle, Target}; diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index e7cd14d..11c2763 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -40,7 +40,7 @@ repeat = { "!" } /// possible literals for single steps constant = _{ hold | rest | chord | target | pitch | number | name } -single = { constant | variable } +single = { constant } choice_op = {"|"} stack_op = {","} @@ -58,8 +58,8 @@ polymeter = { "{" ~ sections? ~ "}" ~ polymeter_tail? } group = _{ subdivision | alternating | polymeter } /// parameter for expressions with operators -parameter = _{ single | group } -single_parameter = _{ single } +parameter = _{ single | group | variable } +single_parameter = _{ single | variable } /// static operators op_replicate = ${ "!" ~ single_parameter } @@ -76,11 +76,11 @@ op_bjorklund = { "(" ~ (single_parameter ~ ",")+ ~ single_parameter ~ ")" } /// all operators op = _{ op_target | op_degrade | op_replicate | op_weight | op_fast | op_slow | op_bjorklund } -expression = { (single | group) ~ op+ } +expression = { (single | group | variable) ~ op+ } range = ${ integer ~ ".." ~ integer } /// helper container that splits steps into sections -section = _{ ( expression | range | single | repeat | group)+ } +section = _{ ( expression | range | single | repeat | group | variable )+ } /// the root of the cycle mini = { SOI ~ sections? ~ EOI } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index ba71b55..2312f4e 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{ + collections::{HashMap, HashSet}, + rc::Rc, +}; #[cfg(test)] use std::fmt::Display; @@ -20,7 +23,35 @@ const OVERFLOW_ERROR: &str = "Internal error: integer overflow in cycle"; // ------------------------------------------------------------------------------------------------- -type Vars = HashMap, Constant>; +type Vars = HashMap, Step>; + +/// public wrapper over a constant Step containing no variables +/// to be used as a variable for the main cycle +#[derive(Debug, Clone, PartialEq, Default)] +pub struct SubCycle { + step: Step, +} + +impl SubCycle { + pub fn from(input: &str) -> Result { + let cycle = Cycle::from(input)?; + let vars = cycle.root.collect_vars(); + if vars.is_empty() { + Ok(SubCycle { step: cycle.root }) + } else { + Err(format!("cycle contains variables\n{vars:?}")) + } + } + pub fn float(f: f64) -> Self { + Self::new(Step::constant(Constant::Float(f), None)) + } + pub fn integer(i: i32) -> Self { + Self::new(Step::constant(Constant::Integer(i), None)) + } + fn new(step: Step) -> Self { + Self { step } + } +} /// Tidal cycle mini notation parser and event generator. #[derive(Debug, Clone, PartialEq)] @@ -73,20 +104,13 @@ impl Cycle { }) } - pub fn constant_from(input: &str) -> Result { + #[cfg(test)] + fn constant_from(input: &str) -> Result { CycleParser::parse_from_rule(Rule::constant_literal, input).and_then(|root| { let string = root.as_str(); - let single = CycleParser::single(root); - if let Ok(single) = single { - match single.value { - Value::Constant(constant) => Ok(constant), - _ => Err(format!( - "variable {:?} found where constant was expected", - string - )), - } - } else { - Err(format!("single constant expected, found {}", string)) + match CycleParser::single(root)? { + Step::Single(single) => Ok(single.value), + _ => Err(format!("single constant expected, found {}", string)), } }) } @@ -123,20 +147,23 @@ impl Cycle { } } - /// Rebuild/configure cycle to use an table to map variables from - pub fn with_vars(self, vars: Vars) -> Self { - Self { - vars: Some(vars), - ..self + pub fn set_var(&mut self, name: &str, subcycle: SubCycle) { + if let Some(vars) = self.vars.as_mut() { + vars.insert(name.into(), subcycle.step); + } else { + let mut vars = HashMap::new(); + vars.insert(name.into(), subcycle.step); + self.vars = Some(vars); } } - pub fn set_var(&mut self, name: &str, constant: Constant) { + #[cfg(test)] + fn set_var_constant(&mut self, name: &str, constant: Constant) { if let Some(vars) = self.vars.as_mut() { - vars.insert(name.into(), constant); + vars.insert(name.into(), Step::constant(constant, Some(name))); } else { let mut vars = HashMap::new(); - vars.insert(name.into(), constant); + vars.insert(name.into(), Step::constant(constant, Some(name))); self.vars = Some(vars); } } @@ -272,6 +299,7 @@ impl Span { /// Possible types of values that are emitted by a [`Cycle`]. #[derive(Clone, Debug, Default, PartialEq)] pub enum Constant { + Null, #[default] Rest, Hold, @@ -287,7 +315,8 @@ pub enum Constant { #[derive(Clone, Debug, PartialEq)] pub enum Target { Index(i32), - Named(Rc, Option), + NamedFloat(Rc, f64), + Named(Rc), } /// The kind of target used for target assignments @@ -303,10 +332,21 @@ impl Target { // both are indices: compare index values (Self::Index(_), Self::Index(_)) => true, // both are names: compare names only - (Self::Named(a, _), Self::Named(b, _)) => a == b, + (Self::NamedFloat(a, _), Self::NamedFloat(b, _)) => a == b, _ => false, } } + + pub fn to_integer(&self) -> Option { + match self { + Target::Index(i) => Some(*i), + _ => None, + } + } + + pub fn named_float(name: &str, float: f64) -> Self { + Self::NamedFloat(Rc::from(name), float) + } } /// Pitch with note and octave information for cycle events. @@ -326,6 +366,7 @@ impl Pitch { #[derive(Clone, Debug, PartialEq)] enum Step { + Var(Rc), Single(Single), Alternating(Alternating), Subdivision(Subdivision), @@ -341,20 +382,34 @@ enum Step { Static(Static), } +impl Default for Step { + fn default() -> Self { + Self::Single(Single::default()) + } +} + impl Step { + pub fn constant(constant: Constant, string: Option<&str>) -> Self { + Self::Single(Single { + value: constant, + string: Rc::from(string.unwrap_or_default()), + }) + } + #[cfg(test)] fn inner_steps(&self) -> Vec<&Step> { match self { + Step::Var(_) => vec![], Step::Single(_s) => vec![], Step::Alternating(a) => a.steps.iter().collect(), Step::Polymeter(pm) => pm.steps.as_ref().inner_steps(), Step::Subdivision(sd) => sd.steps.iter().collect(), Step::Choices(cs) => cs.choices.iter().collect(), Step::Stack(st) => st.stack.iter().collect(), - Step::SpeedExpression(e) => vec![&e.left, &e.right], - Step::WeightExpression(e) => vec![&e.left], - Step::ReplicateExpression(e) => vec![&e.left], - Step::Degrade(e) => vec![&e.step], + Step::SpeedExpression(e) => vec![&e.step, &e.mult], + Step::WeightExpression(e) => vec![&e.step, &e.weight], + Step::ReplicateExpression(e) => vec![&e.step, &e.count], + Step::Degrade(e) => vec![&e.step, &e.chance], Step::TargetExpression(e) => vec![&e.step, &e.target], Step::Bjorklund(b) => { if let Some(rotation) = &b.rotation { @@ -370,16 +425,65 @@ impl Step { } } - fn length(&self, vars: Option<&Vars>) -> Fraction { + fn get_vars(&self, vars: &mut HashSet>) { match self { - Step::ReplicateExpression(re) => re.count.to_fraction(vars).unwrap_or(Fraction::ONE), - _ => Fraction::ONE, + Step::Var(name) => { + vars.insert(Rc::clone(name)); + } + Step::Single(_s) => (), + Step::Static(_s) => (), + Step::Alternating(a) => a.steps.iter().for_each(|s| s.get_vars(vars)), + + Step::Polymeter(pm) => { + pm.count.get_vars(vars); + pm.steps.get_vars(vars); + } + Step::Subdivision(sd) => sd.steps.iter().for_each(|s| s.get_vars(vars)), + Step::Choices(cs) => cs.choices.iter().for_each(|s| s.get_vars(vars)), + Step::Stack(st) => st.stack.iter().for_each(|s| s.get_vars(vars)), + Step::SpeedExpression(e) => { + e.step.get_vars(vars); + e.mult.get_vars(vars); + } + Step::WeightExpression(e) => { + e.step.get_vars(vars); + e.weight.get_vars(vars); + } + Step::ReplicateExpression(e) => { + e.step.get_vars(vars); + e.count.get_vars(vars); + } + Step::Degrade(e) => { + e.step.get_vars(vars); + e.chance.get_vars(vars); + } + Step::TargetExpression(e) => { + e.step.get_vars(vars); + e.target.get_vars(vars); + } + Step::Bjorklund(b) => { + b.steps.get_vars(vars); + b.pulses.get_vars(vars); + if let Some(rotation) = &b.rotation { + rotation.get_vars(vars); + } + } } } + fn collect_vars(&self) -> HashSet> { + let mut vars = HashSet::new(); + self.get_vars(&mut vars); + vars + } + fn rest() -> Self { - Self::Single(Single::default()) + Self::Single(Single { + value: Constant::Rest, + string: Rc::from("~"), + }) } + fn subdivision(steps: Vec) -> Self { Self::Subdivision(Subdivision { steps }) } @@ -400,104 +504,17 @@ enum Static { Repeat, } -#[derive(Clone, Debug, PartialEq)] -enum Value { - Constant(Constant), - Variable(Rc), - VariableTarget(Rc, Target), -} - -impl Default for Value { - fn default() -> Self { - Self::Constant(Constant::default()) - } -} - -impl Value { - fn to_float(&self, vars: Option<&Vars>) -> Option { - match self { - Self::Constant(l) => l.to_float(), - Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_float(), - } - } - fn to_integer(&self, vars: Option<&Vars>) -> Option { - match self { - Self::Constant(l) => l.to_integer(), - Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_integer(), - } - } - fn to_chance(&self, vars: Option<&Vars>) -> Option { - match self { - Self::Constant(l) => l.to_chance(), - Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_chance(), - } - } - fn to_fraction(&self, vars: Option<&Vars>) -> Option { - match self { - Self::Constant(l) => l.to_fraction(), - Self::Variable(_) | Self::VariableTarget(_, _) => self.to_constant(vars).to_fraction(), - } - } - fn to_target( - &self, - string: &Rc, - kind: Option<&TargetKind>, - vars: Option<&Vars>, - ) -> Option { - kind.and_then(|kind| self.to_target_with_kind(kind, vars)) - .or(self.to_target_without_kind(string, vars)) - } - fn to_target_without_kind(&self, string: &Rc, vars: Option<&Vars>) -> Option { - match self { - Self::Constant(l) => l.to_target_without_kind(string), - Self::Variable(_) | Self::VariableTarget(_, _) => { - self.to_constant(vars).to_target_without_kind(string) - } - } - } - fn to_target_with_kind(&self, kind: &TargetKind, vars: Option<&Vars>) -> Option { - match self { - Self::Constant(l) => l.to_target_with_kind(kind), - Self::Variable(_) | Self::VariableTarget(_, _) => { - self.to_constant(vars).to_target_with_kind(kind) - } - } - } - fn resolve_var(var: Rc, vars: Option<&Vars>) -> Constant { - vars.and_then(|vars| vars.get(&var).cloned()) - .unwrap_or(Constant::Name(Rc::clone(&var))) - } - fn to_constant(&self, vars: Option<&Vars>) -> Constant { - match self { - Self::Constant(l) => l.clone(), - Self::Variable(rc) => Self::resolve_var(Rc::clone(rc), vars), - Self::VariableTarget(rc, target) => { - let constant = Self::resolve_var(Rc::clone(rc), vars); - match target { - Target::Index(_) => constant - .to_integer() - .map(|i| Constant::Target(Target::Index(i))) - .unwrap_or(constant), - Target::Named(n, _) => { - Constant::Target(Target::Named(Rc::clone(n), constant.to_float())) - } - } - } - } - } -} - #[derive(Clone, Debug, PartialEq)] struct Single { - value: Value, + value: Constant, string: Rc, } impl Default for Single { fn default() -> Self { Single { - value: Value::Constant(Constant::Rest), - string: Rc::from("~"), + value: Constant::Null, + string: Rc::from(""), } } } @@ -518,21 +535,6 @@ struct Polymeter { steps: Box, } -impl Polymeter { - fn length(&self, vars: Option<&Vars>) -> Fraction { - if let Step::Subdivision(s) = self.steps.as_ref() { - let l = s - .steps - .iter() - .fold(Fraction::ZERO, |a: Fraction, v: &Step| a + v.length(vars)); - l - } else { - // unreachable - Fraction::ONE - } - } -} - #[derive(Clone, Debug, PartialEq)] struct Choices { choices: Vec, @@ -577,26 +579,26 @@ impl Operator { #[derive(Clone, Debug, PartialEq)] struct SpeedExpression { op: SpeedOp, - left: Box, - right: Box, + step: Box, + mult: Box, } #[derive(Clone, Debug, PartialEq)] struct WeightExpression { - left: Box, - weight: Value, + step: Box, + weight: Box, } #[derive(Clone, Debug, PartialEq)] struct ReplicateExpression { - left: Box, - count: Value, + step: Box, + count: Box, } #[derive(Clone, Debug, PartialEq)] struct Degrade { step: Box, - chance: Value, + chance: Box, } #[derive(Clone, Debug, PartialEq)] @@ -628,7 +630,7 @@ impl Target { } fn from_name(str: Rc) -> Self { - Self::Named(str, None) + Self::Named(str) } } @@ -737,30 +739,31 @@ impl Constant { fn to_integer(&self) -> Option { match &self { + Self::Null => None, Self::Rest => None, Self::Hold => None, Self::Integer(i) => Some(*i), Self::Float(f) => Some(*f as i32), Self::Pitch(n) => Some(n.midi_note() as i32), Self::Chord(p, _m) => Some(p.midi_note() as i32), - Self::Target(t) => match t { - Target::Index(i) => Some(*i), - Target::Named(_, v) => v.map(|f| f as i32), - }, + Self::Target(t) => t.to_integer(), Self::Name(_n) => None, } } fn to_float(&self) -> Option { match &self { + Self::Null => None, Self::Float(f) => Some(*f), Self::Integer(i) => Some(*i as f64), Self::Pitch(n) => Some(n.midi_note() as f64), Self::Chord(n, _m) => Some(n.midi_note() as f64), Self::Target(t) => match t { Target::Index(i) => Some(*i as f64), - Target::Named(_, v) => *v, + Target::NamedFloat(_, f) => Some(*f), + Target::Named(_) => None, }, + Self::Rest => None, Self::Hold => None, Self::Name(_n) => None, @@ -769,6 +772,7 @@ impl Constant { fn to_chance(&self) -> Option { match &self { + Self::Null => None, Self::Rest => None, Self::Hold => None, Self::Integer(i) => Some((*i as f64).clamp(0.0, 100.0) / 100.0), @@ -777,7 +781,8 @@ impl Constant { Self::Chord(p, _m) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), Self::Target(t) => match t { Target::Index(i) => Some(*i as f64), - Target::Named(_, v) => v.map(|f| f.clamp(0.0, 1.0)), + Target::NamedFloat(_, f) => Some(f.clamp(0.0, 1.0)), + Target::Named(_) => None, }, Self::Name(_n) => None, } @@ -789,6 +794,7 @@ impl Constant { fn to_target_without_kind(&self, string: &Rc) -> Option { match self { + Self::Null => None, Self::Target(t) => Some(t.clone()), Self::Integer(i) => Some(Target::from_index(*i)), Self::Name(name) => Some(Target::from_name(Rc::clone(name))), @@ -810,7 +816,7 @@ impl Constant { TargetKind::Index => self.to_integer().map(Target::from_index), TargetKind::Named(name) => self .to_float() - .map(|f| Target::Named(Rc::clone(name), Some(f))), + .map(|f| Target::NamedFloat(Rc::clone(name), f)), }, } } @@ -1017,6 +1023,16 @@ impl Events { }) } + fn named(name: &str) -> Self { + Events::Single(Event { + length: Fraction::ONE, + span: Span::default(), + string: Rc::from(name), + value: Constant::Name(Rc::from(name)), + targets: vec![], + }) + } + fn maybe_poly(poly: PolyEvents) -> Self { if poly.channels.len() == 1 { poly.channels.into_iter().next().expect("len is 1") @@ -1041,6 +1057,14 @@ impl Events { } } + fn first(&self) -> Option { + match self { + Events::Single(s) => Some(s.clone()), + Events::Multi(m) => m.events.first().and_then(Self::first), + Events::Poly(p) => p.channels.first().and_then(Self::first), + } + } + fn get_span(&self) -> Span { match self { Events::Single(s) => s.span.clone(), @@ -1375,7 +1399,8 @@ impl CycleParser { /// recursively parse a pair as a Step fn step(pair: Pair) -> Result { match pair.as_rule() { - Rule::single => Ok(Step::Single(Self::single(pair)?)), + Rule::variable => Self::variable(pair), + Rule::single => Ok(Self::single(pair)?), Rule::repeat => Ok(Step::Static(Static::Repeat)), Rule::subdivision | Rule::mini => Self::group(pair, Step::subdivision), Rule::alternating => Self::group(pair, Step::alternating), @@ -1389,71 +1414,106 @@ impl CycleParser { } } - fn variable_identifier(pair: Pair) -> Result, String> { + fn variable(pair: Pair) -> Result { pair.into_inner() .next() .ok_or_else(|| "error in grammar, missing variable name".to_string()) .map(|name_pair| Rc::from(name_pair.as_str())) + .map(Step::Var) } - fn variable(pair: Pair) -> Result { - Self::variable_identifier(pair).map(Value::Variable) + fn variable_target( + pair: Pair, + target_name: &str, + kind: TargetKind, + ) -> Result { + let name = Rc::from(target_name); + pair.clone() + .into_inner() + .next() + .map(|variable_pair| { + Ok(Step::TargetExpression(TargetExpression { + step: Box::new(Step::Single(Single { + value: Constant::Null, + string: Rc::clone(&name), + })), + kind: Some(kind), + target: Box::new(Self::variable(variable_pair)?), + })) + }) + .ok_or_else(|| format!("error in grammar, unexpected rule for variable\n{pair:?}"))? } /// parse a pair inside a single as a value - fn value(pair: Pair) -> Result { - match pair.as_rule() { - Rule::variable => Self::variable(pair), - Rule::target => { - let name = pair.as_str().get(0..1).ok_or_else(|| { - format!("error in grammar, missing target key in pair\n{:?}", pair) - })?; - let value = pair.clone().into_inner().next().ok_or_else(|| { - format!("error in grammar, missing target value in pair\n{:?}", pair) - })?; - - match name.as_bytes() { - b"#" => match value.as_rule() { - Rule::integer => Ok(Value::Constant(Constant::Target(Target::Index( - Constant::parse_integer(value.as_str())?, - )))), - Rule::variable => Ok(Value::VariableTarget( - Self::variable_identifier(value)?, - Target::Index(0), - )), - _ => Err("error in grammar, unexpected rule for target index".to_string()), - }, - _ => match value.as_rule() { - Rule::float => Ok(Value::Constant(Constant::Target(Target::Named( - Rc::from(name), - Some(Constant::parse_float(value.as_str())?), - )))), - Rule::variable => Ok(Value::VariableTarget( - Self::variable_identifier(value)?, - Target::Named(Rc::from(name), None), - )), - _ => Err("error in grammar, unexpected rule for target float".to_string()), - }, + fn single(pair: Pair) -> Result { + let pair = pair + .clone() + .into_inner() + .next() + .ok_or_else(|| format!("empty single {}", pair))?; + + let string = Rc::from(pair.as_str()); + + let constant = + match pair.as_rule() { + // Rule::variable => Self::variable(pair), + Rule::target => { + let name = pair.as_str().get(0..1).ok_or_else(|| { + format!("error in grammar, missing target key in pair\n{:?}", pair) + })?; + let value = pair.clone().into_inner().next().ok_or_else(|| { + format!("error in grammar, missing target value in pair\n{:?}", pair) + })?; + + match name.as_bytes() { + b"#" => match value.as_rule() { + Rule::integer => Constant::Target(Target::Index( + Constant::parse_integer(value.as_str())?, + )), + Rule::variable => { + return Self::variable_target(pair, name, TargetKind::Index) + } + _ => { + return Err("error in grammar, unexpected rule for target index" + .to_string()) + } + }, + _ => match value.as_rule() { + Rule::float => Constant::Target(Target::NamedFloat( + Rc::from(name), + Constant::parse_float(value.as_str())?, + )), + Rule::variable => { + return Self::variable_target( + pair, + name, + TargetKind::Named(Rc::from(name)), + ) + } + _ => { + return Err("error in grammar, unexpected rule for target float" + .to_string()) + } + }, + } } - } - _ => { - let constant = match pair.as_rule() { - Rule::integer => Constant::from_integer(pair.as_str()), - Rule::float => Constant::from_float(pair.as_str()), + _ => match pair.as_rule() { + Rule::integer => Constant::from_integer(pair.as_str())?, + Rule::float => Constant::from_float(pair.as_str())?, Rule::number => { if let Some(n) = pair.into_inner().next() { match n.as_rule() { - Rule::integer => Constant::from_integer(n.as_str()), - Rule::float => Constant::from_float(n.as_str()), - _ => Err(format!("unrecognized number\n{:?}", n)), + Rule::integer => Constant::from_integer(n.as_str())?, + Rule::float => Constant::from_float(n.as_str())?, + _ => return Err(format!("unrecognized number\n{:?}", n)), } } else { - Err("empty single".to_string()) + return Err("empty single".to_string()); } } - Rule::hold => Ok(Constant::Hold), - Rule::rest => Ok(Constant::Rest), - Rule::pitch => Ok(Constant::Pitch(Pitch::parse(pair))), + Rule::hold => Constant::Hold, + Rule::rest => Constant::Rest, + Rule::pitch => Constant::Pitch(Pitch::parse(pair)), Rule::chord => { let mut pitch = Pitch { note: 0, octave: 4 }; let mut mode = ""; @@ -1468,27 +1528,17 @@ impl CycleParser { _ => (), } } - Ok(Constant::Chord(pitch, Rc::from(mode))) + Constant::Chord(pitch, Rc::from(mode)) } - Rule::name => Ok(Constant::Name(Rc::from(pair.as_str()))), - _ => Err(format!("unrecognized target value\n{:?}", pair)), - }?; - Ok(Value::Constant(constant)) - } - } - } + Rule::name => Constant::Name(Rc::from(pair.as_str())), + _ => return Err(format!("unrecognized target value\n{:?}", pair)), + }, + }; - fn single(pair: Pair) -> Result { - pair.clone() - .into_inner() - .next() - .ok_or_else(|| format!("empty single {}", pair)) - .and_then(|value_pair| { - Ok(Single { - string: Rc::from(value_pair.as_str()), - value: Self::value(value_pair)?, - }) - }) + Ok(Step::Single(Single { + value: constant, + string, + })) } /// transform static steps into their final form and push them onto a list @@ -1507,7 +1557,7 @@ impl CycleParser { }; for i in range { steps.push(Step::Single(Single { - value: Value::Constant(Constant::Integer(i)), + value: Constant::Integer(i), string: Rc::from(i.to_string()), })) } @@ -1671,7 +1721,7 @@ impl CycleParser { if stack.len() > 1 && count > 0 { let count = Step::Single(Single { - value: Value::Constant(Constant::Integer(count as i32)), + value: Constant::Integer(count as i32), string: Rc::from(count.to_string()), }); // if there is a stack but no count, the first section will determine the count of the rest @@ -1742,50 +1792,51 @@ impl CycleParser { String::from("unreachable: missing right hand side from op_pair, error in grammar!") } + fn optional_right_hand(op_pair: Pair, default: fn() -> Step) -> Result { + if let Some(pair) = op_pair.into_inner().next() { + match pair.as_rule() { + Rule::single => Self::single(pair), + Rule::variable => Self::variable(pair), + _ => Err("error in grammar".to_string()), + } + } else { + Ok(default()) + } + } + // TODO allow for a pattern on the right for weight and replicate // with the current pest setup, this seems impossible if we want to support optional parameter here // at least it is impossible without major rearrangement of the grammar and parsing fn weight_expression(left: Step, op_pair: Pair) -> Result { - let weight = if let Some(pair) = op_pair.into_inner().next() { - Self::single(pair)?.value - } else { - Value::Constant(Constant::Float(2.0)) - }; + let weight = Self::optional_right_hand(op_pair, || { + Step::constant(Constant::Float(2.0), Some("2.0")) + })?; Ok(Step::WeightExpression(WeightExpression { - left: Box::new(left), - weight, + step: Box::new(left), + weight: Box::new(weight), })) } fn replicate_expression(left: Step, op_pair: Pair) -> Result { - let count = if let Some(pair) = op_pair.into_inner().next() { - Self::single(pair)?.value - } else { - Value::Constant(Constant::Float(2.0)) - }; + let count = Self::optional_right_hand(op_pair, || { + Step::constant(Constant::Float(2.0), Some("2.0")) + })?; Ok(Step::ReplicateExpression(ReplicateExpression { - left: Box::new(left), - count, + step: Box::new(left), + count: Box::new(count), })) } fn degrade_expression(step: Step, op_pair: Pair) -> Result { - let chance = if let Some(right_pair) = op_pair.into_inner().next() { - right_pair - .into_inner() - .next() - .ok_or_else(Self::invalid_right_hand) - .and_then(Self::single)? - .value - } else { - Value::Constant(Constant::Float(0.5)) - }; + let chance = Self::optional_right_hand(op_pair, || { + Step::constant(Constant::Float(0.5), Some("0.5")) + })?; Ok(Step::Degrade(Degrade { step: Box::new(step), - chance, + chance: Box::new(chance), })) } @@ -1796,8 +1847,8 @@ impl CycleParser { .ok_or_else(Self::invalid_right_hand) .and_then(Self::step)?; Ok(Step::SpeedExpression(SpeedExpression { - left: Box::new(left), - right: Box::new(right), + step: Box::new(left), + mult: Box::new(right), op, })) } @@ -1917,10 +1968,57 @@ impl Cycle { Ok(events) } + fn step_length( + step: &Step, + state: &mut CycleState, + cycle: u32, + limit: usize, + overlap: bool, + vars: Option<&Vars>, + ) -> Result { + let right_events = Self::output(step, state, cycle, limit, overlap, vars)?; + Ok(right_events + .first() + .and_then(|e| e.value.to_fraction()) + .unwrap_or(Fraction::ONE)) + } + // helper to calculate the right multiplier for polymeter and speed expressions - fn step_multiplier(step: &Step, value: &Constant, vars: Option<&Vars>) -> Fraction { - match step { - Step::Polymeter(pm) => value.to_fraction().unwrap_or(Fraction::ZERO) / pm.length(vars), + fn step_multiplier( + step: &Step, + value: &Constant, + state: &mut CycleState, + cycle: u32, + limit: usize, + overlap: bool, + vars: Option<&Vars>, + ) -> Result { + let mult = match step { + Step::Polymeter(pm) => { + let length = if let Step::Subdivision(s) = pm.steps.as_ref() { + let mut a = Fraction::ZERO; + for v in s.steps.iter() { + let inner_length = match v { + Step::ReplicateExpression(re) => Self::step_length( + re.count.as_ref(), + state, + cycle, + limit, + overlap, + vars, + )?, + _ => Fraction::ONE, + }; + a += inner_length + } + a + } else { + // unreachable + Fraction::ONE + }; + + value.to_fraction().unwrap_or(Fraction::ZERO) / length + } Step::SpeedExpression(e) => match e.op { SpeedOp::Fast() => value.to_fraction().unwrap_or(Fraction::ZERO), SpeedOp::Slow() => value @@ -1939,7 +2037,8 @@ impl Cycle { // .to_float() // .and_then(Fraction::from_f64) // .unwrap_or(Fraction::ONE), - } + }; + Ok(mult) } // overlay two lists of events and apply the targets from the second to the first @@ -2008,7 +2107,7 @@ impl Cycle { // assign single value to avoid generating events Step::Single(single) => { let mut events = Self::output(step, state, cycle, limit, overlap, vars)?; - if let Some(target) = single.value.to_target(&single.string, target_kind, vars) { + if let Some(target) = single.value.to_target(&single.string, target_kind) { events.mutate_events(&mut |event: &mut Event| { if !{ let this = &event; @@ -2063,14 +2162,15 @@ impl Cycle { ) -> Result { let left = match step { Step::Polymeter(pm) => pm.steps.as_ref(), - Step::SpeedExpression(exp) => exp.left.as_ref(), + Step::SpeedExpression(exp) => exp.step.as_ref(), _ => step, }; match right { // multiply with single values to avoid generating events Step::Single(single) => { // apply multiplier - let multiplier = Self::step_multiplier(step, &single.value.to_constant(vars), vars); + let multiplier = + Self::step_multiplier(step, &single.value, state, cycle, limit, overlap, vars)?; Ok(Self::output_multiplied( left, state, cycle, multiplier, limit, overlap, vars, )?) @@ -2086,7 +2186,15 @@ impl Cycle { let mut multi_events: Vec = Vec::with_capacity(channel.len()); for event in channel { // apply multiplier - let multiplier = Self::step_multiplier(step, &event.value, vars); + let multiplier = Self::step_multiplier( + step, + &event.value, + state, + cycle, + limit, + overlap, + vars, + )?; let mut partial_events = Self::output_multiplied( left, state, cycle, multiplier, limit, overlap, vars, )?; @@ -2121,6 +2229,17 @@ impl Cycle { vars: Option<&Vars>, ) -> Result { let events = match step { + Step::Var(name) => { + if let Some(vars) = vars { + if let Some(step) = vars.get(name) { + Self::output(step, state, cycle, limit, overlap, Some(vars))? + } else { + Events::named(name) + } + } else { + Events::named(name) + } + } Step::Single(s) => { state.events += 1; if state.events > limit { @@ -2133,7 +2252,7 @@ impl Cycle { length: Fraction::ONE, span: Span::default(), string: Rc::clone(&s.string), - value: s.value.to_constant(vars), + value: s.value.clone(), targets: vec![], }) } @@ -2156,34 +2275,35 @@ impl Cycle { } } Step::WeightExpression(we) => { - // TODO if the right side could be a step like alternating, we could evaluate like so - // let right_events = Self::output(we.weight.as_ref(), state, cycle, limit, overlap)?; - // let weight = right_events - // .first() - // .and_then(|e| e.value.to_fraction()) - // .unwrap_or(Fraction::ONE); + let weight = Self::output(we.weight.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_fraction()) + .unwrap_or(Fraction::ONE); let mut events = - Self::output(we.left.as_ref(), state, cycle, limit, overlap, vars)?; - let weight = we.weight.to_fraction(vars).unwrap_or(Fraction::ONE); + Self::output(we.step.as_ref(), state, cycle, limit, overlap, vars)?; events.set_length(weight); events } Step::ReplicateExpression(we) => { - let count = we.count.to_float(vars).unwrap_or(2.0); + let count = Self::output(we.count.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_float()) + .unwrap_or(2.0); + let ceil = count.ceil(); let len = if ceil == 0.0 { 1 } else { ceil as usize }; let mult = count / ceil; - let steps = vec![we.left.as_ref().clone(); len]; + let steps = vec![we.step.as_ref().clone(); len]; let sub = Step::subdivision(steps); // TODO cache this if the right side is static let step = Step::SpeedExpression(SpeedExpression { op: SpeedOp::Fast(), - left: Box::from(sub), - right: Box::from(Step::Single(Single { - value: Value::Constant(Constant::Float(mult)), + step: Box::from(sub), + mult: Box::from(Step::Single(Single { + value: Constant::Float(mult), string: Rc::from(""), })), }); @@ -2235,9 +2355,13 @@ impl Cycle { } } Step::Degrade(d) => { + let chance = Self::output(d.chance.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_chance()); + let mut out = Self::output(d.step.as_ref(), state, cycle, limit, overlap, vars)?; out.mutate_events(&mut |event: &mut Event| { - if let Some(chance) = d.chance.to_chance(vars) { + if let Some(chance) = chance { if chance < state.rng.random_range(0.0..1.0) { event.value = Constant::Rest } @@ -2249,55 +2373,41 @@ impl Cycle { Self::output_with_target(e, state, cycle, limit, overlap, vars)? } Step::SpeedExpression(e) => { - Self::output_with_speed(e.right.as_ref(), step, state, cycle, limit, overlap, vars)? + Self::output_with_speed(e.mult.as_ref(), step, state, cycle, limit, overlap, vars)? } Step::Bjorklund(b) => { let mut events = vec![]; - #[allow(clippy::single_match)] + + let steps = Self::output(b.steps.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + let pulses = Self::output(b.pulses.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + let rotation = { + if let Some(r) = &b.rotation { + Self::output(r.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0) + } else { + 0 + } + }; + // TODO support something other than Step::Single as the right hand side - match b.pulses.as_ref() { - Step::Single(pulses_single) => { - match b.steps.as_ref() { - Step::Single(steps_single) => { - let rotation = match &b.rotation { - None => None, - Some(r) => match r.as_ref() { - Step::Single(rotation_single) => { - rotation_single.value.to_integer(vars) - } - _ => None, // TODO support something other than Step::Single as rotation - }, - }; - if let Some(steps) = steps_single.value.to_integer(vars) { - if let Some(pulses) = pulses_single.value.to_integer(vars) { - events.reserve(pulses as usize); - let out = Self::output( - b.left.as_ref(), - state, - cycle, - limit, - overlap, - vars, - )?; - for pulse in euclidean( - steps.max(0) as u32, - pulses.max(0) as u32, - rotation.unwrap_or(0), - ) { - if pulse { - events.push(out.clone()) - } else { - events.push(Events::empty()) - } - } - } - } - } - _ => (), // TODO support something other than Step::Single as steps - } + events.reserve(pulses as usize); + let out = Self::output(b.left.as_ref(), state, cycle, limit, overlap, vars)?; + for pulse in euclidean(steps.max(0) as u32, pulses.max(0) as u32, rotation) { + if pulse { + events.push(out.clone()) + } else { + events.push(Events::empty()) } - _ => (), // TODO support something other than Step::Single as pulses } + Events::subdivide_lengths(&mut events); Events::Multi(MultiEvents { span: Span::default(), @@ -2318,8 +2428,9 @@ impl Cycle { #[cfg(test)] fn print_steps(step: &Step, level: usize) { let name = match step { + Step::Var(name) => format!("Var {name:?}"), Step::Single(s) => match &s.value { - Value::Constant(Constant::Pitch(_p)) => format!("{:?} {}", s.value, s.string), + Constant::Pitch(_p) => format!("{:?} {}", s.value, s.string), _ => format!("{:?} {:?}", s.value, s.string), }, Step::Subdivision(sd) => format!("Subdivision [{}]", sd.steps.len()), @@ -2330,7 +2441,7 @@ impl Cycle { Step::SpeedExpression(e) => format!("Speed Expression {:?}", e.op), Step::WeightExpression(we) => format!("Weight Expression {:?}", we.weight), Step::ReplicateExpression(we) => format!("Replicate Expression {:?}", we.count), - Step::TargetExpression(_e) => String::from("Target Expression"), + Step::TargetExpression(e) => format!("Target Expression {:?}", e.kind), Step::Static(s) => match s { Static::Repeat => "Repeat".to_string(), Static::Range(r) => format!("Range {}..{}", r.start, r.end), diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index 61a7b74..a9d082d 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -53,30 +53,30 @@ fn weight_and_replicate() -> Result<(), String> { #[test] fn variables() -> Result<(), String> { - let note = Constant::Pitch(Pitch { note: 0, octave: 4 }); + let note = SubCycle::from("e4")?; let mut cycle = Cycle::from("a b $note d")?; cycle.set_var("note", note); - assert_eq!(cycle.generate(), Cycle::from("a b c d")?.generate()); + assert_eq!(cycle.generate(), Cycle::from("a b e4 d")?.generate()); // unset variables convert into named - let mut cycle = Cycle::from("a b $note d")?; - assert_eq!(cycle.generate(), Cycle::from("a b note d")?.generate()); + let mut cycle = Cycle::from("a $note")?; + assert_eq!(cycle.generate(), Cycle::from("a note")?.generate()); let index = Constant::Integer(12); let mut cycle = Cycle::from("a:$index")?; - cycle.set_var("index", index); + cycle.set_var_constant("index", index); assert_eq!(cycle.generate(), Cycle::from("a:12")?.generate()); - let float = Constant::Float(0.9); - let mut cycle = Cycle::from("a:p$float")?; - cycle.set_var("float", float); - assert_eq!(cycle.generate(), Cycle::from("a:p0.9")?.generate()); + let sub = SubCycle::from("1 2")?; + let mut cycle = Cycle::from("a*$sub")?; + cycle.set_var("sub", sub); + assert_eq!(cycle.generate(), Cycle::from("a*[1 2]")?.generate()); let f1 = Constant::Float(0.5); let f2 = Constant::Float(0.9); let mut cycle = Cycle::from("[a b c d]:p=[$f1 $f2]")?; - cycle.set_var("f1", f1); - cycle.set_var("f2", f2); + cycle.set_var_constant("f1", f1); + cycle.set_var_constant("f2", f2); assert_eq!( cycle.generate(), Cycle::from("[a b c d]:p=[0.5 0.9]")?.generate() @@ -84,14 +84,23 @@ fn variables() -> Result<(), String> { let mult = Constant::Float(2.0); let mut cycle = Cycle::from("a*$mult")?; - cycle.set_var("mult", mult); + cycle.set_var_constant("mult", mult); assert_eq!(cycle.generate(), Cycle::from("a*2")?.generate()); let length = Constant::Float(3.0); let mut cycle = Cycle::from("a@$length b")?; - cycle.set_var("length", length); + cycle.set_var_constant("length", length); assert_eq!(cycle.generate(), Cycle::from("a@3 b")?.generate()); + let float = Constant::Float(0.9); + let mut cycle = Cycle::from("a:p$float")?; + cycle.set_var_constant("float", float); + assert_eq!(cycle.generate(), Cycle::from("a:p0.9")?.generate()); + + assert!(SubCycle::from("a b c d").is_ok()); + assert!(SubCycle::from("[a b c d]*$mult").is_err()); + assert!(SubCycle::from("$note $note $note").is_err()); + assert!(SubCycle::from("a:p=<0.5 0.2 $right>").is_err()); Ok(()) } @@ -103,7 +112,7 @@ fn constant_literals() -> Result<(), String> { ); assert_eq!( Cycle::constant_from("v0.5")?, - Constant::Target(Target::Named("v".into(), Some(0.5))) + Constant::Target(Target::named_float("v", 0.5)) ); assert_eq!(Cycle::constant_from("1.0")?, Constant::Float(1.0)); assert_eq!(Cycle::constant_from("3.75")?, Constant::Float(3.75)); @@ -152,7 +161,7 @@ fn targets() -> Result<(), String> { Cycle::from("a:v=0.5")?.generate()?, [[Event::at(Fraction::from(0), Fraction::new(1, 1)) .with_note(9, 4) - .with_target(Target::Named("v".into(), Some(0.5)))]] + .with_target(Target::named_float("v", 0.5))]] ); assert_eq!( @@ -418,10 +427,10 @@ fn targets() -> Result<(), String> { Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) .with_note(11, 4) .with_targets(vec![ - Target::Named("v".into(), Some(0.1)), + Target::named_float("v", 0.1), // second v should not be applied - Target::Named("p".into(), Some(1.0)), - Target::Named("g".into(), Some(100.0)), + Target::named_float("p", 1.0), + Target::named_float("g", 100.0), ]) ]] ); @@ -925,26 +934,26 @@ fn expression_chains() -> Result<(), String> { vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) .with_note(9, 4) .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.1)), + Target::named_float("p", 0.5), + Target::named_float("v", 0.1), ])]], vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) .with_note(11, 4) .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.1)), + Target::named_float("p", 0.5), + Target::named_float("v", 0.1), ])]], vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) .with_note(0, 4) .with_targets(vec![ - Target::Named("v".into(), Some(0.2)), - Target::Named("p".into(), Some(0.5)), + Target::named_float("v", 0.2), + Target::named_float("p", 0.5), ])]], vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) .with_note(2, 4) .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.3)), + Target::named_float("p", 0.5), + Target::named_float("v", 0.3), ])]], ], )?; @@ -1001,16 +1010,16 @@ fn target_assign() -> Result<(), String> { [[ Event::at(Fraction::from(0), Fraction::new(1, 4)) .with_int(1) - .with_target(Target::Named("p".into(), Some(0.1))), + .with_target(Target::named_float("p", 0.1)), Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) .with_int(2) - .with_target(Target::Named("p".into(), Some(0.2))), + .with_target(Target::named_float("p", 0.2)), Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) .with_int(3) - .with_target(Target::Named("p".into(), Some(0.3))), + .with_target(Target::named_float("p", 0.3)), Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) .with_int(4) - .with_target(Target::Named("p".into(), Some(0.4))), + .with_target(Target::named_float("p", 0.4)), ]] ); assert_eq!( @@ -1036,14 +1045,14 @@ fn target_assign() -> Result<(), String> { [[ Event::at(Fraction::from(0), Fraction::new(1, 4)) .with_int(1) - .with_target(Target::Named("long".into(), Some(1.0))), + .with_target(Target::named_float("long", 1.0)), Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) .with_int(2) - .with_target(Target::Named("long".into(), Some(1.0))), + .with_target(Target::named_float("long", 1.0)), Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) .with_int(4) - .with_target(Target::Named("long".into(), Some(0.2))), + .with_target(Target::named_float("long", 0.2)), ]] ); @@ -1054,56 +1063,56 @@ fn target_assign() -> Result<(), String> { Event::at(Fraction::from(0), Fraction::new(1, 2)) .with_int(1) .with_targets(vec![ - Target::Named("d".into(), Some(0.1)), - Target::Named("v".into(), Some(0.3)), + Target::named_float("d", 0.1), + Target::named_float("v", 0.3), ]), Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) .with_int(2) .with_targets(vec![ - Target::Named("d".into(), Some(0.1)), - Target::Named("v".into(), Some(0.3)), + Target::named_float("d", 0.1), + Target::named_float("v", 0.3), ]), ]], vec![vec![ Event::at(Fraction::from(0), Fraction::new(1, 2)) .with_int(3) .with_targets(vec![ - Target::Named("d".into(), Some(0.2)), - Target::Named("v".into(), Some(0.3)), + Target::named_float("d", 0.2), + Target::named_float("v", 0.3), ]), Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) .with_int(4) .with_targets(vec![ - Target::Named("d".into(), Some(0.2)), - Target::Named("v".into(), Some(0.2)), + Target::named_float("d", 0.2), + Target::named_float("v", 0.2), ]), ]], vec![vec![ Event::at(Fraction::from(0), Fraction::new(1, 2)) .with_int(5) .with_targets(vec![ - Target::Named("d".into(), Some(0.3)), - Target::Named("v".into(), Some(0.2)), + Target::named_float("d", 0.3), + Target::named_float("v", 0.2), ]), Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) .with_int(6) .with_targets(vec![ - Target::Named("d".into(), Some(0.3)), - Target::Named("v".into(), Some(0.2)), + Target::named_float("d", 0.3), + Target::named_float("v", 0.2), ]), ]], vec![vec![ Event::at(Fraction::from(0), Fraction::new(1, 2)) .with_int(7) .with_targets(vec![ - Target::Named("d".into(), Some(0.4)), - Target::Named("v".into(), Some(0.1)), + Target::named_float("d", 0.4), + Target::named_float("v", 0.1), ]), Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) .with_int(8) .with_targets(vec![ - Target::Named("d".into(), Some(0.4)), - Target::Named("v".into(), Some(0.1)), + Target::named_float("d", 0.4), + Target::named_float("v", 0.1), ]), ]], ], From c3ce7477efddb7a84f84d6106090d254c2aeec05 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Tue, 24 Feb 2026 04:23:58 +0000 Subject: [PATCH 14/21] make ranges dynamic, clean naming --- src/tidal/cycle.pest | 2 +- src/tidal/cycle.rs | 164 ++++++++++++++++++++++++++----------------- 2 files changed, 101 insertions(+), 65 deletions(-) diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index 11c2763..d4b8d83 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -78,7 +78,7 @@ op = _{ op_target | op_degrade | op_replicate | op_weight | op_fast | expression = { (single | group | variable) ~ op+ } -range = ${ integer ~ ".." ~ integer } +range = ${ (integer | variable) ~ ".." ~ (integer | variable) } /// helper container that splits steps into sections section = _{ ( expression | range | single | repeat | group | variable )+ } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 2312f4e..942a35c 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -374,12 +374,13 @@ enum Step { Stack(Stack), Choices(Choices), SpeedExpression(SpeedExpression), - ReplicateExpression(ReplicateExpression), - WeightExpression(WeightExpression), - TargetExpression(TargetExpression), - Degrade(Degrade), + Replicated(Replicated), + Weighted(Weighted), + Targeted(Targeted), + Degrade(Degraded), Bjorklund(Bjorklund), Static(Static), + Ranged(Ranged), } impl Default for Step { @@ -407,10 +408,10 @@ impl Step { Step::Choices(cs) => cs.choices.iter().collect(), Step::Stack(st) => st.stack.iter().collect(), Step::SpeedExpression(e) => vec![&e.step, &e.mult], - Step::WeightExpression(e) => vec![&e.step, &e.weight], - Step::ReplicateExpression(e) => vec![&e.step, &e.count], + Step::Weighted(e) => vec![&e.step, &e.weight], + Step::Replicated(e) => vec![&e.step, &e.count], Step::Degrade(e) => vec![&e.step, &e.chance], - Step::TargetExpression(e) => vec![&e.step, &e.target], + Step::Targeted(e) => vec![&e.step, &e.target], Step::Bjorklund(b) => { if let Some(rotation) = &b.rotation { vec![&b.left, &b.steps, &b.pulses, &**rotation] @@ -420,8 +421,8 @@ impl Step { } Step::Static(s) => match s { Static::Repeat => vec![], - Static::Range(_) => vec![], }, + Step::Ranged(r) => vec![&r.start, &r.end], } } @@ -445,11 +446,11 @@ impl Step { e.step.get_vars(vars); e.mult.get_vars(vars); } - Step::WeightExpression(e) => { + Step::Weighted(e) => { e.step.get_vars(vars); e.weight.get_vars(vars); } - Step::ReplicateExpression(e) => { + Step::Replicated(e) => { e.step.get_vars(vars); e.count.get_vars(vars); } @@ -457,7 +458,7 @@ impl Step { e.step.get_vars(vars); e.chance.get_vars(vars); } - Step::TargetExpression(e) => { + Step::Targeted(e) => { e.step.get_vars(vars); e.target.get_vars(vars); } @@ -468,6 +469,10 @@ impl Step { rotation.get_vars(vars); } } + Step::Ranged(r) => { + r.start.get_vars(vars); + r.end.get_vars(vars); + } } } @@ -500,7 +505,6 @@ impl Step { #[derive(Clone, Debug, PartialEq)] enum Static { - Range(Range), Repeat, } @@ -584,25 +588,25 @@ struct SpeedExpression { } #[derive(Clone, Debug, PartialEq)] -struct WeightExpression { +struct Weighted { step: Box, weight: Box, } #[derive(Clone, Debug, PartialEq)] -struct ReplicateExpression { +struct Replicated { step: Box, count: Box, } #[derive(Clone, Debug, PartialEq)] -struct Degrade { +struct Degraded { step: Box, chance: Box, } #[derive(Clone, Debug, PartialEq)] -struct TargetExpression { +struct Targeted { step: Box, kind: Option, target: Box, @@ -617,9 +621,9 @@ struct Bjorklund { } #[derive(Clone, Debug, PartialEq)] -struct Range { - start: i32, - end: i32, +struct Ranged { + start: Box, + end: Box, } // ------------------------------------------------------------------------------------------------- @@ -1432,7 +1436,7 @@ impl CycleParser { .into_inner() .next() .map(|variable_pair| { - Ok(Step::TargetExpression(TargetExpression { + Ok(Step::Targeted(Targeted { step: Box::new(Step::Single(Single { value: Constant::Null, string: Rc::clone(&name), @@ -1549,19 +1553,6 @@ impl CycleParser { let repeat = steps.last().cloned().unwrap_or(Step::rest()); steps.push(repeat) } - Static::Range(r) => { - let range = if r.start <= r.end { - Box::new(r.start..=r.end) as Box> - } else { - Box::new((r.end..=r.start).rev()) as Box> - }; - for i in range { - steps.push(Step::Single(Single { - value: Constant::Integer(i), - string: Rc::from(i.to_string()), - })) - } - } }, _ => steps.push(step), } @@ -1743,26 +1734,37 @@ impl CycleParser { } } + fn integer_or_variable(pair: Pair) -> Result { + match pair.as_rule() { + Rule::integer => Ok(Step::constant( + Constant::Integer(pair.as_str().parse::().map_err(|_| { + format!( + "error in grammar, integer cannot be parsed '{}'", + pair.as_str() + ) + })?), + Some(pair.as_str()), + )), + Rule::variable => Self::variable(pair), + _ => Err("error in grammar".to_string()), + } + } + fn range(pair: Pair) -> Result { let mut inner = pair.clone().into_inner(); + let start_pair = inner .next() - .ok_or_else(|| format!("empty expression\n{:?}", pair))?; - let start = start_pair.as_str().parse::().map_err(|_| { - format!( - "range expected integer on the left side, got '{}'", - start_pair.as_str() - ) - })?; + .ok_or_else(|| format!("error in grammar, empty range expression\n{:?}", pair))?; - let end_pair = inner.next().ok_or("range expression has no right side")?; - let end = end_pair.as_str().parse::().map_err(|_| { - format!( - "range expected integer on the right side, got '{}'", - end_pair.as_str() - ) - })?; - Ok(Step::Static(Static::Range(Range { start, end }))) + let end_pair = inner + .next() + .ok_or("error in grammar, incomplete range expression")?; + + Ok(Step::Ranged(Ranged { + start: Box::new(Self::integer_or_variable(start_pair)?), + end: Box::new(Self::integer_or_variable(end_pair)?), + })) } fn bjorklund(left: Step, op_pair: Pair) -> Result { @@ -1812,7 +1814,7 @@ impl CycleParser { Step::constant(Constant::Float(2.0), Some("2.0")) })?; - Ok(Step::WeightExpression(WeightExpression { + Ok(Step::Weighted(Weighted { step: Box::new(left), weight: Box::new(weight), })) @@ -1823,7 +1825,7 @@ impl CycleParser { Step::constant(Constant::Float(2.0), Some("2.0")) })?; - Ok(Step::ReplicateExpression(ReplicateExpression { + Ok(Step::Replicated(Replicated { step: Box::new(left), count: Box::new(count), })) @@ -1834,7 +1836,7 @@ impl CycleParser { Step::constant(Constant::Float(0.5), Some("0.5")) })?; - Ok(Step::Degrade(Degrade { + Ok(Step::Degrade(Degraded { step: Box::new(step), chance: Box::new(chance), })) @@ -1882,7 +1884,7 @@ impl CycleParser { _ => (None, right), }; - Ok(Step::TargetExpression(TargetExpression { + Ok(Step::Targeted(Targeted { step: Box::new(left), kind, target: Box::new(Self::step(target)?), @@ -1999,7 +2001,7 @@ impl Cycle { let mut a = Fraction::ZERO; for v in s.steps.iter() { let inner_length = match v { - Step::ReplicateExpression(re) => Self::step_length( + Step::Replicated(re) => Self::step_length( re.count.as_ref(), state, cycle, @@ -2093,7 +2095,7 @@ impl Cycle { // generate events from Target expressions fn output_with_target( - exp: &TargetExpression, + exp: &Targeted, state: &mut CycleState, cycle: u32, limit: usize, @@ -2274,7 +2276,7 @@ impl Cycle { }) } } - Step::WeightExpression(we) => { + Step::Weighted(we) => { let weight = Self::output(we.weight.as_ref(), state, cycle, limit, overlap, vars)? .first() .and_then(|e| e.value.to_fraction()) @@ -2285,7 +2287,7 @@ impl Cycle { events.set_length(weight); events } - Step::ReplicateExpression(we) => { + Step::Replicated(we) => { let count = Self::output(we.count.as_ref(), state, cycle, limit, overlap, vars)? .first() .and_then(|e| e.value.to_float()) @@ -2298,7 +2300,7 @@ impl Cycle { let steps = vec![we.step.as_ref().clone(); len]; let sub = Step::subdivision(steps); - // TODO cache this if the right side is static + // PERF cache this if the right side is static let step = Step::SpeedExpression(SpeedExpression { op: SpeedOp::Fast(), step: Box::from(sub), @@ -2369,9 +2371,7 @@ impl Cycle { }); out } - Step::TargetExpression(e) => { - Self::output_with_target(e, state, cycle, limit, overlap, vars)? - } + Step::Targeted(e) => Self::output_with_target(e, state, cycle, limit, overlap, vars)?, Step::SpeedExpression(e) => { Self::output_with_speed(e.mult.as_ref(), step, state, cycle, limit, overlap, vars)? } @@ -2421,6 +2421,42 @@ impl Cycle { // Range and Expression should be applied in Self::push_applied Events::empty() } + Step::Ranged(r) => { + let start = Self::output(r.start.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + let end = Self::output(r.end.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + + let length = (start - end).abs(); + let range = if start <= end { + Box::new(start..=end) as Box> + } else { + Box::new((end..=start).rev()) as Box> + }; + + // PERF cache this if the range is static + let step = Step::Weighted(Weighted { + weight: Box::new(Step::constant(Constant::Integer(length), None)), + step: Box::new(Step::Subdivision(Subdivision { + steps: { + let mut steps = vec![]; + for i in range { + steps.push(Step::Single(Single { + value: Constant::Integer(i), + string: Rc::from(i.to_string()), + })) + } + steps + }, + })), + }); + + Self::output(&step, state, cycle, limit, overlap, vars)? + } }; Ok(events) } @@ -2439,15 +2475,15 @@ impl Cycle { Step::Choices(cs) => format!("Choices |{}|", cs.choices.len()), Step::Stack(st) => format!("Stack ({})", st.stack.len()), Step::SpeedExpression(e) => format!("Speed Expression {:?}", e.op), - Step::WeightExpression(we) => format!("Weight Expression {:?}", we.weight), - Step::ReplicateExpression(we) => format!("Replicate Expression {:?}", we.count), - Step::TargetExpression(e) => format!("Target Expression {:?}", e.kind), + Step::Weighted(we) => format!("Weight Expression {:?}", we.weight), + Step::Replicated(we) => format!("Replicate Expression {:?}", we.count), + Step::Targeted(e) => format!("Target Expression {:?}", e.kind), Step::Static(s) => match s { Static::Repeat => "Repeat".to_string(), - Static::Range(r) => format!("Range {}..{}", r.start, r.end), }, Step::Degrade(d) => format!("Degrade ? {:?}", d.chance), Step::Bjorklund(_b) => format!("Bjorklund {}", ""), + Step::Ranged(_) => "Ranged".to_string(), }; println!("{} {}", indent_lines(level), name); for step in step.inner_steps() { From 004bf5bcda158c909f9d0563179bb7c65e611a05 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Tue, 24 Feb 2026 04:26:24 +0000 Subject: [PATCH 15/21] add variable symbol to stateful marks --- src/tidal/cycle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 942a35c..a1512ec 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -171,7 +171,7 @@ impl Cycle { /// Check if a cycle may give different outputs between cycles. pub fn is_stateful(&self) -> bool { // TODO improve: * and / can change the output, <1> does not etc.. - self.input.contains(['<', '{', '|', '?', '/', '*']) + self.input.contains(['<', '{', '|', '?', '/', '*', '$']) } /// When the cycle got created from a script source, From eb8977ce9b66b71c5a41f1b203cff74f87ff75a2 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Tue, 24 Feb 2026 05:19:48 +0000 Subject: [PATCH 16/21] try handling error for subcycle parsing --- src/emitter/cycle.rs | 58 ++++++++++++++++++----------------- src/emitter/scripted_cycle.rs | 34 ++++++++++++-------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/emitter/cycle.rs b/src/emitter/cycle.rs index a9b2f79..0144404 100644 --- a/src/emitter/cycle.rs +++ b/src/emitter/cycle.rs @@ -47,35 +47,33 @@ impl TryFrom<&CycleValue> for Vec> { // ------------------------------------------------------------------------------------------------- /// Convert a [`Parameter`] value to a [`CycleValue`]. -pub type ParameterWithValues = (Rc>, Vec>); +pub type ParameterWithValues = (Rc>, Vec); impl Parameter { - pub fn into_var(&self, values: &[Option]) -> Option { + pub fn into_var(&self, values: &[CycleSubCycle]) -> CycleSubCycle { match self.parameter_type() { - ParameterType::Boolean => Some(CycleSubCycle::integer((self.value() >= 0.5) as i32)), - ParameterType::Float => Some(CycleSubCycle::float(self.value())), - ParameterType::Integer => Some(CycleSubCycle::integer(self.value().round() as i32)), + ParameterType::Boolean => CycleSubCycle::integer((self.value() >= 0.5) as i32), + ParameterType::Float => CycleSubCycle::float(self.value()), + ParameterType::Integer => CycleSubCycle::integer(self.value().round() as i32), ParameterType::Enum => values[self.value().round() as usize].clone(), } } - pub fn set_with_values(parameters: &ParameterSet) -> Vec { - parameters - .iter() - .map(|p| { - ( - Rc::clone(p), - if p.borrow().parameter_type() == ParameterType::Enum { - p.borrow() - .value_strings() - .iter() - .map(|s| CycleSubCycle::from(s).ok()) - .collect() - } else { - Vec::default() - }, - ) - }) - .collect() + pub fn parse_subcycles(parameters: &ParameterSet) -> Result, String> { + let mut result = vec![]; + for p in parameters.iter() { + result.push((Rc::clone(p), { + if p.borrow().parameter_type() == ParameterType::Enum { + let mut values = vec![]; + for string in p.borrow().value_strings().iter() { + values.push(CycleSubCycle::from(string)?) + } + values + } else { + Vec::default() + } + })) + } + Ok(result) } } @@ -341,10 +339,8 @@ impl CycleEmitter { // inject parameter values into the cycle as variables for (parameter_ref, enum_values) in &self.parameters { let parameter = parameter_ref.borrow(); - self.cycle.set_var( - parameter.id(), - parameter.into_var(enum_values).unwrap_or_default(), - ); + self.cycle + .set_var(parameter.id(), parameter.into_var(enum_values)); } // run the cycle event generator let events = { @@ -390,7 +386,13 @@ impl Emitter for CycleEmitter { } fn set_parameters(&mut self, parameters: ParameterSet) { - self.parameters = Parameter::set_with_values(¶meters); + match Parameter::parse_subcycles(¶meters) { + Ok(parameters) => self.parameters = parameters, + Err(err) => { + // FIX handle this more gracefully? + panic!("{err}") + } + } } fn run(&mut self, _pulse: RhythmEvent, emit_event: bool) -> Option> { diff --git a/src/emitter/scripted_cycle.rs b/src/emitter/scripted_cycle.rs index fcc646b..b3534bd 100644 --- a/src/emitter/scripted_cycle.rs +++ b/src/emitter/scripted_cycle.rs @@ -137,10 +137,8 @@ impl ScriptedCycleEmitter { // inject parameter values into cycle as variables for (parameter_ref, enum_values) in &self.parameters { let parameter = parameter_ref.borrow(); - self.cycle.set_var( - parameter.id(), - parameter.into_var(enum_values).unwrap_or_default(), - ); + self.cycle + .set_var(parameter.id(), parameter.into_var(enum_values)); } // run the cycle event generator let events = { @@ -298,15 +296,25 @@ impl Emitter for ScriptedCycleEmitter { } fn set_parameters(&mut self, parameters: ParameterSet) { - // store parameters, so we can inject them in generate() - self.parameters = Parameter::set_with_values(¶meters); - // and pass them to the mapping callback context - if let Some(timeout_hook) = &mut self.timeout_hook { - timeout_hook.reset(); - } - if let Some(callback) = &mut self.mapping_callback { - if let Err(err) = callback.set_context_parameters(parameters) { - callback.handle_error(&err); + match Parameter::parse_subcycles(¶meters) { + Ok(with_subcycles) => { + // store parameters, so we can inject them in generate() + self.parameters = with_subcycles; + + // and pass them to the mapping callback context + if let Some(timeout_hook) = &mut self.timeout_hook { + timeout_hook.reset(); + } + + if let Some(callback) = &mut self.mapping_callback { + if let Err(err) = callback.set_context_parameters(parameters) { + callback.handle_error(&err); + } + } + } + Err(err) => { + // FIX make error point to the enum def? + add_lua_callback_error(None, None, "enum".to_string(), LuaError::RuntimeError(err)); } } } From 5ac768ac86b10f31d39de674935cd1bf777eb9fc Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sat, 28 Feb 2026 21:34:13 +0000 Subject: [PATCH 17/21] make repeat dynamic, alternating as polymeter, generalize polymeter for dynamic lengths --- src/tidal/cycle.rs | 530 +++++++++++++++++---------------------- src/tidal/cycle/tests.rs | 239 ++++++++++++------ 2 files changed, 389 insertions(+), 380 deletions(-) diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index a1512ec..bc8eff7 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -157,17 +157,6 @@ impl Cycle { } } - #[cfg(test)] - fn set_var_constant(&mut self, name: &str, constant: Constant) { - if let Some(vars) = self.vars.as_mut() { - vars.insert(name.into(), Step::constant(constant, Some(name))); - } else { - let mut vars = HashMap::new(); - vars.insert(name.into(), Step::constant(constant, Some(name))); - self.vars = Some(vars); - } - } - /// Check if a cycle may give different outputs between cycles. pub fn is_stateful(&self) -> bool { // TODO improve: * and / can change the output, <1> does not etc.. @@ -368,7 +357,6 @@ impl Pitch { enum Step { Var(Rc), Single(Single), - Alternating(Alternating), Subdivision(Subdivision), Polymeter(Polymeter), Stack(Stack), @@ -379,8 +367,8 @@ enum Step { Targeted(Targeted), Degrade(Degraded), Bjorklund(Bjorklund), - Static(Static), Ranged(Ranged), + Repeat, } impl Default for Step { @@ -402,14 +390,13 @@ impl Step { match self { Step::Var(_) => vec![], Step::Single(_s) => vec![], - Step::Alternating(a) => a.steps.iter().collect(), - Step::Polymeter(pm) => pm.steps.as_ref().inner_steps(), + Step::Polymeter(p) => p.stack.iter().collect(), Step::Subdivision(sd) => sd.steps.iter().collect(), Step::Choices(cs) => cs.choices.iter().collect(), Step::Stack(st) => st.stack.iter().collect(), Step::SpeedExpression(e) => vec![&e.step, &e.mult], Step::Weighted(e) => vec![&e.step, &e.weight], - Step::Replicated(e) => vec![&e.step, &e.count], + Step::Replicated(e) => vec![&e.step, &e.repeats], Step::Degrade(e) => vec![&e.step, &e.chance], Step::Targeted(e) => vec![&e.step, &e.target], Step::Bjorklund(b) => { @@ -419,10 +406,8 @@ impl Step { vec![&b.left, &b.steps, &b.pulses] } } - Step::Static(s) => match s { - Static::Repeat => vec![], - }, Step::Ranged(r) => vec![&r.start, &r.end], + Step::Repeat => vec![], } } @@ -432,12 +417,11 @@ impl Step { vars.insert(Rc::clone(name)); } Step::Single(_s) => (), - Step::Static(_s) => (), - Step::Alternating(a) => a.steps.iter().for_each(|s| s.get_vars(vars)), - Step::Polymeter(pm) => { - pm.count.get_vars(vars); - pm.steps.get_vars(vars); + pm.stack.iter().for_each(|s| s.get_vars(vars)); + if let Some(c) = pm.count.as_ref() { + c.get_vars(vars) + } } Step::Subdivision(sd) => sd.steps.iter().for_each(|s| s.get_vars(vars)), Step::Choices(cs) => cs.choices.iter().for_each(|s| s.get_vars(vars)), @@ -452,7 +436,7 @@ impl Step { } Step::Replicated(e) => { e.step.get_vars(vars); - e.count.get_vars(vars); + e.repeats.get_vars(vars); } Step::Degrade(e) => { e.step.get_vars(vars); @@ -473,6 +457,7 @@ impl Step { r.start.get_vars(vars); r.end.get_vars(vars); } + Step::Repeat => (), } } @@ -489,25 +474,23 @@ impl Step { }) } - fn subdivision(steps: Vec) -> Self { + fn subdivision(steps: Vec) -> Self { Self::Subdivision(Subdivision { steps }) } - fn alternating(steps: Vec) -> Self { - Self::Alternating(Alternating { steps }) + fn alternating(steps: Vec) -> Self { + Self::polymeter( + vec![steps], + Some(Self::constant(Constant::Integer(1), None)), + ) } - fn polymeter(steps: Vec, count: Step) -> Self { - Step::Polymeter(Polymeter { - steps: Box::new(Step::subdivision(steps)), - count: Box::new(count), + fn polymeter(stack: Vec>, count: Option) -> Self { + Self::Polymeter(Polymeter { + stack: stack.into_iter().map(Self::subdivision).collect(), + count: count.map(Box::new), }) } } -#[derive(Clone, Debug, PartialEq)] -enum Static { - Repeat, -} - #[derive(Clone, Debug, PartialEq)] struct Single { value: Constant, @@ -523,11 +506,6 @@ impl Default for Single { } } -#[derive(Clone, Debug, PartialEq)] -struct Alternating { - steps: Vec, -} - #[derive(Clone, Debug, PartialEq)] struct Subdivision { steps: Vec, @@ -535,8 +513,9 @@ struct Subdivision { #[derive(Clone, Debug, PartialEq)] struct Polymeter { - count: Box, - steps: Box, + // a list of exclusively Subdivision steps + stack: Vec, + count: Option>, } #[derive(Clone, Debug, PartialEq)] @@ -553,6 +532,7 @@ struct Stack { enum SpeedOp { Fast(), // * Slow(), // / + Fit(Fraction), } #[derive(Clone, Debug, PartialEq)] @@ -596,7 +576,7 @@ struct Weighted { #[derive(Clone, Debug, PartialEq)] struct Replicated { step: Box, - count: Box, + repeats: Box, } #[derive(Clone, Debug, PartialEq)] @@ -1037,11 +1017,27 @@ impl Events { }) } - fn maybe_poly(poly: PolyEvents) -> Self { - if poly.channels.len() == 1 { - poly.channels.into_iter().next().expect("len is 1") - } else { - Self::Poly(poly) + fn collapsed_poly(events: Vec, length: Fraction, span: Span) -> Self { + match events.len() { + 0 => Self::empty(), + 1 => events.first().expect("len is 1").to_owned(), + _ => Self::Poly(PolyEvents { + length, + span, + channels: events, + }), + } + } + + fn collapsed_multi(events: Vec, length: Fraction, span: Span) -> Self { + match events.len() { + 0 => Self::empty(), + 1 => events.first().expect("len is 1").to_owned(), + _ => Self::Multi(MultiEvents { + span, + length, + events, + }), } } @@ -1318,7 +1314,6 @@ impl Events { #[cfg(test)] { - self.print(0); println!("\nOUTPUT"); let channel_count = channels.len(); for (ci, channel) in channels.iter().enumerate() { @@ -1333,32 +1328,6 @@ impl Events { channels } - - #[cfg(test)] - fn print(&self, depth: usize) { - let indent = " ".repeat(depth * 2); - match self { - Events::Single(s) => println!("{}'{}' {}", indent, s.string, s), - Events::Multi(m) => { - println!( - "{}multi {} -> {} [{}]", - indent, m.span.start, m.span.end, m.length - ); - for e in &m.events { - e.print(depth + 1) - } - } - Events::Poly(p) => { - println!( - "{}poly {} -> {} [{}]", - indent, p.span.start, p.span.end, p.length - ); - for e in &p.channels { - e.print(depth + 1) - } - } - } - } } // ------------------------------------------------------------------------------------------------- @@ -1405,7 +1374,7 @@ impl CycleParser { match pair.as_rule() { Rule::variable => Self::variable(pair), Rule::single => Ok(Self::single(pair)?), - Rule::repeat => Ok(Step::Static(Static::Repeat)), + Rule::repeat => Ok(Step::Repeat), Rule::subdivision | Rule::mini => Self::group(pair, Step::subdivision), Rule::alternating => Self::group(pair, Step::alternating), Rule::polymeter => Self::polymeter(pair), @@ -1545,19 +1514,6 @@ impl CycleParser { })) } - /// transform static steps into their final form and push them onto a list - fn push_applied(steps: &mut Vec, step: Step) { - match &step { - Step::Static(s) => match s { - Static::Repeat => { - let repeat = steps.last().cloned().unwrap_or(Step::rest()); - steps.push(repeat) - } - }, - _ => steps.push(step), - } - } - /// helper to split a list of pairs over a rule, used for stacks and split shorthand fn split_over(pairs: Vec>, rule: Rule) -> Vec>> { pairs.into_iter().fold(vec![vec![]], |mut a, p| { @@ -1596,7 +1552,7 @@ impl CycleParser { if let Some(first) = vs.first() { if vs.len() > 1 { Ok(Step::Choices(Choices { - choices: Self::section_vec(vs)?, + choices: Self::with_choices(vs)?, })) } else { Self::step(first.clone()) @@ -1608,19 +1564,10 @@ impl CycleParser { .collect() } - fn section_vec(pairs: Vec>) -> Result, String> { - let choiced_steps = Self::with_choices(pairs)?; - let mut steps = Vec::with_capacity(choiced_steps.len()); - for step in choiced_steps.into_iter() { - Self::push_applied(&mut steps, step) - } - Ok(steps) - } - fn section(pairs: Vec>) -> Result, String> { let split_pairs = Self::split_over(pairs, Rule::split_op) .into_iter() - .map(Self::section_vec) + .map(Self::with_choices) .collect::>, String>>()?; Ok(if split_pairs.len() > 1 { @@ -1662,7 +1609,10 @@ impl CycleParser { if let Some(count) = pair.clone().into_inner().next() { Self::step(count) } else { - Err(format!("missing polymeter count '{}'", pair.as_str())) + Err(format!( + "error in grammar, missing polymeter count '{}'", + pair.as_str() + )) } } @@ -1691,46 +1641,15 @@ impl CycleParser { }; match (count, stack, steps) { - (Some(count), None, Some(steps)) => { - // a regular polymeter with explicit count - Ok(Step::polymeter(steps, count)) - } - (Some(count), Some(stack), _) => { - // sections in a stack with explicit count will all have that - Ok(Step::Stack(Stack { - stack: stack - .into_iter() - .map(|steps| Step::polymeter(steps, count.clone())) - .collect(), - })) - } - (None, Some(stack), _) => { - let count = stack - .first() - .map(Vec::len) - .ok_or_else(|| format!("empty stack {:?}", stack))?; - - if stack.len() > 1 && count > 0 { - let count = Step::Single(Single { - value: Constant::Integer(count as i32), - string: Rc::from(count.to_string()), - }); - // if there is a stack but no count, the first section will determine the count of the rest - Ok(Step::Stack(Stack { - stack: stack - .into_iter() - .map(|steps| Step::polymeter(steps, count.clone())) - .collect(), - })) - } else { - // unreachable, a stack will always have more than one sections with each having at least one item - Err(format!("invalid stack {:?}", stack)) - } - } + // empty polymeters become a single rest + // {}, {}%2 ... + (_, None, None) => Ok(Step::rest()), // if there is only one section and no count, it is treated as a subdivision (None, None, Some(steps)) => Ok(Step::subdivision(steps)), - // empty polymeter like {} and {}%2 will become a single rest - _ => Ok(Step::rest()), + // a mono polymeter with optional count + (count, None, Some(steps)) => Ok(Step::polymeter(vec![steps], count)), + // a stacked polymeter with optional count + (count, Some(stack), _) => Ok(Step::polymeter(stack, count)), } } @@ -1794,7 +1713,10 @@ impl CycleParser { String::from("unreachable: missing right hand side from op_pair, error in grammar!") } - fn optional_right_hand(op_pair: Pair, default: fn() -> Step) -> Result { + fn optional_single_right_hand( + op_pair: Pair, + default: fn() -> Step, + ) -> Result { if let Some(pair) = op_pair.into_inner().next() { match pair.as_rule() { Rule::single => Self::single(pair), @@ -1810,7 +1732,7 @@ impl CycleParser { // with the current pest setup, this seems impossible if we want to support optional parameter here // at least it is impossible without major rearrangement of the grammar and parsing fn weight_expression(left: Step, op_pair: Pair) -> Result { - let weight = Self::optional_right_hand(op_pair, || { + let weight = Self::optional_single_right_hand(op_pair, || { Step::constant(Constant::Float(2.0), Some("2.0")) })?; @@ -1821,18 +1743,18 @@ impl CycleParser { } fn replicate_expression(left: Step, op_pair: Pair) -> Result { - let count = Self::optional_right_hand(op_pair, || { + let count = Self::optional_single_right_hand(op_pair, || { Step::constant(Constant::Float(2.0), Some("2.0")) })?; Ok(Step::Replicated(Replicated { step: Box::new(left), - count: Box::new(count), + repeats: Box::new(count), })) } fn degrade_expression(step: Step, op_pair: Pair) -> Result { - let chance = Self::optional_right_hand(op_pair, || { + let chance = Self::optional_single_right_hand(op_pair, || { Step::constant(Constant::Float(0.5), Some("0.5")) })?; @@ -1954,9 +1876,9 @@ impl Cycle { fn output_multiplied( step: &Step, + mult: Fraction, state: &mut CycleState, cycle: u32, - mult: Fraction, limit: usize, overlap: bool, vars: Option<&Vars>, @@ -1978,69 +1900,68 @@ impl Cycle { overlap: bool, vars: Option<&Vars>, ) -> Result { - let right_events = Self::output(step, state, cycle, limit, overlap, vars)?; - Ok(right_events - .first() - .and_then(|e| e.value.to_fraction()) - .unwrap_or(Fraction::ONE)) + let length_mod = match step { + Step::Replicated(replicated) => Some(replicated.repeats.as_ref()), + Step::Weighted(weighted) => Some(weighted.weight.as_ref()), + _ => None, + }; + + if let Some(step) = length_mod { + let right_events = Self::output(step, state, cycle, limit, overlap, vars)?; + Ok(right_events + .first() + .and_then(|e| e.value.to_fraction()) + .unwrap_or(Fraction::ONE)) + } else { + Ok(Fraction::ONE) + } } - // helper to calculate the right multiplier for polymeter and speed expressions - fn step_multiplier( + fn sub_length( step: &Step, - value: &Constant, state: &mut CycleState, cycle: u32, limit: usize, overlap: bool, vars: Option<&Vars>, ) -> Result { - let mult = match step { - Step::Polymeter(pm) => { - let length = if let Step::Subdivision(s) = pm.steps.as_ref() { - let mut a = Fraction::ZERO; - for v in s.steps.iter() { - let inner_length = match v { - Step::Replicated(re) => Self::step_length( - re.count.as_ref(), - state, - cycle, - limit, - overlap, - vars, - )?, - _ => Fraction::ONE, - }; - a += inner_length - } - a - } else { - // unreachable - Fraction::ONE - }; - - value.to_fraction().unwrap_or(Fraction::ZERO) / length + Ok(match step { + Step::Subdivision(sub) => { + let mut length = Fraction::ZERO; + for s in sub.steps.iter() { + length += Self::step_length(s, state, cycle, limit, overlap, vars)?; + } + length } - Step::SpeedExpression(e) => match e.op { - SpeedOp::Fast() => value.to_fraction().unwrap_or(Fraction::ZERO), - SpeedOp::Slow() => value - .to_float() - .and_then(|div| { - if div != 0.0 { - Fraction::from_f64(1.0 / div) - } else { - None - } - }) - .unwrap_or(Fraction::ZERO), - }, - _ => Fraction::from(1), - // _ => value - // .to_float() - // .and_then(Fraction::from_f64) - // .unwrap_or(Fraction::ONE), - }; - Ok(mult) + _ => Fraction::ONE, + }) + } + + // helper to calculate the right multiplier for polymeter and speed expressions + fn step_multiplier(op: &SpeedOp, value: &Constant) -> Fraction { + match op { + SpeedOp::Fast() => value.to_fraction().unwrap_or(Fraction::ZERO), + SpeedOp::Slow() => value + .to_float() + .and_then(|div| { + if div != 0.0 { + Fraction::from_f64(1.0 / div) + } else { + None + } + }) + .unwrap_or(Fraction::ZERO), + SpeedOp::Fit(outer) => value + .to_fraction() + .and_then(|inner| { + if *outer != Fraction::ZERO { + Some(inner / outer) + } else { + None + } + }) + .unwrap_or(Fraction::ZERO), + } } // overlay two lists of events and apply the targets from the second to the first @@ -2143,43 +2064,40 @@ impl Cycle { } } // put all the resulting events back together - Ok(Events::maybe_poly(PolyEvents { - length: left_span.length(), - span: left_span, - channels: channel_events, - })) + Ok(Events::collapsed_poly( + channel_events, + left_span.length(), + left_span, + )) } } } // output a multiplied pattern expression with support for patterns on the right side + #[allow(clippy::too_many_arguments)] fn output_with_speed( - right: &Step, step: &Step, + op: &SpeedOp, + mult: &Step, state: &mut CycleState, cycle: u32, limit: usize, overlap: bool, vars: Option<&Vars>, ) -> Result { - let left = match step { - Step::Polymeter(pm) => pm.steps.as_ref(), - Step::SpeedExpression(exp) => exp.step.as_ref(), - _ => step, - }; - match right { + match mult { // multiply with single values to avoid generating events Step::Single(single) => { // apply multiplier - let multiplier = - Self::step_multiplier(step, &single.value, state, cycle, limit, overlap, vars)?; + let multiplier = Self::step_multiplier(op, &single.value); Ok(Self::output_multiplied( - left, state, cycle, multiplier, limit, overlap, vars, + step, multiplier, state, cycle, limit, overlap, vars, )?) } _ => { // generate and flatten the events for the right side of the expression - let events = Self::output(right, state, cycle, limit, overlap, vars)?; + let mut events = Self::output(mult, state, cycle, limit, overlap, vars)?; + events.transform_spans(&Span::default()); let channels = events.export(); // extract a float to use as mult from each event and output the step with it @@ -2187,36 +2105,27 @@ impl Cycle { for channel in channels.into_iter() { let mut multi_events: Vec = Vec::with_capacity(channel.len()); for event in channel { - // apply multiplier - let multiplier = Self::step_multiplier( - step, - &event.value, - state, - cycle, - limit, - overlap, - vars, - )?; + let multiplier = Self::step_multiplier(op, &event.value); let mut partial_events = Self::output_multiplied( - left, state, cycle, multiplier, limit, overlap, vars, + step, multiplier, state, cycle, limit, overlap, vars, )?; - // crop and push to multi events + // crop to each span of the resulting multipliers and concat partial_events.crop(&event.span, overlap); multi_events.push(partial_events); } - channel_events.push(Events::Multi(MultiEvents { - length: events.get_length(), - span: events.get_span(), - events: multi_events, - })); + channel_events.push(Events::collapsed_multi( + multi_events, + events.get_length(), + events.get_span(), + )); } // put all the resulting events back together - Ok(Events::maybe_poly(PolyEvents { - length: events.get_length(), - span: events.get_span(), - channels: channel_events, - })) + Ok(Events::collapsed_poly( + channel_events, + events.get_length(), + events.get_span(), + )) } } } @@ -2262,18 +2171,21 @@ impl Cycle { if sd.steps.is_empty() { Events::empty() } else { - let mut events = Vec::with_capacity(sd.steps.len()); + let mut events: Vec = Vec::with_capacity(sd.steps.len()); for s in &sd.steps { - let e = Self::output(s, state, cycle, limit, overlap, vars)?; - events.push(e) + events.push(if matches!(s, Step::Repeat) { + if let Some(last) = events.last() { + last.clone() + } else { + Events::empty() + } + } else { + Self::output(s, state, cycle, limit, overlap, vars)? + }) } Events::subdivide_lengths(&mut events); - Events::Multi(MultiEvents { - span: Span::default(), - length: Fraction::ONE, - events, - }) + Events::collapsed_multi(events, Fraction::ONE, Span::default()) } } Step::Weighted(we) => { @@ -2288,14 +2200,15 @@ impl Cycle { events } Step::Replicated(we) => { - let count = Self::output(we.count.as_ref(), state, cycle, limit, overlap, vars)? - .first() - .and_then(|e| e.value.to_float()) - .unwrap_or(2.0); + let repeats = + Self::output(we.repeats.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_float()) + .unwrap_or(2.0); - let ceil = count.ceil(); + let ceil = repeats.ceil(); let len = if ceil == 0.0 { 1 } else { ceil as usize }; - let mult = count / ceil; + let mult = repeats / ceil; let steps = vec![we.step.as_ref().clone(); len]; let sub = Step::subdivision(steps); @@ -2311,36 +2224,63 @@ impl Cycle { }); let mut events = Self::output(&step, state, cycle, limit, overlap, vars)?; - events.set_length(Fraction::from_f64(count).unwrap_or(Fraction::ONE)); + events.set_length(Fraction::from_f64(repeats).unwrap_or(Fraction::ONE)); events } - Step::Alternating(a) => { - if a.steps.is_empty() { - Events::empty() - } else { - let length = a.steps.len() as u32; - let current = cycle % length; - a.steps - .get(current as usize) - .map(|step| Self::output(step, state, cycle / length, limit, overlap, vars)) - .unwrap_or( - Ok(Events::empty()), // unreachable - )? - } - } Step::Choices(cs) => { let choice = state.rng.random_range(0..cs.choices.len()); Self::output(&cs.choices[choice], state, cycle, limit, overlap, vars)? } - Step::Polymeter(pm) => Self::output_with_speed( - pm.count.as_ref(), - step, - state, - cycle, - limit, - overlap, - vars, - )?, + Step::Polymeter(pm) => { + if let Some(count) = &pm.count { + let mut channels = vec![]; + for sub in pm.stack.iter() { + let length = Self::sub_length(sub, state, cycle, limit, overlap, vars)?; + let events = Self::output_with_speed( + sub, + &SpeedOp::Fit(length), + count, + state, + cycle, + limit, + overlap, + vars, + )?; + channels.push(events) + } + Events::collapsed_poly(channels, Fraction::ONE, Span::default()) + } else { + let Some(first) = pm.stack.first() else { + return Ok(Events::empty()); + }; + + let first_length = Self::sub_length(first, state, cycle, limit, overlap, vars)?; + let mult = Step::constant( + Constant::Float((first_length).to_f64().unwrap_or_default()), + None, + ); + let mut channels = vec![]; + for (i, sub) in pm.stack.iter().enumerate() { + let length = if i == 0 { + first_length + } else { + Self::sub_length(sub, state, cycle, limit, overlap, vars)? + }; + let events = Self::output_with_speed( + sub, + &SpeedOp::Fit(length), + &mult, + state, + cycle, + limit, + overlap, + vars, + )?; + channels.push(events) + } + Events::collapsed_poly(channels, Fraction::ONE, Span::default()) + } + } Step::Stack(st) => { if st.stack.is_empty() { Events::empty() @@ -2349,11 +2289,7 @@ impl Cycle { for s in &st.stack { channels.push(Self::output(s, state, cycle, limit, overlap, vars)?) } - Events::maybe_poly(PolyEvents { - span: Span::default(), - length: Fraction::ONE, - channels, - }) + Events::collapsed_poly(channels, Fraction::ONE, Span::default()) } } Step::Degrade(d) => { @@ -2372,9 +2308,16 @@ impl Cycle { out } Step::Targeted(e) => Self::output_with_target(e, state, cycle, limit, overlap, vars)?, - Step::SpeedExpression(e) => { - Self::output_with_speed(e.mult.as_ref(), step, state, cycle, limit, overlap, vars)? - } + Step::SpeedExpression(e) => Self::output_with_speed( + e.step.as_ref(), + &e.op, + e.mult.as_ref(), + state, + cycle, + limit, + overlap, + vars, + )?, Step::Bjorklund(b) => { let mut events = vec![]; @@ -2409,18 +2352,9 @@ impl Cycle { } Events::subdivide_lengths(&mut events); - Events::Multi(MultiEvents { - span: Span::default(), - length: Fraction::ONE, - events, - }) + Events::collapsed_multi(events, Fraction::ONE, Span::default()) } - Step::Static(_) => { - // Repeat only makes it here if it had no preceding value - // Range and Expression should be applied in Self::push_applied - Events::empty() - } Step::Ranged(r) => { let start = Self::output(r.start.as_ref(), state, cycle, limit, overlap, vars)? .first() @@ -2457,6 +2391,7 @@ impl Cycle { Self::output(&step, state, cycle, limit, overlap, vars)? } + Step::Repeat => Events::empty(), }; Ok(events) } @@ -2470,20 +2405,17 @@ impl Cycle { _ => format!("{:?} {:?}", s.value, s.string), }, Step::Subdivision(sd) => format!("Subdivision [{}]", sd.steps.len()), - Step::Alternating(a) => format!("Alternating <{}>", a.steps.len()), Step::Polymeter(pm) => format!("Polymeter {{{:?}}}", pm.count), Step::Choices(cs) => format!("Choices |{}|", cs.choices.len()), Step::Stack(st) => format!("Stack ({})", st.stack.len()), Step::SpeedExpression(e) => format!("Speed Expression {:?}", e.op), Step::Weighted(we) => format!("Weight Expression {:?}", we.weight), - Step::Replicated(we) => format!("Replicate Expression {:?}", we.count), + Step::Replicated(we) => format!("Replicate Expression {:?}", we.repeats), Step::Targeted(e) => format!("Target Expression {:?}", e.kind), - Step::Static(s) => match s { - Static::Repeat => "Repeat".to_string(), - }, Step::Degrade(d) => format!("Degrade ? {:?}", d.chance), Step::Bjorklund(_b) => format!("Bjorklund {}", ""), Step::Ranged(_) => "Ranged".to_string(), + Step::Repeat => "Repeat".to_string(), }; println!("{} {}", indent_lines(level), name); for step in step.inner_steps() { diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index a9d082d..dde664b 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -37,6 +37,15 @@ fn assert_cycle_advancing(input: &str) -> Result<(), String> { Ok(()) } +fn cycle_with_vars(input: &str, subcycles: Vec<(&str, &str)>) -> Result { + let mut cycle = Cycle::from(input)?; + for (name, input) in subcycles { + let subcycle = SubCycle::from(input)?; + cycle.set_var(name, subcycle); + } + Ok(cycle) +} + #[test] fn span() -> Result<(), String> { assert!(Span::new(Fraction::new(0, 1), Fraction::new(1, 1)) @@ -53,54 +62,164 @@ fn weight_and_replicate() -> Result<(), String> { #[test] fn variables() -> Result<(), String> { - let note = SubCycle::from("e4")?; - let mut cycle = Cycle::from("a b $note d")?; - cycle.set_var("note", note); - assert_eq!(cycle.generate(), Cycle::from("a b e4 d")?.generate()); + assert!(SubCycle::from("a b c d").is_ok()); + // subcycles cannot contain variables + assert!(SubCycle::from("[a b c d]*$mult").is_err()); + assert!(SubCycle::from("$note $note $note").is_err()); + assert!(SubCycle::from("a:p=<0.5 0.2 $right>").is_err()); + + assert_eq!( + cycle_with_vars("a b $note d", vec![("note", "e4")])?.generate(), + Cycle::from("a b e4 d")?.generate() + ); // unset variables convert into named - let mut cycle = Cycle::from("a $note")?; - assert_eq!(cycle.generate(), Cycle::from("a note")?.generate()); - - let index = Constant::Integer(12); - let mut cycle = Cycle::from("a:$index")?; - cycle.set_var_constant("index", index); - assert_eq!(cycle.generate(), Cycle::from("a:12")?.generate()); - - let sub = SubCycle::from("1 2")?; - let mut cycle = Cycle::from("a*$sub")?; - cycle.set_var("sub", sub); - assert_eq!(cycle.generate(), Cycle::from("a*[1 2]")?.generate()); - - let f1 = Constant::Float(0.5); - let f2 = Constant::Float(0.9); - let mut cycle = Cycle::from("[a b c d]:p=[$f1 $f2]")?; - cycle.set_var_constant("f1", f1); - cycle.set_var_constant("f2", f2); assert_eq!( - cycle.generate(), + Cycle::from("a $note")?.generate(), + Cycle::from("a note")?.generate() + ); + + assert_eq!( + cycle_with_vars("a:$index", vec![("index", "12")])?.generate(), + Cycle::from("a:12")?.generate() + ); + + assert_eq!( + cycle_with_vars("a*$mult", vec![("mult", "1 2")])?.generate(), + Cycle::from("a*[1 2]")?.generate() + ); + + assert_eq!( + cycle_with_vars("[a b c d]:p=[$f1 $f2]", vec![("f1", "0.5"), ("f2", "0.9")])?.generate(), Cycle::from("[a b c d]:p=[0.5 0.9]")?.generate() ); - let mult = Constant::Float(2.0); - let mut cycle = Cycle::from("a*$mult")?; - cycle.set_var_constant("mult", mult); - assert_eq!(cycle.generate(), Cycle::from("a*2")?.generate()); + assert_eq!( + cycle_with_vars("a*$mult", vec![("mult", "2.0")])?.generate(), + Cycle::from("a*2")?.generate() + ); - let length = Constant::Float(3.0); - let mut cycle = Cycle::from("a@$length b")?; - cycle.set_var_constant("length", length); - assert_eq!(cycle.generate(), Cycle::from("a@3 b")?.generate()); + assert_eq!( + cycle_with_vars("a@$length b", vec![("length", "3.0")])?.generate(), + Cycle::from("a@3 b")?.generate() + ); - let float = Constant::Float(0.9); - let mut cycle = Cycle::from("a:p$float")?; - cycle.set_var_constant("float", float); - assert_eq!(cycle.generate(), Cycle::from("a:p0.9")?.generate()); + assert_eq!( + cycle_with_vars("a:p$float", vec![("float", "0.9")])?.generate(), + Cycle::from("a:p0.9")?.generate() + ); + + assert_eq!( + cycle_with_vars("[a b c d]:p=[$f1 $f2]", vec![("f1", "0.5"), ("f2", "0.9")])?.generate(), + Cycle::from("[a b c d]:p=[0.5 0.9]")?.generate() + ); + + assert_eq!( + cycle_with_vars("a@$length b", vec![("length", "3.0")])?.generate(), + Cycle::from("a _ _ b")?.generate() + ); + + assert_eq!( + cycle_with_vars("a!$repeat b", vec![("repeat", "3.0")])?.generate(), + Cycle::from("a a a b")?.generate() + ); + + assert_eq!( + cycle_with_vars( + "a*[[$int1 $int2!$repeat]*$mult]", + vec![ + ("int1", "1"), + ("int2", "2"), + ("repeat", "2.0"), + ("mult", "2.5") + ] + )? + .generate(), + Cycle::from("a*[[1 2 2]*2.5]")?.generate() + ); + + let mut cycle = cycle_with_vars("a!$repeat", vec![])?; + let outputs = ["a", "a a", "a a a", "a a a a"]; + for (i, o) in outputs.iter().enumerate() { + cycle.set_var("repeat", SubCycle::from(&(i + 1).to_string())?); + assert_eq!(cycle.generate(), Cycle::from(o)?.generate()); + } + + let mut cycle = cycle_with_vars("$note", vec![])?; + let mut static_cycle = Cycle::from("")?; + for note in ["a", "b", "c", "d"] { + cycle.set_var("note", SubCycle::from(note)?); + assert_eq!(cycle.generate(), static_cycle.generate()); + } + + Ok(()) +} + +#[test] +fn polymeter() -> Result<(), String> { + assert_eq!( + Cycle::from("{0 1 2, 0 1 2 3}")?.generate(), + Cycle::from("{0 1 2, 0 1 2 3}%3")?.generate(), + ); + + assert_eq!( + Cycle::from("{0 ! ! 1}%2")?.generate(), + Cycle::from("{0 0 0 1}%2")?.generate(), + ); + + assert_eq!( + Cycle::from("{0 1!2, 0 1 2 3}")?.generate(), + Cycle::from("{0 1 1, 0 1 2 3}%3")?.generate(), + ); + + assert_eq!( + Cycle::from("{a b c d}%3")?.generate(), + Cycle::from("[a b c d]*0.75")?.generate(), + ); + + assert_cycles( + "{-3 -2 -1 0 1 2 3}%4", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-3), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(-1), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(0), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(-3), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-2), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(0), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(1), + ]], + ], + )?; + + assert_cycle_equality("{a b!2 c}%3", "{a b b c}%3")?; + assert_cycle_equality("a b, {c d e}%2", "{a b, c d e}")?; + assert_cycle_equality("{a b . c d . f g h}%2", "{[a b] [c d] [f g h]}%2")?; + + assert_cycles( + "{0 1 2 3}%<2 3>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(3), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + ], + )?; - assert!(SubCycle::from("a b c d").is_ok()); - assert!(SubCycle::from("[a b c d]*$mult").is_err()); - assert!(SubCycle::from("$note $note $note").is_err()); - assert!(SubCycle::from("a:p=<0.5 0.2 $right>").is_err()); Ok(()) } @@ -626,30 +745,6 @@ fn generate() -> Result<(), String> { ], )?; - assert_cycles( - "{-3 -2 -1 0 1 2 3}%4", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-3), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(-1), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(0), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(-3), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-2), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(0), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(1), - ]], - ], - )?; - assert_eq!( Cycle::from("[1 middle _] {}%42 [] <>")?.generate()?, [[ @@ -820,8 +915,6 @@ fn generate() -> Result<(), String> { assert_cycle_equality("[! ! a !]", "[~ ~ a a]")?; assert_cycle_equality("a ~ ~ ~", "a - - -")?; assert_cycle_equality("[a b] ! ! !", "[a b] [a b] [a b] ")?; - assert_cycle_equality("{a b!2 c}%3", "{a b b c}%3")?; - assert_cycle_equality("a b, {c d e}%2", "{a b, c d e}")?; assert_cycle_equality("0..3", "0 1 2 3")?; assert_cycle_equality("-5..-8", "-5 -6 -7 -8")?; assert_cycle_equality("a b . c d", "[a b] [c d]")?; @@ -833,7 +926,6 @@ fn generate() -> Result<(), String> { "a b . c d e , f g h i . j k, l m", "[a b] [c d e], [[f g h i] [j k]], [l m]", )?; - assert_cycle_equality("{a b . c d . f g h}%2", "{[a b] [c d] [f g h]}%2")?; assert_cycle_equality("", "<[a b] [c d] [f g h]>")?; assert_cycles( @@ -875,21 +967,6 @@ fn generate() -> Result<(), String> { ]] ); - assert_cycles( - "{0 1 2 3}%<2 3>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(3), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - ], - )?; - // TODO test random outputs // parse_with_debug("[a b c d]?0.5"); Ok(()) From 66fd0aab6c44248c530e058598af377b470afbe2 Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sun, 1 Mar 2026 04:59:18 +0000 Subject: [PATCH 18/21] account for repeats in length --- src/tidal/cycle.rs | 6 +++++- src/tidal/cycle/tests.rs | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index bc8eff7..7029767 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -1928,8 +1928,12 @@ impl Cycle { Ok(match step { Step::Subdivision(sub) => { let mut length = Fraction::ZERO; + let mut last_length = Fraction::ZERO; for s in sub.steps.iter() { - length += Self::step_length(s, state, cycle, limit, overlap, vars)?; + if !matches!(s, Step::Repeat) { + last_length = Self::step_length(s, state, cycle, limit, overlap, vars)?; + } + length += last_length; } length } diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs index dde664b..ca6bbf1 100644 --- a/src/tidal/cycle/tests.rs +++ b/src/tidal/cycle/tests.rs @@ -177,6 +177,11 @@ fn polymeter() -> Result<(), String> { Cycle::from("[a b c d]*0.75")?.generate(), ); + assert_eq!( + Cycle::from("{a@2 ! c, d e f}")?.generate(), + Cycle::from("{a@2 a@2 c, d e f}")?.generate(), + ); + assert_cycles( "{-3 -2 -1 0 1 2 3}%4", vec![ From 3af3d9049bfe49bf7ab3db53053a4e6b1451dc1d Mon Sep 17 00:00:00 2001 From: unlessgames Date: Sun, 1 Mar 2026 05:01:07 +0000 Subject: [PATCH 19/21] cleanup FIX comments --- src/emitter/cycle.rs | 1 - src/emitter/scripted_cycle.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emitter/cycle.rs b/src/emitter/cycle.rs index 0144404..975a8fb 100644 --- a/src/emitter/cycle.rs +++ b/src/emitter/cycle.rs @@ -389,7 +389,6 @@ impl Emitter for CycleEmitter { match Parameter::parse_subcycles(¶meters) { Ok(parameters) => self.parameters = parameters, Err(err) => { - // FIX handle this more gracefully? panic!("{err}") } } diff --git a/src/emitter/scripted_cycle.rs b/src/emitter/scripted_cycle.rs index b3534bd..cd3ac15 100644 --- a/src/emitter/scripted_cycle.rs +++ b/src/emitter/scripted_cycle.rs @@ -313,7 +313,7 @@ impl Emitter for ScriptedCycleEmitter { } } Err(err) => { - // FIX make error point to the enum def? + // TODO make error point to the enum def? add_lua_callback_error(None, None, "enum".to_string(), LuaError::RuntimeError(err)); } } From baceaa3243bfe9cb50fe8a276b289d2cdee0a19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20M=C3=BCller?= Date: Mon, 2 Mar 2026 10:59:19 +0100 Subject: [PATCH 20/21] sub-cycle parsing error handling tweaks - mention enum parameter name and value in sub cycle parameter error strings - forward parse errors as runtime error, but return a "rest" value on errors and continue parsing, so parse errors don't get masked by other errors - add some basic tests --- src/emitter/cycle.rs | 97 +++++++++++++++++++++++++---------- src/emitter/scripted_cycle.rs | 65 ++++++++++++++--------- src/tidal/cycle.rs | 8 ++- 3 files changed, 118 insertions(+), 52 deletions(-) diff --git a/src/emitter/cycle.rs b/src/emitter/cycle.rs index 975a8fb..45fa582 100644 --- a/src/emitter/cycle.rs +++ b/src/emitter/cycle.rs @@ -46,34 +46,33 @@ impl TryFrom<&CycleValue> for Vec> { // ------------------------------------------------------------------------------------------------- -/// Convert a [`Parameter`] value to a [`CycleValue`]. -pub type ParameterWithValues = (Rc>, Vec); impl Parameter { - pub fn into_var(&self, values: &[CycleSubCycle]) -> CycleSubCycle { + /// Convert a [`Parameter`] value to a [`CycleValue`]. + pub fn into_var(&self, enum_sub_cycles: &[CycleSubCycle]) -> CycleSubCycle { match self.parameter_type() { ParameterType::Boolean => CycleSubCycle::integer((self.value() >= 0.5) as i32), ParameterType::Float => CycleSubCycle::float(self.value()), ParameterType::Integer => CycleSubCycle::integer(self.value().round() as i32), - ParameterType::Enum => values[self.value().round() as usize].clone(), + ParameterType::Enum => enum_sub_cycles[self.value().round() as usize].clone(), } } - pub fn parse_subcycles(parameters: &ParameterSet) -> Result, String> { - let mut result = vec![]; - for p in parameters.iter() { - result.push((Rc::clone(p), { - if p.borrow().parameter_type() == ParameterType::Enum { - let mut values = vec![]; - for string in p.borrow().value_strings().iter() { - values.push(CycleSubCycle::from(string)?) - } - values - } else { - Vec::default() - } - })) + /// Parse enum parameter values into sub-cycle results. + pub fn parse_subcycles(&self) -> Vec> { + match self.parameter_type() { + ParameterType::Enum => self + .value_strings() + .iter() + .map(|string| { + CycleSubCycle::from(string).map_err(|err| { + format!( + "Failed to convert enum parameter value '{string}' to sub-cycle: {err}" + ) + }) + }) + .collect(), + _ => vec![], } - Ok(result) } } @@ -274,7 +273,7 @@ impl CycleNoteEvents { #[derive(Clone, Debug)] pub struct CycleEmitter { cycle: Cycle, - parameters: Vec, + parameters: Vec<(Rc>, Vec)>, mappings: HashMap>>, } @@ -386,12 +385,24 @@ impl Emitter for CycleEmitter { } fn set_parameters(&mut self, parameters: ParameterSet) { - match Parameter::parse_subcycles(¶meters) { - Ok(parameters) => self.parameters = parameters, - Err(err) => { - panic!("{err}") - } - } + // parse and unwrap cycle values + self.parameters = parameters + .iter() + .map(|parameter| { + ( + Rc::clone(¶meter), + parameter + .borrow() + .parse_subcycles() + .into_iter() + .map(|result| { + // we got no way indicate runtime errors here, so just panic + result.unwrap_or_else(|err| panic!("{err}")) + }) + .collect(), + ) + }) + .collect(); } fn run(&mut self, _pulse: RhythmEvent, emit_event: bool) -> Option> { @@ -479,7 +490,7 @@ mod tests { let mut expected = new_cycle_emitter("a*3")?; assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); - // enum + // bool let param = Rc::new(RefCell::new(Parameter::with_boolean( "enabled", "", "", true, ))); @@ -505,6 +516,38 @@ mod tests { Ok(()) } + #[test] + fn parameter_enum_values() -> Result<(), Box> { + let param = Rc::new(RefCell::new(Parameter::with_enum( + "enum", + "", + "", + vec!["c4".to_string(), "[c4 c5]*4".to_string()], + "[c4 c5]*4".to_string(), + ))); + let mut emitter = new_cycle_emitter("$enum")?; + emitter.set_parameters(vec![Rc::clone(¶m)]); + + let mut expected = new_cycle_emitter("[c4 c5]*4")?; + assert_eq!(run_emitter(&mut emitter)?, run_emitter(&mut expected)?); + + Ok(()) + } + + #[test] + #[should_panic] + fn parameter_enum_values_error() { + let param = Rc::new(RefCell::new(Parameter::with_enum( + "enum", + "", + "", + vec!["c4".to_string(), "broken]".to_string()], + "c4".to_string(), + ))); + let mut variable = new_cycle_emitter("$enum").unwrap(); + variable.set_parameters(vec![Rc::clone(¶m)]); // this should throw + } + #[test] fn parameter_value_changes() -> Result<(), Box> { let param = Rc::new(RefCell::new(Parameter::with_float( diff --git a/src/emitter/scripted_cycle.rs b/src/emitter/scripted_cycle.rs index cd3ac15..f6875e0 100644 --- a/src/emitter/scripted_cycle.rs +++ b/src/emitter/scripted_cycle.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; use num_traits::ToPrimitive; @@ -9,9 +9,9 @@ use crate::{ add_lua_callback_error, note_events_from_value, ContextPlaybackState, LuaCallback, LuaTimeoutHook, }, - emitter::cycle::{apply_cycle_note_properties, CycleNoteEvents, ParameterWithValues}, - BeatTimeBase, Cycle, CycleEvent, CycleValue, Emitter, EmitterEvent, Event, NoteEvent, - Parameter, ParameterSet, RhythmEvent, + emitter::cycle::{apply_cycle_note_properties, CycleNoteEvents}, + BeatTimeBase, Cycle, CycleEvent, CycleSubCycle, CycleValue, Emitter, EmitterEvent, Event, + NoteEvent, Parameter, ParameterSet, RhythmEvent, }; // ------------------------------------------------------------------------------------------------- @@ -20,13 +20,13 @@ use crate::{ /// /// Channels from cycle are merged down into note events on different voices. /// Values in cycles can be mapped to notes with an optional mapping table or -/// callbacks from from scripts. +/// callbacks from scripts. /// /// See also [`CycleEmitter`](`super::cycle::CycleEmitter`) #[derive(Clone, Debug)] pub struct ScriptedCycleEmitter { cycle: Cycle, - parameters: Vec, + parameters: Vec<(Rc>, Vec)>, mappings: HashMap>>, mapping_callback: Option, timeout_hook: Option, @@ -296,25 +296,42 @@ impl Emitter for ScriptedCycleEmitter { } fn set_parameters(&mut self, parameters: ParameterSet) { - match Parameter::parse_subcycles(¶meters) { - Ok(with_subcycles) => { - // store parameters, so we can inject them in generate() - self.parameters = with_subcycles; - - // and pass them to the mapping callback context - if let Some(timeout_hook) = &mut self.timeout_hook { - timeout_hook.reset(); - } + // parse and unwrap cycle subcycle values from enum parameters + let unwrap_sub_cycle_result = |sub_cycle: Result| -> CycleSubCycle { + sub_cycle.unwrap_or_else(|err| { + // forwarding parse error as runtime errors + add_lua_callback_error( + None, + None, + "cycle".to_string(), + LuaError::RuntimeError(err), + ); + // return rest value to prevent further runtime errors, which would mask the original error + CycleSubCycle::rest() + }) + }; + self.parameters = parameters + .iter() + .map(|parameter| { + ( + Rc::clone(¶meter), + parameter + .borrow() + .parse_subcycles() + .into_iter() + .map(unwrap_sub_cycle_result) + .collect(), + ) + }) + .collect(); - if let Some(callback) = &mut self.mapping_callback { - if let Err(err) = callback.set_context_parameters(parameters) { - callback.handle_error(&err); - } - } - } - Err(err) => { - // TODO make error point to the enum def? - add_lua_callback_error(None, None, "enum".to_string(), LuaError::RuntimeError(err)); + // pass parameters to the mapping callback context + if let Some(timeout_hook) = &mut self.timeout_hook { + timeout_hook.reset(); + } + if let Some(callback) = &mut self.mapping_callback { + if let Err(err) = callback.set_context_parameters(parameters) { + callback.handle_error(&err); } } } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 7029767..2f36a8d 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -39,9 +39,15 @@ impl SubCycle { if vars.is_empty() { Ok(SubCycle { step: cycle.root }) } else { - Err(format!("cycle contains variables\n{vars:?}")) + Err(format!( + "sub-cycles may not contain variables, found '{}'", + vars.into_iter().collect::>().join(",") + )) } } + pub fn rest() -> Self { + Self::new(Step::constant(Constant::Rest, None)) + } pub fn float(f: f64) -> Self { Self::new(Step::constant(Constant::Float(f), None)) } From 111c130c01b035391ca708c2aa5a627fcd469d2b Mon Sep 17 00:00:00 2001 From: unlessgames Date: Tue, 3 Mar 2026 05:14:54 +0000 Subject: [PATCH 21/21] perf tweaks --- src/tidal/cycle.rs | 141 ++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 85 deletions(-) diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 2f36a8d..3adb0e4 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -264,7 +264,7 @@ impl Event { } /// Time span for musical events within Cycles. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Span { start: Fraction, end: Fraction, @@ -483,6 +483,7 @@ impl Step { fn subdivision(steps: Vec) -> Self { Self::Subdivision(Subdivision { steps }) } + fn alternating(steps: Vec) -> Self { Self::polymeter( vec![steps], @@ -1023,19 +1024,19 @@ impl Events { }) } - fn collapsed_poly(events: Vec, length: Fraction, span: Span) -> Self { - match events.len() { + fn poly(channels: Vec, length: Fraction, span: Span) -> Self { + match channels.len() { 0 => Self::empty(), - 1 => events.first().expect("len is 1").to_owned(), + 1 => channels.first().expect("len is 1").to_owned(), _ => Self::Poly(PolyEvents { length, span, - channels: events, + channels, }), } } - fn collapsed_multi(events: Vec, length: Fraction, span: Span) -> Self { + fn multi(events: Vec, length: Fraction, span: Span) -> Self { match events.len() { 0 => Self::empty(), 1 => events.first().expect("len is 1").to_owned(), @@ -1063,9 +1064,9 @@ impl Events { } } - fn first(&self) -> Option { + fn first(&self) -> Option<&Event> { match self { - Events::Single(s) => Some(s.clone()), + Events::Single(s) => Some(s), Events::Multi(m) => m.events.first().and_then(Self::first), Events::Poly(p) => p.channels.first().and_then(Self::first), } @@ -1073,21 +1074,18 @@ impl Events { fn get_span(&self) -> Span { match self { - Events::Single(s) => s.span.clone(), - Events::Multi(m) => m.span.clone(), - Events::Poly(p) => p.span.clone(), + Events::Single(s) => s.span, + Events::Multi(m) => m.span, + Events::Poly(p) => p.span, } } /// Fits a list of events into a Span of 0..1 - fn subdivide_lengths(events: &mut Vec) { + fn subdivide_lengths(events: &mut [Events]) { let mut length = Fraction::ZERO; - for e in &mut *events { - match e { - Events::Single(s) => length += s.length, - Events::Multi(m) => length += m.length, - Events::Poly(p) => length += p.length, - } + + for e in events.iter_mut() { + length += e.get_length(); } let step_size = if length != Fraction::ZERO { Fraction::ONE / length @@ -1095,22 +1093,25 @@ impl Events { Fraction::ZERO }; let mut start = Fraction::ZERO; - for e in &mut *events { + for e in events.iter_mut() { match e { Events::Single(s) => { s.length *= step_size; - s.span = Span::new(start, start + s.length); - start += s.length + s.span.start = start; + s.span.end = start + s.length; + start = s.span.end } Events::Multi(m) => { m.length *= step_size; - m.span = Span::new(start, start + m.length); - start += m.length + m.span.start = start; + m.span.end = start + m.length; + start = m.span.end } Events::Poly(p) => { p.length *= step_size; - p.span = Span::new(start, start + p.length); - start += p.length + p.span.start = start; + p.span.end = start + p.length; + start = p.span.end } } } @@ -1122,35 +1123,14 @@ impl Events { { match self { Events::Multi(m) => { - let mut filtered = Vec::with_capacity(m.events.len()); - for e in &mut m.events { - match e { - Events::Single(s) => { - if predicate(s) { - filtered.push(e.clone()) - } - } - _ => { - if e.filter_mut(predicate) { - filtered.push(e.clone()) - } - } - } - } - m.events = filtered; + m.events.retain_mut(|e| e.filter_mut(predicate)); !m.events.is_empty() } Events::Poly(p) => { - let mut filtered = Vec::with_capacity(p.channels.len()); - for e in &mut p.channels { - if e.filter_mut(predicate) { - filtered.push(e.clone()) - } - } - p.channels = filtered; + p.channels.retain_mut(|e| e.filter_mut(predicate)); !p.channels.is_empty() } - Events::Single(_) => true, + Events::Single(e) => predicate(e), } } @@ -1240,20 +1220,20 @@ impl Events { } } - /// Recursively collapses Multi and Poly Events into vectors of Single Events - fn flatten(&self, channels: &mut Vec>, channel: &mut usize) { + /// recursively collapses Multi and Poly Events into vectors of Single Events + fn flatten(self, channels: &mut Vec>, channel: &mut usize) { if channels.len() <= *channel { channels.push(vec![]) } match self { - Events::Single(s) => channels[*channel].push(s.clone()), + Events::Single(s) => channels[*channel].push(s), Events::Multi(m) => { - for e in &m.events { + for e in m.events { e.flatten(channels, channel); } } Events::Poly(p) => { - for e in &p.channels { + for e in p.channels { e.flatten(channels, channel); *channel += 1 } @@ -1304,15 +1284,15 @@ impl Events { /// Removes Holds by extending preceding events and filters out Rests fn merge(channels: &mut [Vec]) { - for events in &mut *channels { + for events in channels.iter_mut() { Self::merge_holds(events); } - for events in channels { + for events in channels.iter_mut() { Self::merge_rests(events); } } - fn export(&self) -> Vec> { + fn export(self) -> Vec> { let mut channels = vec![]; self.flatten(&mut channels, &mut 0); Self::merge(&mut channels); @@ -1872,7 +1852,7 @@ impl Cycle { cycles.push(events) } let mut events = Events::Multi(MultiEvents { - span: span.clone(), + span: *span, length: span.length(), events: cycles, }); @@ -2019,9 +1999,10 @@ impl Cycle { let mut events = Self::output(step, state, cycle, limit, true, vars)?; events.transform_spans(&events.get_span()); let mut channels = vec![]; + let span = events.get_span(); events.flatten(&mut channels, &mut 0); Events::merge(&mut channels); - Ok((channels, events.get_span())) + Ok((channels, span)) } // generate events from Target expressions @@ -2066,19 +2047,15 @@ impl Cycle { for left_channel in left_channels.iter() { let mut cloned_left = left_channel.clone(); Self::apply_targets(&mut cloned_left, &channel, target_kind); - channel_events.push(Events::Multi(MultiEvents { - length: left_span.length(), - span: left_span.clone(), - events: cloned_left.into_iter().map(Events::Single).collect(), - })); + channel_events.push(Events::multi( + cloned_left.into_iter().map(Events::Single).collect(), + left_span.length(), + left_span, + )); } } // put all the resulting events back together - Ok(Events::collapsed_poly( - channel_events, - left_span.length(), - left_span, - )) + Ok(Events::poly(channel_events, left_span.length(), left_span)) } } } @@ -2108,6 +2085,8 @@ impl Cycle { // generate and flatten the events for the right side of the expression let mut events = Self::output(mult, state, cycle, limit, overlap, vars)?; events.transform_spans(&Span::default()); + let length = events.get_length(); + let span = events.get_span(); let channels = events.export(); // extract a float to use as mult from each event and output the step with it @@ -2123,19 +2102,11 @@ impl Cycle { partial_events.crop(&event.span, overlap); multi_events.push(partial_events); } - channel_events.push(Events::collapsed_multi( - multi_events, - events.get_length(), - events.get_span(), - )); + channel_events.push(Events::multi(multi_events, length, span)); } // put all the resulting events back together - Ok(Events::collapsed_poly( - channel_events, - events.get_length(), - events.get_span(), - )) + Ok(Events::poly(channel_events, length, span)) } } } @@ -2195,7 +2166,7 @@ impl Cycle { } Events::subdivide_lengths(&mut events); - Events::collapsed_multi(events, Fraction::ONE, Span::default()) + Events::multi(events, Fraction::ONE, Span::default()) } } Step::Weighted(we) => { @@ -2258,7 +2229,7 @@ impl Cycle { )?; channels.push(events) } - Events::collapsed_poly(channels, Fraction::ONE, Span::default()) + Events::poly(channels, Fraction::ONE, Span::default()) } else { let Some(first) = pm.stack.first() else { return Ok(Events::empty()); @@ -2269,7 +2240,7 @@ impl Cycle { Constant::Float((first_length).to_f64().unwrap_or_default()), None, ); - let mut channels = vec![]; + let mut channels = Vec::with_capacity(pm.stack.len()); for (i, sub) in pm.stack.iter().enumerate() { let length = if i == 0 { first_length @@ -2288,7 +2259,7 @@ impl Cycle { )?; channels.push(events) } - Events::collapsed_poly(channels, Fraction::ONE, Span::default()) + Events::poly(channels, Fraction::ONE, Span::default()) } } Step::Stack(st) => { @@ -2299,7 +2270,7 @@ impl Cycle { for s in &st.stack { channels.push(Self::output(s, state, cycle, limit, overlap, vars)?) } - Events::collapsed_poly(channels, Fraction::ONE, Span::default()) + Events::poly(channels, Fraction::ONE, Span::default()) } } Step::Degrade(d) => { @@ -2362,7 +2333,7 @@ impl Cycle { } Events::subdivide_lengths(&mut events); - Events::collapsed_multi(events, Fraction::ONE, Span::default()) + Events::multi(events, Fraction::ONE, Span::default()) } Step::Ranged(r) => {