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.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" {
|
||||
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
@@ -355,6 +362,37 @@ fn compile_if(
|
||||
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> {
|
||||
let mut depth = 1;
|
||||
let mut endcase_pos = None;
|
||||
|
||||
@@ -110,7 +110,8 @@ pub enum Op {
|
||||
ClearCmd,
|
||||
SetSpeed,
|
||||
At,
|
||||
Arp,
|
||||
AtLoop(Arc<[Op]>),
|
||||
|
||||
IntRange,
|
||||
StepRange,
|
||||
Generate,
|
||||
|
||||
@@ -96,7 +96,7 @@ pub enum Value {
|
||||
Str(Arc<str>, Option<SourceSpan>),
|
||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||
CycleList(Arc<[Value]>),
|
||||
ArpList(Arc<[Value]>),
|
||||
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
@@ -107,7 +107,7 @@ impl PartialEq for Value {
|
||||
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||
(Value::CycleList(a), Value::CycleList(b)) => a == b,
|
||||
(Value::ArpList(a), Value::ArpList(b)) => a == b,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ impl Value {
|
||||
Value::Float(f, _) => *f != 0.0,
|
||||
Value::Str(s, _) => !s.is_empty(),
|
||||
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::Str(s, _) => s.to_string(),
|
||||
Value::Quotation(..) => String::new(),
|
||||
Value::CycleList(_) | Value::ArpList(_) => String::new(),
|
||||
Value::CycleList(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn span(&self) -> Option<SourceSpan> {
|
||||
match self {
|
||||
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)>,
|
||||
deltas: Vec<Value>,
|
||||
global_params: Vec<(&'static str, Value)>,
|
||||
delta_secs: Option<f64>,
|
||||
}
|
||||
|
||||
impl CmdRegister {
|
||||
@@ -180,6 +181,7 @@ impl CmdRegister {
|
||||
params: Vec::with_capacity(16),
|
||||
deltas: Vec::with_capacity(4),
|
||||
global_params: Vec::new(),
|
||||
delta_secs: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,9 +239,26 @@ impl CmdRegister {
|
||||
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) {
|
||||
self.sound = None;
|
||||
self.params.clear();
|
||||
self.deltas.clear();
|
||||
self.delta_secs = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,31 +241,7 @@ impl Forth {
|
||||
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,
|
||||
arp_idx: usize,
|
||||
poly_idx: usize,
|
||||
delta_secs: f64,
|
||||
outputs: &mut Vec<String>|
|
||||
@@ -277,7 +253,7 @@ impl Forth {
|
||||
return Err("nothing to emit".into());
|
||||
}
|
||||
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 {
|
||||
Some(v) => Some(v.as_str()?.to_string()),
|
||||
None => None,
|
||||
@@ -286,8 +262,8 @@ impl Forth {
|
||||
.iter()
|
||||
.chain(cmd.params().iter())
|
||||
.map(|(k, v)| {
|
||||
let resolved = resolve_value(v, arp_idx, poly_idx);
|
||||
if let Value::CycleList(_) | Value::ArpList(_) = v {
|
||||
let resolved = resolve_value(v, poly_idx);
|
||||
if let Value::CycleList(_) = v {
|
||||
if let Some(span) = resolved.span() {
|
||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||
trace.selected_spans.push(span);
|
||||
@@ -595,47 +571,17 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Emit => {
|
||||
if has_arp_list(cmd) {
|
||||
let arp_count = compute_arp_count(cmd);
|
||||
if let Some(dsecs) = cmd.take_delta_secs() {
|
||||
let poly_count = compute_poly_count(cmd);
|
||||
let explicit_deltas = !cmd.deltas().is_empty();
|
||||
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() {
|
||||
for poly_idx in 0..poly_count {
|
||||
if let Some(sound_val) =
|
||||
emit_with_cycling(cmd, poly_idx, dsecs, outputs)?
|
||||
{
|
||||
if let Some(span) = sound_val.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) =
|
||||
emit_with_cycling(cmd, i, poly_i, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -657,7 +603,7 @@ impl Forth {
|
||||
}
|
||||
}
|
||||
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(trace) =
|
||||
@@ -1196,12 +1142,60 @@ impl Forth {
|
||||
cmd.set_deltas(deltas);
|
||||
}
|
||||
|
||||
Op::Arp => {
|
||||
Op::AtLoop(body_ops) => {
|
||||
ensure(stack, 1)?;
|
||||
let values = std::mem::take(stack);
|
||||
stack.push(Value::ArpList(Arc::from(values)));
|
||||
let deltas = std::mem::take(stack);
|
||||
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 => {
|
||||
let r = pop(stack)?;
|
||||
let s = pop(stack)?;
|
||||
@@ -1499,35 +1493,13 @@ impl Forth {
|
||||
|
||||
// MIDI operations
|
||||
Op::MidiEmit => {
|
||||
let at_loop_delta = cmd.take_delta_secs();
|
||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||
|
||||
// Build schedule: (arp_idx, poly_idx, delta_secs)
|
||||
let schedule: Vec<(usize, usize, f64)> = if has_arp_list(cmd) {
|
||||
let arp_count = compute_arp_count(cmd);
|
||||
// Build schedule: (poly_idx, delta_secs)
|
||||
let schedule: Vec<(usize, f64)> = if let Some(dsecs) = at_loop_delta {
|
||||
let poly_count = compute_poly_count(cmd);
|
||||
let explicit = !cmd.deltas().is_empty();
|
||||
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
|
||||
(0..poly_count).map(|pi| (pi, dsecs)).collect()
|
||||
} else {
|
||||
let poly_count = compute_poly_count(cmd);
|
||||
let deltas: Vec<f64> = if cmd.deltas().is_empty() {
|
||||
@@ -1542,7 +1514,6 @@ impl Forth {
|
||||
for poly_idx in 0..poly_count {
|
||||
for &frac in &deltas {
|
||||
sched.push((
|
||||
0,
|
||||
poly_idx,
|
||||
ctx.nudge_secs + frac * ctx.step_duration(),
|
||||
));
|
||||
@@ -1551,14 +1522,14 @@ impl Forth {
|
||||
sched
|
||||
};
|
||||
|
||||
for (arp_idx, poly_idx, delta_secs) in schedule {
|
||||
for (poly_idx, delta_secs) in schedule {
|
||||
let get_int = |name: &str| -> Option<i64> {
|
||||
params
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(k, _)| *k == name)
|
||||
.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> {
|
||||
@@ -1567,7 +1538,7 @@ impl Forth {
|
||||
.rev()
|
||||
.find(|(k, _)| *k == name)
|
||||
.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")
|
||||
@@ -1960,10 +1931,6 @@ where
|
||||
F: Fn(f64) -> f64 + Copy,
|
||||
{
|
||||
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) => {
|
||||
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
|
||||
Ok(Value::CycleList(Arc::from(mapped?)))
|
||||
@@ -1977,11 +1944,6 @@ where
|
||||
F: Fn(i64) -> i64 + Copy,
|
||||
{
|
||||
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) => {
|
||||
let mapped: Result<Vec<_>, _> =
|
||||
items.iter().map(|x| lift_unary_int(x, f)).collect();
|
||||
@@ -1996,16 +1958,6 @@ where
|
||||
F: Fn(f64, f64) -> f64 + Copy,
|
||||
{
|
||||
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) => {
|
||||
let mapped: Result<Vec<_>, _> =
|
||||
items.iter().map(|x| lift_binary(x, b, f)).collect();
|
||||
@@ -2045,11 +1997,8 @@ where
|
||||
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 {
|
||||
Value::ArpList(items) if !items.is_empty() => {
|
||||
Cow::Owned(items[arp_idx % items.len()].clone())
|
||||
}
|
||||
Value::CycleList(items) if !items.is_empty() => {
|
||||
Cow::Owned(items[poly_idx % items.len()].clone())
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"tempo!" => Op::SetTempo,
|
||||
"speed!" => Op::SetSpeed,
|
||||
"at" => Op::At,
|
||||
"arp" => Op::Arp,
|
||||
|
||||
"adsr" => Op::Adsr,
|
||||
"ad" => Op::Ad,
|
||||
"apply" => Op::Apply,
|
||||
|
||||
@@ -309,9 +309,9 @@ pub(super) const WORDS: &[Word] = &[
|
||||
name: "at",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(v1..vn --)",
|
||||
desc: "Set delta context for emit timing",
|
||||
example: "0 0.5 at kick s . => emits at 0 and 0.5 of step",
|
||||
stack: "(v1..vn -- )",
|
||||
desc: "Looping block: re-executes body per delta. Close with . (audio), m. (MIDI), or done (no emit)",
|
||||
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,
|
||||
varargs: true,
|
||||
},
|
||||
|
||||
@@ -24,16 +24,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
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 {
|
||||
name: "clear",
|
||||
aliases: &[],
|
||||
|
||||
Reference in New Issue
Block a user