Reorganize repository

This commit is contained in:
2026-01-23 20:29:44 +01:00
parent 1433e07066
commit a1ddb4a170
40 changed files with 372 additions and 257084 deletions

7
crates/forth/Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[package]
name = "cagire-forth"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8"

View File

@@ -0,0 +1,282 @@
use super::ops::Op;
use super::types::{Dictionary, SourceSpan};
use super::words::{compile_word, simple_op};
#[derive(Clone, Debug)]
enum Token {
Int(i64, SourceSpan),
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> {
let tokens = tokenize(input);
compile(&tokens, dict)
}
fn tokenize(input: &str) -> Vec<Token> {
let mut tokens = Vec::new();
let mut chars = input.char_indices().peekable();
while let Some(&(pos, c)) = chars.peek() {
if c.is_whitespace() {
chars.next();
continue;
}
if c == '"' {
let start = pos;
chars.next();
let mut s = String::new();
let mut end = start + 1;
while let Some(&(i, ch)) = chars.peek() {
end = i + ch.len_utf8();
chars.next();
if ch == '"' {
break;
}
s.push(ch);
}
tokens.push(Token::Str(s, SourceSpan { start, end }));
continue;
}
if c == '(' {
while let Some(&(_, ch)) = chars.peek() {
chars.next();
if ch == ')' {
break;
}
}
continue;
}
if c == '{' {
chars.next();
tokens.push(Token::QuoteStart(pos));
continue;
}
if c == '}' {
chars.next();
tokens.push(Token::QuoteEnd(pos));
continue;
}
let start = pos;
let mut word = String::new();
let mut end = start;
while let Some(&(i, ch)) = chars.peek() {
if ch.is_whitespace() || ch == '{' || ch == '}' {
break;
}
end = i + ch.len_utf8();
word.push(ch);
chars.next();
}
let span = SourceSpan { start, end };
if let Ok(i) = word.parse::<i64>() {
tokens.push(Token::Int(i, span));
} else if let Ok(f) = word.parse::<f64>() {
tokens.push(Token::Float(f, span));
} else {
tokens.push(Token::Word(word, span));
}
}
tokens
}
fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
let mut ops = Vec::new();
let mut i = 0;
let mut pipe_parity = false;
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 == ":" {
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
i += consumed;
dict.lock().unwrap().insert(name, body);
} else if word == ";" {
return Err("unexpected ;".into());
} else if word == "|" {
if pipe_parity {
ops.push(Op::LocalCycleEnd);
list_depth = list_depth.saturating_sub(1);
} else {
ops.push(Op::ListStart);
list_depth += 1;
}
pipe_parity = !pipe_parity;
} else if word == "if" {
let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..], dict)?;
i += consumed;
if else_ops.is_empty() {
ops.push(Op::BranchIfZero(then_ops.len(), then_span, None));
ops.extend(then_ops);
} else {
ops.push(Op::BranchIfZero(then_ops.len() + 1, then_span, else_span));
ops.extend(then_ops);
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}"));
}
}
}
i += 1;
}
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> {
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;
}
}
_ => {}
}
}
let end_idx = end_idx.ok_or("missing }")?;
let byte_pos = match &tokens[end_idx] {
Token::QuoteEnd(pos) => *pos,
_ => unreachable!(),
};
let quote_ops = compile(&tokens[..end_idx], dict)?;
Ok((quote_ops, end_idx + 1, byte_pos))
}
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 }),
}
}
fn compile_colon_def(tokens: &[Token], dict: &Dictionary) -> Result<(usize, String, Vec<Op>), String> {
if tokens.is_empty() {
return Err("expected word name after ':'".into());
}
let name = match &tokens[0] {
Token::Word(w, _) => w.clone(),
_ => return Err("expected word name after ':'".into()),
};
let mut semi_pos = None;
for (i, tok) in tokens[1..].iter().enumerate() {
if let Token::Word(w, _) = tok {
if w == ";" {
semi_pos = Some(i + 1);
break;
}
}
}
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))
}
fn tokens_span(tokens: &[Token]) -> Option<SourceSpan> {
let first = tokens.first().and_then(token_span)?;
let last = tokens.last().and_then(token_span)?;
Some(SourceSpan { start: first.start, end: last.end })
}
#[allow(clippy::type_complexity)]
fn compile_if(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, Vec<Op>, usize, Option<SourceSpan>, Option<SourceSpan>), String> {
let mut depth = 1;
let mut else_pos = None;
let mut then_pos = None;
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"if" => depth += 1,
"else" if depth == 1 => else_pos = Some(i),
"then" => {
depth -= 1;
if depth == 0 {
then_pos = Some(i);
break;
}
}
_ => {}
}
}
}
let then_pos = then_pos.ok_or("missing 'then'")?;
let (then_ops, else_ops, then_span, else_span) = if let Some(ep) = else_pos {
let then_slice = &tokens[..ep];
let else_slice = &tokens[ep + 1..then_pos];
let then_span = tokens_span(then_slice);
let else_span = tokens_span(else_slice);
let then_ops = compile(then_slice, dict)?;
let else_ops = compile(else_slice, dict)?;
(then_ops, else_ops, then_span, else_span)
} else {
let then_slice = &tokens[..then_pos];
let then_span = tokens_span(then_slice);
let then_ops = compile(then_slice, dict)?;
(then_ops, Vec::new(), then_span, None)
};
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
}

9
crates/forth/src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
mod compiler;
mod ops;
mod types;
mod vm;
mod words;
pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
pub use vm::Forth;
pub use words::{Word, WordCompile, WORDS};

82
crates/forth/src/ops.rs Normal file
View File

