Small fixes and additions
All checks were successful
Deploy Website / deploy (push) Has been skipped

This commit is contained in:
2026-03-05 19:20:52 +01:00
parent 0097777449
commit 5a72e4cef4
6 changed files with 76 additions and 66 deletions

View File

@@ -35,7 +35,7 @@ pub(super) const WORDS: &[Word] = &[
}, },
Word { Word {
name: "attack", name: "attack",
aliases: &["att"], aliases: &["att", "a"],
category: "Envelope", category: "Envelope",
stack: "(v.. --)", stack: "(v.. --)",
desc: "Set attack time", desc: "Set attack time",
@@ -45,7 +45,7 @@ pub(super) const WORDS: &[Word] = &[
}, },
Word { Word {
name: "decay", name: "decay",
aliases: &["dec"], aliases: &["dec", "d"],
category: "Envelope", category: "Envelope",
stack: "(v.. --)", stack: "(v.. --)",
desc: "Set decay time", desc: "Set decay time",
@@ -55,7 +55,7 @@ pub(super) const WORDS: &[Word] = &[
}, },
Word { Word {
name: "sustain", name: "sustain",
aliases: &["sus"], aliases: &["sus", "s"],
category: "Envelope", category: "Envelope",
stack: "(v.. --)", stack: "(v.. --)",
desc: "Set sustain level", desc: "Set sustain level",
@@ -65,7 +65,7 @@ pub(super) const WORDS: &[Word] = &[
}, },
Word { Word {
name: "release", name: "release",
aliases: &["rel"], aliases: &["rel", "r"],
category: "Envelope", category: "Envelope",
stack: "(v.. --)", stack: "(v.. --)",
desc: "Set release time", desc: "Set release time",

View File

@@ -6,7 +6,7 @@ pub(super) const WORDS: &[Word] = &[
// Sound // Sound
Word { Word {
name: "sound", name: "sound",
aliases: &["s"], aliases: &["snd"],
category: "Sound", category: "Sound",
stack: "(name --)", stack: "(name --)",
desc: "Begin sound command", desc: "Begin sound command",
@@ -377,6 +377,16 @@ pub(super) const WORDS: &[Word] = &[
compile: Param, compile: Param,
varargs: true, varargs: true,
}, },
Word {
name: "partials",
aliases: &[],
category: "Oscillator",
stack: "(v.. --)",
desc: "Set number of active harmonics (add source only)",
example: "16 partials",
compile: Param,
varargs: true,
},
Word { Word {
name: "coarse", name: "coarse",
aliases: &[], aliases: &[],

View File

@@ -23,7 +23,7 @@ fn redefine_word_overwrites() {
#[test] #[test]
fn word_with_param() { fn word_with_param() {
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud .", 1); let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" snd loud .", 1);
assert!(outputs[0].contains("gain/0.9")); assert!(outputs[0].contains("gain/0.9"));
} }
@@ -98,7 +98,7 @@ fn define_word_containing_quotation() {
#[test] #[test]
fn define_word_with_sound() { fn define_word_with_sound() {
let outputs = expect_outputs(": kick \"kick\" s . ; kick", 1); let outputs = expect_outputs(": kick \"kick\" snd . ; kick", 1);
assert!(outputs[0].contains("sound/kick")); assert!(outputs[0].contains("sound/kick"));
} }

View File

@@ -59,20 +59,20 @@ fn nested_quotations() {
#[test] #[test]
fn quotation_with_param() { fn quotation_with_param() {
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 1 ? ."#, 1); let outputs = expect_outputs(r#""kick" snd ( 2 distort ) 1 ? ."#, 1);
assert!(outputs[0].contains("distort/2")); assert!(outputs[0].contains("distort/2"));
} }
#[test] #[test]
fn quotation_skips_param() { fn quotation_skips_param() {
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 0 ? ."#, 1); let outputs = expect_outputs(r#""kick" snd ( 2 distort ) 0 ? ."#, 1);
assert!(!outputs[0].contains("distort")); assert!(!outputs[0].contains("distort"));
} }
#[test] #[test]
fn quotation_with_emit() { fn quotation_with_emit() {
// When true, . should fire // When true, . should fire
let outputs = expect_outputs(r#""kick" s ( . ) 1 ?"#, 1); let outputs = expect_outputs(r#""kick" snd ( . ) 1 ?"#, 1);
assert!(outputs[0].contains("kick")); assert!(outputs[0].contains("kick"));
} }
@@ -81,7 +81,7 @@ fn quotation_skips_emit() {
// When false, . should not fire // When false, . should not fire
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate(r#""kick" s ( . ) 0 ?"#, &default_ctx()) .evaluate(r#""kick" snd ( . ) 0 ?"#, &default_ctx())
.unwrap(); .unwrap();
// No output since . was skipped and no implicit emit // No output since . was skipped and no implicit emit
assert_eq!(outputs.len(), 0); assert_eq!(outputs.len(), 0);
@@ -109,7 +109,7 @@ fn every_with_quotation_integration() {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate(r#""kick" s ( 2 distort ) 2 every ."#, &ctx) .evaluate(r#""kick" snd ( 2 distort ) 2 every ."#, &ctx)
.unwrap(); .unwrap();
if iter % 2 == 0 { if iter % 2 == 0 {
assert!( assert!(
@@ -134,7 +134,7 @@ fn bjork_with_sound() {
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8) let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate(r#""kick" s ( 2 distort ) 3 8 bjork ."#, &ctx) .evaluate(r#""kick" snd ( 2 distort ) 3 8 bjork ."#, &ctx)
.unwrap(); .unwrap();
assert!(outputs[0].contains("distort/2")); assert!(outputs[0].contains("distort/2"));
} }
@@ -161,7 +161,7 @@ fn when_and_unless_complementary() {
let f = forth(); let f = forth();
let outputs = f let outputs = f
.evaluate( .evaluate(
r#""kick" s ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#, r#""kick" snd ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
&ctx, &ctx,
) )
.unwrap(); .unwrap();

View File

@@ -8,13 +8,13 @@ fn basic_emit() {
#[test] #[test]
fn alias_s() { fn alias_s() {
let outputs = expect_outputs(r#""snare" s ."#, 1); let outputs = expect_outputs(r#""snare" snd ."#, 1);
assert!(outputs[0].contains("sound/snare")); assert!(outputs[0].contains("sound/snare"));
} }
#[test] #[test]
fn with_params() { fn with_params() {
let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain ."#, 1); let outputs = expect_outputs(r#""kick" snd 440 freq 0.5 gain ."#, 1);
assert!(outputs[0].contains("sound/kick")); assert!(outputs[0].contains("sound/kick"));
assert!(outputs[0].contains("freq/440")); assert!(outputs[0].contains("freq/440"));
assert!(outputs[0].contains("gain/0.5")); assert!(outputs[0].contains("gain/0.5"));
@@ -22,13 +22,13 @@ fn with_params() {
#[test] #[test]
fn auto_dur() { fn auto_dur() {
let outputs = expect_outputs(r#""kick" s ."#, 1); let outputs = expect_outputs(r#""kick" snd ."#, 1);
assert!(outputs[0].contains("dur/")); assert!(outputs[0].contains("dur/"));
} }
#[test] #[test]
fn auto_delaytime() { fn auto_delaytime() {
let outputs = expect_outputs(r#""kick" s ."#, 1); let outputs = expect_outputs(r#""kick" snd ."#, 1);
assert!(outputs[0].contains("delaytime/")); assert!(outputs[0].contains("delaytime/"));
} }
@@ -39,7 +39,7 @@ fn emit_no_sound() {
#[test] #[test]
fn multiple_emits() { fn multiple_emits() {
let outputs = expect_outputs(r#""kick" s . "snare" s ."#, 2); let outputs = expect_outputs(r#""kick" snd . "snare" snd ."#, 2);
assert!(outputs[0].contains("sound/kick")); assert!(outputs[0].contains("sound/kick"));
assert!(outputs[1].contains("sound/snare")); assert!(outputs[1].contains("sound/snare"));
} }
@@ -48,7 +48,7 @@ fn multiple_emits() {
fn envelope_params() { fn envelope_params() {
// Values are tempo-scaled: 0.01 * step_duration(0.125) = 0.00125, etc. // Values are tempo-scaled: 0.01 * step_duration(0.125) = 0.00125, etc.
let outputs = expect_outputs( let outputs = expect_outputs(
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#, r#""synth" snd 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
1, 1,
); );
assert!(outputs[0].contains("attack/0.00125")); assert!(outputs[0].contains("attack/0.00125"));
@@ -59,14 +59,14 @@ fn envelope_params() {
#[test] #[test]
fn filter_params() { fn filter_params() {
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq ."#, 1); let outputs = expect_outputs(r#""synth" snd 2000 lpf 0.5 lpq ."#, 1);
assert!(outputs[0].contains("lpf/2000")); assert!(outputs[0].contains("lpf/2000"));
assert!(outputs[0].contains("lpq/0.5")); assert!(outputs[0].contains("lpq/0.5"));
} }
#[test] #[test]
fn adsr_sets_all_envelope_params() { fn adsr_sets_all_envelope_params() {
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1); let outputs = expect_outputs(r#""synth" snd 0.01 0.1 0.5 0.3 adsr ."#, 1);
assert!(outputs[0].contains("attack/0.00125")); assert!(outputs[0].contains("attack/0.00125"));
assert!(outputs[0].contains("decay/0.0125")); assert!(outputs[0].contains("decay/0.0125"));
assert!(outputs[0].contains("sustain/0.5")); assert!(outputs[0].contains("sustain/0.5"));
@@ -75,7 +75,7 @@ fn adsr_sets_all_envelope_params() {
#[test] #[test]
fn ad_sets_attack_decay_sustain_zero() { fn ad_sets_attack_decay_sustain_zero() {
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1); let outputs = expect_outputs(r#""synth" snd 0.01 0.1 ad ."#, 1);
assert!(outputs[0].contains("attack/0.00125")); assert!(outputs[0].contains("attack/0.00125"));
assert!(outputs[0].contains("decay/0.0125")); assert!(outputs[0].contains("decay/0.0125"));
assert!(outputs[0].contains("sustain/0")); assert!(outputs[0].contains("sustain/0"));
@@ -83,7 +83,7 @@ fn ad_sets_attack_decay_sustain_zero() {
#[test] #[test]
fn bank_param() { fn bank_param() {
let outputs = expect_outputs(r#""loop" s "a" bank ."#, 1); let outputs = expect_outputs(r#""loop" snd "a" bank ."#, 1);
assert!(outputs[0].contains("sound/loop")); assert!(outputs[0].contains("sound/loop"));
assert!(outputs[0].contains("bank/a")); assert!(outputs[0].contains("bank/a"));
} }
@@ -109,7 +109,7 @@ fn param_only_multiple_params() {
#[test] #[test]
fn polyphonic_notes() { fn polyphonic_notes() {
let outputs = expect_outputs(r#"60 64 67 note sine s ."#, 3); let outputs = expect_outputs(r#"60 64 67 note sine snd ."#, 3);
assert!(outputs[0].contains("note/60")); assert!(outputs[0].contains("note/60"));
assert!(outputs[1].contains("note/64")); assert!(outputs[1].contains("note/64"));
assert!(outputs[2].contains("note/67")); assert!(outputs[2].contains("note/67"));
@@ -117,14 +117,14 @@ fn polyphonic_notes() {
#[test] #[test]
fn polyphonic_sounds() { fn polyphonic_sounds() {
let outputs = expect_outputs(r#"440 freq kick hat s ."#, 2); let outputs = expect_outputs(r#"440 freq kick hat snd ."#, 2);
assert!(outputs[0].contains("sound/kick")); assert!(outputs[0].contains("sound/kick"));
assert!(outputs[1].contains("sound/hat")); assert!(outputs[1].contains("sound/hat"));
} }
#[test] #[test]
fn polyphonic_cycling() { fn polyphonic_cycling() {
let outputs = expect_outputs(r#"60 64 67 note 0.5 1.0 gain sine s ."#, 3); let outputs = expect_outputs(r#"60 64 67 note 0.5 1.0 gain sine snd ."#, 3);
assert!(outputs[0].contains("note/60")); assert!(outputs[0].contains("note/60"));
assert!(outputs[0].contains("gain/0.5")); assert!(outputs[0].contains("gain/0.5"));
assert!(outputs[1].contains("note/64")); assert!(outputs[1].contains("note/64"));
@@ -135,7 +135,7 @@ fn polyphonic_cycling() {
#[test] #[test]
fn polyphonic_with_at() { fn polyphonic_with_at() {
let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine s ."#, 4); let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine snd ."#, 4);
assert_eq!(outputs.len(), 4); assert_eq!(outputs.len(), 4);
} }
@@ -148,7 +148,7 @@ fn explicit_dur_zero_is_infinite() {
#[test] #[test]
fn all_before_sounds() { fn all_before_sounds() {
let outputs = expect_outputs( let outputs = expect_outputs(
r#"500 lpf 0.5 verb all "kick" s 60 note . "hat" s 70 note ."#, r#"500 lpf 0.5 verb all "kick" snd 60 note . "hat" snd 70 note ."#,
2, 2,
); );
assert!(outputs[0].contains("sound/kick")); assert!(outputs[0].contains("sound/kick"));
@@ -162,7 +162,7 @@ fn all_before_sounds() {
#[test] #[test]
fn all_after_sounds() { fn all_after_sounds() {
let outputs = expect_outputs( let outputs = expect_outputs(
r#""kick" s 60 note . "hat" s 70 note . 500 lpf 0.5 verb all"#, r#""kick" snd 60 note . "hat" snd 70 note . 500 lpf 0.5 verb all"#,
2, 2,
); );
assert!(outputs[0].contains("sound/kick")); assert!(outputs[0].contains("sound/kick"));
@@ -176,7 +176,7 @@ fn all_after_sounds() {
#[test] #[test]
fn noall_clears_global_params() { fn noall_clears_global_params() {
let outputs = expect_outputs( let outputs = expect_outputs(
r#"500 lpf all "kick" s 60 note . noall "hat" s 70 note ."#, r#"500 lpf all "kick" snd 60 note . noall "hat" snd 70 note ."#,
2, 2,
); );
assert!(outputs[0].contains("lpf/500")); assert!(outputs[0].contains("lpf/500"));
@@ -187,7 +187,7 @@ fn noall_clears_global_params() {
fn all_with_tempo_scaled_params() { fn all_with_tempo_scaled_params() {
// attack is tempo-scaled: 0.01 * step_duration(0.125) = 0.00125 // attack is tempo-scaled: 0.01 * step_duration(0.125) = 0.00125
let outputs = expect_outputs( let outputs = expect_outputs(
r#"0.01 attack all "kick" s 60 note ."#, r#"0.01 attack all "kick" snd 60 note ."#,
1, 1,
); );
assert!(outputs[0].contains("attack/0.00125")); assert!(outputs[0].contains("attack/0.00125"));
@@ -196,7 +196,7 @@ fn all_with_tempo_scaled_params() {
#[test] #[test]
fn all_per_sound_override() { fn all_per_sound_override() {
let outputs = expect_outputs( let outputs = expect_outputs(
r#"500 lpf all "kick" s 2000 lpf . "hat" s ."#, r#"500 lpf all "kick" snd 2000 lpf . "hat" snd ."#,
2, 2,
); );
// kick has both global lpf=500 and per-sound lpf=2000; per-sound wins (comes last) // kick has both global lpf=500 and per-sound lpf=2000; per-sound wins (comes last)
@@ -210,7 +210,7 @@ fn all_persists_across_evaluations() {
let f = forth(); let f = forth();
let ctx = default_ctx(); let ctx = default_ctx();
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap(); f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd 60 note ."#, &ctx).unwrap();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert!(outputs[0].contains("lpf/500"), "global lpf missing: {}", outputs[0]); assert!(outputs[0].contains("lpf/500"), "global lpf missing: {}", outputs[0]);
assert!(outputs[0].contains("verb/0.5"), "global verb missing: {}", outputs[0]); assert!(outputs[0].contains("verb/0.5"), "global verb missing: {}", outputs[0]);
@@ -222,7 +222,7 @@ fn noall_clears_across_evaluations() {
let ctx = default_ctx(); let ctx = default_ctx();
f.evaluate(r#"500 lpf all"#, &ctx).unwrap(); f.evaluate(r#"500 lpf all"#, &ctx).unwrap();
f.evaluate(r#"noall"#, &ctx).unwrap(); f.evaluate(r#"noall"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd 60 note ."#, &ctx).unwrap();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]); assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]);
} }
@@ -251,20 +251,20 @@ fn all_replaces_previous_global() {
let ctx = default_ctx(); let ctx = default_ctx();
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap(); f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
f.evaluate(r#"2000 lpf all"#, &ctx).unwrap(); f.evaluate(r#"2000 lpf all"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s ."#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd ."#, &ctx).unwrap();
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]); assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]);
} }
#[test] #[test]
fn slice_param() { fn slice_param() {
let outputs = expect_outputs(r#""break" s 8 slice ."#, 1); let outputs = expect_outputs(r#""break" snd 8 slice ."#, 1);
assert!(outputs[0].contains("slice/8")); assert!(outputs[0].contains("slice/8"));
} }
#[test] #[test]
fn pick_param() { fn pick_param() {
let outputs = expect_outputs(r#""break" s 8 slice 3 pick ."#, 1); let outputs = expect_outputs(r#""break" snd 8 slice 3 pick ."#, 1);
assert!(outputs[0].contains("slice/8")); assert!(outputs[0].contains("slice/8"));
assert!(outputs[0].contains("pick/3")); assert!(outputs[0].contains("pick/3"));
} }

View File

@@ -56,14 +56,14 @@ fn stepdur_baseline() {
#[test] #[test]
fn single_emit() { fn single_emit() {
let outputs = expect_outputs(r#""kick" s ."#, 1); let outputs = expect_outputs(r#""kick" snd ."#, 1);
let deltas = get_deltas(&outputs); let deltas = get_deltas(&outputs);
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0"); assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
} }
#[test] #[test]
fn multiple_emits_all_at_zero() { fn multiple_emits_all_at_zero() {
let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let outputs = expect_outputs(r#""kick" snd . . . ."#, 4);
let deltas = get_deltas(&outputs); let deltas = get_deltas(&outputs);
for (i, delta) in deltas.iter().enumerate() { for (i, delta) in deltas.iter().enumerate() {
assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta); assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
@@ -72,7 +72,7 @@ fn multiple_emits_all_at_zero() {
#[test] #[test]
fn sound_persists() { fn sound_persists() {
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4); let outputs = expect_outputs(r#""kick" snd . . "hat" snd . ."#, 4);
let sounds = get_sounds(&outputs); let sounds = get_sounds(&outputs);
assert_eq!(sounds[0], "kick"); assert_eq!(sounds[0], "kick");
assert_eq!(sounds[1], "kick"); assert_eq!(sounds[1], "kick");
@@ -82,14 +82,14 @@ fn sound_persists() {
#[test] #[test]
fn alternating_sounds() { fn alternating_sounds() {
let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4); let outputs = expect_outputs(r#""kick" snd . "snare" snd . "kick" snd . "snare" snd ."#, 4);
let sounds = get_sounds(&outputs); let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
} }
#[test] #[test]
fn dur_is_step_duration() { fn dur_is_step_duration() {
let outputs = expect_outputs(r#""kick" s ."#, 1); let outputs = expect_outputs(r#""kick" snd ."#, 1);
let durs = get_durs(&outputs); let durs = get_durs(&outputs);
assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]); assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
} }
@@ -99,7 +99,7 @@ fn cycle_picks_by_runs() {
for runs in 0..4 { for runs in 0..4 {
let ctx = ctx_with(|c| c.runs = runs); let ctx = ctx_with(|c| c.runs = runs);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 cycle"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd ( . ) ( ) 2 cycle"#, &ctx).unwrap();
if runs % 2 == 0 { if runs % 2 == 0 {
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs); assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
} else { } else {
@@ -113,7 +113,7 @@ fn pcycle_picks_by_iter() {
for iter in 0..4 { for iter in 0..4 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 pcycle"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
if iter % 2 == 0 { if iter % 2 == 0 {
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter); assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
} else { } else {
@@ -128,7 +128,7 @@ fn cycle_with_sounds() {
let ctx = ctx_with(|c| c.runs = runs); let ctx = ctx_with(|c| c.runs = runs);
let f = forth(); let f = forth();
let outputs = f.evaluate( let outputs = f.evaluate(
r#"( "kick" s . ) ( "hat" s . ) ( "snare" s . ) 3 cycle"#, r#"( "kick" snd . ) ( "hat" snd . ) ( "snare" snd . ) 3 cycle"#,
&ctx &ctx
).unwrap(); ).unwrap();
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs); assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
@@ -141,7 +141,7 @@ fn cycle_with_sounds() {
#[test] #[test]
fn at_single_delta() { fn at_single_delta() {
let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1); let outputs = expect_outputs(r#"0.5 at "kick" snd ."#, 1);
let deltas = get_deltas(&outputs); let deltas = get_deltas(&outputs);
let step_dur = 0.125; let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]); assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
@@ -149,7 +149,7 @@ fn at_single_delta() {
#[test] #[test]
fn at_list_deltas() { fn at_list_deltas() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s ."#, 2); let outputs = expect_outputs(r#"0 0.5 at "kick" snd ."#, 2);
let deltas = get_deltas(&outputs); let deltas = get_deltas(&outputs);
let step_dur = 0.125; let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]); assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
@@ -158,7 +158,7 @@ fn at_list_deltas() {
#[test] #[test]
fn at_three_deltas() { fn at_three_deltas() {
let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" s ."#, 3); let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" snd ."#, 3);
let deltas = get_deltas(&outputs); let deltas = get_deltas(&outputs);
let step_dur = 0.125; let step_dur = 0.125;
assert!(approx_eq(deltas[0], 0.0), "expected delta 0"); assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
@@ -168,7 +168,7 @@ fn at_three_deltas() {
#[test] #[test]
fn at_persists_across_emits() { fn at_persists_across_emits() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s . "hat" s ."#, 4); let outputs = expect_outputs(r#"0 0.5 at "kick" snd . "hat" snd ."#, 4);
let sounds = get_sounds(&outputs); let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]); assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
} }
@@ -176,14 +176,14 @@ fn at_persists_across_emits() {
#[test] #[test]
fn at_reset_with_zero() { fn at_reset_with_zero() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s . 0.0 at "hat" s ."#, 3); let outputs = expect_outputs(r#"0 0.5 at "kick" snd . 0.0 at "hat" snd ."#, 3);
let sounds = get_sounds(&outputs); let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "kick", "hat"]); assert_eq!(sounds, vec!["kick", "kick", "hat"]);
} }
#[test] #[test]
fn clear_resets_at_deltas() { fn clear_resets_at_deltas() {
let outputs = expect_outputs(r#"0 0.5 at "kick" s . clear "hat" s ."#, 3); let outputs = expect_outputs(r#"0 0.5 at "kick" snd . clear "hat" snd ."#, 3);
let sounds = get_sounds(&outputs); let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "kick", "hat"]); assert_eq!(sounds, vec!["kick", "kick", "hat"]);
let deltas = get_deltas(&outputs); let deltas = get_deltas(&outputs);
@@ -196,7 +196,7 @@ fn at_records_selected_spans() {
let f = forth(); let f = forth();
let mut trace = ExecutionTrace::default(); let mut trace = ExecutionTrace::default();
let script = r#"0 0.5 0.75 at "kick" s ."#; let script = r#"0 0.5 0.75 at "kick" snd ."#;
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap(); 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) // Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
@@ -226,7 +226,7 @@ fn get_gains(outputs: &[String]) -> Vec<f64> {
#[test] #[test]
fn arp_auto_subdivide() { fn arp_auto_subdivide() {
let outputs = expect_outputs(r#"sine s c4 e4 g4 b4 arp note ."#, 4); let outputs = expect_outputs(r#"sine snd c4 e4 g4 b4 arp note ."#, 4);
let notes = get_notes(&outputs); let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0)); assert!(approx_eq(notes[1], 64.0));
@@ -242,7 +242,7 @@ fn arp_auto_subdivide() {
#[test] #[test]
fn arp_with_explicit_at() { 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 outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 g4 b4 arp note ."#, 4);
let notes = get_notes(&outputs); let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0)); assert!(approx_eq(notes[1], 64.0));
@@ -258,14 +258,14 @@ fn arp_with_explicit_at() {
#[test] #[test]
fn arp_single_note() { fn arp_single_note() {
let outputs = expect_outputs(r#"sine s c4 arp note ."#, 1); let outputs = expect_outputs(r#"sine snd c4 arp note ."#, 1);
let notes = get_notes(&outputs); let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
} }
#[test] #[test]
fn arp_fewer_deltas_than_notes() { 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 outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 g4 b4 arp note ."#, 4);
let notes = get_notes(&outputs); let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0)); assert!(approx_eq(notes[1], 64.0));
@@ -281,7 +281,7 @@ fn arp_fewer_deltas_than_notes() {
#[test] #[test]
fn arp_fewer_notes_than_deltas() { 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 outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 arp note ."#, 4);
let notes = get_notes(&outputs); let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0)); assert!(approx_eq(notes[1], 64.0));
@@ -291,7 +291,7 @@ fn arp_fewer_notes_than_deltas() {
#[test] #[test]
fn arp_multiple_params() { 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 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); let notes = get_notes(&outputs);
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0)); assert!(approx_eq(notes[1], 64.0));
@@ -305,7 +305,7 @@ fn arp_multiple_params() {
#[test] #[test]
fn arp_no_arp_unchanged() { fn arp_no_arp_unchanged() {
// Standard CycleList without arp → cross-product (backward compat) // Standard CycleList without arp → cross-product (backward compat)
let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 note ."#, 4); let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 note ."#, 4);
let notes = get_notes(&outputs); let notes = get_notes(&outputs);
// Cross-product: each note at each delta // Cross-product: each note at each delta
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
@@ -318,7 +318,7 @@ fn arp_no_arp_unchanged() {
fn arp_mixed_cycle_and_arp() { fn arp_mixed_cycle_and_arp() {
// CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices // CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
// Each arp step plays both sine and saw simultaneously (poly stacking) // 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 outputs = expect_outputs(r#"sine saw snd c4 e4 g4 arp note ."#, 6);
let sounds = get_sounds(&outputs); let sounds = get_sounds(&outputs);
// Arp step 0: poly 0=sine, poly 1=saw // Arp step 0: poly 0=sine, poly 1=saw
assert_eq!(sounds[0], "sine"); assert_eq!(sounds[0], "sine");
@@ -346,7 +346,7 @@ fn every_offset_fires_at_offset() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 every+"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd ( . ) 4 2 every+"#, &ctx).unwrap();
if iter % 4 == 2 { if iter % 4 == 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter); assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
} else { } else {
@@ -361,7 +361,7 @@ fn every_offset_wraps_large_offset() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s ( . ) 4 6 every+"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd ( . ) 4 6 every+"#, &ctx).unwrap();
if iter % 4 == 2 { if iter % 4 == 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter); assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
} else { } else {
@@ -375,7 +375,7 @@ fn except_offset_inverse() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 except+"#, &ctx).unwrap(); let outputs = f.evaluate(r#""kick" snd ( . ) 4 2 except+"#, &ctx).unwrap();
if iter % 4 != 2 { if iter % 4 != 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter); assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
} else { } else {
@@ -389,8 +389,8 @@ fn every_offset_zero_is_same_as_every() {
for iter in 0..8 { for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter); let ctx = ctx_with(|c| c.iter = iter);
let f = forth(); let f = forth();
let a = f.evaluate(r#""kick" s ( . ) 3 every"#, &ctx).unwrap(); let a = f.evaluate(r#""kick" snd ( . ) 3 every"#, &ctx).unwrap();
let b = f.evaluate(r#""kick" s ( . ) 3 0 every+"#, &ctx).unwrap(); let b = f.evaluate(r#""kick" snd ( . ) 3 0 every+"#, &ctx).unwrap();
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter); assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
} }
} }