Cleaning old temporal model
This commit is contained in:
@@ -49,7 +49,6 @@ pub enum Op {
|
|||||||
NewCmd,
|
NewCmd,
|
||||||
SetParam(String),
|
SetParam(String),
|
||||||
Emit,
|
Emit,
|
||||||
Silence,
|
|
||||||
Get,
|
Get,
|
||||||
Set,
|
Set,
|
||||||
GetContext(String),
|
GetContext(String),
|
||||||
@@ -67,7 +66,6 @@ pub enum Op {
|
|||||||
ListEndCycle,
|
ListEndCycle,
|
||||||
PCycle,
|
PCycle,
|
||||||
ListEndPCycle,
|
ListEndPCycle,
|
||||||
Scale,
|
|
||||||
SetTempo,
|
SetTempo,
|
||||||
Every,
|
Every,
|
||||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||||
@@ -84,9 +82,6 @@ pub enum Op {
|
|||||||
Loop,
|
Loop,
|
||||||
Degree(&'static [i64]),
|
Degree(&'static [i64]),
|
||||||
Oct,
|
Oct,
|
||||||
DivStart,
|
|
||||||
DivEnd,
|
|
||||||
StackStart,
|
|
||||||
EmitN,
|
EmitN,
|
||||||
ClearCmd,
|
ClearCmd,
|
||||||
SetSpeed,
|
SetSpeed,
|
||||||
|
|||||||
@@ -147,54 +147,3 @@ impl CmdRegister {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(super) struct PendingEmission {
|
|
||||||
pub sound: String,
|
|
||||||
pub params: Vec<(String, String)>,
|
|
||||||
pub slot_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(super) struct ResolvedEmission {
|
|
||||||
pub sound: String,
|
|
||||||
pub params: Vec<(String, String)>,
|
|
||||||
pub parent_slot: usize,
|
|
||||||
pub offset_in_slot: f64,
|
|
||||||
pub dur: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(super) struct ScopeContext {
|
|
||||||
pub start: f64,
|
|
||||||
pub duration: f64,
|
|
||||||
pub weight: f64,
|
|
||||||
pub slot_count: usize,
|
|
||||||
pub pending: Vec<PendingEmission>,
|
|
||||||
pub resolved: Vec<ResolvedEmission>,
|
|
||||||
pub stacked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScopeContext {
|
|
||||||
pub fn new(start: f64, duration: f64) -> Self {
|
|
||||||
Self {
|
|
||||||
start,
|
|
||||||
duration,
|
|
||||||
weight: 1.0,
|
|
||||||
slot_count: 0,
|
|
||||||
pending: Vec::new(),
|
|
||||||
resolved: Vec::new(),
|
|
||||||
stacked: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn claim_slot(&mut self) -> usize {
|
|
||||||
if self.stacked {
|
|
||||||
self.slot_count = 1;
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
let idx = self.slot_count;
|
|
||||||
self.slot_count += 1;
|
|
||||||
idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use rand::{Rng as RngTrait, SeedableRng};
|
|||||||
use super::compiler::compile_script;
|
use super::compiler::compile_script;
|
||||||
use super::ops::Op;
|
use super::ops::Op;
|
||||||
use super::types::{
|
use super::types::{
|
||||||
CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext,
|
CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
|
||||||
Stack, StepContext, Value, Variables,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Forth {
|
pub struct Forth {
|
||||||
@@ -70,24 +69,9 @@ impl Forth {
|
|||||||
) -> Result<Vec<String>, String> {
|
) -> Result<Vec<String>, String> {
|
||||||
let mut stack = self.stack.lock().unwrap();
|
let mut stack = self.stack.lock().unwrap();
|
||||||
let mut outputs: Vec<String> = Vec::new();
|
let mut outputs: Vec<String> = Vec::new();
|
||||||
let root_duration = ctx.step_duration() * 4.0;
|
|
||||||
let mut scope_stack: Vec<ScopeContext> = vec![ScopeContext::new(0.0, root_duration)];
|
|
||||||
let mut cmd = CmdRegister::default();
|
let mut cmd = CmdRegister::default();
|
||||||
|
|
||||||
self.execute_ops(
|
self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
|
||||||
ops,
|
|
||||||
ctx,
|
|
||||||
&mut stack,
|
|
||||||
&mut outputs,
|
|
||||||
&mut scope_stack,
|
|
||||||
&mut cmd,
|
|
||||||
trace,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Resolve root scope at end of script
|
|
||||||
if let Some(scope) = scope_stack.pop() {
|
|
||||||
resolve_scope(&scope, ctx.step_duration(), ctx.nudge_secs, &mut outputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(outputs)
|
Ok(outputs)
|
||||||
}
|
}
|
||||||
@@ -100,70 +84,56 @@ impl Forth {
|
|||||||
ctx: &StepContext,
|
ctx: &StepContext,
|
||||||
stack: &mut Vec<Value>,
|
stack: &mut Vec<Value>,
|
||||||
outputs: &mut Vec<String>,
|
outputs: &mut Vec<String>,
|
||||||
scope_stack: &mut Vec<ScopeContext>,
|
|
||||||
cmd: &mut CmdRegister,
|
cmd: &mut CmdRegister,
|
||||||
trace: Option<&mut ExecutionTrace>,
|
trace: Option<&mut ExecutionTrace>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut pc = 0;
|
let mut pc = 0;
|
||||||
let trace_cell = std::cell::RefCell::new(trace);
|
let trace_cell = std::cell::RefCell::new(trace);
|
||||||
|
|
||||||
// Executes a quotation value, handling trace recording and recursive dispatch.
|
let run_quotation =
|
||||||
let run_quotation = |quot: Value,
|
|quot: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, cmd: &mut CmdRegister| -> Result<(), String> {
|
||||||
stack: &mut Vec<Value>,
|
match quot {
|
||||||
outputs: &mut Vec<String>,
|
Value::Quotation(quot_ops, body_span) => {
|
||||||
scope_stack: &mut Vec<ScopeContext>,
|
if let Some(span) = body_span {
|
||||||
cmd: &mut CmdRegister|
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
-> Result<(), String> {
|
trace.executed_spans.push(span);
|
||||||
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,
|
||||||
|
cmd,
|
||||||
|
trace_opt.as_deref_mut(),
|
||||||
|
)?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
_ => Err("expected quotation".into()),
|
||||||
self.execute_ops(
|
}
|
||||||
"_ops,
|
};
|
||||||
ctx,
|
|
||||||
stack,
|
let select_and_run =
|
||||||
outputs,
|
|selected: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, cmd: &mut CmdRegister| -> Result<(), String> {
|
||||||
scope_stack,
|
if let Some(span) = selected.span() {
|
||||||
cmd,
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
trace_opt.as_deref_mut(),
|
trace.selected_spans.push(span);
|
||||||
)?;
|
}
|
||||||
*trace_cell.borrow_mut() = trace_opt;
|
}
|
||||||
|
if matches!(selected, Value::Quotation(..)) {
|
||||||
|
run_quotation(selected, stack, outputs, cmd)
|
||||||
|
} else {
|
||||||
|
stack.push(selected);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err("expected quotation".into()),
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Selects a value from a list, records trace, and either executes (quotation) or pushes (other).
|
|
||||||
let select_and_run = |selected: Value,
|
|
||||||
stack: &mut Vec<Value>,
|
|
||||||
outputs: &mut Vec<String>,
|
|
||||||
scope_stack: &mut Vec<ScopeContext>,
|
|
||||||
cmd: &mut CmdRegister|
|
|
||||||
-> Result<(), String> {
|
|
||||||
if let Some(span) = selected.span() {
|
|
||||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
|
||||||
trace.selected_spans.push(span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if matches!(selected, Value::Quotation(..)) {
|
|
||||||
run_quotation(selected, stack, outputs, scope_stack, cmd)
|
|
||||||
} else {
|
|
||||||
stack.push(selected);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Drains `count` values from the stack, selects one by index, and runs it.
|
|
||||||
let drain_select_run = |count: usize,
|
let drain_select_run = |count: usize,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
stack: &mut Vec<Value>,
|
stack: &mut Vec<Value>,
|
||||||
outputs: &mut Vec<String>,
|
outputs: &mut Vec<String>,
|
||||||
scope_stack: &mut Vec<ScopeContext>,
|
|
||||||
cmd: &mut CmdRegister|
|
cmd: &mut CmdRegister|
|
||||||
-> Result<(), String> {
|
-> Result<(), String> {
|
||||||
if stack.len() < count {
|
if stack.len() < count {
|
||||||
@@ -172,15 +142,13 @@ impl Forth {
|
|||||||
let start = stack.len() - count;
|
let start = stack.len() - count;
|
||||||
let values: Vec<Value> = stack.drain(start..).collect();
|
let values: Vec<Value> = stack.drain(start..).collect();
|
||||||
let selected = values[idx].clone();
|
let selected = values[idx].clone();
|
||||||
select_and_run(selected, stack, outputs, scope_stack, cmd)
|
select_and_run(selected, stack, outputs, cmd)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pops all values until a marker, selects one by index, and runs it.
|
|
||||||
let drain_list_select_run = |idx_source: usize,
|
let drain_list_select_run = |idx_source: usize,
|
||||||
err_msg: &str,
|
err_msg: &str,
|
||||||
stack: &mut Vec<Value>,
|
stack: &mut Vec<Value>,
|
||||||
outputs: &mut Vec<String>,
|
outputs: &mut Vec<String>,
|
||||||
scope_stack: &mut Vec<ScopeContext>,
|
|
||||||
cmd: &mut CmdRegister|
|
cmd: &mut CmdRegister|
|
||||||
-> Result<(), String> {
|
-> Result<(), String> {
|
||||||
let mut values = Vec::new();
|
let mut values = Vec::new();
|
||||||
@@ -196,26 +164,15 @@ impl Forth {
|
|||||||
values.reverse();
|
values.reverse();
|
||||||
let idx = idx_source % values.len();
|
let idx = idx_source % values.len();
|
||||||
let selected = values[idx].clone();
|
let selected = values[idx].clone();
|
||||||
select_and_run(selected, stack, outputs, scope_stack, cmd)
|
select_and_run(selected, stack, outputs, cmd)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emits one sound event from the current command register into the current scope.
|
let emit_once = |cmd: &CmdRegister, outputs: &mut Vec<String>| -> Result<Option<Value>, String> {
|
||||||
let emit_once = |cmd: &CmdRegister,
|
|
||||||
scope_stack: &mut Vec<ScopeContext>|
|
|
||||||
-> Result<Option<Value>, String> {
|
|
||||||
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
|
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
|
||||||
let sound = sound_val.as_str()?.to_string();
|
let sound = sound_val.as_str()?.to_string();
|
||||||
let resolved_params: Vec<(String, String)> = params
|
let resolved_params: Vec<(String, String)> =
|
||||||
.iter()
|
params.iter().map(|(k, v)| (k.clone(), v.to_param_string())).collect();
|
||||||
.map(|(k, v)| (k.clone(), v.to_param_string()))
|
emit_output(&sound, &resolved_params, ctx.step_duration(), ctx.nudge_secs, outputs);
|
||||||
.collect();
|
|
||||||
let scope = scope_stack.last_mut().ok_or("scope stack underflow")?;
|
|
||||||
let slot_idx = scope.claim_slot();
|
|
||||||
scope.pending.push(PendingEmission {
|
|
||||||
sound,
|
|
||||||
params: resolved_params,
|
|
||||||
slot_index: slot_idx,
|
|
||||||
});
|
|
||||||
Ok(Some(sound_val))
|
Ok(Some(sound_val))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -404,7 +361,7 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Op::Emit => {
|
Op::Emit => {
|
||||||
if let Some(sound_val) = emit_once(cmd, scope_stack)? {
|
if let Some(sound_val) = emit_once(cmd, outputs)? {
|
||||||
if let Some(span) = sound_val.span() {
|
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);
|
||||||
@@ -413,17 +370,6 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Silence => {
|
|
||||||
let scope = scope_stack.last_mut().ok_or("scope stack underflow")?;
|
|
||||||
scope.claim_slot();
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::Scale => {
|
|
||||||
let factor = stack.pop().ok_or("stack underflow")?.as_float()?;
|
|
||||||
let scope = scope_stack.last_mut().ok_or("scope stack underflow")?;
|
|
||||||
scope.weight = factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::Get => {
|
Op::Get => {
|
||||||
let name = stack.pop().ok_or("stack underflow")?;
|
let name = stack.pop().ok_or("stack underflow")?;
|
||||||
let name = name.as_str()?;
|
let name = name.as_str()?;
|
||||||
@@ -489,7 +435,7 @@ impl Forth {
|
|||||||
Op::Cycle => ctx.runs,
|
Op::Cycle => ctx.runs,
|
||||||
_ => ctx.iter,
|
_ => ctx.iter,
|
||||||
} % count;
|
} % count;
|
||||||
drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?;
|
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Choose => {
|
Op::Choose => {
|
||||||
@@ -498,7 +444,7 @@ impl Forth {
|
|||||||
return Err("choose count must be > 0".into());
|
return Err("choose count must be > 0".into());
|
||||||
}
|
}
|
||||||
let idx = self.rng.lock().unwrap().gen_range(0..count);
|
let idx = self.rng.lock().unwrap().gen_range(0..count);
|
||||||
drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?;
|
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::ChanceExec | Op::ProbExec => {
|
Op::ChanceExec | Op::ProbExec => {
|
||||||
@@ -510,7 +456,7 @@ impl Forth {
|
|||||||
_ => threshold / 100.0,
|
_ => threshold / 100.0,
|
||||||
};
|
};
|
||||||
if val < limit {
|
if val < limit {
|
||||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
run_quotation(quot, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +486,7 @@ impl Forth {
|
|||||||
_ => !cond.is_truthy(),
|
_ => !cond.is_truthy(),
|
||||||
};
|
};
|
||||||
if should_run {
|
if should_run {
|
||||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
run_quotation(quot, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +499,7 @@ impl Forth {
|
|||||||
} else {
|
} else {
|
||||||
false_quot
|
false_quot
|
||||||
};
|
};
|
||||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
run_quotation(quot, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Pick => {
|
Op::Pick => {
|
||||||
@@ -580,7 +526,7 @@ impl Forth {
|
|||||||
quots.len()
|
quots.len()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
run_quotation(quots.swap_remove(idx), stack, outputs, scope_stack, cmd)?;
|
run_quotation(quots.swap_remove(idx), stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Mtof => {
|
Op::Mtof => {
|
||||||
@@ -690,7 +636,7 @@ impl Forth {
|
|||||||
Op::ListEndCycle => "empty cycle list",
|
Op::ListEndCycle => "empty cycle list",
|
||||||
_ => "empty pattern cycle list",
|
_ => "empty pattern cycle list",
|
||||||
};
|
};
|
||||||
drain_list_select_run(idx_source, err_msg, stack, outputs, scope_stack, cmd)?;
|
drain_list_select_run(idx_source, err_msg, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Adsr => {
|
Op::Adsr => {
|
||||||
@@ -714,7 +660,7 @@ impl Forth {
|
|||||||
|
|
||||||
Op::Apply => {
|
Op::Apply => {
|
||||||
let quot = stack.pop().ok_or("stack underflow")?;
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
run_quotation(quot, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Ramp => {
|
Op::Ramp => {
|
||||||
@@ -744,36 +690,6 @@ impl Forth {
|
|||||||
stack.push(Value::Float(val, None));
|
stack.push(Value::Float(val, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::DivStart => {
|
|
||||||
let parent = scope_stack.last().ok_or("scope stack underflow")?;
|
|
||||||
let mut new_scope = ScopeContext::new(parent.start, parent.duration);
|
|
||||||
new_scope.weight = parent.weight;
|
|
||||||
scope_stack.push(new_scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::DivEnd => {
|
|
||||||
if scope_stack.len() <= 1 {
|
|
||||||
return Err("unmatched ~ (no div/stack to close)".into());
|
|
||||||
}
|
|
||||||
let child = scope_stack.pop().unwrap();
|
|
||||||
|
|
||||||
if child.stacked {
|
|
||||||
resolve_scope(&child, ctx.step_duration(), ctx.nudge_secs, outputs);
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::StackStart => {
|
|
||||||
let parent = scope_stack.last().ok_or("scope stack underflow")?;
|
|
||||||
let mut new_scope = ScopeContext::new(parent.start, parent.duration);
|
|
||||||
new_scope.weight = parent.weight;
|
|
||||||
new_scope.stacked = true;
|
|
||||||
scope_stack.push(new_scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::ClearCmd => {
|
Op::ClearCmd => {
|
||||||
cmd.clear();
|
cmd.clear();
|
||||||
}
|
}
|
||||||
@@ -784,7 +700,7 @@ impl Forth {
|
|||||||
return Err("emit count must be >= 0".into());
|
return Err("emit count must be >= 0".into());
|
||||||
}
|
}
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
emit_once(cmd, scope_stack)?;
|
emit_once(cmd, outputs)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,106 +711,6 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_scope(
|
|
||||||
scope: &ScopeContext,
|
|
||||||
step_duration: f64,
|
|
||||||
nudge_secs: f64,
|
|
||||||
outputs: &mut Vec<String>,
|
|
||||||
) {
|
|
||||||
let slot_dur = if scope.slot_count == 0 {
|
|
||||||
scope.duration * scope.weight
|
|
||||||
} else {
|
|
||||||
scope.duration * scope.weight / scope.slot_count as f64
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Emission {
|
|
||||||
delta: f64,
|
|
||||||
sound: String,
|
|
||||||
params: Vec<(String, String)>,
|
|
||||||
dur: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut emissions: Vec<Emission> = Vec::new();
|
|
||||||
|
|
||||||
for em in &scope.pending {
|
|
||||||
let delta = scope.start + slot_dur * em.slot_index as f64;
|
|
||||||
emissions.push(Emission {
|
|
||||||
delta,
|
|
||||||
sound: em.sound.clone(),
|
|
||||||
params: em.params.clone(),
|
|
||||||
dur: 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(Emission {
|
|
||||||
delta,
|
|
||||||
sound: em.sound.clone(),
|
|
||||||
params: em.params.clone(),
|
|
||||||
dur,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
emissions.sort_by(|a, b| {
|
|
||||||
a.delta
|
|
||||||
.partial_cmp(&b.delta)
|
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
});
|
|
||||||
|
|
||||||
for em in emissions {
|
|
||||||
emit_output(
|
|
||||||
&em.sound,
|
|
||||||
&em.params,
|
|
||||||
em.delta,
|
|
||||||
em.dur,
|
|
||||||
step_duration,
|
|
||||||
nudge_secs,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEMPO_SCALED_PARAMS: &[&str] = &[
|
const TEMPO_SCALED_PARAMS: &[&str] = &[
|
||||||
"attack",
|
"attack",
|
||||||
"decay",
|
"decay",
|
||||||
@@ -924,26 +740,23 @@ const TEMPO_SCALED_PARAMS: &[&str] = &[
|
|||||||
fn emit_output(
|
fn emit_output(
|
||||||
sound: &str,
|
sound: &str,
|
||||||
params: &[(String, String)],
|
params: &[(String, String)],
|
||||||
delta: f64,
|
|
||||||
dur: f64,
|
|
||||||
step_duration: f64,
|
step_duration: f64,
|
||||||
nudge_secs: f64,
|
nudge_secs: f64,
|
||||||
outputs: &mut Vec<String>,
|
outputs: &mut Vec<String>,
|
||||||
) {
|
) {
|
||||||
let nudged_delta = delta + nudge_secs;
|
|
||||||
let mut pairs = vec![("sound".into(), sound.to_string())];
|
let mut pairs = vec![("sound".into(), sound.to_string())];
|
||||||
pairs.extend(params.iter().cloned());
|
pairs.extend(params.iter().cloned());
|
||||||
if nudged_delta > 0.0 {
|
if nudge_secs > 0.0 {
|
||||||
pairs.push(("delta".into(), nudged_delta.to_string()));
|
pairs.push(("delta".into(), nudge_secs.to_string()));
|
||||||
}
|
}
|
||||||
if !pairs.iter().any(|(k, _)| k == "dur") {
|
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||||
pairs.push(("dur".into(), dur.to_string()));
|
pairs.push(("dur".into(), step_duration.to_string()));
|
||||||
}
|
}
|
||||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||||
pairs[idx].1 = (ratio * dur).to_string();
|
pairs[idx].1 = (ratio * step_duration).to_string();
|
||||||
} else {
|
} else {
|
||||||
pairs.push(("delaytime".into(), dur.to_string()));
|
pairs.push(("delaytime".into(), step_duration.to_string()));
|
||||||
}
|
}
|
||||||
for pair in &mut pairs {
|
for pair in &mut pairs {
|
||||||
if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) {
|
if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) {
|
||||||
|
|||||||
@@ -392,19 +392,10 @@ pub const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Sound",
|
category: "Sound",
|
||||||
stack: "(--)",
|
stack: "(--)",
|
||||||
desc: "Emit current sound, claim one time slot",
|
desc: "Emit current sound",
|
||||||
example: "\"kick\" s . . . .",
|
example: "\"kick\" s . . . .",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "_",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(--)",
|
|
||||||
desc: "Silence, claim one time slot",
|
|
||||||
example: "\"kick\" s . _ . _",
|
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: ".!",
|
name: ".!",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -414,33 +405,6 @@ pub const WORDS: &[Word] = &[
|
|||||||
example: "\"kick\" s 4 .!",
|
example: "\"kick\" s 4 .!",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "div",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Time",
|
|
||||||
stack: "(--)",
|
|
||||||
desc: "Start a time subdivision scope (div claims a slot in parent)",
|
|
||||||
example: "div \"kick\" s . \"hat\" s . ~",
|
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "stack",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Time",
|
|
||||||
stack: "(--)",
|
|
||||||
desc: "Start a stacked subdivision scope (sounds stack/superpose)",
|
|
||||||
example: "stack \"kick\" s . \"hat\" s . ~",
|
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "~",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Time",
|
|
||||||
stack: "(--)",
|
|
||||||
desc: "End a time subdivision scope (div or stack)",
|
|
||||||
example: "div \"kick\" s . ~",
|
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
// Variables (prefix syntax: @name to fetch, !name to store)
|
// Variables (prefix syntax: @name to fetch, !name to store)
|
||||||
Word {
|
Word {
|
||||||
name: "@<var>",
|
name: "@<var>",
|
||||||
@@ -713,7 +677,7 @@ pub const WORDS: &[Word] = &[
|
|||||||
category: "Context",
|
category: "Context",
|
||||||
stack: "(-- bool)",
|
stack: "(-- bool)",
|
||||||
desc: "True when fill is on (f key)",
|
desc: "True when fill is on (f key)",
|
||||||
example: "{ 4 div each } fill ?",
|
example: "\"snare\" s . fill ?",
|
||||||
compile: Context("fill"),
|
compile: Context("fill"),
|
||||||
},
|
},
|
||||||
// Music
|
// Music
|
||||||
@@ -799,16 +763,6 @@ pub const WORDS: &[Word] = &[
|
|||||||
example: "0.25 perlin",
|
example: "0.25 perlin",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
// Time
|
|
||||||
Word {
|
|
||||||
name: "scale!",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Time",
|
|
||||||
stack: "(factor --)",
|
|
||||||
desc: "Set weight of current time scope",
|
|
||||||
example: "2 scale!",
|
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: "loop",
|
name: "loop",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -2105,8 +2059,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"ftom" => Op::Ftom,
|
"ftom" => Op::Ftom,
|
||||||
"?" => Op::When,
|
"?" => Op::When,
|
||||||
"!?" => Op::Unless,
|
"!?" => Op::Unless,
|
||||||
"_" => Op::Silence,
|
|
||||||
"scale!" => Op::Scale,
|
|
||||||
"tempo!" => Op::SetTempo,
|
"tempo!" => Op::SetTempo,
|
||||||
"speed!" => Op::SetSpeed,
|
"speed!" => Op::SetSpeed,
|
||||||
"[" => Op::ListStart,
|
"[" => Op::ListStart,
|
||||||
@@ -2123,9 +2075,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"chain" => Op::Chain,
|
"chain" => Op::Chain,
|
||||||
"loop" => Op::Loop,
|
"loop" => Op::Loop,
|
||||||
"oct" => Op::Oct,
|
"oct" => Op::Oct,
|
||||||
"div" => Op::DivStart,
|
|
||||||
"stack" => Op::StackStart,
|
|
||||||
"~" => Op::DivEnd,
|
|
||||||
".!" => Op::EmitN,
|
".!" => Op::EmitN,
|
||||||
"clear" => Op::ClearCmd,
|
"clear" => Op::ClearCmd,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ fn approx_eq(a: f64, b: f64) -> bool {
|
|||||||
(a - b).abs() < EPSILON
|
(a - b).abs() < EPSILON
|
||||||
}
|
}
|
||||||
|
|
||||||
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
|
|
||||||
// Root duration = 4 * stepdur = 0.5s
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stepdur_baseline() {
|
fn stepdur_baseline() {
|
||||||
let f = run("stepdur");
|
let f = run("stepdur");
|
||||||
@@ -65,72 +62,14 @@ fn single_emit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn implicit_subdivision_2() {
|
fn multiple_emits_all_at_zero() {
|
||||||
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");
|
|
||||||
assert!(approx_eq(deltas[1], step), "second slot at {}, got {}", step, deltas[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 deltas = get_deltas(&outputs);
|
||||||
let step = 0.5 / 4.0;
|
|
||||||
for (i, delta) in deltas.iter().enumerate() {
|
for (i, delta) in deltas.iter().enumerate() {
|
||||||
let expected = step * i as f64;
|
assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
|
||||||
assert!(
|
|
||||||
approx_eq(*delta, expected),
|
|
||||||
"slot {}: expected {}, got {}",
|
|
||||||
i, expected, delta
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn implicit_subdivision_3() {
|
|
||||||
let outputs = expect_outputs(r#""kick" s . . ."#, 3);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let step = 0.5 / 3.0;
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
|
||||||
assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn silence_creates_gap() {
|
|
||||||
let outputs = expect_outputs(r#""kick" s . _ ."#, 2);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let step = 0.5 / 3.0;
|
|
||||||
assert!(approx_eq(deltas[0], 0.0), "first at 0");
|
|
||||||
assert!(
|
|
||||||
approx_eq(deltas[1], 2.0 * step),
|
|
||||||
"third slot (after silence) at {}, got {}",
|
|
||||||
2.0 * step,
|
|
||||||
deltas[1]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn silence_at_start() {
|
|
||||||
let outputs = expect_outputs(r#""kick" s _ ."#, 1);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let step = 0.5 / 2.0;
|
|
||||||
assert!(
|
|
||||||
approx_eq(deltas[0], step),
|
|
||||||
"emit after silence at {}, got {}",
|
|
||||||
step,
|
|
||||||
deltas[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn silence_only() {
|
|
||||||
let outputs = expect_outputs(r#""kick" s _"#, 0);
|
|
||||||
assert!(outputs.is_empty(), "silence only should produce no output");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sound_persists() {
|
fn sound_persists() {
|
||||||
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
|
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
|
||||||
@@ -149,41 +88,10 @@ fn alternating_sounds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dur_matches_slot_duration() {
|
fn dur_is_step_duration() {
|
||||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||||
let durs = get_durs(&outputs);
|
let durs = get_durs(&outputs);
|
||||||
let expected_dur = 0.5 / 4.0;
|
assert!(approx_eq(durs[0], 0.125), "dur should be step_duration (0.125), got {}", durs[0]);
|
||||||
for (i, dur) in durs.iter().enumerate() {
|
|
||||||
assert!(
|
|
||||||
approx_eq(*dur, expected_dur),
|
|
||||||
"slot {} dur: expected {}, got {}",
|
|
||||||
i, expected_dur, dur
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tempo_affects_subdivision() {
|
|
||||||
let ctx = ctx_with(|c| c.tempo = 60.0);
|
|
||||||
let f = forth();
|
|
||||||
let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap();
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
// At 60 BPM: stepdur = 0.25, root dur = 1.0
|
|
||||||
let step = 1.0 / 2.0;
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn speed_affects_subdivision() {
|
|
||||||
let ctx = ctx_with(|c| c.speed = 2.0);
|
|
||||||
let f = forth();
|
|
||||||
let outputs = f.evaluate(r#""kick" s . ."#, &ctx).unwrap();
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
// At speed 2.0: stepdur = 0.0625, root dur = 0.25
|
|
||||||
let step = 0.25 / 2.0;
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -227,143 +135,6 @@ fn cycle_with_sounds() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dot_alias_for_emit() {
|
|
||||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dot_with_silence() {
|
|
||||||
let outputs = expect_outputs(r#""kick" s . _ . _"#, 2);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let step = 0.5 / 4.0;
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], 2.0 * step));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn div_basic_subdivision() {
|
|
||||||
let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], 0.25), "second should be at 0.25, got {}", deltas[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn div_sequential() {
|
|
||||||
// Two consecutive divs each claim a slot in root, so they're sequential
|
|
||||||
let outputs = expect_outputs(r#"div "kick" s . ~ div "hat" s . ~"#, 2);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], 0.25), "second div at slot 1, got {}", deltas[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn div_with_root_emit() {
|
|
||||||
// kick claims slot 0 at root, div claims slot 1 at root
|
|
||||||
let outputs = expect_outputs(r#""kick" s . div "hat" s . ~"#, 2);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
|
||||||
assert!(approx_eq(deltas[0], 0.0), "kick at slot 0");
|
|
||||||
assert!(approx_eq(deltas[1], 0.25), "hat at slot 1, got {}", deltas[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn div_nested() {
|
|
||||||
// kick claims slot 0 in outer div, inner div claims slot 1
|
|
||||||
// Inner div's 2 hats subdivide its slot (0.25 duration) into 2 sub-slots
|
|
||||||
let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . ~ ~"#, 3);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
// Output order: kick (slot 0), then hats (slot 1 subdivided)
|
|
||||||
assert_eq!(sounds[0], "kick");
|
|
||||||
assert_eq!(sounds[1], "hat");
|
|
||||||
assert_eq!(sounds[2], "hat");
|
|
||||||
// Outer div has 2 slots of 0.25 each
|
|
||||||
// kick at slot 0 -> delta 0
|
|
||||||
// inner div at slot 1 -> starts at 0.25, subdivided into 2 -> hats at 0.25 and 0.375
|
|
||||||
assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]);
|
|
||||||
assert!(approx_eq(deltas[1], 0.25), "first hat at 0.25, got {}", deltas[1]);
|
|
||||||
assert!(approx_eq(deltas[2], 0.375), "second hat at 0.375, got {}", deltas[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn div_with_silence() {
|
|
||||||
let outputs = expect_outputs(r#"div "kick" s . _ ~"#, 1);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unmatched_scope_terminator_error() {
|
|
||||||
let f = forth();
|
|
||||||
let result = f.evaluate(r#""kick" s . ~"#, &default_ctx());
|
|
||||||
assert!(result.is_err(), "unmatched ~ should error");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stack_superposes_sounds() {
|
|
||||||
let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
assert_eq!(sounds.len(), 2);
|
|
||||||
// Both at delta 0 (stacked/superposed)
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stack_with_multiple_emits() {
|
|
||||||
let outputs = expect_outputs(r#"stack "kick" s . . . . ~"#, 4);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
// All 4 kicks at delta 0
|
|
||||||
for (i, delta) in deltas.iter().enumerate() {
|
|
||||||
assert!(approx_eq(*delta, 0.0), "emit {} should be at 0, got {}", i, delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stack_inside_div() {
|
|
||||||
// div subdivides, stack inside superposes
|
|
||||||
// stack doesn't claim a slot in parent div, so snare is also at 0
|
|
||||||
let outputs = expect_outputs(r#"div stack "kick" s . "hat" s . ~ "snare" s . ~"#, 3);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
// stack resolves first (kick, hat at 0), then div resolves (snare at 0)
|
|
||||||
// since stack doesn't consume a slot in the parent div
|
|
||||||
assert_eq!(sounds[0], "kick");
|
|
||||||
assert_eq!(sounds[1], "hat");
|
|
||||||
assert_eq!(sounds[2], "snare");
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], 0.0));
|
|
||||||
assert!(approx_eq(deltas[2], 0.0), "snare at 0, got {}", deltas[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn div_nested_with_sibling() {
|
|
||||||
// Inner div claims slot 0, snare claims slot 1
|
|
||||||
// Inner div's kick/hat subdivide slot 0
|
|
||||||
let outputs = expect_outputs(r#"div div "kick" s . "hat" s . ~ "snare" s . ~"#, 3);
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
// Outer div has 2 slots of 0.25 each
|
|
||||||
// Inner div at slot 0: kick at 0, hat at 0.125
|
|
||||||
// snare at slot 1: delta 0.25
|
|
||||||
assert_eq!(sounds[0], "kick");
|
|
||||||
assert_eq!(sounds[1], "hat");
|
|
||||||
assert_eq!(sounds[2], "snare");
|
|
||||||
assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]);
|
|
||||||
assert!(approx_eq(deltas[1], 0.125), "hat at 0.125, got {}", deltas[1]);
|
|
||||||
assert!(approx_eq(deltas[2], 0.25), "snare at 0.25, got {}", deltas[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn emit_n_basic() {
|
fn emit_n_basic() {
|
||||||
let outputs = expect_outputs(r#""kick" s 4 .!"#, 4);
|
let outputs = expect_outputs(r#""kick" s 4 .!"#, 4);
|
||||||
@@ -383,4 +154,3 @@ fn emit_n_negative_error() {
|
|||||||
let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx());
|
let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx());
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user