diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 998f18c..ecbad3b 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -83,4 +83,7 @@ pub enum Op { ClearCmd, SetSpeed, At, + IntRange, + Generate, + GeomRange, } diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index b1768e8..1fb9db5 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -140,10 +140,8 @@ impl CmdRegister { &self.deltas } - pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> { - self.sound - .as_ref() - .map(|s| (s.clone(), self.params.clone())) + pub(super) fn snapshot(&self) -> Option<(&Value, &[(String, Value)])> { + self.sound.as_ref().map(|s| (s, self.params.as_slice())) } pub(super) fn clear(&mut self) { diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index d8ea3af..9aa157b 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -1,5 +1,6 @@ use rand::rngs::StdRng; use rand::{Rng as RngTrait, SeedableRng}; +use std::borrow::Cow; use super::compiler::compile_script; use super::ops::Op; @@ -147,13 +148,11 @@ impl Forth { let emit_with_cycling = |cmd: &CmdRegister, emit_idx: usize, delta_secs: f64, outputs: &mut Vec| -> Result, String> { let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?; - let resolved_sound_val = resolve_cycling(&sound_val, emit_idx); - // Note: sound span is recorded by Op::Emit, not here + let resolved_sound_val = resolve_cycling(sound_val, emit_idx); let sound = resolved_sound_val.as_str()?.to_string(); let resolved_params: Vec<(String, String)> = params.iter().map(|(k, v)| { let resolved = resolve_cycling(v, emit_idx); - // Record selected span for params if they came from a CycleList if let Value::CycleList(_) = v { if let Some(span) = resolved.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { @@ -164,7 +163,7 @@ impl Forth { (k.clone(), resolved.to_param_string()) }).collect(); emit_output(&sound, &resolved_params, ctx.step_duration(), delta_secs, outputs); - Ok(Some(resolved_sound_val)) + Ok(Some(resolved_sound_val.into_owned())) }; while pc < ops.len() { @@ -630,8 +629,15 @@ impl Forth { let top = stack.pop().ok_or("stack underflow")?; let deltas = match &top { Value::Float(..) => vec![top], - Value::Int(n, _) if *n > 0 && stack.len() >= *n as usize => { + Value::Int(n, _) => { let count = *n as usize; + if stack.len() < count { + return Err(format!( + "at: stack underflow, expected {} values but got {}", + count, + stack.len() + )); + } let mut vals = Vec::with_capacity(count); for _ in 0..count { vals.push(stack.pop().ok_or("stack underflow")?); @@ -639,8 +645,7 @@ impl Forth { vals.reverse(); vals } - Value::Int(..) => vec![top], - _ => return Err("at expects number or list".into()), + _ => return Err("at expects float or int count".into()), }; cmd.set_deltas(deltas); } @@ -709,6 +714,50 @@ impl Forth { emit_with_cycling(cmd, i, ctx.nudge_secs, outputs)?; } } + + Op::IntRange => { + let end = stack.pop().ok_or("stack underflow")?.as_int()?; + let start = stack.pop().ok_or("stack underflow")?.as_int()?; + if start <= end { + for i in start..=end { + stack.push(Value::Int(i, None)); + } + } else { + for i in (end..=start).rev() { + stack.push(Value::Int(i, None)); + } + } + } + + Op::Generate => { + let count = stack.pop().ok_or("stack underflow")?.as_int()?; + let quot = stack.pop().ok_or("stack underflow")?; + if count < 0 { + return Err("gen count must be >= 0".into()); + } + let mut results = Vec::with_capacity(count as usize); + for _ in 0..count { + run_quotation(quot.clone(), stack, outputs, cmd)?; + results.push(stack.pop().ok_or("gen: quotation must produce a value")?); + } + for val in results { + stack.push(val); + } + } + + Op::GeomRange => { + let count = stack.pop().ok_or("stack underflow")?.as_int()?; + let ratio = stack.pop().ok_or("stack underflow")?.as_float()?; + let start = stack.pop().ok_or("stack underflow")?.as_float()?; + if count < 0 { + return Err("geom.. count must be >= 0".into()); + } + let mut val = start; + for _ in 0..count { + stack.push(float_to_value(val)); + val *= ratio; + } + } } pc += 1; } @@ -717,31 +766,34 @@ impl Forth { } } -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 is_tempo_scaled_param(name: &str) -> bool { + matches!( + name, + "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, @@ -765,7 +817,7 @@ fn emit_output( pairs.push(("delaytime".into(), step_duration.to_string())); } for pair in &mut pairs { - if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) { + if is_tempo_scaled_param(&pair.0) { if let Ok(val) = pair.1.parse::() { pair.1 = (val * step_duration).to_string(); } @@ -853,11 +905,11 @@ fn format_cmd(pairs: &[(String, String)]) -> String { format!("/{}", parts.join("/")) } -fn resolve_cycling(val: &Value, emit_idx: usize) -> Value { +fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> { match val { Value::CycleList(items) if !items.is_empty() => { - items[emit_idx % items.len()].clone() + Cow::Owned(items[emit_idx % items.len()].clone()) } - other => other.clone(), + other => Cow::Borrowed(other), } } diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index 86c547e..a0b59b3 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -17,6 +17,7 @@ pub struct Word { pub desc: &'static str, pub example: &'static str, pub compile: WordCompile, + pub varargs: bool, } use WordCompile::*; @@ -31,6 +32,7 @@ pub const WORDS: &[Word] = &[ desc: "Duplicate top of stack", example: "3 dup => 3 3", compile: Simple, + varargs: false, }, Word { name: "dupn", @@ -40,6 +42,7 @@ pub const WORDS: &[Word] = &[ desc: "Duplicate a onto stack n times", example: "2 4 dupn => 2 2 2 2", compile: Simple, + varargs: true, }, Word { name: "drop", @@ -49,6 +52,7 @@ pub const WORDS: &[Word] = &[ desc: "Remove top of stack", example: "1 2 drop => 1", compile: Simple, + varargs: false, }, Word { name: "swap", @@ -58,6 +62,7 @@ pub const WORDS: &[Word] = &[ desc: "Exchange top two items", example: "1 2 swap => 2 1", compile: Simple, + varargs: false, }, Word { name: "over", @@ -67,6 +72,7 @@ pub const WORDS: &[Word] = &[ desc: "Copy second to top", example: "1 2 over => 1 2 1", compile: Simple, + varargs: false, }, Word { name: "rot", @@ -76,6 +82,7 @@ pub const WORDS: &[Word] = &[ desc: "Rotate top three", example: "1 2 3 rot => 2 3 1", compile: Simple, + varargs: false, }, Word { name: "nip", @@ -85,6 +92,7 @@ pub const WORDS: &[Word] = &[ desc: "Remove second item", example: "1 2 nip => 2", compile: Simple, + varargs: false, }, Word { name: "tuck", @@ -94,6 +102,7 @@ pub const WORDS: &[Word] = &[ desc: "Copy top under second", example: "1 2 tuck => 2 1 2", compile: Simple, + varargs: false, }, // Arithmetic Word { @@ -104,6 +113,7 @@ pub const WORDS: &[Word] = &[ desc: "Add", example: "2 3 + => 5", compile: Simple, + varargs: false, }, Word { name: "-", @@ -113,6 +123,7 @@ pub const WORDS: &[Word] = &[ desc: "Subtract", example: "5 3 - => 2", compile: Simple, + varargs: false, }, Word { name: "*", @@ -122,6 +133,7 @@ pub const WORDS: &[Word] = &[ desc: "Multiply", example: "3 4 * => 12", compile: Simple, + varargs: false, }, Word { name: "/", @@ -131,6 +143,7 @@ pub const WORDS: &[Word] = &[ desc: "Divide", example: "10 2 / => 5", compile: Simple, + varargs: false, }, Word { name: "mod", @@ -140,6 +153,7 @@ pub const WORDS: &[Word] = &[ desc: "Modulo", example: "7 3 mod => 1", compile: Simple, + varargs: false, }, Word { name: "neg", @@ -149,6 +163,7 @@ pub const WORDS: &[Word] = &[ desc: "Negate", example: "5 neg => -5", compile: Simple, + varargs: false, }, Word { name: "abs", @@ -158,6 +173,7 @@ pub const WORDS: &[Word] = &[ desc: "Absolute value", example: "-5 abs => 5", compile: Simple, + varargs: false, }, Word { name: "floor", @@ -167,6 +183,7 @@ pub const WORDS: &[Word] = &[ desc: "Round down to integer", example: "3.7 floor => 3", compile: Simple, + varargs: false, }, Word { name: "ceil", @@ -176,6 +193,7 @@ pub const WORDS: &[Word] = &[ desc: "Round up to integer", example: "3.2 ceil => 4", compile: Simple, + varargs: false, }, Word { name: "round", @@ -185,6 +203,7 @@ pub const WORDS: &[Word] = &[ desc: "Round to nearest integer", example: "3.5 round => 4", compile: Simple, + varargs: false, }, Word { name: "min", @@ -194,6 +213,7 @@ pub const WORDS: &[Word] = &[ desc: "Minimum of two values", example: "3 5 min => 3", compile: Simple, + varargs: false, }, Word { name: "max", @@ -203,6 +223,7 @@ pub const WORDS: &[Word] = &[ desc: "Maximum of two values", example: "3 5 max => 5", compile: Simple, + varargs: false, }, Word { name: "pow", @@ -212,6 +233,7 @@ pub const WORDS: &[Word] = &[ desc: "Exponentiation", example: "2 3 pow => 8", compile: Simple, + varargs: false, }, Word { name: "sqrt", @@ -221,6 +243,7 @@ pub const WORDS: &[Word] = &[ desc: "Square root", example: "16 sqrt => 4", compile: Simple, + varargs: false, }, Word { name: "sin", @@ -230,6 +253,7 @@ pub const WORDS: &[Word] = &[ desc: "Sine (radians)", example: "3.14159 2 / sin => 1.0", compile: Simple, + varargs: false, }, Word { name: "cos", @@ -239,6 +263,7 @@ pub const WORDS: &[Word] = &[ desc: "Cosine (radians)", example: "0 cos => 1.0", compile: Simple, + varargs: false, }, Word { name: "log", @@ -248,6 +273,7 @@ pub const WORDS: &[Word] = &[ desc: "Natural logarithm", example: "2.718 log => 1.0", compile: Simple, + varargs: false, }, // Comparison Word { @@ -258,6 +284,7 @@ pub const WORDS: &[Word] = &[ desc: "Equal", example: "3 3 = => 1", compile: Simple, + varargs: false, }, Word { name: "!=", @@ -267,6 +294,7 @@ pub const WORDS: &[Word] = &[ desc: "Not equal", example: "3 4 != => 1", compile: Simple, + varargs: false, }, Word { name: "lt", @@ -276,6 +304,7 @@ pub const WORDS: &[Word] = &[ desc: "Less than", example: "2 3 lt => 1", compile: Simple, + varargs: false, }, Word { name: "gt", @@ -285,6 +314,7 @@ pub const WORDS: &[Word] = &[ desc: "Greater than", example: "3 2 gt => 1", compile: Simple, + varargs: false, }, Word { name: "<=", @@ -294,6 +324,7 @@ pub const WORDS: &[Word] = &[ desc: "Less or equal", example: "3 3 <= => 1", compile: Simple, + varargs: false, }, Word { name: ">=", @@ -303,6 +334,7 @@ pub const WORDS: &[Word] = &[ desc: "Greater or equal", example: "3 3 >= => 1", compile: Simple, + varargs: false, }, // Logic Word { @@ -313,6 +345,7 @@ pub const WORDS: &[Word] = &[ desc: "Logical and", example: "1 1 and => 1", compile: Simple, + varargs: false, }, Word { name: "or", @@ -322,6 +355,7 @@ pub const WORDS: &[Word] = &[ desc: "Logical or", example: "0 1 or => 1", compile: Simple, + varargs: false, }, Word { name: "not", @@ -331,6 +365,7 @@ pub const WORDS: &[Word] = &[ desc: "Logical not", example: "0 not => 1", compile: Simple, + varargs: false, }, Word { name: "xor", @@ -340,6 +375,7 @@ pub const WORDS: &[Word] = &[ desc: "Exclusive or", example: "1 0 xor => 1", compile: Simple, + varargs: false, }, Word { name: "nand", @@ -349,6 +385,7 @@ pub const WORDS: &[Word] = &[ desc: "Not and", example: "1 1 nand => 0", compile: Simple, + varargs: false, }, Word { name: "nor", @@ -358,6 +395,7 @@ pub const WORDS: &[Word] = &[ desc: "Not or", example: "0 0 nor => 1", compile: Simple, + varargs: false, }, Word { name: "ifelse", @@ -367,6 +405,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute true-quot if true, else false-quot", example: "{ 1 } { 2 } coin ifelse", compile: Simple, + varargs: false, }, Word { name: "pick", @@ -376,6 +415,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute nth quotation (0-indexed)", example: "{ 1 } { 2 } { 3 } 2 pick => 3", compile: Simple, + varargs: true, }, // Sound Word { @@ -386,6 +426,7 @@ pub const WORDS: &[Word] = &[ desc: "Begin sound command", example: "\"kick\" sound", compile: Simple, + varargs: false, }, Word { name: ".", @@ -395,6 +436,7 @@ pub const WORDS: &[Word] = &[ desc: "Emit current sound", example: "\"kick\" s . . . .", compile: Simple, + varargs: false, }, Word { name: ".!", @@ -404,6 +446,7 @@ pub const WORDS: &[Word] = &[ desc: "Emit current sound n times", example: "\"kick\" s 4 .!", compile: Simple, + varargs: true, }, // Variables (prefix syntax: @name to fetch, !name to store) Word { @@ -414,6 +457,7 @@ pub const WORDS: &[Word] = &[ desc: "Fetch variable value", example: "@freq => 440", compile: Simple, + varargs: false, }, Word { name: "!", @@ -423,6 +467,7 @@ pub const WORDS: &[Word] = &[ desc: "Store value in variable", example: "440 !freq", compile: Simple, + varargs: false, }, // Randomness Word { @@ -433,6 +478,7 @@ pub const WORDS: &[Word] = &[ desc: "Random in range. Int if both args are int, float otherwise", example: "1 6 rand => 4 | 0.0 1.0 rand => 0.42", compile: Simple, + varargs: false, }, Word { name: "seed", @@ -442,6 +488,7 @@ pub const WORDS: &[Word] = &[ desc: "Set random seed", example: "12345 seed", compile: Simple, + varargs: false, }, Word { name: "coin", @@ -451,6 +498,7 @@ pub const WORDS: &[Word] = &[ desc: "50/50 random boolean", example: "coin => 0 or 1", compile: Simple, + varargs: false, }, Word { name: "chance", @@ -460,6 +508,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation with probability (0.0-1.0)", example: "{ 2 distort } 0.75 chance", compile: Simple, + varargs: false, }, Word { name: "prob", @@ -469,6 +518,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation with probability (0-100)", example: "{ 2 distort } 75 prob", compile: Simple, + varargs: false, }, Word { name: "choose", @@ -478,6 +528,7 @@ pub const WORDS: &[Word] = &[ desc: "Random pick from n items", example: "1 2 3 3 choose", compile: Simple, + varargs: true, }, Word { name: "cycle", @@ -487,6 +538,7 @@ pub const WORDS: &[Word] = &[ desc: "Cycle through n items by step runs", example: "60 64 67 3 cycle", compile: Simple, + varargs: true, }, Word { name: "pcycle", @@ -496,6 +548,7 @@ pub const WORDS: &[Word] = &[ desc: "Cycle through n items by pattern iteration", example: "60 64 67 3 pcycle", compile: Simple, + varargs: true, }, Word { name: "tcycle", @@ -505,6 +558,7 @@ pub const WORDS: &[Word] = &[ desc: "Create cycle list for emit-time resolution", example: "60 64 67 3 tcycle note", compile: Simple, + varargs: true, }, Word { name: "every", @@ -514,6 +568,7 @@ pub const WORDS: &[Word] = &[ desc: "True every nth iteration", example: "4 every", compile: Simple, + varargs: false, }, // Probability shortcuts Word { @@ -524,6 +579,7 @@ pub const WORDS: &[Word] = &[ desc: "Always execute quotation", example: "{ 2 distort } always", compile: Probability(1.0), + varargs: false, }, Word { name: "never", @@ -533,6 +589,7 @@ pub const WORDS: &[Word] = &[ desc: "Never execute quotation", example: "{ 2 distort } never", compile: Probability(0.0), + varargs: false, }, Word { name: "often", @@ -542,6 +599,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation 75% of the time", example: "{ 2 distort } often", compile: Probability(0.75), + varargs: false, }, Word { name: "sometimes", @@ -551,6 +609,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation 50% of the time", example: "{ 2 distort } sometimes", compile: Probability(0.5), + varargs: false, }, Word { name: "rarely", @@ -560,6 +619,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation 25% of the time", example: "{ 2 distort } rarely", compile: Probability(0.25), + varargs: false, }, Word { name: "almostNever", @@ -569,6 +629,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation 10% of the time", example: "{ 2 distort } almostNever", compile: Probability(0.1), + varargs: false, }, Word { name: "almostAlways", @@ -578,6 +639,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation 90% of the time", example: "{ 2 distort } almostAlways", compile: Probability(0.9), + varargs: false, }, // Context Word { @@ -588,6 +650,7 @@ pub const WORDS: &[Word] = &[ desc: "Current step index", example: "step => 0", compile: Context("step"), + varargs: false, }, Word { name: "beat", @@ -597,6 +660,7 @@ pub const WORDS: &[Word] = &[ desc: "Current beat position", example: "beat => 4.5", compile: Context("beat"), + varargs: false, }, Word { name: "bank", @@ -606,6 +670,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sample bank suffix", example: "\"a\" bank", compile: Param, + varargs: false, }, Word { name: "pattern", @@ -615,6 +680,7 @@ pub const WORDS: &[Word] = &[ desc: "Current pattern index", example: "pattern => 0", compile: Context("pattern"), + varargs: false, }, Word { name: "pbank", @@ -624,6 +690,7 @@ pub const WORDS: &[Word] = &[ desc: "Current pattern's bank index", example: "pbank => 0", compile: Context("bank"), + varargs: false, }, Word { name: "tempo", @@ -633,6 +700,7 @@ pub const WORDS: &[Word] = &[ desc: "Current BPM", example: "tempo => 120.0", compile: Context("tempo"), + varargs: false, }, Word { name: "phase", @@ -642,6 +710,7 @@ pub const WORDS: &[Word] = &[ desc: "Phase in bar (0-1)", example: "phase => 0.25", compile: Context("phase"), + varargs: false, }, Word { name: "slot", @@ -651,6 +720,7 @@ pub const WORDS: &[Word] = &[ desc: "Current slot number", example: "slot => 0", compile: Context("slot"), + varargs: false, }, Word { name: "runs", @@ -660,6 +730,7 @@ pub const WORDS: &[Word] = &[ desc: "Times this step ran", example: "runs => 3", compile: Context("runs"), + varargs: false, }, Word { name: "iter", @@ -669,6 +740,7 @@ pub const WORDS: &[Word] = &[ desc: "Pattern iteration count", example: "iter => 2", compile: Context("iter"), + varargs: false, }, Word { name: "stepdur", @@ -678,6 +750,7 @@ pub const WORDS: &[Word] = &[ desc: "Step duration in seconds", example: "stepdur => 0.125", compile: Context("stepdur"), + varargs: false, }, // Live keys Word { @@ -688,6 +761,7 @@ pub const WORDS: &[Word] = &[ desc: "True when fill is on (f key)", example: "\"snare\" s . fill ?", compile: Context("fill"), + varargs: false, }, // Music Word { @@ -698,6 +772,7 @@ pub const WORDS: &[Word] = &[ desc: "MIDI note to frequency", example: "69 mtof => 440.0", compile: Simple, + varargs: false, }, Word { name: "ftom", @@ -707,6 +782,7 @@ pub const WORDS: &[Word] = &[ desc: "Frequency to MIDI note", example: "440 ftom => 69.0", compile: Simple, + varargs: false, }, // LFO Word { @@ -717,6 +793,7 @@ pub const WORDS: &[Word] = &[ desc: "Ramp [0,1]: fract(freq*beat)^curve", example: "0.25 2.0 ramp", compile: Simple, + varargs: false, }, Word { name: "range", @@ -726,6 +803,7 @@ pub const WORDS: &[Word] = &[ desc: "Scale [0,1] to [min,max]", example: "0.5 200 800 range => 500", compile: Simple, + varargs: false, }, Word { name: "linramp", @@ -735,6 +813,7 @@ pub const WORDS: &[Word] = &[ desc: "Linear ramp (curve=1)", example: "1.0 linramp", compile: Simple, + varargs: false, }, Word { name: "expramp", @@ -744,6 +823,7 @@ pub const WORDS: &[Word] = &[ desc: "Exponential ramp (curve=3)", example: "0.25 expramp", compile: Simple, + varargs: false, }, Word { name: "logramp", @@ -753,6 +833,7 @@ pub const WORDS: &[Word] = &[ desc: "Logarithmic ramp (curve=0.3)", example: "2.0 logramp", compile: Simple, + varargs: false, }, Word { name: "tri", @@ -762,6 +843,7 @@ pub const WORDS: &[Word] = &[ desc: "Triangle wave [0,1]: 0→1→0", example: "0.5 tri", compile: Simple, + varargs: false, }, Word { name: "perlin", @@ -771,6 +853,7 @@ pub const WORDS: &[Word] = &[ desc: "Perlin noise [0,1] sampled at freq*beat", example: "0.25 perlin", compile: Simple, + varargs: false, }, Word { name: "loop", @@ -780,6 +863,7 @@ pub const WORDS: &[Word] = &[ desc: "Fit sample to n beats", example: "\"break\" s 4 loop @", compile: Simple, + varargs: false, }, Word { name: "tempo!", @@ -789,6 +873,7 @@ pub const WORDS: &[Word] = &[ desc: "Set global tempo", example: "140 tempo!", compile: Simple, + varargs: false, }, Word { name: "speed!", @@ -798,6 +883,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pattern speed multiplier", example: "2.0 speed!", compile: Simple, + varargs: false, }, Word { name: "chain", @@ -807,15 +893,17 @@ pub const WORDS: &[Word] = &[ desc: "Chain to bank/pattern (1-indexed) when current pattern ends", example: "1 4 chain", compile: Simple, + varargs: false, }, Word { name: "at", aliases: &[], category: "Time", - stack: "(list|n --)", + stack: "(v1..vn n --)", desc: "Set delta context for emit timing", - example: "[ 0 0.5 ] at kick s . => emits at 0 and 0.5 of step", + example: "0 0.5 2 at kick s . => emits at 0 and 0.5 of step", compile: Simple, + varargs: true, }, // Quotations Word { @@ -826,6 +914,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation if true", example: "{ 2 distort } 0.5 chance ?", compile: Simple, + varargs: false, }, Word { name: "!?", @@ -835,6 +924,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation if false", example: "{ 1 distort } 0.5 chance !?", compile: Simple, + varargs: false, }, // Sample playback Word { @@ -845,6 +935,7 @@ pub const WORDS: &[Word] = &[ desc: "Set time offset", example: "0.1 time", compile: Param, + varargs: false, }, Word { name: "repeat", @@ -854,6 +945,7 @@ pub const WORDS: &[Word] = &[ desc: "Set repeat count", example: "4 repeat", compile: Param, + varargs: false, }, Word { name: "dur", @@ -863,6 +955,7 @@ pub const WORDS: &[Word] = &[ desc: "Set duration", example: "0.5 dur", compile: Param, + varargs: false, }, Word { name: "gate", @@ -872,6 +965,7 @@ pub const WORDS: &[Word] = &[ desc: "Set gate time", example: "0.8 gate", compile: Param, + varargs: false, }, Word { name: "freq", @@ -881,6 +975,7 @@ pub const WORDS: &[Word] = &[ desc: "Set frequency (Hz)", example: "440 freq", compile: Param, + varargs: false, }, Word { name: "detune", @@ -890,6 +985,7 @@ pub const WORDS: &[Word] = &[ desc: "Set detune amount", example: "0.01 detune", compile: Param, + varargs: false, }, Word { name: "speed", @@ -899,6 +995,7 @@ pub const WORDS: &[Word] = &[ desc: "Set playback speed", example: "1.5 speed", compile: Param, + varargs: false, }, Word { name: "glide", @@ -908,6 +1005,7 @@ pub const WORDS: &[Word] = &[ desc: "Set glide/portamento", example: "0.1 glide", compile: Param, + varargs: false, }, Word { name: "pw", @@ -917,6 +1015,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pulse width", example: "0.5 pw", compile: Param, + varargs: false, }, Word { name: "spread", @@ -926,6 +1025,7 @@ pub const WORDS: &[Word] = &[ desc: "Set stereo spread", example: "0.5 spread", compile: Param, + varargs: false, }, Word { name: "mult", @@ -935,6 +1035,7 @@ pub const WORDS: &[Word] = &[ desc: "Set multiplier", example: "2 mult", compile: Param, + varargs: false, }, Word { name: "warp", @@ -944,6 +1045,7 @@ pub const WORDS: &[Word] = &[ desc: "Set warp amount", example: "0.5 warp", compile: Param, + varargs: false, }, Word { name: "mirror", @@ -953,6 +1055,7 @@ pub const WORDS: &[Word] = &[ desc: "Set mirror", example: "1 mirror", compile: Param, + varargs: false, }, Word { name: "harmonics", @@ -962,6 +1065,7 @@ pub const WORDS: &[Word] = &[ desc: "Set harmonics (mutable only)", example: "4 harmonics", compile: Param, + varargs: false, }, Word { name: "timbre", @@ -971,6 +1075,7 @@ pub const WORDS: &[Word] = &[ desc: "Set timbre (mutable only)", example: "0.5 timbre", compile: Param, + varargs: false, }, Word { name: "morph", @@ -980,6 +1085,7 @@ pub const WORDS: &[Word] = &[ desc: "Set morph (mutable only)", example: "0.5 morph", compile: Param, + varargs: false, }, Word { name: "begin", @@ -989,6 +1095,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sample start (0-1)", example: "0.25 begin", compile: Param, + varargs: false, }, Word { name: "end", @@ -998,6 +1105,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sample end (0-1)", example: "0.75 end", compile: Param, + varargs: false, }, Word { name: "gain", @@ -1007,6 +1115,7 @@ pub const WORDS: &[Word] = &[ desc: "Set volume (0-1)", example: "0.8 gain", compile: Param, + varargs: false, }, Word { name: "postgain", @@ -1016,6 +1125,7 @@ pub const WORDS: &[Word] = &[ desc: "Set post gain", example: "1.2 postgain", compile: Param, + varargs: false, }, Word { name: "velocity", @@ -1025,6 +1135,7 @@ pub const WORDS: &[Word] = &[ desc: "Set velocity", example: "100 velocity", compile: Param, + varargs: false, }, Word { name: "pan", @@ -1034,6 +1145,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pan (-1 to 1)", example: "0.5 pan", compile: Param, + varargs: false, }, Word { name: "attack", @@ -1043,6 +1155,7 @@ pub const WORDS: &[Word] = &[ desc: "Set attack time", example: "0.01 attack", compile: Param, + varargs: false, }, Word { name: "decay", @@ -1052,6 +1165,7 @@ pub const WORDS: &[Word] = &[ desc: "Set decay time", example: "0.1 decay", compile: Param, + varargs: false, }, Word { name: "sustain", @@ -1061,6 +1175,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sustain level", example: "0.5 sustain", compile: Param, + varargs: false, }, Word { name: "release", @@ -1070,6 +1185,7 @@ pub const WORDS: &[Word] = &[ desc: "Set release time", example: "0.3 release", compile: Param, + varargs: false, }, Word { name: "adsr", @@ -1079,6 +1195,7 @@ pub const WORDS: &[Word] = &[ desc: "Set attack, decay, sustain, release", example: "0.01 0.1 0.5 0.3 adsr", compile: Simple, + varargs: false, }, Word { name: "ad", @@ -1088,6 +1205,7 @@ pub const WORDS: &[Word] = &[ desc: "Set attack, decay (sustain=0)", example: "0.01 0.1 ad", compile: Simple, + varargs: false, }, Word { name: "lpf", @@ -1097,6 +1215,7 @@ pub const WORDS: &[Word] = &[ desc: "Set lowpass frequency", example: "2000 lpf", compile: Param, + varargs: false, }, Word { name: "lpq", @@ -1106,6 +1225,7 @@ pub const WORDS: &[Word] = &[ desc: "Set lowpass resonance", example: "0.5 lpq", compile: Param, + varargs: false, }, Word { name: "lpe", @@ -1115,6 +1235,7 @@ pub const WORDS: &[Word] = &[ desc: "Set lowpass envelope", example: "0.5 lpe", compile: Param, + varargs: false, }, Word { name: "lpa", @@ -1124,6 +1245,7 @@ pub const WORDS: &[Word] = &[ desc: "Set lowpass attack", example: "0.01 lpa", compile: Param, + varargs: false, }, Word { name: "lpd", @@ -1133,6 +1255,7 @@ pub const WORDS: &[Word] = &[ desc: "Set lowpass decay", example: "0.1 lpd", compile: Param, + varargs: false, }, Word { name: "lps", @@ -1142,6 +1265,7 @@ pub const WORDS: &[Word] = &[ desc: "Set lowpass sustain", example: "0.5 lps", compile: Param, + varargs: false, }, Word { name: "lpr", @@ -1151,6 +1275,7 @@ pub const WORDS: &[Word] = &[ desc: "Set lowpass release", example: "0.3 lpr", compile: Param, + varargs: false, }, Word { name: "hpf", @@ -1160,6 +1285,7 @@ pub const WORDS: &[Word] = &[ desc: "Set highpass frequency", example: "100 hpf", compile: Param, + varargs: false, }, Word { name: "hpq", @@ -1169,6 +1295,7 @@ pub const WORDS: &[Word] = &[ desc: "Set highpass resonance", example: "0.5 hpq", compile: Param, + varargs: false, }, Word { name: "hpe", @@ -1178,6 +1305,7 @@ pub const WORDS: &[Word] = &[ desc: "Set highpass envelope", example: "0.5 hpe", compile: Param, + varargs: false, }, Word { name: "hpa", @@ -1187,6 +1315,7 @@ pub const WORDS: &[Word] = &[ desc: "Set highpass attack", example: "0.01 hpa", compile: Param, + varargs: false, }, Word { name: "hpd", @@ -1196,6 +1325,7 @@ pub const WORDS: &[Word] = &[ desc: "Set highpass decay", example: "0.1 hpd", compile: Param, + varargs: false, }, Word { name: "hps", @@ -1205,6 +1335,7 @@ pub const WORDS: &[Word] = &[ desc: "Set highpass sustain", example: "0.5 hps", compile: Param, + varargs: false, }, Word { name: "hpr", @@ -1214,6 +1345,7 @@ pub const WORDS: &[Word] = &[ desc: "Set highpass release", example: "0.3 hpr", compile: Param, + varargs: false, }, Word { name: "bpf", @@ -1223,6 +1355,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bandpass frequency", example: "1000 bpf", compile: Param, + varargs: false, }, Word { name: "bpq", @@ -1232,6 +1365,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bandpass resonance", example: "0.5 bpq", compile: Param, + varargs: false, }, Word { name: "bpe", @@ -1241,6 +1375,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bandpass envelope", example: "0.5 bpe", compile: Param, + varargs: false, }, Word { name: "bpa", @@ -1250,6 +1385,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bandpass attack", example: "0.01 bpa", compile: Param, + varargs: false, }, Word { name: "bpd", @@ -1259,6 +1395,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bandpass decay", example: "0.1 bpd", compile: Param, + varargs: false, }, Word { name: "bps", @@ -1268,6 +1405,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bandpass sustain", example: "0.5 bps", compile: Param, + varargs: false, }, Word { name: "bpr", @@ -1277,6 +1415,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bandpass release", example: "0.3 bpr", compile: Param, + varargs: false, }, Word { name: "llpf", @@ -1286,6 +1425,7 @@ pub const WORDS: &[Word] = &[ desc: "Set ladder lowpass frequency", example: "2000 llpf", compile: Param, + varargs: false, }, Word { name: "llpq", @@ -1295,6 +1435,7 @@ pub const WORDS: &[Word] = &[ desc: "Set ladder lowpass resonance", example: "0.5 llpq", compile: Param, + varargs: false, }, Word { name: "lhpf", @@ -1304,6 +1445,7 @@ pub const WORDS: &[Word] = &[ desc: "Set ladder highpass frequency", example: "100 lhpf", compile: Param, + varargs: false, }, Word { name: "lhpq", @@ -1313,6 +1455,7 @@ pub const WORDS: &[Word] = &[ desc: "Set ladder highpass resonance", example: "0.5 lhpq", compile: Param, + varargs: false, }, Word { name: "lbpf", @@ -1322,6 +1465,7 @@ pub const WORDS: &[Word] = &[ desc: "Set ladder bandpass frequency", example: "1000 lbpf", compile: Param, + varargs: false, }, Word { name: "lbpq", @@ -1331,6 +1475,7 @@ pub const WORDS: &[Word] = &[ desc: "Set ladder bandpass resonance", example: "0.5 lbpq", compile: Param, + varargs: false, }, Word { name: "ftype", @@ -1340,6 +1485,7 @@ pub const WORDS: &[Word] = &[ desc: "Set filter type", example: "1 ftype", compile: Param, + varargs: false, }, Word { name: "penv", @@ -1349,6 +1495,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pitch envelope", example: "0.5 penv", compile: Param, + varargs: false, }, Word { name: "patt", @@ -1358,6 +1505,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pitch attack", example: "0.01 patt", compile: Param, + varargs: false, }, Word { name: "pdec", @@ -1367,6 +1515,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pitch decay", example: "0.1 pdec", compile: Param, + varargs: false, }, Word { name: "psus", @@ -1376,6 +1525,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pitch sustain", example: "0 psus", compile: Param, + varargs: false, }, Word { name: "prel", @@ -1385,6 +1535,7 @@ pub const WORDS: &[Word] = &[ desc: "Set pitch release", example: "0.1 prel", compile: Param, + varargs: false, }, Word { name: "vib", @@ -1394,6 +1545,7 @@ pub const WORDS: &[Word] = &[ desc: "Set vibrato rate", example: "5 vib", compile: Param, + varargs: false, }, Word { name: "vibmod", @@ -1403,6 +1555,7 @@ pub const WORDS: &[Word] = &[ desc: "Set vibrato depth", example: "0.5 vibmod", compile: Param, + varargs: false, }, Word { name: "vibshape", @@ -1412,6 +1565,7 @@ pub const WORDS: &[Word] = &[ desc: "Set vibrato shape", example: "0 vibshape", compile: Param, + varargs: false, }, Word { name: "fm", @@ -1421,6 +1575,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM frequency", example: "200 fm", compile: Param, + varargs: false, }, Word { name: "fmh", @@ -1430,6 +1585,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM harmonic ratio", example: "2 fmh", compile: Param, + varargs: false, }, Word { name: "fmshape", @@ -1439,6 +1595,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM shape", example: "0 fmshape", compile: Param, + varargs: false, }, Word { name: "fme", @@ -1448,6 +1605,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM envelope", example: "0.5 fme", compile: Param, + varargs: false, }, Word { name: "fma", @@ -1457,6 +1615,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM attack", example: "0.01 fma", compile: Param, + varargs: false, }, Word { name: "fmd", @@ -1466,6 +1625,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM decay", example: "0.1 fmd", compile: Param, + varargs: false, }, Word { name: "fms", @@ -1475,6 +1635,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM sustain", example: "0.5 fms", compile: Param, + varargs: false, }, Word { name: "fmr", @@ -1484,6 +1645,7 @@ pub const WORDS: &[Word] = &[ desc: "Set FM release", example: "0.1 fmr", compile: Param, + varargs: false, }, Word { name: "am", @@ -1493,6 +1655,7 @@ pub const WORDS: &[Word] = &[ desc: "Set AM frequency", example: "10 am", compile: Param, + varargs: false, }, Word { name: "amdepth", @@ -1502,6 +1665,7 @@ pub const WORDS: &[Word] = &[ desc: "Set AM depth", example: "0.5 amdepth", compile: Param, + varargs: false, }, Word { name: "amshape", @@ -1511,6 +1675,7 @@ pub const WORDS: &[Word] = &[ desc: "Set AM shape", example: "0 amshape", compile: Param, + varargs: false, }, Word { name: "rm", @@ -1520,6 +1685,7 @@ pub const WORDS: &[Word] = &[ desc: "Set RM frequency", example: "100 rm", compile: Param, + varargs: false, }, Word { name: "rmdepth", @@ -1529,6 +1695,7 @@ pub const WORDS: &[Word] = &[ desc: "Set RM depth", example: "0.5 rmdepth", compile: Param, + varargs: false, }, Word { name: "rmshape", @@ -1538,6 +1705,7 @@ pub const WORDS: &[Word] = &[ desc: "Set RM shape", example: "0 rmshape", compile: Param, + varargs: false, }, Word { name: "phaser", @@ -1547,6 +1715,7 @@ pub const WORDS: &[Word] = &[ desc: "Set phaser rate", example: "1 phaser", compile: Param, + varargs: false, }, Word { name: "phaserdepth", @@ -1556,6 +1725,7 @@ pub const WORDS: &[Word] = &[ desc: "Set phaser depth", example: "0.5 phaserdepth", compile: Param, + varargs: false, }, Word { name: "phasersweep", @@ -1565,6 +1735,7 @@ pub const WORDS: &[Word] = &[ desc: "Set phaser sweep", example: "0.5 phasersweep", compile: Param, + varargs: false, }, Word { name: "phasercenter", @@ -1574,6 +1745,7 @@ pub const WORDS: &[Word] = &[ desc: "Set phaser center", example: "1000 phasercenter", compile: Param, + varargs: false, }, Word { name: "flanger", @@ -1583,6 +1755,7 @@ pub const WORDS: &[Word] = &[ desc: "Set flanger rate", example: "0.5 flanger", compile: Param, + varargs: false, }, Word { name: "flangerdepth", @@ -1592,6 +1765,7 @@ pub const WORDS: &[Word] = &[ desc: "Set flanger depth", example: "0.5 flangerdepth", compile: Param, + varargs: false, }, Word { name: "flangerfeedback", @@ -1601,6 +1775,7 @@ pub const WORDS: &[Word] = &[ desc: "Set flanger feedback", example: "0.5 flangerfeedback", compile: Param, + varargs: false, }, Word { name: "chorus", @@ -1610,6 +1785,7 @@ pub const WORDS: &[Word] = &[ desc: "Set chorus rate", example: "1 chorus", compile: Param, + varargs: false, }, Word { name: "chorusdepth", @@ -1619,6 +1795,7 @@ pub const WORDS: &[Word] = &[ desc: "Set chorus depth", example: "0.5 chorusdepth", compile: Param, + varargs: false, }, Word { name: "chorusdelay", @@ -1628,6 +1805,7 @@ pub const WORDS: &[Word] = &[ desc: "Set chorus delay", example: "0.02 chorusdelay", compile: Param, + varargs: false, }, Word { name: "eqlo", @@ -1637,6 +1815,7 @@ pub const WORDS: &[Word] = &[ desc: "Set low shelf gain (dB)", example: "3 eqlo", compile: Param, + varargs: false, }, Word { name: "eqmid", @@ -1646,6 +1825,7 @@ pub const WORDS: &[Word] = &[ desc: "Set mid peak gain (dB)", example: "-2 eqmid", compile: Param, + varargs: false, }, Word { name: "eqhi", @@ -1655,6 +1835,7 @@ pub const WORDS: &[Word] = &[ desc: "Set high shelf gain (dB)", example: "1 eqhi", compile: Param, + varargs: false, }, Word { name: "tilt", @@ -1664,6 +1845,7 @@ pub const WORDS: &[Word] = &[ desc: "Set tilt EQ (-1 dark, 1 bright)", example: "-0.5 tilt", compile: Param, + varargs: false, }, Word { name: "width", @@ -1673,6 +1855,7 @@ pub const WORDS: &[Word] = &[ desc: "Set stereo width (0 mono, 1 normal, 2 wide)", example: "0 width", compile: Param, + varargs: false, }, Word { name: "haas", @@ -1682,6 +1865,7 @@ pub const WORDS: &[Word] = &[ desc: "Set Haas delay in ms (spatial placement)", example: "8 haas", compile: Param, + varargs: false, }, Word { name: "comb", @@ -1691,6 +1875,7 @@ pub const WORDS: &[Word] = &[ desc: "Set comb filter mix", example: "0.5 comb", compile: Param, + varargs: false, }, Word { name: "combfreq", @@ -1700,6 +1885,7 @@ pub const WORDS: &[Word] = &[ desc: "Set comb frequency", example: "200 combfreq", compile: Param, + varargs: false, }, Word { name: "combfeedback", @@ -1709,6 +1895,7 @@ pub const WORDS: &[Word] = &[ desc: "Set comb feedback", example: "0.5 combfeedback", compile: Param, + varargs: false, }, Word { name: "combdamp", @@ -1718,6 +1905,7 @@ pub const WORDS: &[Word] = &[ desc: "Set comb damping", example: "0.5 combdamp", compile: Param, + varargs: false, }, Word { name: "coarse", @@ -1727,6 +1915,7 @@ pub const WORDS: &[Word] = &[ desc: "Set coarse tune", example: "12 coarse", compile: Param, + varargs: false, }, Word { name: "crush", @@ -1736,6 +1925,7 @@ pub const WORDS: &[Word] = &[ desc: "Set bit crush", example: "8 crush", compile: Param, + varargs: false, }, Word { name: "sub", @@ -1745,6 +1935,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sub oscillator level", example: "0.5 sub", compile: Param, + varargs: false, }, Word { name: "suboct", @@ -1754,6 +1945,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sub oscillator octave", example: "2 suboct", compile: Param, + varargs: false, }, Word { name: "subwave", @@ -1763,6 +1955,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sub oscillator waveform", example: "1 subwave", compile: Param, + varargs: false, }, Word { name: "fold", @@ -1772,6 +1965,7 @@ pub const WORDS: &[Word] = &[ desc: "Set wave fold", example: "2 fold", compile: Param, + varargs: false, }, Word { name: "wrap", @@ -1781,6 +1975,7 @@ pub const WORDS: &[Word] = &[ desc: "Set wave wrap", example: "0.5 wrap", compile: Param, + varargs: false, }, Word { name: "distort", @@ -1790,6 +1985,7 @@ pub const WORDS: &[Word] = &[ desc: "Set distortion", example: "0.5 distort", compile: Param, + varargs: false, }, Word { name: "distortvol", @@ -1799,6 +1995,7 @@ pub const WORDS: &[Word] = &[ desc: "Set distortion volume", example: "0.8 distortvol", compile: Param, + varargs: false, }, Word { name: "delay", @@ -1808,6 +2005,7 @@ pub const WORDS: &[Word] = &[ desc: "Set delay mix", example: "0.3 delay", compile: Param, + varargs: false, }, Word { name: "delaytime", @@ -1817,6 +2015,7 @@ pub const WORDS: &[Word] = &[ desc: "Set delay time", example: "0.25 delaytime", compile: Param, + varargs: false, }, Word { name: "delayfeedback", @@ -1826,6 +2025,7 @@ pub const WORDS: &[Word] = &[ desc: "Set delay feedback", example: "0.5 delayfeedback", compile: Param, + varargs: false, }, Word { name: "delaytype", @@ -1835,6 +2035,7 @@ pub const WORDS: &[Word] = &[ desc: "Set delay type", example: "1 delaytype", compile: Param, + varargs: false, }, Word { name: "verb", @@ -1844,6 +2045,7 @@ pub const WORDS: &[Word] = &[ desc: "Set reverb mix", example: "0.3 verb", compile: Param, + varargs: false, }, Word { name: "verbdecay", @@ -1853,6 +2055,7 @@ pub const WORDS: &[Word] = &[ desc: "Set reverb decay", example: "2 verbdecay", compile: Param, + varargs: false, }, Word { name: "verbdamp", @@ -1862,6 +2065,7 @@ pub const WORDS: &[Word] = &[ desc: "Set reverb damping", example: "0.5 verbdamp", compile: Param, + varargs: false, }, Word { name: "verbpredelay", @@ -1871,6 +2075,7 @@ pub const WORDS: &[Word] = &[ desc: "Set reverb predelay", example: "0.02 verbpredelay", compile: Param, + varargs: false, }, Word { name: "verbdiff", @@ -1880,6 +2085,7 @@ pub const WORDS: &[Word] = &[ desc: "Set reverb diffusion", example: "0.7 verbdiff", compile: Param, + varargs: false, }, Word { name: "voice", @@ -1889,6 +2095,7 @@ pub const WORDS: &[Word] = &[ desc: "Set voice number", example: "1 voice", compile: Param, + varargs: false, }, Word { name: "orbit", @@ -1898,6 +2105,7 @@ pub const WORDS: &[Word] = &[ desc: "Set orbit/bus", example: "0 orbit", compile: Param, + varargs: false, }, Word { name: "note", @@ -1907,6 +2115,7 @@ pub const WORDS: &[Word] = &[ desc: "Set MIDI note", example: "60 note", compile: Param, + varargs: false, }, Word { name: "size", @@ -1916,6 +2125,7 @@ pub const WORDS: &[Word] = &[ desc: "Set size", example: "1 size", compile: Param, + varargs: false, }, Word { name: "n", @@ -1925,6 +2135,7 @@ pub const WORDS: &[Word] = &[ desc: "Set sample number", example: "0 n", compile: Param, + varargs: false, }, Word { name: "cut", @@ -1934,6 +2145,7 @@ pub const WORDS: &[Word] = &[ desc: "Set cut group", example: "1 cut", compile: Param, + varargs: false, }, Word { name: "reset", @@ -1943,6 +2155,7 @@ pub const WORDS: &[Word] = &[ desc: "Reset parameter", example: "1 reset", compile: Param, + varargs: false, }, Word { name: "clear", @@ -1952,6 +2165,7 @@ pub const WORDS: &[Word] = &[ desc: "Clear sound register (sound and all params)", example: "\"kick\" s 0.5 gain . clear \"hat\" s .", compile: Simple, + varargs: false, }, // Quotation execution Word { @@ -1962,6 +2176,7 @@ pub const WORDS: &[Word] = &[ desc: "Execute quotation unconditionally", example: "{ 2 * } apply", compile: Simple, + varargs: false, }, // Word definitions Word { @@ -1972,6 +2187,7 @@ pub const WORDS: &[Word] = &[ desc: "Begin word definition", example: ": kick \"kick\" s emit ;", compile: Simple, + varargs: false, }, Word { name: ";", @@ -1981,6 +2197,38 @@ pub const WORDS: &[Word] = &[ desc: "End word definition", example: ": kick \"kick\" s emit ;", compile: Simple, + varargs: false, + }, + // Generator + Word { + name: "..", + aliases: &[], + category: "Generator", + stack: "(start end -- start start+1 ... end)", + desc: "Push arithmetic sequence from start to end", + example: "1 4 .. => 1 2 3 4", + compile: Simple, + varargs: false, + }, + Word { + name: "gen", + aliases: &[], + category: "Generator", + stack: "(quot n -- results...)", + desc: "Execute quotation n times, push all results", + example: "{ 1 6 rand } 4 gen => 4 random values", + compile: Simple, + varargs: true, + }, + Word { + name: "geom..", + aliases: &[], + category: "Generator", + stack: "(start ratio count -- start start*r start*r^2 ...)", + desc: "Push geometric sequence", + example: "1 2 4 geom.. => 1 2 4 8", + compile: Simple, + varargs: false, }, ]; @@ -2056,6 +2304,9 @@ pub(super) fn simple_op(name: &str) -> Option { "oct" => Op::Oct, ".!" => Op::EmitN, "clear" => Op::ClearCmd, + ".." => Op::IntRange, + "gen" => Op::Generate, + "geom.." => Op::GeomRange, _ => return None, }) } diff --git a/crates/ratatui/src/scope.rs b/crates/ratatui/src/scope.rs index 93140ea..712d44f 100644 --- a/crates/ratatui/src/scope.rs +++ b/crates/ratatui/src/scope.rs @@ -2,6 +2,11 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Color; use ratatui::widgets::Widget; +use std::cell::RefCell; + +thread_local! { + static PATTERNS: RefCell> = const { RefCell::new(Vec::new()) }; +} #[allow(dead_code)] pub enum Orientation { @@ -58,46 +63,51 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g let fine_width = width * 2; let fine_height = height * 4; - let mut patterns = vec![0u8; width * height]; + PATTERNS.with(|p| { + let mut patterns = p.borrow_mut(); + let size = width * height; + patterns.clear(); + patterns.resize(size, 0); - for fine_x in 0..fine_width { - let sample_idx = (fine_x * data.len()) / fine_width; - let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0); + for fine_x in 0..fine_width { + let sample_idx = (fine_x * data.len()) / fine_width; + let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0); - let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize; - let fine_y = fine_y.min(fine_height - 1); + let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize; + let fine_y = fine_y.min(fine_height - 1); - let char_x = fine_x / 2; - let char_y = fine_y / 4; - let dot_x = fine_x % 2; - let dot_y = fine_y % 4; + let char_x = fine_x / 2; + let char_y = fine_y / 4; + let dot_x = fine_x % 2; + let dot_y = fine_y % 4; - let bit = match (dot_x, dot_y) { - (0, 0) => 0x01, - (0, 1) => 0x02, - (0, 2) => 0x04, - (0, 3) => 0x40, - (1, 0) => 0x08, - (1, 1) => 0x10, - (1, 2) => 0x20, - (1, 3) => 0x80, - _ => unreachable!(), - }; + let bit = match (dot_x, dot_y) { + (0, 0) => 0x01, + (0, 1) => 0x02, + (0, 2) => 0x04, + (0, 3) => 0x40, + (1, 0) => 0x08, + (1, 1) => 0x10, + (1, 2) => 0x20, + (1, 3) => 0x80, + _ => unreachable!(), + }; - patterns[char_y * width + char_x] |= bit; - } + patterns[char_y * width + char_x] |= bit; + } - for cy in 0..height { - for cx in 0..width { - let pattern = patterns[cy * width + cx]; - if pattern != 0 { - let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' '); - buf[(area.x + cx as u16, area.y + cy as u16)] - .set_char(ch) - .set_fg(color); + for cy in 0..height { + for cx in 0..width { + let pattern = patterns[cy * width + cx]; + if pattern != 0 { + let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' '); + buf[(area.x + cx as u16, area.y + cy as u16)] + .set_char(ch) + .set_fg(color); + } } } - } + }); } fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) { @@ -106,44 +116,49 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai let fine_width = width * 2; let fine_height = height * 4; - let mut patterns = vec![0u8; width * height]; + PATTERNS.with(|p| { + let mut patterns = p.borrow_mut(); + let size = width * height; + patterns.clear(); + patterns.resize(size, 0); - for fine_y in 0..fine_height { - let sample_idx = (fine_y * data.len()) / fine_height; - let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0); + for fine_y in 0..fine_height { + let sample_idx = (fine_y * data.len()) / fine_height; + let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0); - let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize; - let fine_x = fine_x.min(fine_width - 1); + let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize; + let fine_x = fine_x.min(fine_width - 1); - let char_x = fine_x / 2; - let char_y = fine_y / 4; - let dot_x = fine_x % 2; - let dot_y = fine_y % 4; + let char_x = fine_x / 2; + let char_y = fine_y / 4; + let dot_x = fine_x % 2; + let dot_y = fine_y % 4; - let bit = match (dot_x, dot_y) { - (0, 0) => 0x01, - (0, 1) => 0x02, - (0, 2) => 0x04, - (0, 3) => 0x40, - (1, 0) => 0x08, - (1, 1) => 0x10, - (1, 2) => 0x20, - (1, 3) => 0x80, - _ => unreachable!(), - }; + let bit = match (dot_x, dot_y) { + (0, 0) => 0x01, + (0, 1) => 0x02, + (0, 2) => 0x04, + (0, 3) => 0x40, + (1, 0) => 0x08, + (1, 1) => 0x10, + (1, 2) => 0x20, + (1, 3) => 0x80, + _ => unreachable!(), + }; - patterns[char_y * width + char_x] |= bit; - } + patterns[char_y * width + char_x] |= bit; + } - for cy in 0..height { - for cx in 0..width { - let pattern = patterns[cy * width + cx]; - if pattern != 0 { - let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' '); - buf[(area.x + cx as u16, area.y + cy as u16)] - .set_char(ch) - .set_fg(color); + for cy in 0..height { + for cx in 0..width { + let pattern = patterns[cy * width + cx]; + if pattern != 0 { + let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' '); + buf[(area.x + cx as u16, area.y + cy as u16)] + .set_char(ch) + .set_fg(color); + } } } - } + }); } diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 7afe59b..0e921ee 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -85,6 +85,7 @@ struct SpectrumAnalyzer { fft: Arc>, window: [f32; FFT_SIZE], scratch: Vec>, + fft_buf: Vec>, band_edges: [usize; NUM_BANDS + 1], } @@ -114,6 +115,7 @@ impl SpectrumAnalyzer { fft, window, scratch: vec![Complex::default(); scratch_len], + fft_buf: vec![Complex::default(); FFT_SIZE], band_edges, } } @@ -130,20 +132,19 @@ impl SpectrumAnalyzer { } fn run_fft(&mut self, output: &SpectrumBuffer) { - let mut buf: Vec> = (0..FFT_SIZE) - .map(|i| { - let idx = (self.pos + i) % FFT_SIZE; - Complex::new(self.ring[idx] * self.window[i], 0.0) - }) - .collect(); + for i in 0..FFT_SIZE { + let idx = (self.pos + i) % FFT_SIZE; + self.fft_buf[i] = Complex::new(self.ring[idx] * self.window[i], 0.0); + } - self.fft.process_with_scratch(&mut buf, &mut self.scratch); + self.fft + .process_with_scratch(&mut self.fft_buf, &mut self.scratch); let mut bands = [0.0f32; NUM_BANDS]; for (band, mag) in bands.iter_mut().enumerate() { let lo = self.band_edges[band]; let hi = self.band_edges[band + 1].max(lo + 1); - let sum: f32 = buf[lo..hi].iter().map(|c| c.norm()).sum(); + let sum: f32 = self.fft_buf[lo..hi].iter().map(|c| c.norm()).sum(); let avg = sum / (hi - lo) as f32; let amplitude = avg / (FFT_SIZE as f32 / 2.0); let db = 20.0 * amplitude.max(1e-10).log10(); diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index dbc100b..a3275b9 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -93,17 +93,19 @@ pub struct ActivePatternState { pub iter: usize, } +pub type StepTracesMap = HashMap<(usize, usize, usize), ExecutionTrace>; + #[derive(Clone, Default)] pub struct SharedSequencerState { pub active_patterns: Vec, - pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>, + pub step_traces: Arc, pub event_count: usize, pub dropped_events: usize, } pub struct SequencerSnapshot { pub active_patterns: Vec, - pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>, + step_traces: Arc, pub event_count: usize, pub dropped_events: usize, } @@ -146,7 +148,7 @@ impl SequencerHandle { let state = self.shared_state.load(); SequencerSnapshot { active_patterns: state.active_patterns.clone(), - step_traces: state.step_traces.clone(), + step_traces: Arc::clone(&state.step_traces), event_count: state.event_count, dropped_events: state.dropped_events, } @@ -366,7 +368,6 @@ pub(crate) struct TickOutput { } struct StepResult { - audio_commands: Vec, completed_iterations: Vec, any_step_fired: bool, } @@ -384,15 +385,44 @@ fn parse_chain_target(s: &str) -> Option { }) } +struct KeyCache { + speed_keys: [[String; MAX_PATTERNS]; MAX_BANKS], + chain_keys: [[String; MAX_PATTERNS]; MAX_BANKS], +} + +impl KeyCache { + fn new() -> Self { + Self { + speed_keys: std::array::from_fn(|bank| { + std::array::from_fn(|pattern| format!("__speed_{bank}_{pattern}__")) + }), + chain_keys: std::array::from_fn(|bank| { + std::array::from_fn(|pattern| format!("__chain_{bank}_{pattern}__")) + }), + } + } + + fn speed_key(&self, bank: usize, pattern: usize) -> &str { + &self.speed_keys[bank][pattern] + } + + fn chain_key(&self, bank: usize, pattern: usize) -> &str { + &self.chain_keys[bank][pattern] + } +} + pub(crate) struct SequencerState { audio_state: AudioState, pattern_cache: PatternCache, runs_counter: RunsCounter, - step_traces: HashMap<(usize, usize, usize), ExecutionTrace>, + step_traces: Arc, event_count: usize, dropped_events: usize, script_engine: ScriptEngine, variables: Variables, + speed_overrides: HashMap<(usize, usize), f64>, + key_cache: KeyCache, + buf_audio_commands: Vec, } impl SequencerState { @@ -406,11 +436,14 @@ impl SequencerState { audio_state: AudioState::new(), pattern_cache: PatternCache::new(), runs_counter: RunsCounter::new(), - step_traces: HashMap::new(), + step_traces: Arc::new(HashMap::new()), event_count: 0, dropped_events: 0, script_engine, variables, + speed_overrides: HashMap::new(), + key_cache: KeyCache::new(), + buf_audio_commands: Vec::new(), } } @@ -459,7 +492,7 @@ impl SequencerState { self.audio_state.active_patterns.clear(); self.audio_state.pending_starts.clear(); self.audio_state.pending_stops.clear(); - self.step_traces.clear(); + Arc::make_mut(&mut self.step_traces).clear(); self.runs_counter.counts.clear(); } SeqCommand::Shutdown => {} @@ -491,7 +524,7 @@ impl SequencerState { self.audio_state.prev_beat = beat; TickOutput { - audio_commands: steps.audio_commands, + audio_commands: std::mem::take(&mut self.buf_audio_commands), new_tempo: vars.new_tempo, shared_state: self.build_shared_state(), } @@ -500,13 +533,14 @@ impl SequencerState { fn tick_paused(&mut self) -> TickOutput { for pending in self.audio_state.pending_stops.drain(..) { self.audio_state.active_patterns.remove(&pending.id); - self.step_traces.retain(|&(bank, pattern, _), _| { + Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { bank != pending.id.bank || pattern != pending.id.pattern }); } self.audio_state.pending_starts.clear(); + self.buf_audio_commands.clear(); TickOutput { - audio_commands: Vec::new(), + audio_commands: std::mem::take(&mut self.buf_audio_commands), new_tempo: None, shared_state: self.build_shared_state(), } @@ -547,7 +581,7 @@ impl SequencerState { for pending in &self.audio_state.pending_stops { if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) { self.audio_state.active_patterns.remove(&pending.id); - self.step_traces.retain(|&(bank, pattern, _), _| { + Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { bank != pending.id.bank || pattern != pending.id.pattern }); stopped.push(pending.id); @@ -565,32 +599,29 @@ impl SequencerState { fill: bool, nudge_secs: f64, ) -> StepResult { + self.buf_audio_commands.clear(); let mut result = StepResult { - audio_commands: Vec::new(), completed_iterations: Vec::new(), any_step_fired: false, }; - let speed_overrides: HashMap<(usize, usize), f64> = { + self.speed_overrides.clear(); + { let vars = self.variables.lock().unwrap(); - self.audio_state - .active_patterns - .keys() - .filter_map(|id| { - let key = format!("__speed_{}_{}__", id.bank, id.pattern); - vars.get(&key) - .and_then(|v| v.as_float().ok()) - .map(|v| ((id.bank, id.pattern), v)) - }) - .collect() - }; + for id in self.audio_state.active_patterns.keys() { + let key = self.key_cache.speed_key(id.bank, id.pattern); + if let Some(v) = vars.get(key).and_then(|v| v.as_float().ok()) { + self.speed_overrides.insert((id.bank, id.pattern), v); + } + } + } for (_id, active) in self.audio_state.active_patterns.iter_mut() { let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else { continue; }; - let speed_mult = speed_overrides + let speed_mult = self.speed_overrides .get(&(active.bank, active.pattern)) .copied() .unwrap_or_else(|| pattern.speed.multiplier()); @@ -634,13 +665,13 @@ impl SequencerState { .script_engine .evaluate_with_trace(script, &ctx, &mut trace) { - self.step_traces.insert( + Arc::make_mut(&mut self.step_traces).insert( (active.bank, active.pattern, source_idx), std::mem::take(&mut trace), ); for cmd in cmds { self.event_count += 1; - result.audio_commands.push(cmd); + self.buf_audio_commands.push(cmd); } } } @@ -681,18 +712,18 @@ impl SequencerState { let mut chain_transitions = Vec::new(); for id in completed { - let chain_key = format!("__chain_{}_{}__", id.bank, id.pattern); - if let Some(Value::Str(s, _)) = vars.get(&chain_key) { + let chain_key = self.key_cache.chain_key(id.bank, id.pattern); + if let Some(Value::Str(s, _)) = vars.get(chain_key) { if let Some(target) = parse_chain_target(s) { chain_transitions.push((*id, target)); } } - vars.remove(&chain_key); + vars.remove(chain_key); } for id in stopped { - let chain_key = format!("__chain_{}_{}__", id.bank, id.pattern); - vars.remove(&chain_key); + let chain_key = self.key_cache.chain_key(id.bank, id.pattern); + vars.remove(chain_key); } VariableReads { @@ -738,7 +769,7 @@ impl SequencerState { iter: a.iter, }) .collect(), - step_traces: self.step_traces.clone(), + step_traces: Arc::clone(&self.step_traces), event_count: self.event_count, dropped_events: self.dropped_events, } diff --git a/src/input.rs b/src/input.rs index b9f38e7..62b841e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -476,7 +476,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { KeyCode::Char('p') if ctrl => { editor.search_prev(); } - KeyCode::Char('k') if ctrl => { + KeyCode::Char('s') if ctrl => { ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack; } KeyCode::Char('a') if ctrl => { diff --git a/src/state/editor.rs b/src/state/editor.rs index b9d26f0..125941b 100644 --- a/src/state/editor.rs +++ b/src/state/editor.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::ops::RangeInclusive; use cagire_ratatui::Editor; @@ -55,6 +56,14 @@ pub struct EditorContext { pub selection_anchor: Option, pub copied_steps: Option, pub show_stack: bool, + pub stack_cache: RefCell>, +} + +#[derive(Clone)] +pub struct StackCache { + pub cursor_line: usize, + pub lines_hash: u64, + pub result: String, } #[derive(Clone)] @@ -96,6 +105,7 @@ impl Default for EditorContext { selection_anchor: None, copied_steps: None, show_stack: false, + stack_cache: RefCell::new(None), } } } diff --git a/src/state/mod.rs b/src/state/mod.rs index d1f6a48..acc19e2 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -13,7 +13,7 @@ pub mod ui; pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind}; pub use options::{OptionsFocus, OptionsState}; -pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField}; +pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField, StackCache}; pub use live_keys::LiveKeyState; pub use modal::Modal; pub use panel::{PanelFocus, PanelState, SidePanel}; diff --git a/src/views/highlight.rs b/src/views/highlight.rs index d178c81..a6e6af3 100644 --- a/src/views/highlight.rs +++ b/src/views/highlight.rs @@ -16,39 +16,90 @@ pub enum TokenKind { Note, Interval, Variable, + Emit, + Vary, + Generator, Default, } impl TokenKind { pub fn style(self) -> Style { match self { - TokenKind::Number => Style::default().fg(Color::Rgb(255, 180, 100)), - TokenKind::String => Style::default().fg(Color::Rgb(150, 220, 150)), - TokenKind::Comment => Style::default().fg(Color::Rgb(100, 100, 100)), - TokenKind::Keyword => Style::default().fg(Color::Rgb(220, 120, 220)), - TokenKind::StackOp => Style::default().fg(Color::Rgb(120, 180, 220)), - TokenKind::Operator => Style::default().fg(Color::Rgb(200, 200, 130)), - TokenKind::Sound => Style::default().fg(Color::Rgb(100, 220, 200)), - TokenKind::Param => Style::default().fg(Color::Rgb(180, 150, 220)), - TokenKind::Context => Style::default().fg(Color::Rgb(220, 180, 120)), - TokenKind::Note => Style::default().fg(Color::Rgb(120, 200, 160)), - TokenKind::Interval => Style::default().fg(Color::Rgb(160, 200, 120)), - TokenKind::Variable => Style::default().fg(Color::Rgb(200, 140, 180)), - TokenKind::Default => Style::default().fg(Color::Rgb(200, 200, 200)), + TokenKind::Emit => Style::default() + .fg(Color::Rgb(255, 255, 255)) + .bg(Color::Rgb(140, 50, 50)) + .add_modifier(Modifier::BOLD), + TokenKind::Number => Style::default() + .fg(Color::Rgb(255, 200, 120)) + .bg(Color::Rgb(60, 40, 15)), + TokenKind::String => Style::default() + .fg(Color::Rgb(150, 230, 150)) + .bg(Color::Rgb(20, 55, 20)), + TokenKind::Comment => Style::default() + .fg(Color::Rgb(100, 100, 100)) + .bg(Color::Rgb(18, 18, 18)), + TokenKind::Keyword => Style::default() + .fg(Color::Rgb(230, 130, 230)) + .bg(Color::Rgb(55, 25, 55)), + TokenKind::StackOp => Style::default() + .fg(Color::Rgb(130, 190, 240)) + .bg(Color::Rgb(20, 40, 70)), + TokenKind::Operator => Style::default() + .fg(Color::Rgb(220, 220, 140)) + .bg(Color::Rgb(45, 45, 20)), + TokenKind::Sound => Style::default() + .fg(Color::Rgb(100, 240, 220)) + .bg(Color::Rgb(15, 60, 55)), + TokenKind::Param => Style::default() + .fg(Color::Rgb(190, 160, 240)) + .bg(Color::Rgb(45, 30, 70)), + TokenKind::Context => Style::default() + .fg(Color::Rgb(240, 190, 120)) + .bg(Color::Rgb(60, 45, 20)), + TokenKind::Note => Style::default() + .fg(Color::Rgb(120, 220, 170)) + .bg(Color::Rgb(20, 55, 40)), + TokenKind::Interval => Style::default() + .fg(Color::Rgb(170, 220, 120)) + .bg(Color::Rgb(35, 55, 20)), + TokenKind::Variable => Style::default() + .fg(Color::Rgb(220, 150, 190)) + .bg(Color::Rgb(60, 30, 50)), + TokenKind::Vary => Style::default() + .fg(Color::Rgb(230, 230, 100)) + .bg(Color::Rgb(55, 55, 15)), + TokenKind::Generator => Style::default() + .fg(Color::Rgb(100, 220, 180)) + .bg(Color::Rgb(15, 55, 45)), + TokenKind::Default => Style::default() + .fg(Color::Rgb(160, 160, 160)) + .bg(Color::Rgb(25, 25, 25)), } } + + pub fn gap_style() -> Style { + Style::default().bg(Color::Rgb(25, 25, 25)) + } } pub struct Token { pub start: usize, pub end: usize, pub kind: TokenKind, + pub varargs: bool, } -fn lookup_word_kind(word: &str) -> Option { +fn lookup_word_kind(word: &str) -> Option<(TokenKind, bool)> { + if word == "." { + return Some((TokenKind::Emit, false)); + } + if word == ".!" { + return Some((TokenKind::Emit, true)); + } + for w in WORDS { if w.name == word || w.aliases.contains(&word) { - return Some(match &w.compile { + let kind = match &w.compile { WordCompile::Param => TokenKind::Param, WordCompile::Context(_) => TokenKind::Context, _ => match w.category { @@ -58,9 +109,12 @@ fn lookup_word_kind(word: &str) -> Option { TokenKind::Operator } "Sound" => TokenKind::Sound, + "Randomness" | "Probability" | "Selection" => TokenKind::Vary, + "Generator" => TokenKind::Generator, _ => TokenKind::Keyword, }, - }); + }; + return Some((kind, w.varargs)); } } None @@ -98,11 +152,11 @@ pub fn tokenize_line(line: &str) -> Vec { } if c == ';' && chars.peek().map(|(_, ch)| *ch) == Some(';') { - // ;; starts a comment to end of line tokens.push(Token { start, end: line.len(), kind: TokenKind::Comment, + varargs: false, }); break; } @@ -119,6 +173,7 @@ pub fn tokenize_line(line: &str) -> Vec { start, end, kind: TokenKind::String, + varargs: false, }); continue; } @@ -133,35 +188,35 @@ pub fn tokenize_line(line: &str) -> Vec { } let word = &line[start..end]; - let kind = classify_word(word); - tokens.push(Token { start, end, kind }); + let (kind, varargs) = classify_word(word); + tokens.push(Token { start, end, kind, varargs }); } tokens } -fn classify_word(word: &str) -> TokenKind { +fn classify_word(word: &str) -> (TokenKind, bool) { if word.parse::().is_ok() || word.parse::().is_ok() { - return TokenKind::Number; + return (TokenKind::Number, false); } - if let Some(kind) = lookup_word_kind(word) { - return kind; + if let Some((kind, varargs)) = lookup_word_kind(word) { + return (kind, varargs); } if INTERVALS.contains(&word) { - return TokenKind::Interval; + return (TokenKind::Interval, false); } if is_note(&word.to_ascii_lowercase()) { - return TokenKind::Note; + return (TokenKind::Note, false); } if word.len() > 1 && (word.starts_with('@') || word.starts_with('!')) { - return TokenKind::Variable; + return (TokenKind::Variable, false); } - TokenKind::Default + (TokenKind::Default, false) } pub fn highlight_line(line: &str) -> Vec<(Style, String)> { @@ -179,13 +234,11 @@ pub fn highlight_line_with_runtime( let executed_bg = Color::Rgb(40, 35, 50); let selected_bg = Color::Rgb(80, 60, 20); + let gap_style = TokenKind::gap_style(); for token in tokens { if token.start > last_end { - result.push(( - TokenKind::Default.style(), - line[last_end..token.start].to_string(), - )); + result.push((gap_style, line[last_end..token.start].to_string())); } let is_selected = selected_spans @@ -196,6 +249,9 @@ pub fn highlight_line_with_runtime( .any(|span| overlaps(token.start, token.end, span.start, span.end)); let mut style = token.kind.style(); + if token.varargs { + style = style.add_modifier(Modifier::UNDERLINED); + } if is_selected { style = style.bg(selected_bg).add_modifier(Modifier::BOLD); } else if is_executed { @@ -207,7 +263,7 @@ pub fn highlight_line_with_runtime( } if last_end < line.len() { - result.push((TokenKind::Default.style(), line[last_end..].to_string())); + result.push((gap_style, line[last_end..].to_string())); } result diff --git a/src/views/render.rs b/src/views/render.rs index 1e91632..1162e17 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -1,4 +1,6 @@ +use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; +use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -15,7 +17,7 @@ use crate::app::App; use crate::engine::{LinkState, SequencerSnapshot}; use crate::model::{SourceSpan, StepContext, Value}; use crate::page::Page; -use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel}; +use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache}; use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime}; use crate::widgets::{ ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal, @@ -25,43 +27,67 @@ use super::{ dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view, }; -fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor) -> String { +fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cache: &std::cell::RefCell>) -> String { let cursor_line = editor.cursor().0; + + let mut hasher = DefaultHasher::new(); + for (i, line) in lines.iter().enumerate() { + if i > cursor_line { + break; + } + line.hash(&mut hasher); + } + let lines_hash = hasher.finish(); + + if let Some(ref c) = *cache.borrow() { + if c.cursor_line == cursor_line && c.lines_hash == lines_hash { + return c.result.clone(); + } + } + let partial: Vec<&str> = lines.iter().take(cursor_line + 1).map(|s| s.as_str()).collect(); let script = partial.join("\n"); - if script.trim().is_empty() { - return "Stack: []".to_string(); - } + let result = if script.trim().is_empty() { + "Stack: []".to_string() + } else { + let vars = Arc::new(Mutex::new(HashMap::new())); + let dict = Arc::new(Mutex::new(HashMap::new())); + let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42))); + let forth = Forth::new(vars, dict, rng); - let vars = Arc::new(Mutex::new(HashMap::new())); - let dict = Arc::new(Mutex::new(HashMap::new())); - let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42))); - let forth = Forth::new(vars, dict, rng); + let ctx = StepContext { + step: 0, + beat: 0.0, + bank: 0, + pattern: 0, + tempo: 120.0, + phase: 0.0, + slot: 0, + runs: 0, + iter: 0, + speed: 1.0, + fill: false, + nudge_secs: 0.0, + }; - let ctx = StepContext { - step: 0, - beat: 0.0, - bank: 0, - pattern: 0, - tempo: 120.0, - phase: 0.0, - slot: 0, - runs: 0, - iter: 0, - speed: 1.0, - fill: false, - nudge_secs: 0.0, + match forth.evaluate(&script, &ctx) { + Ok(_) => { + let stack = forth.stack(); + let formatted: Vec = stack.iter().map(format_value).collect(); + format!("Stack: [{}]", formatted.join(" ")) + } + Err(e) => format!("Error: {e}"), + } }; - match forth.evaluate(&script, &ctx) { - Ok(_) => { - let stack = forth.stack(); - let formatted: Vec = stack.iter().map(format_value).collect(); - format!("Stack: [{}]", formatted.join(" ")) - } - Err(e) => format!("Error: {e}"), - } + *cache.borrow_mut() = Some(StackCache { + cursor_line, + lines_hash, + result: result.clone(), + }); + + result } fn format_value(v: &Value) -> String { @@ -740,13 +766,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term ]); frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area); } else if app.editor_ctx.show_stack { - let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor); + let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor, &app.editor_ctx.stack_cache); let hint = Line::from(vec![ Span::styled("Esc", key), Span::styled(" save ", dim), Span::styled("C-e", key), Span::styled(" eval ", dim), - Span::styled("C-k", key), + Span::styled("C-s", key), Span::styled(" hide", dim), ]); let [hint_left, stack_right] = Layout::horizontal([ @@ -767,7 +793,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term Span::styled(" eval ", dim), Span::styled("C-f", key), Span::styled(" find ", dim), - Span::styled("C-k", key), + Span::styled("C-s", key), Span::styled(" stack ", dim), Span::styled("C-u", key), Span::styled("/", dim), diff --git a/tests/forth.rs b/tests/forth.rs index 36d7df1..91c9ccb 100644 --- a/tests/forth.rs +++ b/tests/forth.rs @@ -48,3 +48,6 @@ mod list_words; #[path = "forth/ramps.rs"] mod ramps; + +#[path = "forth/generator.rs"] +mod generator; diff --git a/tests/forth/generator.rs b/tests/forth/generator.rs new file mode 100644 index 0000000..8dc3b97 --- /dev/null +++ b/tests/forth/generator.rs @@ -0,0 +1,104 @@ +use super::harness::*; +use cagire::forth::Value; + +fn int(n: i64) -> Value { + Value::Int(n, None) +} + +#[test] +fn range_ascending() { + expect_stack("1 4 ..", &[int(1), int(2), int(3), int(4)]); +} + +#[test] +fn range_descending() { + expect_stack("4 1 ..", &[int(4), int(3), int(2), int(1)]); +} + +#[test] +fn range_single() { + expect_stack("3 3 ..", &[int(3)]); +} + +#[test] +fn range_negative() { + expect_stack("-2 1 ..", &[int(-2), int(-1), int(0), int(1)]); +} + +#[test] +fn range_underflow() { + expect_error("1 ..", "stack underflow"); + expect_error("..", "stack underflow"); +} + +#[test] +fn gen_basic() { + expect_stack("{ 42 } 3 gen", &[int(42), int(42), int(42)]); +} + +#[test] +fn gen_with_computation() { + // Each iteration: dup current value, add 1, result is new value + // 0 → dup(0,0) 1+(0,1) → pop 1, stack [0] + // 0 → dup(0,0) 1+(0,1) → pop 1, stack [0] + // So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s + expect_stack("0 { dup 1 + } 3 gen", &[int(0), int(1), int(1), int(1)]); +} + +#[test] +fn gen_chained() { + // Start with 1, each iteration: dup, multiply by 2 + // 1 → dup(1,1) 2*(1,2) → pop 2, stack [1] + // 1 → dup(1,1) 2*(1,2) → pop 2, stack [1] + expect_stack("1 { dup 2 * } 3 gen", &[int(1), int(2), int(2), int(2)]); +} + +#[test] +fn gen_zero() { + expect_stack("{ 1 } 0 gen", &[]); +} + +#[test] +fn gen_underflow() { + expect_error("3 gen", "stack underflow"); +} + +#[test] +fn gen_not_a_number() { + expect_error("{ 1 } gen", "expected number"); +} + +#[test] +fn gen_negative() { + expect_error("{ 1 } -1 gen", "gen count must be >= 0"); +} + +#[test] +fn gen_empty_quot_error() { + expect_error("{ } 3 gen", "quotation must produce"); +} + +#[test] +fn geom_growing() { + expect_stack("1 2 4 geom..", &[int(1), int(2), int(4), int(8)]); +} + +#[test] +fn geom_shrinking() { + expect_stack("8 0.5 4 geom..", &[int(8), int(4), int(2), int(1)]); +} + +#[test] +fn geom_single() { + expect_stack("5 2 1 geom..", &[int(5)]); +} + +#[test] +fn geom_zero_count() { + expect_stack("1 2 0 geom..", &[]); +} + +#[test] +fn geom_underflow() { + expect_error("1 2 geom..", "stack underflow"); +}