use rand::rngs::StdRng; use rand::{Rng as RngTrait, SeedableRng}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct SourceSpan { pub start: usize, pub end: usize, } #[derive(Clone, Debug, Default)] pub struct ExecutionTrace { pub selected_spans: Vec, } pub struct StepContext { pub step: usize, pub beat: f64, pub bank: usize, pub pattern: usize, pub tempo: f64, pub phase: f64, pub slot: usize, pub runs: usize, pub iter: usize, pub speed: f64, pub fill: bool, } impl StepContext { pub fn step_duration(&self) -> f64 { 60.0 / self.tempo / 4.0 / self.speed } } pub type Variables = Arc>>; pub type Rng = Arc>; #[derive(Clone, Debug)] pub enum Value { Int(i64, Option), Float(f64, Option), Str(String, Option), Marker, Quotation(Vec), } impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match (self, other) { (Value::Int(a, _), Value::Int(b, _)) => a == b, (Value::Float(a, _), Value::Float(b, _)) => a == b, (Value::Str(a, _), Value::Str(b, _)) => a == b, (Value::Marker, Value::Marker) => true, (Value::Quotation(a), Value::Quotation(b)) => a == b, _ => false, } } } #[derive(Clone, Debug, Default)] struct CmdRegister { sound: Option, params: Vec<(String, String)>, } impl CmdRegister { fn set_sound(&mut self, name: String) { self.sound = Some(name); } fn set_param(&mut self, key: String, value: String) { self.params.push((key, value)); } fn take(&mut self) -> Option<(String, Vec<(String, String)>)> { let sound = self.sound.take()?; let params = std::mem::take(&mut self.params); Some((sound, params)) } } impl Value { pub fn as_float(&self) -> Result { match self { Value::Float(f, _) => Ok(*f), Value::Int(i, _) => Ok(*i as f64), _ => Err("expected number".into()), } } fn as_int(&self) -> Result { match self { Value::Int(i, _) => Ok(*i), Value::Float(f, _) => Ok(*f as i64), _ => Err("expected number".into()), } } fn as_str(&self) -> Result<&str, String> { match self { Value::Str(s, _) => Ok(s), _ => Err("expected string".into()), } } fn is_truthy(&self) -> bool { match self { Value::Int(i, _) => *i != 0, Value::Float(f, _) => *f != 0.0, Value::Str(s, _) => !s.is_empty(), Value::Marker => false, Value::Quotation(_) => true, } } fn is_marker(&self) -> bool { matches!(self, Value::Marker) } fn to_param_string(&self) -> String { match self { Value::Int(i, _) => i.to_string(), Value::Float(f, _) => f.to_string(), Value::Str(s, _) => s.clone(), Value::Marker => String::new(), Value::Quotation(_) => String::new(), } } fn span(&self) -> Option { match self { Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s, Value::Marker | Value::Quotation(_) => None, } } } #[derive(Clone, Debug, PartialEq)] pub enum Op { PushInt(i64, Option), PushFloat(f64, Option), PushStr(String, Option), Dup, Dupn, Drop, Swap, Over, Rot, Nip, Tuck, Add, Sub, Mul, Div, Mod, Neg, Abs, Floor, Ceil, Round, Min, Max, Eq, Ne, Lt, Gt, Le, Ge, And, Or, Not, BranchIfZero(usize), Branch(usize), NewCmd, SetParam(String), Emit, Get, Set, GetContext(String), Rand, Rrand, Seed, Cycle, Choose, ChanceExec, ProbExec, Coin, Mtof, Ftom, ListStart, ListEnd, ListEndCycle, PCycle, ListEndPCycle, At, Window, Scale, Pop, Subdivide, SetTempo, Each, Every, Quotation(Vec), When, Unless, Adsr, Ad, Stack, For, LocalCycleEnd, Echo, Necho, } pub enum WordCompile { Simple, Context(&'static str), Param, Alias(&'static str), Probability(f64), } pub struct Word { pub name: &'static str, pub stack: &'static str, pub desc: &'static str, pub example: &'static str, pub compile: WordCompile, } use WordCompile::*; pub const WORDS: &[Word] = &[ // Stack manipulation Word { name: "dup", stack: "(a -- a a)", desc: "Duplicate top of stack", example: "3 dup => 3 3", compile: Simple, }, Word { name: "dupn", stack: "(a n -- a a ... a)", desc: "Duplicate a onto stack n times", example: "2 4 dupn => 2 2 2 2", compile: Simple, }, Word { name: "!", stack: "(a n -- a a ... a)", desc: "Duplicate a onto stack n times (alias for dupn)", example: "2 4 ! => 2 2 2 2", compile: Alias("dupn"), }, Word { name: "drop", stack: "(a --)", desc: "Remove top of stack", example: "1 2 drop => 1", compile: Simple, }, Word { name: "swap", stack: "(a b -- b a)", desc: "Exchange top two items", example: "1 2 swap => 2 1", compile: Simple, }, Word { name: "over", stack: "(a b -- a b a)", desc: "Copy second to top", example: "1 2 over => 1 2 1", compile: Simple, }, Word { name: "rot", stack: "(a b c -- b c a)", desc: "Rotate top three", example: "1 2 3 rot => 2 3 1", compile: Simple, }, Word { name: "nip", stack: "(a b -- b)", desc: "Remove second item", example: "1 2 nip => 2", compile: Simple, }, Word { name: "tuck", stack: "(a b -- b a b)", desc: "Copy top under second", example: "1 2 tuck => 2 1 2", compile: Simple, }, // Arithmetic Word { name: "+", stack: "(a b -- a+b)", desc: "Add", example: "2 3 + => 5", compile: Simple, }, Word { name: "-", stack: "(a b -- a-b)", desc: "Subtract", example: "5 3 - => 2", compile: Simple, }, Word { name: "*", stack: "(a b -- a*b)", desc: "Multiply", example: "3 4 * => 12", compile: Simple, }, Word { name: "/", stack: "(a b -- a/b)", desc: "Divide", example: "10 2 / => 5", compile: Simple, }, Word { name: "mod", stack: "(a b -- a%b)", desc: "Modulo", example: "7 3 mod => 1", compile: Simple, }, Word { name: "neg", stack: "(a -- -a)", desc: "Negate", example: "5 neg => -5", compile: Simple, }, Word { name: "abs", stack: "(a -- |a|)", desc: "Absolute value", example: "-5 abs => 5", compile: Simple, }, Word { name: "floor", stack: "(f -- n)", desc: "Round down to integer", example: "3.7 floor => 3", compile: Simple, }, Word { name: "ceil", stack: "(f -- n)", desc: "Round up to integer", example: "3.2 ceil => 4", compile: Simple, }, Word { name: "round", stack: "(f -- n)", desc: "Round to nearest integer", example: "3.5 round => 4", compile: Simple, }, Word { name: "min", stack: "(a b -- min)", desc: "Minimum of two values", example: "3 5 min => 3", compile: Simple, }, Word { name: "max", stack: "(a b -- max)", desc: "Maximum of two values", example: "3 5 max => 5", compile: Simple, }, // Comparison Word { name: "=", stack: "(a b -- bool)", desc: "Equal", example: "3 3 = => 1", compile: Simple, }, Word { name: "<>", stack: "(a b -- bool)", desc: "Not equal", example: "3 4 <> => 1", compile: Simple, }, Word { name: "lt", stack: "(a b -- bool)", desc: "Less than", example: "2 3 lt => 1", compile: Simple, }, Word { name: "gt", stack: "(a b -- bool)", desc: "Greater than", example: "3 2 gt => 1", compile: Simple, }, Word { name: "<=", stack: "(a b -- bool)", desc: "Less or equal", example: "3 3 <= => 1", compile: Simple, }, Word { name: ">=", stack: "(a b -- bool)", desc: "Greater or equal", example: "3 3 >= => 1", compile: Simple, }, // Logic Word { name: "and", stack: "(a b -- bool)", desc: "Logical and", example: "1 1 and => 1", compile: Simple, }, Word { name: "or", stack: "(a b -- bool)", desc: "Logical or", example: "0 1 or => 1", compile: Simple, }, Word { name: "not", stack: "(a -- bool)", desc: "Logical not", example: "0 not => 1", compile: Simple, }, // Sound Word { name: "sound", stack: "(name --)", desc: "Begin sound command", example: "\"kick\" sound", compile: Simple, }, Word { name: "s", stack: "(name --)", desc: "Alias for sound", example: "\"kick\" s", compile: Alias("sound"), }, Word { name: "emit", stack: "(--)", desc: "Output current sound", example: "\"kick\" s emit", compile: Simple, }, Word { name: "@", stack: "(--)", desc: "Alias for emit", example: "\"kick\" s 0.5 at @ pop", compile: Alias("emit"), }, // Variables (prefix syntax: @name to fetch, !name to store) Word { name: "@", stack: "( -- val)", desc: "Fetch variable value", example: "@freq => 440", compile: Simple, }, Word { name: "!", stack: "(val --)", desc: "Store value in variable", example: "440 !freq", compile: Simple, }, // Randomness Word { name: "rand", stack: "(min max -- f)", desc: "Random float in range", example: "0 1 rand => 0.42", compile: Simple, }, Word { name: "rrand", stack: "(min max -- n)", desc: "Random int in range", example: "1 6 rrand => 4", compile: Simple, }, Word { name: "seed", stack: "(n --)", desc: "Set random seed", example: "12345 seed", compile: Simple, }, Word { name: "coin", stack: "(-- bool)", desc: "50/50 random boolean", example: "coin => 0 or 1", compile: Simple, }, Word { name: "chance", stack: "(quot prob --)", desc: "Execute quotation with probability (0.0-1.0)", example: "{ 2 distort } 0.75 chance", compile: Simple, }, Word { name: "prob", stack: "(quot pct --)", desc: "Execute quotation with probability (0-100)", example: "{ 2 distort } 75 prob", compile: Simple, }, Word { name: "choose", stack: "(..n n -- val)", desc: "Random pick from n items", example: "1 2 3 3 choose", compile: Simple, }, Word { name: "cycle", stack: "(..n n -- val)", desc: "Cycle through n items by step", example: "1 2 3 3 cycle", compile: Simple, }, Word { name: "pcycle", stack: "(..n n -- val)", desc: "Cycle through n items by pattern", example: "1 2 3 3 pcycle", compile: Simple, }, Word { name: "every", stack: "(n -- bool)", desc: "True every nth iteration", example: "4 every", compile: Simple, }, // Probability shortcuts Word { name: "always", stack: "(quot --)", desc: "Always execute quotation", example: "{ 2 distort } always", compile: Probability(1.0), }, Word { name: "never", stack: "(quot --)", desc: "Never execute quotation", example: "{ 2 distort } never", compile: Probability(0.0), }, Word { name: "often", stack: "(quot --)", desc: "Execute quotation 75% of the time", example: "{ 2 distort } often", compile: Probability(0.75), }, Word { name: "sometimes", stack: "(quot --)", desc: "Execute quotation 50% of the time", example: "{ 2 distort } sometimes", compile: Probability(0.5), }, Word { name: "rarely", stack: "(quot --)", desc: "Execute quotation 25% of the time", example: "{ 2 distort } rarely", compile: Probability(0.25), }, Word { name: "almostNever", stack: "(quot --)", desc: "Execute quotation 10% of the time", example: "{ 2 distort } almostNever", compile: Probability(0.1), }, Word { name: "almostAlways", stack: "(quot --)", desc: "Execute quotation 90% of the time", example: "{ 2 distort } almostAlways", compile: Probability(0.9), }, // Context Word { name: "step", stack: "(-- n)", desc: "Current step index", example: "step => 0", compile: Context("step"), }, Word { name: "beat", stack: "(-- f)", desc: "Current beat position", example: "beat => 4.5", compile: Context("beat"), }, Word { name: "bank", stack: "(str --)", desc: "Set sample bank suffix", example: "\"a\" bank", compile: Param, }, Word { name: "pattern", stack: "(-- n)", desc: "Current pattern index", example: "pattern => 0", compile: Context("pattern"), }, Word { name: "tempo", stack: "(-- f)", desc: "Current BPM", example: "tempo => 120.0", compile: Context("tempo"), }, Word { name: "phase", stack: "(-- f)", desc: "Phase in bar (0-1)", example: "phase => 0.25", compile: Context("phase"), }, Word { name: "slot", stack: "(-- n)", desc: "Current slot number", example: "slot => 0", compile: Context("slot"), }, Word { name: "runs", stack: "(-- n)", desc: "Times this step ran", example: "runs => 3", compile: Context("runs"), }, Word { name: "iter", stack: "(-- n)", desc: "Pattern iteration count", example: "iter => 2", compile: Context("iter"), }, Word { name: "stepdur", stack: "(-- f)", desc: "Step duration in seconds", example: "stepdur => 0.125", compile: Context("stepdur"), }, // Live keys Word { name: "fill", stack: "(-- bool)", desc: "True when fill is on (f key)", example: "{ 4 div each } fill ?", compile: Context("fill"), }, // Music Word { name: "mtof", stack: "(midi -- hz)", desc: "MIDI note to frequency", example: "69 mtof => 440.0", compile: Simple, }, Word { name: "ftom", stack: "(hz -- midi)", desc: "Frequency to MIDI note", example: "440 ftom => 69.0", compile: Simple, }, // Time Word { name: "at", stack: "(pos --)", desc: "Position in time (push context)", example: "\"kick\" s 0.5 at emit pop", compile: Simple, }, Word { name: "zoom", stack: "(start end --)", desc: "Zoom into time region", example: "0.0 0.5 zoom", compile: Simple, }, Word { name: "scale!", stack: "(factor --)", desc: "Scale time context duration", example: "2 scale!", compile: Simple, }, Word { name: "pop", stack: "(--)", desc: "Pop time context", example: "pop", compile: Simple, }, Word { name: "div", stack: "(n --)", desc: "Subdivide time into n", example: "4 div", compile: Simple, }, Word { name: "each", stack: "(--)", desc: "Emit at each subdivision", example: "4 div each", compile: Simple, }, Word { name: "stack", stack: "(n --)", desc: "Create n subdivisions at same time", example: "3 stack", compile: Simple, }, Word { name: "echo", stack: "(n --)", desc: "Create n subdivisions with halving durations (stutter)", example: "3 echo", compile: Simple, }, Word { name: "necho", stack: "(n --)", desc: "Create n subdivisions with doubling durations (swell)", example: "3 necho", compile: Simple, }, Word { name: "for", stack: "(quot --)", desc: "Execute quotation for each subdivision", example: "{ emit } 3 div for", compile: Simple, }, Word { name: "|", stack: "(-- marker)", desc: "Start local cycle list", example: "| 60 62 64 |", compile: Simple, }, Word { name: "tempo!", stack: "(bpm --)", desc: "Set global tempo", example: "140 tempo!", compile: Simple, }, // Lists Word { name: "[", stack: "(-- marker)", desc: "Start list", example: "[ 1 2 3 ]", compile: Simple, }, Word { name: "]", stack: "(marker..n -- n)", desc: "End list, push count", example: "[ 1 2 3 ] => 3", compile: Simple, }, Word { name: "<", stack: "(-- marker)", desc: "Start cycle list", example: "< 1 2 3 >", compile: Alias("["), }, Word { name: ">", stack: "(marker..n -- val)", desc: "End cycle list, pick by step", example: "< 1 2 3 > => cycles through 1, 2, 3", compile: Simple, }, Word { name: "<<", stack: "(-- marker)", desc: "Start pattern cycle list", example: "<< 1 2 3 >>", compile: Alias("["), }, Word { name: ">>", stack: "(marker..n -- val)", desc: "End pattern cycle list, pick by pattern", example: "<< 1 2 3 >> => cycles through 1, 2, 3 per pattern", compile: Simple, }, // Quotations Word { name: "?", stack: "(quot bool --)", desc: "Execute quotation if true", example: "{ 2 distort } 0.5 chance ?", compile: Simple, }, Word { name: "!?", stack: "(quot bool --)", desc: "Execute quotation if false", example: "{ 1 distort } 0.5 chance !?", compile: Simple, }, // Parameters (synthesis) Word { name: "time", stack: "(f --)", desc: "Set time offset", example: "0.1 time", compile: Param, }, Word { name: "repeat", stack: "(n --)", desc: "Set repeat count", example: "4 repeat", compile: Param, }, Word { name: "dur", stack: "(f --)", desc: "Set duration", example: "0.5 dur", compile: Param, }, Word { name: "gate", stack: "(f --)", desc: "Set gate time", example: "0.8 gate", compile: Param, }, Word { name: "freq", stack: "(f --)", desc: "Set frequency (Hz)", example: "440 freq", compile: Param, }, Word { name: "detune", stack: "(f --)", desc: "Set detune amount", example: "0.01 detune", compile: Param, }, Word { name: "speed", stack: "(f --)", desc: "Set playback speed", example: "1.5 speed", compile: Param, }, Word { name: "glide", stack: "(f --)", desc: "Set glide/portamento", example: "0.1 glide", compile: Param, }, Word { name: "pw", stack: "(f --)", desc: "Set pulse width", example: "0.5 pw", compile: Param, }, Word { name: "spread", stack: "(f --)", desc: "Set stereo spread", example: "0.5 spread", compile: Param, }, Word { name: "mult", stack: "(f --)", desc: "Set multiplier", example: "2 mult", compile: Param, }, Word { name: "warp", stack: "(f --)", desc: "Set warp amount", example: "0.5 warp", compile: Param, }, Word { name: "mirror", stack: "(f --)", desc: "Set mirror", example: "1 mirror", compile: Param, }, Word { name: "harmonics", stack: "(f --)", desc: "Set harmonics", example: "4 harmonics", compile: Param, }, Word { name: "timbre", stack: "(f --)", desc: "Set timbre", example: "0.5 timbre", compile: Param, }, Word { name: "morph", stack: "(f --)", desc: "Set morph", example: "0.5 morph", compile: Param, }, Word { name: "begin", stack: "(f --)", desc: "Set sample start (0-1)", example: "0.25 begin", compile: Param, }, Word { name: "end", stack: "(f --)", desc: "Set sample end (0-1)", example: "0.75 end", compile: Param, }, Word { name: "gain", stack: "(f --)", desc: "Set volume (0-1)", example: "0.8 gain", compile: Param, }, Word { name: "postgain", stack: "(f --)", desc: "Set post gain", example: "1.2 postgain", compile: Param, }, Word { name: "velocity", stack: "(f --)", desc: "Set velocity", example: "100 velocity", compile: Param, }, Word { name: "pan", stack: "(f --)", desc: "Set pan (-1 to 1)", example: "0.5 pan", compile: Param, }, Word { name: "attack", stack: "(f --)", desc: "Set attack time", example: "0.01 attack", compile: Param, }, Word { name: "decay", stack: "(f --)", desc: "Set decay time", example: "0.1 decay", compile: Param, }, Word { name: "sustain", stack: "(f --)", desc: "Set sustain level", example: "0.5 sustain", compile: Param, }, Word { name: "release", stack: "(f --)", desc: "Set release time", example: "0.3 release", compile: Param, }, Word { name: "adsr", stack: "(a d s r --)", desc: "Set attack, decay, sustain, release", example: "0.01 0.1 0.5 0.3 adsr", compile: Simple, }, Word { name: "ad", stack: "(a d --)", desc: "Set attack, decay (sustain=0)", example: "0.01 0.1 ad", compile: Simple, }, Word { name: "lpf", stack: "(f --)", desc: "Set lowpass frequency", example: "2000 lpf", compile: Param, }, Word { name: "lpq", stack: "(f --)", desc: "Set lowpass resonance", example: "0.5 lpq", compile: Param, }, Word { name: "lpe", stack: "(f --)", desc: "Set lowpass envelope", example: "0.5 lpe", compile: Param, }, Word { name: "lpa", stack: "(f --)", desc: "Set lowpass attack", example: "0.01 lpa", compile: Param, }, Word { name: "lpd", stack: "(f --)", desc: "Set lowpass decay", example: "0.1 lpd", compile: Param, }, Word { name: "lps", stack: "(f --)", desc: "Set lowpass sustain", example: "0.5 lps", compile: Param, }, Word { name: "lpr", stack: "(f --)", desc: "Set lowpass release", example: "0.3 lpr", compile: Param, }, Word { name: "hpf", stack: "(f --)", desc: "Set highpass frequency", example: "100 hpf", compile: Param, }, Word { name: "hpq", stack: "(f --)", desc: "Set highpass resonance", example: "0.5 hpq", compile: Param, }, Word { name: "hpe", stack: "(f --)", desc: "Set highpass envelope", example: "0.5 hpe", compile: Param, }, Word { name: "hpa", stack: "(f --)", desc: "Set highpass attack", example: "0.01 hpa", compile: Param, }, Word { name: "hpd", stack: "(f --)", desc: "Set highpass decay", example: "0.1 hpd", compile: Param, }, Word { name: "hps", stack: "(f --)", desc: "Set highpass sustain", example: "0.5 hps", compile: Param, }, Word { name: "hpr", stack: "(f --)", desc: "Set highpass release", example: "0.3 hpr", compile: Param, }, Word { name: "bpf", stack: "(f --)", desc: "Set bandpass frequency", example: "1000 bpf", compile: Param, }, Word { name: "bpq", stack: "(f --)", desc: "Set bandpass resonance", example: "0.5 bpq", compile: Param, }, Word { name: "bpe", stack: "(f --)", desc: "Set bandpass envelope", example: "0.5 bpe", compile: Param, }, Word { name: "bpa", stack: "(f --)", desc: "Set bandpass attack", example: "0.01 bpa", compile: Param, }, Word { name: "bpd", stack: "(f --)", desc: "Set bandpass decay", example: "0.1 bpd", compile: Param, }, Word { name: "bps", stack: "(f --)", desc: "Set bandpass sustain", example: "0.5 bps", compile: Param, }, Word { name: "bpr", stack: "(f --)", desc: "Set bandpass release", example: "0.3 bpr", compile: Param, }, Word { name: "ftype", stack: "(n --)", desc: "Set filter type", example: "1 ftype", compile: Param, }, Word { name: "penv", stack: "(f --)", desc: "Set pitch envelope", example: "0.5 penv", compile: Param, }, Word { name: "patt", stack: "(f --)", desc: "Set pitch attack", example: "0.01 patt", compile: Param, }, Word { name: "pdec", stack: "(f --)", desc: "Set pitch decay", example: "0.1 pdec", compile: Param, }, Word { name: "psus", stack: "(f --)", desc: "Set pitch sustain", example: "0 psus", compile: Param, }, Word { name: "prel", stack: "(f --)", desc: "Set pitch release", example: "0.1 prel", compile: Param, }, Word { name: "vib", stack: "(f --)", desc: "Set vibrato rate", example: "5 vib", compile: Param, }, Word { name: "vibmod", stack: "(f --)", desc: "Set vibrato depth", example: "0.5 vibmod", compile: Param, }, Word { name: "vibshape", stack: "(f --)", desc: "Set vibrato shape", example: "0 vibshape", compile: Param, }, Word { name: "fm", stack: "(f --)", desc: "Set FM frequency", example: "200 fm", compile: Param, }, Word { name: "fmh", stack: "(f --)", desc: "Set FM harmonic ratio", example: "2 fmh", compile: Param, }, Word { name: "fmshape", stack: "(f --)", desc: "Set FM shape", example: "0 fmshape", compile: Param, }, Word { name: "fme", stack: "(f --)", desc: "Set FM envelope", example: "0.5 fme", compile: Param, }, Word { name: "fma", stack: "(f --)", desc: "Set FM attack", example: "0.01 fma", compile: Param, }, Word { name: "fmd", stack: "(f --)", desc: "Set FM decay", example: "0.1 fmd", compile: Param, }, Word { name: "fms", stack: "(f --)", desc: "Set FM sustain", example: "0.5 fms", compile: Param, }, Word { name: "fmr", stack: "(f --)", desc: "Set FM release", example: "0.1 fmr", compile: Param, }, Word { name: "am", stack: "(f --)", desc: "Set AM frequency", example: "10 am", compile: Param, }, Word { name: "amdepth", stack: "(f --)", desc: "Set AM depth", example: "0.5 amdepth", compile: Param, }, Word { name: "amshape", stack: "(f --)", desc: "Set AM shape", example: "0 amshape", compile: Param, }, Word { name: "rm", stack: "(f --)", desc: "Set RM frequency", example: "100 rm", compile: Param, }, Word { name: "rmdepth", stack: "(f --)", desc: "Set RM depth", example: "0.5 rmdepth", compile: Param, }, Word { name: "rmshape", stack: "(f --)", desc: "Set RM shape", example: "0 rmshape", compile: Param, }, Word { name: "phaser", stack: "(f --)", desc: "Set phaser rate", example: "1 phaser", compile: Param, }, Word { name: "phaserdepth", stack: "(f --)", desc: "Set phaser depth", example: "0.5 phaserdepth", compile: Param, }, Word { name: "phasersweep", stack: "(f --)", desc: "Set phaser sweep", example: "0.5 phasersweep", compile: Param, }, Word { name: "phasercenter", stack: "(f --)", desc: "Set phaser center", example: "1000 phasercenter", compile: Param, }, Word { name: "flanger", stack: "(f --)", desc: "Set flanger rate", example: "0.5 flanger", compile: Param, }, Word { name: "flangerdepth", stack: "(f --)", desc: "Set flanger depth", example: "0.5 flangerdepth", compile: Param, }, Word { name: "flangerfeedback", stack: "(f --)", desc: "Set flanger feedback", example: "0.5 flangerfeedback", compile: Param, }, Word { name: "chorus", stack: "(f --)", desc: "Set chorus rate", example: "1 chorus", compile: Param, }, Word { name: "chorusdepth", stack: "(f --)", desc: "Set chorus depth", example: "0.5 chorusdepth", compile: Param, }, Word { name: "chorusdelay", stack: "(f --)", desc: "Set chorus delay", example: "0.02 chorusdelay", compile: Param, }, Word { name: "comb", stack: "(f --)", desc: "Set comb filter mix", example: "0.5 comb", compile: Param, }, Word { name: "combfreq", stack: "(f --)", desc: "Set comb frequency", example: "200 combfreq", compile: Param, }, Word { name: "combfeedback", stack: "(f --)", desc: "Set comb feedback", example: "0.5 combfeedback", compile: Param, }, Word { name: "combdamp", stack: "(f --)", desc: "Set comb damping", example: "0.5 combdamp", compile: Param, }, Word { name: "coarse", stack: "(f --)", desc: "Set coarse tune", example: "12 coarse", compile: Param, }, Word { name: "crush", stack: "(f --)", desc: "Set bit crush", example: "8 crush", compile: Param, }, Word { name: "fold", stack: "(f --)", desc: "Set wave fold", example: "2 fold", compile: Param, }, Word { name: "wrap", stack: "(f --)", desc: "Set wave wrap", example: "0.5 wrap", compile: Param, }, Word { name: "distort", stack: "(f --)", desc: "Set distortion", example: "0.5 distort", compile: Param, }, Word { name: "distortvol", stack: "(f --)", desc: "Set distortion volume", example: "0.8 distortvol", compile: Param, }, Word { name: "delay", stack: "(f --)", desc: "Set delay mix", example: "0.3 delay", compile: Param, }, Word { name: "delaytime", stack: "(f --)", desc: "Set delay time", example: "0.25 delaytime", compile: Param, }, Word { name: "delayfeedback", stack: "(f --)", desc: "Set delay feedback", example: "0.5 delayfeedback", compile: Param, }, Word { name: "delaytype", stack: "(n --)", desc: "Set delay type", example: "1 delaytype", compile: Param, }, Word { name: "verb", stack: "(f --)", desc: "Set reverb mix", example: "0.3 verb", compile: Param, }, Word { name: "verbdecay", stack: "(f --)", desc: "Set reverb decay", example: "2 verbdecay", compile: Param, }, Word { name: "verbdamp", stack: "(f --)", desc: "Set reverb damping", example: "0.5 verbdamp", compile: Param, }, Word { name: "verbpredelay", stack: "(f --)", desc: "Set reverb predelay", example: "0.02 verbpredelay", compile: Param, }, Word { name: "verbdiff", stack: "(f --)", desc: "Set reverb diffusion", example: "0.7 verbdiff", compile: Param, }, Word { name: "voice", stack: "(n --)", desc: "Set voice number", example: "1 voice", compile: Param, }, Word { name: "orbit", stack: "(n --)", desc: "Set orbit/bus", example: "0 orbit", compile: Param, }, Word { name: "note", stack: "(n --)", desc: "Set MIDI note", example: "60 note", compile: Param, }, Word { name: "size", stack: "(f --)", desc: "Set size", example: "1 size", compile: Param, }, Word { name: "n", stack: "(n --)", desc: "Set sample number", example: "0 n", compile: Param, }, Word { name: "cut", stack: "(n --)", desc: "Set cut group", example: "1 cut", compile: Param, }, Word { name: "reset", stack: "(n --)", desc: "Reset parameter", example: "1 reset", compile: Param, }, ]; fn simple_op(name: &str) -> Option { Some(match name { "dup" => Op::Dup, "dupn" => Op::Dupn, "drop" => Op::Drop, "swap" => Op::Swap, "over" => Op::Over, "rot" => Op::Rot, "nip" => Op::Nip, "tuck" => Op::Tuck, "+" => Op::Add, "-" => Op::Sub, "*" => Op::Mul, "/" => Op::Div, "mod" => Op::Mod, "neg" => Op::Neg, "abs" => Op::Abs, "floor" => Op::Floor, "ceil" => Op::Ceil, "round" => Op::Round, "min" => Op::Min, "max" => Op::Max, "=" => Op::Eq, "<>" => Op::Ne, "lt" => Op::Lt, "gt" => Op::Gt, "<=" => Op::Le, ">=" => Op::Ge, "and" => Op::And, "or" => Op::Or, "not" => Op::Not, "sound" => Op::NewCmd, "emit" => Op::Emit, "rand" => Op::Rand, "rrand" => Op::Rrand, "seed" => Op::Seed, "cycle" => Op::Cycle, "pcycle" => Op::PCycle, "choose" => Op::Choose, "every" => Op::Every, "chance" => Op::ChanceExec, "prob" => Op::ProbExec, "coin" => Op::Coin, "mtof" => Op::Mtof, "ftom" => Op::Ftom, "?" => Op::When, "!?" => Op::Unless, "at" => Op::At, "zoom" => Op::Window, "scale!" => Op::Scale, "pop" => Op::Pop, "div" => Op::Subdivide, "each" => Op::Each, "tempo!" => Op::SetTempo, "[" => Op::ListStart, "]" => Op::ListEnd, ">" => Op::ListEndCycle, ">>" => Op::ListEndPCycle, "adsr" => Op::Adsr, "ad" => Op::Ad, "stack" => Op::Stack, "for" => Op::For, "echo" => Op::Echo, "necho" => Op::Necho, _ => return None, }) } /// Parse note names like c4, c#4, cs4, eb4 into MIDI numbers. /// C4 = 60 (middle C), A4 = 69 (440 Hz reference). fn parse_note_name(name: &str) -> Option { let name = name.to_lowercase(); let bytes = name.as_bytes(); if bytes.len() < 2 { return None; } let base = match bytes[0] { b'c' => 0, b'd' => 2, b'e' => 4, b'f' => 5, b'g' => 7, b'a' => 9, b'b' => 11, _ => return None, }; let (modifier, octave_start) = match bytes[1] { b'#' | b's' => (1, 2), b'b' if bytes.len() > 2 && bytes[2].is_ascii_digit() => (-1, 2), // flat: eb4, bb4 b'0'..=b'9' => (0, 1), _ => return None, }; let octave_str = &name[octave_start..]; let octave: i64 = octave_str.parse().ok()?; if !(-1..=9).contains(&octave) { return None; } // MIDI: C4 = 60, so C-1 = 0 Some((octave + 1) * 12 + base + modifier) } /// Parse interval names like m3, M3, P5 into semitone counts. /// Supports simple intervals (1-8) and compound intervals (9-15). fn parse_interval(name: &str) -> Option { // Simple intervals: unison through octave let simple = match name { "P1" | "unison" => 0, "m2" => 1, "M2" => 2, "m3" => 3, "M3" => 4, "P4" => 5, "aug4" | "dim5" | "tritone" => 6, "P5" => 7, "m6" => 8, "M6" => 9, "m7" => 10, "M7" => 11, "P8" | "oct" => 12, // Compound intervals (octave + simple) "m9" => 13, "M9" => 14, "m10" => 15, "M10" => 16, "P11" => 17, "aug11" => 18, "P12" => 19, "m13" => 20, "M13" => 21, "m14" => 22, "M14" => 23, "P15" => 24, _ => return None, }; Some(simple) } fn compile_word(name: &str, ops: &mut Vec) -> bool { for word in WORDS { if word.name == name { match &word.compile { Simple => { if let Some(op) = simple_op(name) { ops.push(op); } } Context(ctx) => ops.push(Op::GetContext((*ctx).into())), Param => ops.push(Op::SetParam(name.into())), Alias(target) => return compile_word(target, ops), Probability(p) => { ops.push(Op::PushFloat(*p, None)); ops.push(Op::ChanceExec); } } return true; } } // @varname - fetch variable if let Some(var_name) = name.strip_prefix('@') { if !var_name.is_empty() { ops.push(Op::PushStr(var_name.to_string(), None)); ops.push(Op::Get); return true; } } // !varname - store into variable if let Some(var_name) = name.strip_prefix('!') { if !var_name.is_empty() { ops.push(Op::PushStr(var_name.to_string(), None)); ops.push(Op::Set); return true; } } // Note names: c4, c#4, cs4, eb4, etc. -> MIDI number if let Some(midi) = parse_note_name(name) { ops.push(Op::PushInt(midi, None)); return true; } // Intervals: m3, M3, P5, etc. -> dup top, add semitones (for chord building) if let Some(semitones) = parse_interval(name) { ops.push(Op::Dup); ops.push(Op::PushInt(semitones, None)); ops.push(Op::Add); return true; } // Internal ops not exposed in WORDS if let Some(op) = simple_op(name) { ops.push(op); return true; } false } #[derive(Clone, Debug)] struct TimeContext { start: f64, duration: f64, subdivisions: Option>, iteration_index: Option, } #[derive(Clone, Debug)] enum Token { Int(i64, SourceSpan), Float(f64, SourceSpan), Str(String, SourceSpan), Word(String, SourceSpan), QuoteStart(SourceSpan), QuoteEnd(SourceSpan), } fn tokenize(input: &str) -> Vec { let mut tokens = Vec::new(); let mut chars = input.char_indices().peekable(); while let Some(&(pos, c)) = chars.peek() { if c.is_whitespace() { chars.next(); continue; } if c == '"' { let start = pos; chars.next(); let mut s = String::new(); let mut end = start + 1; while let Some(&(i, ch)) = chars.peek() { end = i + ch.len_utf8(); chars.next(); if ch == '"' { break; } s.push(ch); } tokens.push(Token::Str(s, SourceSpan { start, end })); continue; } if c == '(' { while let Some(&(_, ch)) = chars.peek() { chars.next(); if ch == ')' { break; } } continue; } if c == '{' { let start = pos; chars.next(); tokens.push(Token::QuoteStart(SourceSpan { start, end: start + 1, })); continue; } if c == '}' { let start = pos; chars.next(); tokens.push(Token::QuoteEnd(SourceSpan { start, end: start + 1, })); continue; } let start = pos; let mut word = String::new(); let mut end = start; while let Some(&(i, ch)) = chars.peek() { if ch.is_whitespace() || ch == '{' || ch == '}' { break; } end = i + ch.len_utf8(); word.push(ch); chars.next(); } let span = SourceSpan { start, end }; if let Ok(i) = word.parse::() { tokens.push(Token::Int(i, span)); } else if let Ok(f) = word.parse::() { tokens.push(Token::Float(f, span)); } else { tokens.push(Token::Word(word, span)); } } tokens } fn compile(tokens: &[Token]) -> Result, String> { let mut ops = Vec::new(); let mut i = 0; let mut pipe_parity = false; while i < tokens.len() { match &tokens[i] { Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))), Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))), Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))), Token::QuoteStart(_) => { let (quote_ops, consumed) = compile_quotation(&tokens[i + 1..])?; i += consumed; ops.push(Op::Quotation(quote_ops)); } Token::QuoteEnd(_) => { return Err("unexpected }".into()); } Token::Word(w, _) => { let word = w.as_str(); if word == "|" { if pipe_parity { ops.push(Op::LocalCycleEnd); } else { ops.push(Op::ListStart); } pipe_parity = !pipe_parity; } else if word == "if" { let (then_ops, else_ops, consumed) = compile_if(&tokens[i + 1..])?; i += consumed; if else_ops.is_empty() { ops.push(Op::BranchIfZero(then_ops.len())); ops.extend(then_ops); } else { ops.push(Op::BranchIfZero(then_ops.len() + 1)); ops.extend(then_ops); ops.push(Op::Branch(else_ops.len())); ops.extend(else_ops); } } else if !compile_word(word, &mut ops) { return Err(format!("unknown word: {word}")); } } } i += 1; } Ok(ops) } fn compile_quotation(tokens: &[Token]) -> Result<(Vec, usize), String> { let mut depth = 1; let mut end_pos = None; for (i, tok) in tokens.iter().enumerate() { match tok { Token::QuoteStart(_) => depth += 1, Token::QuoteEnd(_) => { depth -= 1; if depth == 0 { end_pos = Some(i); break; } } _ => {} } } let end_pos = end_pos.ok_or("missing }")?; let quote_ops = compile(&tokens[..end_pos])?; Ok((quote_ops, end_pos + 1)) } fn compile_if(tokens: &[Token]) -> Result<(Vec, Vec, usize), String> { let mut depth = 1; let mut else_pos = None; let mut then_pos = None; for (i, tok) in tokens.iter().enumerate() { if let Token::Word(w, _) = tok { match w.as_str() { "if" => depth += 1, "else" if depth == 1 => else_pos = Some(i), "then" => { depth -= 1; if depth == 0 { then_pos = Some(i); break; } } _ => {} } } } let then_pos = then_pos.ok_or("missing 'then'")?; let (then_ops, else_ops) = if let Some(ep) = else_pos { let then_ops = compile(&tokens[..ep])?; let else_ops = compile(&tokens[ep + 1..then_pos])?; (then_ops, else_ops) } else { let then_ops = compile(&tokens[..then_pos])?; (then_ops, Vec::new()) }; Ok((then_ops, else_ops, then_pos + 1)) } pub type Stack = Arc>>; pub struct Forth { stack: Stack, vars: Variables, rng: Rng, } impl Forth { pub fn new(vars: Variables, rng: Rng) -> Self { Self { stack: Arc::new(Mutex::new(Vec::new())), vars, rng, } } pub fn stack(&self) -> Vec { self.stack.lock().unwrap().clone() } pub fn clear_stack(&self) { self.stack.lock().unwrap().clear(); } pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result, String> { self.evaluate_impl(script, ctx, None) } pub fn evaluate_with_trace( &self, script: &str, ctx: &StepContext, trace: &mut ExecutionTrace, ) -> Result, String> { self.evaluate_impl(script, ctx, Some(trace)) } fn evaluate_impl( &self, script: &str, ctx: &StepContext, trace: Option<&mut ExecutionTrace>, ) -> Result, String> { if script.trim().is_empty() { return Err("empty script".into()); } let tokens = tokenize(script); let ops = compile(&tokens)?; self.execute(&ops, ctx, trace) } fn execute( &self, ops: &[Op], ctx: &StepContext, trace: Option<&mut ExecutionTrace>, ) -> Result, String> { let mut stack = self.stack.lock().unwrap(); let mut outputs: Vec = Vec::new(); let mut time_stack: Vec = vec![TimeContext { start: 0.0, duration: ctx.step_duration(), subdivisions: None, iteration_index: None, }]; let mut cmd = CmdRegister::default(); self.execute_ops( ops, ctx, &mut stack, &mut outputs, &mut time_stack, &mut cmd, trace, )?; Ok(outputs) } fn execute_ops( &self, ops: &[Op], ctx: &StepContext, stack: &mut Vec, outputs: &mut Vec, time_stack: &mut Vec, cmd: &mut CmdRegister, trace: Option<&mut ExecutionTrace>, ) -> Result<(), String> { let mut pc = 0; let trace_cell = std::cell::RefCell::new(trace); 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 => binary_op(stack, |a, b| a / b)?, Op::Mod => { let b = stack.pop().ok_or("stack underflow")?.as_int()?; let a = stack.pop().ok_or("stack underflow")?.as_int()?; stack.push(Value::Int(a % b, None)); } Op::Neg => { let v = stack.pop().ok_or("stack underflow")?; match v { Value::Int(i, s) => stack.push(Value::Int(-i, s)), Value::Float(f, s) => stack.push(Value::Float(-f, s)), _ => return Err("expected number".into()), } } Op::Abs => { let v = stack.pop().ok_or("stack underflow")?; match v { Value::Int(i, s) => stack.push(Value::Int(i.abs(), s)), Value::Float(f, s) => stack.push(Value::Float(f.abs(), s)), _ => return Err("expected number".into()), } } Op::Floor => { let v = stack.pop().ok_or("stack underflow")?.as_float()?; stack.push(Value::Int(v.floor() as i64, None)); } Op::Ceil => { let v = stack.pop().ok_or("stack underflow")?.as_float()?; stack.push(Value::Int(v.ceil() as i64, None)); } Op::Round => { let v = stack.pop().ok_or("stack underflow")?.as_float()?; stack.push(Value::Int(v.round() as i64, None)); } Op::Min => binary_op(stack, |a, b| a.min(b))?, Op::Max => binary_op(stack, |a, b| a.max(b))?, 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::BranchIfZero(offset) => { let v = stack.pop().ok_or("stack underflow")?; if !v.is_truthy() { pc += offset; } } Op::Branch(offset) => { pc += offset; } Op::NewCmd => { let name = stack.pop().ok_or("stack underflow")?; let name = name.as_str()?; cmd.set_sound(name.to_string()); } Op::SetParam(param) => { let val = stack.pop().ok_or("stack underflow")?; cmd.set_param(param.clone(), val.to_param_string()); } Op::Emit => { let (sound, mut params) = cmd.take().ok_or("no sound set")?; let mut pairs = vec![("sound".into(), sound)]; pairs.append(&mut params); let time_ctx = time_stack.last().ok_or("time stack underflow")?; if time_ctx.start > 0.0 { pairs.push(("delta".into(), time_ctx.start.to_string())); } if !pairs.iter().any(|(k, _)| k == "dur") { pairs.push(("dur".into(), time_ctx.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 * time_ctx.duration).to_string(); } else { pairs.push(("delaytime".into(), time_ctx.duration.to_string())); } outputs.push(format_cmd(&pairs)); } 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), "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 max = stack.pop().ok_or("stack underflow")?.as_float()?; let min = stack.pop().ok_or("stack underflow")?.as_float()?; let val = self.rng.lock().unwrap().gen_range(min..max); stack.push(Value::Float(val, None)); } Op::Rrand => { let max = stack.pop().ok_or("stack underflow")?.as_int()?; let min = stack.pop().ok_or("stack underflow")?.as_int()?; let val = self.rng.lock().unwrap().gen_range(min..=max); stack.push(Value::Int(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 => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("cycle count must be > 0".into()); } if stack.len() < count { return Err("stack underflow".into()); } let start = stack.len() - count; let values: Vec = stack.drain(start..).collect(); let idx = ctx.runs % count; let selected = values[idx].clone(); if let Some(span) = selected.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { trace.selected_spans.push(span); } } stack.push(selected); } Op::PCycle => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("pcycle count must be > 0".into()); } if stack.len() < count { return Err("stack underflow".into()); } let start = stack.len() - count; let values: Vec = stack.drain(start..).collect(); let idx = ctx.iter % count; let selected = values[idx].clone(); if let Some(span) = selected.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { trace.selected_spans.push(span); } } stack.push(selected); } 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()); } if stack.len() < count { return Err("stack underflow".into()); } let start = stack.len() - count; let values: Vec = stack.drain(start..).collect(); let idx = self.rng.lock().unwrap().gen_range(0..count); let selected = values[idx].clone(); if let Some(span) = selected.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { trace.selected_spans.push(span); } } stack.push(selected); } Op::ChanceExec => { let prob = stack.pop().ok_or("stack underflow")?.as_float()?; let quot = stack.pop().ok_or("stack underflow")?; let val: f64 = self.rng.lock().unwrap().gen(); if val < prob { match quot { Value::Quotation(quot_ops) => { let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; } _ => return Err("expected quotation".into()), } } } Op::ProbExec => { let pct = stack.pop().ok_or("stack underflow")?.as_float()?; let quot = stack.pop().ok_or("stack underflow")?; let val: f64 = self.rng.lock().unwrap().gen(); if val < pct / 100.0 { match quot { Value::Quotation(quot_ops) => { let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; } _ => return Err("expected quotation".into()), } } } 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) => { stack.push(Value::Quotation(quote_ops.clone())); } Op::When => { let cond = stack.pop().ok_or("stack underflow")?; let quot = stack.pop().ok_or("stack underflow")?; if cond.is_truthy() { match quot { Value::Quotation(quot_ops) => { let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; } _ => return Err("expected quotation".into()), } } } Op::Unless => { let cond = stack.pop().ok_or("stack underflow")?; let quot = stack.pop().ok_or("stack underflow")?; if !cond.is_truthy() { match quot { Value::Quotation(quot_ops) => { let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; *trace_cell.borrow_mut() = trace_opt; } _ => return Err("expected quotation".into()), } } } 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::At => { let pos = stack.pop().ok_or("stack underflow")?.as_float()?; let parent = time_stack.last().ok_or("time stack underflow")?; let new_start = parent.start + parent.duration * pos; time_stack.push(TimeContext { start: new_start, duration: parent.duration * (1.0 - pos), subdivisions: None, iteration_index: parent.iteration_index, }); } Op::Window => { let end = stack.pop().ok_or("stack underflow")?.as_float()?; let start_pos = stack.pop().ok_or("stack underflow")?.as_float()?; let parent = time_stack.last().ok_or("time stack underflow")?; let new_start = parent.start + parent.duration * start_pos; let new_duration = parent.duration * (end - start_pos); time_stack.push(TimeContext { start: new_start, duration: new_duration, subdivisions: None, iteration_index: parent.iteration_index, }); } Op::Scale => { let factor = stack.pop().ok_or("stack underflow")?.as_float()?; let parent = time_stack.last().ok_or("time stack underflow")?; time_stack.push(TimeContext { start: parent.start, duration: parent.duration * factor, subdivisions: None, iteration_index: parent.iteration_index, }); } Op::Pop => { if time_stack.len() <= 1 { return Err("cannot pop root time context".into()); } time_stack.pop(); } Op::Subdivide => { let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if n == 0 { return Err("subdivide count must be > 0".into()); } let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; let sub_duration = time_ctx.duration / n as f64; let mut subs = Vec::with_capacity(n); for i in 0..n { subs.push((time_ctx.start + sub_duration * i as f64, sub_duration)); } time_ctx.subdivisions = Some(subs); } Op::Each => { let (sound, params) = cmd.take().ok_or("no sound set")?; let time_ctx = time_stack.last().ok_or("time stack underflow")?; let subs = time_ctx .subdivisions .as_ref() .ok_or("each requires subdivide first")?; for (sub_start, sub_dur) in subs { let mut pairs = vec![("sound".into(), sound.clone())]; pairs.extend(params.iter().cloned()); if *sub_start > 0.0 { pairs.push(("delta".into(), sub_start.to_string())); } if !pairs.iter().any(|(k, _)| k == "dur") { pairs.push(("dur".into(), sub_dur.to_string())); } if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); pairs[idx].1 = (ratio * sub_dur).to_string(); } else { pairs.push(("delaytime".into(), sub_dur.to_string())); } outputs.push(format_cmd(&pairs)); } } 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::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 => { 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("empty cycle list".into()); } values.reverse(); let idx = ctx.runs % values.len(); let selected = values[idx].clone(); if let Some(span) = selected.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { trace.selected_spans.push(span); } } stack.push(selected); } Op::ListEndPCycle => { 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("empty pattern cycle list".into()); } values.reverse(); let idx = ctx.iter % values.len(); let selected = values[idx].clone(); if let Some(span) = selected.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { trace.selected_spans.push(span); } } stack.push(selected); } 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.to_param_string()); cmd.set_param("decay".into(), d.to_param_string()); cmd.set_param("sustain".into(), s.to_param_string()); cmd.set_param("release".into(), r.to_param_string()); } Op::Ad => { let d = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; cmd.set_param("attack".into(), a.to_param_string()); cmd.set_param("decay".into(), d.to_param_string()); cmd.set_param("sustain".into(), "0".into()); } Op::Stack => { let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if n == 0 { return Err("stack count must be > 0".into()); } let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; let sub_duration = time_ctx.duration / n as f64; let mut subs = Vec::with_capacity(n); for _ in 0..n { subs.push((time_ctx.start, sub_duration)); } time_ctx.subdivisions = Some(subs); } Op::Echo => { let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if n == 0 { return Err("echo count must be > 0".into()); } let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; // Geometric series: d1 * (2 - 2^(1-n)) = total let d1 = time_ctx.duration / (2.0 - 2.0_f64.powi(1 - n as i32)); let mut subs = Vec::with_capacity(n); for i in 0..n { let dur = d1 / 2.0_f64.powi(i as i32); let start = if i == 0 { time_ctx.start } else { time_ctx.start + d1 * (2.0 - 2.0_f64.powi(1 - i as i32)) }; subs.push((start, dur)); } time_ctx.subdivisions = Some(subs); } Op::Necho => { let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if n == 0 { return Err("necho count must be > 0".into()); } let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; // Reverse geometric: d1 + 2*d1 + 4*d1 + ... = d1 * (2^n - 1) = total let d1 = time_ctx.duration / (2.0_f64.powi(n as i32) - 1.0); let mut subs = Vec::with_capacity(n); for i in 0..n { let dur = d1 * 2.0_f64.powi(i as i32); let start = if i == 0 { time_ctx.start } else { // Sum of previous durations: d1 * (2^i - 1) time_ctx.start + d1 * (2.0_f64.powi(i as i32) - 1.0) }; subs.push((start, dur)); } time_ctx.subdivisions = Some(subs); } Op::For => { let quot = stack.pop().ok_or("stack underflow")?; let time_ctx = time_stack.last().ok_or("time stack underflow")?; let subs = time_ctx .subdivisions .clone() .ok_or("for requires subdivide first")?; match quot { Value::Quotation(quot_ops) => { for (i, (sub_start, sub_dur)) in subs.iter().enumerate() { time_stack.push(TimeContext { start: *sub_start, duration: *sub_dur, subdivisions: None, iteration_index: Some(i), }); let mut trace_opt = trace_cell.borrow_mut().take(); self.execute_ops( "_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut(), )?; *trace_cell.borrow_mut() = trace_opt; time_stack.pop(); } } _ => return Err("expected quotation".into()), } } Op::LocalCycleEnd => { 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("empty local cycle list".into()); } values.reverse(); let time_ctx = time_stack.last().ok_or("time stack underflow")?; let idx = time_ctx.iteration_index.unwrap_or(0) % values.len(); let selected = values[idx].clone(); if let Some(span) = selected.span() { if let Some(trace) = trace_cell.borrow_mut().as_mut() { trace.selected_spans.push(span); } } stack.push(selected); } } pc += 1; } Ok(()) } } fn binary_op(stack: &mut Vec, f: F) -> Result<(), String> where F: Fn(f64, f64) -> f64, { let b = stack.pop().ok_or("stack underflow")?.as_float()?; let a = stack.pop().ok_or("stack underflow")?.as_float()?; let result = f(a, b); if result.fract() == 0.0 && result.abs() < i64::MAX as f64 { stack.push(Value::Int(result as i64, None)); } else { stack.push(Value::Float(result, None)); } Ok(()) } fn cmp_op(stack: &mut Vec, f: F) -> Result<(), String> where F: Fn(f64, f64) -> bool, { let b = stack.pop().ok_or("stack underflow")?.as_float()?; let a = stack.pop().ok_or("stack underflow")?.as_float()?; stack.push(Value::Int(if f(a, b) { 1 } else { 0 }, None)); Ok(()) } fn format_cmd(pairs: &[(String, String)]) -> String { let parts: Vec = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect(); format!("/{}", parts.join("/")) }