@@ -0,0 +1,82 @@
use super::types::SourceSpan;
#[derive(Clone, Debug, PartialEq)]
pub enum Op {
PushInt(i64, Option<SourceSpan>),
PushFloat(f64, Option<SourceSpan>),
PushStr(String, Option<SourceSpan>),
Dup,
Dupn,
Drop,
Swap,
Over,
Rot,
Nip,
Tuck,
Add,
Sub,
Mul,
Div,
Mod,
Neg,
Abs,
Floor,
Ceil,
Round,
Min,
Max,
Eq,
Ne,
Lt,
Gt,
Le,
Ge,
And,
Or,
Not,
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
Branch(usize),
NewCmd,
SetParam(String),
Emit,
Get,
Set,
GetContext(String),
Rand,
Rrand,
Seed,
Cycle,
Choose,
ChanceExec,
ProbExec,
Coin,
Mtof,
Ftom,
ListStart,
ListEnd,
ListEndCycle,
PCycle,
ListEndPCycle,
At,
Window,
Scale,
Pop,
Subdivide,
SetTempo,
Each,
Every,
Quotation(Vec<Op>, Option<SourceSpan>),
When,
Unless,
Adsr,
Ad,
Stack,
For,
LocalCycleEnd,
Echo,
Necho,
Apply,
Ramp,
Range,
Noise,
}

141
crates/forth/src/types.rs Normal file
View File

@@ -0,0 +1,141 @@
use rand::rngs::StdRng;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use super::ops::Op;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SourceSpan {
pub start: usize,
pub end: usize,
}
#[derive(Clone, Debug, Default)]
pub struct ExecutionTrace {
pub executed_spans: Vec<SourceSpan>,
pub selected_spans: Vec<SourceSpan>,
}
pub struct StepContext {
pub step: usize,
pub beat: f64,
pub pattern: usize,
pub tempo: f64,
pub phase: f64,
pub slot: usize,
pub runs: usize,
pub iter: usize,
pub speed: f64,
pub fill: bool,
}
impl StepContext {
pub fn step_duration(&self) -> f64 {
60.0 / self.tempo / 4.0 / self.speed
}
}
pub type Variables = Arc<Mutex<HashMap<String, Value>>>;
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
pub type Rng = Arc<Mutex<StdRng>>;
pub type Stack = Arc<Mutex<Vec<Value>>>;
#[derive(Clone, Debug)]
pub enum Value {
Int(i64, Option<SourceSpan>),
Float(f64, Option<SourceSpan>),
Str(String, Option<SourceSpan>),
Marker,
Quotation(Vec<Op>, Option<SourceSpan>),
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(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,
_ => false,
}
}
}
impl Value {
pub fn as_float(&self) -> Result<f64, String> {
match self {
Value::Float(f, _) => Ok(*f),
Value::Int(i, _) => Ok(*i as f64),
_ => Err("expected number".into()),
}
}
pub(super) fn as_int(&self) -> Result<i64, String> {
match self {
Value::Int(i, _) => Ok(*i),
Value::Float(f, _) => Ok(*f as i64),
_ => Err("expected number".into()),
}
}
pub(super) fn as_str(&self) -> Result<&str, String> {
match self {
Value::Str(s, _) => Ok(s),
_ => Err("expected string".into()),
}
}
pub(super) fn is_truthy(&self) -> bool {
match self {
Value::Int(i, _) => *i != 0,
Value::Float(f, _) => *f != 0.0,
Value::Str(s, _) => !s.is_empty(),
Value::Marker => false,
Value::Quotation(..) => true,
}
}
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(),
}
}
pub(super) fn span(&self) -> Option<SourceSpan> {
match self {
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s,
Value::Marker | Value::Quotation(..) => None,
}
}
}
#[derive(Clone, Debug, Default)]
pub(super) struct CmdRegister {
sound: Option<String>,
params: Vec<(String, String)>,
}
impl CmdRegister {
pub(super) fn set_sound(&mut self, name: String) {
self.sound = Some(name);
}
pub(super) fn set_param(&mut self, key: String, value: String) {
self.params.push((key, value));
}
pub(super) fn take(&mut self) -> Option<(String, Vec<(String, String)>)> {
let sound = self.sound.take()?;
let params = std::mem::take(&mut self.params);
Some((sound, params))
}
}

956
crates/forth/src/vm.rs Normal file
View File

