ok
This commit is contained in:
@@ -3,6 +3,10 @@ name = "seq"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "seq"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "seq"
|
||||
path = "src/main.rs"
|
||||
|
||||
42841
seq/bullshit
Normal file
42841
seq/bullshit
Normal file
File diff suppressed because it is too large
Load Diff
@@ -262,6 +262,7 @@ impl App {
|
||||
phase: link.phase(),
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed,
|
||||
};
|
||||
|
||||
@@ -335,6 +336,7 @@ impl App {
|
||||
phase: 0.0,
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed,
|
||||
};
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ pub struct PatternSlot {
|
||||
pub bank: usize,
|
||||
pub pattern: usize,
|
||||
pub step_index: usize,
|
||||
pub iter: usize,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -66,12 +66,14 @@ pub struct SlotState {
|
||||
pub active: bool,
|
||||
pub bank: usize,
|
||||
pub pattern: usize,
|
||||
pub iter: usize,
|
||||
}
|
||||
|
||||
pub struct AtomicSlotData {
|
||||
active: AtomicBool,
|
||||
bank: AtomicUsize,
|
||||
pattern: AtomicUsize,
|
||||
iter: AtomicUsize,
|
||||
}
|
||||
|
||||
impl AtomicSlotData {
|
||||
@@ -80,6 +82,7 @@ impl AtomicSlotData {
|
||||
active: AtomicBool::new(false),
|
||||
bank: AtomicUsize::new(0),
|
||||
pattern: AtomicUsize::new(0),
|
||||
iter: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +91,7 @@ impl AtomicSlotData {
|
||||
active: self.active.load(Ordering::Relaxed),
|
||||
bank: self.bank.load(Ordering::Relaxed),
|
||||
pattern: self.pattern.load(Ordering::Relaxed),
|
||||
iter: self.iter.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +99,7 @@ impl AtomicSlotData {
|
||||
self.active.store(state.active, Ordering::Relaxed);
|
||||
self.bank.store(state.bank, Ordering::Relaxed);
|
||||
self.pattern.store(state.pattern, Ordering::Relaxed);
|
||||
self.iter.store(state.iter, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +354,7 @@ fn sequencer_loop(
|
||||
bank,
|
||||
pattern,
|
||||
step_index: 0,
|
||||
iter: 0,
|
||||
active: true,
|
||||
};
|
||||
}
|
||||
@@ -398,6 +404,7 @@ fn sequencer_loop(
|
||||
phase: beat % quantum,
|
||||
slot: slot_idx,
|
||||
runs,
|
||||
iter: slot.iter,
|
||||
speed: speed_mult,
|
||||
};
|
||||
if let Some(script) = resolved_script {
|
||||
@@ -424,7 +431,11 @@ fn sequencer_loop(
|
||||
}
|
||||
}
|
||||
|
||||
slot.step_index = (slot.step_index + 1) % pattern.length;
|
||||
let next_step = slot.step_index + 1;
|
||||
if next_step >= pattern.length {
|
||||
slot.iter += 1;
|
||||
}
|
||||
slot.step_index = next_step % pattern.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,6 +444,7 @@ fn sequencer_loop(
|
||||
active: slot.active,
|
||||
bank: slot.bank,
|
||||
pattern: slot.pattern,
|
||||
iter: slot.iter,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
2
seq/src/lib.rs
Normal file
2
seq/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod config;
|
||||
pub mod model;
|
||||
@@ -12,6 +12,7 @@ pub struct StepContext {
|
||||
pub phase: f64,
|
||||
pub slot: usize,
|
||||
pub runs: usize,
|
||||
pub iter: usize,
|
||||
pub speed: f64,
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ pub enum Value {
|
||||
Float(f64),
|
||||
Str(String),
|
||||
Marker,
|
||||
Quotation(Vec<Op>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -84,6 +86,7 @@ impl Value {
|
||||
Value::Float(f) => *f != 0.0,
|
||||
Value::Str(s) => !s.is_empty(),
|
||||
Value::Marker => false,
|
||||
Value::Quotation(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,11 +100,12 @@ impl Value {
|
||||
Value::Float(f) => f.to_string(),
|
||||
Value::Str(s) => s.clone(),
|
||||
Value::Marker => String::new(),
|
||||
Value::Quotation(_) => String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Op {
|
||||
PushInt(i64),
|
||||
PushFloat(f64),
|
||||
@@ -160,6 +164,10 @@ pub enum Op {
|
||||
Subdivide,
|
||||
SetTempo,
|
||||
Each,
|
||||
Every,
|
||||
Quotation(Vec<Op>),
|
||||
When,
|
||||
Unless,
|
||||
}
|
||||
|
||||
pub enum WordCompile {
|
||||
@@ -468,6 +476,13 @@ pub const WORDS: &[Word] = &[
|
||||
example: "1 2 3 3 cycle",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "every",
|
||||
stack: "(n -- bool)",
|
||||
desc: "True every nth iteration",
|
||||
example: "4 every",
|
||||
compile: Simple,
|
||||
},
|
||||
// Probability shortcuts
|
||||
Word {
|
||||
name: "always",
|
||||
@@ -575,6 +590,13 @@ pub const WORDS: &[Word] = &[
|
||||
example: "runs => 3",
|
||||
compile: Context("runs"),
|
||||
},
|
||||
Word {
|
||||
name: "iter",
|
||||
stack: "(-- n)",
|
||||
desc: "Pattern iteration count",
|
||||
example: "iter => 2",
|
||||
compile: Context("iter"),
|
||||
},
|
||||
Word {
|
||||
name: "stepdur",
|
||||
stack: "(-- f)",
|
||||
@@ -655,12 +677,19 @@ pub const WORDS: &[Word] = &[
|
||||
example: "[ 1 2 3 ] => 3",
|
||||
compile: Simple,
|
||||
},
|
||||
// Other
|
||||
// Quotations
|
||||
Word {
|
||||
name: "?",
|
||||
stack: "(prob --)",
|
||||
desc: "Maybe (not implemented)",
|
||||
example: "0.5 ?",
|
||||
stack: "(quot bool --)",
|
||||
desc: "Execute quotation if true",
|
||||
example: "{ 2 distort } 0.5 chance ?",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "!?",
|
||||
stack: "(quot bool --)",
|
||||
desc: "Execute quotation if false",
|
||||
example: "{ 1 distort } 0.5 chance !?",
|
||||
compile: Simple,
|
||||
},
|
||||
// Parameters (synthesis)
|
||||
@@ -1447,11 +1476,13 @@ fn simple_op(name: &str) -> Option<Op> {
|
||||
"seed" => Op::Seed,
|
||||
"cycle" => Op::Cycle,
|
||||
"choose" => Op::Choose,
|
||||
"every" => Op::Every,
|
||||
"chance" => Op::Chance,
|
||||
"coin" => Op::Coin,
|
||||
"mtof" => Op::Mtof,
|
||||
"ftom" => Op::Ftom,
|
||||
"?" => Op::Maybe,
|
||||
"?" => Op::When,
|
||||
"!?" => Op::Unless,
|
||||
"at" => Op::At,
|
||||
"window" => Op::Window,
|
||||
"pop" => Op::Pop,
|
||||
@@ -1500,6 +1531,8 @@ enum Token {
|
||||
Float(f64),
|
||||
Str(String),
|
||||
Word(String),
|
||||
QuoteStart,
|
||||
QuoteEnd,
|
||||
}
|
||||
|
||||
fn tokenize(input: &str) -> Vec<Token> {
|
||||
@@ -1537,9 +1570,21 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '{' {
|
||||
chars.next();
|
||||
tokens.push(Token::QuoteStart);
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '}' {
|
||||
chars.next();
|
||||
tokens.push(Token::QuoteEnd);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut word = String::new();
|
||||
while let Some(&ch) = chars.peek() {
|
||||
if ch.is_whitespace() {
|
||||
if ch.is_whitespace() || ch == '{' || ch == '}' {
|
||||
break;
|
||||
}
|
||||
word.push(ch);
|
||||
@@ -1567,6 +1612,14 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
|
||||
Token::Int(n) => ops.push(Op::PushInt(*n)),
|
||||
Token::Float(f) => ops.push(Op::PushFloat(*f)),
|
||||
Token::Str(s) => ops.push(Op::PushStr(s.clone())),
|
||||
Token::QuoteStart => {
|
||||
let (quote_ops, consumed) = compile_quotation(&tokens[i + 1..])?;
|
||||
i += consumed;
|
||||
ops.push(Op::Quotation(quote_ops));
|
||||
}
|
||||
Token::QuoteEnd => {
|
||||
return Err("unexpected }".into());
|
||||
}
|
||||
Token::Word(w) => {
|
||||
let word = w.as_str();
|
||||
if word == "if" {
|
||||
@@ -1592,6 +1645,29 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn compile_quotation(tokens: &[Token]) -> Result<(Vec<Op>, usize), String> {
|
||||
let mut depth = 1;
|
||||
let mut end_pos = None;
|
||||
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
match tok {
|
||||
Token::QuoteStart => depth += 1,
|
||||
Token::QuoteEnd => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end_pos = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let end_pos = end_pos.ok_or("missing }")?;
|
||||
let quote_ops = compile(&tokens[..end_pos])?;
|
||||
Ok((quote_ops, end_pos + 1))
|
||||
}
|
||||
|
||||
fn compile_if(tokens: &[Token]) -> Result<(Vec<Op>, Vec<Op>, usize), String> {
|
||||
let mut depth = 1;
|
||||
let mut else_pos = None;
|
||||
@@ -1672,6 +1748,38 @@ impl Forth {
|
||||
subdivisions: None,
|
||||
}];
|
||||
let mut cmd = CmdRegister::default();
|
||||
|
||||
self.execute_ops(
|
||||
ops,
|
||||
ctx,
|
||||
&mut stack,
|
||||
&mut outputs,
|
||||
&mut time_stack,
|
||||
&mut cmd,
|
||||
)?;
|
||||
|
||||
if outputs.is_empty() {
|
||||
if let Some((sound, params)) = cmd.take() {
|
||||
let mut pairs = vec![("sound".into(), sound)];
|
||||
pairs.extend(params);
|
||||
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||
pairs.push(("delaytime".into(), ctx.step_duration().to_string()));
|
||||
outputs.push(format_cmd(&pairs));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
fn execute_ops(
|
||||
&self,
|
||||
ops: &[Op],
|
||||
ctx: &StepContext,
|
||||
stack: &mut Vec<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
time_stack: &mut Vec<TimeContext>,
|
||||
cmd: &mut CmdRegister,
|
||||
) -> Result<(), String> {
|
||||
let mut pc = 0;
|
||||
|
||||
while pc < ops.len() {
|
||||
@@ -1726,10 +1834,10 @@ impl Forth {
|
||||
stack.insert(len - 2, v);
|
||||
}
|
||||
|
||||
Op::Add => binary_op(&mut stack, |a, b| a + b)?,
|
||||
Op::Sub => binary_op(&mut stack, |a, b| a - b)?,
|
||||
Op::Mul => binary_op(&mut stack, |a, b| a * b)?,
|
||||
Op::Div => binary_op(&mut stack, |a, b| a / b)?,
|
||||
Op::Add => binary_op(stack, |a, b| a + b)?,
|
||||
Op::Sub => binary_op(stack, |a, b| a - b)?,
|
||||
Op::Mul => binary_op(stack, |a, b| a * b)?,
|
||||
Op::Div => binary_op(stack, |a, b| a / b)?,
|
||||
Op::Mod => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let a = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
@@ -1763,15 +1871,15 @@ impl Forth {
|
||||
let v = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
stack.push(Value::Int(v.round() as i64));
|
||||
}
|
||||
Op::Min => binary_op(&mut stack, |a, b| a.min(b))?,
|
||||
Op::Max => binary_op(&mut stack, |a, b| a.max(b))?,
|
||||
Op::Min => binary_op(stack, |a, b| a.min(b))?,
|
||||
Op::Max => binary_op(stack, |a, b| a.max(b))?,
|
||||
|
||||
Op::Eq => cmp_op(&mut stack, |a, b| (a - b).abs() < f64::EPSILON)?,
|
||||
Op::Ne => cmp_op(&mut stack, |a, b| (a - b).abs() >= f64::EPSILON)?,
|
||||
Op::Lt => cmp_op(&mut stack, |a, b| a < b)?,
|
||||
Op::Gt => cmp_op(&mut stack, |a, b| a > b)?,
|
||||
Op::Le => cmp_op(&mut stack, |a, b| a <= b)?,
|
||||
Op::Ge => cmp_op(&mut stack, |a, b| a >= b)?,
|
||||
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
|
||||
Op::Ne => cmp_op(stack, |a, b| (a - b).abs() >= f64::EPSILON)?,
|
||||
Op::Lt => cmp_op(stack, |a, b| a < b)?,
|
||||
Op::Gt => cmp_op(stack, |a, b| a > b)?,
|
||||
Op::Le => cmp_op(stack, |a, b| a <= b)?,
|
||||
Op::Ge => cmp_op(stack, |a, b| a >= b)?,
|
||||
|
||||
Op::And => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
@@ -1852,6 +1960,7 @@ impl Forth {
|
||||
"phase" => Value::Float(ctx.phase),
|
||||
"slot" => Value::Int(ctx.slot as i64),
|
||||
"runs" => Value::Int(ctx.runs as i64),
|
||||
"iter" => Value::Int(ctx.iter as i64),
|
||||
"speed" => Value::Float(ctx.speed),
|
||||
"stepdur" => Value::Float(ctx.step_duration()),
|
||||
_ => Value::Int(0),
|
||||
@@ -1915,6 +2024,45 @@ impl Forth {
|
||||
stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }));
|
||||
}
|
||||
|
||||
Op::Every => {
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
if n <= 0 {
|
||||
return Err("every count must be > 0".into());
|
||||
}
|
||||
let result = ctx.iter as i64 % n == 0;
|
||||
stack.push(Value::Int(if result { 1 } else { 0 }));
|
||||
}
|
||||
|
||||
Op::Quotation(quote_ops) => {
|
||||
stack.push(Value::Quotation(quote_ops.clone()));
|
||||
}
|
||||
|
||||
Op::When => {
|
||||
let cond = stack.pop().ok_or("stack underflow")?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
if cond.is_truthy() {
|
||||
match quot {
|
||||
Value::Quotation(quot_ops) => {
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd)?;
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Op::Unless => {
|
||||
let cond = stack.pop().ok_or("stack underflow")?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
if !cond.is_truthy() {
|
||||
match quot {
|
||||
Value::Quotation(quot_ops) => {
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd)?;
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Op::Maybe => {
|
||||
return Err("? is not yet implemented with the new param model".into());
|
||||
}
|
||||
@@ -2048,17 +2196,7 @@ impl Forth {
|
||||
pc += 1;
|
||||
}
|
||||
|
||||
if outputs.is_empty() {
|
||||
if let Some((sound, params)) = cmd.take() {
|
||||
let mut pairs = vec![("sound".into(), sound)];
|
||||
pairs.extend(params);
|
||||
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||
pairs.push(("delaytime".into(), ctx.step_duration().to_string()));
|
||||
outputs.push(format_cmd(&pairs));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outputs)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
mod arithmetic;
|
||||
mod comparison;
|
||||
mod context;
|
||||
mod control_flow;
|
||||
mod errors;
|
||||
mod harness;
|
||||
mod randomness;
|
||||
mod sound;
|
||||
mod stack;
|
||||
mod variables;
|
||||
@@ -1,7 +1,5 @@
|
||||
mod file;
|
||||
pub mod forth;
|
||||
#[cfg(test)]
|
||||
mod forth_tests;
|
||||
mod project;
|
||||
mod script;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
||||
])
|
||||
.areas(padded);
|
||||
|
||||
render_header(frame, app, link, header_area);
|
||||
render_header(frame, app, link, snapshot, header_area);
|
||||
|
||||
match app.page {
|
||||
Page::Main => main_view::render(frame, app, snapshot, body_area),
|
||||
@@ -49,7 +49,13 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
||||
render_modal(frame, app, term);
|
||||
}
|
||||
|
||||
fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
fn render_header(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
link: &LinkState,
|
||||
snapshot: &SequencerSnapshot,
|
||||
area: Rect,
|
||||
) {
|
||||
use crate::model::PatternSpeed;
|
||||
|
||||
let bank = &app.project_state.project.banks[app.editor_ctx.bank];
|
||||
@@ -105,7 +111,7 @@ fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
bank_area,
|
||||
);
|
||||
|
||||
// Pattern block (name + length + speed)
|
||||
// Pattern block (name + length + speed + iter)
|
||||
let default_pattern_name = format!("Pattern {:02}", app.editor_ctx.pattern + 1);
|
||||
let pattern_name = pattern.name.as_deref().unwrap_or(&default_pattern_name);
|
||||
let speed_info = if pattern.speed != PatternSpeed::Normal {
|
||||
@@ -113,9 +119,15 @@ fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let iter_info = snapshot
|
||||
.slot_data
|
||||
.iter()
|
||||
.find(|s| s.active && s.bank == app.editor_ctx.bank && s.pattern == app.editor_ctx.pattern)
|
||||
.map(|s| format!(" · #{}", s.iter + 1))
|
||||
.unwrap_or_default();
|
||||
let pattern_text = format!(
|
||||
" {} · {} steps{} ",
|
||||
pattern_name, pattern.length, speed_info
|
||||
" {} · {} steps{}{} ",
|
||||
pattern_name, pattern.length, speed_info, iter_info
|
||||
);
|
||||
let pattern_style = Style::new().bg(Color::Rgb(30, 50, 50)).fg(Color::White);
|
||||
frame.render_widget(
|
||||
|
||||
42841
seq/techno
Normal file
42841
seq/techno
Normal file
File diff suppressed because it is too large
Load Diff
35
seq/tests/forth.rs
Normal file
35
seq/tests/forth.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#[path = "forth/harness.rs"]
|
||||
mod harness;
|
||||
|
||||
#[path = "forth/arithmetic.rs"]
|
||||
mod arithmetic;
|
||||
|
||||
#[path = "forth/comparison.rs"]
|
||||
mod comparison;
|
||||
|
||||
#[path = "forth/context.rs"]
|
||||
mod context;
|
||||
|
||||
#[path = "forth/control_flow.rs"]
|
||||
mod control_flow;
|
||||
|
||||
#[path = "forth/errors.rs"]
|
||||
mod errors;
|
||||
|
||||
#[path = "forth/randomness.rs"]
|
||||
mod randomness;
|
||||
|
||||
#[path = "forth/sound.rs"]
|
||||
mod sound;
|
||||
|
||||
#[path = "forth/stack.rs"]
|
||||
mod stack;
|
||||
|
||||
#[path = "forth/temporal.rs"]
|
||||
mod temporal;
|
||||
|
||||
#[path = "forth/variables.rs"]
|
||||
mod variables;
|
||||
|
||||
#[path = "forth/quotations.rs"]
|
||||
mod quotations;
|
||||
@@ -56,6 +56,41 @@ fn runs() {
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() {
|
||||
let ctx = ctx_with(|c| c.iter = 5);
|
||||
let f = run_ctx("iter", &ctx);
|
||||
assert_eq!(stack_int(&f), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_true_on_zero() {
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_true_on_multiple() {
|
||||
let ctx = ctx_with(|c| c.iter = 8);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_false_between() {
|
||||
for i in 1..4 {
|
||||
let ctx = ctx_with(|c| c.iter = i);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 0, "iter={} should be false", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_zero_count() {
|
||||
expect_error("0 every", "every count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stepdur() {
|
||||
// stepdur = 60.0 / tempo / 4.0 / speed = 60 / 120 / 4 / 1 = 0.125
|
||||
@@ -56,7 +56,7 @@ fn float_literal() {
|
||||
fn string_with_spaces() {
|
||||
let f = run(r#""hello world" "x" set "x" get"#);
|
||||
match stack_top(&f) {
|
||||
crate::model::forth::Value::Str(s) => assert_eq!(s, "hello world"),
|
||||
seq::model::forth::Value::Str(s) => assert_eq!(s, "hello world"),
|
||||
other => panic!("expected string, got {:?}", other),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::model::forth::{Forth, Rng, StepContext, Value, Variables};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use seq::model::forth::{Forth, Rng, StepContext, Value, Variables};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -14,6 +14,7 @@ pub fn default_ctx() -> StepContext {
|
||||
phase: 0.0,
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed: 1.0,
|
||||
}
|
||||
}
|
||||
184
seq/tests/forth/quotations.rs
Normal file
184
seq/tests/forth/quotations.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn quotation_on_stack() {
|
||||
// Quotation should be pushable to stack
|
||||
let f = forth();
|
||||
let result = f.evaluate("{ 1 2 + }", &default_ctx());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_true_executes() {
|
||||
let f = run("{ 42 } 1 ?");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_false_skips() {
|
||||
let f = run("99 { 42 } 0 ?");
|
||||
// Stack should still have 99, quotation not executed
|
||||
assert_eq!(stack_int(&f), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_with_arithmetic() {
|
||||
let f = run("10 { 5 + } 1 ?");
|
||||
assert_eq!(stack_int(&f), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_with_every() {
|
||||
// iter=0, every 2 should be true
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("{ 100 } 2 every ?", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
|
||||
// iter=1, every 2 should be false
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("50 { 100 } 2 every ?", &ctx);
|
||||
assert_eq!(stack_int(&f), 50); // quotation not executed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_with_chance_deterministic() {
|
||||
// 1.0 chance always true
|
||||
let f = run("{ 42 } 1.0 chance ?");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
|
||||
// 0.0 chance always false
|
||||
let f = run("99 { 42 } 0.0 chance ?");
|
||||
assert_eq!(stack_int(&f), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_quotations() {
|
||||
let f = run("{ { 42 } 1 ? } 1 ?");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_with_param() {
|
||||
let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? emit"#, 1);
|
||||
assert!(outputs[0].contains("distort/2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_skips_param() {
|
||||
let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? emit"#, 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);
|
||||
assert!(outputs[0].contains("kick"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_skips_emit() {
|
||||
// When false, emit should not fire
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx())
|
||||
.unwrap();
|
||||
// Should have 1 output from implicit emit at end (since cmd is set but not emitted)
|
||||
assert_eq!(outputs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_quotation_error() {
|
||||
expect_error("42 1 ?", "expected quotation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_quotation_error() {
|
||||
expect_error("{ 1 2", "missing }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unexpected_close_error() {
|
||||
expect_error("1 2 }", "unexpected }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_with_quotation_integration() {
|
||||
// Simulating: { 2 distort } 2 every ?
|
||||
// On even iterations, distort is applied
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s { 2 distort } 2 every ? emit"#, &ctx)
|
||||
.unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert!(
|
||||
outputs[0].contains("distort/2"),
|
||||
"iter {} should have distort",
|
||||
iter
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
!outputs[0].contains("distort"),
|
||||
"iter {} should not have distort",
|
||||
iter
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unless (!?) tests
|
||||
|
||||
#[test]
|
||||
fn unless_false_executes() {
|
||||
let f = run("{ 42 } 0 !?");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unless_true_skips() {
|
||||
let f = run("99 { 42 } 1 !?");
|
||||
assert_eq!(stack_int(&f), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unless_with_every() {
|
||||
// iter=0, every 2 is true, so unless skips
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("50 { 100 } 2 every !?", &ctx);
|
||||
assert_eq!(stack_int(&f), 50);
|
||||
|
||||
// iter=1, every 2 is false, so unless executes
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("{ 100 } 2 every !?", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_and_unless_complementary() {
|
||||
// Using both ? and !? for if-else like behavior
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(
|
||||
r#""kick" s { 2 distort } 2 every ? { 4 distort } 2 every !? emit"#,
|
||||
&ctx,
|
||||
)
|
||||
.unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert!(
|
||||
outputs[0].contains("distort/2"),
|
||||
"iter {} should have distort/2",
|
||||
iter
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
outputs[0].contains("distort/4"),
|
||||
"iter {} should have distort/4",
|
||||
iter
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::harness::*;
|
||||
use crate::model::forth::Value::Int;
|
||||
use seq::model::forth::Value::Int;
|
||||
|
||||
#[test]
|
||||
fn dup() {
|
||||
229
seq/tests/forth/temporal.rs
Normal file
229
seq/tests/forth/temporal.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
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()
|
||||
}
|
||||
|
||||
const EPSILON: f64 = 1e-9;
|
||||
|
||||
fn approx_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < EPSILON
|
||||
}
|
||||
|
||||
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
|
||||
|
||||
#[test]
|
||||
fn stepdur_baseline() {
|
||||
let f = run("stepdur");
|
||||
assert!(approx_eq(stack_float(&f), 0.125));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_no_delta() {
|
||||
let outputs = expect_outputs(r#""kick" s emit"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.0),
|
||||
"emit at start should have delta 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_half() {
|
||||
// at 0.5 in root window (0..0.125) => delta = 0.5 * 0.125 = 0.0625
|
||||
let outputs = expect_outputs(r#""kick" s 0.5 at"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.0625),
|
||||
"at 0.5 should be delta 0.0625, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_quarter() {
|
||||
let outputs = expect_outputs(r#""kick" s 0.25 at"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.03125),
|
||||
"at 0.25 should be delta 0.03125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_zero() {
|
||||
let outputs = expect_outputs(r#""kick" s 0.0 at"#, 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.0625 (half of 0.125)
|
||||
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.0625),
|
||||
"second subdivision at 0.0625, got {}",
|
||||
deltas[1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_4_each() {
|
||||
// 4 subdivisions: 0, 0.03125, 0.0625, 0.09375
|
||||
let outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let expected = [0.0, 0.03125, 0.0625, 0.09375];
|
||||
for (i, (got, exp)) in deltas.iter().zip(expected.iter()).enumerate() {
|
||||
assert!(
|
||||
approx_eq(*got, *exp),
|
||||
"subdivision {}: expected {}, got {}",
|
||||
i,
|
||||
exp,
|
||||
got
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_3_each() {
|
||||
// 3 subdivisions: 0, 0.125/3, 2*0.125/3
|
||||
let outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.125 / 3.0;
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
||||
assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_full() {
|
||||
// window 0.0 1.0 is the full step, same as root
|
||||
let outputs = expect_outputs(r#"0.0 1.0 window "kick" s 0.5 at"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0625), "full window at 0.5 = 0.0625");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_first_half() {
|
||||
// window 0.0 0.5 restricts to first half (0..0.0625)
|
||||
// at 0.5 within that = 0.25 of full step = 0.03125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 0.5 at"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.03125),
|
||||
"first-half window at 0.5 = 0.03125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_second_half() {
|
||||
// window 0.5 1.0 restricts to second half (0.0625..0.125)
|
||||
// at 0.0 within that = start of second half = 0.0625
|
||||
let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.0 at"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.0625),
|
||||
"second-half window at 0.0 = 0.0625, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_second_half_middle() {
|
||||
// window 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375
|
||||
let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.5 at"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.09375), "got {}", deltas[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_windows() {
|
||||
// window 0.0 0.5, then window 0.5 1.0 within that
|
||||
// outer: 0..0.0625, inner: 0.5..1.0 of that = 0.03125..0.0625
|
||||
// at 0.0 in inner = 0.03125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 window 0.5 1.0 window "kick" s 0.0 at"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.03125),
|
||||
"nested window at 0.0 = 0.03125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_pop_sequence() {
|
||||
// First in window 0.0 0.5 at 0.0 -> delta 0
|
||||
// Pop, then in window 0.5 1.0 at 0.0 -> delta 0.0625
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 window "kick" s 0.0 at pop 0.5 1.0 window "snare" s 0.0 at"#,
|
||||
2,
|
||||
);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "first window start");
|
||||
assert!(
|
||||
approx_eq(deltas[1], 0.0625),
|
||||
"second window start, got {}",
|
||||
deltas[1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_in_window() {
|
||||
// window 0.0 0.5 (duration 0.0625), then div 2 each
|
||||
// subdivisions at 0 and 0.03125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 2 div each"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.03125), "got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tempo_affects_stepdur() {
|
||||
// At 60 BPM: stepdur = 60/60/4/1 = 0.25
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_affects_stepdur() {
|
||||
// At 120 BPM, speed 2.0: stepdur = 60/120/4/2 = 0.0625
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_each_at_different_tempo() {
|
||||
// At 60 BPM: stepdur = 0.25, so div 2 each => 0, 0.125
|
||||
let ctx = ctx_with(|c| c.tempo = 60.0);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s 2 div each"#, &ctx).unwrap();
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]);
|
||||
}
|
||||
Reference in New Issue
Block a user