Files
Cagire/tests/forth/temporal.rs
2026-02-23 01:18:43 +01:00

341 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::harness::*;
use std::collections::HashMap;
fn parse_params(output: &str) -> HashMap<String, f64> {
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::<f64>() {
params.insert(parts[i].to_string(), v);
}
i += 2;
}
params
}
fn get_deltas(outputs: &[String]) -> Vec<f64> {
outputs
.iter()
.map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0))
.collect()
}
fn get_durs(outputs: &[String]) -> Vec<f64> {
outputs
.iter()
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
.collect()
}
fn get_sounds(outputs: &[String]) -> Vec<String> {
outputs
.iter()
.map(|o| {
let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect();
if parts.len() >= 2 && parts[0] == "sound" {
parts[1].to_string()
} else {
String::new()
}
})
.collect()
}
const EPSILON: f64 = 1e-9;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn stepdur_baseline() {
let f = run("stepdur");
assert!(approx_eq(stack_float(&f), 0.125));
}
#[test]
fn single_emit() {
let outputs = expect_outputs(r#""kick" s ."#, 1);
let deltas = get_deltas(&outputs);
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
}
#[test]
fn multiple_emits_all_at_zero() {
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
let deltas = get_deltas(&outputs);
for (i, delta) in deltas.iter().enumerate() {
assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
}
}
#[test]
fn sound_persists() {
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
let sounds = get_sounds(&outputs);
assert_eq!(sounds[0], "kick");
assert_eq!(sounds[1], "kick");
assert_eq!(sounds[2], "hat");
assert_eq!(sounds[3], "hat");
}
#[test]
fn alternating_sounds() {
let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4);
let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
}
#[test]
fn dur_is_step_duration() {
let outputs = expect_outputs(r#""kick" s ."#, 1);
let durs = get_durs(&outputs);
assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
}
#[test]
fn cycle_picks_by_runs() {
for runs in 0..4 {
let ctx = ctx_with(|c| c.runs = runs);
let f = forth();
let outputs = f.evaluate(r#""kick" s { . } { } 2 cycle"#, &ctx).unwrap();
if runs % 2 == 0 {
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
} else {
assert_eq!(outputs.len(), 0, "runs={}: no-op should be picked", runs);
}
}
}
#[test]
fn pcycle_picks_by_iter() {
for iter in 0..4 {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let outputs = f.evaluate(r#""kick" s { . } { } 2 pcycle"#, &ctx).unwrap();
if iter % 2 == 0 {
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
} else {
assert_eq!(outputs.len(), 0, "iter={}: no-op should be picked", iter);
}
}
}
#[test]
fn cycle_with_sounds() {
for runs in 0..3 {
let ctx = ctx_with(|c| c.runs = runs);
let f = forth();
let outputs = f.evaluate(
r#"{ "kick" s . } { "hat" s . } { "snare" s . } 3 cycle"#,
&ctx
).unwrap();
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
let sounds = get_sounds(&outputs);
let expected = ["kick", "hat", "snare"][runs % 3];
assert_eq!(sounds[0], expected, "runs={}: expected {}", runs, expected);
}
}
#[test]
fn at_single_delta() {
let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1);
let deltas = get_deltas(&outputs);
let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
}
#[test]
fn at_list_deltas() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s ."#, 2);
let deltas = get_deltas(&outputs);
let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
assert!(approx_eq(deltas[1], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[1]);
}
#[test]
fn at_three_deltas() {
let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" s ."#, 3);
let deltas = get_deltas(&outputs);
let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
assert!((deltas[1] - 0.33 * step_dur).abs() < 0.001, "expected delta at 0.33 of step");
assert!((deltas[2] - 0.67 * step_dur).abs() < 0.001, "expected delta at 0.67 of step");
}
#[test]
fn at_persists_across_emits() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s . "hat" s ."#, 4);
let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
}
#[test]
fn at_reset_with_zero() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s . 0.0 at "hat" s ."#, 3);
let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
}
#[test]
fn clear_resets_at_deltas() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s . clear "hat" s ."#, 3);
let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
let deltas = get_deltas(&outputs);
assert!(approx_eq(deltas[2], 0.0), "after clear, hat should emit at delta 0, got {}", deltas[2]);
}
#[test]
fn at_records_selected_spans() {
use cagire::forth::ExecutionTrace;
let f = forth();
let mut trace = ExecutionTrace::default();
let script = r#"0 0.5 0.75 at "kick" s ."#;
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
// Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)");
// Verify at delta spans (even indices: 0, 2, 4)
assert_eq!(&script[trace.selected_spans[0].start as usize..trace.selected_spans[0].end as usize], "0");
assert_eq!(&script[trace.selected_spans[2].start as usize..trace.selected_spans[2].end as usize], "0.5");
assert_eq!(&script[trace.selected_spans[4].start as usize..trace.selected_spans[4].end as usize], "0.75");
}
// --- arp tests ---
fn get_notes(outputs: &[String]) -> Vec<f64> {
outputs
.iter()
.map(|o| parse_params(o).get("note").copied().unwrap_or(0.0))
.collect()
}
fn get_gains(outputs: &[String]) -> Vec<f64> {
outputs
.iter()
.map(|o| parse_params(o).get("gain").copied().unwrap_or(f64::NAN))
.collect()
}
#[test]
fn arp_auto_subdivide() {
let outputs = expect_outputs(r#"sine s c4 e4 g4 b4 arp note ."#, 4);
let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0));
assert!(approx_eq(notes[2], 67.0));
assert!(approx_eq(notes[3], 71.0));
let deltas = get_deltas(&outputs);
let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.0));
assert!(approx_eq(deltas[1], 0.25 * step_dur));
assert!(approx_eq(deltas[2], 0.5 * step_dur));
assert!(approx_eq(deltas[3], 0.75 * step_dur));
}
#[test]
fn arp_with_explicit_at() {
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 g4 b4 arp note ."#, 4);
let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0));
assert!(approx_eq(notes[2], 67.0));
assert!(approx_eq(notes[3], 71.0));
let deltas = get_deltas(&outputs);
let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.0));
assert!(approx_eq(deltas[1], 0.25 * step_dur));
assert!(approx_eq(deltas[2], 0.5 * step_dur));
assert!(approx_eq(deltas[3], 0.75 * step_dur));
}
#[test]
fn arp_single_note() {
let outputs = expect_outputs(r#"sine s c4 arp note ."#, 1);
let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0));
}
#[test]
fn arp_fewer_deltas_than_notes() {
let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 g4 b4 arp note ."#, 4);
let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0));
assert!(approx_eq(notes[2], 67.0));
assert!(approx_eq(notes[3], 71.0));
let deltas = get_deltas(&outputs);
let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.0));
assert!(approx_eq(deltas[1], 0.5 * step_dur));
assert!(approx_eq(deltas[2], 0.0)); // wraps: 2 % 2 = 0
assert!(approx_eq(deltas[3], 0.5 * step_dur)); // wraps: 3 % 2 = 1
}
#[test]
fn arp_fewer_notes_than_deltas() {
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 arp note ."#, 4);
let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0));
assert!(approx_eq(notes[2], 60.0)); // wraps
assert!(approx_eq(notes[3], 64.0)); // wraps
}
#[test]
fn arp_multiple_params() {
let outputs = expect_outputs(r#"sine s c4 e4 g4 arp note 0.5 0.7 0.9 arp gain ."#, 3);
let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0));
assert!(approx_eq(notes[2], 67.0));
let gains = get_gains(&outputs);
assert!(approx_eq(gains[0], 0.5));
assert!(approx_eq(gains[1], 0.7));
assert!(approx_eq(gains[2], 0.9));
}
#[test]
fn arp_no_arp_unchanged() {
// Standard CycleList without arp → cross-product (backward compat)
let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 note ."#, 4);
let notes = get_notes(&outputs);
// Cross-product: each note at each delta
assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 60.0));
assert!(approx_eq(notes[2], 64.0));
assert!(approx_eq(notes[3], 64.0));
}
#[test]
fn arp_mixed_cycle_and_arp() {
// CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
// Each arp step plays both sine and saw simultaneously (poly stacking)
let outputs = expect_outputs(r#"sine saw s c4 e4 g4 arp note ."#, 6);
let sounds = get_sounds(&outputs);
// Arp step 0: poly 0=sine, poly 1=saw
assert_eq!(sounds[0], "sine");
assert_eq!(sounds[1], "saw");
// Arp step 1: poly 0=sine, poly 1=saw
assert_eq!(sounds[2], "sine");
assert_eq!(sounds[3], "saw");
// Arp step 2: poly 0=sine, poly 1=saw
assert_eq!(sounds[4], "sine");
assert_eq!(sounds[5], "saw");
let notes = get_notes(&outputs);
// Both poly voices in each arp step share the same note
assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 60.0));
assert!(approx_eq(notes[2], 64.0));
assert!(approx_eq(notes[3], 64.0));
assert!(approx_eq(notes[4], 67.0));
assert!(approx_eq(notes[5], 67.0));
}