Notes and intervals

This commit is contained in:
2026-01-22 01:38:55 +01:00
parent c73e25e207
commit 409e815414
9 changed files with 85965 additions and 0 deletions

View File

@@ -143,6 +143,7 @@ pub enum Op {
PushFloat(f64, Option<SourceSpan>),
PushStr(String, Option<SourceSpan>),
Dup,
Dupn,
Drop,
Swap,
Over,
@@ -240,6 +241,20 @@ pub const WORDS: &[Word] = &[
example: "3 dup => 3 3",
compile: Simple,
},
Word {
name: "dupn",
stack: "(a n -- a a ... a)",
desc: "Duplicate a onto stack n times",
example: "2 4 dupn => 2 2 2 2",
compile: Simple,
},
Word {
name: "!",
stack: "(a n -- a a ... a)",
desc: "Duplicate a onto stack n times (alias for dupn)",
example: "2 4 ! => 2 2 2 2",
compile: Alias("dupn"),
},
Word {
name: "drop",
stack: "(a --)",
@@ -1596,6 +1611,7 @@ pub const WORDS: &[Word] = &[
fn simple_op(name: &str) -> Option<Op> {
Some(match name {
"dup" => Op::Dup,
"dupn" => Op::Dupn,
"drop" => Op::Drop,
"swap" => Op::Swap,
"over" => Op::Over,
@@ -1660,6 +1676,81 @@ fn simple_op(name: &str) -> Option<Op> {
})
}
/// Parse note names like c4, c#4, cs4, eb4 into MIDI numbers.
/// C4 = 60 (middle C), A4 = 69 (440 Hz reference).
fn parse_note_name(name: &str) -> Option<i64> {
let name = name.to_lowercase();
let bytes = name.as_bytes();
if bytes.len() < 2 {
return None;
}
let base = match bytes[0] {
b'c' => 0,
b'd' => 2,
b'e' => 4,
b'f' => 5,
b'g' => 7,
b'a' => 9,
b'b' => 11,
_ => return None,
};
let (modifier, octave_start) = match bytes[1] {
b'#' | b's' => (1, 2),
b'b' if bytes.len() > 2 && bytes[2].is_ascii_digit() => (-1, 2), // flat: eb4, bb4
b'0'..=b'9' => (0, 1),
_ => return None,
};
let octave_str = &name[octave_start..];
let octave: i64 = octave_str.parse().ok()?;
if !(-1..=9).contains(&octave) {
return None;
}
// MIDI: C4 = 60, so C-1 = 0
Some((octave + 1) * 12 + base + modifier)
}
/// Parse interval names like m3, M3, P5 into semitone counts.
/// Supports simple intervals (1-8) and compound intervals (9-15).
fn parse_interval(name: &str) -> Option<i64> {
// Simple intervals: unison through octave
let simple = match name {
"P1" | "unison" => 0,
"m2" => 1,
"M2" => 2,
"m3" => 3,
"M3" => 4,
"P4" => 5,
"aug4" | "dim5" | "tritone" => 6,
"P5" => 7,
"m6" => 8,
"M6" => 9,
"m7" => 10,
"M7" => 11,
"P8" | "oct" => 12,
// Compound intervals (octave + simple)
"m9" => 13,
"M9" => 14,
"m10" => 15,
"M10" => 16,
"P11" => 17,
"aug11" => 18,
"P12" => 19,
"m13" => 20,
"M13" => 21,
"m14" => 22,
"M14" => 23,
"P15" => 24,
_ => return None,
};
Some(simple)
}
fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
for word in WORDS {
if word.name == name {
@@ -1699,6 +1790,20 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
}
}
// Note names: c4, c#4, cs4, eb4, etc. -> MIDI number
if let Some(midi) = parse_note_name(name) {
ops.push(Op::PushInt(midi, None));
return true;
}
// Intervals: m3, M3, P5, etc. -> dup top, add semitones (for chord building)
if let Some(semitones) = parse_interval(name) {
ops.push(Op::Dup);
ops.push(Op::PushInt(semitones, None));
ops.push(Op::Add);
return true;
}
// Internal ops not exposed in WORDS
if let Some(op) = simple_op(name) {
ops.push(op);
@@ -2022,6 +2127,13 @@ impl Forth {
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")?;
}