2 Commits

Author SHA1 Message Date
adee8d0d57 Feat: adding some basic music theory
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-01 16:15:09 +01:00
f9c284effd Feat: adding logrand and exprand 2026-02-01 15:16:20 +01:00
8 changed files with 732 additions and 1 deletions

View File

@@ -53,6 +53,8 @@ pub enum Op {
Set,
GetContext(String),
Rand,
ExpRand,
LogRand,
Seed,
Cycle,
PCycle,
@@ -86,6 +88,7 @@ pub enum Op {
Generate,
GeomRange,
Times,
Chord(&'static [i64]),
// MIDI
MidiEmit,
GetMidiCC,

View File

@@ -0,0 +1,129 @@
pub struct Chord {
pub name: &'static str,
pub intervals: &'static [i64],
}
pub static CHORDS: &[Chord] = &[
// Triads
Chord {
name: "maj",
intervals: &[0, 4, 7],
},
Chord {
name: "m",
intervals: &[0, 3, 7],
},
Chord {
name: "dim",
intervals: &[0, 3, 6],
},
Chord {
name: "aug",
intervals: &[0, 4, 8],
},
Chord {
name: "sus2",
intervals: &[0, 2, 7],
},
Chord {
name: "sus4",
intervals: &[0, 5, 7],
},
// Seventh chords
Chord {
name: "maj7",
intervals: &[0, 4, 7, 11],
},
Chord {
name: "min7",
intervals: &[0, 3, 7, 10],
},
Chord {
name: "dom7",
intervals: &[0, 4, 7, 10],
},
Chord {
name: "dim7",
intervals: &[0, 3, 6, 9],
},
Chord {
name: "m7b5",
intervals: &[0, 3, 6, 10],
},
Chord {
name: "minmaj7",
intervals: &[0, 3, 7, 11],
},
Chord {
name: "aug7",
intervals: &[0, 4, 8, 10],
},
// Sixth chords
Chord {
name: "maj6",
intervals: &[0, 4, 7, 9],
},
Chord {
name: "min6",
intervals: &[0, 3, 7, 9],
},
// Extended chords
Chord {
name: "dom9",
intervals: &[0, 4, 7, 10, 14],
},
Chord {
name: "maj9",
intervals: &[0, 4, 7, 11, 14],
},
Chord {
name: "min9",
intervals: &[0, 3, 7, 10, 14],
},
Chord {
name: "dom11",
intervals: &[0, 4, 7, 10, 14, 17],
},
Chord {
name: "min11",
intervals: &[0, 3, 7, 10, 14, 17],
},
Chord {
name: "dom13",
intervals: &[0, 4, 7, 10, 14, 21],
},
// Add chords
Chord {
name: "add9",
intervals: &[0, 4, 7, 14],
},
Chord {
name: "add11",
intervals: &[0, 4, 7, 17],
},
Chord {
name: "madd9",
intervals: &[0, 3, 7, 14],
},
// Altered dominants
Chord {
name: "dom7b9",
intervals: &[0, 4, 7, 10, 13],
},
Chord {
name: "dom7s9",
intervals: &[0, 4, 7, 10, 15],
},
Chord {
name: "dom7b5",
intervals: &[0, 4, 6, 10],
},
Chord {
name: "dom7s5",
intervals: &[0, 4, 8, 10],
},
];
pub fn lookup(name: &str) -> Option<&'static [i64]> {
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
}

View File

@@ -1,3 +1,4 @@
pub mod chords;
mod scales;
pub use scales::lookup;

View File

@@ -455,11 +455,37 @@ impl Forth {
let a_f = a.as_float()?;
let b_f = b.as_float()?;
let (lo, hi) = if a_f <= b_f { (a_f, b_f) } else { (b_f, a_f) };
let val = self.rng.lock().unwrap().gen_range(lo..hi);
let val = if (hi - lo).abs() < f64::EPSILON {
lo
} else {
self.rng.lock().unwrap().gen_range(lo..hi)
};
stack.push(Value::Float(val, None));
}
}
}
Op::ExpRand => {
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
let lo = stack.pop().ok_or("stack underflow")?.as_float()?;
if lo <= 0.0 || hi <= 0.0 {
return Err("exprand requires positive values".into());
}
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().unwrap().gen();
let val = lo * (hi / lo).powf(u);
stack.push(Value::Float(val, None));
}
Op::LogRand => {
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
let lo = stack.pop().ok_or("stack underflow")?.as_float()?;
if lo <= 0.0 || hi <= 0.0 {
return Err("logrand requires positive values".into());
}
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().unwrap().gen();
let val = hi * (lo / hi).powf(u);
stack.push(Value::Float(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);
@@ -607,6 +633,13 @@ impl Forth {
stack.push(result);
}
Op::Chord(intervals) => {
let root = stack.pop().ok_or("stack underflow")?.as_int()?;
for &interval in *intervals {
stack.push(Value::Int(root + interval, None));
}
}
Op::Oct => {
let shift = stack.pop().ok_or("stack underflow")?;
let note = stack.pop().ok_or("stack underflow")?;

View File

@@ -473,6 +473,26 @@ pub const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "exprand",
aliases: &[],
category: "Probability",
stack: "(lo hi -- f)",
desc: "Exponential random biased toward lo. Both args must be positive",
example: "1.0 100.0 exprand => 3.7",
compile: Simple,
varargs: false,
},
Word {
name: "logrand",
aliases: &[],
category: "Probability",
stack: "(lo hi -- f)",
desc: "Exponential random biased toward hi. Both args must be positive",
example: "1.0 100.0 logrand => 87.2",
compile: Simple,
varargs: false,
},
Word {
name: "seed",
aliases: &[],
@@ -810,6 +830,292 @@ pub const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
// Chords - Triads
Word {
name: "maj",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth)",
desc: "Major triad",
example: "c4 maj => 60 64 67",
compile: Simple,
varargs: true,
},
Word {
name: "m",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth)",
desc: "Minor triad",
example: "c4 m => 60 63 67",
compile: Simple,
varargs: true,
},
Word {
name: "dim",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth)",
desc: "Diminished triad",
example: "c4 dim => 60 63 66",
compile: Simple,
varargs: true,
},
Word {
name: "aug",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth)",
desc: "Augmented triad",
example: "c4 aug => 60 64 68",
compile: Simple,
varargs: true,
},
Word {
name: "sus2",
aliases: &[],
category: "Chord",
stack: "(root -- root second fifth)",
desc: "Suspended 2nd",
example: "c4 sus2 => 60 62 67",
compile: Simple,
varargs: true,
},
Word {
name: "sus4",
aliases: &[],
category: "Chord",
stack: "(root -- root fourth fifth)",
desc: "Suspended 4th",
example: "c4 sus4 => 60 65 67",
compile: Simple,
varargs: true,
},
// Chords - Seventh
Word {
name: "maj7",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh)",
desc: "Major 7th",
example: "c4 maj7 => 60 64 67 71",
compile: Simple,
varargs: true,
},
Word {
name: "min7",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh)",
desc: "Minor 7th",
example: "c4 min7 => 60 63 67 70",
compile: Simple,
varargs: true,
},
Word {
name: "dom7",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh)",
desc: "Dominant 7th",
example: "c4 dom7 => 60 64 67 70",
compile: Simple,
varargs: true,
},
Word {
name: "dim7",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh)",
desc: "Diminished 7th",
example: "c4 dim7 => 60 63 66 69",
compile: Simple,
varargs: true,
},
Word {
name: "m7b5",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh)",
desc: "Half-diminished (min7b5)",
example: "c4 m7b5 => 60 63 66 70",
compile: Simple,
varargs: true,
},
Word {
name: "minmaj7",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh)",
desc: "Minor-major 7th",
example: "c4 minmaj7 => 60 63 67 71",
compile: Simple,
varargs: true,
},
Word {
name: "aug7",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh)",
desc: "Augmented 7th",
example: "c4 aug7 => 60 64 68 70",
compile: Simple,
varargs: true,
},
// Chords - Sixth
Word {
name: "maj6",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth sixth)",
desc: "Major 6th",
example: "c4 maj6 => 60 64 67 69",
compile: Simple,
varargs: true,
},
Word {
name: "min6",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth sixth)",
desc: "Minor 6th",
example: "c4 min6 => 60 63 67 69",
compile: Simple,
varargs: true,
},
// Chords - Extended
Word {
name: "dom9",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh ninth)",
desc: "Dominant 9th",
example: "c4 dom9 => 60 64 67 70 74",
compile: Simple,
varargs: true,
},
Word {
name: "maj9",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh ninth)",
desc: "Major 9th",
example: "c4 maj9 => 60 64 67 71 74",
compile: Simple,
varargs: true,
},
Word {
name: "min9",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh ninth)",
desc: "Minor 9th",
example: "c4 min9 => 60 63 67 70 74",
compile: Simple,
varargs: true,
},
Word {
name: "dom11",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh ninth eleventh)",
desc: "Dominant 11th",
example: "c4 dom11 => 60 64 67 70 74 77",
compile: Simple,
varargs: true,
},
Word {
name: "min11",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh ninth eleventh)",
desc: "Minor 11th",
example: "c4 min11 => 60 63 67 70 74 77",
compile: Simple,
varargs: true,
},
Word {
name: "dom13",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh ninth thirteenth)",
desc: "Dominant 13th",
example: "c4 dom13 => 60 64 67 70 74 81",
compile: Simple,
varargs: true,
},
// Chords - Add
Word {
name: "add9",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth ninth)",
desc: "Major add 9",
example: "c4 add9 => 60 64 67 74",
compile: Simple,
varargs: true,
},
Word {
name: "add11",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth eleventh)",
desc: "Major add 11",
example: "c4 add11 => 60 64 67 77",
compile: Simple,
varargs: true,
},
Word {
name: "madd9",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth ninth)",
desc: "Minor add 9",
example: "c4 madd9 => 60 63 67 74",
compile: Simple,
varargs: true,
},
// Chords - Altered dominants
Word {
name: "dom7b9",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh flatninth)",
desc: "7th flat 9",
example: "c4 dom7b9 => 60 64 67 70 73",
compile: Simple,
varargs: true,
},
Word {
name: "dom7s9",
aliases: &[],
category: "Chord",
stack: "(root -- root third fifth seventh sharpninth)",
desc: "7th sharp 9 (Hendrix chord)",
example: "c4 dom7s9 => 60 64 67 70 75",
compile: Simple,
varargs: true,
},
Word {
name: "dom7b5",
aliases: &[],
category: "Chord",
stack: "(root -- root third flatfifth seventh)",
desc: "7th flat 5",
example: "c4 dom7b5 => 60 64 66 70",
compile: Simple,
varargs: true,
},
Word {
name: "dom7s5",
aliases: &[],
category: "Chord",
stack: "(root -- root third sharpfifth seventh)",
desc: "7th sharp 5",
example: "c4 dom7s5 => 60 64 68 70",
compile: Simple,
varargs: true,
},
// LFO
Word {
name: "ramp",
@@ -2508,6 +2814,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"sound" => Op::NewCmd,
"." => Op::Emit,
"rand" => Op::Rand,
"exprand" => Op::ExpRand,
"logrand" => Op::LogRand,
"seed" => Op::Seed,
"cycle" => Op::Cycle,
"pcycle" => Op::PCycle,
@@ -2654,6 +2962,11 @@ pub(super) fn compile_word(
return true;
}
if let Some(intervals) = theory::chords::lookup(name) {
ops.push(Op::Chord(intervals));
return true;
}
if let Some(word) = lookup_word(name) {
match &word.compile {
Simple => {

View File

@@ -54,3 +54,6 @@ mod generator;
#[path = "forth/midi.rs"]
mod midi;
#[path = "forth/chords.rs"]
mod chords;

178
tests/forth/chords.rs Normal file
View File

@@ -0,0 +1,178 @@
use cagire::forth::Value;
use super::harness::{expect_stack, run};
fn ints(vals: &[i64]) -> Vec<Value> {
vals.iter().map(|&v| Value::Int(v, None)).collect()
}
// Triads
#[test]
fn chord_major() {
expect_stack("c4 maj", &ints(&[60, 64, 67]));
}
#[test]
fn chord_minor() {
expect_stack("c4 m", &ints(&[60, 63, 67]));
}
#[test]
fn chord_diminished() {
expect_stack("c4 dim", &ints(&[60, 63, 66]));
}
#[test]
fn chord_augmented() {
expect_stack("c4 aug", &ints(&[60, 64, 68]));
}
#[test]
fn chord_sus2() {
expect_stack("c4 sus2", &ints(&[60, 62, 67]));
}
#[test]
fn chord_sus4() {
expect_stack("c4 sus4", &ints(&[60, 65, 67]));
}
// Seventh chords
#[test]
fn chord_maj7() {
expect_stack("c4 maj7", &ints(&[60, 64, 67, 71]));
}
#[test]
fn chord_min7() {
expect_stack("c4 min7", &ints(&[60, 63, 67, 70]));
}
#[test]
fn chord_dom7() {
expect_stack("c4 dom7", &ints(&[60, 64, 67, 70]));
}
#[test]
fn chord_dim7() {
expect_stack("c4 dim7", &ints(&[60, 63, 66, 69]));
}
#[test]
fn chord_half_dim() {
expect_stack("c4 m7b5", &ints(&[60, 63, 66, 70]));
}
#[test]
fn chord_minmaj7() {
expect_stack("c4 minmaj7", &ints(&[60, 63, 67, 71]));
}
#[test]
fn chord_aug7() {
expect_stack("c4 aug7", &ints(&[60, 64, 68, 70]));
}
// Sixth chords
#[test]
fn chord_maj6() {
expect_stack("c4 maj6", &ints(&[60, 64, 67, 69]));
}
#[test]
fn chord_min6() {
expect_stack("c4 min6", &ints(&[60, 63, 67, 69]));
}
// Extended chords
#[test]
fn chord_dom9() {
expect_stack("c4 dom9", &ints(&[60, 64, 67, 70, 74]));
}
#[test]
fn chord_maj9() {
expect_stack("c4 maj9", &ints(&[60, 64, 67, 71, 74]));
}
#[test]
fn chord_min9() {
expect_stack("c4 min9", &ints(&[60, 63, 67, 70, 74]));
}
#[test]
fn chord_dom11() {
expect_stack("c4 dom11", &ints(&[60, 64, 67, 70, 74, 77]));
}
#[test]
fn chord_min11() {
expect_stack("c4 min11", &ints(&[60, 63, 67, 70, 74, 77]));
}
#[test]
fn chord_dom13() {
expect_stack("c4 dom13", &ints(&[60, 64, 67, 70, 74, 81]));
}
// Add chords
#[test]
fn chord_add9() {
expect_stack("c4 add9", &ints(&[60, 64, 67, 74]));
}
#[test]
fn chord_add11() {
expect_stack("c4 add11", &ints(&[60, 64, 67, 77]));
}
#[test]
fn chord_madd9() {
expect_stack("c4 madd9", &ints(&[60, 63, 67, 74]));
}
// Altered dominants
#[test]
fn chord_dom7b9() {
expect_stack("c4 dom7b9", &ints(&[60, 64, 67, 70, 73]));
}
#[test]
fn chord_dom7s9() {
expect_stack("c4 dom7s9", &ints(&[60, 64, 67, 70, 75]));
}
#[test]
fn chord_dom7b5() {
expect_stack("c4 dom7b5", &ints(&[60, 64, 66, 70]));
}
#[test]
fn chord_dom7s5() {
expect_stack("c4 dom7s5", &ints(&[60, 64, 68, 70]));
}
// Different roots
#[test]
fn chord_a3_min7() {
expect_stack("a3 min7", &ints(&[57, 60, 64, 67]));
}
#[test]
fn chord_e4_dom7s9() {
expect_stack("e4 dom7s9", &ints(&[64, 68, 71, 74, 79]));
}
#[test]
fn chord_with_integer_root() {
let f = run("60 maj");
let stack = f.stack();
assert_eq!(stack, ints(&[60, 64, 67]));
}

View File

@@ -105,3 +105,74 @@ fn ftom_880() {
fn mtof_ftom_roundtrip() {
expect_float("60 mtof ftom", 60.0);
}
#[test]
fn exprand_in_range() {
let f = forth_seeded(12345);
f.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap();
let val = stack_float(&f);
assert!(val >= 1.0 && val <= 100.0, "exprand {} not in [1, 100]", val);
}
#[test]
fn exprand_deterministic() {
let f1 = forth_seeded(99);
let f2 = forth_seeded(99);
f1.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap();
f2.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap();
assert_eq!(f1.stack(), f2.stack());
}
#[test]
fn exprand_swapped_args() {
let f1 = forth_seeded(42);
let f2 = forth_seeded(42);
f1.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap();
f2.evaluate("100.0 1.0 exprand", &default_ctx()).unwrap();
assert_eq!(f1.stack(), f2.stack());
}
#[test]
fn exprand_requires_positive() {
expect_error("0.0 10.0 exprand", "exprand requires positive values");
expect_error("-1.0 10.0 exprand", "exprand requires positive values");
expect_error("1.0 0.0 exprand", "exprand requires positive values");
}
#[test]
fn logrand_in_range() {
let f = forth_seeded(12345);
f.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap();
let val = stack_float(&f);
assert!(val >= 1.0 && val <= 100.0, "logrand {} not in [1, 100]", val);
}
#[test]
fn logrand_deterministic() {
let f1 = forth_seeded(99);
let f2 = forth_seeded(99);
f1.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap();
f2.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap();
assert_eq!(f1.stack(), f2.stack());
}
#[test]
fn logrand_swapped_args() {
let f1 = forth_seeded(42);
let f2 = forth_seeded(42);
f1.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap();
f2.evaluate("100.0 1.0 logrand", &default_ctx()).unwrap();
assert_eq!(f1.stack(), f2.stack());
}
#[test]
fn logrand_requires_positive() {
expect_error("0.0 10.0 logrand", "logrand requires positive values");
expect_error("-1.0 10.0 logrand", "logrand requires positive values");
expect_error("1.0 0.0 logrand", "logrand requires positive values");
}
#[test]
fn rand_equal_bounds() {
expect_float("5.0 5.0 rand", 5.0);
}