@@ -0,0 +1,956 @@
use rand::rngs::StdRng;
use rand::{Rng as RngTrait, SeedableRng};
use super::compiler::compile_script;
use super::ops::Op;
use super::types::{
CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
};
#[derive(Clone, Debug)]
struct TimeContext {
start: f64,
duration: f64,
subdivisions: Option<Vec<(f64, f64)>>,
iteration_index: Option<usize>,
}
pub struct Forth {
stack: Stack,
vars: Variables,
dict: Dictionary,
rng: Rng,
}
impl Forth {
pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
Self {
stack: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
vars,
dict,
rng,
}
}
#[allow(dead_code)]
pub fn stack(&self) -> Vec<Value> {
self.stack.lock().unwrap().clone()
}
#[allow(dead_code)]
pub fn clear_stack(&self) {
self.stack.lock().unwrap().clear();
}
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
self.evaluate_impl(script, ctx, None)
}
pub fn evaluate_with_trace(
&self,
script: &str,
ctx: &StepContext,
trace: &mut ExecutionTrace,
) -> Result<Vec<String>, String> {
self.evaluate_impl(script, ctx, Some(trace))
}
fn evaluate_impl(
&self,
script: &str,
ctx: &StepContext,
trace: Option<&mut ExecutionTrace>,
) -> Result<Vec<String>, String> {
if script.trim().is_empty() {
return Err("empty script".into());
}
let ops = compile_script(script, &self.dict)?;
self.execute(&ops, ctx, trace)
}
fn execute(
&self,
ops: &[Op],
ctx: &StepContext,
trace: Option<&mut ExecutionTrace>,
) -> Result<Vec<String>, String> {
let mut stack = self.stack.lock().unwrap();
let mut outputs: Vec<String> = Vec::new();
let mut time_stack: Vec<TimeContext> = vec![TimeContext {
start: 0.0,
duration: ctx.step_duration() * 4.0,
subdivisions: None,
iteration_index: None,
}];
let mut cmd = CmdRegister::default();
self.execute_ops(
ops,
ctx,
&mut stack,
&mut outputs,
&mut time_stack,
&mut cmd,
trace,
)?;
Ok(outputs)
}
#[allow(clippy::too_many_arguments)]
fn execute_ops(
&self,
ops: &[Op],
ctx: &StepContext,
stack: &mut Vec<Value>,
outputs: &mut Vec<String>,
time_stack: &mut Vec<TimeContext>,
cmd: &mut CmdRegister,
trace: Option<&mut ExecutionTrace>,
) -> Result<(), String> {
let mut pc = 0;
let trace_cell = std::cell::RefCell::new(trace);
while pc < ops.len() {
match &ops[pc] {
Op::PushInt(n, span) => stack.push(Value::Int(*n, *span)),
Op::PushFloat(f, span) => stack.push(Value::Float(*f, *span)),
Op::PushStr(s, span) => stack.push(Value::Str(s.clone(), *span)),
Op::Dup => {
let v = stack.last().ok_or("stack underflow")?.clone();
stack.push(v);
}
Op::Dupn => {
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
let v = stack.pop().ok_or("stack underflow")?;
for _ in 0..n {
stack.push(v.clone());
}
}
Op::Drop => {
stack.pop().ok_or("stack underflow")?;
}
Op::Swap => {
let len = stack.len();
if len < 2 {
return Err("stack underflow".into());
}
stack.swap(len - 1, len - 2);
}
Op::Over => {
let len = stack.len();
if len < 2 {
return Err("stack underflow".into());
}
let v = stack[len - 2].clone();
stack.push(v);
}
Op::Rot => {
let len = stack.len();
if len < 3 {
return Err("stack underflow".into());
}
let v = stack.remove(len - 3);
stack.push(v);
}
Op::Nip => {
let len = stack.len();
if len < 2 {
return Err("stack underflow".into());
}
stack.remove(len - 2);
}
Op::Tuck => {
let len = stack.len();
if len < 2 {
return Err("stack underflow".into());
}
let v = stack[len - 1].clone();
stack.insert(len - 2, v);
}
Op::Add => binary_op(stack, |a, b| a + b)?,
Op::Sub => binary_op(stack, |a, b| a - b)?,
Op::Mul => binary_op(stack, |a, b| a * b)?,
Op::Div => binary_op(stack, |a, b| a / b)?,
Op::Mod => {
let b = stack.pop().ok_or("stack underflow")?.as_int()?;
let a = stack.pop().ok_or("stack underflow")?.as_int()?;
stack.push(Value::Int(a % b, None));
}
Op::Neg => {
let v = stack.pop().ok_or("stack underflow")?;
match v {
Value::Int(i, s) => stack.push(Value::Int(-i, s)),
Value::Float(f, s) => stack.push(Value::Float(-f, s)),
_ => return Err("expected number".into()),
}
}
Op::Abs => {
let v = stack.pop().ok_or("stack underflow")?;
match v {
Value::Int(i, s) => stack.push(Value::Int(i.abs(), s)),
Value::Float(f, s) => stack.push(Value::Float(f.abs(), s)),
_ => return Err("expected number".into()),
}
}
Op::Floor => {
let v = stack.pop().ok_or("stack underflow")?.as_float()?;
stack.push(Value::Int(v.floor() as i64, None));
}
Op::Ceil => {
let v = stack.pop().ok_or("stack underflow")?.as_float()?;
stack.push(Value::Int(v.ceil() as i64, None));
}
Op::Round => {
let v = stack.pop().ok_or("stack underflow")?.as_float()?;
stack.push(Value::Int(v.round() as i64, None));
}
Op::Min => binary_op(stack, |a, b| a.min(b))?,
Op::Max => binary_op(stack, |a, b| a.max(b))?,
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
Op::Ne => cmp_op(stack, |a, b| (a - b).abs() >= f64::EPSILON)?,
Op::Lt => cmp_op(stack, |a, b| a < b)?,
Op::Gt => cmp_op(stack, |a, b| a > b)?,
Op::Le => cmp_op(stack, |a, b| a <= b)?,
Op::Ge => cmp_op(stack, |a, b| a >= b)?,
Op::And => {
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
stack.push(Value::Int(if a && b { 1 } else { 0 }, None));
}
Op::Or => {
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
stack.push(Value::Int(if a || b { 1 } else { 0 }, None));
}
Op::Not => {
let v = stack.pop().ok_or("stack underflow")?.is_truthy();
stack.push(Value::Int(if v { 0 } else { 1 }, None));
}
Op::BranchIfZero(offset, then_span, else_span) => {
let v = stack.pop().ok_or("stack underflow")?;
if !v.is_truthy() {
if let Some(span) = else_span {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.executed_spans.push(*span);
}
}
pc += offset;
} else if let Some(span) = then_span {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.executed_spans.push(*span);
}
}
}
Op::Branch(offset) => {
pc += offset;
}
Op::NewCmd => {
let name = stack.pop().ok_or("stack underflow")?;
let name = name.as_str()?;
cmd.set_sound(name.to_string());
}
Op::SetParam(param) => {
let val = stack.pop().ok_or("stack underflow")?;
cmd.set_param(param.clone(), val.to_param_string());
}
Op::Emit => {
let (sound, mut params) = cmd.take().ok_or("no sound set")?;
let mut pairs = vec![("sound".into(), sound)];
pairs.append(&mut params);
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
if time_ctx.start > 0.0 {
pairs.push(("delta".into(), time_ctx.start.to_string()));
}
if !pairs.iter().any(|(k, _)| k == "dur") {
pairs.push(("dur".into(), time_ctx.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 * time_ctx.duration).to_string();
} else {
pairs.push(("delaytime".into(), time_ctx.duration.to_string()));
}
outputs.push(format_cmd(&pairs));
}
Op::Get => {
let name = stack.pop().ok_or("stack underflow")?;
let name = name.as_str()?;
let vars = self.vars.lock().unwrap();
let val = vars.get(name).cloned().unwrap_or(Value::Int(0, None));
stack.push(val);
}
Op::Set => {
let name = stack.pop().ok_or("stack underflow")?;
let name = name.as_str()?.to_string();
let val = stack.pop().ok_or("stack underflow")?;
self.vars.lock().unwrap().insert(name, val);
}
Op::GetContext(name) => {
let val = match name.as_str() {
"step" => Value::Int(ctx.step as i64, None),
"beat" => Value::Float(ctx.beat, None),
"pattern" => Value::Int(ctx.pattern as i64, None),
"tempo" => Value::Float(ctx.tempo, None),
"phase" => Value::Float(ctx.phase, None),
"slot" => Value::Int(ctx.slot as i64, None),
"runs" => Value::Int(ctx.runs as i64, None),
"iter" => Value::Int(ctx.iter as i64, None),
"speed" => Value::Float(ctx.speed, None),
"stepdur" => Value::Float(ctx.step_duration(), None),
"fill" => Value::Int(if ctx.fill { 1 } else { 0 }, None),
_ => Value::Int(0, None),
};
stack.push(val);
}
Op::Rand => {
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
let val = self.rng.lock().unwrap().gen_range(min..max);
stack.push(Value::Float(val, None));
}
Op::Rrand => {
let max = stack.pop().ok_or("stack underflow")?.as_int()?;
let min = stack.pop().ok_or("stack underflow")?.as_int()?;
let val = self.rng.lock().unwrap().gen_range(min..=max);
stack.push(Value::Int(val, None));
}
Op::Seed => {
let s = stack.pop().ok_or("stack underflow")?.as_int()?;
*self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64);
}
Op::Cycle => {
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if count == 0 {
return Err("cycle 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();
let idx = ctx.runs % count;
let selected = values[idx].clone();
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Value::Quotation(quot_ops, body_span) = selected {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
} else {
stack.push(selected);
}
}
Op::PCycle => {
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if count == 0 {
return Err("pcycle 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();
let idx = ctx.iter % count;
let selected = values[idx].clone();
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Value::Quotation(quot_ops, body_span) = selected {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
} else {
stack.push(selected);
}
}
Op::Choose => {
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if count == 0 {
return Err("choose 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();
let idx = self.rng.lock().unwrap().gen_range(0..count);
let selected = values[idx].clone();
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Value::Quotation(quot_ops, body_span) = selected {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
} else {
stack.push(selected);
}
}
Op::ChanceExec => {
let prob = stack.pop().ok_or("stack underflow")?.as_float()?;
let quot = stack.pop().ok_or("stack underflow")?;
let val: f64 = self.rng.lock().unwrap().gen();
if val < prob {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
}
}
Op::ProbExec => {
let pct = stack.pop().ok_or("stack underflow")?.as_float()?;
let quot = stack.pop().ok_or("stack underflow")?;
let val: f64 = self.rng.lock().unwrap().gen();
if val < pct / 100.0 {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
}
}
Op::Coin => {
let val: f64 = self.rng.lock().unwrap().gen();
stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None));
}
Op::Every => {
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
if n <= 0 {
return Err("every count must be > 0".into());
}
let result = ctx.iter as i64 % n == 0;
stack.push(Value::Int(if result { 1 } else { 0 }, None));
}
Op::Quotation(quote_ops, body_span) => {
stack.push(Value::Quotation(quote_ops.clone(), *body_span));
}
Op::When => {
let cond = stack.pop().ok_or("stack underflow")?;
let quot = stack.pop().ok_or("stack underflow")?;
if cond.is_truthy() {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
}
}
Op::Unless => {
let cond = stack.pop().ok_or("stack underflow")?;
let quot = stack.pop().ok_or("stack underflow")?;
if !cond.is_truthy() {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
}
}
Op::Mtof => {
let note = stack.pop().ok_or("stack underflow")?.as_float()?;
let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0);
stack.push(Value::Float(freq, None));
}
Op::Ftom => {
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
let note = 69.0 + 12.0 * (freq / 440.0).log2();
stack.push(Value::Float(note, None));
}
Op::At => {
let pos = stack.pop().ok_or("stack underflow")?.as_float()?;
let parent = time_stack.last().ok_or("time stack underflow")?;
let new_start = parent.start + parent.duration * pos;
time_stack.push(TimeContext {
start: new_start,
duration: parent.duration * (1.0 - pos),
subdivisions: None,
iteration_index: parent.iteration_index,
});
}
Op::Window => {
let end = stack.pop().ok_or("stack underflow")?.as_float()?;
let start_pos = stack.pop().ok_or("stack underflow")?.as_float()?;
let parent = time_stack.last().ok_or("time stack underflow")?;
let new_start = parent.start + parent.duration * start_pos;
let new_duration = parent.duration * (end - start_pos);
time_stack.push(TimeContext {
start: new_start,
duration: new_duration,
subdivisions: None,
iteration_index: parent.iteration_index,
});
}
Op::Scale => {
let factor = stack.pop().ok_or("stack underflow")?.as_float()?;
let parent = time_stack.last().ok_or("time stack underflow")?;
time_stack.push(TimeContext {
start: parent.start,
duration: parent.duration * factor,
subdivisions: None,
iteration_index: parent.iteration_index,
});
}
Op::Pop => {
if time_stack.len() <= 1 {
return Err("cannot pop root time context".into());
}
time_stack.pop();
}
Op::Subdivide => {
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if n == 0 {
return Err("subdivide count must be > 0".into());
}
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
let sub_duration = time_ctx.duration / n as f64;
let mut subs = Vec::with_capacity(n);
for i in 0..n {
subs.push((time_ctx.start + sub_duration * i as f64, sub_duration));
}
time_ctx.subdivisions = Some(subs);
}
Op::Each => {
let (sound, params) = cmd.take().ok_or("no sound set")?;
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
let subs = time_ctx
.subdivisions
.as_ref()
.ok_or("each requires subdivide first")?;
for (sub_start, sub_dur) in subs {
let mut pairs = vec![("sound".into(), sound.clone())];
pairs.extend(params.iter().cloned());
if *sub_start > 0.0 {
pairs.push(("delta".into(), sub_start.to_string()));
}
if !pairs.iter().any(|(k, _)| k == "dur") {
pairs.push(("dur".into(), sub_dur.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 * sub_dur).to_string();
} else {
pairs.push(("delaytime".into(), sub_dur.to_string()));
}
outputs.push(format_cmd(&pairs));
}
}
Op::SetTempo => {
let tempo = stack.pop().ok_or("stack underflow")?.as_float()?;
let clamped = tempo.clamp(20.0, 300.0);
self.vars
.lock()
.unwrap()
.insert("__tempo__".to_string(), Value::Float(clamped, 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;
}
values.push(v);
count += 1;
}
values.reverse();
for v in values {
stack.push(v);
}
stack.push(Value::Int(count, None));
}
Op::ListEndCycle => {
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("empty cycle list".into());
}
values.reverse();
let idx = ctx.runs % values.len();
let selected = values[idx].clone();
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Value::Quotation(quot_ops, body_span) = selected {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
} else {
stack.push(selected);
}
}
Op::ListEndPCycle => {
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("empty pattern cycle list".into());
}
values.reverse();
let idx = ctx.iter % values.len();
let selected = values[idx].clone();
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Value::Quotation(quot_ops, body_span) = selected {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
} else {
stack.push(selected);
}
}
Op::Adsr => {
let r = stack.pop().ok_or("stack underflow")?;
let s = stack.pop().ok_or("stack underflow")?;
let d = stack.pop().ok_or("stack underflow")?;
let a = stack.pop().ok_or("stack underflow")?;
cmd.set_param("attack".into(), a.to_param_string());
cmd.set_param("decay".into(), d.to_param_string());
cmd.set_param("sustain".into(), s.to_param_string());
cmd.set_param("release".into(), r.to_param_string());
}
Op::Ad => {
let d = stack.pop().ok_or("stack underflow")?;
let a = stack.pop().ok_or("stack underflow")?;
cmd.set_param("attack".into(), a.to_param_string());
cmd.set_param("decay".into(), d.to_param_string());
cmd.set_param("sustain".into(), "0".into());
}
Op::Stack => {
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if n == 0 {
return Err("stack count must be > 0".into());
}
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
let sub_duration = time_ctx.duration / n as f64;
let mut subs = Vec::with_capacity(n);
for _ in 0..n {
subs.push((time_ctx.start, sub_duration));
}
time_ctx.subdivisions = Some(subs);
}
Op::Echo => {
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if n == 0 {
return Err("echo count must be > 0".into());
}
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
// Geometric series: d1 * (2 - 2^(1-n)) = total
let d1 = time_ctx.duration / (2.0 - 2.0_f64.powi(1 - n as i32));
let mut subs = Vec::with_capacity(n);
for i in 0..n {
let dur = d1 / 2.0_f64.powi(i as i32);
let start = if i == 0 {
time_ctx.start
} else {
time_ctx.start + d1 * (2.0 - 2.0_f64.powi(1 - i as i32))
};
subs.push((start, dur));
}
time_ctx.subdivisions = Some(subs);
}
Op::Necho => {
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if n == 0 {
return Err("necho count must be > 0".into());
}
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
// Reverse geometric: d1 + 2*d1 + 4*d1 + ... = d1 * (2^n - 1) = total
let d1 = time_ctx.duration / (2.0_f64.powi(n as i32) - 1.0);
let mut subs = Vec::with_capacity(n);
for i in 0..n {
let dur = d1 * 2.0_f64.powi(i as i32);
let start = if i == 0 {
time_ctx.start
} else {
// Sum of previous durations: d1 * (2^i - 1)
time_ctx.start + d1 * (2.0_f64.powi(i as i32) - 1.0)
};
subs.push((start, dur));
}
time_ctx.subdivisions = Some(subs);
}
Op::For => {
let quot = stack.pop().ok_or("stack underflow")?;
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
let subs = time_ctx
.subdivisions
.clone()
.ok_or("for requires subdivide first")?;
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);
}
}
for (i, (sub_start, sub_dur)) in subs.iter().enumerate() {
time_stack.push(TimeContext {
start: *sub_start,
duration: *sub_dur,
subdivisions: None,
iteration_index: Some(i),
});
let mut trace_opt = trace_cell.borrow_mut().take();
self.execute_ops(
&quot_ops,
ctx,
stack,
outputs,
time_stack,
cmd,
trace_opt.as_deref_mut(),
)?;
*trace_cell.borrow_mut() = trace_opt;
time_stack.pop();
}
}
_ => return Err("expected quotation".into()),
}
}
Op::LocalCycleEnd => {
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("empty local cycle list".into());
}
values.reverse();
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
let idx = time_ctx.iteration_index.unwrap_or(0) % values.len();
let selected = values[idx].clone();
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Value::Quotation(quot_ops, body_span) = selected {
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
} else {
stack.push(selected);
}
}
Op::Apply => {
let quot = stack.pop().ok_or("stack underflow")?;
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(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
}
Op::Ramp => {
let curve = stack.pop().ok_or("stack underflow")?.as_float()?;
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
let phase = (freq * ctx.beat).fract();
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
let val = phase.powf(curve);
stack.push(Value::Float(val, None));
}
Op::Range => {
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
let val = stack.pop().ok_or("stack underflow")?.as_float()?;
stack.push(Value::Float(min + val * (max - min), None));
}
Op::Noise => {
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
let val = perlin_noise_1d(freq * ctx.beat);
stack.push(Value::Float(val, None));
}
}
pc += 1;
}
Ok(())
}
}
fn perlin_grad(hash_input: i64) -> f64 {
let mut h = (hash_input as u64).wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
h ^= h >> 33;
h = h.wrapping_mul(0xff51afd7ed558ccd);
h ^= h >> 33;
if h & 1 == 0 { 1.0 } else { -1.0 }
}
fn perlin_noise_1d(x: f64) -> f64 {
let x0 = x.floor() as i64;
let t = x - x0 as f64;
let s = t * t * (3.0 - 2.0 * t);
let d0 = perlin_grad(x0) * t;
let d1 = perlin_grad(x0 + 1) * (t - 1.0);
(d0 + s * (d1 - d0)) * 0.5 + 0.5
}
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
where
F: Fn(f64, f64) -> f64,
{
let b = stack.pop().ok_or("stack underflow")?.as_float()?;
let a = stack.pop().ok_or("stack underflow")?.as_float()?;
let result = f(a, b);
if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
stack.push(Value::Int(result as i64, None));
} else {
stack.push(Value::Float(result, None));
}
Ok(())
}
fn cmp_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
where
F: Fn(f64, f64) -> bool,
{
let b = stack.pop().ok_or("stack underflow")?.as_float()?;
let a = stack.pop().ok_or("stack underflow")?.as_float()?;
stack.push(Value::Int(if f(a, b) { 1 } else { 0 }, None));
Ok(())
}
fn format_cmd(pairs: &[(String, String)]) -> String {
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
format!("/{}", parts.join("/"))
}

