big commit

This commit is contained in:
2026-01-27 01:04:08 +01:00
parent 66933433d1
commit 5456c9414a
15 changed files with 821 additions and 222 deletions

View File

@@ -4,8 +4,8 @@ use rand::{Rng as RngTrait, SeedableRng};
use super::compiler::compile_script;
use super::ops::Op;
use super::types::{
CmdRegister, Dictionary, ExecutionTrace, PendingEmission, Rng, ScopeContext, SourceSpan, Stack,
StepContext, Value, Variables,
CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext,
SourceSpan, Stack, StepContext, Value, Variables,
};
pub type EmissionCounter = std::sync::Arc<std::sync::Mutex<usize>>;
@@ -220,6 +220,23 @@ impl Forth {
}
Op::Min => binary_op(stack, |a, b| a.min(b))?,
Op::Max => binary_op(stack, |a, b| a.max(b))?,
Op::Pow => binary_op(stack, |a, b| a.powf(b))?,
Op::Sqrt => {
let v = stack.pop().ok_or("stack underflow")?;
stack.push(lift_unary(v, |x| x.sqrt())?);
}
Op::Sin => {
let v = stack.pop().ok_or("stack underflow")?;
stack.push(lift_unary(v, |x| x.sin())?);
}
Op::Cos => {
let v = stack.pop().ok_or("stack underflow")?;
stack.push(lift_unary(v, |x| x.cos())?);
}
Op::Log => {
let v = stack.pop().ok_or("stack underflow")?;
stack.push(lift_unary(v, |x| x.ln())?);
}
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
Op::Ne => cmp_op(stack, |a, b| (a - b).abs() >= f64::EPSILON)?,
@@ -242,6 +259,21 @@ impl Forth {
let v = stack.pop().ok_or("stack underflow")?.is_truthy();
stack.push(Value::Int(if v { 0 } else { 1 }, None));
}
Op::Xor => {
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
stack.push(Value::Int(if a ^ b { 1 } else { 0 }, None));
}
Op::Nand => {
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
stack.push(Value::Int(if !(a && b) { 1 } else { 0 }, None));
}
Op::Nor => {
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
stack.push(Value::Int(if !(a || b) { 1 } else { 0 }, None));
}
Op::BranchIfZero(offset, then_span, else_span) => {
let v = stack.pop().ok_or("stack underflow")?;
@@ -630,6 +662,80 @@ impl Forth {
}
}
Op::IfElse => {
let cond = stack.pop().ok_or("stack underflow")?;
let false_quot = stack.pop().ok_or("stack underflow")?;
let true_quot = stack.pop().ok_or("stack underflow")?;
let quot = if cond.is_truthy() { true_quot } else { false_quot };
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,
scope_stack,
cmd,
trace_opt.as_deref_mut(),
emission_count,
)?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
}
Op::Pick => {
let idx = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
let mut quots: Vec<Value> = Vec::new();
while let Some(val) = stack.pop() {
match &val {
Value::Quotation(_, _) => quots.push(val),
_ => {
stack.push(val);
break;
}
}
}
quots.reverse();
if idx >= quots.len() {
return Err(format!(
"pick index {} out of range (have {} quotations)",
idx,
quots.len()
)
.into());
}
match &quots[idx] {
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,
scope_stack,
cmd,
trace_opt.as_deref_mut(),
emission_count,
)?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => unreachable!(),
}
}
Op::Mtof => {
let note = stack.pop().ok_or("stack underflow")?.as_float()?;
let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0);
@@ -851,13 +957,20 @@ impl Forth {
let val = phase.powf(curve);
stack.push(Value::Float(val, None));
}
Op::Tri => {
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
let phase = (freq * ctx.beat).fract();
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
let val = 1.0 - (2.0 * phase - 1.0).abs();
stack.push(Value::Float(val, None));
}
Op::Range => {
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
let val = stack.pop().ok_or("stack underflow")?.as_float()?;
stack.push(Value::Float(min + val * (max - min), None));
}
Op::Noise => {
Op::Perlin => {
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
let val = perlin_noise_1d(freq * ctx.beat);
stack.push(Value::Float(val, None));
@@ -887,10 +1000,19 @@ impl Forth {
Op::DivEnd => {
if scope_stack.len() <= 1 {
return Err("unmatched end".into());
return Err("unmatched ~ (no div/stack to close)".into());
}
let child = scope_stack.pop().unwrap();
if child.stacked {
// Stack doesn't claim a slot - resolve directly to outputs
resolve_scope(&child, outputs);
} else {
// Div claims a slot in the parent
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);
}
let scope = scope_stack.pop().unwrap();
resolve_scope(&scope, outputs);
}
Op::StackStart => {
@@ -966,28 +1088,89 @@ fn resolve_value_with_span(val: &Value, emission_count: usize) -> (Value, Option
}
fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec<String>) {
if scope.slot_count == 0 || scope.pending.is_empty() {
return;
}
let slot_dur = scope.duration * scope.weight / scope.slot_count as f64;
let slot_dur = if scope.slot_count == 0 {
scope.duration * scope.weight
} else {
scope.duration * scope.weight / scope.slot_count as f64
};
// Collect all emissions with their deltas for sorting
let mut emissions: Vec<(f64, String, Vec<(String, String)>, f64)> = Vec::new();
for em in &scope.pending {
let delta = scope.start + slot_dur * em.slot_index as f64;
let mut pairs = vec![("sound".into(), em.sound.clone())];
pairs.extend(em.params.iter().cloned());
if delta > 0.0 {
pairs.push(("delta".into(), delta.to_string()));
}
if !pairs.iter().any(|(k, _)| k == "dur") {
pairs.push(("dur".into(), slot_dur.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 * slot_dur).to_string();
} else {
pairs.push(("delaytime".into(), slot_dur.to_string()));
}
outputs.push(format_cmd(&pairs));
emissions.push((delta, em.sound.clone(), em.params.clone(), 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((delta, em.sound.clone(), em.params.clone(), dur));
}
// Sort by delta to ensure temporal ordering
emissions.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
for (delta, sound, params, dur) in emissions {
emit_output(&sound, &params, delta, dur, 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,
});
}
}
fn emit_output(sound: &str, params: &[(String, String)], delta: f64, dur: f64, outputs: &mut Vec<String>) {
let mut pairs = vec![("sound".into(), sound.to_string())];
pairs.extend(params.iter().cloned());
if delta > 0.0 {
pairs.push(("delta".into(), delta.to_string()));
}
if !pairs.iter().any(|(k, _)| k == "dur") {
pairs.push(("dur".into(), dur.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();
} else {
pairs.push(("delaytime".into(), dur.to_string()));
}
outputs.push(format_cmd(&pairs));
}
fn perlin_grad(hash_input: i64) -> f64 {
@@ -996,11 +1179,8 @@ fn perlin_grad(hash_input: i64) -> f64 {
h ^= h >> 33;
h = h.wrapping_mul(0xff51afd7ed558ccd);
h ^= h >> 33;
if h & 1 == 0 {
1.0
} else {
-1.0
}
// Convert to float in [-1, 1] range for varied gradients
(h as i64 as f64) / (i64::MAX as f64)
}
fn perlin_noise_1d(x: f64) -> f64 {