big commit
This commit is contained in:
@@ -25,6 +25,11 @@ pub enum Op {
|
||||
Round,
|
||||
Min,
|
||||
Max,
|
||||
Pow,
|
||||
Sqrt,
|
||||
Sin,
|
||||
Cos,
|
||||
Log,
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
@@ -34,6 +39,11 @@ pub enum Op {
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
Xor,
|
||||
Nand,
|
||||
Nor,
|
||||
IfElse,
|
||||
Pick,
|
||||
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
|
||||
Branch(usize),
|
||||
NewCmd,
|
||||
@@ -67,8 +77,9 @@ pub enum Op {
|
||||
Ad,
|
||||
Apply,
|
||||
Ramp,
|
||||
Tri,
|
||||
Range,
|
||||
Noise,
|
||||
Perlin,
|
||||
Chain,
|
||||
Loop,
|
||||
Degree(&'static [i64]),
|
||||
|
||||
@@ -150,6 +150,15 @@ pub(super) struct PendingEmission {
|
||||
pub slot_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct ResolvedEmission {
|
||||
pub sound: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
pub parent_slot: usize,
|
||||
pub offset_in_slot: f64,
|
||||
pub dur: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct ScopeContext {
|
||||
pub start: f64,
|
||||
@@ -157,6 +166,7 @@ pub(super) struct ScopeContext {
|
||||
pub weight: f64,
|
||||
pub slot_count: usize,
|
||||
pub pending: Vec<PendingEmission>,
|
||||
pub resolved: Vec<ResolvedEmission>,
|
||||
pub stacked: bool,
|
||||
}
|
||||
|
||||
@@ -168,6 +178,7 @@ impl ScopeContext {
|
||||
weight: 1.0,
|
||||
slot_count: 0,
|
||||
pending: Vec::new(),
|
||||
resolved: Vec::new(),
|
||||
stacked: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use rand::{Rng as RngTrait, SeedableRng};
|
||||
use super::compiler::compile_script;
|
||||
use super::ops::Op;
|
||||
use super::types::{
|
||||
CmdRegister, Dictionary, ExecutionTrace, PendingEmission, Rng, ScopeContext, SourceSpan, Stack,
|
||||
StepContext, Value, Variables,
|
||||
CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext,
|
||||
SourceSpan, Stack, StepContext, Value, Variables,
|
||||
};
|
||||
|
||||
pub type EmissionCounter = std::sync::Arc<std::sync::Mutex<usize>>;
|
||||
@@ -220,6 +220,23 @@ impl Forth {
|
||||
}
|
||||
Op::Min => binary_op(stack, |a, b| a.min(b))?,
|
||||
Op::Max => binary_op(stack, |a, b| a.max(b))?,
|
||||
Op::Pow => binary_op(stack, |a, b| a.powf(b))?,
|
||||
Op::Sqrt => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
stack.push(lift_unary(v, |x| x.sqrt())?);
|
||||
}
|
||||
Op::Sin => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
stack.push(lift_unary(v, |x| x.sin())?);
|
||||
}
|
||||
Op::Cos => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
stack.push(lift_unary(v, |x| x.cos())?);
|
||||
}
|
||||
Op::Log => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
stack.push(lift_unary(v, |x| x.ln())?);
|
||||
}
|
||||
|
||||
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
|
||||
Op::Ne => cmp_op(stack, |a, b| (a - b).abs() >= f64::EPSILON)?,
|
||||
@@ -242,6 +259,21 @@ impl Forth {
|
||||
let v = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
stack.push(Value::Int(if v { 0 } else { 1 }, None));
|
||||
}
|
||||
Op::Xor => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
stack.push(Value::Int(if a ^ b { 1 } else { 0 }, None));
|
||||
}
|
||||
Op::Nand => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
stack.push(Value::Int(if !(a && b) { 1 } else { 0 }, None));
|
||||
}
|
||||
Op::Nor => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
stack.push(Value::Int(if !(a || b) { 1 } else { 0 }, None));
|
||||
}
|
||||
|
||||
Op::BranchIfZero(offset, then_span, else_span) => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
@@ -630,6 +662,80 @@ impl Forth {
|
||||
}
|
||||
}
|
||||
|
||||
Op::IfElse => {
|
||||
let cond = stack.pop().ok_or("stack underflow")?;
|
||||
let false_quot = stack.pop().ok_or("stack underflow")?;
|
||||
let true_quot = stack.pop().ok_or("stack underflow")?;
|
||||
let quot = if cond.is_truthy() { true_quot } else { false_quot };
|
||||
match quot {
|
||||
Value::Quotation(quot_ops, body_span) => {
|
||||
if let Some(span) = body_span {
|
||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||
trace.executed_spans.push(span);
|
||||
}
|
||||
}
|
||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||
self.execute_ops(
|
||||
"_ops,
|
||||
ctx,
|
||||
stack,
|
||||
outputs,
|
||||
scope_stack,
|
||||
cmd,
|
||||
trace_opt.as_deref_mut(),
|
||||
emission_count,
|
||||
)?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
}
|
||||
|
||||
Op::Pick => {
|
||||
let idx = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
let mut quots: Vec<Value> = Vec::new();
|
||||
while let Some(val) = stack.pop() {
|
||||
match &val {
|
||||
Value::Quotation(_, _) => quots.push(val),
|
||||
_ => {
|
||||
stack.push(val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
quots.reverse();
|
||||
if idx >= quots.len() {
|
||||
return Err(format!(
|
||||
"pick index {} out of range (have {} quotations)",
|
||||
idx,
|
||||
quots.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
match "s[idx] {
|
||||
Value::Quotation(quot_ops, body_span) => {
|
||||
if let Some(span) = body_span {
|
||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||
trace.executed_spans.push(*span);
|
||||
}
|
||||
}
|
||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||
self.execute_ops(
|
||||
quot_ops,
|
||||
ctx,
|
||||
stack,
|
||||
outputs,
|
||||
scope_stack,
|
||||
cmd,
|
||||
trace_opt.as_deref_mut(),
|
||||
emission_count,
|
||||
)?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Op::Mtof => {
|
||||
let note = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0);
|
||||
@@ -851,13 +957,20 @@ impl Forth {
|
||||
let val = phase.powf(curve);
|
||||
stack.push(Value::Float(val, None));
|
||||
}
|
||||
Op::Tri => {
|
||||
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let phase = (freq * ctx.beat).fract();
|
||||
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
|
||||
let val = 1.0 - (2.0 * phase - 1.0).abs();
|
||||
stack.push(Value::Float(val, None));
|
||||
}
|
||||
Op::Range => {
|
||||
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let val = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
stack.push(Value::Float(min + val * (max - min), None));
|
||||
}
|
||||
Op::Noise => {
|
||||
Op::Perlin => {
|
||||
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let val = perlin_noise_1d(freq * ctx.beat);
|
||||
stack.push(Value::Float(val, None));
|
||||
@@ -887,10 +1000,19 @@ impl Forth {
|
||||
|
||||
Op::DivEnd => {
|
||||
if scope_stack.len() <= 1 {
|
||||
return Err("unmatched end".into());
|
||||
return Err("unmatched ~ (no div/stack to close)".into());
|
||||
}
|
||||
let child = scope_stack.pop().unwrap();
|
||||
|
||||
if child.stacked {
|
||||
// Stack doesn't claim a slot - resolve directly to outputs
|
||||
resolve_scope(&child, outputs);
|
||||
} else {
|
||||
// Div claims a slot in the parent
|
||||
let parent = scope_stack.last_mut().ok_or("scope stack underflow")?;
|
||||
let parent_slot = parent.claim_slot();
|
||||
resolve_scope_to_parent(&child, parent_slot, parent);
|
||||
}
|
||||
let scope = scope_stack.pop().unwrap();
|
||||
resolve_scope(&scope, outputs);
|
||||
}
|
||||
|
||||
Op::StackStart => {
|
||||
@@ -966,28 +1088,89 @@ fn resolve_value_with_span(val: &Value, emission_count: usize) -> (Value, Option
|
||||
}
|
||||
|
||||
fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec<String>) {
|
||||
if scope.slot_count == 0 || scope.pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
let slot_dur = scope.duration * scope.weight / scope.slot_count as f64;
|
||||
let slot_dur = if scope.slot_count == 0 {
|
||||
scope.duration * scope.weight
|
||||
} else {
|
||||
scope.duration * scope.weight / scope.slot_count as f64
|
||||
};
|
||||
|
||||
// Collect all emissions with their deltas for sorting
|
||||
let mut emissions: Vec<(f64, String, Vec<(String, String)>, f64)> = Vec::new();
|
||||
|
||||
for em in &scope.pending {
|
||||
let delta = scope.start + slot_dur * em.slot_index as f64;
|
||||
let mut pairs = vec![("sound".into(), em.sound.clone())];
|
||||
pairs.extend(em.params.iter().cloned());
|
||||
if delta > 0.0 {
|
||||
pairs.push(("delta".into(), delta.to_string()));
|
||||
}
|
||||
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||
pairs.push(("dur".into(), slot_dur.to_string()));
|
||||
}
|
||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||
pairs[idx].1 = (ratio * slot_dur).to_string();
|
||||
} else {
|
||||
pairs.push(("delaytime".into(), slot_dur.to_string()));
|
||||
}
|
||||
outputs.push(format_cmd(&pairs));
|
||||
emissions.push((delta, em.sound.clone(), em.params.clone(), slot_dur));
|
||||
}
|
||||
|
||||
for em in &scope.resolved {
|
||||
let slot_start = slot_dur * em.parent_slot as f64;
|
||||
let delta = scope.start + slot_start + em.offset_in_slot * slot_dur;
|
||||
let dur = em.dur * slot_dur;
|
||||
emissions.push((delta, em.sound.clone(), em.params.clone(), dur));
|
||||
}
|
||||
|
||||
// Sort by delta to ensure temporal ordering
|
||||
emissions.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
for (delta, sound, params, dur) in emissions {
|
||||
emit_output(&sound, ¶ms, delta, dur, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_scope_to_parent(child: &ScopeContext, parent_slot: usize, parent: &mut ScopeContext) {
|
||||
if child.slot_count == 0 && child.pending.is_empty() && child.resolved.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let child_slot_count = child.slot_count.max(1);
|
||||
|
||||
// Store offsets and durations as fractions of the parent slot
|
||||
// Child's internal structure: slot_count slots, each slot is 1/slot_count of the whole
|
||||
for em in &child.pending {
|
||||
let offset_fraction = em.slot_index as f64 / child_slot_count as f64;
|
||||
let dur_fraction = 1.0 / child_slot_count as f64;
|
||||
parent.resolved.push(ResolvedEmission {
|
||||
sound: em.sound.clone(),
|
||||
params: em.params.clone(),
|
||||
parent_slot,
|
||||
offset_in_slot: offset_fraction,
|
||||
dur: dur_fraction,
|
||||
});
|
||||
}
|
||||
|
||||
// Child's resolved emissions already have fractional offsets/durs relative to their slots
|
||||
// We need to compose them: em belongs to child slot em.parent_slot, which is a fraction of child
|
||||
for em in &child.resolved {
|
||||
let child_slot_offset = em.parent_slot as f64 / child_slot_count as f64;
|
||||
let child_slot_size = 1.0 / child_slot_count as f64;
|
||||
let offset_fraction = child_slot_offset + em.offset_in_slot * child_slot_size;
|
||||
let dur_fraction = em.dur * child_slot_size;
|
||||
parent.resolved.push(ResolvedEmission {
|
||||
sound: em.sound.clone(),
|
||||
params: em.params.clone(),
|
||||
parent_slot,
|
||||
offset_in_slot: offset_fraction,
|
||||
dur: dur_fraction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_output(sound: &str, params: &[(String, String)], delta: f64, dur: f64, outputs: &mut Vec<String>) {
|
||||
let mut pairs = vec![("sound".into(), sound.to_string())];
|
||||
pairs.extend(params.iter().cloned());
|
||||
if delta > 0.0 {
|
||||
pairs.push(("delta".into(), delta.to_string()));
|
||||
}
|
||||
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||
pairs.push(("dur".into(), dur.to_string()));
|
||||
}
|
||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||
pairs[idx].1 = (ratio * dur).to_string();
|
||||
} else {
|
||||
pairs.push(("delaytime".into(), dur.to_string()));
|
||||
}
|
||||
outputs.push(format_cmd(&pairs));
|
||||
}
|
||||
|
||||
fn perlin_grad(hash_input: i64) -> f64 {
|
||||
@@ -996,11 +1179,8 @@ fn perlin_grad(hash_input: i64) -> f64 {
|
||||
h ^= h >> 33;
|
||||
h = h.wrapping_mul(0xff51afd7ed558ccd);
|
||||
h ^= h >> 33;
|
||||
if h & 1 == 0 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
// Convert to float in [-1, 1] range for varied gradients
|
||||
(h as i64 as f64) / (i64::MAX as f64)
|
||||
}
|
||||
|
||||
fn perlin_noise_1d(x: f64) -> f64 {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn basic_emit() {
|
||||
let outputs = expect_outputs(r#""kick" sound emit"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" sound ."#, 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_s() {
|
||||
let outputs = expect_outputs(r#""snare" s emit"#, 1);
|
||||
let outputs = expect_outputs(r#""snare" s ."#, 1);
|
||||
assert!(outputs[0].contains("sound/snare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_params() {
|
||||
let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain emit"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain ."#, 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[0].contains("freq/440"));
|
||||
assert!(outputs[0].contains("gain/0.5"));
|
||||
@@ -22,24 +22,24 @@ fn with_params() {
|
||||
|
||||
#[test]
|
||||
fn auto_dur() {
|
||||
let outputs = expect_outputs(r#""kick" s emit"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
assert!(outputs[0].contains("dur/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_delaytime() {
|
||||
let outputs = expect_outputs(r#""kick" s emit"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
assert!(outputs[0].contains("delaytime/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_no_sound() {
|
||||
expect_error("emit", "no sound set");
|
||||
expect_error(".", "no sound set");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_emits() {
|
||||
let outputs = expect_outputs(r#""kick" s emit "snare" s emit"#, 2);
|
||||
let outputs = expect_outputs(r#""kick" s . "snare" s ."#, 2);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[1].contains("sound/snare"));
|
||||
}
|
||||
@@ -47,7 +47,7 @@ fn multiple_emits() {
|
||||
#[test]
|
||||
fn envelope_params() {
|
||||
let outputs = expect_outputs(
|
||||
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release emit"#,
|
||||
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
|
||||
1,
|
||||
);
|
||||
assert!(outputs[0].contains("attack/0.01"));
|
||||
@@ -58,14 +58,14 @@ fn envelope_params() {
|
||||
|
||||
#[test]
|
||||
fn filter_params() {
|
||||
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq emit"#, 1);
|
||||
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq ."#, 1);
|
||||
assert!(outputs[0].contains("lpf/2000"));
|
||||
assert!(outputs[0].contains("lpq/0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adsr_sets_all_envelope_params() {
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr emit"#, 1);
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1);
|
||||
assert!(outputs[0].contains("attack/0.01"));
|
||||
assert!(outputs[0].contains("decay/0.1"));
|
||||
assert!(outputs[0].contains("sustain/0.5"));
|
||||
@@ -74,7 +74,7 @@ fn adsr_sets_all_envelope_params() {
|
||||
|
||||
#[test]
|
||||
fn ad_sets_attack_decay_sustain_zero() {
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad emit"#, 1);
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1);
|
||||
assert!(outputs[0].contains("attack/0.01"));
|
||||
assert!(outputs[0].contains("decay/0.1"));
|
||||
assert!(outputs[0].contains("sustain/0"));
|
||||
@@ -82,7 +82,7 @@ fn ad_sets_attack_decay_sustain_zero() {
|
||||
|
||||
#[test]
|
||||
fn bank_param() {
|
||||
let outputs = expect_outputs(r#""loop" s "a" bank emit"#, 1);
|
||||
let outputs = expect_outputs(r#""loop" s "a" bank ."#, 1);
|
||||
assert!(outputs[0].contains("sound/loop"));
|
||||
assert!(outputs[0].contains("bank/a"));
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
|
||||
@@ -61,14 +61,14 @@ fn stepdur_baseline() {
|
||||
|
||||
#[test]
|
||||
fn single_emit() {
|
||||
let outputs = expect_outputs(r#""kick" s @"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_subdivision_2() {
|
||||
let outputs = expect_outputs(r#""kick" s @ @"#, 2);
|
||||
let outputs = expect_outputs(r#""kick" s . ."#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.5 / 2.0;
|
||||
assert!(approx_eq(deltas[0], 0.0), "first slot at 0");
|
||||
@@ -77,7 +77,7 @@ fn implicit_subdivision_2() {
|
||||
|
||||
#[test]
|
||||
fn implicit_subdivision_4() {
|
||||
let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 4);
|
||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.5 / 4.0;
|
||||
for (i, delta) in deltas.iter().enumerate() {
|
||||
@@ -92,7 +92,7 @@ fn implicit_subdivision_4() {
|
||||
|
||||
#[test]
|
||||
fn implicit_subdivision_3() {
|
||||
let outputs = expect_outputs(r#""kick" s @ @ @"#, 3);
|
||||
let outputs = expect_outputs(r#""kick" s . . ."#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.5 / 3.0;
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
@@ -102,7 +102,7 @@ fn implicit_subdivision_3() {
|
||||
|
||||
#[test]
|
||||
fn silence_creates_gap() {
|
||||
let outputs = expect_outputs(r#""kick" s @ ~ @"#, 2);
|
||||
let outputs = expect_outputs(r#""kick" s . _ ."#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.5 / 3.0;
|
||||
assert!(approx_eq(deltas[0], 0.0), "first at 0");
|
||||
@@ -116,7 +116,7 @@ fn silence_creates_gap() {
|
||||
|
||||
#[test]
|
||||
fn silence_at_start() {
|
||||
let outputs = expect_outputs(r#""kick" s ~ @"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" s _ ."#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.5 / 2.0;
|
||||
assert!(
|
||||
@@ -129,13 +129,13 @@ fn silence_at_start() {
|
||||
|
||||
#[test]
|
||||
fn silence_only() {
|
||||
let outputs = expect_outputs(r#""kick" s ~"#, 0);
|
||||
let outputs = expect_outputs(r#""kick" s _"#, 0);
|
||||
assert!(outputs.is_empty(), "silence only should produce no output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sound_persists() {
|
||||
let outputs = expect_outputs(r#""kick" s @ @ "hat" s @ @"#, 4);
|
||||
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds[0], "kick");
|
||||
assert_eq!(sounds[1], "kick");
|
||||
@@ -145,14 +145,14 @@ fn sound_persists() {
|
||||
|
||||
#[test]
|
||||
fn alternating_sounds() {
|
||||
let outputs = expect_outputs(r#""kick" s @ "snare" s @ "kick" s @ "snare" s @"#, 4);
|
||||
let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dur_matches_slot_duration() {
|
||||
let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 4);
|
||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
||||
let durs = get_durs(&outputs);
|
||||
let expected_dur = 0.5 / 4.0;
|
||||
for (i, dur) in durs.iter().enumerate() {
|
||||
@@ -168,7 +168,7 @@ fn dur_matches_slot_duration() {
|
||||
fn tempo_affects_subdivision() {
|
||||
let ctx = ctx_with(|c| c.tempo = 60.0);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s @ @"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap();
|
||||
let deltas = get_deltas(&outputs);
|
||||
// At 60 BPM: stepdur = 0.25, root dur = 1.0
|
||||
let step = 1.0 / 2.0;
|
||||
@@ -180,7 +180,7 @@ fn tempo_affects_subdivision() {
|
||||
fn speed_affects_subdivision() {
|
||||
let ctx = ctx_with(|c| c.speed = 2.0);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s @ @"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap();
|
||||
let deltas = get_deltas(&outputs);
|
||||
// At speed 2.0: stepdur = 0.0625, root dur = 0.25
|
||||
let step = 0.25 / 2.0;
|
||||
@@ -193,11 +193,11 @@ fn cycle_picks_by_step() {
|
||||
for runs in 0..4 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s < @ ~ >"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s < . _ >"#, &ctx).unwrap();
|
||||
if runs % 2 == 0 {
|
||||
assert_eq!(outputs.len(), 1, "runs={}: @ should be picked", runs);
|
||||
assert_eq!(outputs.len(), 1, "runs={}: . should be picked", runs);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "runs={}: ~ should be picked", runs);
|
||||
assert_eq!(outputs.len(), 0, "runs={}: _ should be picked", runs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,11 +207,11 @@ fn pcycle_picks_by_pattern() {
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s << @ ~ >>"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s << . _ >>"#, &ctx).unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: @ should be picked", iter);
|
||||
assert_eq!(outputs.len(), 1, "iter={}: . should be picked", iter);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "iter={}: ~ should be picked", iter);
|
||||
assert_eq!(outputs.len(), 0, "iter={}: _ should be picked", iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ fn cycle_with_sounds() {
|
||||
for runs in 0..3 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#"< { "kick" s @ } { "hat" s @ } { "snare" s @ } >"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#"< { "kick" s . } { "hat" s . } { "snare" s . } >"#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
let expected = ["kick", "hat", "snare"][runs % 3];
|
||||
@@ -238,7 +238,7 @@ fn dot_alias_for_emit() {
|
||||
|
||||
#[test]
|
||||
fn dot_with_silence() {
|
||||
let outputs = expect_outputs(r#""kick" s . ~ . ~"#, 2);
|
||||
let outputs = expect_outputs(r#""kick" s . _ . _"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.5 / 4.0;
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
@@ -292,7 +292,7 @@ fn internal_alternation_empty_error() {
|
||||
|
||||
#[test]
|
||||
fn div_basic_subdivision() {
|
||||
let outputs = expect_outputs(r#"div "kick" s . "hat" s . end"#, 2);
|
||||
let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
||||
@@ -301,59 +301,58 @@ fn div_basic_subdivision() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_superposition() {
|
||||
let outputs = expect_outputs(r#"div "kick" s . end div "hat" s . end"#, 2);
|
||||
fn div_sequential() {
|
||||
// Two consecutive divs each claim a slot in root, so they're sequential
|
||||
let outputs = expect_outputs(r#"div "kick" s . ~ div "hat" s . ~"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds.len(), 2);
|
||||
// Both at delta 0 (superposed)
|
||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.25), "second div at slot 1, got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_with_root_emit() {
|
||||
// kick at root level, hat in div - both should superpose at 0
|
||||
// Note: div resolves first (when end is hit), root resolves at script end
|
||||
let outputs = expect_outputs(r#""kick" s . div "hat" s . end"#, 2);
|
||||
// kick claims slot 0 at root, div claims slot 1 at root
|
||||
let outputs = expect_outputs(r#""kick" s . div "hat" s . ~"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
// Order is hat then kick because div resolves before root
|
||||
assert_eq!(sounds, vec!["hat", "kick"]);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.0));
|
||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
||||
assert!(approx_eq(deltas[0], 0.0), "kick at slot 0");
|
||||
assert!(approx_eq(deltas[1], 0.25), "hat at slot 1, got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_nested() {
|
||||
// kick takes first slot in outer div, inner div takes second slot
|
||||
// Inner div resolves first, then outer div resolves
|
||||
let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . end end"#, 3);
|
||||
// kick claims slot 0 in outer div, inner div claims slot 1
|
||||
// Inner div's 2 hats subdivide its slot (0.25 duration) into 2 sub-slots
|
||||
let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . ~ ~"#, 3);
|
||||
let sounds = get_sounds(&outputs);
|
||||
let deltas = get_deltas(&outputs);
|
||||
// Inner div resolves first (hat, hat), then outer div (kick)
|
||||
assert_eq!(sounds[0], "hat");
|
||||
// Output order: kick (slot 0), then hats (slot 1 subdivided)
|
||||
assert_eq!(sounds[0], "kick");
|
||||
assert_eq!(sounds[1], "hat");
|
||||
assert_eq!(sounds[2], "kick");
|
||||
// Inner div inherits parent's start (0) and duration (0.5), subdivides into 2
|
||||
assert!(approx_eq(deltas[0], 0.0), "first hat at 0, got {}", deltas[0]);
|
||||
assert!(approx_eq(deltas[1], 0.25), "second hat at 0.25, got {}", deltas[1]);
|
||||
// Outer div has 2 slots: kick at 0, inner div at slot 1 (but inner resolved independently)
|
||||
assert!(approx_eq(deltas[2], 0.0), "kick at 0, got {}", deltas[2]);
|
||||
assert_eq!(sounds[2], "hat");
|
||||
// Outer div has 2 slots of 0.25 each
|
||||
// kick at slot 0 -> delta 0
|
||||
// inner div at slot 1 -> starts at 0.25, subdivided into 2 -> hats at 0.25 and 0.375
|
||||
assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]);
|
||||
assert!(approx_eq(deltas[1], 0.25), "first hat at 0.25, got {}", deltas[1]);
|
||||
assert!(approx_eq(deltas[2], 0.375), "second hat at 0.375, got {}", deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_with_silence() {
|
||||
let outputs = expect_outputs(r#"div "kick" s . ~ end"#, 1);
|
||||
let outputs = expect_outputs(r#"div "kick" s . _ ~"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_unmatched_end_error() {
|
||||
fn unmatched_scope_terminator_error() {
|
||||
let f = forth();
|
||||
let result = f.evaluate(r#""kick" s . end"#, &default_ctx());
|
||||
assert!(result.is_err(), "unmatched end should error");
|
||||
let result = f.evaluate(r#""kick" s . ~"#, &default_ctx());
|
||||
assert!(result.is_err(), "unmatched ~ should error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -392,7 +391,7 @@ fn alternator_with_arithmetic() {
|
||||
|
||||
#[test]
|
||||
fn stack_superposes_sounds() {
|
||||
let outputs = expect_outputs(r#"stack "kick" s . "hat" s . end"#, 2);
|
||||
let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds.len(), 2);
|
||||
@@ -403,7 +402,7 @@ fn stack_superposes_sounds() {
|
||||
|
||||
#[test]
|
||||
fn stack_with_multiple_emits() {
|
||||
let outputs = expect_outputs(r#"stack "kick" s . . . . end"#, 4);
|
||||
let outputs = expect_outputs(r#"stack "kick" s . . . . ~"#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
// All 4 kicks at delta 0
|
||||
for (i, delta) in deltas.iter().enumerate() {
|
||||
@@ -415,7 +414,7 @@ fn stack_with_multiple_emits() {
|
||||
fn stack_inside_div() {
|
||||
// div subdivides, stack inside superposes
|
||||
// stack doesn't claim a slot in parent div, so snare is also at 0
|
||||
let outputs = expect_outputs(r#"div stack "kick" s . "hat" s . end "snare" s . end"#, 3);
|
||||
let outputs = expect_outputs(r#"div stack "kick" s . "hat" s . ~ "snare" s . ~"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
// stack resolves first (kick, hat at 0), then div resolves (snare at 0)
|
||||
@@ -429,20 +428,21 @@ fn stack_inside_div() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_then_stack_sequential() {
|
||||
// Nested div doesn't claim a slot in parent, only emit/silence do
|
||||
// So nested div and snare both resolve with parent's timing
|
||||
let outputs = expect_outputs(r#"div div "kick" s . "hat" s . end "snare" s . end"#, 3);
|
||||
fn div_nested_with_sibling() {
|
||||
// Inner div claims slot 0, snare claims slot 1
|
||||
// Inner div's kick/hat subdivide slot 0
|
||||
let outputs = expect_outputs(r#"div div "kick" s . "hat" s . ~ "snare" s . ~"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
// Inner div resolves first (kick at 0, hat at 0.25 of parent duration)
|
||||
// Outer div has 1 slot (snare's .), so snare at 0
|
||||
// Outer div has 2 slots of 0.25 each
|
||||
// Inner div at slot 0: kick at 0, hat at 0.125
|
||||
// snare at slot 1: delta 0.25
|
||||
assert_eq!(sounds[0], "kick");
|
||||
assert_eq!(sounds[1], "hat");
|
||||
assert_eq!(sounds[2], "snare");
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.25), "hat at 0.25, got {}", deltas[1]);
|
||||
assert!(approx_eq(deltas[2], 0.0), "snare at 0, got {}", deltas[2]);
|
||||
assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]);
|
||||
assert!(approx_eq(deltas[1], 0.125), "hat at 0.125, got {}", deltas[1]);
|
||||
assert!(approx_eq(deltas[2], 0.25), "snare at 0.25, got {}", deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user