1705
crates/forth/src/words.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
[package]
name = "cagire-project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -0,0 +1,89 @@
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::project::{Bank, Project};
const VERSION: u8 = 1;
#[derive(Serialize, Deserialize)]
struct ProjectFile {
version: u8,
banks: Vec<Bank>,
#[serde(default)]
sample_paths: Vec<PathBuf>,
#[serde(default = "default_tempo")]
tempo: f64,
}
fn default_tempo() -> f64 {
120.0
}
impl From<&Project> for ProjectFile {
fn from(project: &Project) -> Self {
Self {
version: VERSION,
banks: project.banks.clone(),
sample_paths: project.sample_paths.clone(),
tempo: project.tempo,
}
}
}
impl From<ProjectFile> for Project {
fn from(file: ProjectFile) -> Self {
Self {
banks: file.banks,
sample_paths: file.sample_paths,
tempo: file.tempo,
}
}
}
#[derive(Debug)]
pub enum FileError {
Io(io::Error),
Json(serde_json::Error),
Version(u8),
}
impl std::fmt::Display for FileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileError::Io(e) => write!(f, "IO error: {e}"),
FileError::Json(e) => write!(f, "JSON error: {e}"),
FileError::Version(v) => write!(f, "Unsupported version: {v}"),
}
}
}
impl From<io::Error> for FileError {
fn from(e: io::Error) -> Self {
FileError::Io(e)
}
}
impl From<serde_json::Error> for FileError {
fn from(e: serde_json::Error) -> Self {
FileError::Json(e)
}
}
pub fn save(project: &Project, path: &Path) -> Result<(), FileError> {
let file = ProjectFile::from(project);
let json = serde_json::to_string_pretty(&file)?;
fs::write(path, json)?;
Ok(())
}
pub fn load(path: &Path) -> Result<Project, FileError> {
let json = fs::read_to_string(path)?;
let file: ProjectFile = serde_json::from_str(&json)?;
if file.version > VERSION {
return Err(FileError::Version(file.version));
}
Ok(Project::from(file))
}

