From f1f1b28b31ed1c80f510f63271de9e317c989a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Thu, 29 Jan 2026 01:28:57 +0100 Subject: [PATCH] Cleaning old temporal model --- crates/forth/src/ops.rs | 5 - crates/forth/src/types.rs | 51 ------- crates/forth/src/vm.rs | 303 ++++++++------------------------------ crates/forth/src/words.rs | 55 +------ tests/forth/temporal.rs | 240 +----------------------------- 5 files changed, 65 insertions(+), 589 deletions(-) diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 936004e..6a0f5c1 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -49,7 +49,6 @@ pub enum Op { NewCmd, SetParam(String), Emit, - Silence, Get, Set, GetContext(String), @@ -67,7 +66,6 @@ pub enum Op { ListEndCycle, PCycle, ListEndPCycle, - Scale, SetTempo, Every, Quotation(Vec, Option), @@ -84,9 +82,6 @@ pub enum Op { Loop, Degree(&'static [i64]), Oct, - DivStart, - DivEnd, - StackStart, EmitN, ClearCmd, SetSpeed, diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index 0957299..1e429db 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -147,54 +147,3 @@ impl CmdRegister { } } -#[derive(Clone, Debug)] -pub(super) struct PendingEmission { - pub sound: String, - pub params: Vec<(String, String)>, - pub slot_index: usize, -} - -#[derive(Clone, Debug)] -pub(super) struct ResolvedEmission { - pub sound: String, - pub params: Vec<(String, String)>, - pub parent_slot: usize, - pub offset_in_slot: f64, - pub dur: f64, -} - -#[derive(Clone, Debug)] -pub(super) struct ScopeContext { - pub start: f64, - pub duration: f64, - pub weight: f64, - pub slot_count: usize, - pub pending: Vec, - pub resolved: Vec, - pub stacked: bool, -} - -impl ScopeContext { - pub fn new(start: f64, duration: f64) -> Self { - Self { - start, - duration, - weight: 1.0, - slot_count: 0, - pending: Vec::new(), - resolved: Vec::new(), - stacked: false, - } - } - - pub fn claim_slot(&mut self) -> usize { - if self.stacked { - self.slot_count = 1; - 0 - } else { - let idx = self.slot_count; - self.slot_count += 1; - idx - } - } -} diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 08c9057..3f2a7e3 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -4,8 +4,7 @@ use rand::{Rng as RngTrait, SeedableRng}; use super::compiler::compile_script; use super::ops::Op; use super::types::{ - CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext, - Stack, StepContext, Value, Variables, + CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables, }; pub struct Forth { @@ -70,24 +69,9 @@ impl Forth { ) -> Result, String> { let mut stack = self.stack.lock().unwrap(); let mut outputs: Vec = Vec::new(); - let root_duration = ctx.step_duration() * 4.0; - let mut scope_stack: Vec = vec![ScopeContext::new(0.0, root_duration)]; let mut cmd = CmdRegister::default(); - self.execute_ops( - ops, - ctx, - &mut stack, - &mut outputs, - &mut scope_stack, - &mut cmd, - trace, - )?; - - // Resolve root scope at end of script - if let Some(scope) = scope_stack.pop() { - resolve_scope(&scope, ctx.step_duration(), ctx.nudge_secs, &mut outputs); - } + self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?; Ok(outputs) } @@ -100,70 +84,56 @@ impl Forth { ctx: &StepContext, stack: &mut Vec, outputs: &mut Vec, - scope_stack: &mut Vec, cmd: &mut CmdRegister, trace: Option<&mut ExecutionTrace>, ) -> Result<(), String> { let mut pc = 0; let trace_cell = std::cell::RefCell::new(trace); - // Executes a quotation value, handling trace recording and recursive dispatch. - let run_quotation = |quot: Value, - stack: &mut Vec, - outputs: &mut Vec, - scope_stack: &mut Vec, - cmd: &mut CmdRegister| - -> Result<(), String> { - match quot { - Value::Quotation(quot_ops, body_span) => { - if let Some(span) = body_span { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.executed_spans.push(span); + let run_quotation = + |quot: Value, stack: &mut Vec, outputs: &mut Vec, cmd: &mut CmdRegister| -> Result<(), String> { + match quot { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } } + let mut trace_opt = trace_cell.borrow_mut().take(); + self.execute_ops( + "_ops, + ctx, + stack, + outputs, + cmd, + trace_opt.as_deref_mut(), + )?; + *trace_cell.borrow_mut() = trace_opt; + Ok(()) } - let mut trace_opt = trace_cell.borrow_mut().take(); - self.execute_ops( - "_ops, - ctx, - stack, - outputs, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - )?; - *trace_cell.borrow_mut() = trace_opt; + _ => Err("expected quotation".into()), + } + }; + + let select_and_run = + |selected: Value, stack: &mut Vec, outputs: &mut Vec, cmd: &mut CmdRegister| -> Result<(), String> { + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if matches!(selected, Value::Quotation(..)) { + run_quotation(selected, stack, outputs, cmd) + } else { + stack.push(selected); Ok(()) } - _ => Err("expected quotation".into()), - } - }; + }; - // Selects a value from a list, records trace, and either executes (quotation) or pushes (other). - let select_and_run = |selected: Value, - stack: &mut Vec, - outputs: &mut Vec, - scope_stack: &mut Vec, - cmd: &mut CmdRegister| - -> Result<(), String> { - if let Some(span) = selected.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - if matches!(selected, Value::Quotation(..)) { - run_quotation(selected, stack, outputs, scope_stack, cmd) - } else { - stack.push(selected); - Ok(()) - } - }; - - // Drains `count` values from the stack, selects one by index, and runs it. let drain_select_run = |count: usize, idx: usize, stack: &mut Vec, outputs: &mut Vec, - scope_stack: &mut Vec, cmd: &mut CmdRegister| -> Result<(), String> { if stack.len() < count { @@ -172,15 +142,13 @@ impl Forth { let start = stack.len() - count; let values: Vec = stack.drain(start..).collect(); let selected = values[idx].clone(); - select_and_run(selected, stack, outputs, scope_stack, cmd) + select_and_run(selected, stack, outputs, cmd) }; - // Pops all values until a marker, selects one by index, and runs it. let drain_list_select_run = |idx_source: usize, err_msg: &str, stack: &mut Vec, outputs: &mut Vec, - scope_stack: &mut Vec, cmd: &mut CmdRegister| -> Result<(), String> { let mut values = Vec::new(); @@ -196,26 +164,15 @@ impl Forth { values.reverse(); let idx = idx_source % values.len(); let selected = values[idx].clone(); - select_and_run(selected, stack, outputs, scope_stack, cmd) + select_and_run(selected, stack, outputs, cmd) }; - // Emits one sound event from the current command register into the current scope. - let emit_once = |cmd: &CmdRegister, - scope_stack: &mut Vec| - -> Result, String> { + let emit_once = |cmd: &CmdRegister, outputs: &mut Vec| -> Result, String> { let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?; let sound = sound_val.as_str()?.to_string(); - let resolved_params: Vec<(String, String)> = params - .iter() - .map(|(k, v)| (k.clone(), v.to_param_string())) - .collect(); - let scope = scope_stack.last_mut().ok_or("scope stack underflow")?; - let slot_idx = scope.claim_slot(); - scope.pending.push(PendingEmission { - sound, - params: resolved_params, - slot_index: slot_idx, - }); + let resolved_params: Vec<(String, String)> = + params.iter().map(|(k, v)| (k.clone(), v.to_param_string())).collect(); + emit_output(&sound, &resolved_params, ctx.step_duration(), ctx.nudge_secs, outputs); Ok(Some(sound_val)) }; @@ -404,7 +361,7 @@ impl Forth { } Op::Emit => { - if let Some(sound_val) = emit_once(cmd, scope_stack)? { + if let Some(sound_val) = emit_once(cmd, outputs)? { if let Some(span) = sound_val.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { trace.selected_spans.push(span); @@ -413,17 +370,6 @@ impl Forth { } } - Op::Silence => { - let scope = scope_stack.last_mut().ok_or("scope stack underflow")?; - scope.claim_slot(); - } - - Op::Scale => { - let factor = stack.pop().ok_or("stack underflow")?.as_float()?; - let scope = scope_stack.last_mut().ok_or("scope stack underflow")?; - scope.weight = factor; - } - Op::Get => { let name = stack.pop().ok_or("stack underflow")?; let name = name.as_str()?; @@ -489,7 +435,7 @@ impl Forth { Op::Cycle => ctx.runs, _ => ctx.iter, } % count; - drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?; + drain_select_run(count, idx, stack, outputs, cmd)?; } Op::Choose => { @@ -498,7 +444,7 @@ impl Forth { return Err("choose count must be > 0".into()); } let idx = self.rng.lock().unwrap().gen_range(0..count); - drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?; + drain_select_run(count, idx, stack, outputs, cmd)?; } Op::ChanceExec | Op::ProbExec => { @@ -510,7 +456,7 @@ impl Forth { _ => threshold / 100.0, }; if val < limit { - run_quotation(quot, stack, outputs, scope_stack, cmd)?; + run_quotation(quot, stack, outputs, cmd)?; } } @@ -540,7 +486,7 @@ impl Forth { _ => !cond.is_truthy(), }; if should_run { - run_quotation(quot, stack, outputs, scope_stack, cmd)?; + run_quotation(quot, stack, outputs, cmd)?; } } @@ -553,7 +499,7 @@ impl Forth { } else { false_quot }; - run_quotation(quot, stack, outputs, scope_stack, cmd)?; + run_quotation(quot, stack, outputs, cmd)?; } Op::Pick => { @@ -580,7 +526,7 @@ impl Forth { quots.len() )); } - run_quotation(quots.swap_remove(idx), stack, outputs, scope_stack, cmd)?; + run_quotation(quots.swap_remove(idx), stack, outputs, cmd)?; } Op::Mtof => { @@ -690,7 +636,7 @@ impl Forth { Op::ListEndCycle => "empty cycle list", _ => "empty pattern cycle list", }; - drain_list_select_run(idx_source, err_msg, stack, outputs, scope_stack, cmd)?; + drain_list_select_run(idx_source, err_msg, stack, outputs, cmd)?; } Op::Adsr => { @@ -714,7 +660,7 @@ impl Forth { Op::Apply => { let quot = stack.pop().ok_or("stack underflow")?; - run_quotation(quot, stack, outputs, scope_stack, cmd)?; + run_quotation(quot, stack, outputs, cmd)?; } Op::Ramp => { @@ -744,36 +690,6 @@ impl Forth { stack.push(Value::Float(val, None)); } - Op::DivStart => { - let parent = scope_stack.last().ok_or("scope stack underflow")?; - let mut new_scope = ScopeContext::new(parent.start, parent.duration); - new_scope.weight = parent.weight; - scope_stack.push(new_scope); - } - - Op::DivEnd => { - if scope_stack.len() <= 1 { - return Err("unmatched ~ (no div/stack to close)".into()); - } - let child = scope_stack.pop().unwrap(); - - if child.stacked { - resolve_scope(&child, ctx.step_duration(), ctx.nudge_secs, outputs); - } else { - let parent = scope_stack.last_mut().ok_or("scope stack underflow")?; - let parent_slot = parent.claim_slot(); - resolve_scope_to_parent(&child, parent_slot, parent); - } - } - - Op::StackStart => { - let parent = scope_stack.last().ok_or("scope stack underflow")?; - let mut new_scope = ScopeContext::new(parent.start, parent.duration); - new_scope.weight = parent.weight; - new_scope.stacked = true; - scope_stack.push(new_scope); - } - Op::ClearCmd => { cmd.clear(); } @@ -784,7 +700,7 @@ impl Forth { return Err("emit count must be >= 0".into()); } for _ in 0..n { - emit_once(cmd, scope_stack)?; + emit_once(cmd, outputs)?; } } } @@ -795,106 +711,6 @@ impl Forth { } } -fn resolve_scope( - scope: &ScopeContext, - step_duration: f64, - nudge_secs: f64, - outputs: &mut Vec, -) { - let slot_dur = if scope.slot_count == 0 { - scope.duration * scope.weight - } else { - scope.duration * scope.weight / scope.slot_count as f64 - }; - - struct Emission { - delta: f64, - sound: String, - params: Vec<(String, String)>, - dur: f64, - } - - let mut emissions: Vec = Vec::new(); - - for em in &scope.pending { - let delta = scope.start + slot_dur * em.slot_index as f64; - emissions.push(Emission { - delta, - sound: em.sound.clone(), - params: em.params.clone(), - dur: slot_dur, - }); - } - - for em in &scope.resolved { - let slot_start = slot_dur * em.parent_slot as f64; - let delta = scope.start + slot_start + em.offset_in_slot * slot_dur; - let dur = em.dur * slot_dur; - emissions.push(Emission { - delta, - sound: em.sound.clone(), - params: em.params.clone(), - dur, - }); - } - - emissions.sort_by(|a, b| { - a.delta - .partial_cmp(&b.delta) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - for em in emissions { - emit_output( - &em.sound, - &em.params, - em.delta, - em.dur, - step_duration, - nudge_secs, - outputs, - ); - } -} - -fn resolve_scope_to_parent(child: &ScopeContext, parent_slot: usize, parent: &mut ScopeContext) { - if child.slot_count == 0 && child.pending.is_empty() && child.resolved.is_empty() { - return; - } - - let child_slot_count = child.slot_count.max(1); - - // Store offsets and durations as fractions of the parent slot - // Child's internal structure: slot_count slots, each slot is 1/slot_count of the whole - for em in &child.pending { - let offset_fraction = em.slot_index as f64 / child_slot_count as f64; - let dur_fraction = 1.0 / child_slot_count as f64; - parent.resolved.push(ResolvedEmission { - sound: em.sound.clone(), - params: em.params.clone(), - parent_slot, - offset_in_slot: offset_fraction, - dur: dur_fraction, - }); - } - - // Child's resolved emissions already have fractional offsets/durs relative to their slots - // We need to compose them: em belongs to child slot em.parent_slot, which is a fraction of child - for em in &child.resolved { - let child_slot_offset = em.parent_slot as f64 / child_slot_count as f64; - let child_slot_size = 1.0 / child_slot_count as f64; - let offset_fraction = child_slot_offset + em.offset_in_slot * child_slot_size; - let dur_fraction = em.dur * child_slot_size; - parent.resolved.push(ResolvedEmission { - sound: em.sound.clone(), - params: em.params.clone(), - parent_slot, - offset_in_slot: offset_fraction, - dur: dur_fraction, - }); - } -} - const TEMPO_SCALED_PARAMS: &[&str] = &[ "attack", "decay", @@ -924,26 +740,23 @@ const TEMPO_SCALED_PARAMS: &[&str] = &[ fn emit_output( sound: &str, params: &[(String, String)], - delta: f64, - dur: f64, step_duration: f64, nudge_secs: f64, outputs: &mut Vec, ) { - let nudged_delta = delta + nudge_secs; let mut pairs = vec![("sound".into(), sound.to_string())]; pairs.extend(params.iter().cloned()); - if nudged_delta > 0.0 { - pairs.push(("delta".into(), nudged_delta.to_string())); + if nudge_secs > 0.0 { + pairs.push(("delta".into(), nudge_secs.to_string())); } if !pairs.iter().any(|(k, _)| k == "dur") { - pairs.push(("dur".into(), dur.to_string())); + pairs.push(("dur".into(), step_duration.to_string())); } if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); - pairs[idx].1 = (ratio * dur).to_string(); + pairs[idx].1 = (ratio * step_duration).to_string(); } else { - pairs.push(("delaytime".into(), dur.to_string())); + pairs.push(("delaytime".into(), step_duration.to_string())); } for pair in &mut pairs { if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) { diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index 6392109..2e2bdd1 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -392,19 +392,10 @@ pub const WORDS: &[Word] = &[ aliases: &[], category: "Sound", stack: "(--)", - desc: "Emit current sound, claim one time slot", + desc: "Emit current sound", example: "\"kick\" s . . . .", compile: Simple, }, - Word { - name: "_", - aliases: &[], - category: "Sound", - stack: "(--)", - desc: "Silence, claim one time slot", - example: "\"kick\" s . _ . _", - compile: Simple, - }, Word { name: ".!", aliases: &[], @@ -414,33 +405,6 @@ pub const WORDS: &[Word] = &[ example: "\"kick\" s 4 .!", compile: Simple, }, - Word { - name: "div", - aliases: &[], - category: "Time", - stack: "(--)", - desc: "Start a time subdivision scope (div claims a slot in parent)", - example: "div \"kick\" s . \"hat\" s . ~", - compile: Simple, - }, - Word { - name: "stack", - aliases: &[], - category: "Time", - stack: "(--)", - desc: "Start a stacked subdivision scope (sounds stack/superpose)", - example: "stack \"kick\" s . \"hat\" s . ~", - compile: Simple, - }, - Word { - name: "~", - aliases: &[], - category: "Time", - stack: "(--)", - desc: "End a time subdivision scope (div or stack)", - example: "div \"kick\" s . ~", - compile: Simple, - }, // Variables (prefix syntax: @name to fetch, !name to store) Word { name: "@", @@ -713,7 +677,7 @@ pub const WORDS: &[Word] = &[ category: "Context", stack: "(-- bool)", desc: "True when fill is on (f key)", - example: "{ 4 div each } fill ?", + example: "\"snare\" s . fill ?", compile: Context("fill"), }, // Music @@ -799,16 +763,6 @@ pub const WORDS: &[Word] = &[ example: "0.25 perlin", compile: Simple, }, - // Time - Word { - name: "scale!", - aliases: &[], - category: "Time", - stack: "(factor --)", - desc: "Set weight of current time scope", - example: "2 scale!", - compile: Simple, - }, Word { name: "loop", aliases: &[], @@ -2105,8 +2059,6 @@ pub(super) fn simple_op(name: &str) -> Option { "ftom" => Op::Ftom, "?" => Op::When, "!?" => Op::Unless, - "_" => Op::Silence, - "scale!" => Op::Scale, "tempo!" => Op::SetTempo, "speed!" => Op::SetSpeed, "[" => Op::ListStart, @@ -2123,9 +2075,6 @@ pub(super) fn simple_op(name: &str) -> Option { "chain" => Op::Chain, "loop" => Op::Loop, "oct" => Op::Oct, - "div" => Op::DivStart, - "stack" => Op::StackStart, - "~" => Op::DivEnd, ".!" => Op::EmitN, "clear" => Op::ClearCmd, _ => return None, diff --git a/tests/forth/temporal.rs b/tests/forth/temporal.rs index 644cddf..5616341 100644 --- a/tests/forth/temporal.rs +++ b/tests/forth/temporal.rs @@ -48,9 +48,6 @@ fn approx_eq(a: f64, b: f64) -> bool { (a - b).abs() < EPSILON } -// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s -// Root duration = 4 * stepdur = 0.5s - #[test] fn stepdur_baseline() { let f = run("stepdur"); @@ -65,72 +62,14 @@ fn single_emit() { } #[test] -fn implicit_subdivision_2() { - let outputs = expect_outputs(r#""kick" s . ."#, 2); - let deltas = get_deltas(&outputs); - let step = 0.5 / 2.0; - assert!(approx_eq(deltas[0], 0.0), "first slot at 0"); - assert!(approx_eq(deltas[1], step), "second slot at {}, got {}", step, deltas[1]); -} - -#[test] -fn implicit_subdivision_4() { +fn multiple_emits_all_at_zero() { let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let deltas = get_deltas(&outputs); - let step = 0.5 / 4.0; for (i, delta) in deltas.iter().enumerate() { - let expected = step * i as f64; - assert!( - approx_eq(*delta, expected), - "slot {}: expected {}, got {}", - i, expected, delta - ); + assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta); } } -#[test] -fn implicit_subdivision_3() { - let outputs = expect_outputs(r#""kick" s . . ."#, 3); - let deltas = get_deltas(&outputs); - let step = 0.5 / 3.0; - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); - assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]); -} - -#[test] -fn silence_creates_gap() { - let outputs = expect_outputs(r#""kick" s . _ ."#, 2); - let deltas = get_deltas(&outputs); - let step = 0.5 / 3.0; - assert!(approx_eq(deltas[0], 0.0), "first at 0"); - assert!( - approx_eq(deltas[1], 2.0 * step), - "third slot (after silence) at {}, got {}", - 2.0 * step, - deltas[1] - ); -} - -#[test] -fn silence_at_start() { - let outputs = expect_outputs(r#""kick" s _ ."#, 1); - let deltas = get_deltas(&outputs); - let step = 0.5 / 2.0; - assert!( - approx_eq(deltas[0], step), - "emit after silence at {}, got {}", - step, - deltas[0] - ); -} - -#[test] -fn silence_only() { - let outputs = expect_outputs(r#""kick" s _"#, 0); - assert!(outputs.is_empty(), "silence only should produce no output"); -} - #[test] fn sound_persists() { let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4); @@ -149,41 +88,10 @@ fn alternating_sounds() { } #[test] -fn dur_matches_slot_duration() { - let outputs = expect_outputs(r#""kick" s . . . ."#, 4); +fn dur_is_step_duration() { + let outputs = expect_outputs(r#""kick" s ."#, 1); let durs = get_durs(&outputs); - let expected_dur = 0.5 / 4.0; - for (i, dur) in durs.iter().enumerate() { - assert!( - approx_eq(*dur, expected_dur), - "slot {} dur: expected {}, got {}", - i, expected_dur, dur - ); - } -} - -#[test] -fn tempo_affects_subdivision() { - let ctx = ctx_with(|c| c.tempo = 60.0); - let f = forth(); - let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap(); - let deltas = get_deltas(&outputs); - // At 60 BPM: stepdur = 0.25, root dur = 1.0 - let step = 1.0 / 2.0; - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); -} - -#[test] -fn speed_affects_subdivision() { - let ctx = ctx_with(|c| c.speed = 2.0); - let f = forth(); - let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap(); - let deltas = get_deltas(&outputs); - // At speed 2.0: stepdur = 0.0625, root dur = 0.25 - let step = 0.25 / 2.0; - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); + assert!(approx_eq(durs[0], 0.125), "dur should be step_duration (0.125), got {}", durs[0]); } #[test] @@ -227,143 +135,6 @@ fn cycle_with_sounds() { } } -#[test] -fn dot_alias_for_emit() { - let outputs = expect_outputs(r#""kick" s . . . ."#, 4); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]); -} - -#[test] -fn dot_with_silence() { - let outputs = expect_outputs(r#""kick" s . _ . _"#, 2); - let deltas = get_deltas(&outputs); - let step = 0.5 / 4.0; - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 2.0 * step)); -} - -#[test] -fn div_basic_subdivision() { - let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2); - let deltas = get_deltas(&outputs); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "hat"]); - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.25), "second should be at 0.25, got {}", deltas[1]); -} - -#[test] -fn div_sequential() { - // Two consecutive divs each claim a slot in root, so they're sequential - let outputs = expect_outputs(r#"div "kick" s . ~ div "hat" s . ~"#, 2); - let deltas = get_deltas(&outputs); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "hat"]); - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.25), "second div at slot 1, got {}", deltas[1]); -} - -#[test] -fn div_with_root_emit() { - // kick claims slot 0 at root, div claims slot 1 at root - let outputs = expect_outputs(r#""kick" s . div "hat" s . ~"#, 2); - let deltas = get_deltas(&outputs); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "hat"]); - assert!(approx_eq(deltas[0], 0.0), "kick at slot 0"); - assert!(approx_eq(deltas[1], 0.25), "hat at slot 1, got {}", deltas[1]); -} - -#[test] -fn div_nested() { - // kick claims slot 0 in outer div, inner div claims slot 1 - // Inner div's 2 hats subdivide its slot (0.25 duration) into 2 sub-slots - let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . ~ ~"#, 3); - let sounds = get_sounds(&outputs); - let deltas = get_deltas(&outputs); - // Output order: kick (slot 0), then hats (slot 1 subdivided) - assert_eq!(sounds[0], "kick"); - assert_eq!(sounds[1], "hat"); - assert_eq!(sounds[2], "hat"); - // Outer div has 2 slots of 0.25 each - // kick at slot 0 -> delta 0 - // inner div at slot 1 -> starts at 0.25, subdivided into 2 -> hats at 0.25 and 0.375 - assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]); - assert!(approx_eq(deltas[1], 0.25), "first hat at 0.25, got {}", deltas[1]); - assert!(approx_eq(deltas[2], 0.375), "second hat at 0.375, got {}", deltas[2]); -} - -#[test] -fn div_with_silence() { - let outputs = expect_outputs(r#"div "kick" s . _ ~"#, 1); - let deltas = get_deltas(&outputs); - assert!(approx_eq(deltas[0], 0.0)); -} - -#[test] -fn unmatched_scope_terminator_error() { - let f = forth(); - let result = f.evaluate(r#""kick" s . ~"#, &default_ctx()); - assert!(result.is_err(), "unmatched ~ should error"); -} - -#[test] -fn stack_superposes_sounds() { - let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2); - let deltas = get_deltas(&outputs); - let sounds = get_sounds(&outputs); - assert_eq!(sounds.len(), 2); - // Both at delta 0 (stacked/superposed) - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.0)); -} - -#[test] -fn stack_with_multiple_emits() { - let outputs = expect_outputs(r#"stack "kick" s . . . . ~"#, 4); - let deltas = get_deltas(&outputs); - // All 4 kicks at delta 0 - for (i, delta) in deltas.iter().enumerate() { - assert!(approx_eq(*delta, 0.0), "emit {} should be at 0, got {}", i, delta); - } -} - -#[test] -fn stack_inside_div() { - // div subdivides, stack inside superposes - // stack doesn't claim a slot in parent div, so snare is also at 0 - let outputs = expect_outputs(r#"div stack "kick" s . "hat" s . ~ "snare" s . ~"#, 3); - let deltas = get_deltas(&outputs); - let sounds = get_sounds(&outputs); - // stack resolves first (kick, hat at 0), then div resolves (snare at 0) - // since stack doesn't consume a slot in the parent div - assert_eq!(sounds[0], "kick"); - assert_eq!(sounds[1], "hat"); - assert_eq!(sounds[2], "snare"); - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.0)); - assert!(approx_eq(deltas[2], 0.0), "snare at 0, got {}", deltas[2]); -} - -#[test] -fn div_nested_with_sibling() { - // Inner div claims slot 0, snare claims slot 1 - // Inner div's kick/hat subdivide slot 0 - let outputs = expect_outputs(r#"div div "kick" s . "hat" s . ~ "snare" s . ~"#, 3); - let deltas = get_deltas(&outputs); - let sounds = get_sounds(&outputs); - // Outer div has 2 slots of 0.25 each - // Inner div at slot 0: kick at 0, hat at 0.125 - // snare at slot 1: delta 0.25 - assert_eq!(sounds[0], "kick"); - assert_eq!(sounds[1], "hat"); - assert_eq!(sounds[2], "snare"); - assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]); - assert!(approx_eq(deltas[1], 0.125), "hat at 0.125, got {}", deltas[1]); - assert!(approx_eq(deltas[2], 0.25), "snare at 0.25, got {}", deltas[2]); -} - #[test] fn emit_n_basic() { let outputs = expect_outputs(r#""kick" s 4 .!"#, 4); @@ -383,4 +154,3 @@ fn emit_n_negative_error() { let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx()); assert!(result.is_err()); } -