diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 9928d4d..8c0c225 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -25,6 +25,11 @@ pub enum Op { Round, Min, Max, + Pow, + Sqrt, + Sin, + Cos, + Log, Eq, Ne, Lt, @@ -34,6 +39,11 @@ pub enum Op { And, Or, Not, + Xor, + Nand, + Nor, + IfElse, + Pick, BranchIfZero(usize, Option, Option), Branch(usize), NewCmd, @@ -67,8 +77,9 @@ pub enum Op { Ad, Apply, Ramp, + Tri, Range, - Noise, + Perlin, Chain, Loop, Degree(&'static [i64]), diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index 375d8ef..a2a42e8 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -150,6 +150,15 @@ pub(super) struct PendingEmission { pub slot_index: usize, } +#[derive(Clone, Debug)] +pub(super) struct ResolvedEmission { + pub sound: String, + pub params: Vec<(String, String)>, + pub parent_slot: usize, + pub offset_in_slot: f64, + pub dur: f64, +} + #[derive(Clone, Debug)] pub(super) struct ScopeContext { pub start: f64, @@ -157,6 +166,7 @@ pub(super) struct ScopeContext { pub weight: f64, pub slot_count: usize, pub pending: Vec, + pub resolved: Vec, pub stacked: bool, } @@ -168,6 +178,7 @@ impl ScopeContext { weight: 1.0, slot_count: 0, pending: Vec::new(), + resolved: Vec::new(), stacked: false, } } diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 4515542..b4792f4 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -4,8 +4,8 @@ use rand::{Rng as RngTrait, SeedableRng}; use super::compiler::compile_script; use super::ops::Op; use super::types::{ - CmdRegister, Dictionary, ExecutionTrace, PendingEmission, Rng, ScopeContext, SourceSpan, Stack, - StepContext, Value, Variables, + CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext, + SourceSpan, Stack, StepContext, Value, Variables, }; pub type EmissionCounter = std::sync::Arc>; @@ -220,6 +220,23 @@ impl Forth { } Op::Min => binary_op(stack, |a, b| a.min(b))?, Op::Max => binary_op(stack, |a, b| a.max(b))?, + Op::Pow => binary_op(stack, |a, b| a.powf(b))?, + Op::Sqrt => { + let v = stack.pop().ok_or("stack underflow")?; + stack.push(lift_unary(v, |x| x.sqrt())?); + } + Op::Sin => { + let v = stack.pop().ok_or("stack underflow")?; + stack.push(lift_unary(v, |x| x.sin())?); + } + Op::Cos => { + let v = stack.pop().ok_or("stack underflow")?; + stack.push(lift_unary(v, |x| x.cos())?); + } + Op::Log => { + let v = stack.pop().ok_or("stack underflow")?; + stack.push(lift_unary(v, |x| x.ln())?); + } Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?, Op::Ne => cmp_op(stack, |a, b| (a - b).abs() >= f64::EPSILON)?, @@ -242,6 +259,21 @@ impl Forth { let v = stack.pop().ok_or("stack underflow")?.is_truthy(); stack.push(Value::Int(if v { 0 } else { 1 }, None)); } + Op::Xor => { + let b = stack.pop().ok_or("stack underflow")?.is_truthy(); + let a = stack.pop().ok_or("stack underflow")?.is_truthy(); + stack.push(Value::Int(if a ^ b { 1 } else { 0 }, None)); + } + Op::Nand => { + let b = stack.pop().ok_or("stack underflow")?.is_truthy(); + let a = stack.pop().ok_or("stack underflow")?.is_truthy(); + stack.push(Value::Int(if !(a && b) { 1 } else { 0 }, None)); + } + Op::Nor => { + let b = stack.pop().ok_or("stack underflow")?.is_truthy(); + let a = stack.pop().ok_or("stack underflow")?.is_truthy(); + stack.push(Value::Int(if !(a || b) { 1 } else { 0 }, None)); + } Op::BranchIfZero(offset, then_span, else_span) => { let v = stack.pop().ok_or("stack underflow")?; @@ -630,6 +662,80 @@ impl Forth { } } + Op::IfElse => { + let cond = stack.pop().ok_or("stack underflow")?; + let false_quot = stack.pop().ok_or("stack underflow")?; + let true_quot = stack.pop().ok_or("stack underflow")?; + let quot = if cond.is_truthy() { true_quot } else { false_quot }; + match quot { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } + let mut trace_opt = trace_cell.borrow_mut().take(); + self.execute_ops( + "_ops, + ctx, + stack, + outputs, + scope_stack, + cmd, + trace_opt.as_deref_mut(), + emission_count, + )?; + *trace_cell.borrow_mut() = trace_opt; + } + _ => return Err("expected quotation".into()), + } + } + + Op::Pick => { + let idx = stack.pop().ok_or("stack underflow")?.as_int()? as usize; + let mut quots: Vec = Vec::new(); + while let Some(val) = stack.pop() { + match &val { + Value::Quotation(_, _) => quots.push(val), + _ => { + stack.push(val); + break; + } + } + } + quots.reverse(); + if idx >= quots.len() { + return Err(format!( + "pick index {} out of range (have {} quotations)", + idx, + quots.len() + ) + .into()); + } + match "s[idx] { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(*span); + } + } + let mut trace_opt = trace_cell.borrow_mut().take(); + self.execute_ops( + quot_ops, + ctx, + stack, + outputs, + scope_stack, + cmd, + trace_opt.as_deref_mut(), + emission_count, + )?; + *trace_cell.borrow_mut() = trace_opt; + } + _ => unreachable!(), + } + } + Op::Mtof => { let note = stack.pop().ok_or("stack underflow")?.as_float()?; let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0); @@ -851,13 +957,20 @@ impl Forth { let val = phase.powf(curve); stack.push(Value::Float(val, None)); } + Op::Tri => { + let freq = stack.pop().ok_or("stack underflow")?.as_float()?; + let phase = (freq * ctx.beat).fract(); + let phase = if phase < 0.0 { phase + 1.0 } else { phase }; + let val = 1.0 - (2.0 * phase - 1.0).abs(); + stack.push(Value::Float(val, None)); + } Op::Range => { let max = stack.pop().ok_or("stack underflow")?.as_float()?; let min = stack.pop().ok_or("stack underflow")?.as_float()?; let val = stack.pop().ok_or("stack underflow")?.as_float()?; stack.push(Value::Float(min + val * (max - min), None)); } - Op::Noise => { + Op::Perlin => { let freq = stack.pop().ok_or("stack underflow")?.as_float()?; let val = perlin_noise_1d(freq * ctx.beat); stack.push(Value::Float(val, None)); @@ -887,10 +1000,19 @@ impl Forth { Op::DivEnd => { if scope_stack.len() <= 1 { - return Err("unmatched end".into()); + return Err("unmatched ~ (no div/stack to close)".into()); + } + let child = scope_stack.pop().unwrap(); + + if child.stacked { + // Stack doesn't claim a slot - resolve directly to outputs + resolve_scope(&child, outputs); + } else { + // Div claims a slot in the parent + let parent = scope_stack.last_mut().ok_or("scope stack underflow")?; + let parent_slot = parent.claim_slot(); + resolve_scope_to_parent(&child, parent_slot, parent); } - let scope = scope_stack.pop().unwrap(); - resolve_scope(&scope, outputs); } Op::StackStart => { @@ -966,28 +1088,89 @@ fn resolve_value_with_span(val: &Value, emission_count: usize) -> (Value, Option } fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec) { - if scope.slot_count == 0 || scope.pending.is_empty() { - return; - } - let slot_dur = scope.duration * scope.weight / scope.slot_count as f64; + let slot_dur = if scope.slot_count == 0 { + scope.duration * scope.weight + } else { + scope.duration * scope.weight / scope.slot_count as f64 + }; + + // Collect all emissions with their deltas for sorting + let mut emissions: Vec<(f64, String, Vec<(String, String)>, f64)> = Vec::new(); + for em in &scope.pending { let delta = scope.start + slot_dur * em.slot_index as f64; - let mut pairs = vec![("sound".into(), em.sound.clone())]; - pairs.extend(em.params.iter().cloned()); - if delta > 0.0 { - pairs.push(("delta".into(), delta.to_string())); - } - if !pairs.iter().any(|(k, _)| k == "dur") { - pairs.push(("dur".into(), slot_dur.to_string())); - } - if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { - let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); - pairs[idx].1 = (ratio * slot_dur).to_string(); - } else { - pairs.push(("delaytime".into(), slot_dur.to_string())); - } - outputs.push(format_cmd(&pairs)); + emissions.push((delta, em.sound.clone(), em.params.clone(), slot_dur)); } + + for em in &scope.resolved { + let slot_start = slot_dur * em.parent_slot as f64; + let delta = scope.start + slot_start + em.offset_in_slot * slot_dur; + let dur = em.dur * slot_dur; + emissions.push((delta, em.sound.clone(), em.params.clone(), dur)); + } + + // Sort by delta to ensure temporal ordering + emissions.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); + + for (delta, sound, params, dur) in emissions { + emit_output(&sound, ¶ms, delta, dur, outputs); + } +} + +fn resolve_scope_to_parent(child: &ScopeContext, parent_slot: usize, parent: &mut ScopeContext) { + if child.slot_count == 0 && child.pending.is_empty() && child.resolved.is_empty() { + return; + } + + let child_slot_count = child.slot_count.max(1); + + // Store offsets and durations as fractions of the parent slot + // Child's internal structure: slot_count slots, each slot is 1/slot_count of the whole + for em in &child.pending { + let offset_fraction = em.slot_index as f64 / child_slot_count as f64; + let dur_fraction = 1.0 / child_slot_count as f64; + parent.resolved.push(ResolvedEmission { + sound: em.sound.clone(), + params: em.params.clone(), + parent_slot, + offset_in_slot: offset_fraction, + dur: dur_fraction, + }); + } + + // Child's resolved emissions already have fractional offsets/durs relative to their slots + // We need to compose them: em belongs to child slot em.parent_slot, which is a fraction of child + for em in &child.resolved { + let child_slot_offset = em.parent_slot as f64 / child_slot_count as f64; + let child_slot_size = 1.0 / child_slot_count as f64; + let offset_fraction = child_slot_offset + em.offset_in_slot * child_slot_size; + let dur_fraction = em.dur * child_slot_size; + parent.resolved.push(ResolvedEmission { + sound: em.sound.clone(), + params: em.params.clone(), + parent_slot, + offset_in_slot: offset_fraction, + dur: dur_fraction, + }); + } +} + +fn emit_output(sound: &str, params: &[(String, String)], delta: f64, dur: f64, outputs: &mut Vec) { + let mut pairs = vec![("sound".into(), sound.to_string())]; + pairs.extend(params.iter().cloned()); + if delta > 0.0 { + pairs.push(("delta".into(), delta.to_string())); + } + if !pairs.iter().any(|(k, _)| k == "dur") { + pairs.push(("dur".into(), dur.to_string())); + } + if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { + let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); + pairs[idx].1 = (ratio * dur).to_string(); + } else { + pairs.push(("delaytime".into(), dur.to_string())); + } + outputs.push(format_cmd(&pairs)); } fn perlin_grad(hash_input: i64) -> f64 { @@ -996,11 +1179,8 @@ fn perlin_grad(hash_input: i64) -> f64 { h ^= h >> 33; h = h.wrapping_mul(0xff51afd7ed558ccd); h ^= h >> 33; - if h & 1 == 0 { - 1.0 - } else { - -1.0 - } + // Convert to float in [-1, 1] range for varied gradients + (h as i64 as f64) / (i64::MAX as f64) } fn perlin_noise_1d(x: f64) -> f64 { diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index fa23be5..47f1859 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -12,6 +12,7 @@ pub enum WordCompile { pub struct Word { pub name: &'static str, + pub category: &'static str, pub stack: &'static str, pub desc: &'static str, pub example: &'static str, @@ -24,6 +25,7 @@ pub const WORDS: &[Word] = &[ // Stack manipulation Word { name: "dup", + category: "Stack", stack: "(a -- a a)", desc: "Duplicate top of stack", example: "3 dup => 3 3", @@ -31,20 +33,15 @@ pub const WORDS: &[Word] = &[ }, Word { name: "dupn", + category: "Stack", 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", + category: "Stack", stack: "(a --)", desc: "Remove top of stack", example: "1 2 drop => 1", @@ -52,6 +49,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "swap", + category: "Stack", stack: "(a b -- b a)", desc: "Exchange top two items", example: "1 2 swap => 2 1", @@ -59,6 +57,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "over", + category: "Stack", stack: "(a b -- a b a)", desc: "Copy second to top", example: "1 2 over => 1 2 1", @@ -66,6 +65,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rot", + category: "Stack", stack: "(a b c -- b c a)", desc: "Rotate top three", example: "1 2 3 rot => 2 3 1", @@ -73,6 +73,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "nip", + category: "Stack", stack: "(a b -- b)", desc: "Remove second item", example: "1 2 nip => 2", @@ -80,6 +81,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tuck", + category: "Stack", stack: "(a b -- b a b)", desc: "Copy top under second", example: "1 2 tuck => 2 1 2", @@ -88,6 +90,7 @@ pub const WORDS: &[Word] = &[ // Arithmetic Word { name: "+", + category: "Arithmetic", stack: "(a b -- a+b)", desc: "Add", example: "2 3 + => 5", @@ -95,6 +98,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "-", + category: "Arithmetic", stack: "(a b -- a-b)", desc: "Subtract", example: "5 3 - => 2", @@ -102,6 +106,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "*", + category: "Arithmetic", stack: "(a b -- a*b)", desc: "Multiply", example: "3 4 * => 12", @@ -109,6 +114,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "/", + category: "Arithmetic", stack: "(a b -- a/b)", desc: "Divide", example: "10 2 / => 5", @@ -116,6 +122,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "mod", + category: "Arithmetic", stack: "(a b -- a%b)", desc: "Modulo", example: "7 3 mod => 1", @@ -123,6 +130,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "neg", + category: "Arithmetic", stack: "(a -- -a)", desc: "Negate", example: "5 neg => -5", @@ -130,6 +138,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "abs", + category: "Arithmetic", stack: "(a -- |a|)", desc: "Absolute value", example: "-5 abs => 5", @@ -137,6 +146,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "floor", + category: "Arithmetic", stack: "(f -- n)", desc: "Round down to integer", example: "3.7 floor => 3", @@ -144,6 +154,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ceil", + category: "Arithmetic", stack: "(f -- n)", desc: "Round up to integer", example: "3.2 ceil => 4", @@ -151,6 +162,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "round", + category: "Arithmetic", stack: "(f -- n)", desc: "Round to nearest integer", example: "3.5 round => 4", @@ -158,6 +170,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "min", + category: "Arithmetic", stack: "(a b -- min)", desc: "Minimum of two values", example: "3 5 min => 3", @@ -165,28 +178,80 @@ pub const WORDS: &[Word] = &[ }, Word { name: "max", + category: "Arithmetic", stack: "(a b -- max)", desc: "Maximum of two values", example: "3 5 max => 5", compile: Simple, }, + Word { + name: "pow", + category: "Arithmetic", + stack: "(a b -- a^b)", + desc: "Exponentiation", + example: "2 3 pow => 8", + compile: Simple, + }, + Word { + name: "sqrt", + category: "Arithmetic", + stack: "(a -- √a)", + desc: "Square root", + example: "16 sqrt => 4", + compile: Simple, + }, + Word { + name: "sin", + category: "Arithmetic", + stack: "(a -- sin(a))", + desc: "Sine (radians)", + example: "3.14159 2 / sin => 1.0", + compile: Simple, + }, + Word { + name: "cos", + category: "Arithmetic", + stack: "(a -- cos(a))", + desc: "Cosine (radians)", + example: "0 cos => 1.0", + compile: Simple, + }, + Word { + name: "log", + category: "Arithmetic", + stack: "(a -- ln(a))", + desc: "Natural logarithm", + example: "2.718 log => 1.0", + compile: Simple, + }, // Comparison Word { name: "=", + category: "Comparison", stack: "(a b -- bool)", desc: "Equal", example: "3 3 = => 1", compile: Simple, }, Word { - name: "<>", + name: "!=", + category: "Comparison", stack: "(a b -- bool)", desc: "Not equal", - example: "3 4 <> => 1", + example: "3 4 != => 1", compile: Simple, }, + Word { + name: "<>", + category: "Comparison", + stack: "(a b -- bool)", + desc: "Not equal (traditional Forth)", + example: "3 4 <> => 1", + compile: Alias("!="), + }, Word { name: "lt", + category: "Comparison", stack: "(a b -- bool)", desc: "Less than", example: "2 3 lt => 1", @@ -194,6 +259,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "gt", + category: "Comparison", stack: "(a b -- bool)", desc: "Greater than", example: "3 2 gt => 1", @@ -201,6 +267,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "<=", + category: "Comparison", stack: "(a b -- bool)", desc: "Less or equal", example: "3 3 <= => 1", @@ -208,6 +275,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: ">=", + category: "Comparison", stack: "(a b -- bool)", desc: "Greater or equal", example: "3 3 >= => 1", @@ -216,6 +284,7 @@ pub const WORDS: &[Word] = &[ // Logic Word { name: "and", + category: "Logic", stack: "(a b -- bool)", desc: "Logical and", example: "1 1 and => 1", @@ -223,6 +292,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "or", + category: "Logic", stack: "(a b -- bool)", desc: "Logical or", example: "0 1 or => 1", @@ -230,14 +300,56 @@ pub const WORDS: &[Word] = &[ }, Word { name: "not", + category: "Logic", stack: "(a -- bool)", desc: "Logical not", example: "0 not => 1", compile: Simple, }, + Word { + name: "xor", + category: "Logic", + stack: "(a b -- bool)", + desc: "Exclusive or", + example: "1 0 xor => 1", + compile: Simple, + }, + Word { + name: "nand", + category: "Logic", + stack: "(a b -- bool)", + desc: "Not and", + example: "1 1 nand => 0", + compile: Simple, + }, + Word { + name: "nor", + category: "Logic", + stack: "(a b -- bool)", + desc: "Not or", + example: "0 0 nor => 1", + compile: Simple, + }, + Word { + name: "ifelse", + category: "Logic", + stack: "(true-quot false-quot bool --)", + desc: "Execute true-quot if true, else false-quot", + example: "{ 1 } { 2 } coin ifelse", + compile: Simple, + }, + Word { + name: "pick", + category: "Logic", + stack: "(..quots n --)", + desc: "Execute nth quotation (0-indexed)", + example: "{ 1 } { 2 } { 3 } 2 pick => 3", + compile: Simple, + }, // Sound Word { name: "sound", + category: "Sound", stack: "(name --)", desc: "Begin sound command", example: "\"kick\" sound", @@ -245,41 +357,31 @@ pub const WORDS: &[Word] = &[ }, Word { name: "s", + category: "Sound", 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: "Emit current sound, claim one time slot", - example: "\"kick\" s @ @ @ @", - compile: Alias("emit"), - }, Word { name: ".", + category: "Sound", stack: "(--)", desc: "Emit current sound, claim one time slot", example: "\"kick\" s . . . .", - compile: Alias("emit"), + compile: Simple, }, Word { - name: "~", + name: "_", + category: "Sound", stack: "(--)", desc: "Silence, claim one time slot", - example: "\"kick\" s @ ~ @ ~", + example: "\"kick\" s . _ . _", compile: Simple, }, Word { name: ".!", + category: "Sound", stack: "(n --)", desc: "Emit current sound n times", example: "\"kick\" s 4 .!", @@ -287,28 +389,32 @@ pub const WORDS: &[Word] = &[ }, Word { name: "div", + category: "Time", stack: "(--)", - desc: "Start a time subdivision scope", - example: "div \"kick\" s . \"hat\" s . end", + desc: "Start a time subdivision scope (div claims a slot in parent)", + example: "div \"kick\" s . \"hat\" s . ~", compile: Simple, }, Word { name: "stack", + category: "Time", stack: "(--)", desc: "Start a stacked subdivision scope (sounds stack/superpose)", - example: "stack \"kick\" s . \"hat\" s . end", + example: "stack \"kick\" s . \"hat\" s . ~", compile: Simple, }, Word { - name: "end", + name: "~", + category: "Time", stack: "(--)", - desc: "End a time subdivision scope", - example: "div \"kick\" s . end", + desc: "End a time subdivision scope (div or stack)", + example: "div \"kick\" s . ~", compile: Simple, }, // Variables (prefix syntax: @name to fetch, !name to store) Word { name: "@", + category: "Variables", stack: "( -- val)", desc: "Fetch variable value", example: "@freq => 440", @@ -316,6 +422,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "!", + category: "Variables", stack: "(val --)", desc: "Store value in variable", example: "440 !freq", @@ -324,6 +431,7 @@ pub const WORDS: &[Word] = &[ // Randomness Word { name: "rand", + category: "Randomness", stack: "(min max -- n|f)", desc: "Random in range. Int if both args are int, float otherwise", example: "1 6 rand => 4 | 0.0 1.0 rand => 0.42", @@ -331,6 +439,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "seed", + category: "Randomness", stack: "(n --)", desc: "Set random seed", example: "12345 seed", @@ -338,6 +447,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "coin", + category: "Randomness", stack: "(-- bool)", desc: "50/50 random boolean", example: "coin => 0 or 1", @@ -345,6 +455,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chance", + category: "Probability", stack: "(quot prob --)", desc: "Execute quotation with probability (0.0-1.0)", example: "{ 2 distort } 0.75 chance", @@ -352,6 +463,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "prob", + category: "Probability", stack: "(quot pct --)", desc: "Execute quotation with probability (0-100)", example: "{ 2 distort } 75 prob", @@ -359,6 +471,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "choose", + category: "Randomness", stack: "(..n n -- val)", desc: "Random pick from n items", example: "1 2 3 3 choose", @@ -366,6 +479,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "cycle", + category: "Lists", stack: "(..n n -- val)", desc: "Cycle through n items by step", example: "1 2 3 3 cycle", @@ -373,6 +487,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pcycle", + category: "Lists", stack: "(..n n -- val)", desc: "Cycle through n items by pattern", example: "1 2 3 3 pcycle", @@ -380,6 +495,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "every", + category: "Time", stack: "(n -- bool)", desc: "True every nth iteration", example: "4 every", @@ -388,6 +504,7 @@ pub const WORDS: &[Word] = &[ // Probability shortcuts Word { name: "always", + category: "Probability", stack: "(quot --)", desc: "Always execute quotation", example: "{ 2 distort } always", @@ -395,6 +512,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "never", + category: "Probability", stack: "(quot --)", desc: "Never execute quotation", example: "{ 2 distort } never", @@ -402,6 +520,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "often", + category: "Probability", stack: "(quot --)", desc: "Execute quotation 75% of the time", example: "{ 2 distort } often", @@ -409,6 +528,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sometimes", + category: "Probability", stack: "(quot --)", desc: "Execute quotation 50% of the time", example: "{ 2 distort } sometimes", @@ -416,6 +536,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rarely", + category: "Probability", stack: "(quot --)", desc: "Execute quotation 25% of the time", example: "{ 2 distort } rarely", @@ -423,6 +544,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "almostNever", + category: "Probability", stack: "(quot --)", desc: "Execute quotation 10% of the time", example: "{ 2 distort } almostNever", @@ -430,6 +552,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "almostAlways", + category: "Probability", stack: "(quot --)", desc: "Execute quotation 90% of the time", example: "{ 2 distort } almostAlways", @@ -438,6 +561,7 @@ pub const WORDS: &[Word] = &[ // Context Word { name: "step", + category: "Context", stack: "(-- n)", desc: "Current step index", example: "step => 0", @@ -445,6 +569,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "beat", + category: "Context", stack: "(-- f)", desc: "Current beat position", example: "beat => 4.5", @@ -452,6 +577,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bank", + category: "Sample", stack: "(str --)", desc: "Set sample bank suffix", example: "\"a\" bank", @@ -459,6 +585,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pattern", + category: "Context", stack: "(-- n)", desc: "Current pattern index", example: "pattern => 0", @@ -466,6 +593,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pbank", + category: "Context", stack: "(-- n)", desc: "Current pattern's bank index", example: "pbank => 0", @@ -473,6 +601,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tempo", + category: "Context", stack: "(-- f)", desc: "Current BPM", example: "tempo => 120.0", @@ -480,6 +609,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phase", + category: "Context", stack: "(-- f)", desc: "Phase in bar (0-1)", example: "phase => 0.25", @@ -487,6 +617,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "slot", + category: "Context", stack: "(-- n)", desc: "Current slot number", example: "slot => 0", @@ -494,6 +625,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "runs", + category: "Context", stack: "(-- n)", desc: "Times this step ran", example: "runs => 3", @@ -501,6 +633,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "iter", + category: "Context", stack: "(-- n)", desc: "Pattern iteration count", example: "iter => 2", @@ -508,6 +641,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "stepdur", + category: "Context", stack: "(-- f)", desc: "Step duration in seconds", example: "stepdur => 0.125", @@ -516,6 +650,7 @@ pub const WORDS: &[Word] = &[ // Live keys Word { name: "fill", + category: "Context", stack: "(-- bool)", desc: "True when fill is on (f key)", example: "{ 4 div each } fill ?", @@ -524,6 +659,7 @@ pub const WORDS: &[Word] = &[ // Music Word { name: "mtof", + category: "Music", stack: "(midi -- hz)", desc: "MIDI note to frequency", example: "69 mtof => 440.0", @@ -531,14 +667,16 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ftom", + category: "Music", stack: "(hz -- midi)", desc: "Frequency to MIDI note", example: "440 ftom => 69.0", compile: Simple, }, - // Ramps + // LFO Word { name: "ramp", + category: "LFO", stack: "(freq curve -- val)", desc: "Ramp [0,1]: fract(freq*beat)^curve", example: "0.25 2.0 ramp", @@ -546,6 +684,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "range", + category: "LFO", stack: "(val min max -- scaled)", desc: "Scale [0,1] to [min,max]", example: "0.5 200 800 range => 500", @@ -553,6 +692,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "linramp", + category: "LFO", stack: "(freq -- val)", desc: "Linear ramp (curve=1)", example: "1.0 linramp", @@ -560,6 +700,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "expramp", + category: "LFO", stack: "(freq -- val)", desc: "Exponential ramp (curve=3)", example: "0.25 expramp", @@ -567,21 +708,32 @@ pub const WORDS: &[Word] = &[ }, Word { name: "logramp", + category: "LFO", stack: "(freq -- val)", desc: "Logarithmic ramp (curve=0.3)", example: "2.0 logramp", compile: Simple, }, Word { - name: "noise", + name: "tri", + category: "LFO", + stack: "(freq -- val)", + desc: "Triangle wave [0,1]: 0→1→0", + example: "0.5 tri", + compile: Simple, + }, + Word { + name: "perlin", + category: "LFO", stack: "(freq -- val)", desc: "Perlin noise [0,1] sampled at freq*beat", - example: "0.25 noise", + example: "0.25 perlin", compile: Simple, }, // Time Word { name: "scale!", + category: "Time", stack: "(factor --)", desc: "Set weight of current time scope", example: "2 scale!", @@ -589,6 +741,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "loop", + category: "Time", stack: "(n --)", desc: "Fit sample to n beats", example: "\"break\" s 4 loop @", @@ -596,6 +749,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tempo!", + category: "Time", stack: "(bpm --)", desc: "Set global tempo", example: "140 tempo!", @@ -603,6 +757,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chain", + category: "Time", stack: "(bank pattern --)", desc: "Chain to bank/pattern (1-indexed) when current pattern ends", example: "1 4 chain", @@ -611,6 +766,7 @@ pub const WORDS: &[Word] = &[ // Lists Word { name: "[", + category: "Lists", stack: "(-- marker)", desc: "Start list", example: "[ 1 2 3 ]", @@ -618,6 +774,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "]", + category: "Lists", stack: "(marker..n -- n)", desc: "End list, push count", example: "[ 1 2 3 ] => 3", @@ -625,6 +782,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "<", + category: "Lists", stack: "(-- marker)", desc: "Start cycle list", example: "< 1 2 3 >", @@ -632,6 +790,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: ">", + category: "Lists", stack: "(marker..n -- val)", desc: "End cycle list, pick by step", example: "< 1 2 3 > => cycles through 1, 2, 3", @@ -639,6 +798,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "<<", + category: "Lists", stack: "(-- marker)", desc: "Start pattern cycle list", example: "<< 1 2 3 >>", @@ -646,6 +806,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: ">>", + category: "Lists", stack: "(marker..n -- val)", desc: "End pattern cycle list, pick by pattern", example: "<< 1 2 3 >> => cycles through 1, 2, 3 per pattern", @@ -654,6 +815,7 @@ pub const WORDS: &[Word] = &[ // Quotations Word { name: "?", + category: "Logic", stack: "(quot bool --)", desc: "Execute quotation if true", example: "{ 2 distort } 0.5 chance ?", @@ -661,14 +823,16 @@ pub const WORDS: &[Word] = &[ }, Word { name: "!?", + category: "Logic", stack: "(quot bool --)", desc: "Execute quotation if false", example: "{ 1 distort } 0.5 chance !?", compile: Simple, }, - // Parameters (synthesis) + // Sample playback Word { name: "time", + category: "Sample", stack: "(f --)", desc: "Set time offset", example: "0.1 time", @@ -676,6 +840,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "repeat", + category: "Sample", stack: "(n --)", desc: "Set repeat count", example: "4 repeat", @@ -683,6 +848,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "dur", + category: "Sample", stack: "(f --)", desc: "Set duration", example: "0.5 dur", @@ -690,6 +856,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "gate", + category: "Sample", stack: "(f --)", desc: "Set gate time", example: "0.8 gate", @@ -697,6 +864,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "freq", + category: "Oscillator", stack: "(f --)", desc: "Set frequency (Hz)", example: "440 freq", @@ -704,6 +872,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "detune", + category: "Oscillator", stack: "(f --)", desc: "Set detune amount", example: "0.01 detune", @@ -711,6 +880,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "speed", + category: "Sample", stack: "(f --)", desc: "Set playback speed", example: "1.5 speed", @@ -718,6 +888,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "glide", + category: "Oscillator", stack: "(f --)", desc: "Set glide/portamento", example: "0.1 glide", @@ -725,6 +896,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pw", + category: "Oscillator", stack: "(f --)", desc: "Set pulse width", example: "0.5 pw", @@ -732,6 +904,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "spread", + category: "Oscillator", stack: "(f --)", desc: "Set stereo spread", example: "0.5 spread", @@ -739,6 +912,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "mult", + category: "Oscillator", stack: "(f --)", desc: "Set multiplier", example: "2 mult", @@ -746,6 +920,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "warp", + category: "Oscillator", stack: "(f --)", desc: "Set warp amount", example: "0.5 warp", @@ -753,6 +928,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "mirror", + category: "Oscillator", stack: "(f --)", desc: "Set mirror", example: "1 mirror", @@ -760,27 +936,31 @@ pub const WORDS: &[Word] = &[ }, Word { name: "harmonics", + category: "Oscillator", stack: "(f --)", - desc: "Set harmonics", + desc: "Set harmonics (mutable only)", example: "4 harmonics", compile: Param, }, Word { name: "timbre", + category: "Oscillator", stack: "(f --)", - desc: "Set timbre", + desc: "Set timbre (mutable only)", example: "0.5 timbre", compile: Param, }, Word { name: "morph", + category: "Oscillator", stack: "(f --)", - desc: "Set morph", + desc: "Set morph (mutable only)", example: "0.5 morph", compile: Param, }, Word { name: "begin", + category: "Sample", stack: "(f --)", desc: "Set sample start (0-1)", example: "0.25 begin", @@ -788,6 +968,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "end", + category: "Sample", stack: "(f --)", desc: "Set sample end (0-1)", example: "0.75 end", @@ -795,6 +976,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "gain", + category: "Gain", stack: "(f --)", desc: "Set volume (0-1)", example: "0.8 gain", @@ -802,6 +984,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "postgain", + category: "Gain", stack: "(f --)", desc: "Set post gain", example: "1.2 postgain", @@ -809,6 +992,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "velocity", + category: "Gain", stack: "(f --)", desc: "Set velocity", example: "100 velocity", @@ -816,6 +1000,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pan", + category: "Gain", stack: "(f --)", desc: "Set pan (-1 to 1)", example: "0.5 pan", @@ -823,6 +1008,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "attack", + category: "Envelope", stack: "(f --)", desc: "Set attack time", example: "0.01 attack", @@ -830,6 +1016,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "decay", + category: "Envelope", stack: "(f --)", desc: "Set decay time", example: "0.1 decay", @@ -837,6 +1024,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sustain", + category: "Envelope", stack: "(f --)", desc: "Set sustain level", example: "0.5 sustain", @@ -844,6 +1032,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "release", + category: "Envelope", stack: "(f --)", desc: "Set release time", example: "0.3 release", @@ -851,6 +1040,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "adsr", + category: "Envelope", stack: "(a d s r --)", desc: "Set attack, decay, sustain, release", example: "0.01 0.1 0.5 0.3 adsr", @@ -858,6 +1048,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ad", + category: "Envelope", stack: "(a d --)", desc: "Set attack, decay (sustain=0)", example: "0.01 0.1 ad", @@ -865,6 +1056,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpf", + category: "Filter", stack: "(f --)", desc: "Set lowpass frequency", example: "2000 lpf", @@ -872,6 +1064,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpq", + category: "Filter", stack: "(f --)", desc: "Set lowpass resonance", example: "0.5 lpq", @@ -879,6 +1072,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpe", + category: "Filter", stack: "(f --)", desc: "Set lowpass envelope", example: "0.5 lpe", @@ -886,6 +1080,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpa", + category: "Filter", stack: "(f --)", desc: "Set lowpass attack", example: "0.01 lpa", @@ -893,6 +1088,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpd", + category: "Filter", stack: "(f --)", desc: "Set lowpass decay", example: "0.1 lpd", @@ -900,6 +1096,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lps", + category: "Filter", stack: "(f --)", desc: "Set lowpass sustain", example: "0.5 lps", @@ -907,6 +1104,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpr", + category: "Filter", stack: "(f --)", desc: "Set lowpass release", example: "0.3 lpr", @@ -914,6 +1112,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpf", + category: "Filter", stack: "(f --)", desc: "Set highpass frequency", example: "100 hpf", @@ -921,6 +1120,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpq", + category: "Filter", stack: "(f --)", desc: "Set highpass resonance", example: "0.5 hpq", @@ -928,6 +1128,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpe", + category: "Filter", stack: "(f --)", desc: "Set highpass envelope", example: "0.5 hpe", @@ -935,6 +1136,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpa", + category: "Filter", stack: "(f --)", desc: "Set highpass attack", example: "0.01 hpa", @@ -942,6 +1144,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpd", + category: "Filter", stack: "(f --)", desc: "Set highpass decay", example: "0.1 hpd", @@ -949,6 +1152,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hps", + category: "Filter", stack: "(f --)", desc: "Set highpass sustain", example: "0.5 hps", @@ -956,6 +1160,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpr", + category: "Filter", stack: "(f --)", desc: "Set highpass release", example: "0.3 hpr", @@ -963,6 +1168,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpf", + category: "Filter", stack: "(f --)", desc: "Set bandpass frequency", example: "1000 bpf", @@ -970,6 +1176,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpq", + category: "Filter", stack: "(f --)", desc: "Set bandpass resonance", example: "0.5 bpq", @@ -977,6 +1184,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpe", + category: "Filter", stack: "(f --)", desc: "Set bandpass envelope", example: "0.5 bpe", @@ -984,6 +1192,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpa", + category: "Filter", stack: "(f --)", desc: "Set bandpass attack", example: "0.01 bpa", @@ -991,6 +1200,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpd", + category: "Filter", stack: "(f --)", desc: "Set bandpass decay", example: "0.1 bpd", @@ -998,6 +1208,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bps", + category: "Filter", stack: "(f --)", desc: "Set bandpass sustain", example: "0.5 bps", @@ -1005,6 +1216,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpr", + category: "Filter", stack: "(f --)", desc: "Set bandpass release", example: "0.3 bpr", @@ -1012,6 +1224,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ftype", + category: "Filter", stack: "(n --)", desc: "Set filter type", example: "1 ftype", @@ -1019,6 +1232,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "penv", + category: "Pitch Env", stack: "(f --)", desc: "Set pitch envelope", example: "0.5 penv", @@ -1026,6 +1240,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "patt", + category: "Pitch Env", stack: "(f --)", desc: "Set pitch attack", example: "0.01 patt", @@ -1033,6 +1248,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pdec", + category: "Pitch Env", stack: "(f --)", desc: "Set pitch decay", example: "0.1 pdec", @@ -1040,6 +1256,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "psus", + category: "Pitch Env", stack: "(f --)", desc: "Set pitch sustain", example: "0 psus", @@ -1047,6 +1264,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "prel", + category: "Pitch Env", stack: "(f --)", desc: "Set pitch release", example: "0.1 prel", @@ -1054,6 +1272,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "vib", + category: "Modulation", stack: "(f --)", desc: "Set vibrato rate", example: "5 vib", @@ -1061,6 +1280,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "vibmod", + category: "Modulation", stack: "(f --)", desc: "Set vibrato depth", example: "0.5 vibmod", @@ -1068,6 +1288,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "vibshape", + category: "Modulation", stack: "(f --)", desc: "Set vibrato shape", example: "0 vibshape", @@ -1075,6 +1296,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fm", + category: "Modulation", stack: "(f --)", desc: "Set FM frequency", example: "200 fm", @@ -1082,6 +1304,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmh", + category: "Modulation", stack: "(f --)", desc: "Set FM harmonic ratio", example: "2 fmh", @@ -1089,6 +1312,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmshape", + category: "Modulation", stack: "(f --)", desc: "Set FM shape", example: "0 fmshape", @@ -1096,6 +1320,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fme", + category: "Modulation", stack: "(f --)", desc: "Set FM envelope", example: "0.5 fme", @@ -1103,6 +1328,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fma", + category: "Modulation", stack: "(f --)", desc: "Set FM attack", example: "0.01 fma", @@ -1110,6 +1336,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmd", + category: "Modulation", stack: "(f --)", desc: "Set FM decay", example: "0.1 fmd", @@ -1117,6 +1344,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fms", + category: "Modulation", stack: "(f --)", desc: "Set FM sustain", example: "0.5 fms", @@ -1124,6 +1352,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmr", + category: "Modulation", stack: "(f --)", desc: "Set FM release", example: "0.1 fmr", @@ -1131,6 +1360,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "am", + category: "Modulation", stack: "(f --)", desc: "Set AM frequency", example: "10 am", @@ -1138,6 +1368,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "amdepth", + category: "Modulation", stack: "(f --)", desc: "Set AM depth", example: "0.5 amdepth", @@ -1145,6 +1376,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "amshape", + category: "Modulation", stack: "(f --)", desc: "Set AM shape", example: "0 amshape", @@ -1152,6 +1384,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rm", + category: "Modulation", stack: "(f --)", desc: "Set RM frequency", example: "100 rm", @@ -1159,6 +1392,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rmdepth", + category: "Modulation", stack: "(f --)", desc: "Set RM depth", example: "0.5 rmdepth", @@ -1166,6 +1400,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rmshape", + category: "Modulation", stack: "(f --)", desc: "Set RM shape", example: "0 rmshape", @@ -1173,6 +1408,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phaser", + category: "Mod FX", stack: "(f --)", desc: "Set phaser rate", example: "1 phaser", @@ -1180,6 +1416,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phaserdepth", + category: "Mod FX", stack: "(f --)", desc: "Set phaser depth", example: "0.5 phaserdepth", @@ -1187,6 +1424,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phasersweep", + category: "Mod FX", stack: "(f --)", desc: "Set phaser sweep", example: "0.5 phasersweep", @@ -1194,6 +1432,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phasercenter", + category: "Mod FX", stack: "(f --)", desc: "Set phaser center", example: "1000 phasercenter", @@ -1201,6 +1440,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "flanger", + category: "Mod FX", stack: "(f --)", desc: "Set flanger rate", example: "0.5 flanger", @@ -1208,6 +1448,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "flangerdepth", + category: "Mod FX", stack: "(f --)", desc: "Set flanger depth", example: "0.5 flangerdepth", @@ -1215,6 +1456,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "flangerfeedback", + category: "Mod FX", stack: "(f --)", desc: "Set flanger feedback", example: "0.5 flangerfeedback", @@ -1222,6 +1464,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chorus", + category: "Mod FX", stack: "(f --)", desc: "Set chorus rate", example: "1 chorus", @@ -1229,6 +1472,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chorusdepth", + category: "Mod FX", stack: "(f --)", desc: "Set chorus depth", example: "0.5 chorusdepth", @@ -1236,6 +1480,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chorusdelay", + category: "Mod FX", stack: "(f --)", desc: "Set chorus delay", example: "0.02 chorusdelay", @@ -1243,6 +1488,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "comb", + category: "Filter", stack: "(f --)", desc: "Set comb filter mix", example: "0.5 comb", @@ -1250,6 +1496,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "combfreq", + category: "Filter", stack: "(f --)", desc: "Set comb frequency", example: "200 combfreq", @@ -1257,6 +1504,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "combfeedback", + category: "Filter", stack: "(f --)", desc: "Set comb feedback", example: "0.5 combfeedback", @@ -1264,6 +1512,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "combdamp", + category: "Filter", stack: "(f --)", desc: "Set comb damping", example: "0.5 combdamp", @@ -1271,6 +1520,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "coarse", + category: "Oscillator", stack: "(f --)", desc: "Set coarse tune", example: "12 coarse", @@ -1278,6 +1528,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "crush", + category: "Lo-fi", stack: "(f --)", desc: "Set bit crush", example: "8 crush", @@ -1285,6 +1536,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sub", + category: "Oscillator", stack: "(f --)", desc: "Set sub oscillator level", example: "0.5 sub", @@ -1292,6 +1544,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "suboct", + category: "Oscillator", stack: "(n --)", desc: "Set sub oscillator octave", example: "2 suboct", @@ -1299,6 +1552,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "subwave", + category: "Oscillator", stack: "(n --)", desc: "Set sub oscillator waveform", example: "1 subwave", @@ -1306,6 +1560,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fold", + category: "Lo-fi", stack: "(f --)", desc: "Set wave fold", example: "2 fold", @@ -1313,6 +1568,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "wrap", + category: "Lo-fi", stack: "(f --)", desc: "Set wave wrap", example: "0.5 wrap", @@ -1320,6 +1576,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "distort", + category: "Lo-fi", stack: "(f --)", desc: "Set distortion", example: "0.5 distort", @@ -1327,6 +1584,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "distortvol", + category: "Lo-fi", stack: "(f --)", desc: "Set distortion volume", example: "0.8 distortvol", @@ -1334,6 +1592,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delay", + category: "Delay", stack: "(f --)", desc: "Set delay mix", example: "0.3 delay", @@ -1341,6 +1600,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delaytime", + category: "Delay", stack: "(f --)", desc: "Set delay time", example: "0.25 delaytime", @@ -1348,6 +1608,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delayfeedback", + category: "Delay", stack: "(f --)", desc: "Set delay feedback", example: "0.5 delayfeedback", @@ -1355,6 +1616,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delaytype", + category: "Delay", stack: "(n --)", desc: "Set delay type", example: "1 delaytype", @@ -1362,6 +1624,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verb", + category: "Reverb", stack: "(f --)", desc: "Set reverb mix", example: "0.3 verb", @@ -1369,6 +1632,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbdecay", + category: "Reverb", stack: "(f --)", desc: "Set reverb decay", example: "2 verbdecay", @@ -1376,6 +1640,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbdamp", + category: "Reverb", stack: "(f --)", desc: "Set reverb damping", example: "0.5 verbdamp", @@ -1383,6 +1648,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbpredelay", + category: "Reverb", stack: "(f --)", desc: "Set reverb predelay", example: "0.02 verbpredelay", @@ -1390,6 +1656,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbdiff", + category: "Reverb", stack: "(f --)", desc: "Set reverb diffusion", example: "0.7 verbdiff", @@ -1397,6 +1664,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "voice", + category: "Sample", stack: "(n --)", desc: "Set voice number", example: "1 voice", @@ -1404,6 +1672,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "orbit", + category: "Sample", stack: "(n --)", desc: "Set orbit/bus", example: "0 orbit", @@ -1411,6 +1680,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "note", + category: "Oscillator", stack: "(n --)", desc: "Set MIDI note", example: "60 note", @@ -1418,6 +1688,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "size", + category: "Reverb", stack: "(f --)", desc: "Set size", example: "1 size", @@ -1425,6 +1696,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "n", + category: "Sample", stack: "(n --)", desc: "Set sample number", example: "0 n", @@ -1432,6 +1704,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "cut", + category: "Sample", stack: "(n --)", desc: "Set cut group", example: "1 cut", @@ -1439,6 +1712,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "reset", + category: "Sample", stack: "(n --)", desc: "Reset parameter", example: "1 reset", @@ -1447,6 +1721,7 @@ pub const WORDS: &[Word] = &[ // Quotation execution Word { name: "apply", + category: "Logic", stack: "(quot --)", desc: "Execute quotation unconditionally", example: "{ 2 * } apply", @@ -1455,6 +1730,7 @@ pub const WORDS: &[Word] = &[ // Word definitions Word { name: ":", + category: "Definitions", stack: "( -- )", desc: "Begin word definition", example: ": kick \"kick\" s emit ;", @@ -1462,6 +1738,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: ";", + category: "Definitions", stack: "( -- )", desc: "End word definition", example: ": kick \"kick\" s emit ;", @@ -1491,8 +1768,13 @@ pub(super) fn simple_op(name: &str) -> Option { "round" => Op::Round, "min" => Op::Min, "max" => Op::Max, + "pow" => Op::Pow, + "sqrt" => Op::Sqrt, + "sin" => Op::Sin, + "cos" => Op::Cos, + "log" => Op::Log, "=" => Op::Eq, - "<>" => Op::Ne, + "!=" => Op::Ne, "lt" => Op::Lt, "gt" => Op::Gt, "<=" => Op::Le, @@ -1500,8 +1782,13 @@ pub(super) fn simple_op(name: &str) -> Option { "and" => Op::And, "or" => Op::Or, "not" => Op::Not, + "xor" => Op::Xor, + "nand" => Op::Nand, + "nor" => Op::Nor, + "ifelse" => Op::IfElse, + "pick" => Op::Pick, "sound" => Op::NewCmd, - "emit" => Op::Emit, + "." => Op::Emit, "rand" => Op::Rand, "seed" => Op::Seed, "cycle" => Op::Cycle, @@ -1515,7 +1802,7 @@ pub(super) fn simple_op(name: &str) -> Option { "ftom" => Op::Ftom, "?" => Op::When, "!?" => Op::Unless, - "~" => Op::Silence, + "_" => Op::Silence, "scale!" => Op::Scale, "tempo!" => Op::SetTempo, "[" => Op::ListStart, @@ -1526,14 +1813,15 @@ pub(super) fn simple_op(name: &str) -> Option { "ad" => Op::Ad, "apply" => Op::Apply, "ramp" => Op::Ramp, + "tri" => Op::Tri, "range" => Op::Range, - "noise" => Op::Noise, + "perlin" => Op::Perlin, "chain" => Op::Chain, "loop" => Op::Loop, "oct" => Op::Oct, "div" => Op::DivStart, "stack" => Op::StackStart, - "end" => Op::DivEnd, + "~" => Op::DivEnd, ".!" => Op::EmitN, _ => return None, }) diff --git a/src/model/mod.rs b/src/model/mod.rs index bd68f45..45a6cbf 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,6 +1,6 @@ mod script; -pub use cagire_forth::{Word, WordCompile, WORDS}; +pub use cagire_forth::{Word, WORDS}; pub use cagire_project::{ load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS, MAX_PATTERNS, diff --git a/src/views/dict_view.rs b/src/views/dict_view.rs index 9bb11a3..45d5343 100644 --- a/src/views/dict_view.rs +++ b/src/views/dict_view.rs @@ -5,22 +5,39 @@ use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; use ratatui::Frame; use crate::app::App; -use crate::model::{Word, WordCompile, WORDS}; +use crate::model::{Word, WORDS}; use crate::state::DictFocus; const CATEGORIES: &[&str] = &[ + // Forth core "Stack", "Arithmetic", "Comparison", "Logic", - "Sound", "Variables", "Randomness", "Probability", + "Lists", + "Definitions", + // Live coding + "Sound", + "Time", "Context", "Music", - "Time", - "Parameters", + "LFO", + // Synthesis + "Oscillator", + "Envelope", + "Pitch Env", + "Gain", + "Sample", + // Effects + "Filter", + "Modulation", + "Mod FX", + "Lo-fi", + "Delay", + "Reverb", ]; pub fn render(frame: &mut Frame, app: &App, area: Rect) { @@ -98,7 +115,7 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { let category = CATEGORIES[app.ui.dict_category]; WORDS .iter() - .filter(|w| word_category(w.name, &w.compile) == category) + .filter(|w| w.category == category) .collect() }; @@ -193,39 +210,6 @@ fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(Paragraph::new(vec![line]), area); } -fn word_category(name: &str, compile: &WordCompile) -> &'static str { - const STACK: &[&str] = &["dup", "drop", "swap", "over", "rot", "nip", "tuck"]; - const ARITH: &[&str] = &[ - "+", "-", "*", "/", "mod", "neg", "abs", "floor", "ceil", "round", "min", "max", - ]; - const CMP: &[&str] = &["=", "<>", "<", ">", "<=", ">="]; - const LOGIC: &[&str] = &["and", "or", "not"]; - const SOUND: &[&str] = &["sound", "s", "emit"]; - const VAR: &[&str] = &["get", "set"]; - const RAND: &[&str] = &["rand", "rrand", "seed", "coin", "chance", "choose", "cycle"]; - const MUSIC: &[&str] = &["mtof", "ftom"]; - const TIME: &[&str] = &[ - "at", "window", "pop", "div", "each", "tempo!", "[", "]", "?", - ]; - - match compile { - WordCompile::Simple if STACK.contains(&name) => "Stack", - WordCompile::Simple if ARITH.contains(&name) => "Arithmetic", - WordCompile::Simple if CMP.contains(&name) => "Comparison", - WordCompile::Simple if LOGIC.contains(&name) => "Logic", - WordCompile::Simple if SOUND.contains(&name) => "Sound", - WordCompile::Alias(_) => "Sound", - WordCompile::Simple if VAR.contains(&name) => "Variables", - WordCompile::Simple if RAND.contains(&name) => "Randomness", - WordCompile::Probability(_) => "Probability", - WordCompile::Context(_) => "Context", - WordCompile::Simple if MUSIC.contains(&name) => "Music", - WordCompile::Simple if TIME.contains(&name) => "Time", - WordCompile::Param => "Parameters", - _ => "Other", - } -} - pub fn category_count() -> usize { CATEGORIES.len() } diff --git a/tests/forth/arithmetic.rs b/tests/forth/arithmetic.rs index a1b122c..d268856 100644 --- a/tests/forth/arithmetic.rs +++ b/tests/forth/arithmetic.rs @@ -150,3 +150,53 @@ fn chain() { fn underflow() { expect_error("1 +", "stack underflow"); } + +#[test] +fn pow_int() { + expect_int("2 3 pow", 8); +} + +#[test] +fn pow_float() { + expect_float("2 0.5 pow", std::f64::consts::SQRT_2); +} + +#[test] +fn sqrt() { + expect_int("16 sqrt", 4); +} + +#[test] +fn sqrt_float() { + expect_float("2 sqrt", std::f64::consts::SQRT_2); +} + +#[test] +fn sin_zero() { + expect_int("0 sin", 0); +} + +#[test] +fn sin_pi_half() { + expect_float("3.14159265358979 2 / sin", 1.0); +} + +#[test] +fn cos_zero() { + expect_int("0 cos", 1); +} + +#[test] +fn cos_pi() { + expect_int("3.14159265358979 cos", -1); +} + +#[test] +fn log_e() { + expect_float("2.718281828459045 log", 1.0); +} + +#[test] +fn log_one() { + expect_int("1 log", 0); +} diff --git a/tests/forth/comparison.rs b/tests/forth/comparison.rs index 749ad5a..7f7f37b 100644 --- a/tests/forth/comparison.rs +++ b/tests/forth/comparison.rs @@ -134,3 +134,83 @@ fn truthy_nonzero() { fn truthy_negative() { expect_int("-1 not", 0); } + +#[test] +fn xor_tt() { + expect_int("1 1 xor", 0); +} + +#[test] +fn xor_tf() { + expect_int("1 0 xor", 1); +} + +#[test] +fn xor_ft() { + expect_int("0 1 xor", 1); +} + +#[test] +fn xor_ff() { + expect_int("0 0 xor", 0); +} + +#[test] +fn nand_tt() { + expect_int("1 1 nand", 0); +} + +#[test] +fn nand_tf() { + expect_int("1 0 nand", 1); +} + +#[test] +fn nand_ff() { + expect_int("0 0 nand", 1); +} + +#[test] +fn nor_tt() { + expect_int("1 1 nor", 0); +} + +#[test] +fn nor_tf() { + expect_int("1 0 nor", 0); +} + +#[test] +fn nor_ff() { + expect_int("0 0 nor", 1); +} + +#[test] +fn ifelse_true() { + expect_int("{ 42 } { 99 } 1 ifelse", 42); +} + +#[test] +fn ifelse_false() { + expect_int("{ 42 } { 99 } 0 ifelse", 99); +} + +#[test] +fn pick_first() { + expect_int("{ 10 } { 20 } { 30 } 0 pick", 10); +} + +#[test] +fn pick_second() { + expect_int("{ 10 } { 20 } { 30 } 1 pick", 20); +} + +#[test] +fn pick_third() { + expect_int("{ 10 } { 20 } { 30 } 2 pick", 30); +} + +#[test] +fn pick_preserves_stack() { + expect_int("5 { 10 } { 20 } 0 pick +", 15); +} diff --git a/tests/forth/definitions.rs b/tests/forth/definitions.rs index 48435a7..5637aee 100644 --- a/tests/forth/definitions.rs +++ b/tests/forth/definitions.rs @@ -22,7 +22,7 @@ fn redefine_word_overwrites() { #[test] fn word_with_param() { - let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud emit", 1); + let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud .", 1); assert!(outputs[0].contains("gain/0.9")); } @@ -97,7 +97,7 @@ fn define_word_containing_quotation() { #[test] fn define_word_with_sound() { - let outputs = expect_outputs(": kick \"kick\" s emit ; kick", 1); + let outputs = expect_outputs(": kick \"kick\" s . ; kick", 1); assert!(outputs[0].contains("sound/kick")); } diff --git a/tests/forth/list_words.rs b/tests/forth/list_words.rs index 999bf08..1a205c8 100644 --- a/tests/forth/list_words.rs +++ b/tests/forth/list_words.rs @@ -72,7 +72,7 @@ fn word_with_sound_params() { let f = forth(); let ctx = ctx_with(|c| c.runs = 0); let outputs = f.evaluate( - ": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > emit", + ": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > .", &ctx ).unwrap(); assert_eq!(outputs.len(), 1); diff --git a/tests/forth/quotations.rs b/tests/forth/quotations.rs index 9b62aaf..dbf7c1e 100644 --- a/tests/forth/quotations.rs +++ b/tests/forth/quotations.rs @@ -59,31 +59,31 @@ fn nested_quotations() { #[test] fn quotation_with_param() { - let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? emit"#, 1); + let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? ."#, 1); assert!(outputs[0].contains("distort/2")); } #[test] fn quotation_skips_param() { - let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? emit"#, 1); + let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? ."#, 1); assert!(!outputs[0].contains("distort")); } #[test] fn quotation_with_emit() { - // When true, emit should fire - let outputs = expect_outputs(r#""kick" s { emit } 1 ?"#, 1); + // When true, . should fire + let outputs = expect_outputs(r#""kick" s { . } 1 ?"#, 1); assert!(outputs[0].contains("kick")); } #[test] fn quotation_skips_emit() { - // When false, emit should not fire + // When false, . should not fire let f = forth(); let outputs = f - .evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx()) + .evaluate(r#""kick" s { . } 0 ?"#, &default_ctx()) .unwrap(); - // No output since emit was skipped and no implicit emit + // No output since . was skipped and no implicit emit assert_eq!(outputs.len(), 0); } @@ -110,7 +110,7 @@ fn every_with_quotation_integration() { let ctx = ctx_with(|c| c.iter = iter); let f = forth(); let outputs = f - .evaluate(r#""kick" s { 2 distort } 2 every ? emit"#, &ctx) + .evaluate(r#""kick" s { 2 distort } 2 every ? ."#, &ctx) .unwrap(); if iter % 2 == 0 { assert!( @@ -163,7 +163,7 @@ fn when_and_unless_complementary() { let f = forth(); let outputs = f .evaluate( - r#""kick" s { 2 distort } 2 every ? { 4 distort } 2 every !? emit"#, + r#""kick" s { 2 distort } 2 every ? { 4 distort } 2 every !? ."#, &ctx, ) .unwrap(); diff --git a/tests/forth/ramps.rs b/tests/forth/ramps.rs index 440e4e5..a3dd744 100644 --- a/tests/forth/ramps.rs +++ b/tests/forth/ramps.rs @@ -130,64 +130,64 @@ fn ramp_with_range() { } #[test] -fn noise_deterministic() { +fn perlin_deterministic() { let ctx = ctx_with(|c| c.beat = 2.7); let f = forth(); - f.evaluate("1.0 noise", &ctx).unwrap(); + f.evaluate("1.0 perlin", &ctx).unwrap(); let val1 = stack_float(&f); - f.evaluate("1.0 noise", &ctx).unwrap(); + f.evaluate("1.0 perlin", &ctx).unwrap(); let val2 = stack_float(&f); - assert!((val1 - val2).abs() < 1e-9, "noise should be deterministic"); + assert!((val1 - val2).abs() < 1e-9, "perlin should be deterministic"); } #[test] -fn noise_in_range() { +fn perlin_in_range() { for i in 0..100 { let ctx = ctx_with(|c| c.beat = i as f64 * 0.1); let f = forth(); - f.evaluate("1.0 noise", &ctx).unwrap(); + f.evaluate("1.0 perlin", &ctx).unwrap(); let val = stack_float(&f); - assert!(val >= 0.0 && val <= 1.0, "noise out of range: {}", val); + assert!(val >= 0.0 && val <= 1.0, "perlin out of range: {}", val); } } #[test] -fn noise_varies() { +fn perlin_varies() { let ctx1 = ctx_with(|c| c.beat = 0.5); let ctx2 = ctx_with(|c| c.beat = 1.5); let f = forth(); - f.evaluate("1.0 noise", &ctx1).unwrap(); + f.evaluate("1.0 perlin", &ctx1).unwrap(); let val1 = stack_float(&f); - f.evaluate("1.0 noise", &ctx2).unwrap(); + f.evaluate("1.0 perlin", &ctx2).unwrap(); let val2 = stack_float(&f); - assert!((val1 - val2).abs() > 1e-9, "noise should vary with beat"); + assert!((val1 - val2).abs() > 1e-9, "perlin should vary with beat"); } #[test] -fn noise_smooth() { +fn perlin_smooth() { let f = forth(); let mut prev = 0.0; for i in 0..100 { let ctx = ctx_with(|c| c.beat = i as f64 * 0.01); - f.evaluate("1.0 noise", &ctx).unwrap(); + f.evaluate("1.0 perlin", &ctx).unwrap(); let val = stack_float(&f); if i > 0 { - assert!((val - prev).abs() < 0.2, "noise not smooth: jump {} at step {}", (val - prev).abs(), i); + assert!((val - prev).abs() < 0.2, "perlin not smooth: jump {} at step {}", (val - prev).abs(), i); } prev = val; } } #[test] -fn noise_with_range() { +fn perlin_with_range() { let ctx = ctx_with(|c| c.beat = 1.3); let f = forth(); - f.evaluate("1.0 noise 200.0 800.0 range", &ctx).unwrap(); + f.evaluate("1.0 perlin 200.0 800.0 range", &ctx).unwrap(); let val = stack_float(&f); - assert!(val >= 200.0 && val <= 800.0, "noise+range out of bounds: {}", val); + assert!(val >= 200.0 && val <= 800.0, "perlin+range out of bounds: {}", val); } #[test] -fn noise_underflow() { - expect_error("noise", "stack underflow"); +fn perlin_underflow() { + expect_error("perlin", "stack underflow"); } diff --git a/tests/forth/sound.rs b/tests/forth/sound.rs index 7f40569..8482b59 100644 --- a/tests/forth/sound.rs +++ b/tests/forth/sound.rs @@ -2,19 +2,19 @@ use super::harness::*; #[test] fn basic_emit() { - let outputs = expect_outputs(r#""kick" sound emit"#, 1); + let outputs = expect_outputs(r#""kick" sound ."#, 1); assert!(outputs[0].contains("sound/kick")); } #[test] fn alias_s() { - let outputs = expect_outputs(r#""snare" s emit"#, 1); + let outputs = expect_outputs(r#""snare" s ."#, 1); assert!(outputs[0].contains("sound/snare")); } #[test] fn with_params() { - let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain emit"#, 1); + let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain ."#, 1); assert!(outputs[0].contains("sound/kick")); assert!(outputs[0].contains("freq/440")); assert!(outputs[0].contains("gain/0.5")); @@ -22,24 +22,24 @@ fn with_params() { #[test] fn auto_dur() { - let outputs = expect_outputs(r#""kick" s emit"#, 1); + let outputs = expect_outputs(r#""kick" s ."#, 1); assert!(outputs[0].contains("dur/")); } #[test] fn auto_delaytime() { - let outputs = expect_outputs(r#""kick" s emit"#, 1); + let outputs = expect_outputs(r#""kick" s ."#, 1); assert!(outputs[0].contains("delaytime/")); } #[test] fn emit_no_sound() { - expect_error("emit", "no sound set"); + expect_error(".", "no sound set"); } #[test] fn multiple_emits() { - let outputs = expect_outputs(r#""kick" s emit "snare" s emit"#, 2); + let outputs = expect_outputs(r#""kick" s . "snare" s ."#, 2); assert!(outputs[0].contains("sound/kick")); assert!(outputs[1].contains("sound/snare")); } @@ -47,7 +47,7 @@ fn multiple_emits() { #[test] fn envelope_params() { let outputs = expect_outputs( - r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release emit"#, + r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#, 1, ); assert!(outputs[0].contains("attack/0.01")); @@ -58,14 +58,14 @@ fn envelope_params() { #[test] fn filter_params() { - let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq emit"#, 1); + let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq ."#, 1); assert!(outputs[0].contains("lpf/2000")); assert!(outputs[0].contains("lpq/0.5")); } #[test] fn adsr_sets_all_envelope_params() { - let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr emit"#, 1); + let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1); assert!(outputs[0].contains("attack/0.01")); assert!(outputs[0].contains("decay/0.1")); assert!(outputs[0].contains("sustain/0.5")); @@ -74,7 +74,7 @@ fn adsr_sets_all_envelope_params() { #[test] fn ad_sets_attack_decay_sustain_zero() { - let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad emit"#, 1); + let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1); assert!(outputs[0].contains("attack/0.01")); assert!(outputs[0].contains("decay/0.1")); assert!(outputs[0].contains("sustain/0")); @@ -82,7 +82,7 @@ fn ad_sets_attack_decay_sustain_zero() { #[test] fn bank_param() { - let outputs = expect_outputs(r#""loop" s "a" bank emit"#, 1); + let outputs = expect_outputs(r#""loop" s "a" bank ."#, 1); assert!(outputs[0].contains("sound/loop")); assert!(outputs[0].contains("bank/a")); } diff --git a/tests/forth/stack.rs b/tests/forth/stack.rs index 27fbfd4..f4ded94 100644 --- a/tests/forth/stack.rs +++ b/tests/forth/stack.rs @@ -35,11 +35,6 @@ fn dupn_underflow() { expect_error("3 dupn", "stack underflow"); } -#[test] -fn bang_alias() { - expect_stack("c4 3 !", &[int(60), int(60), int(60)]); -} - #[test] fn drop() { expect_stack("1 2 drop", &[int(1)]); diff --git a/tests/forth/temporal.rs b/tests/forth/temporal.rs index 684834c..8663e35 100644 --- a/tests/forth/temporal.rs +++ b/tests/forth/temporal.rs @@ -61,14 +61,14 @@ fn stepdur_baseline() { #[test] fn single_emit() { - let outputs = expect_outputs(r#""kick" s @"#, 1); + let outputs = expect_outputs(r#""kick" s ."#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0"); } #[test] fn implicit_subdivision_2() { - let outputs = expect_outputs(r#""kick" s @ @"#, 2); + let outputs = expect_outputs(r#""kick" s . ."#, 2); let deltas = get_deltas(&outputs); let step = 0.5 / 2.0; assert!(approx_eq(deltas[0], 0.0), "first slot at 0"); @@ -77,7 +77,7 @@ fn implicit_subdivision_2() { #[test] fn implicit_subdivision_4() { - let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 4); + let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let deltas = get_deltas(&outputs); let step = 0.5 / 4.0; for (i, delta) in deltas.iter().enumerate() { @@ -92,7 +92,7 @@ fn implicit_subdivision_4() { #[test] fn implicit_subdivision_3() { - let outputs = expect_outputs(r#""kick" s @ @ @"#, 3); + let outputs = expect_outputs(r#""kick" s . . ."#, 3); let deltas = get_deltas(&outputs); let step = 0.5 / 3.0; assert!(approx_eq(deltas[0], 0.0)); @@ -102,7 +102,7 @@ fn implicit_subdivision_3() { #[test] fn silence_creates_gap() { - let outputs = expect_outputs(r#""kick" s @ ~ @"#, 2); + let outputs = expect_outputs(r#""kick" s . _ ."#, 2); let deltas = get_deltas(&outputs); let step = 0.5 / 3.0; assert!(approx_eq(deltas[0], 0.0), "first at 0"); @@ -116,7 +116,7 @@ fn silence_creates_gap() { #[test] fn silence_at_start() { - let outputs = expect_outputs(r#""kick" s ~ @"#, 1); + let outputs = expect_outputs(r#""kick" s _ ."#, 1); let deltas = get_deltas(&outputs); let step = 0.5 / 2.0; assert!( @@ -129,13 +129,13 @@ fn silence_at_start() { #[test] fn silence_only() { - let outputs = expect_outputs(r#""kick" s ~"#, 0); + let outputs = expect_outputs(r#""kick" s _"#, 0); assert!(outputs.is_empty(), "silence only should produce no output"); } #[test] fn sound_persists() { - let outputs = expect_outputs(r#""kick" s @ @ "hat" s @ @"#, 4); + let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds[0], "kick"); assert_eq!(sounds[1], "kick"); @@ -145,14 +145,14 @@ fn sound_persists() { #[test] fn alternating_sounds() { - let outputs = expect_outputs(r#""kick" s @ "snare" s @ "kick" s @ "snare" s @"#, 4); + let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); } #[test] fn dur_matches_slot_duration() { - let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 4); + let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let durs = get_durs(&outputs); let expected_dur = 0.5 / 4.0; for (i, dur) in durs.iter().enumerate() { @@ -168,7 +168,7 @@ fn dur_matches_slot_duration() { fn tempo_affects_subdivision() { let ctx = ctx_with(|c| c.tempo = 60.0); let f = forth(); - let outputs = f.evaluate(r#""kick" s @ @"#, &ctx).unwrap(); + let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap(); let deltas = get_deltas(&outputs); // At 60 BPM: stepdur = 0.25, root dur = 1.0 let step = 1.0 / 2.0; @@ -180,7 +180,7 @@ fn tempo_affects_subdivision() { fn speed_affects_subdivision() { let ctx = ctx_with(|c| c.speed = 2.0); let f = forth(); - let outputs = f.evaluate(r#""kick" s @ @"#, &ctx).unwrap(); + let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap(); let deltas = get_deltas(&outputs); // At speed 2.0: stepdur = 0.0625, root dur = 0.25 let step = 0.25 / 2.0; @@ -193,11 +193,11 @@ fn cycle_picks_by_step() { for runs in 0..4 { let ctx = ctx_with(|c| c.runs = runs); let f = forth(); - let outputs = f.evaluate(r#""kick" s < @ ~ >"#, &ctx).unwrap(); + let outputs = f.evaluate(r#""kick" s < . _ >"#, &ctx).unwrap(); if runs % 2 == 0 { - assert_eq!(outputs.len(), 1, "runs={}: @ should be picked", runs); + assert_eq!(outputs.len(), 1, "runs={}: . should be picked", runs); } else { - assert_eq!(outputs.len(), 0, "runs={}: ~ should be picked", runs); + assert_eq!(outputs.len(), 0, "runs={}: _ should be picked", runs); } } } @@ -207,11 +207,11 @@ fn pcycle_picks_by_pattern() { for iter in 0..4 { let ctx = ctx_with(|c| c.iter = iter); let f = forth(); - let outputs = f.evaluate(r#""kick" s << @ ~ >>"#, &ctx).unwrap(); + let outputs = f.evaluate(r#""kick" s << . _ >>"#, &ctx).unwrap(); if iter % 2 == 0 { - assert_eq!(outputs.len(), 1, "iter={}: @ should be picked", iter); + assert_eq!(outputs.len(), 1, "iter={}: . should be picked", iter); } else { - assert_eq!(outputs.len(), 0, "iter={}: ~ should be picked", iter); + assert_eq!(outputs.len(), 0, "iter={}: _ should be picked", iter); } } } @@ -221,7 +221,7 @@ fn cycle_with_sounds() { for runs in 0..3 { let ctx = ctx_with(|c| c.runs = runs); let f = forth(); - let outputs = f.evaluate(r#"< { "kick" s @ } { "hat" s @ } { "snare" s @ } >"#, &ctx).unwrap(); + let outputs = f.evaluate(r#"< { "kick" s . } { "hat" s . } { "snare" s . } >"#, &ctx).unwrap(); assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs); let sounds = get_sounds(&outputs); let expected = ["kick", "hat", "snare"][runs % 3]; @@ -238,7 +238,7 @@ fn dot_alias_for_emit() { #[test] fn dot_with_silence() { - let outputs = expect_outputs(r#""kick" s . ~ . ~"#, 2); + let outputs = expect_outputs(r#""kick" s . _ . _"#, 2); let deltas = get_deltas(&outputs); let step = 0.5 / 4.0; assert!(approx_eq(deltas[0], 0.0)); @@ -292,7 +292,7 @@ fn internal_alternation_empty_error() { #[test] fn div_basic_subdivision() { - let outputs = expect_outputs(r#"div "kick" s . "hat" s . end"#, 2); + let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "hat"]); @@ -301,59 +301,58 @@ fn div_basic_subdivision() { } #[test] -fn div_superposition() { - let outputs = expect_outputs(r#"div "kick" s . end div "hat" s . end"#, 2); +fn div_sequential() { + // Two consecutive divs each claim a slot in root, so they're sequential + let outputs = expect_outputs(r#"div "kick" s . ~ div "hat" s . ~"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); - assert_eq!(sounds.len(), 2); - // Both at delta 0 (superposed) + assert_eq!(sounds, vec!["kick", "hat"]); assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.0)); + assert!(approx_eq(deltas[1], 0.25), "second div at slot 1, got {}", deltas[1]); } #[test] fn div_with_root_emit() { - // kick at root level, hat in div - both should superpose at 0 - // Note: div resolves first (when end is hit), root resolves at script end - let outputs = expect_outputs(r#""kick" s . div "hat" s . end"#, 2); + // kick claims slot 0 at root, div claims slot 1 at root + let outputs = expect_outputs(r#""kick" s . div "hat" s . ~"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); - // Order is hat then kick because div resolves before root - assert_eq!(sounds, vec!["hat", "kick"]); - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.0)); + assert_eq!(sounds, vec!["kick", "hat"]); + assert!(approx_eq(deltas[0], 0.0), "kick at slot 0"); + assert!(approx_eq(deltas[1], 0.25), "hat at slot 1, got {}", deltas[1]); } #[test] fn div_nested() { - // kick takes first slot in outer div, inner div takes second slot - // Inner div resolves first, then outer div resolves - let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . end end"#, 3); + // kick claims slot 0 in outer div, inner div claims slot 1 + // Inner div's 2 hats subdivide its slot (0.25 duration) into 2 sub-slots + let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . ~ ~"#, 3); let sounds = get_sounds(&outputs); let deltas = get_deltas(&outputs); - // Inner div resolves first (hat, hat), then outer div (kick) - assert_eq!(sounds[0], "hat"); + // Output order: kick (slot 0), then hats (slot 1 subdivided) + assert_eq!(sounds[0], "kick"); assert_eq!(sounds[1], "hat"); - assert_eq!(sounds[2], "kick"); - // Inner div inherits parent's start (0) and duration (0.5), subdivides into 2 - assert!(approx_eq(deltas[0], 0.0), "first hat at 0, got {}", deltas[0]); - assert!(approx_eq(deltas[1], 0.25), "second hat at 0.25, got {}", deltas[1]); - // Outer div has 2 slots: kick at 0, inner div at slot 1 (but inner resolved independently) - assert!(approx_eq(deltas[2], 0.0), "kick at 0, got {}", deltas[2]); + assert_eq!(sounds[2], "hat"); + // Outer div has 2 slots of 0.25 each + // kick at slot 0 -> delta 0 + // inner div at slot 1 -> starts at 0.25, subdivided into 2 -> hats at 0.25 and 0.375 + assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]); + assert!(approx_eq(deltas[1], 0.25), "first hat at 0.25, got {}", deltas[1]); + assert!(approx_eq(deltas[2], 0.375), "second hat at 0.375, got {}", deltas[2]); } #[test] fn div_with_silence() { - let outputs = expect_outputs(r#"div "kick" s . ~ end"#, 1); + let outputs = expect_outputs(r#"div "kick" s . _ ~"#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); } #[test] -fn div_unmatched_end_error() { +fn unmatched_scope_terminator_error() { let f = forth(); - let result = f.evaluate(r#""kick" s . end"#, &default_ctx()); - assert!(result.is_err(), "unmatched end should error"); + let result = f.evaluate(r#""kick" s . ~"#, &default_ctx()); + assert!(result.is_err(), "unmatched ~ should error"); } #[test] @@ -392,7 +391,7 @@ fn alternator_with_arithmetic() { #[test] fn stack_superposes_sounds() { - let outputs = expect_outputs(r#"stack "kick" s . "hat" s . end"#, 2); + let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); assert_eq!(sounds.len(), 2); @@ -403,7 +402,7 @@ fn stack_superposes_sounds() { #[test] fn stack_with_multiple_emits() { - let outputs = expect_outputs(r#"stack "kick" s . . . . end"#, 4); + let outputs = expect_outputs(r#"stack "kick" s . . . . ~"#, 4); let deltas = get_deltas(&outputs); // All 4 kicks at delta 0 for (i, delta) in deltas.iter().enumerate() { @@ -415,7 +414,7 @@ fn stack_with_multiple_emits() { fn stack_inside_div() { // div subdivides, stack inside superposes // stack doesn't claim a slot in parent div, so snare is also at 0 - let outputs = expect_outputs(r#"div stack "kick" s . "hat" s . end "snare" s . end"#, 3); + let outputs = expect_outputs(r#"div stack "kick" s . "hat" s . ~ "snare" s . ~"#, 3); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); // stack resolves first (kick, hat at 0), then div resolves (snare at 0) @@ -429,20 +428,21 @@ fn stack_inside_div() { } #[test] -fn div_then_stack_sequential() { - // Nested div doesn't claim a slot in parent, only emit/silence do - // So nested div and snare both resolve with parent's timing - let outputs = expect_outputs(r#"div div "kick" s . "hat" s . end "snare" s . end"#, 3); +fn div_nested_with_sibling() { + // Inner div claims slot 0, snare claims slot 1 + // Inner div's kick/hat subdivide slot 0 + let outputs = expect_outputs(r#"div div "kick" s . "hat" s . ~ "snare" s . ~"#, 3); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); - // Inner div resolves first (kick at 0, hat at 0.25 of parent duration) - // Outer div has 1 slot (snare's .), so snare at 0 + // Outer div has 2 slots of 0.25 each + // Inner div at slot 0: kick at 0, hat at 0.125 + // snare at slot 1: delta 0.25 assert_eq!(sounds[0], "kick"); assert_eq!(sounds[1], "hat"); assert_eq!(sounds[2], "snare"); - assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.25), "hat at 0.25, got {}", deltas[1]); - assert!(approx_eq(deltas[2], 0.0), "snare at 0, got {}", deltas[2]); + assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]); + assert!(approx_eq(deltas[1], 0.125), "hat at 0.125, got {}", deltas[1]); + assert!(approx_eq(deltas[2], 0.25), "snare at 0.25, got {}", deltas[2]); } #[test]