Feat: improve 'at' in cagire grammar
This commit is contained in:
@@ -253,19 +253,12 @@ fn test_midi_at_with_polyphony() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_arp_notes() {
|
||||
let outputs = expect_outputs("c4 e4 g4 arp note m.", 3);
|
||||
assert!(outputs[0].contains("/note/60/"));
|
||||
assert!(outputs[1].contains("/note/64/"));
|
||||
assert!(outputs[2].contains("/note/67/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_arp_with_at() {
|
||||
let outputs = expect_outputs("0 0.25 0.5 at c4 e4 g4 arp note m.", 3);
|
||||
assert!(outputs[0].contains("/note/60/"));
|
||||
assert!(outputs[1].contains("/note/64/"));
|
||||
assert!(outputs[2].contains("/note/67/"));
|
||||
fn test_midi_at_loop_notes() {
|
||||
// at-loop with m. closer: 3 iterations, each emits one MIDI note
|
||||
let outputs = expect_outputs("0 0.25 0.5 at 60 note m.", 3);
|
||||
for o in &outputs {
|
||||
assert!(o.contains("/note/60/"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -171,7 +171,9 @@ fn at_three_deltas() {
|
||||
|
||||
#[test]
|
||||
fn at_persists_across_emits() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . "hat" snd ."#, 4);
|
||||
// With at as a loop, each at...block is independent.
|
||||
// Two separate at blocks each emit twice.
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . 0 0.5 at "hat" snd ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
||||
}
|
||||
@@ -202,17 +204,10 @@ fn at_records_selected_spans() {
|
||||
let script = r#"0 0.5 0.75 at "kick" snd ."#;
|
||||
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");
|
||||
// With at-loop, each iteration emits once: 3 sound spans total
|
||||
assert_eq!(trace.selected_spans.len(), 3, "expected 3 selected spans (1 sound per iteration)");
|
||||
}
|
||||
|
||||
// --- arp tests ---
|
||||
|
||||
fn get_notes(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
@@ -220,16 +215,13 @@ fn get_notes(outputs: &[String]) -> Vec<f64> {
|
||||
.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 snd c4 e4 g4 b4 arp note ."#, 4);
|
||||
fn at_loop_with_cycle_notes() {
|
||||
// at-loop + cycle replaces at + arp: cycle advances per subdivision
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#"0 0.25 0.5 0.75 at sine snd [ c4 e4 g4 b4 ] cycle note ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 4);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
@@ -245,104 +237,41 @@ fn arp_auto_subdivide() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arp_with_explicit_at() {
|
||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 g4 b4 arp note ."#, 4);
|
||||
fn at_loop_cycle_wraps() {
|
||||
// cycle inside at-loop wraps when more deltas than items
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#"0 0.25 0.5 0.75 at sine snd [ c4 e4 ] cycle note ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 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;
|
||||
let sr: f64 = 48000.0;
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], (0.25 * step_dur * sr).round()));
|
||||
assert!(approx_eq(deltas[2], (0.5 * step_dur * sr).round()));
|
||||
assert!(approx_eq(deltas[3], (0.75 * step_dur * sr).round()));
|
||||
assert!(approx_eq(notes[0], 60.0)); // idx 0 % 2 = 0 -> c4
|
||||
assert!(approx_eq(notes[1], 64.0)); // idx 1 % 2 = 1 -> e4
|
||||
assert!(approx_eq(notes[2], 60.0)); // idx 2 % 2 = 0 -> c4
|
||||
assert!(approx_eq(notes[3], 64.0)); // idx 3 % 2 = 1 -> e4
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arp_single_note() {
|
||||
let outputs = expect_outputs(r#"sine snd c4 arp note ."#, 1);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
fn at_loop_rand_different_per_subdivision() {
|
||||
// rand inside at-loop produces different values per iteration
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#"0 0.5 at sine snd 1 1000 rand freq ."#, &default_ctx()).unwrap();
|
||||
assert_eq!(outputs.len(), 2);
|
||||
let freqs: Vec<f64> = outputs.iter()
|
||||
.map(|o| parse_params(o).get("freq").copied().unwrap_or(0.0))
|
||||
.collect();
|
||||
assert!(freqs[0] != freqs[1], "rand should produce different values: {} vs {}", freqs[0], freqs[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arp_fewer_deltas_than_notes() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at sine snd 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;
|
||||
let sr: f64 = 48000.0;
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], (0.5 * step_dur * sr).round()));
|
||||
assert!(approx_eq(deltas[2], 0.0)); // wraps: 2 % 2 = 0
|
||||
assert!(approx_eq(deltas[3], (0.5 * step_dur * sr).round())); // 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 snd 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 snd 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)
|
||||
fn at_loop_poly_cycling() {
|
||||
// CycleList inside at-loop: poly stacking within each iteration
|
||||
let outputs = expect_outputs(r#"0 0.5 at sine snd 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 snd 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));
|
||||
// Each iteration emits both poly voices (c4 and e4)
|
||||
assert!(approx_eq(notes[0], 60.0)); // iter 0, poly 0
|
||||
assert!(approx_eq(notes[1], 64.0)); // iter 0, poly 1
|
||||
assert!(approx_eq(notes[2], 60.0)); // iter 1, poly 0
|
||||
assert!(approx_eq(notes[3], 64.0)); // iter 1, poly 1
|
||||
}
|
||||
|
||||
// --- every+ / except+ tests ---
|
||||
@@ -400,3 +329,102 @@ fn every_offset_zero_is_same_as_every() {
|
||||
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
||||
}
|
||||
}
|
||||
|
||||
// --- at-loop feature tests ---
|
||||
|
||||
#[test]
|
||||
fn at_loop_choose_independent_per_subdivision() {
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#"0 0.5 at sine snd [ 60 64 67 71 ] choose note ."#, &default_ctx()).unwrap();
|
||||
assert_eq!(outputs.len(), 2);
|
||||
// Both are valid notes from the set (just verify they're within range)
|
||||
let notes = get_notes(&outputs);
|
||||
for n in ¬es {
|
||||
assert!([60.0, 64.0, 67.0, 71.0].contains(n), "unexpected note {n}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_loop_multiple_blocks_independent() {
|
||||
let outputs = expect_outputs(
|
||||
r#"0 0.5 at "kick" snd . 0 0.25 0.5 at "hat" snd ."#,
|
||||
5,
|
||||
);
|
||||
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");
|
||||
assert_eq!(sounds[4], "hat");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_loop_single_delta_one_iteration() {
|
||||
let outputs = expect_outputs(r#"0.25 at "kick" snd ."#, 1);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds[0], "kick");
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step_dur = 0.125;
|
||||
let sr: f64 = 48000.0;
|
||||
assert!(approx_eq(deltas[0], (0.25 * step_dur * sr).round()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_without_closer_falls_back_to_legacy() {
|
||||
// When at has no matching closer (., m., done), falls back to Op::At
|
||||
let f = forth();
|
||||
let result = f.evaluate(r#"0 0.5 at "kick" snd"#, &default_ctx());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_loop_cycle_advances_across_runs() {
|
||||
// Across different runs values, cycle inside at-loop picks correctly
|
||||
for base_runs in 0..3 {
|
||||
let ctx = ctx_with(|c| c.runs = base_runs);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(
|
||||
r#"0 0.5 at sine snd [ c4 e4 g4 ] cycle note ."#,
|
||||
&ctx,
|
||||
).unwrap();
|
||||
assert_eq!(outputs.len(), 2);
|
||||
let notes = get_notes(&outputs);
|
||||
// runs for iter i = base_runs * 2 + i
|
||||
let expected_0 = [60.0, 64.0, 67.0][(base_runs * 2) % 3];
|
||||
let expected_1 = [60.0, 64.0, 67.0][(base_runs * 2 + 1) % 3];
|
||||
assert!(approx_eq(notes[0], expected_0), "runs={base_runs}: iter 0 expected {expected_0}, got {}", notes[0]);
|
||||
assert!(approx_eq(notes[1], expected_1), "runs={base_runs}: iter 1 expected {expected_1}, got {}", notes[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_loop_midi_emit() {
|
||||
let f = forth();
|
||||
let outputs = f.evaluate("0 0.25 0.5 at 60 note m.", &default_ctx()).unwrap();
|
||||
assert_eq!(outputs.len(), 3);
|
||||
for o in &outputs {
|
||||
assert!(o.contains("/midi/note/60/"));
|
||||
}
|
||||
// First should have no delta (or delta/0), others should have delta
|
||||
assert!(outputs[1].contains("/delta/"));
|
||||
assert!(outputs[2].contains("/delta/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_loop_done_no_emit() {
|
||||
let f = forth();
|
||||
let outputs = f.evaluate("0 0.5 at [ 1 2 ] cycle drop done", &default_ctx()).unwrap();
|
||||
assert!(outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_loop_done_sets_variables() {
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate("0 0.5 at [ 10 20 ] cycle !x done kick snd @x freq .", &default_ctx())
|
||||
.unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
// Last iteration wins: cycle(1) = 20
|
||||
let params = parse_params(&outputs[0]);
|
||||
assert!(approx_eq(*params.get("freq").unwrap(), 20.0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user