10
crates/project/src/lib.rs Normal file
View File

@@ -0,0 +1,10 @@
mod file;
mod project;
pub const MAX_BANKS: usize = 16;
pub const MAX_PATTERNS: usize = 16;
pub const MAX_STEPS: usize = 128;
pub const DEFAULT_LENGTH: usize = 16;
pub use file::{load, save, FileError};
pub use project::{Bank, Pattern, PatternSpeed, Project, Step};

View File

@@ -0,0 +1,210 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq)]
pub enum PatternSpeed {
Eighth, // 1/8x
Quarter, // 1/4x
Half, // 1/2x
#[default]
Normal, // 1x
Double, // 2x
Quad, // 4x
Octo, // 8x
}
impl PatternSpeed {
pub fn multiplier(&self) -> f64 {
match self {
Self::Eighth => 0.125,
Self::Quarter => 0.25,
Self::Half => 0.5,
Self::Normal => 1.0,
Self::Double => 2.0,
Self::Quad => 4.0,
Self::Octo => 8.0,
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Eighth => "1/8x",
Self::Quarter => "1/4x",
Self::Half => "1/2x",
Self::Normal => "1x",
Self::Double => "2x",
Self::Quad => "4x",
Self::Octo => "8x",
}
}
pub fn next(&self) -> Self {
match self {
Self::Eighth => Self::Quarter,
Self::Quarter => Self::Half,
Self::Half => Self::Normal,
Self::Normal => Self::Double,
Self::Double => Self::Quad,
Self::Quad => Self::Octo,
Self::Octo => Self::Octo,
}
}
pub fn prev(&self) -> Self {
match self {
Self::Eighth => Self::Eighth,
Self::Quarter => Self::Eighth,
Self::Half => Self::Quarter,
Self::Normal => Self::Half,
Self::Double => Self::Normal,
Self::Quad => Self::Double,
Self::Octo => Self::Quad,
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s.trim() {
"1/8x" | "1/8" | "0.125x" => Some(Self::Eighth),
"1/4x" | "1/4" | "0.25x" => Some(Self::Quarter),
"1/2x" | "1/2" | "0.5x" => Some(Self::Half),
"1x" | "1" => Some(Self::Normal),
"2x" | "2" => Some(Self::Double),
"4x" | "4" => Some(Self::Quad),
"8x" | "8" => Some(Self::Octo),
_ => None,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Step {
pub active: bool,
pub script: String,
#[serde(skip)]
pub command: Option<String>,
#[serde(default)]
pub source: Option<usize>,
}
impl Default for Step {
fn default() -> Self {
Self {
active: true,
script: String::new(),
command: None,
source: None,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Pattern {
pub steps: Vec<Step>,
pub length: usize,
#[serde(default)]
pub speed: PatternSpeed,
#[serde(default)]
pub name: Option<String>,
}
impl Default for Pattern {
fn default() -> Self {
Self {
steps: (0..MAX_STEPS).map(|_| Step::default()).collect(),
length: DEFAULT_LENGTH,
speed: PatternSpeed::default(),
name: None,
}
}
}
impl Pattern {
pub fn step(&self, index: usize) -> Option<&Step> {
self.steps.get(index)
}
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
self.steps.get_mut(index)
}
pub fn set_length(&mut self, length: usize) {
let length = length.clamp(1, MAX_STEPS);
while self.steps.len() < length {
self.steps.push(Step::default());
}
self.length = length;
}
pub fn resolve_source(&self, index: usize) -> usize {
let mut current = index;
for _ in 0..self.steps.len() {
if let Some(step) = self.steps.get(current) {
if let Some(source) = step.source {
current = source;
} else {
return current;
}
} else {
return index;
}
}
index
}
pub fn resolve_script(&self, index: usize) -> Option<&str> {
let source_idx = self.resolve_source(index);
self.steps.get(source_idx).map(|s| s.script.as_str())
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Bank {
pub patterns: Vec<Pattern>,
#[serde(default)]
pub name: Option<String>,
}
impl Default for Bank {
fn default() -> Self {
Self {
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
name: None,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Project {
pub banks: Vec<Bank>,
#[serde(default)]
pub sample_paths: Vec<PathBuf>,
#[serde(default = "default_tempo")]
pub tempo: f64,
}
fn default_tempo() -> f64 {
120.0
}
impl Default for Project {
fn default() -> Self {
Self {
banks: (0..MAX_BANKS).map(|_| Bank::default()).collect(),
sample_paths: Vec::new(),
tempo: default_tempo(),
}
}
}
impl Project {
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
&self.banks[bank].patterns[pattern]
}
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
&mut self.banks[bank].patterns[pattern]
}
}

View File

@@ -0,0 +1,7 @@
[package]
name = "cagire-ratatui"
version = "0.1.0"
edition = "2021"
[dependencies]
ratatui = "0.29"

View File

@@ -0,0 +1,60 @@
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use super::ModalFrame;
pub struct ConfirmModal<'a> {
title: &'a str,
message: &'a str,
selected: bool,
}
impl<'a> ConfirmModal<'a> {
pub fn new(title: &'a str, message: &'a str, selected: bool) -> Self {
Self {
title,
message,
selected,
}
}
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
let inner = ModalFrame::new(self.title)
.width(30)
.height(5)
.border_color(Color::Yellow)
.render_centered(frame, term);
let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
frame.render_widget(
Paragraph::new(self.message).alignment(Alignment::Center),
rows[0],
);
let yes_style = if self.selected {
Style::new().fg(Color::Black).bg(Color::Yellow)
} else {
Style::default()
};
let no_style = if !self.selected {
Style::new().fg(Color::Black).bg(Color::Yellow)
} else {
Style::default()
};
let buttons = Line::from(vec![
Span::styled(" Yes ", yes_style),
Span::raw(" "),
Span::styled(" No ", no_style),
]);
frame.render_widget(
Paragraph::new(buttons).alignment(Alignment::Center),
rows[1],
);
}
}

13
crates/ratatui/src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
mod confirm;
mod modal;
mod scope;
mod spectrum;
mod text_input;
mod vu_meter;
pub use confirm::ConfirmModal;
pub use modal::ModalFrame;
pub use scope::{Orientation, Scope};
pub use spectrum::Spectrum;
pub use text_input::TextInputModal;
pub use vu_meter::VuMeter;

View File

@@ -0,0 +1,58 @@
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Clear};
use ratatui::Frame;
pub struct ModalFrame<'a> {
title: &'a str,
width: u16,
height: u16,
border_color: Color,
}
impl<'a> ModalFrame<'a> {
pub fn new(title: &'a str) -> Self {
Self {
title,
width: 40,
height: 5,
border_color: Color::White,
}
}
pub fn width(mut self, w: u16) -> Self {
self.width = w;
self
}
pub fn height(mut self, h: u16) -> Self {
self.height = h;
self
}
pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn render_centered(&self, frame: &mut Frame, term: Rect) -> Rect {
let width = self.width.min(term.width.saturating_sub(4));
let height = self.height.min(term.height.saturating_sub(4));
let x = term.x + (term.width.saturating_sub(width)) / 2;
let y = term.y + (term.height.saturating_sub(height)) / 2;
let area = Rect::new(x, y, width, height);
frame.render_widget(Clear, area);
let block = Block::default()
.borders(Borders::ALL)
.title(self.title)
.border_style(Style::new().fg(self.border_color));
let inner = block.inner(area);
frame.render_widget(block, area);
inner
}
}

149
crates/ratatui/src/scope.rs Normal file
View File

@@ -0,0 +1,149 @@
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
#[allow(dead_code)]
pub enum Orientation {
Horizontal,
Vertical,
}
pub struct Scope<'a> {
data: &'a [f32],
orientation: Orientation,
color: Color,
gain: f32,
}
impl<'a> Scope<'a> {
pub fn new(data: &'a [f32]) -> Self {
Self {
data,
orientation: Orientation::Horizontal,
color: Color::Green,
gain: 1.0,
}
}
pub fn orientation(mut self, o: Orientation) -> Self {
self.orientation = o;
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = c;
self
}
}
impl Widget for Scope<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 || self.data.is_empty() {
return;
}
match self.orientation {
Orientation::Horizontal => {
render_horizontal(self.data, area, buf, self.color, self.gain)
}
Orientation::Vertical => render_vertical(self.data, area, buf, self.color, self.gain),
}
}
}
fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
let width = area.width as usize;
let height = area.height as usize;
let fine_width = width * 2;
let fine_height = height * 4;
let mut patterns = vec![0u8; width * height];
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 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!(),
};
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);
}
}
}
}
fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
let width = area.width as usize;
let height = area.height as usize;
let fine_width = width * 2;
let fine_height = height * 4;
let mut patterns = vec![0u8; width * height];
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 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!(),
};
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);
}
}
}
}

