Feat: polyphony + iterator reset
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
This commit is contained in:
@@ -58,7 +58,6 @@ pub enum Op {
|
|||||||
Seed,
|
Seed,
|
||||||
Cycle,
|
Cycle,
|
||||||
PCycle,
|
PCycle,
|
||||||
TCycle,
|
|
||||||
Choose,
|
Choose,
|
||||||
ChanceExec,
|
ChanceExec,
|
||||||
ProbExec,
|
ProbExec,
|
||||||
|
|||||||
@@ -154,6 +154,14 @@ impl CmdRegister {
|
|||||||
&self.deltas
|
&self.deltas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn sound(&self) -> Option<&Value> {
|
||||||
|
self.sound.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn params(&self) -> &[(String, Value)] {
|
||||||
|
&self.params
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn snapshot(&self) -> Option<CmdSnapshot<'_>> {
|
pub(super) fn snapshot(&self) -> Option<CmdSnapshot<'_>> {
|
||||||
if self.sound.is_some() || !self.params.is_empty() {
|
if self.sound.is_some() || !self.params.is_empty() {
|
||||||
Some((self.sound.as_ref(), self.params.as_slice()))
|
Some((self.sound.as_ref(), self.params.as_slice()))
|
||||||
|
|||||||
@@ -152,6 +152,23 @@ impl Forth {
|
|||||||
select_and_run(selected, stack, outputs, cmd)
|
select_and_run(selected, stack, outputs, cmd)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let compute_poly_count = |cmd: &CmdRegister| -> usize {
|
||||||
|
let sound_len = match cmd.sound() {
|
||||||
|
Some(Value::CycleList(items)) => items.len(),
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
let param_max = cmd
|
||||||
|
.params()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, v)| match v {
|
||||||
|
Value::CycleList(items) => items.len(),
|
||||||
|
_ => 1,
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or(1);
|
||||||
|
sound_len.max(param_max)
|
||||||
|
};
|
||||||
|
|
||||||
let emit_with_cycling = |cmd: &CmdRegister,
|
let emit_with_cycling = |cmd: &CmdRegister,
|
||||||
emit_idx: usize,
|
emit_idx: usize,
|
||||||
delta_secs: f64,
|
delta_secs: f64,
|
||||||
@@ -363,38 +380,56 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Op::NewCmd => {
|
Op::NewCmd => {
|
||||||
let val = stack.pop().ok_or("stack underflow")?;
|
if stack.is_empty() {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let values = std::mem::take(stack);
|
||||||
|
let val = if values.len() == 1 {
|
||||||
|
values.into_iter().next().unwrap()
|
||||||
|
} else {
|
||||||
|
Value::CycleList(values)
|
||||||
|
};
|
||||||
cmd.set_sound(val);
|
cmd.set_sound(val);
|
||||||
}
|
}
|
||||||
Op::SetParam(param) => {
|
Op::SetParam(param) => {
|
||||||
let val = stack.pop().ok_or("stack underflow")?;
|
if stack.is_empty() {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let values = std::mem::take(stack);
|
||||||
|
let val = if values.len() == 1 {
|
||||||
|
values.into_iter().next().unwrap()
|
||||||
|
} else {
|
||||||
|
Value::CycleList(values)
|
||||||
|
};
|
||||||
cmd.set_param(param.clone(), val);
|
cmd.set_param(param.clone(), val);
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Emit => {
|
Op::Emit => {
|
||||||
|
let poly_count = compute_poly_count(cmd);
|
||||||
let deltas = if cmd.deltas().is_empty() {
|
let deltas = if cmd.deltas().is_empty() {
|
||||||
vec![Value::Float(0.0, None)]
|
vec![Value::Float(0.0, None)]
|
||||||
} else {
|
} else {
|
||||||
cmd.deltas().to_vec()
|
cmd.deltas().to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
for (emit_idx, delta_val) in deltas.iter().enumerate() {
|
for poly_idx in 0..poly_count {
|
||||||
let delta_frac = delta_val.as_float()?;
|
for delta_val in deltas.iter() {
|
||||||
let delta_secs = ctx.nudge_secs + delta_frac * ctx.step_duration();
|
let delta_frac = delta_val.as_float()?;
|
||||||
// Record delta span for highlighting
|
let delta_secs = ctx.nudge_secs + delta_frac * ctx.step_duration();
|
||||||
if let Some(span) = delta_val.span() {
|
if let Some(span) = delta_val.span() {
|
||||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
|
||||||
trace.selected_spans.push(span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(sound_val) =
|
|
||||||
emit_with_cycling(cmd, emit_idx, delta_secs, outputs)?
|
|
||||||
{
|
|
||||||
if let Some(span) = sound_val.span() {
|
|
||||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
trace.selected_spans.push(span);
|
trace.selected_spans.push(span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(sound_val) =
|
||||||
|
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)?
|
||||||
|
{
|
||||||
|
if let Some(span) = sound_val.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,19 +538,6 @@ impl Forth {
|
|||||||
drain_select_run(count, idx, stack, outputs, cmd)?;
|
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::TCycle => {
|
|
||||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
|
||||||
if count == 0 {
|
|
||||||
return Err("tcycle count must be > 0".into());
|
|
||||||
}
|
|
||||||
if stack.len() < count {
|
|
||||||
return Err("stack underflow".into());
|
|
||||||
}
|
|
||||||
let start = stack.len() - count;
|
|
||||||
let values: Vec<Value> = stack.drain(start..).collect();
|
|
||||||
stack.push(Value::CycleList(values));
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::Choose => {
|
Op::Choose => {
|
||||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
@@ -692,27 +714,10 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Op::At => {
|
Op::At => {
|
||||||
let top = stack.pop().ok_or("stack underflow")?;
|
if stack.is_empty() {
|
||||||
let deltas = match &top {
|
return Err("stack underflow".into());
|
||||||
Value::Float(..) => vec![top],
|
}
|
||||||
Value::Int(n, _) => {
|
let deltas = std::mem::take(stack);
|
||||||
let count = *n as usize;
|
|
||||||
if stack.len() < count {
|
|
||||||
return Err(format!(
|
|
||||||
"at: stack underflow, expected {} values but got {}",
|
|
||||||
count,
|
|
||||||
stack.len()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut vals = Vec::with_capacity(count);
|
|
||||||
for _ in 0..count {
|
|
||||||
vals.push(stack.pop().ok_or("stack underflow")?);
|
|
||||||
}
|
|
||||||
vals.reverse();
|
|
||||||
vals
|
|
||||||
}
|
|
||||||
_ => return Err("at expects float or int count".into()),
|
|
||||||
};
|
|
||||||
cmd.set_deltas(deltas);
|
cmd.set_deltas(deltas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -563,16 +563,6 @@ pub const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "tcycle",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Probability",
|
|
||||||
stack: "(v1..vn n -- CycleList)",
|
|
||||||
desc: "Create cycle list for emit-time resolution",
|
|
||||||
example: "60 64 67 3 tcycle note",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: "every",
|
name: "every",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -1231,9 +1221,9 @@ pub const WORDS: &[Word] = &[
|
|||||||
name: "at",
|
name: "at",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(v1..vn n --)",
|
stack: "(v1..vn --)",
|
||||||
desc: "Set delta context for emit timing",
|
desc: "Set delta context for emit timing",
|
||||||
example: "0 0.5 2 at kick s . => emits at 0 and 0.5 of step",
|
example: "0 0.5 at kick s . => emits at 0 and 0.5 of step",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
@@ -2819,7 +2809,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"seed" => Op::Seed,
|
"seed" => Op::Seed,
|
||||||
"cycle" => Op::Cycle,
|
"cycle" => Op::Cycle,
|
||||||
"pcycle" => Op::PCycle,
|
"pcycle" => Op::PCycle,
|
||||||
"tcycle" => Op::TCycle,
|
|
||||||
"choose" => Op::Choose,
|
"choose" => Op::Choose,
|
||||||
"every" => Op::Every,
|
"every" => Op::Every,
|
||||||
"chance" => Op::ChanceExec,
|
"chance" => Op::ChanceExec,
|
||||||
|
|||||||
@@ -193,21 +193,42 @@ You can also use quotations if you need to execute code:
|
|||||||
|
|
||||||
When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack.
|
When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack.
|
||||||
|
|
||||||
Three cycling words exist:
|
Two cycling words exist:
|
||||||
|
|
||||||
- `cycle` - selects based on `runs` (how many times this step has played)
|
- `cycle` - selects based on `runs` (how many times this step has played)
|
||||||
- `pcycle` - selects based on `iter` (how many times the pattern has looped)
|
- `pcycle` - selects based on `iter` (how many times the pattern has looped)
|
||||||
- `tcycle` - creates a cycle list that resolves at emit time
|
|
||||||
|
|
||||||
The difference between `cycle` and `pcycle` matters when patterns have different lengths. `cycle` counts per-step, `pcycle` counts per-pattern.
|
The difference between `cycle` and `pcycle` matters when patterns have different lengths. `cycle` counts per-step, `pcycle` counts per-pattern.
|
||||||
|
|
||||||
`tcycle` is special. It does not select immediately. Instead it creates a value that cycles when emitted:
|
## Polyphonic Parameters
|
||||||
|
|
||||||
|
Parameter words like `note`, `freq`, and `gain` consume the entire stack. If you push multiple values before a param word, you get polyphony:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
0.3 0.5 0.7 3 tcycle gain
|
60 64 67 note sine s . ;; emits 3 voices with notes 60, 64, 67
|
||||||
```
|
```
|
||||||
|
|
||||||
If you emit multiple times in one step (using `at`), each emit gets the next value from the cycle.
|
This works for any param and for the sound word itself:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
440 880 freq sine tri s . ;; 2 voices: sine at 440, tri at 880
|
||||||
|
```
|
||||||
|
|
||||||
|
When params have different lengths, shorter lists cycle:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
60 64 67 note ;; 3 notes
|
||||||
|
0.5 1.0 gain ;; 2 gains (cycles: 0.5, 1.0, 0.5)
|
||||||
|
sine s . ;; emits 3 voices
|
||||||
|
```
|
||||||
|
|
||||||
|
Polyphony multiplies with `at` deltas:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0 0.5 at ;; 2 time points
|
||||||
|
60 64 note ;; 2 notes
|
||||||
|
sine s . ;; emits 4 voices (2 notes × 2 times)
|
||||||
|
```
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
|
|||||||
@@ -450,6 +450,10 @@ impl RunsCounter {
|
|||||||
*count += 1;
|
*count += 1;
|
||||||
current
|
current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
|
self.counts.retain(|&(b, p, _), _| b != bank || p != pattern);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct TickInput {
|
pub(crate) struct TickInput {
|
||||||
@@ -716,6 +720,7 @@ impl SequencerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
self.runs_counter.clear_pattern(pending.id.bank, pending.id.pattern);
|
||||||
self.audio_state.active_patterns.insert(
|
self.audio_state.active_patterns.insert(
|
||||||
pending.id,
|
pending.id,
|
||||||
ActivePattern {
|
ActivePattern {
|
||||||
|
|||||||
@@ -74,22 +74,6 @@ fn dupn_alias() {
|
|||||||
expect_int("5 3 ! + +", 15);
|
expect_int("5 3 ! + +", 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_creates_cycle_list() {
|
|
||||||
let outputs = expect_outputs(r#"0.0 at 60 64 67 3 tcycle note sine s ."#, 1);
|
|
||||||
assert!(outputs[0].contains("note/60"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_with_multiple_emits() {
|
|
||||||
let f = forth();
|
|
||||||
let ctx = default_ctx();
|
|
||||||
let outputs = f.evaluate(r#"0 0.5 2 at 60 64 2 tcycle note sine s ."#, &ctx).unwrap();
|
|
||||||
assert_eq!(outputs.len(), 2);
|
|
||||||
assert!(outputs[0].contains("note/60"));
|
|
||||||
assert!(outputs[1].contains("note/64"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_zero_count_error() {
|
fn cycle_zero_count_error() {
|
||||||
expect_error("1 2 3 0 cycle", "cycle count must be > 0");
|
expect_error("1 2 3 0 cycle", "cycle count must be > 0");
|
||||||
@@ -99,8 +83,3 @@ fn cycle_zero_count_error() {
|
|||||||
fn choose_zero_count_error() {
|
fn choose_zero_count_error() {
|
||||||
expect_error("1 2 3 0 choose", "choose count must be > 0");
|
expect_error("1 2 3 0 choose", "choose count must be > 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_zero_count_error() {
|
|
||||||
expect_error("1 2 3 0 tcycle", "tcycle count must be > 0");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -106,3 +106,35 @@ fn param_only_multiple_params() {
|
|||||||
assert!(outputs[0].contains("gain/0.5"));
|
assert!(outputs[0].contains("gain/0.5"));
|
||||||
assert!(!outputs[0].contains("sound/"));
|
assert!(!outputs[0].contains("sound/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn polyphonic_notes() {
|
||||||
|
let outputs = expect_outputs(r#"60 64 67 note sine s ."#, 3);
|
||||||
|
assert!(outputs[0].contains("note/60"));
|
||||||
|
assert!(outputs[1].contains("note/64"));
|
||||||
|
assert!(outputs[2].contains("note/67"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn polyphonic_sounds() {
|
||||||
|
let outputs = expect_outputs(r#"440 freq kick hat s ."#, 2);
|
||||||
|
assert!(outputs[0].contains("sound/kick"));
|
||||||
|
assert!(outputs[1].contains("sound/hat"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn polyphonic_cycling() {
|
||||||
|
let outputs = expect_outputs(r#"60 64 67 note 0.5 1.0 gain sine s ."#, 3);
|
||||||
|
assert!(outputs[0].contains("note/60"));
|
||||||
|
assert!(outputs[0].contains("gain/0.5"));
|
||||||
|
assert!(outputs[1].contains("note/64"));
|
||||||
|
assert!(outputs[1].contains("gain/1"));
|
||||||
|
assert!(outputs[2].contains("note/67"));
|
||||||
|
assert!(outputs[2].contains("gain/0.5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn polyphonic_with_at() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine s ."#, 4);
|
||||||
|
assert_eq!(outputs.len(), 4);
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,13 +42,6 @@ fn get_sounds(outputs: &[String]) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_param(outputs: &[String], param: &str) -> Vec<f64> {
|
|
||||||
outputs
|
|
||||||
.iter()
|
|
||||||
.map(|o| parse_params(o).get(param).copied().unwrap_or(0.0))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
const EPSILON: f64 = 1e-9;
|
const EPSILON: f64 = 1e-9;
|
||||||
|
|
||||||
fn approx_eq(a: f64, b: f64) -> bool {
|
fn approx_eq(a: f64, b: f64) -> bool {
|
||||||
@@ -156,7 +149,7 @@ fn at_single_delta() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_list_deltas() {
|
fn at_list_deltas() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s ."#, 2);
|
let outputs = expect_outputs(r#"0 0.5 at "kick" s ."#, 2);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
let step_dur = 0.125;
|
let step_dur = 0.125;
|
||||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
|
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
|
||||||
@@ -165,7 +158,7 @@ fn at_list_deltas() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_three_deltas() {
|
fn at_three_deltas() {
|
||||||
let outputs = expect_outputs(r#"0 0.33 0.67 3 at "kick" s ."#, 3);
|
let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" s ."#, 3);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
let step_dur = 0.125;
|
let step_dur = 0.125;
|
||||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
|
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
|
||||||
@@ -175,70 +168,26 @@ fn at_three_deltas() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_persists_across_emits() {
|
fn at_persists_across_emits() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s . "hat" s ."#, 4);
|
let outputs = expect_outputs(r#"0 0.5 at "kick" s . "hat" s ."#, 4);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_basic() {
|
|
||||||
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note sine s ."#, 3);
|
|
||||||
let notes = get_param(&outputs, "note");
|
|
||||||
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_wraps() {
|
|
||||||
let outputs = expect_outputs(r#"0 0.33 0.67 3 at 60 64 2 tcycle note sine s ."#, 3);
|
|
||||||
let notes = get_param(&outputs, "note");
|
|
||||||
assert_eq!(notes, vec![60.0, 64.0, 60.0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_with_sound() {
|
|
||||||
let outputs = expect_outputs(r#"0 0.5 2 at kick hat 2 tcycle s ."#, 2);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_multiple_params() {
|
|
||||||
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note 0.5 1.0 2 tcycle gain sine s ."#, 3);
|
|
||||||
let notes = get_param(&outputs, "note");
|
|
||||||
let gains = get_param(&outputs, "gain");
|
|
||||||
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
|
||||||
assert_eq!(gains, vec![0.5, 1.0, 0.5]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_reset_with_zero() {
|
fn at_reset_with_zero() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s . 0.0 at "hat" s ."#, 3);
|
let outputs = expect_outputs(r#"0 0.5 at "kick" s . 0.0 at "hat" s ."#, 3);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tcycle_records_selected_spans() {
|
|
||||||
use cagire::forth::ExecutionTrace;
|
|
||||||
|
|
||||||
let f = forth();
|
|
||||||
let mut trace = ExecutionTrace::default();
|
|
||||||
let script = r#"0 0.5 2 at kick hat 2 tcycle s ."#;
|
|
||||||
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
|
||||||
|
|
||||||
// Should have 4 selected spans:
|
|
||||||
// - 2 for at deltas (0 and 0.5)
|
|
||||||
// - 2 for tcycle sound values (kick and hat)
|
|
||||||
assert_eq!(trace.selected_spans.len(), 4, "expected 4 selected spans (2 at + 2 tcycle)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_records_selected_spans() {
|
fn at_records_selected_spans() {
|
||||||
use cagire::forth::ExecutionTrace;
|
use cagire::forth::ExecutionTrace;
|
||||||
|
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let mut trace = ExecutionTrace::default();
|
let mut trace = ExecutionTrace::default();
|
||||||
let script = r#"0 0.5 0.75 3 at "kick" s ."#;
|
let script = r#"0 0.5 0.75 at "kick" s ."#;
|
||||||
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
||||||
|
|
||||||
// Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
|
// Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
|
||||||
|
|||||||
Reference in New Issue
Block a user