diff --git a/crates/forth/src/lib.rs b/crates/forth/src/lib.rs index dfd269b..a024ad8 100644 --- a/crates/forth/src/lib.rs +++ b/crates/forth/src/lib.rs @@ -1,5 +1,6 @@ mod compiler; mod ops; +mod theory; mod types; mod vm; mod words; diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 54f972b..8c5d4dc 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -43,7 +43,6 @@ pub enum Op { Set, GetContext(String), Rand, - Rrand, Seed, Cycle, Choose, @@ -81,4 +80,6 @@ pub enum Op { Noise, Chain, Loop, + Degree(&'static [i64]), + Oct, } diff --git a/crates/forth/src/theory/mod.rs b/crates/forth/src/theory/mod.rs new file mode 100644 index 0000000..a6cfabb --- /dev/null +++ b/crates/forth/src/theory/mod.rs @@ -0,0 +1,3 @@ +mod scales; + +pub use scales::lookup; diff --git a/crates/forth/src/theory/scales.rs b/crates/forth/src/theory/scales.rs new file mode 100644 index 0000000..62f0d99 --- /dev/null +++ b/crates/forth/src/theory/scales.rs @@ -0,0 +1,130 @@ +pub struct Scale { + pub name: &'static str, + pub pattern: &'static [i64], +} + +pub static SCALES: &[Scale] = &[ + Scale { + name: "major", + pattern: &[0, 2, 4, 5, 7, 9, 11], + }, + Scale { + name: "minor", + pattern: &[0, 2, 3, 5, 7, 8, 10], + }, + Scale { + name: "dorian", + pattern: &[0, 2, 3, 5, 7, 9, 10], + }, + Scale { + name: "phrygian", + pattern: &[0, 1, 3, 5, 7, 8, 10], + }, + Scale { + name: "lydian", + pattern: &[0, 2, 4, 6, 7, 9, 11], + }, + Scale { + name: "mixolydian", + pattern: &[0, 2, 4, 5, 7, 9, 10], + }, + Scale { + name: "aeolian", + pattern: &[0, 2, 3, 5, 7, 8, 10], + }, + Scale { + name: "locrian", + pattern: &[0, 1, 3, 5, 6, 8, 10], + }, + Scale { + name: "pentatonic", + pattern: &[0, 2, 4, 7, 9], + }, + Scale { + name: "minpent", + pattern: &[0, 3, 5, 7, 10], + }, + Scale { + name: "blues", + pattern: &[0, 3, 5, 6, 7, 10], + }, + Scale { + name: "chromatic", + pattern: &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + }, + Scale { + name: "wholetone", + pattern: &[0, 2, 4, 6, 8, 10], + }, + Scale { + name: "harmonicminor", + pattern: &[0, 2, 3, 5, 7, 8, 11], + }, + Scale { + name: "melodicminor", + pattern: &[0, 2, 3, 5, 7, 9, 11], + }, + // Jazz/Bebop + Scale { + name: "bebop", + pattern: &[0, 2, 4, 5, 7, 9, 10, 11], + }, + Scale { + name: "bebopmaj", + pattern: &[0, 2, 4, 5, 7, 8, 9, 11], + }, + Scale { + name: "bebopmin", + pattern: &[0, 2, 3, 5, 7, 8, 9, 10], + }, + Scale { + name: "altered", + pattern: &[0, 1, 3, 4, 6, 8, 10], + }, + Scale { + name: "lyddom", + pattern: &[0, 2, 4, 6, 7, 9, 10], + }, + Scale { + name: "halfwhole", + pattern: &[0, 1, 3, 4, 6, 7, 9, 10], + }, + Scale { + name: "wholehalf", + pattern: &[0, 2, 3, 5, 6, 8, 9, 11], + }, + // Symmetric + Scale { + name: "augmented", + pattern: &[0, 3, 4, 7, 8, 11], + }, + Scale { + name: "tritone", + pattern: &[0, 1, 4, 6, 7, 10], + }, + Scale { + name: "prometheus", + pattern: &[0, 2, 4, 6, 9, 10], + }, + // Modal variants (from melodic minor) + Scale { + name: "dorianb2", + pattern: &[0, 1, 3, 5, 7, 9, 10], + }, + Scale { + name: "lydianaug", + pattern: &[0, 2, 4, 6, 8, 9, 11], + }, + Scale { + name: "mixb6", + pattern: &[0, 2, 4, 5, 7, 8, 10], + }, + Scale { + name: "locrian2", + pattern: &[0, 2, 3, 5, 6, 8, 10], + }, +]; + +pub fn lookup(name: &str) -> Option<&'static [i64]> { + SCALES.iter().find(|s| s.name == name).map(|s| s.pattern) +} diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index b3b8c52..8d84879 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -315,16 +315,20 @@ impl Forth { } 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)); + let max = stack.pop().ok_or("stack underflow")?; + let min = stack.pop().ok_or("stack underflow")?; + match (&min, &max) { + (Value::Int(min_i, _), Value::Int(max_i, _)) => { + let val = self.rng.lock().unwrap().gen_range(*min_i..=*max_i); + stack.push(Value::Int(val, None)); + } + _ => { + let min_f = min.as_float()?; + let max_f = max.as_float()?; + let val = self.rng.lock().unwrap().gen_range(min_f..max_f); + stack.push(Value::Float(val, None)); + } + } } Op::Seed => { let s = stack.pop().ok_or("stack underflow")?.as_int()?; @@ -536,6 +540,21 @@ impl Forth { stack.push(Value::Float(note, None)); } + Op::Degree(pattern) => { + let degree = stack.pop().ok_or("stack underflow")?.as_int()?; + let len = pattern.len() as i64; + let octave_offset = degree.div_euclid(len); + let idx = degree.rem_euclid(len) as usize; + let midi = 60 + octave_offset * 12 + pattern[idx]; + stack.push(Value::Int(midi, None)); + } + + Op::Oct => { + let shift = stack.pop().ok_or("stack underflow")?.as_int()?; + let note = stack.pop().ok_or("stack underflow")?.as_int()?; + stack.push(Value::Int(note + shift * 12, None)); + } + Op::At => { let pos = stack.pop().ok_or("stack underflow")?.as_float()?; let parent = time_stack.last().ok_or("time stack underflow")?; diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index 9e3ac58..99bfa05 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -1,4 +1,5 @@ use super::ops::Op; +use super::theory; use super::types::{Dictionary, SourceSpan}; pub enum WordCompile { @@ -281,16 +282,9 @@ pub const WORDS: &[Word] = &[ // Randomness Word { name: "rand", - stack: "(min max -- f)", - desc: "Random float in range", - example: "0 1 rand => 0.42", - compile: Simple, - }, - Word { - name: "rrand", - stack: "(min max -- n)", - desc: "Random int in range", - example: "1 6 rrand => 4", + stack: "(min max -- n|f)", + desc: "Random in range. Int if both args are int, float otherwise", + example: "1 6 rand => 4 | 0.0 1.0 rand => 0.42", compile: Simple, }, Word { @@ -1537,7 +1531,6 @@ pub(super) fn simple_op(name: &str) -> Option { "sound" => Op::NewCmd, "emit" => Op::Emit, "rand" => Op::Rand, - "rrand" => Op::Rrand, "seed" => Op::Seed, "cycle" => Op::Cycle, "pcycle" => Op::PCycle, @@ -1573,6 +1566,7 @@ pub(super) fn simple_op(name: &str) -> Option { "noise" => Op::Noise, "chain" => Op::Chain, "loop" => Op::Loop, + "oct" => Op::Oct, _ => return None, }) } @@ -1633,7 +1627,7 @@ fn parse_interval(name: &str) -> Option { "M6" => 9, "m7" => 10, "M7" => 11, - "P8" | "oct" => 12, + "P8" => 12, // Compound intervals (octave + simple) "m9" => 13, "M9" => 14, @@ -1660,6 +1654,11 @@ pub(super) fn compile_word(name: &str, span: Option, ops: &mut Vec {} } + if let Some(pattern) = theory::lookup(name) { + ops.push(Op::Degree(pattern)); + return true; + } + for word in WORDS { if word.name == name { match &word.compile {