Feat: new euclidean words and sugar for floating point numbers
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
This commit is contained in:
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
|
|||||||
- TachyonFX based animations
|
- TachyonFX based animations
|
||||||
- Prelude: project-level Forth script for persistent word definitions. Press `d` to edit, `D` to re-evaluate. Runs automatically on playback start and project load.
|
- Prelude: project-level Forth script for persistent word definitions. Press `d` to edit, `D` to re-evaluate. Runs automatically on playback start and project load.
|
||||||
- Varargs stack words: `rev`, `shuffle`, `sort` (ascending), `rsort` (descending), `sum`, `prod`. All take a count and operate on the top n items.
|
- Varargs stack words: `rev`, `shuffle`, `sort` (ascending), `rsort` (descending), `sum`, `prod`. All take a count and operate on the top n items.
|
||||||
|
- Euclidean rhythm words: `euclid` (k n -- hits) distributes k hits across n steps, `euclidrot` (k n r -- hits) adds rotation offset.
|
||||||
|
- Shorthand float syntax: `.25` parses as `0.25`, `-.5` parses as `-0.5`.
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -86,9 +86,25 @@ fn tokenize(input: &str) -> Vec<Token> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let span = SourceSpan { start, end };
|
let span = SourceSpan { start, end };
|
||||||
if let Ok(i) = word.parse::<i64>() {
|
|
||||||
|
// Normalize shorthand float syntax: .25 -> 0.25, -.5 -> -0.5
|
||||||
|
let word_to_parse = if word.starts_with('.')
|
||||||
|
&& word.len() > 1
|
||||||
|
&& word.as_bytes()[1].is_ascii_digit()
|
||||||
|
{
|
||||||
|
format!("0{word}")
|
||||||
|
} else if word.starts_with("-.")
|
||||||
|
&& word.len() > 2
|
||||||
|
&& word.as_bytes()[2].is_ascii_digit()
|
||||||
|
{
|
||||||
|
format!("-0{}", &word[1..])
|
||||||
|
} else {
|
||||||
|
word.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(i) = word_to_parse.parse::<i64>() {
|
||||||
tokens.push(Token::Int(i, span));
|
tokens.push(Token::Int(i, span));
|
||||||
} else if let Ok(f) = word.parse::<f64>() {
|
} else if let Ok(f) = word_to_parse.parse::<f64>() {
|
||||||
tokens.push(Token::Float(f, span));
|
tokens.push(Token::Float(f, span));
|
||||||
} else {
|
} else {
|
||||||
tokens.push(Token::Word(word, span));
|
tokens.push(Token::Word(word, span));
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ pub enum Op {
|
|||||||
StepRange,
|
StepRange,
|
||||||
Generate,
|
Generate,
|
||||||
GeomRange,
|
GeomRange,
|
||||||
|
Euclid,
|
||||||
|
EuclidRot,
|
||||||
Times,
|
Times,
|
||||||
Chord(&'static [i64]),
|
Chord(&'static [i64]),
|
||||||
// MIDI
|
// MIDI
|
||||||
|
|||||||
@@ -1025,6 +1025,29 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Op::Euclid => {
|
||||||
|
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let k = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
if k < 0 || n < 0 {
|
||||||
|
return Err("euclid: k and n must be >= 0".into());
|
||||||
|
}
|
||||||
|
for idx in euclidean_rhythm(k as usize, n as usize, 0) {
|
||||||
|
stack.push(Value::Int(idx, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::EuclidRot => {
|
||||||
|
let r = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let k = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
if k < 0 || n < 0 || r < 0 {
|
||||||
|
return Err("euclidrot: k, n, and r must be >= 0".into());
|
||||||
|
}
|
||||||
|
for idx in euclidean_rhythm(k as usize, n as usize, r as usize) {
|
||||||
|
stack.push(Value::Int(idx, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MIDI operations
|
// MIDI operations
|
||||||
Op::MidiEmit => {
|
Op::MidiEmit => {
|
||||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||||
@@ -1216,6 +1239,58 @@ fn emit_output(
|
|||||||
outputs.push(out);
|
outputs.push(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
|
||||||
|
if k == 0 || n == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
if k >= n {
|
||||||
|
return (0..n as i64).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut groups: Vec<Vec<bool>> = (0..k).map(|_| vec![true]).collect();
|
||||||
|
groups.extend((0..(n - k)).map(|_| vec![false]));
|
||||||
|
|
||||||
|
while groups.len() > 1 {
|
||||||
|
let ones_count = groups.iter().filter(|g| g[0]).count();
|
||||||
|
let zeros_count = groups.len() - ones_count;
|
||||||
|
|
||||||
|
if zeros_count == 0 || ones_count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_count = ones_count.min(zeros_count);
|
||||||
|
let mut new_groups = Vec::with_capacity(groups.len() - min_count);
|
||||||
|
|
||||||
|
let (mut ones, mut zeros): (Vec<_>, Vec<_>) =
|
||||||
|
groups.into_iter().partition(|g| g[0]);
|
||||||
|
|
||||||
|
for _ in 0..min_count {
|
||||||
|
let mut one = ones.pop().unwrap();
|
||||||
|
one.extend(zeros.pop().unwrap());
|
||||||
|
new_groups.push(one);
|
||||||
|
}
|
||||||
|
new_groups.extend(ones);
|
||||||
|
new_groups.extend(zeros);
|
||||||
|
|
||||||
|
groups = new_groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pattern: Vec<bool> = groups.into_iter().flatten().collect();
|
||||||
|
|
||||||
|
let rotated = if rotation > 0 && !pattern.is_empty() {
|
||||||
|
let r = rotation % pattern.len();
|
||||||
|
pattern.iter().cycle().skip(r).take(pattern.len()).copied().collect()
|
||||||
|
} else {
|
||||||
|
pattern
|
||||||
|
};
|
||||||
|
|
||||||
|
rotated
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, hit)| if hit { Some(i as i64) } else { None })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn perlin_grad(hash_input: i64) -> f64 {
|
fn perlin_grad(hash_input: i64) -> f64 {
|
||||||
let mut h = (hash_input as u64)
|
let mut h = (hash_input as u64)
|
||||||
.wrapping_mul(6364136223846793005)
|
.wrapping_mul(6364136223846793005)
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
".," => Op::StepRange,
|
".," => Op::StepRange,
|
||||||
"gen" => Op::Generate,
|
"gen" => Op::Generate,
|
||||||
"geom.." => Op::GeomRange,
|
"geom.." => Op::GeomRange,
|
||||||
|
"euclid" => Op::Euclid,
|
||||||
|
"euclidrot" => Op::EuclidRot,
|
||||||
"times" => Op::Times,
|
"times" => Op::Times,
|
||||||
"m." => Op::MidiEmit,
|
"m." => Op::MidiEmit,
|
||||||
"ccval" => Op::GetMidiCC,
|
"ccval" => Op::GetMidiCC,
|
||||||
|
|||||||
@@ -420,4 +420,24 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "euclid",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Generator",
|
||||||
|
stack: "(k n -- i1 i2 ... ik)",
|
||||||
|
desc: "Push indices for k hits evenly distributed over n steps",
|
||||||
|
example: "4 8 euclid => 0 2 4 6",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "euclidrot",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Generator",
|
||||||
|
stack: "(k n r -- i1 i2 ... ik)",
|
||||||
|
desc: "Push Euclidean indices with rotation r",
|
||||||
|
example: "3 8 2 euclidrot => 1 4 6",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -57,3 +57,6 @@ mod midi;
|
|||||||
|
|
||||||
#[path = "forth/chords.rs"]
|
#[path = "forth/chords.rs"]
|
||||||
mod chords;
|
mod chords;
|
||||||
|
|
||||||
|
#[path = "forth/euclidean.rs"]
|
||||||
|
mod euclidean;
|
||||||
|
|||||||
@@ -200,3 +200,13 @@ fn log_e() {
|
|||||||
fn log_one() {
|
fn log_one() {
|
||||||
expect_int("1 log", 0);
|
expect_int("1 log", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shorthand_float() {
|
||||||
|
expect_float(".25 .5 +", 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shorthand_float_negative() {
|
||||||
|
expect_float("-.5 1 +", 0.5);
|
||||||
|
}
|
||||||
|
|||||||
105
tests/forth/euclidean.rs
Normal file
105
tests/forth/euclidean.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use super::harness::*;
|
||||||
|
use cagire::forth::Value;
|
||||||
|
|
||||||
|
fn int(n: i64) -> Value {
|
||||||
|
Value::Int(n, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_4_8() {
|
||||||
|
expect_stack("4 8 euclid", &[int(0), int(2), int(4), int(6)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_3_8_tresillo() {
|
||||||
|
expect_stack("3 8 euclid", &[int(0), int(3), int(6)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_5_8_cinquillo() {
|
||||||
|
expect_stack("5 8 euclid", &[int(0), int(2), int(4), int(6), int(7)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_4_16() {
|
||||||
|
expect_stack("4 16 euclid", &[int(0), int(4), int(8), int(12)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_5_16() {
|
||||||
|
expect_stack("5 16 euclid", &[int(0), int(4), int(7), int(10), int(13)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_zero_hits() {
|
||||||
|
expect_stack("0 8 euclid", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_zero_steps() {
|
||||||
|
expect_stack("4 0 euclid", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_k_equals_n() {
|
||||||
|
expect_stack("4 4 euclid", &[int(0), int(1), int(2), int(3)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_k_greater_than_n() {
|
||||||
|
expect_stack("8 4 euclid", &[int(0), int(1), int(2), int(3)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_single_hit() {
|
||||||
|
expect_stack("1 8 euclid", &[int(0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_negative_k() {
|
||||||
|
expect_error("-1 8 euclid", "k and n must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_negative_n() {
|
||||||
|
expect_error("4 -8 euclid", "k and n must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_underflow() {
|
||||||
|
expect_error("8 euclid", "stack underflow");
|
||||||
|
expect_error("euclid", "stack underflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclidrot_basic() {
|
||||||
|
expect_stack("3 8 1 euclidrot", &[int(2), int(5), int(7)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclidrot_zero_rotation() {
|
||||||
|
expect_stack("3 8 0 euclidrot", &[int(0), int(3), int(6)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclidrot_full_rotation() {
|
||||||
|
expect_stack("3 8 8 euclidrot", &[int(0), int(3), int(6)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclidrot_negative_r() {
|
||||||
|
expect_error("3 8 -1 euclidrot", "k, n, and r must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclidrot_underflow() {
|
||||||
|
expect_error("8 1 euclidrot", "stack underflow");
|
||||||
|
expect_error("1 euclidrot", "stack underflow");
|
||||||
|
expect_error("euclidrot", "stack underflow");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn euclid_with_at() {
|
||||||
|
let outputs = expect_outputs("4 8 euclid at kick sound .", 4);
|
||||||
|
assert_eq!(outputs.len(), 4);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user