Notes and intervals
This commit is contained in:
@@ -133,6 +133,7 @@ fn main() -> io::Result<()> {
|
||||
io::stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
|
||||
loop {
|
||||
if app.audio.restart_pending {
|
||||
|
||||
@@ -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")?;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ use super::{audio_view, doc_view, main_view, patterns_view, title_view};
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
let term = frame.area();
|
||||
let blank = " ".repeat(term.width as usize);
|
||||
let lines: Vec<Line> = (0..term.height).map(|_| Line::raw(&blank)).collect();
|
||||
frame.render_widget(Paragraph::new(lines), term);
|
||||
|
||||
if app.ui.show_title {
|
||||
title_view::render(frame, term, &mut app.ui);
|
||||
|
||||
@@ -36,3 +36,9 @@ mod quotations;
|
||||
|
||||
#[path = "forth/iteration.rs"]
|
||||
mod iteration;
|
||||
|
||||
#[path = "forth/notes.rs"]
|
||||
mod notes;
|
||||
|
||||
#[path = "forth/intervals.rs"]
|
||||
mod intervals;
|
||||
|
||||
75
tests/forth/intervals.rs
Normal file
75
tests/forth/intervals.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use super::harness::{expect_int, expect_stack};
|
||||
use cagire::model::forth::Value;
|
||||
|
||||
fn ints(vals: &[i64]) -> Vec<Value> {
|
||||
vals.iter().map(|&v| Value::Int(v, None)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_keeps_root() {
|
||||
// Interval dups the root, then adds - so both values remain
|
||||
expect_stack("c4 M3", &ints(&[60, 64]));
|
||||
expect_stack("c4 P5", &ints(&[60, 67]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_stacking_builds_chord() {
|
||||
// C major: root + M3 + m3 (stacked)
|
||||
expect_stack("c4 M3 m3", &ints(&[60, 64, 67]));
|
||||
// C minor: root + m3 + M3
|
||||
expect_stack("c4 m3 M3", &ints(&[60, 63, 67]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_tritone() {
|
||||
expect_stack("c4 tritone", &ints(&[60, 66]));
|
||||
expect_stack("c4 aug4", &ints(&[60, 66]));
|
||||
expect_stack("c4 dim5", &ints(&[60, 66]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_all_simple() {
|
||||
expect_stack("c4 P1", &ints(&[60, 60])); // unison
|
||||
expect_stack("c4 m2", &ints(&[60, 61]));
|
||||
expect_stack("c4 M2", &ints(&[60, 62]));
|
||||
expect_stack("c4 m3", &ints(&[60, 63]));
|
||||
expect_stack("c4 M3", &ints(&[60, 64]));
|
||||
expect_stack("c4 P4", &ints(&[60, 65]));
|
||||
expect_stack("c4 P5", &ints(&[60, 67]));
|
||||
expect_stack("c4 m6", &ints(&[60, 68]));
|
||||
expect_stack("c4 M6", &ints(&[60, 69]));
|
||||
expect_stack("c4 m7", &ints(&[60, 70]));
|
||||
expect_stack("c4 M7", &ints(&[60, 71]));
|
||||
expect_stack("c4 P8", &ints(&[60, 72]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_compound() {
|
||||
expect_stack("c4 m9", &ints(&[60, 73]));
|
||||
expect_stack("c4 M9", &ints(&[60, 74]));
|
||||
expect_stack("c4 P15", &ints(&[60, 84]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_from_any_note() {
|
||||
expect_stack("a4 m3", &ints(&[69, 72])); // A4 + m3 = C5
|
||||
expect_stack("e4 P5", &ints(&[64, 71])); // E4 + P5 = B4
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_dominant_seventh() {
|
||||
// C7: C E G Bb = root + M3 + m3 + m3
|
||||
expect_stack("c4 M3 m3 m3", &ints(&[60, 64, 67, 70]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_major_seventh() {
|
||||
// Cmaj7: C E G B = root + M3 + m3 + M3
|
||||
expect_stack("c4 M3 m3 M3", &ints(&[60, 64, 67, 71]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_with_nip() {
|
||||
// If you only want the new note, nip drops the one below top
|
||||
expect_int("c4 M3 nip", 64);
|
||||
}
|
||||
62
tests/forth/notes.rs
Normal file
62
tests/forth/notes.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use super::harness::expect_int;
|
||||
|
||||
#[test]
|
||||
fn note_c4_is_middle_c() {
|
||||
expect_int("c4", 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_a4_is_440hz_reference() {
|
||||
expect_int("a4", 69);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_c0() {
|
||||
expect_int("c0", 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_sharps() {
|
||||
expect_int("c#4", 61);
|
||||
expect_int("cs4", 61);
|
||||
expect_int("f#3", 54);
|
||||
expect_int("fs3", 54);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_flats() {
|
||||
expect_int("db4", 61);
|
||||
expect_int("eb4", 63);
|
||||
expect_int("ab4", 68);
|
||||
expect_int("bb4", 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_all_naturals_octave4() {
|
||||
expect_int("c4", 60);
|
||||
expect_int("d4", 62);
|
||||
expect_int("e4", 64);
|
||||
expect_int("f4", 65);
|
||||
expect_int("g4", 67);
|
||||
expect_int("a4", 69);
|
||||
expect_int("b4", 71);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_octave_range() {
|
||||
expect_int("c0", 12);
|
||||
expect_int("c1", 24);
|
||||
expect_int("c2", 36);
|
||||
expect_int("c3", 48);
|
||||
expect_int("c5", 72);
|
||||
expect_int("c6", 84);
|
||||
expect_int("c7", 96);
|
||||
expect_int("c8", 108);
|
||||
expect_int("c9", 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_in_expression() {
|
||||
expect_int("c4 12 +", 72); // C4 + octave = C5
|
||||
expect_int("a4 c4 -", 9); // interval from C4 to A4
|
||||
}
|
||||
@@ -15,6 +15,31 @@ fn dup_underflow() {
|
||||
expect_error("dup", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dupn() {
|
||||
expect_stack("2 4 dupn", &[int(2), int(2), int(2), int(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dupn_one() {
|
||||
expect_stack("5 1 dupn", &[int(5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dupn_zero() {
|
||||
expect_stack("5 0 dupn", &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dupn_underflow() {
|
||||
expect_error("3 dupn", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bang_alias() {
|
||||
expect_stack("c4 3 !", &[int(60), int(60), int(60)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop() {
|
||||
expect_stack("1 2 drop", &[int(1)]);
|
||||
|
||||
Reference in New Issue
Block a user