use super::harness::*; use std::collections::HashMap; fn parse_params(output: &str) -> HashMap { let mut params = HashMap::new(); let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); let mut i = 0; while i + 1 < parts.len() { if let Ok(v) = parts[i + 1].parse::() { params.insert(parts[i].to_string(), v); } i += 2; } params } fn get_deltas(outputs: &[String]) -> Vec { outputs .iter() .map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0)) .collect() } fn get_durs(outputs: &[String]) -> Vec { outputs .iter() .map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0)) .collect() } fn get_notes(outputs: &[String]) -> Vec { outputs .iter() .map(|o| parse_params(o).get("note").copied().unwrap_or(0.0)) .collect() } fn get_sounds(outputs: &[String]) -> Vec { outputs .iter() .map(|o| { let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect(); for i in (0..parts.len()).step_by(2) { if parts[i] == "sound" && i + 1 < parts.len() { return parts[i + 1].to_string(); } } String::new() }) .collect() } const EPSILON: f64 = 1e-9; fn approx_eq(a: f64, b: f64) -> bool { (a - b).abs() < EPSILON } #[test] fn stack_creates_subdivisions_at_same_time() { let outputs = expect_outputs(r#""kick" s 3 stack each"#, 3); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.0)); assert!(approx_eq(deltas[2], 0.0)); } #[test] fn stack_vs_div_timing() { let stack_outputs = expect_outputs(r#""kick" s 3 stack each"#, 3); let div_outputs = expect_outputs(r#""kick" s 3 div each"#, 3); let stack_deltas = get_deltas(&stack_outputs); let div_deltas = get_deltas(&div_outputs); for d in stack_deltas { assert!(approx_eq(d, 0.0), "stack should have all delta=0"); } assert!(approx_eq(div_deltas[0], 0.0)); assert!(!approx_eq(div_deltas[1], 0.0), "div should spread in time"); assert!(!approx_eq(div_deltas[2], 0.0), "div should spread in time"); } #[test] fn for_with_div_arpeggio() { let outputs = expect_outputs(r#"{ "kick" s emit } 3 div for"#, 3); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); assert!(!approx_eq(deltas[1], 0.0)); assert!(!approx_eq(deltas[2], 0.0)); } #[test] fn for_with_stack_chord() { let outputs = expect_outputs(r#"{ "kick" s emit } 3 stack for"#, 3); let deltas = get_deltas(&outputs); for d in deltas { assert!(approx_eq(d, 0.0), "stack for should have all delta=0"); } } #[test] fn local_cycle_with_for() { let outputs = expect_outputs(r#"{ | 60 62 64 | note "sine" s emit } 3 div for"#, 3); let notes = get_notes(&outputs); assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[1], 62.0)); assert!(approx_eq(notes[2], 64.0)); } #[test] fn local_cycle_wraps_around() { let outputs = expect_outputs(r#"{ | 60 62 | note "sine" s emit } 4 div for"#, 4); let notes = get_notes(&outputs); assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[1], 62.0)); assert!(approx_eq(notes[2], 60.0)); assert!(approx_eq(notes[3], 62.0)); } #[test] fn multiple_local_cycles() { let outputs = expect_outputs(r#"{ | "bd" "sn" | s | 60 64 | note emit } 2 stack for"#, 2); let sounds = get_sounds(&outputs); let notes = get_notes(&outputs); assert_eq!(sounds[0], "bd"); assert_eq!(sounds[1], "sn"); assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[1], 64.0)); } #[test] fn local_cycle_outside_for_defaults_to_first() { expect_int("| 60 62 64 |", 60); } #[test] fn polymetric_cycles() { let outputs = expect_outputs( r#"{ | 0 1 | n | "a" "b" "c" | s emit } 6 div for"#, 6, ); let sounds = get_sounds(&outputs); assert_eq!(sounds[0], "a"); assert_eq!(sounds[1], "b"); assert_eq!(sounds[2], "c"); assert_eq!(sounds[3], "a"); assert_eq!(sounds[4], "b"); assert_eq!(sounds[5], "c"); } #[test] fn stack_error_zero_count() { expect_error(r#""kick" s 0 stack each"#, "stack count must be > 0"); } #[test] fn for_requires_subdivide() { expect_error(r#"{ "kick" s emit } for"#, "for requires subdivide first"); } #[test] fn for_requires_quotation() { expect_error(r#"42 3 div for"#, "expected quotation"); } #[test] fn empty_local_cycle() { expect_error("| |", "empty local cycle list"); } // Echo tests - stutter effect with halving durations #[test] fn echo_creates_decaying_subdivisions() { // default dur = 0.5, echo 3 // d1 + d1/2 + d1/4 = d1 * 1.75 = 0.5 // d1 = 0.5 / 1.75 let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3); let durs = get_durs(&outputs); let deltas = get_deltas(&outputs); let d1 = 0.5 / 1.75; let d2 = d1 / 2.0; let d3 = d1 / 4.0; assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]); assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]); assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]); assert!(approx_eq(deltas[0], 0.0), "first delta should be 0"); assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]); assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]); } #[test] fn echo_with_for() { let outputs = expect_outputs(r#"{ "kick" s emit } 3 echo for"#, 3); let durs = get_durs(&outputs); // Each subsequent duration should be half the previous assert!(approx_eq(durs[1], durs[0] / 2.0), "second should be half of first"); assert!(approx_eq(durs[2], durs[1] / 2.0), "third should be half of second"); } #[test] fn echo_error_zero_count() { expect_error(r#""kick" s 0 echo each"#, "echo count must be > 0"); } // Necho tests - reverse echo (durations grow) #[test] fn necho_creates_growing_subdivisions() { // default dur = 0.5, necho 3 // d1 + 2*d1 + 4*d1 = d1 * 7 = 0.5 // d1 = 0.5 / 7 let outputs = expect_outputs(r#""kick" s 3 necho each"#, 3); let durs = get_durs(&outputs); let deltas = get_deltas(&outputs); let d1 = 0.5 / 7.0; let d2 = d1 * 2.0; let d3 = d1 * 4.0; assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]); assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]); assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]); assert!(approx_eq(deltas[0], 0.0), "first delta should be 0"); assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]); assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]); } #[test] fn necho_with_for() { let outputs = expect_outputs(r#"{ "kick" s emit } 3 necho for"#, 3); let durs = get_durs(&outputs); // Each subsequent duration should be double the previous assert!(approx_eq(durs[1], durs[0] * 2.0), "second should be double first"); assert!(approx_eq(durs[2], durs[1] * 2.0), "third should be double second"); } #[test] fn necho_error_zero_count() { expect_error(r#""kick" s 0 necho each"#, "necho count must be > 0"); }