Cleaning old temporal model

This commit is contained in:
2026-01-29 01:28:57 +01:00
parent 48f5920fed
commit 8efafffaff
5 changed files with 65 additions and 589 deletions

View File

@@ -49,7 +49,6 @@ pub enum Op {
NewCmd, NewCmd,
SetParam(String), SetParam(String),
Emit, Emit,
Silence,
Get, Get,
Set, Set,
GetContext(String), GetContext(String),
@@ -67,7 +66,6 @@ pub enum Op {
ListEndCycle, ListEndCycle,
PCycle, PCycle,
ListEndPCycle, ListEndPCycle,
Scale,
SetTempo, SetTempo,
Every, Every,
Quotation(Vec<Op>, Option<SourceSpan>), Quotation(Vec<Op>, Option<SourceSpan>),
@@ -84,9 +82,6 @@ pub enum Op {
Loop, Loop,
Degree(&'static [i64]), Degree(&'static [i64]),
Oct, Oct,
DivStart,
DivEnd,
StackStart,
EmitN, EmitN,
ClearCmd, ClearCmd,
SetSpeed, SetSpeed,

View File

@@ -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<PendingEmission>,
pub resolved: Vec<ResolvedEmission>,
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
}
}
}

View File

@@ -4,8 +4,7 @@ use rand::{Rng as RngTrait, SeedableRng};
use super::compiler::compile_script; use super::compiler::compile_script;
use super::ops::Op; use super::ops::Op;
use super::types::{ use super::types::{
CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext, CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
Stack, StepContext, Value, Variables,
}; };
pub struct Forth { pub struct Forth {
@@ -70,24 +69,9 @@ impl Forth {
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let mut stack = self.stack.lock().unwrap(); let mut stack = self.stack.lock().unwrap();
let mut outputs: Vec<String> = Vec::new(); let mut outputs: Vec<String> = Vec::new();
let root_duration = ctx.step_duration() * 4.0;
let mut scope_stack: Vec<ScopeContext> = vec![ScopeContext::new(0.0, root_duration)];
let mut cmd = CmdRegister::default(); let mut cmd = CmdRegister::default();
self.execute_ops( self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
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);
}
Ok(outputs) Ok(outputs)
} }
@@ -100,70 +84,56 @@ impl Forth {
ctx: &StepContext, ctx: &StepContext,
stack: &mut Vec<Value>, stack: &mut Vec<Value>,
outputs: &mut Vec<String>, outputs: &mut Vec<String>,
scope_stack: &mut Vec<ScopeContext>,
cmd: &mut CmdRegister, cmd: &mut CmdRegister,
trace: Option<&mut ExecutionTrace>, trace: Option<&mut ExecutionTrace>,
) -> Result<(), String> { ) -> Result<(), String> {
let mut pc = 0; let mut pc = 0;
let trace_cell = std::cell::RefCell::new(trace); let trace_cell = std::cell::RefCell::new(trace);
// Executes a quotation value, handling trace recording and recursive dispatch. let run_quotation =
let run_quotation = |quot: Value, |quot: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, cmd: &mut CmdRegister| -> Result<(), String> {
stack: &mut Vec<Value>, match quot {
outputs: &mut Vec<String>, Value::Quotation(quot_ops, body_span) => {
scope_stack: &mut Vec<ScopeContext>, if let Some(span) = body_span {
cmd: &mut CmdRegister| if let Some(trace) = trace_cell.borrow_mut().as_mut() {
-> Result<(), String> { trace.executed_spans.push(span);
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(
&quot_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(); _ => Err("expected quotation".into()),
self.execute_ops( }
&quot_ops, };
ctx,
stack, let select_and_run =
outputs, |selected: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, cmd: &mut CmdRegister| -> Result<(), String> {
scope_stack, if let Some(span) = selected.span() {
cmd, if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace_opt.as_deref_mut(), trace.selected_spans.push(span);
)?; }
*trace_cell.borrow_mut() = trace_opt; }
if matches!(selected, Value::Quotation(..)) {
run_quotation(selected, stack, outputs, cmd)
} else {
stack.push(selected);
Ok(()) 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<Value>,
outputs: &mut Vec<String>,
scope_stack: &mut Vec<ScopeContext>,
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, let drain_select_run = |count: usize,
idx: usize, idx: usize,
stack: &mut Vec<Value>, stack: &mut Vec<Value>,
outputs: &mut Vec<String>, outputs: &mut Vec<String>,
scope_stack: &mut Vec<ScopeContext>,
cmd: &mut CmdRegister| cmd: &mut CmdRegister|
-> Result<(), String> { -> Result<(), String> {
if stack.len() < count { if stack.len() < count {
@@ -172,15 +142,13 @@ impl Forth {
let start = stack.len() - count; let start = stack.len() - count;
let values: Vec<Value> = stack.drain(start..).collect(); let values: Vec<Value> = stack.drain(start..).collect();
let selected = values[idx].clone(); 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, let drain_list_select_run = |idx_source: usize,
err_msg: &str, err_msg: &str,
stack: &mut Vec<Value>, stack: &mut Vec<Value>,
outputs: &mut Vec<String>, outputs: &mut Vec<String>,
scope_stack: &mut Vec<ScopeContext>,
cmd: &mut CmdRegister| cmd: &mut CmdRegister|
-> Result<(), String> { -> Result<(), String> {
let mut values = Vec::new(); let mut values = Vec::new();
@@ -196,26 +164,15 @@ impl Forth {
values.reverse(); values.reverse();
let idx = idx_source % values.len(); let idx = idx_source % values.len();
let selected = values[idx].clone(); 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, outputs: &mut Vec<String>| -> Result<Option<Value>, String> {
let emit_once = |cmd: &CmdRegister,
scope_stack: &mut Vec<ScopeContext>|
-> Result<Option<Value>, String> {
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?; let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
let sound = sound_val.as_str()?.to_string(); let sound = sound_val.as_str()?.to_string();
let resolved_params: Vec<(String, String)> = params let resolved_params: Vec<(String, String)> =
.iter() params.iter().map(|(k, v)| (k.clone(), v.to_param_string())).collect();
.map(|(k, v)| (k.clone(), v.to_param_string())) emit_output(&sound, &resolved_params, ctx.step_duration(), ctx.nudge_secs, outputs);
.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,
});
Ok(Some(sound_val)) Ok(Some(sound_val))
}; };
@@ -404,7 +361,7 @@ impl Forth {
} }
Op::Emit => { 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(span) = sound_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() { if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span); 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 => { Op::Get => {
let name = stack.pop().ok_or("stack underflow")?; let name = stack.pop().ok_or("stack underflow")?;
let name = name.as_str()?; let name = name.as_str()?;
@@ -489,7 +435,7 @@ impl Forth {
Op::Cycle => ctx.runs, Op::Cycle => ctx.runs,
_ => ctx.iter, _ => ctx.iter,
} % count; } % count;
drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?; drain_select_run(count, idx, stack, outputs, cmd)?;
} }
Op::Choose => { Op::Choose => {
@@ -498,7 +444,7 @@ impl Forth {
return Err("choose count must be > 0".into()); return Err("choose count must be > 0".into());
} }
let idx = self.rng.lock().unwrap().gen_range(0..count); 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 => { Op::ChanceExec | Op::ProbExec => {
@@ -510,7 +456,7 @@ impl Forth {
_ => threshold / 100.0, _ => threshold / 100.0,
}; };
if val < limit { 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(), _ => !cond.is_truthy(),
}; };
if should_run { if should_run {
run_quotation(quot, stack, outputs, scope_stack, cmd)?; run_quotation(quot, stack, outputs, cmd)?;
} }
} }
@@ -553,7 +499,7 @@ impl Forth {
} else { } else {
false_quot false_quot
}; };
run_quotation(quot, stack, outputs, scope_stack, cmd)?; run_quotation(quot, stack, outputs, cmd)?;
} }
Op::Pick => { Op::Pick => {
@@ -580,7 +526,7 @@ impl Forth {
quots.len() quots.len()
)); ));
} }
run_quotation(quots.swap_remove(idx), stack, outputs, scope_stack, cmd)?; run_quotation(quots.swap_remove(idx), stack, outputs, cmd)?;
} }
Op::Mtof => { Op::Mtof => {
@@ -690,7 +636,7 @@ impl Forth {
Op::ListEndCycle => "empty cycle list", Op::ListEndCycle => "empty cycle list",
_ => "empty pattern 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 => { Op::Adsr => {
@@ -714,7 +660,7 @@ impl Forth {
Op::Apply => { Op::Apply => {
let quot = stack.pop().ok_or("stack underflow")?; let quot = stack.pop().ok_or("stack underflow")?;
run_quotation(quot, stack, outputs, scope_stack, cmd)?; run_quotation(quot, stack, outputs, cmd)?;
} }
Op::Ramp => { Op::Ramp => {
@@ -744,36 +690,6 @@ impl Forth {
stack.push(Value::Float(val, None)); 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 => { Op::ClearCmd => {
cmd.clear(); cmd.clear();
} }
@@ -784,7 +700,7 @@ impl Forth {
return Err("emit count must be >= 0".into()); return Err("emit count must be >= 0".into());
} }
for _ in 0..n { 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<String>,
) {
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<Emission> = 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] = &[ const TEMPO_SCALED_PARAMS: &[&str] = &[
"attack", "attack",
"decay", "decay",
@@ -924,26 +740,23 @@ const TEMPO_SCALED_PARAMS: &[&str] = &[
fn emit_output( fn emit_output(
sound: &str, sound: &str,
params: &[(String, String)], params: &[(String, String)],
delta: f64,
dur: f64,
step_duration: f64, step_duration: f64,
nudge_secs: f64, nudge_secs: f64,
outputs: &mut Vec<String>, outputs: &mut Vec<String>,
) { ) {
let nudged_delta = delta + nudge_secs;
let mut pairs = vec![("sound".into(), sound.to_string())]; let mut pairs = vec![("sound".into(), sound.to_string())];
pairs.extend(params.iter().cloned()); pairs.extend(params.iter().cloned());
if nudged_delta > 0.0 { if nudge_secs > 0.0 {
pairs.push(("delta".into(), nudged_delta.to_string())); pairs.push(("delta".into(), nudge_secs.to_string()));
} }
if !pairs.iter().any(|(k, _)| k == "dur") { 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") { if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); 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 { } else {
pairs.push(("delaytime".into(), dur.to_string())); pairs.push(("delaytime".into(), step_duration.to_string()));
} }
for pair in &mut pairs { for pair in &mut pairs {
if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) { if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) {

View File

@@ -392,19 +392,10 @@ pub const WORDS: &[Word] = &[
aliases: &[], aliases: &[],
category: "Sound", category: "Sound",
stack: "(--)", stack: "(--)",
desc: "Emit current sound, claim one time slot", desc: "Emit current sound",
example: "\"kick\" s . . . .", example: "\"kick\" s . . . .",
compile: Simple, compile: Simple,
}, },
Word {
name: "_",
aliases: &[],
category: "Sound",
stack: "(--)",
desc: "Silence, claim one time slot",
example: "\"kick\" s . _ . _",
compile: Simple,
},
Word { Word {
name: ".!", name: ".!",
aliases: &[], aliases: &[],
@@ -414,33 +405,6 @@ pub const WORDS: &[Word] = &[
example: "\"kick\" s 4 .!", example: "\"kick\" s 4 .!",
compile: Simple, 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) // Variables (prefix syntax: @name to fetch, !name to store)
Word { Word {
name: "@<var>", name: "@<var>",
@@ -713,7 +677,7 @@ pub const WORDS: &[Word] = &[
category: "Context", category: "Context",
stack: "(-- bool)", stack: "(-- bool)",
desc: "True when fill is on (f key)", desc: "True when fill is on (f key)",
example: "{ 4 div each } fill ?", example: "\"snare\" s . fill ?",
compile: Context("fill"), compile: Context("fill"),
}, },
// Music // Music
@@ -799,16 +763,6 @@ pub const WORDS: &[Word] = &[
example: "0.25 perlin", example: "0.25 perlin",
compile: Simple, compile: Simple,
}, },
// Time
Word {
name: "scale!",
aliases: &[],
category: "Time",
stack: "(factor --)",
desc: "Set weight of current time scope",
example: "2 scale!",
compile: Simple,
},
Word { Word {
name: "loop", name: "loop",
aliases: &[], aliases: &[],
@@ -2105,8 +2059,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"ftom" => Op::Ftom, "ftom" => Op::Ftom,
"?" => Op::When, "?" => Op::When,
"!?" => Op::Unless, "!?" => Op::Unless,
"_" => Op::Silence,
"scale!" => Op::Scale,
"tempo!" => Op::SetTempo, "tempo!" => Op::SetTempo,
"speed!" => Op::SetSpeed, "speed!" => Op::SetSpeed,
"[" => Op::ListStart, "[" => Op::ListStart,
@@ -2123,9 +2075,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"chain" => Op::Chain, "chain" => Op::Chain,
"loop" => Op::Loop, "loop" => Op::Loop,
"oct" => Op::Oct, "oct" => Op::Oct,
"div" => Op::DivStart,
"stack" => Op::StackStart,
"~" => Op::DivEnd,
".!" => Op::EmitN, ".!" => Op::EmitN,
"clear" => Op::ClearCmd, "clear" => Op::ClearCmd,
_ => return None, _ => return None,

View File

@@ -48,9 +48,6 @@ fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPSILON (a - b).abs() < EPSILON
} }
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
// Root duration = 4 * stepdur = 0.5s
#[test] #[test]
fn stepdur_baseline() { fn stepdur_baseline() {
let f = run("stepdur"); let f = run("stepdur");
@@ -65,72 +62,14 @@ fn single_emit() {
} }
#[test] #[test]
fn implicit_subdivision_2() { fn multiple_emits_all_at_zero() {
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() {
let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
let deltas = get_deltas(&outputs); let deltas = get_deltas(&outputs);
let step = 0.5 / 4.0;
for (i, delta) in deltas.iter().enumerate() { for (i, delta) in deltas.iter().enumerate() {
let expected = step * i as f64; assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
assert!(
approx_eq(*delta, expected),
"slot {}: expected {}, got {}",
i, expected, 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] #[test]
fn sound_persists() { fn sound_persists() {
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4); let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
@@ -149,41 +88,10 @@ fn alternating_sounds() {
} }
#[test] #[test]
fn dur_matches_slot_duration() { fn dur_is_step_duration() {
let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let outputs = expect_outputs(r#""kick" s ."#, 1);
let durs = get_durs(&outputs); let durs = get_durs(&outputs);
let expected_dur = 0.5 / 4.0; assert!(approx_eq(durs[0], 0.125), "dur should be step_duration (0.125), got {}", durs[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]);
} }
#[test] #[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] #[test]
fn emit_n_basic() { fn emit_n_basic() {
let outputs = expect_outputs(r#""kick" s 4 .!"#, 4); 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()); let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx());
assert!(result.is_err()); assert!(result.is_err());
} }