849 lines
33 KiB
Rust
849 lines
33 KiB
Rust
use rand::rngs::StdRng;
|
|
use rand::{Rng as RngTrait, SeedableRng};
|
|
|
|
use super::compiler::compile_script;
|
|
use super::ops::Op;
|
|
use super::types::{
|
|
CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
|
|
};
|
|
|
|
pub struct Forth {
|
|
stack: Stack,
|
|
vars: Variables,
|
|
dict: Dictionary,
|
|
rng: Rng,
|
|
}
|
|
|
|
impl Forth {
|
|
pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
|
|
Self {
|
|
stack: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
vars,
|
|
dict,
|
|
rng,
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn stack(&self) -> Vec<Value> {
|
|
self.stack.lock().unwrap().clone()
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn clear_stack(&self) {
|
|
self.stack.lock().unwrap().clear();
|
|
}
|
|
|
|
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
|
self.evaluate_impl(script, ctx, None)
|
|
}
|
|
|
|
pub fn evaluate_with_trace(
|
|
&self,
|
|
script: &str,
|
|
ctx: &StepContext,
|
|
trace: &mut ExecutionTrace,
|
|
) -> Result<Vec<String>, String> {
|
|
self.evaluate_impl(script, ctx, Some(trace))
|
|
}
|
|
|
|
fn evaluate_impl(
|
|
&self,
|
|
script: &str,
|
|
ctx: &StepContext,
|
|
trace: Option<&mut ExecutionTrace>,
|
|
) -> Result<Vec<String>, String> {
|
|
if script.trim().is_empty() {
|
|
return Err("empty script".into());
|
|
}
|
|
|
|
let ops = compile_script(script, &self.dict)?;
|
|
self.execute(&ops, ctx, trace)
|
|
}
|
|
|
|
fn execute(
|
|
&self,
|
|
ops: &[Op],
|
|
ctx: &StepContext,
|
|
trace: Option<&mut ExecutionTrace>,
|
|
) -> Result<Vec<String>, String> {
|
|
let mut stack = self.stack.lock().unwrap();
|
|
let mut outputs: Vec<String> = Vec::new();
|
|
let mut cmd = CmdRegister::default();
|
|
|
|
self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
|
|
|
|
Ok(outputs)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[allow(clippy::only_used_in_recursion)]
|
|
fn execute_ops(
|
|
&self,
|
|
ops: &[Op],
|
|
ctx: &StepContext,
|
|
stack: &mut Vec<Value>,
|
|
outputs: &mut Vec<String>,
|
|
cmd: &mut CmdRegister,
|
|
trace: Option<&mut ExecutionTrace>,
|
|
) -> Result<(), String> {
|
|
let mut pc = 0;
|
|
let trace_cell = std::cell::RefCell::new(trace);
|
|
|
|
let run_quotation =
|
|
|quot: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, 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(())
|
|
}
|
|
_ => Err("expected quotation".into()),
|
|
}
|
|
};
|
|
|
|
let select_and_run =
|
|
|selected: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, 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(())
|
|
}
|
|
};
|
|
|
|
let drain_select_run = |count: usize,
|
|
idx: usize,
|
|
stack: &mut Vec<Value>,
|
|
outputs: &mut Vec<String>,
|
|
cmd: &mut CmdRegister|
|
|
-> Result<(), String> {
|
|
if stack.len() < count {
|
|
return Err("stack underflow".into());
|
|
}
|
|
let start = stack.len() - count;
|
|
let values: Vec<Value> = stack.drain(start..).collect();
|
|
let selected = values[idx].clone();
|
|
select_and_run(selected, stack, outputs, cmd)
|
|
};
|
|
|
|
let drain_list_select_run = |idx_source: usize,
|
|
err_msg: &str,
|
|
stack: &mut Vec<Value>,
|
|
outputs: &mut Vec<String>,
|
|
cmd: &mut CmdRegister|
|
|
-> Result<(), String> {
|
|
let mut values = Vec::new();
|
|
while let Some(v) = stack.pop() {
|
|
if v.is_marker() {
|
|
break;
|
|
}
|
|
values.push(v);
|
|
}
|
|
if values.is_empty() {
|
|
return Err(err_msg.into());
|
|
}
|
|
values.reverse();
|
|
let idx = idx_source % values.len();
|
|
let selected = values[idx].clone();
|
|
select_and_run(selected, stack, outputs, cmd)
|
|
};
|
|
|
|
let emit_once = |cmd: &CmdRegister, outputs: &mut Vec<String>| -> Result<Option<Value>, 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();
|
|
emit_output(&sound, &resolved_params, ctx.step_duration(), ctx.nudge_secs, outputs);
|
|
Ok(Some(sound_val))
|
|
};
|
|
|
|
while pc < ops.len() {
|
|
match &ops[pc] {
|
|
Op::PushInt(n, span) => stack.push(Value::Int(*n, *span)),
|
|
Op::PushFloat(f, span) => stack.push(Value::Float(*f, *span)),
|
|
Op::PushStr(s, span) => stack.push(Value::Str(s.clone(), *span)),
|
|
|
|
Op::Dup => {
|
|
let v = stack.last().ok_or("stack underflow")?.clone();
|
|
stack.push(v);
|
|
}
|
|
Op::Dupn => {
|
|
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
|
let v = stack.pop().ok_or("stack underflow")?;
|
|
for _ in 0..n {
|
|
stack.push(v.clone());
|
|
}
|
|
}
|
|
Op::Drop => {
|
|
stack.pop().ok_or("stack underflow")?;
|
|
}
|
|
Op::Swap => {
|
|
let len = stack.len();
|
|
if len < 2 {
|
|
return Err("stack underflow".into());
|
|
}
|
|
stack.swap(len - 1, len - 2);
|
|
}
|
|
Op::Over => {
|
|
let len = stack.len();
|
|
if len < 2 {
|
|
return Err("stack underflow".into());
|
|
}
|
|
let v = stack[len - 2].clone();
|
|
stack.push(v);
|
|
}
|
|
Op::Rot => {
|
|
let len = stack.len();
|
|
if len < 3 {
|
|
return Err("stack underflow".into());
|
|
}
|
|
let v = stack.remove(len - 3);
|
|
stack.push(v);
|
|
}
|
|
Op::Nip => {
|
|
let len = stack.len();
|
|
if len < 2 {
|
|
return Err("stack underflow".into());
|
|
}
|
|
stack.remove(len - 2);
|
|
}
|
|
Op::Tuck => {
|
|
let len = stack.len();
|
|
if len < 2 {
|
|
return Err("stack underflow".into());
|
|
}
|
|
let v = stack[len - 1].clone();
|
|
stack.insert(len - 2, v);
|
|
}
|
|
|
|
Op::Add => binary_op(stack, |a, b| a + b)?,
|
|
Op::Sub => binary_op(stack, |a, b| a - b)?,
|
|
Op::Mul => binary_op(stack, |a, b| a * b)?,
|
|
Op::Div => {
|
|
let b = stack.pop().ok_or("stack underflow")?;
|
|
let a = stack.pop().ok_or("stack underflow")?;
|
|
if b.as_float().map_or(true, |v| v == 0.0) {
|
|
return Err("division by zero".into());
|
|
}
|
|
stack.push(lift_binary(a, b, |x, y| x / y)?);
|
|
}
|
|
Op::Mod => {
|
|
let b = stack.pop().ok_or("stack underflow")?;
|
|
let a = stack.pop().ok_or("stack underflow")?;
|
|
if b.as_float().map_or(true, |v| v == 0.0) {
|
|
return Err("modulo by zero".into());
|
|
}
|
|
let result = lift_binary(a, b, |x, y| (x as i64 % y as i64) as f64)?;
|
|
stack.push(result);
|
|
}
|
|
Op::Neg => {
|
|
let v = stack.pop().ok_or("stack underflow")?;
|
|
stack.push(lift_unary(v, |x| -x)?);
|
|
}
|
|
Op::Abs => {
|
|
let v = stack.pop().ok_or("stack underflow")?;
|
|
stack.push(lift_unary(v, |x| x.abs())?);
|
|
}
|
|
Op::Floor => {
|
|
let v = stack.pop().ok_or("stack underflow")?;
|
|
stack.push(lift_unary(v, |x| x.floor())?);
|
|
}
|
|
Op::Ceil => {
|
|
let v = stack.pop().ok_or("stack underflow")?;
|
|
stack.push(lift_unary(v, |x| x.ceil())?);
|
|
}
|
|
Op::Round => {
|
|
let v = stack.pop().ok_or("stack underflow")?;
|
|
stack.push(lift_unary(v, |x| x.round())?);
|
|
}
|
|
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)?,
|
|
Op::Lt => cmp_op(stack, |a, b| a < b)?,
|
|
Op::Gt => cmp_op(stack, |a, b| a > b)?,
|
|
Op::Le => cmp_op(stack, |a, b| a <= b)?,
|
|
Op::Ge => cmp_op(stack, |a, b| a >= b)?,
|
|
|
|
Op::And => {
|
|
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::Or => {
|
|
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::Not => {
|
|
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")?;
|
|
if !v.is_truthy() {
|
|
if let Some(span) = else_span {
|
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
|
trace.executed_spans.push(*span);
|
|
}
|
|
}
|
|
pc += offset;
|
|
} else if let Some(span) = then_span {
|
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
|
trace.executed_spans.push(*span);
|
|
}
|
|
}
|
|
}
|
|
Op::Branch(offset) => {
|
|
pc += offset;
|
|
}
|
|
|
|
Op::NewCmd => {
|
|
let val = stack.pop().ok_or("stack underflow")?;
|
|
cmd.set_sound(val);
|
|
}
|
|
Op::SetParam(param) => {
|
|
let val = stack.pop().ok_or("stack underflow")?;
|
|
cmd.set_param(param.clone(), val);
|
|
}
|
|
|
|
Op::Emit => {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Op::Get => {
|
|
let name = stack.pop().ok_or("stack underflow")?;
|
|
let name = name.as_str()?;
|
|
let vars = self.vars.lock().unwrap();
|
|
let val = vars.get(name).cloned().unwrap_or(Value::Int(0, None));
|
|
stack.push(val);
|
|
}
|
|
Op::Set => {
|
|
let name = stack.pop().ok_or("stack underflow")?;
|
|
let name = name.as_str()?.to_string();
|
|
let val = stack.pop().ok_or("stack underflow")?;
|
|
self.vars.lock().unwrap().insert(name, val);
|
|
}
|
|
|
|
Op::GetContext(name) => {
|
|
let val = match name.as_str() {
|
|
"step" => Value::Int(ctx.step as i64, None),
|
|
"beat" => Value::Float(ctx.beat, None),
|
|
"bank" => Value::Int(ctx.bank as i64, None),
|
|
"pattern" => Value::Int(ctx.pattern as i64, None),
|
|
"tempo" => Value::Float(ctx.tempo, None),
|
|
"phase" => Value::Float(ctx.phase, None),
|
|
"slot" => Value::Int(ctx.slot as i64, None),
|
|
"runs" => Value::Int(ctx.runs as i64, None),
|
|
"iter" => Value::Int(ctx.iter as i64, None),
|
|
"speed" => Value::Float(ctx.speed, None),
|
|
"stepdur" => Value::Float(ctx.step_duration(), None),
|
|
"fill" => Value::Int(if ctx.fill { 1 } else { 0 }, None),
|
|
_ => Value::Int(0, None),
|
|
};
|
|
stack.push(val);
|
|
}
|
|
|
|
Op::Rand => {
|
|
let b = stack.pop().ok_or("stack underflow")?;
|
|
let a = stack.pop().ok_or("stack underflow")?;
|
|
match (&a, &b) {
|
|
(Value::Int(a_i, _), Value::Int(b_i, _)) => {
|
|
let (lo, hi) = if a_i <= b_i { (*a_i, *b_i) } else { (*b_i, *a_i) };
|
|
let val = self.rng.lock().unwrap().gen_range(lo..=hi);
|
|
stack.push(Value::Int(val, None));
|
|
}
|
|
_ => {
|
|
let a_f = a.as_float()?;
|
|
let b_f = b.as_float()?;
|
|
let (lo, hi) = if a_f <= b_f { (a_f, b_f) } else { (b_f, a_f) };
|
|
let val = self.rng.lock().unwrap().gen_range(lo..hi);
|
|
stack.push(Value::Float(val, None));
|
|
}
|
|
}
|
|
}
|
|
Op::Seed => {
|
|
let s = stack.pop().ok_or("stack underflow")?.as_int()?;
|
|
*self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64);
|
|
}
|
|
|
|
Op::Cycle | Op::PCycle => {
|
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
|
if count == 0 {
|
|
return Err("cycle count must be > 0".into());
|
|
}
|
|
let idx = match &ops[pc] {
|
|
Op::Cycle => ctx.runs,
|
|
_ => ctx.iter,
|
|
} % count;
|
|
drain_select_run(count, idx, stack, outputs, cmd)?;
|
|
}
|
|
|
|
Op::Choose => {
|
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
|
if count == 0 {
|
|
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, cmd)?;
|
|
}
|
|
|
|
Op::ChanceExec | Op::ProbExec => {
|
|
let threshold = stack.pop().ok_or("stack underflow")?.as_float()?;
|
|
let quot = stack.pop().ok_or("stack underflow")?;
|
|
let val: f64 = self.rng.lock().unwrap().gen();
|
|
let limit = match &ops[pc] {
|
|
Op::ChanceExec => threshold,
|
|
_ => threshold / 100.0,
|
|
};
|
|
if val < limit {
|
|
run_quotation(quot, stack, outputs, cmd)?;
|
|
}
|
|
}
|
|
|
|
Op::Coin => {
|
|
let val: f64 = self.rng.lock().unwrap().gen();
|
|
stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None));
|
|
}
|
|
|
|
Op::Every => {
|
|
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
|
if n <= 0 {
|
|
return Err("every count must be > 0".into());
|
|
}
|
|
let result = ctx.iter as i64 % n == 0;
|
|
stack.push(Value::Int(if result { 1 } else { 0 }, None));
|
|
}
|
|
|
|
Op::Quotation(quote_ops, body_span) => {
|
|
stack.push(Value::Quotation(quote_ops.clone(), *body_span));
|
|
}
|
|
|
|
Op::When | Op::Unless => {
|
|
let cond = stack.pop().ok_or("stack underflow")?;
|
|
let quot = stack.pop().ok_or("stack underflow")?;
|
|
let should_run = match &ops[pc] {
|
|
Op::When => cond.is_truthy(),
|
|
_ => !cond.is_truthy(),
|
|
};
|
|
if should_run {
|
|
run_quotation(quot, stack, outputs, cmd)?;
|
|
}
|
|
}
|
|
|
|
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
|
|
};
|
|
run_quotation(quot, stack, outputs, cmd)?;
|
|
}
|
|
|
|
Op::Pick => {
|
|
let idx_i = stack.pop().ok_or("stack underflow")?.as_int()?;
|
|
if idx_i < 0 {
|
|
return Err(format!("pick index must be >= 0, got {idx_i}"));
|
|
}
|
|
let idx = idx_i 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()
|
|
));
|
|
}
|
|
run_quotation(quots.swap_remove(idx), stack, outputs, cmd)?;
|
|
}
|
|
|
|
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);
|
|
stack.push(Value::Float(freq, None));
|
|
}
|
|
|
|
Op::Ftom => {
|
|
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
|
let note = 69.0 + 12.0 * (freq / 440.0).log2();
|
|
stack.push(Value::Float(note, None));
|
|
}
|
|
|
|
Op::Degree(pattern) => {
|
|
if pattern.is_empty() {
|
|
return Err("empty scale pattern".into());
|
|
}
|
|
let val = stack.pop().ok_or("stack underflow")?;
|
|
let len = pattern.len() as i64;
|
|
let result = lift_unary_int(val, |degree| {
|
|
let octave_offset = degree.div_euclid(len);
|
|
let idx = degree.rem_euclid(len) as usize;
|
|
60 + octave_offset * 12 + pattern[idx]
|
|
})?;
|
|
stack.push(result);
|
|
}
|
|
|
|
Op::Oct => {
|
|
let shift = stack.pop().ok_or("stack underflow")?;
|
|
let note = stack.pop().ok_or("stack underflow")?;
|
|
let result = lift_binary(note, shift, |n, s| n + s * 12.0)?;
|
|
stack.push(result);
|
|
}
|
|
|
|
Op::SetTempo => {
|
|
let tempo = stack.pop().ok_or("stack underflow")?.as_float()?;
|
|
let clamped = tempo.clamp(20.0, 300.0);
|
|
self.vars
|
|
.lock()
|
|
.unwrap()
|
|
.insert("__tempo__".to_string(), Value::Float(clamped, None));
|
|
}
|
|
|
|
Op::SetSpeed => {
|
|
let speed = stack.pop().ok_or("stack underflow")?.as_float()?;
|
|
let clamped = speed.clamp(0.125, 8.0);
|
|
let key = format!("__speed_{}_{}__", ctx.bank, ctx.pattern);
|
|
self.vars
|
|
.lock()
|
|
.unwrap()
|
|
.insert(key, Value::Float(clamped, None));
|
|
}
|
|
|
|
Op::Chain => {
|
|
let pattern = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
|
let bank = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
|
if bank < 0 || pattern < 0 {
|
|
return Err("chain: bank and pattern must be >= 1".into());
|
|
}
|
|
if bank as usize == ctx.bank && pattern as usize == ctx.pattern {
|
|
// chaining to self is a no-op
|
|
} else {
|
|
let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern);
|
|
let val = format!("{bank}:{pattern}");
|
|
self.vars.lock().unwrap().insert(key, Value::Str(val, None));
|
|
}
|
|
}
|
|
|
|
Op::Loop => {
|
|
let beats = stack.pop().ok_or("stack underflow")?.as_float()?;
|
|
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
|
return Err("tempo and speed must be non-zero".into());
|
|
}
|
|
let dur = beats * 60.0 / ctx.tempo / ctx.speed;
|
|
cmd.set_param("fit".into(), Value::Float(dur, None));
|
|
cmd.set_param("dur".into(), Value::Float(dur, None));
|
|
}
|
|
|
|
Op::ListStart => {
|
|
stack.push(Value::Marker);
|
|
}
|
|
|
|
Op::ListEnd => {
|
|
let mut count = 0;
|
|
let mut values = Vec::new();
|
|
while let Some(v) = stack.pop() {
|
|
if v.is_marker() {
|
|
break;
|
|
}
|
|
values.push(v);
|
|
count += 1;
|
|
}
|
|
values.reverse();
|
|
for v in values {
|
|
stack.push(v);
|
|
}
|
|
stack.push(Value::Int(count, None));
|
|
}
|
|
|
|
Op::ListEndCycle | Op::ListEndPCycle => {
|
|
let idx_source = match &ops[pc] {
|
|
Op::ListEndCycle => ctx.runs,
|
|
_ => ctx.iter,
|
|
};
|
|
let err_msg = match &ops[pc] {
|
|
Op::ListEndCycle => "empty cycle list",
|
|
_ => "empty pattern cycle list",
|
|
};
|
|
drain_list_select_run(idx_source, err_msg, stack, outputs, cmd)?;
|
|
}
|
|
|
|
Op::Adsr => {
|
|
let r = stack.pop().ok_or("stack underflow")?;
|
|
let s = stack.pop().ok_or("stack underflow")?;
|
|
let d = stack.pop().ok_or("stack underflow")?;
|
|
let a = stack.pop().ok_or("stack underflow")?;
|
|
cmd.set_param("attack".into(), a);
|
|
cmd.set_param("decay".into(), d);
|
|
cmd.set_param("sustain".into(), s);
|
|
cmd.set_param("release".into(), r);
|
|
}
|
|
|
|
Op::Ad => {
|
|
let d = stack.pop().ok_or("stack underflow")?;
|
|
let a = stack.pop().ok_or("stack underflow")?;
|
|
cmd.set_param("attack".into(), a);
|
|
cmd.set_param("decay".into(), d);
|
|
cmd.set_param("sustain".into(), Value::Int(0, None));
|
|
}
|
|
|
|
Op::Apply => {
|
|
let quot = stack.pop().ok_or("stack underflow")?;
|
|
run_quotation(quot, stack, outputs, cmd)?;
|
|
}
|
|
|
|
Op::Ramp => {
|
|
let curve = stack.pop().ok_or("stack underflow")?.as_float()?;
|
|
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 = 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::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));
|
|
}
|
|
|
|
Op::ClearCmd => {
|
|
cmd.clear();
|
|
}
|
|
|
|
Op::EmitN => {
|
|
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
|
if n < 0 {
|
|
return Err("emit count must be >= 0".into());
|
|
}
|
|
for _ in 0..n {
|
|
emit_once(cmd, outputs)?;
|
|
}
|
|
}
|
|
}
|
|
pc += 1;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
const TEMPO_SCALED_PARAMS: &[&str] = &[
|
|
"attack",
|
|
"decay",
|
|
"release",
|
|
"lpa",
|
|
"lpd",
|
|
"lpr",
|
|
"hpa",
|
|
"hpd",
|
|
"hpr",
|
|
"bpa",
|
|
"bpd",
|
|
"bpr",
|
|
"patt",
|
|
"pdec",
|
|
"prel",
|
|
"fma",
|
|
"fmd",
|
|
"fmr",
|
|
"glide",
|
|
"verbdecay",
|
|
"verbpredelay",
|
|
"chorusdelay",
|
|
"duration",
|
|
];
|
|
|
|
fn emit_output(
|
|
sound: &str,
|
|
params: &[(String, String)],
|
|
step_duration: f64,
|
|
nudge_secs: f64,
|
|
outputs: &mut Vec<String>,
|
|
) {
|
|
let mut pairs = vec![("sound".into(), sound.to_string())];
|
|
pairs.extend(params.iter().cloned());
|
|
if nudge_secs > 0.0 {
|
|
pairs.push(("delta".into(), nudge_secs.to_string()));
|
|
}
|
|
if !pairs.iter().any(|(k, _)| k == "dur") {
|
|
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 * step_duration).to_string();
|
|
} else {
|
|
pairs.push(("delaytime".into(), step_duration.to_string()));
|
|
}
|
|
for pair in &mut pairs {
|
|
if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) {
|
|
if let Ok(val) = pair.1.parse::<f64>() {
|
|
pair.1 = (val * step_duration).to_string();
|
|
}
|
|
}
|
|
}
|
|
outputs.push(format_cmd(&pairs));
|
|
}
|
|
|
|
fn perlin_grad(hash_input: i64) -> f64 {
|
|
let mut h = (hash_input as u64)
|
|
.wrapping_mul(6364136223846793005)
|
|
.wrapping_add(1442695040888963407);
|
|
h ^= h >> 33;
|
|
h = h.wrapping_mul(0xff51afd7ed558ccd);
|
|
h ^= h >> 33;
|
|
// 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 {
|
|
let x0 = x.floor() as i64;
|
|
let t = x - x0 as f64;
|
|
let s = t * t * (3.0 - 2.0 * t);
|
|
let d0 = perlin_grad(x0) * t;
|
|
let d1 = perlin_grad(x0 + 1) * (t - 1.0);
|
|
(d0 + s * (d1 - d0)) * 0.5 + 0.5
|
|
}
|
|
|
|
fn float_to_value(result: f64) -> Value {
|
|
if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
|
|
Value::Int(result as i64, None)
|
|
} else {
|
|
Value::Float(result, None)
|
|
}
|
|
}
|
|
|
|
fn lift_unary<F>(val: Value, f: F) -> Result<Value, String>
|
|
where
|
|
F: Fn(f64) -> f64,
|
|
{
|
|
Ok(float_to_value(f(val.as_float()?)))
|
|
}
|
|
|
|
fn lift_unary_int<F>(val: Value, f: F) -> Result<Value, String>
|
|
where
|
|
F: Fn(i64) -> i64,
|
|
{
|
|
Ok(Value::Int(f(val.as_int()?), None))
|
|
}
|
|
|
|
fn lift_binary<F>(a: Value, b: Value, f: F) -> Result<Value, String>
|
|
where
|
|
F: Fn(f64, f64) -> f64,
|
|
{
|
|
Ok(float_to_value(f(a.as_float()?, b.as_float()?)))
|
|
}
|
|
|
|
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
|
where
|
|
F: Fn(f64, f64) -> f64 + Copy,
|
|
{
|
|
let b = stack.pop().ok_or("stack underflow")?;
|
|
let a = stack.pop().ok_or("stack underflow")?;
|
|
stack.push(lift_binary(a, b, f)?);
|
|
Ok(())
|
|
}
|
|
|
|
fn cmp_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
|
where
|
|
F: Fn(f64, f64) -> bool,
|
|
{
|
|
let b = stack.pop().ok_or("stack underflow")?;
|
|
let a = stack.pop().ok_or("stack underflow")?;
|
|
let result = if f(a.as_float()?, b.as_float()?) {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
stack.push(Value::Int(result, None));
|
|
Ok(())
|
|
}
|
|
|
|
fn format_cmd(pairs: &[(String, String)]) -> String {
|
|
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
|
format!("/{}", parts.join("/"))
|
|
}
|