From d9e6505e07c5864f412fa2e3a16facfc704645e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 23 Feb 2026 01:18:43 +0100 Subject: [PATCH] Feat: fixes and demo --- crates/forth/src/vm.rs | 78 ++++++++++++++++++++++++----------------- demos/01.cagire | 38 +++++++++++++++++--- tests/forth/temporal.rs | 20 ++++++++--- 3 files changed, 94 insertions(+), 42 deletions(-) diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index ecad6a6..a74c9bc 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -237,12 +237,14 @@ impl Forth { }; let emit_with_cycling = |cmd: &CmdRegister, - emit_idx: usize, + arp_idx: usize, + poly_idx: usize, delta_secs: f64, outputs: &mut Vec| -> Result, String> { let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?; - let resolved_sound_val = sound_opt.map(|sv| resolve_cycling(sv, emit_idx)); + let resolved_sound_val = + sound_opt.map(|sv| resolve_value(sv, arp_idx, poly_idx)); let sound_str = match &resolved_sound_val { Some(v) => Some(v.as_str()?.to_string()), None => None, @@ -250,7 +252,7 @@ impl Forth { let resolved_params: Vec<(&str, String)> = params .iter() .map(|(k, v)| { - let resolved = resolve_cycling(v, emit_idx); + let resolved = resolve_value(v, arp_idx, poly_idx); if let Value::CycleList(_) | Value::ArpList(_) = v { if let Some(span) = resolved.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { @@ -544,6 +546,7 @@ impl Forth { Op::Emit => { if has_arp_list(cmd) { let arp_count = compute_arp_count(cmd); + let poly_count = compute_poly_count(cmd); let explicit_deltas = !cmd.deltas().is_empty(); let delta_list: Vec = if explicit_deltas { cmd.deltas().to_vec() @@ -570,12 +573,16 @@ impl Forth { ctx.nudge_secs + (i as f64 / count as f64) * ctx.step_duration() }; - if let Some(sound_val) = - emit_with_cycling(cmd, i, delta_secs, outputs)? - { - if let Some(span) = sound_val.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); + for poly_i in 0..poly_count { + if let Some(sound_val) = + emit_with_cycling(cmd, i, poly_i, delta_secs, outputs)? + { + if let Some(span) = sound_val.span() { + if let Some(trace) = + trace_cell.borrow_mut().as_mut() + { + trace.selected_spans.push(span); + } } } } @@ -599,7 +606,7 @@ impl Forth { } } if let Some(sound_val) = - emit_with_cycling(cmd, poly_idx, delta_secs, outputs)? + emit_with_cycling(cmd, 0, poly_idx, delta_secs, outputs)? { if let Some(span) = sound_val.span() { if let Some(trace) = @@ -1241,9 +1248,10 @@ impl Forth { Op::MidiEmit => { let (_, params) = cmd.snapshot().unwrap_or((None, &[])); - // Build schedule: (emit_idx, delta_secs) — same logic as Op::Emit - let schedule: Vec<(usize, f64)> = if has_arp_list(cmd) { + // Build schedule: (arp_idx, poly_idx, delta_secs) + let schedule: Vec<(usize, usize, f64)> = if has_arp_list(cmd) { let arp_count = compute_arp_count(cmd); + let poly_count = compute_poly_count(cmd); let explicit = !cmd.deltas().is_empty(); let delta_list = cmd.deltas(); let count = if explicit { @@ -1251,20 +1259,22 @@ impl Forth { } else { arp_count }; - (0..count) - .map(|i| { - let delta_secs = if explicit { - let frac = delta_list[i % delta_list.len()] - .as_float() - .unwrap_or(0.0); - ctx.nudge_secs + frac * ctx.step_duration() - } else { - ctx.nudge_secs - + (i as f64 / count as f64) * ctx.step_duration() - }; - (i, delta_secs) - }) - .collect() + let mut sched = Vec::with_capacity(count * poly_count); + for i in 0..count { + let delta_secs = if explicit { + let frac = delta_list[i % delta_list.len()] + .as_float() + .unwrap_or(0.0); + ctx.nudge_secs + frac * ctx.step_duration() + } else { + ctx.nudge_secs + + (i as f64 / count as f64) * ctx.step_duration() + }; + for poly_i in 0..poly_count { + sched.push((i, poly_i, delta_secs)); + } + } + sched } else { let poly_count = compute_poly_count(cmd); let deltas: Vec = if cmd.deltas().is_empty() { @@ -1279,6 +1289,7 @@ impl Forth { for poly_idx in 0..poly_count { for &frac in &deltas { sched.push(( + 0, poly_idx, ctx.nudge_secs + frac * ctx.step_duration(), )); @@ -1287,14 +1298,14 @@ impl Forth { sched }; - for (emit_idx, delta_secs) in schedule { + for (arp_idx, poly_idx, delta_secs) in schedule { let get_int = |name: &str| -> Option { params .iter() .rev() .find(|(k, _)| *k == name) .and_then(|(_, v)| { - resolve_cycling(v, emit_idx).as_int().ok() + resolve_value(v, arp_idx, poly_idx).as_int().ok() }) }; let get_float = |name: &str| -> Option { @@ -1303,7 +1314,7 @@ impl Forth { .rev() .find(|(k, _)| *k == name) .and_then(|(_, v)| { - resolve_cycling(v, emit_idx).as_float().ok() + resolve_value(v, arp_idx, poly_idx).as_float().ok() }) }; let chan = get_int("chan") @@ -1680,10 +1691,13 @@ where Ok(()) } -fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> { +fn resolve_value(val: &Value, arp_idx: usize, poly_idx: usize) -> Cow<'_, Value> { match val { - Value::CycleList(items) | Value::ArpList(items) if !items.is_empty() => { - Cow::Owned(items[emit_idx % items.len()].clone()) + Value::ArpList(items) if !items.is_empty() => { + Cow::Owned(items[arp_idx % items.len()].clone()) + } + Value::CycleList(items) if !items.is_empty() => { + Cow::Owned(items[poly_idx % items.len()].clone()) } other => Cow::Borrowed(other), } diff --git a/demos/01.cagire b/demos/01.cagire index c8075cd..f1f76d2 100644 --- a/demos/01.cagire +++ b/demos/01.cagire @@ -7,22 +7,45 @@ "steps": [ { "i": 0, - "script": "sine sound ." + "script": "0 7 .. at\n c2 maj9 arp note\n wide bigverb mysynth \n 2000 1000 0.4 0.8 rand expslide llpf\n 0.4 0.8 rand llpq\n ." + }, + { + "i": 8, + "source": 0 } ], "length": 16, "speed": [ 1, 1 - ] + ], + "name": "bigsynth" }, { - "steps": [], + "steps": [ + { + "i": 0, + "script": "kick sound ." + }, + { + "i": 4, + "source": 0 + }, + { + "i": 8, + "source": 0 + }, + { + "i": 12, + "source": 0 + } + ], "length": 16, "speed": [ 1, 1 - ] + ], + "name": "kick" }, { "steps": [], @@ -8365,6 +8388,11 @@ [ 0, 0 + ], + [ + 0, + 1 ] - ] + ], + "prelude": ": mysynth saw pulse white sound \n0.7 gain 1 decay ;\n: bigverb 0.5 verb 0.1 verbdamp ;\n: wide 0.2 haas 2 width ;" } \ No newline at end of file diff --git a/tests/forth/temporal.rs b/tests/forth/temporal.rs index a41f104..f960b2e 100644 --- a/tests/forth/temporal.rs +++ b/tests/forth/temporal.rs @@ -316,15 +316,25 @@ fn arp_no_arp_unchanged() { #[test] fn arp_mixed_cycle_and_arp() { - // CycleList sound + ArpList note → flat loop, sound cycles - let outputs = expect_outputs(r#"sine saw s c4 e4 g4 arp note ."#, 3); + // 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); - // Sound is CycleList, cycles across the 3 arp emissions + // 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], 64.0)); - assert!(approx_eq(notes[2], 67.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)); }