Compare commits
5 Commits
d106711708
...
4d22bd5d2b
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d22bd5d2b | |||
| 495bfb3bdc | |||
| 73db616139 | |||
| 8efafffaff | |||
| 48f5920fed |
@@ -1,6 +1,6 @@
|
||||
use super::ops::Op;
|
||||
use super::types::{Dictionary, SourceSpan};
|
||||
use super::words::{compile_word, simple_op};
|
||||
use super::words::compile_word;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Token {
|
||||
@@ -8,8 +8,6 @@ enum Token {
|
||||
Float(f64, SourceSpan),
|
||||
Str(String, SourceSpan),
|
||||
Word(String, SourceSpan),
|
||||
QuoteStart(usize),
|
||||
QuoteEnd(usize),
|
||||
}
|
||||
|
||||
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
@@ -27,6 +25,11 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '(' || c == ')' {
|
||||
chars.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '"' {
|
||||
let start = pos;
|
||||
chars.next();
|
||||
@@ -44,25 +47,21 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '(' {
|
||||
while let Some(&(_, ch)) = chars.peek() {
|
||||
chars.next();
|
||||
if ch == ')' {
|
||||
break;
|
||||
if c == ';' {
|
||||
chars.next(); // consume first ;
|
||||
if let Some(&(_, ';')) = chars.peek() {
|
||||
// ;; starts a comment to end of line
|
||||
chars.next(); // consume second ;
|
||||
while let Some(&(_, ch)) = chars.peek() {
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
chars.next();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '{' {
|
||||
chars.next();
|
||||
tokens.push(Token::QuoteStart(pos));
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '}' {
|
||||
chars.next();
|
||||
tokens.push(Token::QuoteEnd(pos));
|
||||
// single ; is a word, create token
|
||||
tokens.push(Token::Word(";".to_string(), SourceSpan { start: pos, end: pos + 1 }));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
let mut word = String::new();
|
||||
let mut end = start;
|
||||
while let Some(&(i, ch)) = chars.peek() {
|
||||
if ch.is_whitespace() || ch == '{' || ch == '}' {
|
||||
if ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
end = i + ch.len_utf8();
|
||||
@@ -94,25 +93,22 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
let mut ops = Vec::new();
|
||||
let mut i = 0;
|
||||
let mut list_depth: usize = 0;
|
||||
|
||||
while i < tokens.len() {
|
||||
match &tokens[i] {
|
||||
Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))),
|
||||
Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))),
|
||||
Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))),
|
||||
Token::QuoteStart(start_pos) => {
|
||||
let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 };
|
||||
ops.push(Op::Quotation(quote_ops, Some(body_span)));
|
||||
}
|
||||
Token::QuoteEnd(_) => {
|
||||
return Err("unexpected }".into());
|
||||
}
|
||||
Token::Word(w, span) => {
|
||||
let word = w.as_str();
|
||||
if word == ":" {
|
||||
if word == "{" {
|
||||
let (quote_ops, consumed, end_span) = compile_quotation(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
let body_span = SourceSpan { start: span.start, end: end_span.end };
|
||||
ops.push(Op::Quotation(quote_ops, Some(body_span)));
|
||||
} else if word == "}" {
|
||||
return Err("unexpected }".into());
|
||||
} else if word == ":" {
|
||||
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
dict.lock().unwrap().insert(name, body);
|
||||
@@ -130,20 +126,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
ops.push(Op::Branch(else_ops.len()));
|
||||
ops.extend(else_ops);
|
||||
}
|
||||
} else if is_list_start(word) {
|
||||
ops.push(Op::ListStart);
|
||||
list_depth += 1;
|
||||
} else if is_list_end(word) {
|
||||
list_depth = list_depth.saturating_sub(1);
|
||||
if let Some(op) = simple_op(word) {
|
||||
ops.push(op);
|
||||
}
|
||||
} else if list_depth > 0 {
|
||||
let mut word_ops = Vec::new();
|
||||
if !compile_word(word, Some(*span), &mut word_ops, dict) {
|
||||
return Err(format!("unknown word: {word}"));
|
||||
}
|
||||
ops.push(Op::Quotation(word_ops, Some(*span)));
|
||||
} else if !compile_word(word, Some(*span), &mut ops, dict) {
|
||||
return Err(format!("unknown word: {word}"));
|
||||
}
|
||||
@@ -155,46 +137,38 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn is_list_start(word: &str) -> bool {
|
||||
matches!(word, "[" | "<" | "<<")
|
||||
}
|
||||
|
||||
fn is_list_end(word: &str) -> bool {
|
||||
matches!(word, "]" | ">" | ">>")
|
||||
}
|
||||
|
||||
fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize, usize), String> {
|
||||
fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize, SourceSpan), String> {
|
||||
let mut depth = 1;
|
||||
let mut end_idx = None;
|
||||
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
match tok {
|
||||
Token::QuoteStart(_) => depth += 1,
|
||||
Token::QuoteEnd(_) => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end_idx = Some(i);
|
||||
break;
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"{" => depth += 1,
|
||||
"}" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end_idx = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let end_idx = end_idx.ok_or("missing }")?;
|
||||
let byte_pos = match &tokens[end_idx] {
|
||||
Token::QuoteEnd(pos) => *pos,
|
||||
let end_span = match &tokens[end_idx] {
|
||||
Token::Word(_, span) => *span,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let quote_ops = compile(&tokens[..end_idx], dict)?;
|
||||
Ok((quote_ops, end_idx + 1, byte_pos))
|
||||
Ok((quote_ops, end_idx + 1, end_span))
|
||||
}
|
||||
|
||||
fn token_span(tok: &Token) -> Option<SourceSpan> {
|
||||
match tok {
|
||||
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),
|
||||
Token::QuoteStart(p) => Some(SourceSpan { start: *p, end: *p + 1 }),
|
||||
Token::QuoteEnd(p) => Some(SourceSpan { start: *p, end: *p + 1 }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +192,6 @@ fn compile_colon_def(tokens: &[Token], dict: &Dictionary) -> Result<(usize, Stri
|
||||
let semi_pos = semi_pos.ok_or("missing ';' in word definition")?;
|
||||
let body_tokens = &tokens[1..semi_pos];
|
||||
let body_ops = compile(body_tokens, dict)?;
|
||||
// consumed = name + body + semicolon
|
||||
Ok((semi_pos + 1, name, body_ops))
|
||||
}
|
||||
|
||||
|
||||
@@ -49,25 +49,20 @@ pub enum Op {
|
||||
NewCmd,
|
||||
SetParam(String),
|
||||
Emit,
|
||||
Silence,
|
||||
Get,
|
||||
Set,
|
||||
GetContext(String),
|
||||
Rand,
|
||||
Seed,
|
||||
Cycle,
|
||||
PCycle,
|
||||
TCycle,
|
||||
Choose,
|
||||
ChanceExec,
|
||||
ProbExec,
|
||||
Coin,
|
||||
Mtof,
|
||||
Ftom,
|
||||
ListStart,
|
||||
ListEnd,
|
||||
ListEndCycle,
|
||||
PCycle,
|
||||
ListEndPCycle,
|
||||
Scale,
|
||||
SetTempo,
|
||||
Every,
|
||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||
@@ -84,10 +79,11 @@ pub enum Op {
|
||||
Loop,
|
||||
Degree(&'static [i64]),
|
||||
Oct,
|
||||
DivStart,
|
||||
DivEnd,
|
||||
StackStart,
|
||||
EmitN,
|
||||
ClearCmd,
|
||||
SetSpeed,
|
||||
At,
|
||||
IntRange,
|
||||
Generate,
|
||||
GeomRange,
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ pub enum Value {
|
||||
Int(i64, Option<SourceSpan>),
|
||||
Float(f64, Option<SourceSpan>),
|
||||
Str(String, Option<SourceSpan>),
|
||||
Marker,
|
||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||
CycleList(Vec<Value>),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
@@ -57,8 +57,8 @@ impl PartialEq for Value {
|
||||
(Value::Int(a, _), Value::Int(b, _)) => a == b,
|
||||
(Value::Float(a, _), Value::Float(b, _)) => a == b,
|
||||
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||
(Value::Marker, Value::Marker) => true,
|
||||
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||
(Value::CycleList(a), Value::CycleList(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -93,29 +93,25 @@ impl Value {
|
||||
Value::Int(i, _) => *i != 0,
|
||||
Value::Float(f, _) => *f != 0.0,
|
||||
Value::Str(s, _) => !s.is_empty(),
|
||||
Value::Marker => false,
|
||||
Value::Quotation(..) => true,
|
||||
Value::CycleList(items) => !items.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_marker(&self) -> bool {
|
||||
matches!(self, Value::Marker)
|
||||
}
|
||||
|
||||
pub(super) fn to_param_string(&self) -> String {
|
||||
match self {
|
||||
Value::Int(i, _) => i.to_string(),
|
||||
Value::Float(f, _) => f.to_string(),
|
||||
Value::Str(s, _) => s.clone(),
|
||||
Value::Marker => String::new(),
|
||||
Value::Quotation(..) => 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::Marker => None,
|
||||
Value::CycleList(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +120,7 @@ impl Value {
|
||||
pub(super) struct CmdRegister {
|
||||
sound: Option<Value>,
|
||||
params: Vec<(String, Value)>,
|
||||
deltas: Vec<Value>,
|
||||
}
|
||||
|
||||
impl CmdRegister {
|
||||
@@ -135,10 +132,16 @@ impl CmdRegister {
|
||||
self.params.push((key, val));
|
||||
}
|
||||
|
||||
pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> {
|
||||
self.sound
|
||||
.as_ref()
|
||||
.map(|s| (s.clone(), self.params.clone()))
|
||||
pub(super) fn set_deltas(&mut self, deltas: Vec<Value>) {
|
||||
self.deltas = deltas;
|
||||
}
|
||||
|
||||
pub(super) fn deltas(&self) -> &[Value] {
|
||||
&self.deltas
|
||||
}
|
||||
|
||||
pub(super) fn snapshot(&self) -> Option<(&Value, &[(String, Value)])> {
|
||||
self.sound.as_ref().map(|s| (s, self.params.as_slice()))
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
@@ -147,54 +150,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng as RngTrait, SeedableRng};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::compiler::compile_script;
|
||||
use super::ops::Op;
|
||||
use super::types::{
|
||||
CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext,
|
||||
Stack, StepContext, Value, Variables,
|
||||
CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
|
||||
};
|
||||
|
||||
pub struct Forth {
|
||||
@@ -70,24 +70,9 @@ impl Forth {
|
||||
) -> Result<Vec<String>, String> {
|
||||
let mut stack = self.stack.lock().unwrap();
|
||||
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();
|
||||
|
||||
self.execute_ops(
|
||||
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);
|
||||
}
|
||||
self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
|
||||
|
||||
Ok(outputs)
|
||||
}
|
||||
@@ -100,70 +85,56 @@ impl Forth {
|
||||
ctx: &StepContext,
|
||||
stack: &mut Vec<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
cmd: &mut CmdRegister,
|
||||
trace: Option<&mut ExecutionTrace>,
|
||||
) -> Result<(), String> {
|
||||
let mut pc = 0;
|
||||
let trace_cell = std::cell::RefCell::new(trace);
|
||||
|
||||
// Executes a quotation value, handling trace recording and recursive dispatch.
|
||||
let run_quotation = |quot: Value,
|
||||
stack: &mut Vec<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
cmd: &mut CmdRegister|
|
||||
-> Result<(), String> {
|
||||
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 run_quotation =
|
||||
|quot: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, cmd: &mut CmdRegister| -> Result<(), String> {
|
||||
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();
|
||||
self.execute_ops(
|
||||
"_ops,
|
||||
ctx,
|
||||
stack,
|
||||
outputs,
|
||||
scope_stack,
|
||||
cmd,
|
||||
trace_opt.as_deref_mut(),
|
||||
)?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
_ => Err("expected quotation".into()),
|
||||
}
|
||||
};
|
||||
|
||||
let select_and_run =
|
||||
|selected: Value, stack: &mut Vec<Value>, outputs: &mut Vec<String>, 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, cmd)
|
||||
} else {
|
||||
stack.push(selected);
|
||||
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,
|
||||
idx: usize,
|
||||
stack: &mut Vec<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
cmd: &mut CmdRegister|
|
||||
-> Result<(), String> {
|
||||
if stack.len() < count {
|
||||
@@ -172,51 +143,27 @@ impl Forth {
|
||||
let start = stack.len() - count;
|
||||
let values: Vec<Value> = stack.drain(start..).collect();
|
||||
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,
|
||||
err_msg: &str,
|
||||
stack: &mut Vec<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
cmd: &mut CmdRegister|
|
||||
-> Result<(), String> {
|
||||
let mut values = Vec::new();
|
||||
while let Some(v) = stack.pop() {
|
||||
if v.is_marker() {
|
||||
break;
|
||||
}
|
||||
values.push(v);
|
||||
}
|
||||
if values.is_empty() {
|
||||
return Err(err_msg.into());
|
||||
}
|
||||
values.reverse();
|
||||
let idx = idx_source % values.len();
|
||||
let selected = values[idx].clone();
|
||||
select_and_run(selected, stack, outputs, scope_stack, cmd)
|
||||
};
|
||||
|
||||
// Emits one sound event from the current command register into the current scope.
|
||||
let emit_once = |cmd: &CmdRegister,
|
||||
scope_stack: &mut Vec<ScopeContext>|
|
||||
-> Result<Option<Value>, String> {
|
||||
let emit_with_cycling = |cmd: &CmdRegister, emit_idx: usize, delta_secs: f64, outputs: &mut Vec<String>| -> Result<Option<Value>, String> {
|
||||
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
|
||||
let sound = sound_val.as_str()?.to_string();
|
||||
let resolved_params: Vec<(String, String)> = params
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.to_param_string()))
|
||||
.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))
|
||||
let resolved_sound_val = resolve_cycling(sound_val, emit_idx);
|
||||
let sound = resolved_sound_val.as_str()?.to_string();
|
||||
let resolved_params: Vec<(String, String)> =
|
||||
params.iter().map(|(k, v)| {
|
||||
let resolved = resolve_cycling(v, emit_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
(k.clone(), resolved.to_param_string())
|
||||
}).collect();
|
||||
emit_output(&sound, &resolved_params, ctx.step_duration(), delta_secs, outputs);
|
||||
Ok(Some(resolved_sound_val.into_owned()))
|
||||
};
|
||||
|
||||
while pc < ops.len() {
|
||||
@@ -404,26 +351,31 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Emit => {
|
||||
if let Some(sound_val) = emit_once(cmd, scope_stack)? {
|
||||
if let Some(span) = sound_val.span() {
|
||||
let deltas = if cmd.deltas().is_empty() {
|
||||
vec![Value::Float(0.0, None)]
|
||||
} else {
|
||||
cmd.deltas().to_vec()
|
||||
};
|
||||
|
||||
for (emit_idx, delta_val) in deltas.iter().enumerate() {
|
||||
let delta_frac = delta_val.as_float()?;
|
||||
let delta_secs = ctx.nudge_secs + delta_frac * ctx.step_duration();
|
||||
// Record delta span for highlighting
|
||||
if let Some(span) = delta_val.span() {
|
||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||
trace.selected_spans.push(span);
|
||||
}
|
||||
}
|
||||
if let Some(sound_val) = emit_with_cycling(cmd, emit_idx, delta_secs, outputs)? {
|
||||
if let Some(span) = sound_val.span() {
|
||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||
trace.selected_spans.push(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 => {
|
||||
let name = stack.pop().ok_or("stack underflow")?;
|
||||
let name = name.as_str()?;
|
||||
@@ -489,7 +441,20 @@ impl Forth {
|
||||
Op::Cycle => ctx.runs,
|
||||
_ => ctx.iter,
|
||||
} % count;
|
||||
drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?;
|
||||
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::TCycle => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count == 0 {
|
||||
return Err("tcycle count must be > 0".into());
|
||||
}
|
||||
if stack.len() < count {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
let values: Vec<Value> = stack.drain(start..).collect();
|
||||
stack.push(Value::CycleList(values));
|
||||
}
|
||||
|
||||
Op::Choose => {
|
||||
@@ -498,7 +463,7 @@ impl Forth {
|
||||
return Err("choose count must be > 0".into());
|
||||
}
|
||||
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 => {
|
||||
@@ -510,7 +475,7 @@ impl Forth {
|
||||
_ => threshold / 100.0,
|
||||
};
|
||||
if val < limit {
|
||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +505,7 @@ impl Forth {
|
||||
_ => !cond.is_truthy(),
|
||||
};
|
||||
if should_run {
|
||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,13 +518,13 @@ impl Forth {
|
||||
} else {
|
||||
false_quot
|
||||
};
|
||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::Pick => {
|
||||
let idx_i = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
if idx_i < 0 {
|
||||
return Err(format!("pick index must be >= 0, got {}", idx_i));
|
||||
return Err(format!("pick index must be >= 0, got {idx_i}"));
|
||||
}
|
||||
let idx = idx_i as usize;
|
||||
let mut quots: Vec<Value> = Vec::new();
|
||||
@@ -580,7 +545,7 @@ impl Forth {
|
||||
quots.len()
|
||||
));
|
||||
}
|
||||
run_quotation(quots.swap_remove(idx), stack, outputs, scope_stack, cmd)?;
|
||||
run_quotation(quots.swap_remove(idx), stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::Mtof => {
|
||||
@@ -660,37 +625,29 @@ impl Forth {
|
||||
cmd.set_param("dur".into(), Value::Float(dur, None));
|
||||
}
|
||||
|
||||
Op::ListStart => {
|
||||
stack.push(Value::Marker);
|
||||
}
|
||||
|
||||
Op::ListEnd => {
|
||||
let mut count = 0;
|
||||
let mut values = Vec::new();
|
||||
while let Some(v) = stack.pop() {
|
||||
if v.is_marker() {
|
||||
break;
|
||||
Op::At => {
|
||||
let top = stack.pop().ok_or("stack underflow")?;
|
||||
let deltas = match &top {
|
||||
Value::Float(..) => vec![top],
|
||||
Value::Int(n, _) => {
|
||||
let count = *n as usize;
|
||||
if stack.len() < count {
|
||||
return Err(format!(
|
||||
"at: stack underflow, expected {} values but got {}",
|
||||
count,
|
||||
stack.len()
|
||||
));
|
||||
}
|
||||
let mut vals = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
vals.push(stack.pop().ok_or("stack underflow")?);
|
||||
}
|
||||
vals.reverse();
|
||||
vals
|
||||
}
|
||||
values.push(v);
|
||||
count += 1;
|
||||
}
|
||||
values.reverse();
|
||||
for v in values {
|
||||
stack.push(v);
|
||||
}
|
||||
stack.push(Value::Int(count, None));
|
||||
}
|
||||
|
||||
Op::ListEndCycle | Op::ListEndPCycle => {
|
||||
let idx_source = match &ops[pc] {
|
||||
Op::ListEndCycle => ctx.runs,
|
||||
_ => ctx.iter,
|
||||
_ => return Err("at expects float or int count".into()),
|
||||
};
|
||||
let err_msg = match &ops[pc] {
|
||||
Op::ListEndCycle => "empty cycle list",
|
||||
_ => "empty pattern cycle list",
|
||||
};
|
||||
drain_list_select_run(idx_source, err_msg, stack, outputs, scope_stack, cmd)?;
|
||||
cmd.set_deltas(deltas);
|
||||
}
|
||||
|
||||
Op::Adsr => {
|
||||
@@ -714,7 +671,7 @@ impl Forth {
|
||||
|
||||
Op::Apply => {
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
run_quotation(quot, stack, outputs, scope_stack, cmd)?;
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::Ramp => {
|
||||
@@ -744,36 +701,6 @@ impl Forth {
|
||||
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 => {
|
||||
cmd.clear();
|
||||
}
|
||||
@@ -783,8 +710,52 @@ impl Forth {
|
||||
if n < 0 {
|
||||
return Err("emit count must be >= 0".into());
|
||||
}
|
||||
for _ in 0..n {
|
||||
emit_once(cmd, scope_stack)?;
|
||||
for i in 0..n as usize {
|
||||
emit_with_cycling(cmd, i, ctx.nudge_secs, outputs)?;
|
||||
}
|
||||
}
|
||||
|
||||
Op::IntRange => {
|
||||
let end = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let start = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
if start <= end {
|
||||
for i in start..=end {
|
||||
stack.push(Value::Int(i, None));
|
||||
}
|
||||
} else {
|
||||
for i in (end..=start).rev() {
|
||||
stack.push(Value::Int(i, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Op::Generate => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
if count < 0 {
|
||||
return Err("gen count must be >= 0".into());
|
||||
}
|
||||
let mut results = Vec::with_capacity(count as usize);
|
||||
for _ in 0..count {
|
||||
run_quotation(quot.clone(), stack, outputs, cmd)?;
|
||||
results.push(stack.pop().ok_or("gen: quotation must produce a value")?);
|
||||
}
|
||||
for val in results {
|
||||
stack.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
Op::GeomRange => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let ratio = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let start = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
if count < 0 {
|
||||
return Err("geom.. count must be >= 0".into());
|
||||
}
|
||||
let mut val = start;
|
||||
for _ in 0..count {
|
||||
stack.push(float_to_value(val));
|
||||
val *= ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,158 +766,58 @@ 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 is_tempo_scaled_param(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"attack"
|
||||
| "decay"
|
||||
| "release"
|
||||
| "lpa"
|
||||
| "lpd"
|
||||
| "lpr"
|
||||
| "hpa"
|
||||
| "hpd"
|
||||
| "hpr"
|
||||
| "bpa"
|
||||
| "bpd"
|
||||
| "bpr"
|
||||
| "patt"
|
||||
| "pdec"
|
||||
| "prel"
|
||||
| "fma"
|
||||
| "fmd"
|
||||
| "fmr"
|
||||
| "glide"
|
||||
| "verbdecay"
|
||||
| "verbpredelay"
|
||||
| "chorusdelay"
|
||||
| "duration"
|
||||
)
|
||||
}
|
||||
|
||||
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] = &[
|
||||
"attack",
|
||||
"decay",
|
||||
"release",
|
||||
"lpa",
|
||||
"lpd",
|
||||
"lpr",
|
||||
"hpa",
|
||||
"hpd",
|
||||
"hpr",
|
||||
"bpa",
|
||||
"bpd",
|
||||
"bpr",
|
||||
"patt",
|
||||
"pdec",
|
||||
"prel",
|
||||
"fma",
|
||||
"fmd",
|
||||
"fmr",
|
||||
"glide",
|
||||
"verbdecay",
|
||||
"verbpredelay",
|
||||
"chorusdelay",
|
||||
"duration",
|
||||
];
|
||||
|
||||
fn emit_output(
|
||||
sound: &str,
|
||||
params: &[(String, String)],
|
||||
delta: f64,
|
||||
dur: f64,
|
||||
step_duration: f64,
|
||||
nudge_secs: f64,
|
||||
outputs: &mut Vec<String>,
|
||||
) {
|
||||
let nudged_delta = delta + nudge_secs;
|
||||
let mut pairs = vec![("sound".into(), sound.to_string())];
|
||||
pairs.extend(params.iter().cloned());
|
||||
if nudged_delta > 0.0 {
|
||||
pairs.push(("delta".into(), nudged_delta.to_string()));
|
||||
if nudge_secs > 0.0 {
|
||||
pairs.push(("delta".into(), nudge_secs.to_string()));
|
||||
}
|
||||
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") {
|
||||
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 {
|
||||
pairs.push(("delaytime".into(), dur.to_string()));
|
||||
pairs.push(("delaytime".into(), step_duration.to_string()));
|
||||
}
|
||||
for pair in &mut pairs {
|
||||
if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) {
|
||||
if is_tempo_scaled_param(&pair.0) {
|
||||
if let Ok(val) = pair.1.parse::<f64>() {
|
||||
pair.1 = (val * step_duration).to_string();
|
||||
}
|
||||
@@ -1033,3 +904,12 @@ fn format_cmd(pairs: &[(String, String)]) -> String {
|
||||
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
||||
format!("/{}", parts.join("/"))
|
||||
}
|
||||
|
||||
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
|
||||
match val {
|
||||
Value::CycleList(items) if !items.is_empty() => {
|
||||
Cow::Owned(items[emit_idx % items.len()].clone())
|
||||
}
|
||||
other => Cow::Borrowed(other),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,11 @@ use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum Orientation {
|
||||
@@ -58,46 +63,51 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
|
||||
let mut patterns = vec![0u8; width * height];
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
patterns.clear();
|
||||
patterns.resize(size, 0);
|
||||
|
||||
for fine_x in 0..fine_width {
|
||||
let sample_idx = (fine_x * data.len()) / fine_width;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
for fine_x in 0..fine_width {
|
||||
let sample_idx = (fine_x * data.len()) / fine_width;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fine_y = fine_y.min(fine_height - 1);
|
||||
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fine_y = fine_y.min(fine_height - 1);
|
||||
|
||||
let char_x = fine_x / 2;
|
||||
let char_y = fine_y / 4;
|
||||
let dot_x = fine_x % 2;
|
||||
let dot_y = fine_y % 4;
|
||||
let char_x = fine_x / 2;
|
||||
let char_y = fine_y / 4;
|
||||
let dot_x = fine_x % 2;
|
||||
let dot_y = fine_y % 4;
|
||||
|
||||
let bit = match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let bit = match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
patterns[char_y * width + char_x] |= bit;
|
||||
}
|
||||
patterns[char_y * width + char_x] |= bit;
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||
@@ -106,44 +116,49 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
|
||||
let mut patterns = vec![0u8; width * height];
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
patterns.clear();
|
||||
patterns.resize(size, 0);
|
||||
|
||||
for fine_y in 0..fine_height {
|
||||
let sample_idx = (fine_y * data.len()) / fine_height;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
for fine_y in 0..fine_height {
|
||||
let sample_idx = (fine_y * data.len()) / fine_height;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fine_x = fine_x.min(fine_width - 1);
|
||||
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fine_x = fine_x.min(fine_width - 1);
|
||||
|
||||
let char_x = fine_x / 2;
|
||||
let char_y = fine_y / 4;
|
||||
let dot_x = fine_x % 2;
|
||||
let dot_y = fine_y % 4;
|
||||
let char_x = fine_x / 2;
|
||||
let char_y = fine_y / 4;
|
||||
let dot_x = fine_x % 2;
|
||||
let dot_y = fine_y % 4;
|
||||
|
||||
let bit = match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let bit = match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
patterns[char_y * width + char_x] |= bit;
|
||||
}
|
||||
patterns[char_y * width + char_x] |= bit;
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ struct SpectrumAnalyzer {
|
||||
fft: Arc<dyn rustfft::Fft<f32>>,
|
||||
window: [f32; FFT_SIZE],
|
||||
scratch: Vec<Complex<f32>>,
|
||||
fft_buf: Vec<Complex<f32>>,
|
||||
band_edges: [usize; NUM_BANDS + 1],
|
||||
}
|
||||
|
||||
@@ -114,6 +115,7 @@ impl SpectrumAnalyzer {
|
||||
fft,
|
||||
window,
|
||||
scratch: vec![Complex::default(); scratch_len],
|
||||
fft_buf: vec![Complex::default(); FFT_SIZE],
|
||||
band_edges,
|
||||
}
|
||||
}
|
||||
@@ -130,20 +132,19 @@ impl SpectrumAnalyzer {
|
||||
}
|
||||
|
||||
fn run_fft(&mut self, output: &SpectrumBuffer) {
|
||||
let mut buf: Vec<Complex<f32>> = (0..FFT_SIZE)
|
||||
.map(|i| {
|
||||
let idx = (self.pos + i) % FFT_SIZE;
|
||||
Complex::new(self.ring[idx] * self.window[i], 0.0)
|
||||
})
|
||||
.collect();
|
||||
for i in 0..FFT_SIZE {
|
||||
let idx = (self.pos + i) % FFT_SIZE;
|
||||
self.fft_buf[i] = Complex::new(self.ring[idx] * self.window[i], 0.0);
|
||||
}
|
||||
|
||||
self.fft.process_with_scratch(&mut buf, &mut self.scratch);
|
||||
self.fft
|
||||
.process_with_scratch(&mut self.fft_buf, &mut self.scratch);
|
||||
|
||||
let mut bands = [0.0f32; NUM_BANDS];
|
||||
for (band, mag) in bands.iter_mut().enumerate() {
|
||||
let lo = self.band_edges[band];
|
||||
let hi = self.band_edges[band + 1].max(lo + 1);
|
||||
let sum: f32 = buf[lo..hi].iter().map(|c| c.norm()).sum();
|
||||
let sum: f32 = self.fft_buf[lo..hi].iter().map(|c| c.norm()).sum();
|
||||
let avg = sum / (hi - lo) as f32;
|
||||
let amplitude = avg / (FFT_SIZE as f32 / 2.0);
|
||||
let db = 20.0 * amplitude.max(1e-10).log10();
|
||||
|
||||
@@ -93,17 +93,19 @@ pub struct ActivePatternState {
|
||||
pub iter: usize,
|
||||
}
|
||||
|
||||
pub type StepTracesMap = HashMap<(usize, usize, usize), ExecutionTrace>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SharedSequencerState {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>,
|
||||
pub step_traces: Arc<StepTracesMap>,
|
||||
pub event_count: usize,
|
||||
pub dropped_events: usize,
|
||||
}
|
||||
|
||||
pub struct SequencerSnapshot {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
pub step_traces: HashMap<(usize, usize, usize), ExecutionTrace>,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
pub event_count: usize,
|
||||
pub dropped_events: usize,
|
||||
}
|
||||
@@ -146,7 +148,7 @@ impl SequencerHandle {
|
||||
let state = self.shared_state.load();
|
||||
SequencerSnapshot {
|
||||
active_patterns: state.active_patterns.clone(),
|
||||
step_traces: state.step_traces.clone(),
|
||||
step_traces: Arc::clone(&state.step_traces),
|
||||
event_count: state.event_count,
|
||||
dropped_events: state.dropped_events,
|
||||
}
|
||||
@@ -197,6 +199,7 @@ impl AudioState {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_sequencer(
|
||||
link: Arc<LinkState>,
|
||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||
@@ -365,7 +368,6 @@ pub(crate) struct TickOutput {
|
||||
}
|
||||
|
||||
struct StepResult {
|
||||
audio_commands: Vec<String>,
|
||||
completed_iterations: Vec<PatternId>,
|
||||
any_step_fired: bool,
|
||||
}
|
||||
@@ -383,15 +385,44 @@ fn parse_chain_target(s: &str) -> Option<PatternId> {
|
||||
})
|
||||
}
|
||||
|
||||
struct KeyCache {
|
||||
speed_keys: [[String; MAX_PATTERNS]; MAX_BANKS],
|
||||
chain_keys: [[String; MAX_PATTERNS]; MAX_BANKS],
|
||||
}
|
||||
|
||||
impl KeyCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
speed_keys: std::array::from_fn(|bank| {
|
||||
std::array::from_fn(|pattern| format!("__speed_{bank}_{pattern}__"))
|
||||
}),
|
||||
chain_keys: std::array::from_fn(|bank| {
|
||||
std::array::from_fn(|pattern| format!("__chain_{bank}_{pattern}__"))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn speed_key(&self, bank: usize, pattern: usize) -> &str {
|
||||
&self.speed_keys[bank][pattern]
|
||||
}
|
||||
|
||||
fn chain_key(&self, bank: usize, pattern: usize) -> &str {
|
||||
&self.chain_keys[bank][pattern]
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SequencerState {
|
||||
audio_state: AudioState,
|
||||
pattern_cache: PatternCache,
|
||||
runs_counter: RunsCounter,
|
||||
step_traces: HashMap<(usize, usize, usize), ExecutionTrace>,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
event_count: usize,
|
||||
dropped_events: usize,
|
||||
script_engine: ScriptEngine,
|
||||
variables: Variables,
|
||||
speed_overrides: HashMap<(usize, usize), f64>,
|
||||
key_cache: KeyCache,
|
||||
buf_audio_commands: Vec<String>,
|
||||
}
|
||||
|
||||
impl SequencerState {
|
||||
@@ -405,11 +436,14 @@ impl SequencerState {
|
||||
audio_state: AudioState::new(),
|
||||
pattern_cache: PatternCache::new(),
|
||||
runs_counter: RunsCounter::new(),
|
||||
step_traces: HashMap::new(),
|
||||
step_traces: Arc::new(HashMap::new()),
|
||||
event_count: 0,
|
||||
dropped_events: 0,
|
||||
script_engine,
|
||||
variables,
|
||||
speed_overrides: HashMap::new(),
|
||||
key_cache: KeyCache::new(),
|
||||
buf_audio_commands: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +492,7 @@ impl SequencerState {
|
||||
self.audio_state.active_patterns.clear();
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.pending_stops.clear();
|
||||
self.step_traces.clear();
|
||||
Arc::make_mut(&mut self.step_traces).clear();
|
||||
self.runs_counter.counts.clear();
|
||||
}
|
||||
SeqCommand::Shutdown => {}
|
||||
@@ -490,7 +524,7 @@ impl SequencerState {
|
||||
self.audio_state.prev_beat = beat;
|
||||
|
||||
TickOutput {
|
||||
audio_commands: steps.audio_commands,
|
||||
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||
new_tempo: vars.new_tempo,
|
||||
shared_state: self.build_shared_state(),
|
||||
}
|
||||
@@ -499,13 +533,15 @@ impl SequencerState {
|
||||
fn tick_paused(&mut self) -> TickOutput {
|
||||
for pending in self.audio_state.pending_stops.drain(..) {
|
||||
self.audio_state.active_patterns.remove(&pending.id);
|
||||
self.step_traces.retain(|&(bank, pattern, _), _| {
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
}
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.buf_audio_commands.clear();
|
||||
TickOutput {
|
||||
audio_commands: Vec::new(),
|
||||
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||
new_tempo: None,
|
||||
shared_state: self.build_shared_state(),
|
||||
}
|
||||
@@ -546,7 +582,7 @@ impl SequencerState {
|
||||
for pending in &self.audio_state.pending_stops {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
self.audio_state.active_patterns.remove(&pending.id);
|
||||
self.step_traces.retain(|&(bank, pattern, _), _| {
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
stopped.push(pending.id);
|
||||
@@ -564,32 +600,29 @@ impl SequencerState {
|
||||
fill: bool,
|
||||
nudge_secs: f64,
|
||||
) -> StepResult {
|
||||
self.buf_audio_commands.clear();
|
||||
let mut result = StepResult {
|
||||
audio_commands: Vec::new(),
|
||||
completed_iterations: Vec::new(),
|
||||
any_step_fired: false,
|
||||
};
|
||||
|
||||
let speed_overrides: HashMap<(usize, usize), f64> = {
|
||||
self.speed_overrides.clear();
|
||||
{
|
||||
let vars = self.variables.lock().unwrap();
|
||||
self.audio_state
|
||||
.active_patterns
|
||||
.keys()
|
||||
.filter_map(|id| {
|
||||
let key = format!("__speed_{}_{}__", id.bank, id.pattern);
|
||||
vars.get(&key)
|
||||
.and_then(|v| v.as_float().ok())
|
||||
.map(|v| ((id.bank, id.pattern), v))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
for id in self.audio_state.active_patterns.keys() {
|
||||
let key = self.key_cache.speed_key(id.bank, id.pattern);
|
||||
if let Some(v) = vars.get(key).and_then(|v| v.as_float().ok()) {
|
||||
self.speed_overrides.insert((id.bank, id.pattern), v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_id, active) in self.audio_state.active_patterns.iter_mut() {
|
||||
let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let speed_mult = speed_overrides
|
||||
let speed_mult = self.speed_overrides
|
||||
.get(&(active.bank, active.pattern))
|
||||
.copied()
|
||||
.unwrap_or_else(|| pattern.speed.multiplier());
|
||||
@@ -633,13 +666,13 @@ impl SequencerState {
|
||||
.script_engine
|
||||
.evaluate_with_trace(script, &ctx, &mut trace)
|
||||
{
|
||||
self.step_traces.insert(
|
||||
Arc::make_mut(&mut self.step_traces).insert(
|
||||
(active.bank, active.pattern, source_idx),
|
||||
std::mem::take(&mut trace),
|
||||
);
|
||||
for cmd in cmds {
|
||||
self.event_count += 1;
|
||||
result.audio_commands.push(cmd);
|
||||
self.buf_audio_commands.push(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -680,18 +713,18 @@ impl SequencerState {
|
||||
|
||||
let mut chain_transitions = Vec::new();
|
||||
for id in completed {
|
||||
let chain_key = format!("__chain_{}_{}__", id.bank, id.pattern);
|
||||
if let Some(Value::Str(s, _)) = vars.get(&chain_key) {
|
||||
let chain_key = self.key_cache.chain_key(id.bank, id.pattern);
|
||||
if let Some(Value::Str(s, _)) = vars.get(chain_key) {
|
||||
if let Some(target) = parse_chain_target(s) {
|
||||
chain_transitions.push((*id, target));
|
||||
}
|
||||
}
|
||||
vars.remove(&chain_key);
|
||||
vars.remove(chain_key);
|
||||
}
|
||||
|
||||
for id in stopped {
|
||||
let chain_key = format!("__chain_{}_{}__", id.bank, id.pattern);
|
||||
vars.remove(&chain_key);
|
||||
let chain_key = self.key_cache.chain_key(id.bank, id.pattern);
|
||||
vars.remove(chain_key);
|
||||
}
|
||||
|
||||
VariableReads {
|
||||
@@ -737,7 +770,7 @@ impl SequencerState {
|
||||
iter: a.iter,
|
||||
})
|
||||
.collect(),
|
||||
step_traces: self.step_traces.clone(),
|
||||
step_traces: Arc::clone(&self.step_traces),
|
||||
event_count: self.event_count,
|
||||
dropped_events: self.dropped_events,
|
||||
}
|
||||
|
||||
@@ -476,6 +476,9 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
KeyCode::Char('p') if ctrl => {
|
||||
editor.search_prev();
|
||||
}
|
||||
KeyCode::Char('s') if ctrl => {
|
||||
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
|
||||
}
|
||||
KeyCode::Char('a') if ctrl => {
|
||||
editor.select_all();
|
||||
}
|
||||
@@ -890,7 +893,11 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter),
|
||||
KeyCode::Char(' ') => ctx.dispatch(AppCommand::PatternsTogglePlay),
|
||||
KeyCode::Char(' ') => {
|
||||
if ctx.app.patterns_nav.column == PatternsColumn::Patterns {
|
||||
ctx.dispatch(AppCommand::PatternsTogglePlay);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('c') if !ctrl => ctx.dispatch(AppCommand::CommitStagedChanges),
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use cagire_ratatui::Editor;
|
||||
@@ -54,6 +55,15 @@ pub struct EditorContext {
|
||||
pub editor: Editor,
|
||||
pub selection_anchor: Option<usize>,
|
||||
pub copied_steps: Option<CopiedSteps>,
|
||||
pub show_stack: bool,
|
||||
pub stack_cache: RefCell<Option<StackCache>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StackCache {
|
||||
pub cursor_line: usize,
|
||||
pub lines_hash: u64,
|
||||
pub result: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -94,6 +104,8 @@ impl Default for EditorContext {
|
||||
editor: Editor::new(),
|
||||
selection_anchor: None,
|
||||
copied_steps: None,
|
||||
show_stack: false,
|
||||
stack_cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ pub mod ui;
|
||||
|
||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
||||
pub use options::{OptionsFocus, OptionsState};
|
||||
pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField};
|
||||
pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField, StackCache};
|
||||
pub use live_keys::LiveKeyState;
|
||||
pub use modal::Modal;
|
||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||
|
||||
@@ -187,6 +187,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_device_column(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
|
||||
@@ -242,7 +242,7 @@ fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
|
||||
match line {
|
||||
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
|
||||
code_line_nr += 1;
|
||||
let raw: String = composite.compounds.iter().map(|c| &*c.src).collect();
|
||||
let raw: String = composite.compounds.iter().map(|c: &minimad::Compound| c.src).collect();
|
||||
let mut spans = vec![
|
||||
Span::styled(format!(" {code_line_nr:>2} "), code_border_style()),
|
||||
Span::styled("│ ", code_border_style()),
|
||||
|
||||
@@ -16,39 +16,90 @@ pub enum TokenKind {
|
||||
Note,
|
||||
Interval,
|
||||
Variable,
|
||||
Emit,
|
||||
Vary,
|
||||
Generator,
|
||||
Default,
|
||||
}
|
||||
|
||||
impl TokenKind {
|
||||
pub fn style(self) -> Style {
|
||||
match self {
|
||||
TokenKind::Number => Style::default().fg(Color::Rgb(255, 180, 100)),
|
||||
TokenKind::String => Style::default().fg(Color::Rgb(150, 220, 150)),
|
||||
TokenKind::Comment => Style::default().fg(Color::Rgb(100, 100, 100)),
|
||||
TokenKind::Keyword => Style::default().fg(Color::Rgb(220, 120, 220)),
|
||||
TokenKind::StackOp => Style::default().fg(Color::Rgb(120, 180, 220)),
|
||||
TokenKind::Operator => Style::default().fg(Color::Rgb(200, 200, 130)),
|
||||
TokenKind::Sound => Style::default().fg(Color::Rgb(100, 220, 200)),
|
||||
TokenKind::Param => Style::default().fg(Color::Rgb(180, 150, 220)),
|
||||
TokenKind::Context => Style::default().fg(Color::Rgb(220, 180, 120)),
|
||||
TokenKind::Note => Style::default().fg(Color::Rgb(120, 200, 160)),
|
||||
TokenKind::Interval => Style::default().fg(Color::Rgb(160, 200, 120)),
|
||||
TokenKind::Variable => Style::default().fg(Color::Rgb(200, 140, 180)),
|
||||
TokenKind::Default => Style::default().fg(Color::Rgb(200, 200, 200)),
|
||||
TokenKind::Emit => Style::default()
|
||||
.fg(Color::Rgb(255, 255, 255))
|
||||
.bg(Color::Rgb(140, 50, 50))
|
||||
.add_modifier(Modifier::BOLD),
|
||||
TokenKind::Number => Style::default()
|
||||
.fg(Color::Rgb(255, 200, 120))
|
||||
.bg(Color::Rgb(60, 40, 15)),
|
||||
TokenKind::String => Style::default()
|
||||
.fg(Color::Rgb(150, 230, 150))
|
||||
.bg(Color::Rgb(20, 55, 20)),
|
||||
TokenKind::Comment => Style::default()
|
||||
.fg(Color::Rgb(100, 100, 100))
|
||||
.bg(Color::Rgb(18, 18, 18)),
|
||||
TokenKind::Keyword => Style::default()
|
||||
.fg(Color::Rgb(230, 130, 230))
|
||||
.bg(Color::Rgb(55, 25, 55)),
|
||||
TokenKind::StackOp => Style::default()
|
||||
.fg(Color::Rgb(130, 190, 240))
|
||||
.bg(Color::Rgb(20, 40, 70)),
|
||||
TokenKind::Operator => Style::default()
|
||||
.fg(Color::Rgb(220, 220, 140))
|
||||
.bg(Color::Rgb(45, 45, 20)),
|
||||
TokenKind::Sound => Style::default()
|
||||
.fg(Color::Rgb(100, 240, 220))
|
||||
.bg(Color::Rgb(15, 60, 55)),
|
||||
TokenKind::Param => Style::default()
|
||||
.fg(Color::Rgb(190, 160, 240))
|
||||
.bg(Color::Rgb(45, 30, 70)),
|
||||
TokenKind::Context => Style::default()
|
||||
.fg(Color::Rgb(240, 190, 120))
|
||||
.bg(Color::Rgb(60, 45, 20)),
|
||||
TokenKind::Note => Style::default()
|
||||
.fg(Color::Rgb(120, 220, 170))
|
||||
.bg(Color::Rgb(20, 55, 40)),
|
||||
TokenKind::Interval => Style::default()
|
||||
.fg(Color::Rgb(170, 220, 120))
|
||||
.bg(Color::Rgb(35, 55, 20)),
|
||||
TokenKind::Variable => Style::default()
|
||||
.fg(Color::Rgb(220, 150, 190))
|
||||
.bg(Color::Rgb(60, 30, 50)),
|
||||
TokenKind::Vary => Style::default()
|
||||
.fg(Color::Rgb(230, 230, 100))
|
||||
.bg(Color::Rgb(55, 55, 15)),
|
||||
TokenKind::Generator => Style::default()
|
||||
.fg(Color::Rgb(100, 220, 180))
|
||||
.bg(Color::Rgb(15, 55, 45)),
|
||||
TokenKind::Default => Style::default()
|
||||
.fg(Color::Rgb(160, 160, 160))
|
||||
.bg(Color::Rgb(25, 25, 25)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gap_style() -> Style {
|
||||
Style::default().bg(Color::Rgb(25, 25, 25))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Token {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub kind: TokenKind,
|
||||
pub varargs: bool,
|
||||
}
|
||||
|
||||
fn lookup_word_kind(word: &str) -> Option<TokenKind> {
|
||||
fn lookup_word_kind(word: &str) -> Option<(TokenKind, bool)> {
|
||||
if word == "." {
|
||||
return Some((TokenKind::Emit, false));
|
||||
}
|
||||
if word == ".!" {
|
||||
return Some((TokenKind::Emit, true));
|
||||
}
|
||||
|
||||
for w in WORDS {
|
||||
if w.name == word || w.aliases.contains(&word) {
|
||||
return Some(match &w.compile {
|
||||
let kind = match &w.compile {
|
||||
WordCompile::Param => TokenKind::Param,
|
||||
WordCompile::Context(_) => TokenKind::Context,
|
||||
_ => match w.category {
|
||||
@@ -58,9 +109,12 @@ fn lookup_word_kind(word: &str) -> Option<TokenKind> {
|
||||
TokenKind::Operator
|
||||
}
|
||||
"Sound" => TokenKind::Sound,
|
||||
"Randomness" | "Probability" | "Selection" => TokenKind::Vary,
|
||||
"Generator" => TokenKind::Generator,
|
||||
_ => TokenKind::Keyword,
|
||||
},
|
||||
});
|
||||
};
|
||||
return Some((kind, w.varargs));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -97,24 +151,14 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '(' {
|
||||
let end = line.len();
|
||||
let comment_end = line[start..]
|
||||
.find(')')
|
||||
.map(|i| start + i + 1)
|
||||
.unwrap_or(end);
|
||||
if c == ';' && chars.peek().map(|(_, ch)| *ch) == Some(';') {
|
||||
tokens.push(Token {
|
||||
start,
|
||||
end: comment_end,
|
||||
end: line.len(),
|
||||
kind: TokenKind::Comment,
|
||||
varargs: false,
|
||||
});
|
||||
while let Some((i, _)) = chars.peek() {
|
||||
if *i >= comment_end {
|
||||
break;
|
||||
}
|
||||
chars.next();
|
||||
}
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
if c == '"' {
|
||||
@@ -129,6 +173,7 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
|
||||
start,
|
||||
end,
|
||||
kind: TokenKind::String,
|
||||
varargs: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -143,35 +188,35 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
|
||||
}
|
||||
|
||||
let word = &line[start..end];
|
||||
let kind = classify_word(word);
|
||||
tokens.push(Token { start, end, kind });
|
||||
let (kind, varargs) = classify_word(word);
|
||||
tokens.push(Token { start, end, kind, varargs });
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
fn classify_word(word: &str) -> TokenKind {
|
||||
fn classify_word(word: &str) -> (TokenKind, bool) {
|
||||
if word.parse::<f64>().is_ok() || word.parse::<i64>().is_ok() {
|
||||
return TokenKind::Number;
|
||||
return (TokenKind::Number, false);
|
||||
}
|
||||
|
||||
if let Some(kind) = lookup_word_kind(word) {
|
||||
return kind;
|
||||
if let Some((kind, varargs)) = lookup_word_kind(word) {
|
||||
return (kind, varargs);
|
||||
}
|
||||
|
||||
if INTERVALS.contains(&word) {
|
||||
return TokenKind::Interval;
|
||||
return (TokenKind::Interval, false);
|
||||
}
|
||||
|
||||
if is_note(&word.to_ascii_lowercase()) {
|
||||
return TokenKind::Note;
|
||||
return (TokenKind::Note, false);
|
||||
}
|
||||
|
||||
if word.len() > 1 && (word.starts_with('@') || word.starts_with('!')) {
|
||||
return TokenKind::Variable;
|
||||
return (TokenKind::Variable, false);
|
||||
}
|
||||
|
||||
TokenKind::Default
|
||||
(TokenKind::Default, false)
|
||||
}
|
||||
|
||||
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
|
||||
@@ -189,13 +234,11 @@ pub fn highlight_line_with_runtime(
|
||||
|
||||
let executed_bg = Color::Rgb(40, 35, 50);
|
||||
let selected_bg = Color::Rgb(80, 60, 20);
|
||||
let gap_style = TokenKind::gap_style();
|
||||
|
||||
for token in tokens {
|
||||
if token.start > last_end {
|
||||
result.push((
|
||||
TokenKind::Default.style(),
|
||||
line[last_end..token.start].to_string(),
|
||||
));
|
||||
result.push((gap_style, line[last_end..token.start].to_string()));
|
||||
}
|
||||
|
||||
let is_selected = selected_spans
|
||||
@@ -206,6 +249,9 @@ pub fn highlight_line_with_runtime(
|
||||
.any(|span| overlaps(token.start, token.end, span.start, span.end));
|
||||
|
||||
let mut style = token.kind.style();
|
||||
if token.varargs {
|
||||
style = style.add_modifier(Modifier::UNDERLINED);
|
||||
}
|
||||
if is_selected {
|
||||
style = style.bg(selected_bg).add_modifier(Modifier::BOLD);
|
||||
} else if is_executed {
|
||||
@@ -217,7 +263,7 @@ pub fn highlight_line_with_runtime(
|
||||
}
|
||||
|
||||
if last_end < line.len() {
|
||||
result.push((TokenKind::Default.style(), line[last_end..].to_string()));
|
||||
result.push((gap_style, line[last_end..].to_string()));
|
||||
}
|
||||
|
||||
result
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
|
||||
use ratatui::Frame;
|
||||
|
||||
use cagire_forth::Forth;
|
||||
use crate::app::App;
|
||||
use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::model::SourceSpan;
|
||||
use crate::model::{SourceSpan, StepContext, Value};
|
||||
use crate::page::Page;
|
||||
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel};
|
||||
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache};
|
||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||
use crate::widgets::{
|
||||
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
|
||||
@@ -20,6 +27,88 @@ use super::{
|
||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
||||
};
|
||||
|
||||
fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cache: &std::cell::RefCell<Option<StackCache>>) -> String {
|
||||
let cursor_line = editor.cursor().0;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if i > cursor_line {
|
||||
break;
|
||||
}
|
||||
line.hash(&mut hasher);
|
||||
}
|
||||
let lines_hash = hasher.finish();
|
||||
|
||||
if let Some(ref c) = *cache.borrow() {
|
||||
if c.cursor_line == cursor_line && c.lines_hash == lines_hash {
|
||||
return c.result.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let partial: Vec<&str> = lines.iter().take(cursor_line + 1).map(|s| s.as_str()).collect();
|
||||
let script = partial.join("\n");
|
||||
|
||||
let result = if script.trim().is_empty() {
|
||||
"Stack: []".to_string()
|
||||
} else {
|
||||
let vars = Arc::new(Mutex::new(HashMap::new()));
|
||||
let dict = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42)));
|
||||
let forth = Forth::new(vars, dict, rng);
|
||||
|
||||
let ctx = StepContext {
|
||||
step: 0,
|
||||
beat: 0.0,
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
tempo: 120.0,
|
||||
phase: 0.0,
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed: 1.0,
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
};
|
||||
|
||||
match forth.evaluate(&script, &ctx) {
|
||||
Ok(_) => {
|
||||
let stack = forth.stack();
|
||||
let formatted: Vec<String> = stack.iter().map(format_value).collect();
|
||||
format!("Stack: [{}]", formatted.join(" "))
|
||||
}
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
};
|
||||
|
||||
*cache.borrow_mut() = Some(StackCache {
|
||||
cursor_line,
|
||||
lines_hash,
|
||||
result: result.clone(),
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn format_value(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Int(n, _) => n.to_string(),
|
||||
Value::Float(f, _) => {
|
||||
if f.fract() == 0.0 && f.abs() < 1_000_000.0 {
|
||||
format!("{f:.1}")
|
||||
} else {
|
||||
format!("{f:.4}")
|
||||
}
|
||||
}
|
||||
Value::Str(s, _) => format!("\"{s}\""),
|
||||
Value::Quotation(..) => "[...]".to_string(),
|
||||
Value::CycleList(items) => {
|
||||
let inner: Vec<String> = items.iter().map(format_value).collect();
|
||||
format!("({})", inner.join(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn adjust_spans_for_line(
|
||||
spans: &[SourceSpan],
|
||||
line_start: usize,
|
||||
@@ -69,7 +158,7 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
|
||||
};
|
||||
|
||||
let [header_area, _padding, body_area, _bottom_padding, footer_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(header_height(padded.width)),
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
@@ -136,6 +225,10 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
|
||||
}
|
||||
}
|
||||
|
||||
fn header_height(width: u16) -> u16 {
|
||||
if width >= 80 { 1 } else { 2 }
|
||||
}
|
||||
|
||||
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let focused = app.panel.focus == PanelFocus::Side;
|
||||
match &app.panel.side {
|
||||
@@ -163,17 +256,41 @@ fn render_header(
|
||||
let bank = &app.project_state.project.banks[app.editor_ctx.bank];
|
||||
let pattern = &bank.patterns[app.editor_ctx.pattern];
|
||||
|
||||
// Layout: [Transport] [Live] [Tempo] [Bank] [Pattern] [Stats]
|
||||
let [transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] =
|
||||
Layout::horizontal([
|
||||
Constraint::Min(12),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(14),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(2),
|
||||
Constraint::Min(20),
|
||||
])
|
||||
.areas(area);
|
||||
let (transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area) =
|
||||
if area.height == 1 {
|
||||
let [t, l, tp, b, p, s] = Layout::horizontal([
|
||||
Constraint::Min(12),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(14),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(2),
|
||||
Constraint::Min(20),
|
||||
])
|
||||
.areas(area);
|
||||
(t, l, tp, b, p, s)
|
||||
} else {
|
||||
let [line1, line2] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let [t, l, tp, s] = Layout::horizontal([
|
||||
Constraint::Min(12),
|
||||
Constraint::Length(9),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Min(20),
|
||||
])
|
||||
.areas(line1);
|
||||
|
||||
let [b, p] = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(2),
|
||||
])
|
||||
.areas(line2);
|
||||
|
||||
(t, l, tp, b, p, s)
|
||||
};
|
||||
|
||||
// Transport block
|
||||
let (transport_bg, transport_text) = if app.playback.playing {
|
||||
@@ -619,28 +736,24 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
let show_search = app.editor_ctx.editor.search_active()
|
||||
|| !app.editor_ctx.editor.search_query().is_empty();
|
||||
|
||||
let (search_area, editor_area, hint_area) = if show_search {
|
||||
let search_area = Rect::new(inner.x, inner.y, inner.width, 1);
|
||||
let editor_area = Rect::new(
|
||||
inner.x,
|
||||
inner.y + 1,
|
||||
inner.width,
|
||||
inner.height.saturating_sub(2),
|
||||
);
|
||||
let hint_area =
|
||||
Rect::new(inner.x, inner.y + 1 + editor_area.height, inner.width, 1);
|
||||
(Some(search_area), editor_area, hint_area)
|
||||
let reserved_lines = 1 + if show_search { 1 } else { 0 };
|
||||
let editor_height = inner.height.saturating_sub(reserved_lines);
|
||||
|
||||
let mut y = inner.y;
|
||||
|
||||
let search_area = if show_search {
|
||||
let area = Rect::new(inner.x, y, inner.width, 1);
|
||||
y += 1;
|
||||
Some(area)
|
||||
} else {
|
||||
let editor_area = Rect::new(
|
||||
inner.x,
|
||||
inner.y,
|
||||
inner.width,
|
||||
inner.height.saturating_sub(1),
|
||||
);
|
||||
let hint_area = Rect::new(inner.x, inner.y + editor_area.height, inner.width, 1);
|
||||
(None, editor_area, hint_area)
|
||||
None
|
||||
};
|
||||
|
||||
let editor_area = Rect::new(inner.x, y, inner.width, editor_height);
|
||||
y += editor_height;
|
||||
|
||||
let hint_area = Rect::new(inner.x, y, inner.width, 1);
|
||||
|
||||
if let Some(sa) = search_area {
|
||||
let style = if app.editor_ctx.editor.search_active() {
|
||||
Style::default().fg(Color::Yellow)
|
||||
@@ -671,32 +784,52 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
|
||||
let dim = Style::default().fg(Color::DarkGray);
|
||||
let key = Style::default().fg(Color::Yellow);
|
||||
let hint = if app.editor_ctx.editor.search_active() {
|
||||
Line::from(vec![
|
||||
|
||||
if app.editor_ctx.editor.search_active() {
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("Enter", key),
|
||||
Span::styled(" confirm ", dim),
|
||||
Span::styled("Esc", key),
|
||||
Span::styled(" cancel", dim),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||
} else if app.editor_ctx.show_stack {
|
||||
let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor, &app.editor_ctx.stack_cache);
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("Esc", key),
|
||||
Span::styled(" save ", dim),
|
||||
Span::styled("C-e", key),
|
||||
Span::styled(" eval ", dim),
|
||||
Span::styled("C-s", key),
|
||||
Span::styled(" hide", dim),
|
||||
]);
|
||||
let [hint_left, stack_right] = Layout::horizontal([
|
||||
Constraint::Length(hint.width() as u16),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(hint_area);
|
||||
frame.render_widget(Paragraph::new(hint), hint_left);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
|
||||
stack_right,
|
||||
);
|
||||
} else {
|
||||
Line::from(vec![
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("Esc", key),
|
||||
Span::styled(" save ", dim),
|
||||
Span::styled("C-e", key),
|
||||
Span::styled(" eval ", dim),
|
||||
Span::styled("C-f", key),
|
||||
Span::styled(" find ", dim),
|
||||
Span::styled("C-n", key),
|
||||
Span::styled("/", dim),
|
||||
Span::styled("C-p", key),
|
||||
Span::styled(" next/prev ", dim),
|
||||
Span::styled("C-s", key),
|
||||
Span::styled(" stack ", dim),
|
||||
Span::styled("C-u", key),
|
||||
Span::styled("/", dim),
|
||||
Span::styled("C-r", key),
|
||||
Span::styled(" undo/redo", dim),
|
||||
])
|
||||
};
|
||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||
}
|
||||
}
|
||||
Modal::PatternProps {
|
||||
bank,
|
||||
|
||||
@@ -48,3 +48,6 @@ mod list_words;
|
||||
|
||||
#[path = "forth/ramps.rs"]
|
||||
mod ramps;
|
||||
|
||||
#[path = "forth/generator.rs"]
|
||||
mod generator;
|
||||
|
||||
@@ -46,8 +46,8 @@ fn word_defined_in_one_forth_available_in_same() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_word_errors() {
|
||||
expect_error("nosuchword", "unknown word");
|
||||
fn unknown_word_becomes_string() {
|
||||
expect_str("nosuchword", "nosuchword");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,8 +11,8 @@ fn whitespace_only() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_word() {
|
||||
expect_error("foobar", "unknown word");
|
||||
fn unknown_word_becomes_string() {
|
||||
expect_str("foobar", "foobar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -22,12 +22,12 @@ fn string_not_number() {
|
||||
|
||||
#[test]
|
||||
fn comment_ignored() {
|
||||
expect_int("1 (this is a comment) 2 +", 3);
|
||||
expect_int("1 ;; this is a comment\n2 +", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_comment() {
|
||||
expect_int("1 (multi\nline\ncomment) 2 +", 3);
|
||||
expect_int("1 ;; first comment\n;; entire line comment\n2 +", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -51,22 +51,6 @@ fn string_with_spaces() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_count() {
|
||||
let f = run("[ 1 2 3 ]");
|
||||
assert_eq!(stack_int(&f), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_empty() {
|
||||
expect_int("[ ]", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_preserves_values() {
|
||||
expect_int("[ 10 20 ] drop +", 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_based_on_step() {
|
||||
let ctx0 = ctx_with(|c| c.step = 0);
|
||||
|
||||
104
tests/forth/generator.rs
Normal file
104
tests/forth/generator.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use super::harness::*;
|
||||
use cagire::forth::Value;
|
||||
|
||||
fn int(n: i64) -> Value {
|
||||
Value::Int(n, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_ascending() {
|
||||
expect_stack("1 4 ..", &[int(1), int(2), int(3), int(4)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_descending() {
|
||||
expect_stack("4 1 ..", &[int(4), int(3), int(2), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_single() {
|
||||
expect_stack("3 3 ..", &[int(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_negative() {
|
||||
expect_stack("-2 1 ..", &[int(-2), int(-1), int(0), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_underflow() {
|
||||
expect_error("1 ..", "stack underflow");
|
||||
expect_error("..", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_basic() {
|
||||
expect_stack("{ 42 } 3 gen", &[int(42), int(42), int(42)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_with_computation() {
|
||||
// Each iteration: dup current value, add 1, result is new value
|
||||
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
||||
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
||||
// So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s
|
||||
expect_stack("0 { dup 1 + } 3 gen", &[int(0), int(1), int(1), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_chained() {
|
||||
// Start with 1, each iteration: dup, multiply by 2
|
||||
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
||||
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
||||
expect_stack("1 { dup 2 * } 3 gen", &[int(1), int(2), int(2), int(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_zero() {
|
||||
expect_stack("{ 1 } 0 gen", &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_underflow() {
|
||||
expect_error("3 gen", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_not_a_number() {
|
||||
expect_error("{ 1 } gen", "expected number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_negative() {
|
||||
expect_error("{ 1 } -1 gen", "gen count must be >= 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_empty_quot_error() {
|
||||
expect_error("{ } 3 gen", "quotation must produce");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geom_growing() {
|
||||
expect_stack("1 2 4 geom..", &[int(1), int(2), int(4), int(8)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geom_shrinking() {
|
||||
expect_stack("8 0.5 4 geom..", &[int(8), int(4), int(2), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geom_single() {
|
||||
expect_stack("5 2 1 geom..", &[int(5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geom_zero_count() {
|
||||
expect_stack("1 2 0 geom..", &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geom_underflow() {
|
||||
expect_error("1 2 geom..", "stack underflow");
|
||||
}
|
||||
@@ -88,6 +88,10 @@ pub fn expect_int(script: &str, expected: i64) {
|
||||
expect_stack(script, &[Value::Int(expected, None)]);
|
||||
}
|
||||
|
||||
pub fn expect_str(script: &str, expected: &str) {
|
||||
expect_stack(script, &[Value::Str(expected.to_string(), None)]);
|
||||
}
|
||||
|
||||
pub fn expect_float(script: &str, expected: f64) {
|
||||
let f = run(script);
|
||||
let stack = f.stack();
|
||||
|
||||
@@ -1,124 +1,106 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn choose_word_from_list() {
|
||||
// 1 2 [ + - ] choose: picks + or -, applies to 1 2
|
||||
// With seed 42, choose picks one deterministically
|
||||
fn choose_from_stack() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate("1 2 [ + - ] choose", &ctx).unwrap();
|
||||
f.evaluate("1 2 3 3 choose", &ctx).unwrap();
|
||||
let val = stack_int(&f);
|
||||
assert!(val == 3 || val == -1, "expected 3 or -1, got {}", val);
|
||||
assert!(val >= 1 && val <= 3, "expected 1, 2, or 3, got {}", val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_word_from_list() {
|
||||
// At runs=0, picks first word (dup)
|
||||
fn cycle_by_runs() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("5 < dup nip >", &ctx);
|
||||
assert_eq!(stack_int(&f), 5); // dup leaves 5 5, but stack check takes top
|
||||
|
||||
// At runs=1, picks second word (2 *)
|
||||
let f = forth();
|
||||
let ctx = ctx_with(|c| c.runs = 1);
|
||||
f.evaluate(": double 2 * ; 5 < dup double >", &ctx).unwrap();
|
||||
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_word_in_list() {
|
||||
let f = forth();
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
f.evaluate(": add3 3 + ; : add5 5 + ; 10 < add3 add5 >", &ctx).unwrap();
|
||||
assert_eq!(stack_int(&f), 13); // runs=0 picks add3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_word_in_list_second() {
|
||||
let f = forth();
|
||||
let ctx = ctx_with(|c| c.runs = 1);
|
||||
f.evaluate(": add3 3 + ; : add5 5 + ; 10 < add3 add5 >", &ctx).unwrap();
|
||||
assert_eq!(stack_int(&f), 15); // runs=1 picks add5
|
||||
}
|
||||
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 20);
|
||||
|
||||
#[test]
|
||||
fn values_in_list_still_work() {
|
||||
// Numbers inside lists should still push as values (not quotations)
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("< 10 20 30 >", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_in_list_cycle() {
|
||||
let ctx = ctx_with(|c| c.runs = 2);
|
||||
let f = run_ctx("< 10 20 30 >", &ctx);
|
||||
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 30);
|
||||
|
||||
let ctx = ctx_with(|c| c.runs = 3);
|
||||
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcycle_by_iter() {
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("10 20 30 3 pcycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("10 20 30 3 pcycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 20);
|
||||
|
||||
let ctx = ctx_with(|c| c.iter = 2);
|
||||
let f = run_ctx("10 20 30 3 pcycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_values_and_words() {
|
||||
// Values stay as values, words become quotations
|
||||
// [ 10 20 ] choose just picks a number
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate("[ 10 20 ] choose", &ctx).unwrap();
|
||||
let val = stack_int(&f);
|
||||
assert!(val == 10 || val == 20, "expected 10 or 20, got {}", val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word_with_sound_params() {
|
||||
let f = forth();
|
||||
fn cycle_with_quotations() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let outputs = f.evaluate(
|
||||
": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > .",
|
||||
&ctx
|
||||
).unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert!(outputs[0].contains("verb/0.5"), "expected verb/0.5 in {}", outputs[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_word_in_list() {
|
||||
// 3 4 [ + ] choose -> picks + (only option), applies to 3 4 = 7
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("3 4 < + >", &ctx);
|
||||
assert_eq!(stack_int(&f), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcycle_word_from_list() {
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("10 << dup 2 * >>", &ctx);
|
||||
// iter=0 picks dup: 10 10
|
||||
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
|
||||
let stack = f.stack();
|
||||
assert_eq!(stack.len(), 2);
|
||||
assert_eq!(stack_int(&f), 5);
|
||||
|
||||
let ctx = ctx_with(|c| c.runs = 1);
|
||||
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcycle_word_second() {
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("10 << dup 2 * >>", &ctx);
|
||||
// iter=1 picks "2 *" — but wait, each token is its own element
|
||||
// so << dup 2 * >> has 3 elements: {dup}, 2, {*}
|
||||
// iter=1 picks element index 1 which is value 2
|
||||
assert_eq!(stack_int(&f), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_op_quotation_in_list() {
|
||||
// Use { } for multi-op quotations inside lists
|
||||
fn cycle_executes_quotation() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("10 < { 2 * } { 3 + } >", &ctx);
|
||||
assert_eq!(stack_int(&f), 20); // runs=0 picks {2 *}
|
||||
let f = run_ctx("10 { 3 + } { 5 + } 2 cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_op_quotation_second() {
|
||||
let ctx = ctx_with(|c| c.runs = 1);
|
||||
let f = run_ctx("10 < { 2 * } { 3 + } >", &ctx);
|
||||
assert_eq!(stack_int(&f), 13); // runs=1 picks {3 +}
|
||||
fn dupn_basic() {
|
||||
// 5 3 dupn -> 5 5 5, then + + -> 15
|
||||
expect_int("5 3 dupn + +", 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dupn_alias() {
|
||||
expect_int("5 3 ! + +", 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_creates_cycle_list() {
|
||||
let outputs = expect_outputs(r#"0.0 at 60 64 67 3 tcycle note sine s ."#, 1);
|
||||
assert!(outputs[0].contains("note/60"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_with_multiple_emits() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
let outputs = f.evaluate(r#"0 0.5 2 at 60 64 2 tcycle note sine s ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 2);
|
||||
assert!(outputs[0].contains("note/60"));
|
||||
assert!(outputs[1].contains("note/64"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_zero_count_error() {
|
||||
expect_error("1 2 3 0 cycle", "cycle count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choose_zero_count_error() {
|
||||
expect_error("1 2 3 0 choose", "choose count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_zero_count_error() {
|
||||
expect_error("1 2 3 0 tcycle", "tcycle count must be > 0");
|
||||
}
|
||||
|
||||
@@ -42,15 +42,19 @@ fn get_sounds(outputs: &[String]) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_param(outputs: &[String], param: &str) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get(param).copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
const EPSILON: f64 = 1e-9;
|
||||
|
||||
fn approx_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < EPSILON
|
||||
}
|
||||
|
||||
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
|
||||
// Root duration = 4 * stepdur = 0.5s
|
||||
|
||||
#[test]
|
||||
fn stepdur_baseline() {
|
||||
let f = run("stepdur");
|
||||
@@ -65,72 +69,14 @@ fn single_emit() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_subdivision_2() {
|
||||
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() {
|
||||
fn multiple_emits_all_at_zero() {
|
||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step = 0.5 / 4.0;
|
||||
for (i, delta) in deltas.iter().enumerate() {
|
||||
let expected = step * i as f64;
|
||||
assert!(
|
||||
approx_eq(*delta, expected),
|
||||
"slot {}: expected {}, got {}",
|
||||
i, expected, delta
|
||||
);
|
||||
assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, 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]
|
||||
fn sound_persists() {
|
||||
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
|
||||
@@ -149,67 +95,36 @@ fn alternating_sounds() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dur_matches_slot_duration() {
|
||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
||||
fn dur_is_step_duration() {
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
let durs = get_durs(&outputs);
|
||||
let expected_dur = 0.5 / 4.0;
|
||||
for (i, dur) in durs.iter().enumerate() {
|
||||
assert!(
|
||||
approx_eq(*dur, expected_dur),
|
||||
"slot {} dur: expected {}, got {}",
|
||||
i, expected_dur, dur
|
||||
);
|
||||
}
|
||||
assert!(approx_eq(durs[0], 0.125), "dur should be step_duration (0.125), got {}", durs[0]);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn cycle_picks_by_step() {
|
||||
fn cycle_picks_by_runs() {
|
||||
for runs in 0..4 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s < . _ >"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s { . } { } 2 cycle"#, &ctx).unwrap();
|
||||
if runs % 2 == 0 {
|
||||
assert_eq!(outputs.len(), 1, "runs={}: . should be picked", runs);
|
||||
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "runs={}: _ should be picked", runs);
|
||||
assert_eq!(outputs.len(), 0, "runs={}: no-op should be picked", runs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcycle_picks_by_pattern() {
|
||||
fn pcycle_picks_by_iter() {
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s << . _ >>"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s { . } { } 2 pcycle"#, &ctx).unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: . should be picked", iter);
|
||||
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "iter={}: _ should be picked", iter);
|
||||
assert_eq!(outputs.len(), 0, "iter={}: no-op should be picked", iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +134,10 @@ fn cycle_with_sounds() {
|
||||
for runs in 0..3 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#"< { "kick" s . } { "hat" s . } { "snare" s . } >"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(
|
||||
r#"{ "kick" s . } { "hat" s . } { "snare" s . } 3 cycle"#,
|
||||
&ctx
|
||||
).unwrap();
|
||||
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
||||
let sounds = get_sounds(&outputs);
|
||||
let expected = ["kick", "hat", "snare"][runs % 3];
|
||||
@@ -227,143 +145,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]
|
||||
fn emit_n_basic() {
|
||||
let outputs = expect_outputs(r#""kick" s 4 .!"#, 4);
|
||||
@@ -384,3 +165,106 @@ fn emit_n_negative_error() {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_single_delta() {
|
||||
let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step_dur = 0.125;
|
||||
assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_list_deltas() {
|
||||
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s ."#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step_dur = 0.125;
|
||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
|
||||
assert!(approx_eq(deltas[1], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_three_deltas() {
|
||||
let outputs = expect_outputs(r#"0 0.33 0.67 3 at "kick" s ."#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step_dur = 0.125;
|
||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
|
||||
assert!((deltas[1] - 0.33 * step_dur).abs() < 0.001, "expected delta at 0.33 of step");
|
||||
assert!((deltas[2] - 0.67 * step_dur).abs() < 0.001, "expected delta at 0.67 of step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_persists_across_emits() {
|
||||
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s . "hat" s ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_basic() {
|
||||
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note sine s ."#, 3);
|
||||
let notes = get_param(&outputs, "note");
|
||||
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_wraps() {
|
||||
let outputs = expect_outputs(r#"0 0.33 0.67 3 at 60 64 2 tcycle note sine s ."#, 3);
|
||||
let notes = get_param(&outputs, "note");
|
||||
assert_eq!(notes, vec![60.0, 64.0, 60.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_with_sound() {
|
||||
let outputs = expect_outputs(r#"0 0.5 2 at kick hat 2 tcycle s ."#, 2);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "hat"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_multiple_params() {
|
||||
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note 0.5 1.0 2 tcycle gain sine s ."#, 3);
|
||||
let notes = get_param(&outputs, "note");
|
||||
let gains = get_param(&outputs, "gain");
|
||||
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
||||
assert_eq!(gains, vec![0.5, 1.0, 0.5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_reset_with_zero() {
|
||||
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s . 0.0 at "hat" s ."#, 3);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcycle_records_selected_spans() {
|
||||
use cagire::forth::ExecutionTrace;
|
||||
|
||||
let f = forth();
|
||||
let mut trace = ExecutionTrace::default();
|
||||
let script = r#"0 0.5 2 at kick hat 2 tcycle s ."#;
|
||||
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
||||
|
||||
// Should have 4 selected spans:
|
||||
// - 2 for at deltas (0 and 0.5)
|
||||
// - 2 for tcycle sound values (kick and hat)
|
||||
assert_eq!(trace.selected_spans.len(), 4, "expected 4 selected spans (2 at + 2 tcycle)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_records_selected_spans() {
|
||||
use cagire::forth::ExecutionTrace;
|
||||
|
||||
let f = forth();
|
||||
let mut trace = ExecutionTrace::default();
|
||||
let script = r#"0 0.5 0.75 3 at "kick" s ."#;
|
||||
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)
|
||||
assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)");
|
||||
|
||||
// Verify at delta spans (even indices: 0, 2, 4)
|
||||
assert_eq!(&script[trace.selected_spans[0].start..trace.selected_spans[0].end], "0");
|
||||
assert_eq!(&script[trace.selected_spans[2].start..trace.selected_spans[2].end], "0.5");
|
||||
assert_eq!(&script[trace.selected_spans[4].start..trace.selected_spans[4].end], "0.75");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user