big commit
This commit is contained in:
@@ -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(
|
||||
"_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 "s[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, ¶ms, 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 {
|
||||
|
||||
Reference in New Issue
Block a user