View File

@@ -0,0 +1,62 @@
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
pub struct Spectrum<'a> {
data: &'a [f32; 32],
}
impl<'a> Spectrum<'a> {
pub fn new(data: &'a [f32; 32]) -> Self {
Self { data }
}
}
impl Widget for Spectrum<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
let height = area.height as f32;
let band_width = area.width as usize / 32;
if band_width == 0 {
return;
}
for (band, &mag) in self.data.iter().enumerate() {
let bar_height = mag * height;
let full_cells = bar_height as usize;
let frac = bar_height - full_cells as f32;
let frac_idx = (frac * 8.0) as usize;
let x_start = area.x + (band * band_width) as u16;
for row in 0..area.height as usize {
let y = area.y + area.height - 1 - row as u16;
let ratio = row as f32 / area.height as f32;
let color = if ratio < 0.33 {
Color::Rgb(40, 180, 80)
} else if ratio < 0.66 {
Color::Rgb(220, 180, 40)
} else {
Color::Rgb(220, 60, 40)
};
for dx in 0..band_width as u16 {
let x = x_start + dx;
if x >= area.x + area.width {
break;
}
if row < full_cells {
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
} else if row == full_cells && frac_idx > 0 {
buf[(x, y)].set_char(BLOCKS[frac_idx - 1]).set_fg(color);
}
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use super::ModalFrame;
pub struct TextInputModal<'a> {
title: &'a str,
input: &'a str,
hint: Option<&'a str>,
border_color: Color,
width: u16,
}
impl<'a> TextInputModal<'a> {
pub fn new(title: &'a str, input: &'a str) -> Self {
Self {
title,
input,
hint: None,
border_color: Color::White,
width: 50,
}
}
pub fn hint(mut self, h: &'a str) -> Self {
self.hint = Some(h);
self
}
pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn width(mut self, w: u16) -> Self {
self.width = w;
self
}
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
let height = if self.hint.is_some() { 6 } else { 5 };
let inner = ModalFrame::new(self.title)
.width(self.width)
.height(height)
.border_color(self.border_color)
.render_centered(frame, term);
if self.hint.is_some() {
let rows =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(Color::Cyan)),
Span::styled("", Style::new().fg(Color::White)),
])),
rows[0],
);
if let Some(hint) = self.hint {
frame.render_widget(
Paragraph::new(Span::styled(hint, Style::new().fg(Color::DarkGray))),
rows[1],
);
}
} else {
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(Color::Cyan)),
Span::styled("", Style::new().fg(Color::White)),
])),
inner,
);
}
}
}

