Compare commits
2 Commits
223679acf8
...
61daa9d79d
| Author | SHA1 | Date | |
|---|---|---|---|
| 61daa9d79d | |||
| 9e597258e4 |
@@ -94,8 +94,8 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
let mut ops = Vec::new();
|
||||
let mut i = 0;
|
||||
let mut pipe_parity = false;
|
||||
let mut list_depth: usize = 0;
|
||||
let mut pipe_parity = false;
|
||||
|
||||
while i < tokens.len() {
|
||||
match &tokens[i] {
|
||||
@@ -119,15 +119,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
dict.lock().unwrap().insert(name, body);
|
||||
} else if word == ";" {
|
||||
return Err("unexpected ;".into());
|
||||
} else if word == "|" {
|
||||
if pipe_parity {
|
||||
ops.push(Op::LocalCycleEnd);
|
||||
list_depth = list_depth.saturating_sub(1);
|
||||
} else {
|
||||
ops.push(Op::ListStart);
|
||||
list_depth += 1;
|
||||
}
|
||||
pipe_parity = !pipe_parity;
|
||||
} else if word == "if" {
|
||||
let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
@@ -140,6 +131,13 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
ops.push(Op::Branch(else_ops.len()));
|
||||
ops.extend(else_ops);
|
||||
}
|
||||
} else if word == "|" {
|
||||
if pipe_parity {
|
||||
ops.push(Op::InternalCycleEnd);
|
||||
} else {
|
||||
ops.push(Op::ListStart);
|
||||
}
|
||||
pipe_parity = !pipe_parity;
|
||||
} else if is_list_start(word) {
|
||||
ops.push(Op::ListStart);
|
||||
list_depth += 1;
|
||||
|
||||
@@ -6,5 +6,5 @@ mod vm;
|
||||
mod words;
|
||||
|
||||
pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
|
||||
pub use vm::Forth;
|
||||
pub use vm::{EmissionCounter, Forth};
|
||||
pub use words::{Word, WordCompile, WORDS};
|
||||
|
||||
@@ -25,6 +25,11 @@ pub enum Op {
|
||||
Round,
|
||||
Min,
|
||||
Max,
|
||||
Pow,
|
||||
Sqrt,
|
||||
Sin,
|
||||
Cos,
|
||||
Log,
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
@@ -34,11 +39,17 @@ pub enum Op {
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
Xor,
|
||||
Nand,
|
||||
Nor,
|
||||
IfElse,
|
||||
Pick,
|
||||
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
|
||||
Branch(usize),
|
||||
NewCmd,
|
||||
SetParam(String),
|
||||
Emit,
|
||||
Silence,
|
||||
Get,
|
||||
Set,
|
||||
GetContext(String),
|
||||
@@ -56,30 +67,26 @@ pub enum Op {
|
||||
ListEndCycle,
|
||||
PCycle,
|
||||
ListEndPCycle,
|
||||
At,
|
||||
Window,
|
||||
Scale,
|
||||
Pop,
|
||||
Subdivide,
|
||||
SetTempo,
|
||||
Each,
|
||||
Every,
|
||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||
When,
|
||||
Unless,
|
||||
Adsr,
|
||||
Ad,
|
||||
Stack,
|
||||
For,
|
||||
LocalCycleEnd,
|
||||
Echo,
|
||||
Necho,
|
||||
Apply,
|
||||
Ramp,
|
||||
Tri,
|
||||
Range,
|
||||
Noise,
|
||||
Perlin,
|
||||
Chain,
|
||||
Loop,
|
||||
Degree(&'static [i64]),
|
||||
Oct,
|
||||
InternalCycleEnd,
|
||||
DivStart,
|
||||
DivEnd,
|
||||
StackStart,
|
||||
EmitN,
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ pub enum Value {
|
||||
Str(String, Option<SourceSpan>),
|
||||
Marker,
|
||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||
Alternator(Vec<Value>),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
@@ -58,6 +59,7 @@ impl PartialEq for Value {
|
||||
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||
(Value::Marker, Value::Marker) => true,
|
||||
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||
(Value::Alternator(a), Value::Alternator(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -94,6 +96,7 @@ impl Value {
|
||||
Value::Str(s, _) => !s.is_empty(),
|
||||
Value::Marker => false,
|
||||
Value::Quotation(..) => true,
|
||||
Value::Alternator(items) => !items.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,35 +111,86 @@ impl Value {
|
||||
Value::Str(s, _) => s.clone(),
|
||||
Value::Marker => String::new(),
|
||||
Value::Quotation(..) => String::new(),
|
||||
Value::Alternator(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn span(&self) -> Option<SourceSpan> {
|
||||
match self {
|
||||
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s,
|
||||
Value::Marker | Value::Quotation(..) => None,
|
||||
Value::Marker | Value::Quotation(..) | Value::Alternator(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(super) struct CmdRegister {
|
||||
sound: Option<String>,
|
||||
params: Vec<(String, String)>,
|
||||
sound: Option<Value>,
|
||||
params: Vec<(String, Value)>,
|
||||
}
|
||||
|
||||
impl CmdRegister {
|
||||
pub(super) fn set_sound(&mut self, name: String) {
|
||||
self.sound = Some(name);
|
||||
pub(super) fn set_sound(&mut self, val: Value) {
|
||||
self.sound = Some(val);
|
||||
}
|
||||
|
||||
pub(super) fn set_param(&mut self, key: String, value: String) {
|
||||
self.params.push((key, value));
|
||||
pub(super) fn set_param(&mut self, key: String, val: Value) {
|
||||
self.params.push((key, val));
|
||||
}
|
||||
|
||||
pub(super) 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))
|
||||
pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> {
|
||||
self.sound.as_ref().map(|s| (s.clone(), self.params.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct PendingEmission {
|
||||
pub sound: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
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,
|
||||
pub duration: f64,
|
||||
pub weight: f64,
|
||||
pub slot_count: usize,
|
||||
pub pending: Vec<PendingEmission>,
|
||||
pub resolved: Vec<ResolvedEmission>,
|
||||
pub stacked: bool,
|
||||
}
|
||||
|
||||
impl ScopeContext {
|
||||
pub fn new(start: f64, duration: f64) -> Self {
|
||||
Self {
|
||||
start,
|
||||
duration,
|
||||
weight: 1.0,
|
||||
slot_count: 0,
|
||||
pending: Vec::new(),
|
||||
resolved: Vec::new(),
|
||||
stacked: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn claim_slot(&mut self) -> usize {
|
||||
if self.stacked {
|
||||
self.slot_count = 1;
|
||||
0
|
||||
} else {
|
||||
let idx = self.slot_count;
|
||||
self.slot_count += 1;
|
||||
idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -34,9 +34,6 @@ mod variables;
|
||||
#[path = "forth/quotations.rs"]
|
||||
mod quotations;
|
||||
|
||||
#[path = "forth/iteration.rs"]
|
||||
mod iteration;
|
||||
|
||||
#[path = "forth/notes.rs"]
|
||||
mod notes;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use cagire::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables};
|
||||
use cagire::forth::{Dictionary, EmissionCounter, Forth, Rng, StepContext, Value, Variables};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -46,6 +46,14 @@ pub fn forth_seeded(seed: u64) -> Forth {
|
||||
Forth::new(new_vars(), new_dict(), seeded_rng(seed))
|
||||
}
|
||||
|
||||
pub fn new_emission_counter() -> EmissionCounter {
|
||||
Arc::new(Mutex::new(0))
|
||||
}
|
||||
|
||||
pub fn forth_with_counter(counter: EmissionCounter) -> Forth {
|
||||
Forth::new_with_counter(new_vars(), new_dict(), seeded_rng(42), counter)
|
||||
}
|
||||
|
||||
pub fn run(script: &str) -> Forth {
|
||||
let f = forth();
|
||||
f.evaluate(script, &default_ctx()).unwrap();
|
||||
|
||||
@@ -22,7 +22,7 @@ fn interval_stacking_builds_chord() {
|
||||
|
||||
#[test]
|
||||
fn interval_tritone() {
|
||||
expect_stack("c4 tritone", &ints(&[60, 66]));
|
||||
// "tritone" word is taken by the scale, use aug4 or dim5 for interval
|
||||
expect_stack("c4 aug4", &ints(&[60, 66]));
|
||||
expect_stack("c4 dim5", &ints(&[60, 66]));
|
||||
}
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
use super::harness::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn parse_params(output: &str) -> HashMap<String, f64> {
|
||||
let mut params = HashMap::new();
|
||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
||||
let mut i = 0;
|
||||
while i + 1 < parts.len() {
|
||||
if let Ok(v) = parts[i + 1].parse::<f64>() {
|
||||
params.insert(parts[i].to_string(), v);
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_notes(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("note").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_sounds(outputs: &[String]) -> Vec<String> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| {
|
||||
let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect();
|
||||
for i in (0..parts.len()).step_by(2) {
|
||||
if parts[i] == "sound" && i + 1 < parts.len() {
|
||||
return parts[i + 1].to_string();
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
const EPSILON: f64 = 1e-9;
|
||||
|
||||
fn approx_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < EPSILON
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_creates_subdivisions_at_same_time() {
|
||||
let outputs = expect_outputs(r#""kick" s 3 stack each"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.0));
|
||||
assert!(approx_eq(deltas[2], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_vs_div_timing() {
|
||||
let stack_outputs = expect_outputs(r#""kick" s 3 stack each"#, 3);
|
||||
let div_outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
||||
|
||||
let stack_deltas = get_deltas(&stack_outputs);
|
||||
let div_deltas = get_deltas(&div_outputs);
|
||||
|
||||
for d in stack_deltas {
|
||||
assert!(approx_eq(d, 0.0), "stack should have all delta=0");
|
||||
}
|
||||
|
||||
assert!(approx_eq(div_deltas[0], 0.0));
|
||||
assert!(!approx_eq(div_deltas[1], 0.0), "div should spread in time");
|
||||
assert!(!approx_eq(div_deltas[2], 0.0), "div should spread in time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_with_div_arpeggio() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 div for"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(!approx_eq(deltas[1], 0.0));
|
||||
assert!(!approx_eq(deltas[2], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_with_stack_chord() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 stack for"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
for d in deltas {
|
||||
assert!(approx_eq(d, 0.0), "stack for should have all delta=0");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_with_for() {
|
||||
let outputs = expect_outputs(r#"{ | 60 62 64 | note "sine" s emit } 3 div for"#, 3);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 62.0));
|
||||
assert!(approx_eq(notes[2], 64.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_wraps_around() {
|
||||
let outputs = expect_outputs(r#"{ | 60 62 | note "sine" s emit } 4 div for"#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 62.0));
|
||||
assert!(approx_eq(notes[2], 60.0));
|
||||
assert!(approx_eq(notes[3], 62.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_local_cycles() {
|
||||
let outputs =
|
||||
expect_outputs(r#"{ | "bd" "sn" | s | 60 64 | note emit } 2 stack for"#, 2);
|
||||
let sounds = get_sounds(&outputs);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert_eq!(sounds[0], "bd");
|
||||
assert_eq!(sounds[1], "sn");
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_outside_for_defaults_to_first() {
|
||||
expect_int("| 60 62 64 |", 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polymetric_cycles() {
|
||||
let outputs = expect_outputs(
|
||||
r#"{ | 0 1 | n | "a" "b" "c" | s emit } 6 div for"#,
|
||||
6,
|
||||
);
|
||||
let sounds = get_sounds(&outputs);
|
||||
|
||||
assert_eq!(sounds[0], "a");
|
||||
assert_eq!(sounds[1], "b");
|
||||
assert_eq!(sounds[2], "c");
|
||||
assert_eq!(sounds[3], "a");
|
||||
assert_eq!(sounds[4], "b");
|
||||
assert_eq!(sounds[5], "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 stack each"#, "stack count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_requires_subdivide() {
|
||||
expect_error(r#"{ "kick" s emit } for"#, "for requires subdivide first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_requires_quotation() {
|
||||
expect_error(r#"42 3 div for"#, "expected quotation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_local_cycle() {
|
||||
expect_error("| |", "empty local cycle list");
|
||||
}
|
||||
|
||||
// Echo tests - stutter effect with halving durations
|
||||
|
||||
#[test]
|
||||
fn echo_creates_decaying_subdivisions() {
|
||||
// default dur = 0.5, echo 3
|
||||
// d1 + d1/2 + d1/4 = d1 * 1.75 = 0.5
|
||||
// d1 = 0.5 / 1.75
|
||||
let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
let d1 = 0.5 / 1.75;
|
||||
let d2 = d1 / 2.0;
|
||||
let d3 = d1 / 4.0;
|
||||
|
||||
assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]);
|
||||
assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]);
|
||||
assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0), "first delta should be 0");
|
||||
assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]);
|
||||
assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_with_for() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 echo for"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
|
||||
// Each subsequent duration should be half the previous
|
||||
assert!(approx_eq(durs[1], durs[0] / 2.0), "second should be half of first");
|
||||
assert!(approx_eq(durs[2], durs[1] / 2.0), "third should be half of second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 echo each"#, "echo count must be > 0");
|
||||
}
|
||||
|
||||
// Necho tests - reverse echo (durations grow)
|
||||
|
||||
#[test]
|
||||
fn necho_creates_growing_subdivisions() {
|
||||
// default dur = 0.5, necho 3
|
||||
// d1 + 2*d1 + 4*d1 = d1 * 7 = 0.5
|
||||
// d1 = 0.5 / 7
|
||||
let outputs = expect_outputs(r#""kick" s 3 necho each"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
let d1 = 0.5 / 7.0;
|
||||
let d2 = d1 * 2.0;
|
||||
let d3 = d1 * 4.0;
|
||||
|
||||
assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]);
|
||||
assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]);
|
||||
assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0), "first delta should be 0");
|
||||
assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]);
|
||||
assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn necho_with_for() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 necho for"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
|
||||
// Each subsequent duration should be double the previous
|
||||
assert!(approx_eq(durs[1], durs[0] * 2.0), "second should be double first");
|
||||
assert!(approx_eq(durs[2], durs[1] * 2.0), "third should be double second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn necho_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 necho each"#, "necho count must be > 0");
|
||||
}
|
||||
@@ -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);
|
||||
@@ -122,13 +122,3 @@ fn multi_op_quotation_second() {
|
||||
assert_eq!(stack_int(&f), 13); // runs=1 picks {3 +}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_syntax_with_words() {
|
||||
// | word1 word2 | uses LocalCycleEnd which should auto-apply quotations
|
||||
// LocalCycleEnd uses time_ctx.iteration_index, which defaults to 0 outside for loops
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(": add3 3 + ; : add5 5 + ; 10 | add3 add5 |", &ctx).unwrap();
|
||||
// iteration_index defaults to 0, picks first word (add3)
|
||||
assert_eq!(stack_int(&f), 13);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -17,17 +17,6 @@ fn rand_deterministic() {
|
||||
assert_eq!(f1.stack(), f2.stack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rrand_inclusive() {
|
||||
let f = forth_seeded(42);
|
||||
for _ in 0..20 {
|
||||
f.clear_stack();
|
||||
f.evaluate("1 3 rrand", &default_ctx()).unwrap();
|
||||
let val = stack_int(&f);
|
||||
assert!(val >= 1 && val <= 3, "rrand {} not in [1, 3]", val);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_resets() {
|
||||
let f1 = forth_seeded(1);
|
||||
|
||||
@@ -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,62 +22,32 @@ 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"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdivide_each() {
|
||||
let _outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_pop() {
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 zoom "kick" s emit pop 0.5 1.0 zoom "snare" s emit"#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[1].contains("sound/snare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_root_fails() {
|
||||
expect_error("pop", "cannot pop root time context");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdivide_zero() {
|
||||
expect_error(r#""kick" s 0 div each"#, "subdivide count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_without_div() {
|
||||
expect_error(r#""kick" s each"#, "each requires subdivide first");
|
||||
}
|
||||
|
||||
#[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"));
|
||||
@@ -88,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"));
|
||||
@@ -104,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"));
|
||||
@@ -112,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"));
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::harness::*;
|
||||
#[allow(unused_imports)]
|
||||
use super::harness::{forth_with_counter, new_emission_counter};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn parse_params(output: &str) -> HashMap<String, f64> {
|
||||
@@ -21,6 +23,27 @@ fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_sounds(outputs: &[String]) -> Vec<String> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| {
|
||||
let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect();
|
||||
if parts.len() >= 2 && parts[0] == "sound" {
|
||||
parts[1].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
const EPSILON: f64 = 1e-9;
|
||||
|
||||
fn approx_eq(a: f64, b: f64) -> bool {
|
||||
@@ -28,7 +51,7 @@ fn approx_eq(a: f64, b: f64) -> bool {
|
||||
}
|
||||
|
||||
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
|
||||
// Default duration = 4 * stepdur = 0.5s
|
||||
// Root duration = 4 * stepdur = 0.5s
|
||||
|
||||
#[test]
|
||||
fn stepdur_baseline() {
|
||||
@@ -37,80 +60,39 @@ fn stepdur_baseline() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_no_delta() {
|
||||
let outputs = expect_outputs(r#""kick" s emit"#, 1);
|
||||
fn single_emit() {
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.0),
|
||||
"emit at start should have delta 0"
|
||||
);
|
||||
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_half() {
|
||||
// at 0.5 in root (0..0.5) => delta = 0.5 * 0.5 = 0.25
|
||||
let outputs = expect_outputs(r#""kick" s 0.5 at emit pop"#, 1);
|
||||
fn implicit_subdivision_2() {
|
||||
let outputs = expect_outputs(r#""kick" s . ."#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.25),
|
||||
"at 0.5 should be delta 0.25, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
let step = 0.5 / 2.0;
|
||||
assert!(approx_eq(deltas[0], 0.0), "first slot at 0");
|
||||
assert!(approx_eq(deltas[1], step), "second slot at {}, got {}", step, deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_quarter() {
|
||||
// at 0.25 in root (0..0.5) => delta = 0.25 * 0.5 = 0.125
|
||||
let outputs = expect_outputs(r#""kick" s 0.25 at emit pop"#, 1);
|
||||
fn implicit_subdivision_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() {
|
||||
let expected = step * i as f64;
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.125),
|
||||
"at 0.25 should be delta 0.125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_zero() {
|
||||
let outputs = expect_outputs(r#""kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "at 0.0 should be delta 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_2_each() {
|
||||
// 2 subdivisions: deltas at 0 and 0.25 (half of 0.5)
|
||||
let outputs = expect_outputs(r#""kick" s 2 div each"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "first subdivision at 0");
|
||||
assert!(
|
||||
approx_eq(deltas[1], 0.25),
|
||||
"second subdivision at 0.25, got {}",
|
||||
deltas[1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_4_each() {
|
||||
// 4 subdivisions: 0, 0.125, 0.25, 0.375
|
||||
let outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let expected = [0.0, 0.125, 0.25, 0.375];
|
||||
for (i, (got, exp)) in deltas.iter().zip(expected.iter()).enumerate() {
|
||||
assert!(
|
||||
approx_eq(*got, *exp),
|
||||
"subdivision {}: expected {}, got {}",
|
||||
i,
|
||||
exp,
|
||||
got
|
||||
approx_eq(*delta, expected),
|
||||
"slot {}: expected {}, got {}",
|
||||
i, expected, delta
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_3_each() {
|
||||
// 3 subdivisions: 0, 0.5/3, 2*0.5/3
|
||||
let outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
||||
fn implicit_subdivision_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));
|
||||
@@ -119,113 +101,433 @@ fn div_3_each() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_full() {
|
||||
// zoom 0.0 1.0 is the full duration, same as root
|
||||
let outputs = expect_outputs(r#"0.0 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.25), "full zoom at 0.5 = 0.25");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_first_half() {
|
||||
// zoom 0.0 0.5 restricts to first half (0..0.25)
|
||||
// at 0.5 within that = 0.125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
fn silence_creates_gap() {
|
||||
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");
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.125),
|
||||
"first-half zoom at 0.5 = 0.125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_second_half() {
|
||||
// zoom 0.5 1.0 restricts to second half (0.25..0.5)
|
||||
// at 0.0 within that = start of second half = 0.25
|
||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.25),
|
||||
"second-half zoom at 0.0 = 0.25, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_second_half_middle() {
|
||||
// zoom 0.5 1.0, at 0.5 within that = 0.75 of full duration = 0.375
|
||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.375), "got {}", deltas[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_zooms() {
|
||||
// zoom 0.0 0.5, then zoom 0.5 1.0 within that
|
||||
// outer: 0..0.25, inner: 0.5..1.0 of that = 0.125..0.25
|
||||
// at 0.0 in inner = 0.125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom 0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.125),
|
||||
"nested zoom at 0.0 = 0.125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_pop_sequence() {
|
||||
// First in zoom 0.0 0.5 at 0.0 -> delta 0
|
||||
// Pop at context and zoom, then in zoom 0.5 1.0 at 0.0 -> delta 0.25
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 zoom "kick" s 0.0 at emit pop pop 0.5 1.0 zoom "snare" s 0.0 at emit pop"#,
|
||||
2,
|
||||
);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "first zoom start");
|
||||
assert!(
|
||||
approx_eq(deltas[1], 0.25),
|
||||
"second zoom start, got {}",
|
||||
approx_eq(deltas[1], 2.0 * step),
|
||||
"third slot (after silence) at {}, got {}",
|
||||
2.0 * step,
|
||||
deltas[1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_in_zoom() {
|
||||
// zoom 0.0 0.5 (duration 0.25), then div 2 each
|
||||
// subdivisions at 0 and 0.125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 2 div each"#, 2);
|
||||
fn silence_at_start() {
|
||||
let outputs = expect_outputs(r#""kick" s _ ."#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]);
|
||||
let step = 0.5 / 2.0;
|
||||
assert!(
|
||||
approx_eq(deltas[0], step),
|
||||
"emit after silence at {}, got {}",
|
||||
step,
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tempo_affects_stepdur() {
|
||||
// At 60 BPM: stepdur = 60/60/4/1 = 0.25
|
||||
fn silence_only() {
|
||||
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 sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds[0], "kick");
|
||||
assert_eq!(sounds[1], "kick");
|
||||
assert_eq!(sounds[2], "hat");
|
||||
assert_eq!(sounds[3], "hat");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternating_sounds() {
|
||||
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 durs = get_durs(&outputs);
|
||||
let expected_dur = 0.5 / 4.0;
|
||||
for (i, dur) in durs.iter().enumerate() {
|
||||
assert!(
|
||||
approx_eq(*dur, expected_dur),
|
||||
"slot {} dur: expected {}, got {}",
|
||||
i, expected_dur, dur
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tempo_affects_subdivision() {
|
||||
let ctx = ctx_with(|c| c.tempo = 60.0);
|
||||
let f = forth();
|
||||
f.evaluate("stepdur", &ctx).unwrap();
|
||||
assert!(approx_eq(stack_float(&f), 0.25));
|
||||
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;
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_affects_stepdur() {
|
||||
// At 120 BPM, speed 2.0: stepdur = 60/120/4/2 = 0.0625
|
||||
fn speed_affects_subdivision() {
|
||||
let ctx = ctx_with(|c| c.speed = 2.0);
|
||||
let f = forth();
|
||||
f.evaluate("stepdur", &ctx).unwrap();
|
||||
assert!(approx_eq(stack_float(&f), 0.0625));
|
||||
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;
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_each_at_different_tempo() {
|
||||
// At 60 BPM: stepdur = 0.25, default dur = 1.0, so div 2 each => 0, 0.5
|
||||
let ctx = ctx_with(|c| c.tempo = 60.0);
|
||||
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 2 div each"#, &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);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "runs={}: _ should be picked", runs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
if iter % 2 == 0 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: . should be picked", iter);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "iter={}: _ should be picked", iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
let expected = ["kick", "hat", "snare"][runs % 3];
|
||||
assert_eq!(sounds[0], expected, "runs={}: expected {}", runs, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_alias_for_emit() {
|
||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_with_silence() {
|
||||
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));
|
||||
assert!(approx_eq(deltas[1], 2.0 * step));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_alternation_basic() {
|
||||
let outputs = expect_outputs(r#"| "kick" "snare" | s . . . ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_alternation_three_sounds() {
|
||||
let outputs = expect_outputs(r#"| "kick" "snare" "hat" | s . . . . . ."#, 6);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "snare", "hat", "kick", "snare", "hat"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_alternation_single_item() {
|
||||
let outputs = expect_outputs(r#"| "kick" | s . . . ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_alternation_with_params() {
|
||||
let outputs = expect_outputs(r#"| 0.5 0.9 | gain "kick" s . ."#, 2);
|
||||
fn parse_gain(output: &str) -> f64 {
|
||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
||||
for i in 0..parts.len() - 1 {
|
||||
if parts[i] == "gain" {
|
||||
return parts[i + 1].parse().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
0.0
|
||||
}
|
||||
let gains: Vec<f64> = outputs.iter().map(|o| parse_gain(o)).collect();
|
||||
assert!(approx_eq(gains[0], 0.5), "first gain should be 0.5, got {}", gains[0]);
|
||||
assert!(approx_eq(gains[1], 0.9), "second gain should be 0.9, got {}", gains[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_alternation_empty_error() {
|
||||
let f = forth();
|
||||
let result = f.evaluate(r#"| | . ."#, &default_ctx());
|
||||
assert!(result.is_err(), "empty internal cycle should error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_basic_subdivision() {
|
||||
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"]);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.25), "second should be at 0.25, got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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, vec!["kick", "hat"]);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.25), "second div at slot 1, got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_with_root_emit() {
|
||||
// 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);
|
||||
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 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);
|
||||
// Output order: kick (slot 0), then hats (slot 1 subdivided)
|
||||
assert_eq!(sounds[0], "kick");
|
||||
assert_eq!(sounds[1], "hat");
|
||||
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 . _ ~"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.5), "got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmatched_scope_terminator_error() {
|
||||
let f = forth();
|
||||
let result = f.evaluate(r#""kick" s . ~"#, &default_ctx());
|
||||
assert!(result.is_err(), "unmatched ~ should error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternator_with_scale() {
|
||||
let outputs = expect_outputs(r#""sine" s | 0 1 2 3 | mixolydian note . . . ."#, 4);
|
||||
fn parse_note(output: &str) -> i64 {
|
||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
||||
for i in 0..parts.len() - 1 {
|
||||
if parts[i] == "note" {
|
||||
return parts[i + 1].parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
let notes: Vec<i64> = outputs.iter().map(|o| parse_note(o)).collect();
|
||||
// mixolydian from C4: 0->60, 1->62, 2->64, 3->65
|
||||
assert_eq!(notes, vec![60, 62, 64, 65]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternator_with_arithmetic() {
|
||||
let outputs = expect_outputs(r#""sine" s | 100 200 | 2 * freq . ."#, 2);
|
||||
fn parse_freq(output: &str) -> f64 {
|
||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
||||
for i in 0..parts.len() - 1 {
|
||||
if parts[i] == "freq" {
|
||||
return parts[i + 1].parse().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
0.0
|
||||
}
|
||||
let freqs: Vec<f64> = outputs.iter().map(|o| parse_freq(o)).collect();
|
||||
assert!(approx_eq(freqs[0], 200.0), "first freq: expected 200, got {}", freqs[0]);
|
||||
assert!(approx_eq(freqs[1], 400.0), "second freq: expected 400, got {}", freqs[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_superposes_sounds() {
|
||||
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);
|
||||
// Both at delta 0 (stacked/superposed)
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_with_multiple_emits() {
|
||||
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() {
|
||||
assert!(approx_eq(*delta, 0.0), "emit {} should be at 0, got {}", i, delta);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 . ~ "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)
|
||||
// since stack doesn't consume a slot in the parent div
|
||||
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.0));
|
||||
assert!(approx_eq(deltas[2], 0.0), "snare at 0, got {}", deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
// 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), "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]
|
||||
fn emit_n_basic() {
|
||||
let outputs = expect_outputs(r#""kick" s 4 .!"#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_n_with_alternator() {
|
||||
let outputs = expect_outputs(r#"| "kick" "snare" | s 4 .!"#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_n_zero() {
|
||||
let outputs = expect_outputs(r#""kick" s 0 .!"#, 0);
|
||||
assert!(outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_n_negative_error() {
|
||||
let f = forth();
|
||||
let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_counter_across_evaluations() {
|
||||
let counter = new_emission_counter();
|
||||
let ctx = default_ctx();
|
||||
|
||||
// First evaluation: kick, snare, kick, snare
|
||||
let f1 = forth_with_counter(counter.clone());
|
||||
let outputs1 = f1.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap();
|
||||
let sounds1 = get_sounds(&outputs1);
|
||||
assert_eq!(sounds1, vec!["kick", "snare"]);
|
||||
|
||||
// Second evaluation: continues from where we left off
|
||||
let f2 = forth_with_counter(counter.clone());
|
||||
let outputs2 = f2.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap();
|
||||
let sounds2 = get_sounds(&outputs2);
|
||||
assert_eq!(sounds2, vec!["kick", "snare"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_counter_three_item_cycle() {
|
||||
let counter = new_emission_counter();
|
||||
let ctx = default_ctx();
|
||||
|
||||
// First eval: kick, snare
|
||||
let f1 = forth_with_counter(counter.clone());
|
||||
let outputs1 = f1.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap();
|
||||
let sounds1 = get_sounds(&outputs1);
|
||||
assert_eq!(sounds1, vec!["kick", "snare"]);
|
||||
|
||||
// Second eval: continues from hat (index 2)
|
||||
let f2 = forth_with_counter(counter.clone());
|
||||
let outputs2 = f2.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap();
|
||||
let sounds2 = get_sounds(&outputs2);
|
||||
assert_eq!(sounds2, vec!["hat", "kick"]);
|
||||
|
||||
// Third eval: snare, hat
|
||||
let f3 = forth_with_counter(counter.clone());
|
||||
let outputs3 = f3.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap();
|
||||
let sounds3 = get_sounds(&outputs3);
|
||||
assert_eq!(sounds3, vec!["snare", "hat"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_n_with_persistent_counter() {
|
||||
let counter = new_emission_counter();
|
||||
let ctx = default_ctx();
|
||||
|
||||
// First eval: 3 emits from a 4-item cycle
|
||||
let f1 = forth_with_counter(counter.clone());
|
||||
let outputs1 = f1.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap();
|
||||
let sounds1 = get_sounds(&outputs1);
|
||||
assert_eq!(sounds1, vec!["a", "b", "c"]);
|
||||
|
||||
// Second eval: continues from d
|
||||
let f2 = forth_with_counter(counter.clone());
|
||||
let outputs2 = f2.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap();
|
||||
let sounds2 = get_sounds(&outputs2);
|
||||
assert_eq!(sounds2, vec!["d", "a", "b"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user