2809 lines
80 KiB
Rust
2809 lines
80 KiB
Rust
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<SourceSpan>,
|
|
}
|
|
|
|
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<Mutex<HashMap<String, Value>>>;
|
|
pub type Rng = Arc<Mutex<StdRng>>;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Value {
|
|
Int(i64, Option<SourceSpan>),
|
|
Float(f64, Option<SourceSpan>),
|
|
Str(String, Option<SourceSpan>),
|
|
Marker,
|
|
Quotation(Vec<Op>),
|
|
}
|
|
|
|
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<String>,
|
|
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<f64, String> {
|
|
match self {
|
|
Value::Float(f, _) => Ok(*f),
|
|
Value::Int(i, _) => Ok(*i as f64),
|
|
_ => Err("expected number".into()),
|
|
}
|
|
}
|
|
|
|
fn as_int(&self) -> Result<i64, String> {
|
|
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<SourceSpan> {
|
|
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<SourceSpan>),
|
|
PushFloat(f64, Option<SourceSpan>),
|
|
PushStr(String, Option<SourceSpan>),
|
|
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<Op>),
|
|
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: "@<var>",
|
|
stack: "( -- val)",
|
|
desc: "Fetch variable value",
|
|
example: "@freq => 440",
|
|
compile: Simple,
|
|
},
|
|
Word {
|
|
name: "!<var>",
|
|
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<Op> {
|
|
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<i64> {
|
|
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<i64> {
|
|
// 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<Op>) -> 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<Vec<(f64, f64)>>,
|
|
iteration_index: Option<usize>,
|
|
}
|
|
|
|
#[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<Token> {
|
|
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::<i64>() {
|
|
tokens.push(Token::Int(i, span));
|
|
} else if let Ok(f) = word.parse::<f64>() {
|
|
tokens.push(Token::Float(f, span));
|
|
} else {
|
|
tokens.push(Token::Word(word, span));
|
|
}
|
|
}
|
|
|
|
tokens
|
|
}
|
|
|
|
fn compile(tokens: &[Token]) -> Result<Vec<Op>, 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<Op>, 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<Op>, Vec<Op>, 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<Mutex<Vec<Value>>>;
|
|
|
|
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<Value> {
|
|
self.stack.lock().unwrap().clone()
|
|
}
|
|
|
|
pub fn clear_stack(&self) {
|
|
self.stack.lock().unwrap().clear();
|
|
}
|
|
|
|
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
|
self.evaluate_impl(script, ctx, None)
|
|
}
|
|
|
|
pub fn evaluate_with_trace(
|
|
&self,
|
|
script: &str,
|
|
ctx: &StepContext,
|
|
trace: &mut ExecutionTrace,
|
|
) -> Result<Vec<String>, String> {
|
|
self.evaluate_impl(script, ctx, Some(trace))
|
|
}
|
|
|
|
fn evaluate_impl(
|
|
&self,
|
|
script: &str,
|
|
ctx: &StepContext,
|
|
trace: Option<&mut ExecutionTrace>,
|
|
) -> Result<Vec<String>, String> {
|
|
if script.trim().is_empty() {
|
|
return Err("empty script".into());
|
|
}
|
|
|
|
let tokens = tokenize(script);
|
|
let ops = compile(&tokens)?;
|
|
self.execute(&ops, ctx, trace)
|
|
}
|
|
|
|
fn execute(
|
|
&self,
|
|
ops: &[Op],
|
|
ctx: &StepContext,
|
|
trace: Option<&mut ExecutionTrace>,
|
|
) -> Result<Vec<String>, String> {
|
|
let mut stack = self.stack.lock().unwrap();
|
|
let mut outputs: Vec<String> = Vec::new();
|
|
let mut time_stack: Vec<TimeContext> = 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<Value>,
|
|
outputs: &mut Vec<String>,
|
|
time_stack: &mut Vec<TimeContext>,
|
|
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<Value> = 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<Value> = 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<Value> = 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<F>(stack: &mut Vec<Value>, 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<F>(stack: &mut Vec<Value>, 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<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
|
format!("/{}", parts.join("/"))
|
|
}
|