View File

@@ -0,0 +1,81 @@
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
const DB_MIN: f32 = -48.0;
const DB_MAX: f32 = 3.0;
const DB_RANGE: f32 = DB_MAX - DB_MIN;
pub struct VuMeter {
left: f32,
right: f32,
}
impl VuMeter {
pub fn new(left: f32, right: f32) -> Self {
Self { left, right }
}
fn amplitude_to_db(amp: f32) -> f32 {
if amp <= 0.0 {
DB_MIN
} else {
(20.0 * amp.log10()).clamp(DB_MIN, DB_MAX)
}
}
fn db_to_normalized(db: f32) -> f32 {
(db - DB_MIN) / DB_RANGE
}
fn row_to_color(row_position: f32) -> Color {
if row_position > 0.9 {
Color::Red
} else if row_position > 0.75 {
Color::Yellow
} else {
Color::Green
}
}
}
impl Widget for VuMeter {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 3 || area.height == 0 {
return;
}
let height = area.height as usize;
let half_width = area.width / 2;
let gap = 1u16;
let left_db = Self::amplitude_to_db(self.left);
let right_db = Self::amplitude_to_db(self.right);
let left_norm = Self::db_to_normalized(left_db);
let right_norm = Self::db_to_normalized(right_db);
let left_rows = (left_norm * height as f32).round() as usize;
let right_rows = (right_norm * height as f32).round() as usize;
for row in 0..height {
let y = area.y + area.height - 1 - row as u16;
let row_position = (row as f32 + 0.5) / height as f32;
let color = Self::row_to_color(row_position);
for col in 0..half_width.saturating_sub(gap) {
let x = area.x + col;
if row < left_rows {
buf[(x, y)].set_char(' ').set_bg(color);
}
}
for col in 0..half_width.saturating_sub(gap) {
let x = area.x + half_width + gap + col;
if x < area.x + area.width && row < right_rows {
buf[(x, y)].set_char(' ').set_bg(color);
}
}
}
}
}