Feat: improve 'at' in cagire grammar
This commit is contained in:
@@ -176,6 +176,13 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
ops.push(Op::Branch(else_ops.len()));
|
ops.push(Op::Branch(else_ops.len()));
|
||||||
ops.extend(else_ops);
|
ops.extend(else_ops);
|
||||||
}
|
}
|
||||||
|
} else if word == "at" {
|
||||||
|
if let Some((body_ops, consumed)) = compile_at(&tokens[i + 1..], dict)? {
|
||||||
|
i += consumed;
|
||||||
|
ops.push(Op::AtLoop(Arc::from(body_ops)));
|
||||||
|
} else if !compile_word(word, Some(*span), &mut ops, dict) {
|
||||||
|
return Err(format!("unknown word: {word}"));
|
||||||
|
}
|
||||||
} else if word == "case" {
|
} else if word == "case" {
|
||||||
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
|
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
|
||||||
i += consumed;
|
i += consumed;
|
||||||
@@ -355,6 +362,37 @@ fn compile_if(
|
|||||||
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
|
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compile_at(tokens: &[Token], dict: &Dictionary) -> Result<Option<(Vec<Op>, usize)>, String> {
|
||||||
|
let mut depth = 1;
|
||||||
|
|
||||||
|
enum AtCloser { Dot, MidiDot, Done }
|
||||||
|
let mut found: Option<(usize, AtCloser)> = None;
|
||||||
|
|
||||||
|
for (i, tok) in tokens.iter().enumerate() {
|
||||||
|
if let Token::Word(w, _) = tok {
|
||||||
|
match w.as_str() {
|
||||||
|
"at" => depth += 1,
|
||||||
|
"." if depth == 1 => { found = Some((i, AtCloser::Dot)); break; }
|
||||||
|
"m." if depth == 1 => { found = Some((i, AtCloser::MidiDot)); break; }
|
||||||
|
"done" if depth == 1 => { found = Some((i, AtCloser::Done)); break; }
|
||||||
|
"." | "m." | "done" => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((pos, closer)) = found else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let mut body_ops = compile(&tokens[..pos], dict)?;
|
||||||
|
match closer {
|
||||||
|
AtCloser::Dot => body_ops.push(Op::Emit),
|
||||||
|
AtCloser::MidiDot => body_ops.push(Op::MidiEmit),
|
||||||
|
AtCloser::Done => {}
|
||||||
|
}
|
||||||
|
Ok(Some((body_ops, pos + 1)))
|
||||||
|
}
|
||||||
|
|
||||||
fn compile_case(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize), String> {
|
fn compile_case(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize), String> {
|
||||||
let mut depth = 1;
|
let mut depth = 1;
|
||||||
let mut endcase_pos = None;
|
let mut endcase_pos = None;
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ pub enum Op {
|
|||||||
ClearCmd,
|
ClearCmd,
|
||||||
SetSpeed,
|
SetSpeed,
|
||||||
At,
|
At,
|
||||||
Arp,
|
AtLoop(Arc<[Op]>),
|
||||||
|
|
||||||
IntRange,
|
IntRange,
|
||||||
StepRange,
|
StepRange,
|
||||||
Generate,
|
Generate,
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ pub enum Value {
|
|||||||
Str(Arc<str>, Option<SourceSpan>),
|
Str(Arc<str>, Option<SourceSpan>),
|
||||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||||
CycleList(Arc<[Value]>),
|
CycleList(Arc<[Value]>),
|
||||||
ArpList(Arc<[Value]>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
@@ -107,7 +107,7 @@ impl PartialEq for Value {
|
|||||||
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||||
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||||
(Value::CycleList(a), Value::CycleList(b)) => a == b,
|
(Value::CycleList(a), Value::CycleList(b)) => a == b,
|
||||||
(Value::ArpList(a), Value::ArpList(b)) => a == b,
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ impl Value {
|
|||||||
Value::Float(f, _) => *f != 0.0,
|
Value::Float(f, _) => *f != 0.0,
|
||||||
Value::Str(s, _) => !s.is_empty(),
|
Value::Str(s, _) => !s.is_empty(),
|
||||||
Value::Quotation(..) => true,
|
Value::Quotation(..) => true,
|
||||||
Value::CycleList(items) | Value::ArpList(items) => !items.is_empty(),
|
Value::CycleList(items) => !items.is_empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +153,14 @@ impl Value {
|
|||||||
Value::Float(f, _) => f.to_string(),
|
Value::Float(f, _) => f.to_string(),
|
||||||
Value::Str(s, _) => s.to_string(),
|
Value::Str(s, _) => s.to_string(),
|
||||||
Value::Quotation(..) => String::new(),
|
Value::Quotation(..) => String::new(),
|
||||||
Value::CycleList(_) | Value::ArpList(_) => String::new(),
|
Value::CycleList(_) => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn span(&self) -> Option<SourceSpan> {
|
pub(super) fn span(&self) -> Option<SourceSpan> {
|
||||||
match self {
|
match self {
|
||||||
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
|
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
|
||||||
Value::CycleList(_) | Value::ArpList(_) => None,
|
Value::CycleList(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +171,7 @@ pub(super) struct CmdRegister {
|
|||||||
params: Vec<(&'static str, Value)>,
|
params: Vec<(&'static str, Value)>,
|
||||||
deltas: Vec<Value>,
|
deltas: Vec<Value>,
|
||||||
global_params: Vec<(&'static str, Value)>,
|
global_params: Vec<(&'static str, Value)>,
|
||||||
|
delta_secs: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CmdRegister {
|
impl CmdRegister {
|
||||||
@@ -180,6 +181,7 @@ impl CmdRegister {
|
|||||||
params: Vec::with_capacity(16),
|
params: Vec::with_capacity(16),
|
||||||
deltas: Vec::with_capacity(4),
|
deltas: Vec::with_capacity(4),
|
||||||
global_params: Vec::new(),
|
global_params: Vec::new(),
|
||||||
|
delta_secs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,9 +239,26 @@ impl CmdRegister {
|
|||||||
std::mem::take(&mut self.global_params)
|
std::mem::take(&mut self.global_params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_delta_secs(&mut self, secs: f64) {
|
||||||
|
self.delta_secs = Some(secs);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn take_delta_secs(&mut self) -> Option<f64> {
|
||||||
|
self.delta_secs.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear_sound(&mut self) {
|
||||||
|
self.sound = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear_params(&mut self) {
|
||||||
|
self.params.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn clear(&mut self) {
|
pub(super) fn clear(&mut self) {
|
||||||
self.sound = None;
|
self.sound = None;
|
||||||
self.params.clear();
|
self.params.clear();
|
||||||
self.deltas.clear();
|
self.deltas.clear();
|
||||||
|
self.delta_secs = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,31 +241,7 @@ impl Forth {
|
|||||||
sound_len.max(param_max)
|
sound_len.max(param_max)
|
||||||
};
|
};
|
||||||
|
|
||||||
let has_arp_list = |cmd: &CmdRegister| -> bool {
|
|
||||||
matches!(cmd.sound(), Some(Value::ArpList(_)))
|
|
||||||
|| cmd.global_params().iter().chain(cmd.params().iter())
|
|
||||||
.any(|(_, v)| matches!(v, Value::ArpList(_)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let compute_arp_count = |cmd: &CmdRegister| -> usize {
|
|
||||||
let sound_len = match cmd.sound() {
|
|
||||||
Some(Value::ArpList(items)) => items.len(),
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
let param_max = cmd
|
|
||||||
.params()
|
|
||||||
.iter()
|
|
||||||
.map(|(_, v)| match v {
|
|
||||||
Value::ArpList(items) => items.len(),
|
|
||||||
_ => 0,
|
|
||||||
})
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
sound_len.max(param_max).max(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
let emit_with_cycling = |cmd: &CmdRegister,
|
let emit_with_cycling = |cmd: &CmdRegister,
|
||||||
arp_idx: usize,
|
|
||||||
poly_idx: usize,
|
poly_idx: usize,
|
||||||
delta_secs: f64,
|
delta_secs: f64,
|
||||||
outputs: &mut Vec<String>|
|
outputs: &mut Vec<String>|
|
||||||
@@ -277,7 +253,7 @@ impl Forth {
|
|||||||
return Err("nothing to emit".into());
|
return Err("nothing to emit".into());
|
||||||
}
|
}
|
||||||
let resolved_sound_val =
|
let resolved_sound_val =
|
||||||
cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx));
|
cmd.sound().map(|sv| resolve_value(sv, poly_idx));
|
||||||
let sound_str = match &resolved_sound_val {
|
let sound_str = match &resolved_sound_val {
|
||||||
Some(v) => Some(v.as_str()?.to_string()),
|
Some(v) => Some(v.as_str()?.to_string()),
|
||||||
None => None,
|
None => None,
|
||||||
@@ -286,8 +262,8 @@ impl Forth {
|
|||||||
.iter()
|
.iter()
|
||||||
.chain(cmd.params().iter())
|
.chain(cmd.params().iter())
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
let resolved = resolve_value(v, arp_idx, poly_idx);
|
let resolved = resolve_value(v, poly_idx);
|
||||||
if let Value::CycleList(_) | Value::ArpList(_) = v {
|
if let Value::CycleList(_) = v {
|
||||||
if let Some(span) = resolved.span() {
|
if let Some(span) = resolved.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);
|
||||||
@@ -595,49 +571,19 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Op::Emit => {
|
Op::Emit => {
|
||||||
if has_arp_list(cmd) {
|
if let Some(dsecs) = cmd.take_delta_secs() {
|
||||||
let arp_count = compute_arp_count(cmd);
|
|
||||||
let poly_count = compute_poly_count(cmd);
|
let poly_count = compute_poly_count(cmd);
|
||||||
let explicit_deltas = !cmd.deltas().is_empty();
|
for poly_idx in 0..poly_count {
|
||||||
let delta_list: Vec<Value> = if explicit_deltas {
|
|
||||||
cmd.deltas().to_vec()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
let count = if explicit_deltas {
|
|
||||||
arp_count.max(delta_list.len())
|
|
||||||
} else {
|
|
||||||
arp_count
|
|
||||||
};
|
|
||||||
|
|
||||||
for i in 0..count {
|
|
||||||
let delta_secs = if explicit_deltas {
|
|
||||||
let dv = &delta_list[i % delta_list.len()];
|
|
||||||
let frac = dv.as_float()?;
|
|
||||||
if let Some(span) = dv.span() {
|
|
||||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
|
||||||
trace.selected_spans.push(span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.nudge_secs + frac * ctx.step_duration()
|
|
||||||
} else {
|
|
||||||
ctx.nudge_secs
|
|
||||||
+ (i as f64 / count as f64) * ctx.step_duration()
|
|
||||||
};
|
|
||||||
for poly_i in 0..poly_count {
|
|
||||||
if let Some(sound_val) =
|
if let Some(sound_val) =
|
||||||
emit_with_cycling(cmd, i, poly_i, delta_secs, outputs)?
|
emit_with_cycling(cmd, poly_idx, dsecs, outputs)?
|
||||||
{
|
{
|
||||||
if let Some(span) = sound_val.span() {
|
if let Some(span) = sound_val.span() {
|
||||||
if let Some(trace) =
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
trace_cell.borrow_mut().as_mut()
|
|
||||||
{
|
|
||||||
trace.selected_spans.push(span);
|
trace.selected_spans.push(span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let poly_count = compute_poly_count(cmd);
|
let poly_count = compute_poly_count(cmd);
|
||||||
let deltas = if cmd.deltas().is_empty() {
|
let deltas = if cmd.deltas().is_empty() {
|
||||||
@@ -657,7 +603,7 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(sound_val) =
|
if let Some(sound_val) =
|
||||||
emit_with_cycling(cmd, 0, poly_idx, delta_secs, outputs)?
|
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)?
|
||||||
{
|
{
|
||||||
if let Some(span) = sound_val.span() {
|
if let Some(span) = sound_val.span() {
|
||||||
if let Some(trace) =
|
if let Some(trace) =
|
||||||
@@ -1196,11 +1142,59 @@ impl Forth {
|
|||||||
cmd.set_deltas(deltas);
|
cmd.set_deltas(deltas);
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Arp => {
|
Op::AtLoop(body_ops) => {
|
||||||
ensure(stack, 1)?;
|
ensure(stack, 1)?;
|
||||||
let values = std::mem::take(stack);
|
let deltas = std::mem::take(stack);
|
||||||
stack.push(Value::ArpList(Arc::from(values)));
|
let n = deltas.len();
|
||||||
|
|
||||||
|
for (i, delta_val) in deltas.iter().enumerate() {
|
||||||
|
let frac = delta_val.as_float()?;
|
||||||
|
let delta_secs = ctx.nudge_secs + frac * ctx.step_duration();
|
||||||
|
|
||||||
|
let iter_ctx = StepContext {
|
||||||
|
step: ctx.step,
|
||||||
|
beat: ctx.beat,
|
||||||
|
bank: ctx.bank,
|
||||||
|
pattern: ctx.pattern,
|
||||||
|
tempo: ctx.tempo,
|
||||||
|
phase: ctx.phase,
|
||||||
|
slot: ctx.slot,
|
||||||
|
runs: ctx.runs * n + i,
|
||||||
|
iter: ctx.iter,
|
||||||
|
speed: ctx.speed,
|
||||||
|
fill: ctx.fill,
|
||||||
|
nudge_secs: ctx.nudge_secs,
|
||||||
|
sr: ctx.sr,
|
||||||
|
cc_access: ctx.cc_access,
|
||||||
|
speed_key: ctx.speed_key,
|
||||||
|
mouse_x: ctx.mouse_x,
|
||||||
|
mouse_y: ctx.mouse_y,
|
||||||
|
mouse_down: ctx.mouse_down,
|
||||||
|
};
|
||||||
|
|
||||||
|
cmd.set_delta_secs(delta_secs);
|
||||||
|
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
let mut var_writes_guard = var_writes_cell.borrow_mut();
|
||||||
|
let vw = var_writes_guard.as_mut().expect("var_writes taken");
|
||||||
|
self.execute_ops(
|
||||||
|
body_ops,
|
||||||
|
&iter_ctx,
|
||||||
|
stack,
|
||||||
|
outputs,
|
||||||
|
cmd,
|
||||||
|
trace_opt.as_deref_mut(),
|
||||||
|
vars_snapshot,
|
||||||
|
vw,
|
||||||
|
)?;
|
||||||
|
drop(var_writes_guard);
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
|
||||||
|
cmd.clear_params();
|
||||||
|
cmd.clear_sound();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Op::Adsr => {
|
Op::Adsr => {
|
||||||
let r = pop(stack)?;
|
let r = pop(stack)?;
|
||||||
@@ -1499,35 +1493,13 @@ impl Forth {
|
|||||||
|
|
||||||
// MIDI operations
|
// MIDI operations
|
||||||
Op::MidiEmit => {
|
Op::MidiEmit => {
|
||||||
|
let at_loop_delta = cmd.take_delta_secs();
|
||||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||||
|
|
||||||
// Build schedule: (arp_idx, poly_idx, delta_secs)
|
// Build schedule: (poly_idx, delta_secs)
|
||||||
let schedule: Vec<(usize, usize, f64)> = if has_arp_list(cmd) {
|
let schedule: Vec<(usize, f64)> = if let Some(dsecs) = at_loop_delta {
|
||||||
let arp_count = compute_arp_count(cmd);
|
|
||||||
let poly_count = compute_poly_count(cmd);
|
let poly_count = compute_poly_count(cmd);
|
||||||
let explicit = !cmd.deltas().is_empty();
|
(0..poly_count).map(|pi| (pi, dsecs)).collect()
|
||||||
let delta_list = cmd.deltas();
|
|
||||||
let count = if explicit {
|
|
||||||
arp_count.max(delta_list.len())
|
|
||||||
} else {
|
|
||||||
arp_count
|
|
||||||
};
|
|
||||||
let mut sched = Vec::with_capacity(count * poly_count);
|
|
||||||
for i in 0..count {
|
|
||||||
let delta_secs = if explicit {
|
|
||||||
let frac = delta_list[i % delta_list.len()]
|
|
||||||
.as_float()
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
ctx.nudge_secs + frac * ctx.step_duration()
|
|
||||||
} else {
|
|
||||||
ctx.nudge_secs
|
|
||||||
+ (i as f64 / count as f64) * ctx.step_duration()
|
|
||||||
};
|
|
||||||
for poly_i in 0..poly_count {
|
|
||||||
sched.push((i, poly_i, delta_secs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sched
|
|
||||||
} else {
|
} else {
|
||||||
let poly_count = compute_poly_count(cmd);
|
let poly_count = compute_poly_count(cmd);
|
||||||
let deltas: Vec<f64> = if cmd.deltas().is_empty() {
|
let deltas: Vec<f64> = if cmd.deltas().is_empty() {
|
||||||
@@ -1542,7 +1514,6 @@ impl Forth {
|
|||||||
for poly_idx in 0..poly_count {
|
for poly_idx in 0..poly_count {
|
||||||
for &frac in &deltas {
|
for &frac in &deltas {
|
||||||
sched.push((
|
sched.push((
|
||||||
0,
|
|
||||||
poly_idx,
|
poly_idx,
|
||||||
ctx.nudge_secs + frac * ctx.step_duration(),
|
ctx.nudge_secs + frac * ctx.step_duration(),
|
||||||
));
|
));
|
||||||
@@ -1551,14 +1522,14 @@ impl Forth {
|
|||||||
sched
|
sched
|
||||||
};
|
};
|
||||||
|
|
||||||
for (arp_idx, poly_idx, delta_secs) in schedule {
|
for (poly_idx, delta_secs) in schedule {
|
||||||
let get_int = |name: &str| -> Option<i64> {
|
let get_int = |name: &str| -> Option<i64> {
|
||||||
params
|
params
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.find(|(k, _)| *k == name)
|
.find(|(k, _)| *k == name)
|
||||||
.and_then(|(_, v)| {
|
.and_then(|(_, v)| {
|
||||||
resolve_value(v, arp_idx, poly_idx).as_int().ok()
|
resolve_value(v, poly_idx).as_int().ok()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let get_float = |name: &str| -> Option<f64> {
|
let get_float = |name: &str| -> Option<f64> {
|
||||||
@@ -1567,7 +1538,7 @@ impl Forth {
|
|||||||
.rev()
|
.rev()
|
||||||
.find(|(k, _)| *k == name)
|
.find(|(k, _)| *k == name)
|
||||||
.and_then(|(_, v)| {
|
.and_then(|(_, v)| {
|
||||||
resolve_value(v, arp_idx, poly_idx).as_float().ok()
|
resolve_value(v, poly_idx).as_float().ok()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let chan = get_int("chan")
|
let chan = get_int("chan")
|
||||||
@@ -1960,10 +1931,6 @@ where
|
|||||||
F: Fn(f64) -> f64 + Copy,
|
F: Fn(f64) -> f64 + Copy,
|
||||||
{
|
{
|
||||||
match val {
|
match val {
|
||||||
Value::ArpList(items) => {
|
|
||||||
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
|
|
||||||
Ok(Value::ArpList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
Value::CycleList(items) => {
|
Value::CycleList(items) => {
|
||||||
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
|
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
|
||||||
Ok(Value::CycleList(Arc::from(mapped?)))
|
Ok(Value::CycleList(Arc::from(mapped?)))
|
||||||
@@ -1977,11 +1944,6 @@ where
|
|||||||
F: Fn(i64) -> i64 + Copy,
|
F: Fn(i64) -> i64 + Copy,
|
||||||
{
|
{
|
||||||
match val {
|
match val {
|
||||||
Value::ArpList(items) => {
|
|
||||||
let mapped: Result<Vec<_>, _> =
|
|
||||||
items.iter().map(|x| lift_unary_int(x, f)).collect();
|
|
||||||
Ok(Value::ArpList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
Value::CycleList(items) => {
|
Value::CycleList(items) => {
|
||||||
let mapped: Result<Vec<_>, _> =
|
let mapped: Result<Vec<_>, _> =
|
||||||
items.iter().map(|x| lift_unary_int(x, f)).collect();
|
items.iter().map(|x| lift_unary_int(x, f)).collect();
|
||||||
@@ -1996,16 +1958,6 @@ where
|
|||||||
F: Fn(f64, f64) -> f64 + Copy,
|
F: Fn(f64, f64) -> f64 + Copy,
|
||||||
{
|
{
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
(Value::ArpList(items), b) => {
|
|
||||||
let mapped: Result<Vec<_>, _> =
|
|
||||||
items.iter().map(|x| lift_binary(x, b, f)).collect();
|
|
||||||
Ok(Value::ArpList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
(a, Value::ArpList(items)) => {
|
|
||||||
let mapped: Result<Vec<_>, _> =
|
|
||||||
items.iter().map(|x| lift_binary(a, x, f)).collect();
|
|
||||||
Ok(Value::ArpList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
(Value::CycleList(items), b) => {
|
(Value::CycleList(items), b) => {
|
||||||
let mapped: Result<Vec<_>, _> =
|
let mapped: Result<Vec<_>, _> =
|
||||||
items.iter().map(|x| lift_binary(x, b, f)).collect();
|
items.iter().map(|x| lift_binary(x, b, f)).collect();
|
||||||
@@ -2045,11 +1997,8 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_value(val: &Value, arp_idx: usize, poly_idx: usize) -> Cow<'_, Value> {
|
fn resolve_value(val: &Value, poly_idx: usize) -> Cow<'_, Value> {
|
||||||
match val {
|
match val {
|
||||||
Value::ArpList(items) if !items.is_empty() => {
|
|
||||||
Cow::Owned(items[arp_idx % items.len()].clone())
|
|
||||||
}
|
|
||||||
Value::CycleList(items) if !items.is_empty() => {
|
Value::CycleList(items) if !items.is_empty() => {
|
||||||
Cow::Owned(items[poly_idx % items.len()].clone())
|
Cow::Owned(items[poly_idx % items.len()].clone())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"tempo!" => Op::SetTempo,
|
"tempo!" => Op::SetTempo,
|
||||||
"speed!" => Op::SetSpeed,
|
"speed!" => Op::SetSpeed,
|
||||||
"at" => Op::At,
|
"at" => Op::At,
|
||||||
"arp" => Op::Arp,
|
|
||||||
"adsr" => Op::Adsr,
|
"adsr" => Op::Adsr,
|
||||||
"ad" => Op::Ad,
|
"ad" => Op::Ad,
|
||||||
"apply" => Op::Apply,
|
"apply" => Op::Apply,
|
||||||
|
|||||||
@@ -310,8 +310,8 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(v1..vn -- )",
|
stack: "(v1..vn -- )",
|
||||||
desc: "Set delta context for emit timing",
|
desc: "Looping block: re-executes body per delta. Close with . (audio), m. (MIDI), or done (no emit)",
|
||||||
example: "0 0.5 at kick s . => emits at 0 and 0.5 of step",
|
example: "0 0.5 at kick snd 1 2 rand freq . | 0 0.5 at 60 note m. | 0 0.5 at !x done",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,16 +24,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "arp",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(v1..vn -- arplist)",
|
|
||||||
desc: "Wrap stack values as arpeggio list for spreading across deltas",
|
|
||||||
example: "c4 e4 g4 b4 arp note => arpeggio",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: "clear",
|
name: "clear",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"i": 0,
|
"i": 0,
|
||||||
"script": "0 7 .. at\n c2 maj9 arp note\n wide bigverb mysynth \n 2000 1000 0.4 0.8 rand expslide llpf\n 0.4 0.8 rand llpq\n ."
|
"script": "0 7 .. at\n mysynth [ c2 maj9 ] cycle note\n wide bigverb\n 2000 1000 0.4 0.8 rand expslide llpf\n 0.4 0.8 rand llpq\n ."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"i": 8,
|
"i": 8,
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ clear
|
|||||||
snare snd . ;; 1 snare (deltas cleared)
|
snare snd . ;; 1 snare (deltas cleared)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cross-product: at Without arp
|
## Polyphonic at
|
||||||
|
|
||||||
Without `arp`, deltas multiply with polyphonic voices. If you have 3 notes and 2 deltas, you get 6 emits -- every note at every delta:
|
Deltas multiply with polyphonic voices. If you have 3 notes and 2 deltas, you get 6 emits — every note at every delta:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
0 0.5 at
|
0 0.5 at
|
||||||
@@ -45,27 +45,25 @@ c4 e4 g4 note 1.5 decay sine snd .
|
|||||||
|
|
||||||
6 emits: 3 notes x 2 deltas. A chord played twice per step.
|
6 emits: 3 notes x 2 deltas. A chord played twice per step.
|
||||||
|
|
||||||
## 1:1 Pairing: at With arp
|
## Arpeggios with at + cycle
|
||||||
|
|
||||||
`arp` changes the behavior. Instead of cross-product, deltas and arp values pair up 1:1. Each delta gets one note from the arpeggio:
|
Use `cycle` inside an `at` block to pick one note per subdivision:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
0 0.33 0.66 at
|
0 0.33 0.66 at
|
||||||
c4 e4 g4 arp note 0.5 decay sine snd .
|
sine snd [ c4 e4 g4 ] cycle note 0.5 decay .
|
||||||
```
|
```
|
||||||
|
|
||||||
C4 at 0, E4 at 0.33, G4 at 0.66.
|
C4 at 0, E4 at 0.33, G4 at 0.66. `cycle` advances per iteration of the at-loop.
|
||||||
|
|
||||||
If the lists differ in length, the shorter one wraps around:
|
If the list is shorter than the number of deltas, it wraps:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
0 0.25 0.5 0.75 at
|
0 0.25 0.5 0.75 at
|
||||||
c4 e4 arp note 0.3 decay sine snd .
|
sine snd [ c4 e4 ] cycle note 0.3 decay .
|
||||||
```
|
```
|
||||||
|
|
||||||
C4, E4, C4, E4 — the shorter list wraps to fill 4 time points.
|
C4, E4, C4, E4 — wraps to fill 4 time points.
|
||||||
|
|
||||||
This is THE key distinction. Without `arp`: every note at every time. With `arp`: one note per time slot.
|
|
||||||
|
|
||||||
## Generating Deltas
|
## Generating Deltas
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Generators & Sequences
|
# Generators & Sequences
|
||||||
|
|
||||||
Sequences of values drive music: arpeggios, parameter sweeps, rhythmic patterns. Cagire has dedicated words for building sequences on the stack, transforming them, and collapsing them to single values.
|
Sequences of values drive music: melodic patterns, parameter sweeps, rhythmic patterns. Cagire has dedicated words for building sequences on the stack, transforming them, and collapsing them to single values.
|
||||||
|
|
||||||
## Ranges
|
## Ranges
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ Four words reshape values already on the stack. All take n (the count of items t
|
|||||||
|
|
||||||
```forth
|
```forth
|
||||||
1 2 3 4 4 rev ;; 4 3 2 1
|
1 2 3 4 4 rev ;; 4 3 2 1
|
||||||
c4 e4 g4 3 rev ;; g4 e4 c4 (descending arpeggio)
|
c4 e4 g4 3 rev ;; g4 e4 c4 (descending)
|
||||||
```
|
```
|
||||||
|
|
||||||
`shuffle` randomizes order:
|
`shuffle` randomizes order:
|
||||||
|
|||||||
@@ -302,10 +302,10 @@ Combine with voicings for smoother voice leading:
|
|||||||
note 1.5 decay saw snd .
|
note 1.5 decay saw snd .
|
||||||
```
|
```
|
||||||
|
|
||||||
Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for details on `arp`):
|
Arpeggiate diatonic chords using `at` + `cycle` (see the *Timing with at* tutorial):
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
0 major seventh arp note 0.5 decay sine snd .
|
0 0.25 0.5 0.75 at sine snd [ 0 major seventh ] cycle note 0.5 decay .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frequency Conversion
|
## Frequency Conversion
|
||||||
|
|||||||
@@ -253,19 +253,12 @@ fn test_midi_at_with_polyphony() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_arp_notes() {
|
fn test_midi_at_loop_notes() {
|
||||||
let outputs = expect_outputs("c4 e4 g4 arp note m.", 3);
|
// at-loop with m. closer: 3 iterations, each emits one MIDI note
|
||||||
assert!(outputs[0].contains("/note/60/"));
|
let outputs = expect_outputs("0 0.25 0.5 at 60 note m.", 3);
|
||||||
assert!(outputs[1].contains("/note/64/"));
|
for o in &outputs {
|
||||||
assert!(outputs[2].contains("/note/67/"));
|
assert!(o.contains("/note/60/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_midi_arp_with_at() {
|
|
||||||
let outputs = expect_outputs("0 0.25 0.5 at c4 e4 g4 arp note m.", 3);
|
|
||||||
assert!(outputs[0].contains("/note/60/"));
|
|
||||||
assert!(outputs[1].contains("/note/64/"));
|
|
||||||
assert!(outputs[2].contains("/note/67/"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -171,7 +171,9 @@ fn at_three_deltas() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_persists_across_emits() {
|
fn at_persists_across_emits() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . "hat" snd ."#, 4);
|
// With at as a loop, each at...block is independent.
|
||||||
|
// Two separate at blocks each emit twice.
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . 0 0.5 at "hat" snd ."#, 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"]);
|
||||||
}
|
}
|
||||||
@@ -202,17 +204,10 @@ fn at_records_selected_spans() {
|
|||||||
let script = r#"0 0.5 0.75 at "kick" snd ."#;
|
let script = r#"0 0.5 0.75 at "kick" snd ."#;
|
||||||
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)
|
// With at-loop, each iteration emits once: 3 sound spans total
|
||||||
assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)");
|
assert_eq!(trace.selected_spans.len(), 3, "expected 3 selected spans (1 sound per iteration)");
|
||||||
|
|
||||||
// Verify at delta spans (even indices: 0, 2, 4)
|
|
||||||
assert_eq!(&script[trace.selected_spans[0].start as usize..trace.selected_spans[0].end as usize], "0");
|
|
||||||
assert_eq!(&script[trace.selected_spans[2].start as usize..trace.selected_spans[2].end as usize], "0.5");
|
|
||||||
assert_eq!(&script[trace.selected_spans[4].start as usize..trace.selected_spans[4].end as usize], "0.75");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- arp tests ---
|
|
||||||
|
|
||||||
fn get_notes(outputs: &[String]) -> Vec<f64> {
|
fn get_notes(outputs: &[String]) -> Vec<f64> {
|
||||||
outputs
|
outputs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -220,16 +215,13 @@ fn get_notes(outputs: &[String]) -> Vec<f64> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_gains(outputs: &[String]) -> Vec<f64> {
|
|
||||||
outputs
|
|
||||||
.iter()
|
|
||||||
.map(|o| parse_params(o).get("gain").copied().unwrap_or(f64::NAN))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_auto_subdivide() {
|
fn at_loop_with_cycle_notes() {
|
||||||
let outputs = expect_outputs(r#"sine snd c4 e4 g4 b4 arp note ."#, 4);
|
// at-loop + cycle replaces at + arp: cycle advances per subdivision
|
||||||
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
|
let f = forth();
|
||||||
|
let outputs = f.evaluate(r#"0 0.25 0.5 0.75 at sine snd [ c4 e4 g4 b4 ] cycle note ."#, &ctx).unwrap();
|
||||||
|
assert_eq!(outputs.len(), 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0));
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
assert!(approx_eq(notes[1], 64.0));
|
||||||
@@ -245,104 +237,41 @@ fn arp_auto_subdivide() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_with_explicit_at() {
|
fn at_loop_cycle_wraps() {
|
||||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 g4 b4 arp note ."#, 4);
|
// cycle inside at-loop wraps when more deltas than items
|
||||||
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
|
let f = forth();
|
||||||
|
let outputs = f.evaluate(r#"0 0.25 0.5 0.75 at sine snd [ c4 e4 ] cycle note ."#, &ctx).unwrap();
|
||||||
|
assert_eq!(outputs.len(), 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0)); // idx 0 % 2 = 0 -> c4
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
assert!(approx_eq(notes[1], 64.0)); // idx 1 % 2 = 1 -> e4
|
||||||
assert!(approx_eq(notes[2], 67.0));
|
assert!(approx_eq(notes[2], 60.0)); // idx 2 % 2 = 0 -> c4
|
||||||
assert!(approx_eq(notes[3], 71.0));
|
assert!(approx_eq(notes[3], 64.0)); // idx 3 % 2 = 1 -> e4
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let step_dur = 0.125;
|
|
||||||
let sr: f64 = 48000.0;
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], (0.25 * step_dur * sr).round()));
|
|
||||||
assert!(approx_eq(deltas[2], (0.5 * step_dur * sr).round()));
|
|
||||||
assert!(approx_eq(deltas[3], (0.75 * step_dur * sr).round()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_single_note() {
|
fn at_loop_rand_different_per_subdivision() {
|
||||||
let outputs = expect_outputs(r#"sine snd c4 arp note ."#, 1);
|
// rand inside at-loop produces different values per iteration
|
||||||
let notes = get_notes(&outputs);
|
let f = forth();
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
let outputs = f.evaluate(r#"0 0.5 at sine snd 1 1000 rand freq ."#, &default_ctx()).unwrap();
|
||||||
|
assert_eq!(outputs.len(), 2);
|
||||||
|
let freqs: Vec<f64> = outputs.iter()
|
||||||
|
.map(|o| parse_params(o).get("freq").copied().unwrap_or(0.0))
|
||||||
|
.collect();
|
||||||
|
assert!(freqs[0] != freqs[1], "rand should produce different values: {} vs {}", freqs[0], freqs[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arp_fewer_deltas_than_notes() {
|
fn at_loop_poly_cycling() {
|
||||||
let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 g4 b4 arp note ."#, 4);
|
// CycleList inside at-loop: poly stacking within each iteration
|
||||||
let notes = get_notes(&outputs);
|
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
|
||||||
assert!(approx_eq(notes[2], 67.0));
|
|
||||||
assert!(approx_eq(notes[3], 71.0));
|
|
||||||
let deltas = get_deltas(&outputs);
|
|
||||||
let step_dur = 0.125;
|
|
||||||
let sr: f64 = 48000.0;
|
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
|
||||||
assert!(approx_eq(deltas[1], (0.5 * step_dur * sr).round()));
|
|
||||||
assert!(approx_eq(deltas[2], 0.0)); // wraps: 2 % 2 = 0
|
|
||||||
assert!(approx_eq(deltas[3], (0.5 * step_dur * sr).round())); // wraps: 3 % 2 = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn arp_fewer_notes_than_deltas() {
|
|
||||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 arp note ."#, 4);
|
|
||||||
let notes = get_notes(&outputs);
|
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
|
||||||
assert!(approx_eq(notes[2], 60.0)); // wraps
|
|
||||||
assert!(approx_eq(notes[3], 64.0)); // wraps
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn arp_multiple_params() {
|
|
||||||
let outputs = expect_outputs(r#"sine snd c4 e4 g4 arp note 0.5 0.7 0.9 arp gain ."#, 3);
|
|
||||||
let notes = get_notes(&outputs);
|
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
|
||||||
assert!(approx_eq(notes[1], 64.0));
|
|
||||||
assert!(approx_eq(notes[2], 67.0));
|
|
||||||
let gains = get_gains(&outputs);
|
|
||||||
assert!(approx_eq(gains[0], 0.5));
|
|
||||||
assert!(approx_eq(gains[1], 0.7));
|
|
||||||
assert!(approx_eq(gains[2], 0.9));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn arp_no_arp_unchanged() {
|
|
||||||
// Standard CycleList without arp → cross-product (backward compat)
|
|
||||||
let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 note ."#, 4);
|
let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 note ."#, 4);
|
||||||
let notes = get_notes(&outputs);
|
let notes = get_notes(&outputs);
|
||||||
// Cross-product: each note at each delta
|
// Each iteration emits both poly voices (c4 and e4)
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
assert!(approx_eq(notes[0], 60.0)); // iter 0, poly 0
|
||||||
assert!(approx_eq(notes[1], 60.0));
|
assert!(approx_eq(notes[1], 64.0)); // iter 0, poly 1
|
||||||
assert!(approx_eq(notes[2], 64.0));
|
assert!(approx_eq(notes[2], 60.0)); // iter 1, poly 0
|
||||||
assert!(approx_eq(notes[3], 64.0));
|
assert!(approx_eq(notes[3], 64.0)); // iter 1, poly 1
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn arp_mixed_cycle_and_arp() {
|
|
||||||
// CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
|
|
||||||
// Each arp step plays both sine and saw simultaneously (poly stacking)
|
|
||||||
let outputs = expect_outputs(r#"sine saw snd c4 e4 g4 arp note ."#, 6);
|
|
||||||
let sounds = get_sounds(&outputs);
|
|
||||||
// Arp step 0: poly 0=sine, poly 1=saw
|
|
||||||
assert_eq!(sounds[0], "sine");
|
|
||||||
assert_eq!(sounds[1], "saw");
|
|
||||||
// Arp step 1: poly 0=sine, poly 1=saw
|
|
||||||
assert_eq!(sounds[2], "sine");
|
|
||||||
assert_eq!(sounds[3], "saw");
|
|
||||||
// Arp step 2: poly 0=sine, poly 1=saw
|
|
||||||
assert_eq!(sounds[4], "sine");
|
|
||||||
assert_eq!(sounds[5], "saw");
|
|
||||||
let notes = get_notes(&outputs);
|
|
||||||
// Both poly voices in each arp step share the same note
|
|
||||||
assert!(approx_eq(notes[0], 60.0));
|
|
||||||
assert!(approx_eq(notes[1], 60.0));
|
|
||||||
assert!(approx_eq(notes[2], 64.0));
|
|
||||||
assert!(approx_eq(notes[3], 64.0));
|
|
||||||
assert!(approx_eq(notes[4], 67.0));
|
|
||||||
assert!(approx_eq(notes[5], 67.0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- every+ / except+ tests ---
|
// --- every+ / except+ tests ---
|
||||||
@@ -400,3 +329,102 @@ fn every_offset_zero_is_same_as_every() {
|
|||||||
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- at-loop feature tests ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_loop_choose_independent_per_subdivision() {
|
||||||
|
let f = forth();
|
||||||
|
let outputs = f.evaluate(r#"0 0.5 at sine snd [ 60 64 67 71 ] choose note ."#, &default_ctx()).unwrap();
|
||||||
|
assert_eq!(outputs.len(), 2);
|
||||||
|
// Both are valid notes from the set (just verify they're within range)
|
||||||
|
let notes = get_notes(&outputs);
|
||||||
|
for n in ¬es {
|
||||||
|
assert!([60.0, 64.0, 67.0, 71.0].contains(n), "unexpected note {n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_loop_multiple_blocks_independent() {
|
||||||
|
let outputs = expect_outputs(
|
||||||
|
r#"0 0.5 at "kick" snd . 0 0.25 0.5 at "hat" snd ."#,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
let sounds = get_sounds(&outputs);
|
||||||
|
assert_eq!(sounds[0], "kick");
|
||||||
|
assert_eq!(sounds[1], "kick");
|
||||||
|
assert_eq!(sounds[2], "hat");
|
||||||
|
assert_eq!(sounds[3], "hat");
|
||||||
|
assert_eq!(sounds[4], "hat");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_loop_single_delta_one_iteration() {
|
||||||
|
let outputs = expect_outputs(r#"0.25 at "kick" snd ."#, 1);
|
||||||
|
let sounds = get_sounds(&outputs);
|
||||||
|
assert_eq!(sounds[0], "kick");
|
||||||
|
let deltas = get_deltas(&outputs);
|
||||||
|
let step_dur = 0.125;
|
||||||
|
let sr: f64 = 48000.0;
|
||||||
|
assert!(approx_eq(deltas[0], (0.25 * step_dur * sr).round()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_without_closer_falls_back_to_legacy() {
|
||||||
|
// When at has no matching closer (., m., done), falls back to Op::At
|
||||||
|
let f = forth();
|
||||||
|
let result = f.evaluate(r#"0 0.5 at "kick" snd"#, &default_ctx());
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_loop_cycle_advances_across_runs() {
|
||||||
|
// Across different runs values, cycle inside at-loop picks correctly
|
||||||
|
for base_runs in 0..3 {
|
||||||
|
let ctx = ctx_with(|c| c.runs = base_runs);
|
||||||
|
let f = forth();
|
||||||
|
let outputs = f.evaluate(
|
||||||
|
r#"0 0.5 at sine snd [ c4 e4 g4 ] cycle note ."#,
|
||||||
|
&ctx,
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(outputs.len(), 2);
|
||||||
|
let notes = get_notes(&outputs);
|
||||||
|
// runs for iter i = base_runs * 2 + i
|
||||||
|
let expected_0 = [60.0, 64.0, 67.0][(base_runs * 2) % 3];
|
||||||
|
let expected_1 = [60.0, 64.0, 67.0][(base_runs * 2 + 1) % 3];
|
||||||
|
assert!(approx_eq(notes[0], expected_0), "runs={base_runs}: iter 0 expected {expected_0}, got {}", notes[0]);
|
||||||
|
assert!(approx_eq(notes[1], expected_1), "runs={base_runs}: iter 1 expected {expected_1}, got {}", notes[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_loop_midi_emit() {
|
||||||
|
let f = forth();
|
||||||
|
let outputs = f.evaluate("0 0.25 0.5 at 60 note m.", &default_ctx()).unwrap();
|
||||||
|
assert_eq!(outputs.len(), 3);
|
||||||
|
for o in &outputs {
|
||||||
|
assert!(o.contains("/midi/note/60/"));
|
||||||
|
}
|
||||||
|
// First should have no delta (or delta/0), others should have delta
|
||||||
|
assert!(outputs[1].contains("/delta/"));
|
||||||
|
assert!(outputs[2].contains("/delta/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_loop_done_no_emit() {
|
||||||
|
let f = forth();
|
||||||
|
let outputs = f.evaluate("0 0.5 at [ 1 2 ] cycle drop done", &default_ctx()).unwrap();
|
||||||
|
assert!(outputs.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_loop_done_sets_variables() {
|
||||||
|
let f = forth();
|
||||||
|
let outputs = f
|
||||||
|
.evaluate("0 0.5 at [ 10 20 ] cycle !x done kick snd @x freq .", &default_ctx())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(outputs.len(), 1);
|
||||||
|
// Last iteration wins: cycle(1) = 20
|
||||||
|
let params = parse_params(&outputs[0]);
|
||||||
|
assert!(approx_eq(*params.get("freq").unwrap(), 20.0));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user