Files
Cagire/tests/forth/midi.rs

366 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::harness::{default_ctx, expect_outputs, forth};
use cagire::forth::{CcAccess, StepContext};
use cagire::midi::CcMemory;
#[allow(unused_imports)]
use cagire::forth::Value;
#[test]
fn test_midi_channel_set() {
let outputs = expect_outputs("60 note 100 velocity 3 chan m.", 1);
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/2/dur/"));
}
#[test]
fn test_midi_note_default_channel() {
let outputs = expect_outputs("72 note 80 velocity m.", 1);
assert!(outputs[0].starts_with("/midi/note/72/vel/80/chan/0/dur/"));
}
#[test]
fn test_midi_cc() {
let outputs = expect_outputs("64 ccnum 127 ccout m.", 1);
assert!(outputs[0].contains("/midi/cc/64/127/chan/0"));
}
#[test]
fn test_midi_cc_with_channel() {
let outputs = expect_outputs("5 chan 1 ccnum 64 ccout m.", 1);
assert!(outputs[0].contains("/midi/cc/1/64/chan/4"));
}
#[test]
fn test_ccval_returns_zero_without_cc_memory() {
let f = forth();
let ctx = default_ctx();
let outputs = f.evaluate("1 1 ccval", &ctx).unwrap();
assert!(outputs.is_empty());
let stack = f.stack();
assert_eq!(stack.len(), 1);
match &stack[0] {
cagire::forth::Value::Int(v, _) => assert_eq!(*v, 0),
_ => panic!("expected Int"),
}
}
#[test]
fn test_ccval_reads_from_cc_memory() {
let cc_memory = CcMemory::new();
cc_memory.set_cc(0, 0, 1, 64); // device 0, channel 1 (0-indexed), CC 1, value 64
cc_memory.set_cc(0, 5, 74, 127); // device 0, channel 6 (0-indexed), CC 74, value 127
let f = forth();
let ctx = StepContext {
cc_access: Some(&cc_memory as &dyn CcAccess),
..default_ctx()
};
// Test CC 1 on channel 1 (user provides 1, internally 0)
f.evaluate("1 1 ccval", &ctx).unwrap();
let stack = f.stack();
assert_eq!(stack.len(), 1);
match &stack[0] {
cagire::forth::Value::Int(v, _) => assert_eq!(*v, 64),
_ => panic!("expected Int"),
}
f.clear_stack();
// Test CC 74 on channel 6 (user provides 6, internally 5)
f.evaluate("74 6 ccval", &ctx).unwrap();
let stack = f.stack();
assert_eq!(stack.len(), 1);
match &stack[0] {
cagire::forth::Value::Int(v, _) => assert_eq!(*v, 127),
_ => panic!("expected Int"),
}
}
#[test]
fn test_midi_channel_clamping() {
// Channel should be clamped 1-16, then converted to 0-15 internally
let outputs = expect_outputs("60 note 100 velocity 0 chan m.", 1);
assert!(outputs[0].contains("/chan/0")); // 0 clamped to 1, then -1 = 0
let outputs = expect_outputs("60 note 100 velocity 17 chan m.", 1);
assert!(outputs[0].contains("/chan/15")); // 17 clamped to 16, then -1 = 15
}
#[test]
fn test_midi_note_clamping() {
let outputs = expect_outputs("-1 note 100 velocity m.", 1);
assert!(outputs[0].contains("/note/0"));
let outputs = expect_outputs("200 note 100 velocity m.", 1);
assert!(outputs[0].contains("/note/127"));
}
#[test]
fn test_midi_velocity_clamping() {
let outputs = expect_outputs("60 note -10 velocity m.", 1);
assert!(outputs[0].contains("/vel/0"));
let outputs = expect_outputs("60 note 200 velocity m.", 1);
assert!(outputs[0].contains("/vel/127"));
}
#[test]
fn test_midi_defaults() {
// With only note specified, velocity defaults to 100 and channel to 0
let outputs = expect_outputs("60 note m.", 1);
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/0/dur/"));
}
#[test]
fn test_midi_full_defaults() {
// With nothing specified, defaults to note=60, velocity=100, channel=0
let outputs = expect_outputs("m.", 1);
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/0/dur/"));
}
// Pitch bend tests
#[test]
fn test_midi_bend_center() {
let outputs = expect_outputs("0.0 bend m.", 1);
// 0.0 -> 8192 (center)
assert!(
outputs[0].contains("/midi/bend/8191/chan/0")
|| outputs[0].contains("/midi/bend/8192/chan/0")
);
}
#[test]
fn test_midi_bend_max() {
let outputs = expect_outputs("1.0 bend m.", 1);
// 1.0 -> 16383 (max)
assert!(outputs[0].contains("/midi/bend/16383/chan/0"));
}
#[test]
fn test_midi_bend_min() {
let outputs = expect_outputs("-1.0 bend m.", 1);
// -1.0 -> 0 (min)
assert!(outputs[0].contains("/midi/bend/0/chan/0"));
}
#[test]
fn test_midi_bend_with_channel() {
let outputs = expect_outputs("0.5 bend 3 chan m.", 1);
assert!(outputs[0].contains("/chan/2")); // channel 3 -> 2 (0-indexed)
assert!(outputs[0].contains("/midi/bend/"));
}
#[test]
fn test_midi_bend_clamping() {
let outputs = expect_outputs("2.0 bend m.", 1);
// 2.0 clamped to 1.0 -> 16383
assert!(outputs[0].contains("/midi/bend/16383/chan/0"));
let outputs = expect_outputs("-5.0 bend m.", 1);
// -5.0 clamped to -1.0 -> 0
assert!(outputs[0].contains("/midi/bend/0/chan/0"));
}
// Channel pressure (aftertouch) tests
#[test]
fn test_midi_pressure() {
let outputs = expect_outputs("64 pressure m.", 1);
assert!(outputs[0].contains("/midi/pressure/64/chan/0"));
}
#[test]
fn test_midi_pressure_with_channel() {
let outputs = expect_outputs("100 pressure 5 chan m.", 1);
assert!(outputs[0].contains("/midi/pressure/100/chan/4"));
}
#[test]
fn test_midi_pressure_clamping() {
let outputs = expect_outputs("-10 pressure m.", 1);
assert!(outputs[0].contains("/midi/pressure/0/chan/0"));
let outputs = expect_outputs("200 pressure m.", 1);
assert!(outputs[0].contains("/midi/pressure/127/chan/0"));
}
// Program change tests
#[test]
fn test_midi_program() {
let outputs = expect_outputs("0 program m.", 1);
assert!(outputs[0].contains("/midi/program/0/chan/0"));
}
#[test]
fn test_midi_program_with_channel() {
let outputs = expect_outputs("42 program 10 chan m.", 1);
assert!(outputs[0].contains("/midi/program/42/chan/9"));
}
#[test]
fn test_midi_program_clamping() {
let outputs = expect_outputs("-1 program m.", 1);
assert!(outputs[0].contains("/midi/program/0/chan/0"));
let outputs = expect_outputs("200 program m.", 1);
assert!(outputs[0].contains("/midi/program/127/chan/0"));
}
// MIDI real-time messages
#[test]
fn test_midi_clock() {
let outputs = expect_outputs("mclock", 1);
assert_eq!(outputs[0], "/midi/clock/dev/0");
}
#[test]
fn test_midi_start() {
let outputs = expect_outputs("mstart", 1);
assert_eq!(outputs[0], "/midi/start/dev/0");
}
#[test]
fn test_midi_stop() {
let outputs = expect_outputs("mstop", 1);
assert_eq!(outputs[0], "/midi/stop/dev/0");
}
#[test]
fn test_midi_continue() {
let outputs = expect_outputs("mcont", 1);
assert_eq!(outputs[0], "/midi/continue/dev/0");
}
// at (delta) tests
#[test]
fn test_midi_at_single_delta() {
let outputs = expect_outputs("0.5 at 60 note m.", 1);
assert!(outputs[0].contains("/note/60/"));
assert!(outputs[0].contains("/delta/"));
}
#[test]
fn test_midi_at_multiple_deltas() {
let outputs = expect_outputs("0 0.5 at 60 note m.", 2);
assert!(outputs[0].contains("/note/60/"));
assert!(outputs[1].contains("/note/60/"));
assert!(outputs[1].contains("/delta/"));
}
#[test]
fn test_midi_at_with_polyphony() {
// 2 notes × 2 deltas = 4 events
expect_outputs("0 0.5 at 60 64 note m.", 4);
}
#[test]
fn test_midi_arp_notes() {
let outputs = expect_outputs("c4 e4 g4 arp note m.", 3);
assert!(outputs[0].contains("/note/60/"));
assert!(outputs[1].contains("/note/64/"));
assert!(outputs[2].contains("/note/67/"));
}
#[test]
fn test_midi_arp_with_at() {
let outputs = expect_outputs("0 0.25 0.5 at c4 e4 g4 arp note m.", 3);
assert!(outputs[0].contains("/note/60/"));
assert!(outputs[1].contains("/note/64/"));
assert!(outputs[2].contains("/note/67/"));
}
#[test]
fn test_midi_at_cc() {
let outputs = expect_outputs("0 0.5 at 1 ccnum 64 ccout m.", 2);
assert!(outputs[0].contains("/midi/cc/1/64/"));
assert!(outputs[1].contains("/midi/cc/1/64/"));
assert!(outputs[1].contains("/delta/"));
}
// Test message type priority (first matching type wins)
#[test]
fn test_midi_message_priority_cc_over_note() {
// CC params take priority over note
let outputs = expect_outputs("60 note 1 ccnum 64 ccout m.", 1);
assert!(outputs[0].contains("/midi/cc/1/64"));
}
#[test]
fn test_midi_message_priority_bend_over_note() {
// bend takes priority over note (but not over CC)
let outputs = expect_outputs("60 note 0.5 bend m.", 1);
assert!(outputs[0].contains("/midi/bend/"));
}
// MIDI note duration tests
#[test]
fn test_midi_note_default_duration() {
// Default dur=1.0, with tempo=120 and speed=1.0, step_duration = 60/120/4/1 = 0.125
let outputs = expect_outputs("60 note m.", 1);
assert!(outputs[0].contains("/dur/0.125"));
}
#[test]
fn test_midi_note_explicit_duration() {
// dur=0.5 means half step duration = 0.0625 seconds
let outputs = expect_outputs("60 note 0.5 dur m.", 1);
assert!(outputs[0].contains("/dur/0.0625"));
}
#[test]
fn test_midi_note_long_duration() {
// dur=2.0 means two step durations = 0.25 seconds
let outputs = expect_outputs("60 note 2 dur m.", 1);
assert!(outputs[0].contains("/dur/0.25"));
}
#[test]
fn test_midi_note_duration_with_tempo() {
use crate::harness::{ctx_with, forth};
let f = forth();
// At tempo=60, step_duration = 60/60/4/1 = 0.25 seconds
let ctx = ctx_with(|c| c.tempo = 60.0);
let outputs = f.evaluate("60 note m.", &ctx).unwrap();
assert!(outputs[0].contains("/dur/0.25"));
}
#[test]
fn test_midi_note_duration_with_speed() {
use crate::harness::{ctx_with, forth};
let f = forth();
// At tempo=120 speed=2, step_duration = 60/120/4/2 = 0.0625 seconds
let ctx = ctx_with(|c| c.speed = 2.0);
let outputs = f.evaluate("60 note m.", &ctx).unwrap();
assert!(outputs[0].contains("/dur/0.0625"));
}
// Polyphonic MIDI tests
#[test]
fn test_midi_polyphonic_notes() {
let outputs = expect_outputs("60 64 67 note m.", 3);
assert!(outputs[0].contains("/midi/note/60/"));
assert!(outputs[1].contains("/midi/note/64/"));
assert!(outputs[2].contains("/midi/note/67/"));
}
#[test]
fn test_midi_polyphonic_notes_with_velocity() {
let outputs = expect_outputs("60 64 67 note 100 80 60 velocity m.", 3);
assert!(outputs[0].contains("/note/60/vel/100/"));
assert!(outputs[1].contains("/note/64/vel/80/"));
assert!(outputs[2].contains("/note/67/vel/60/"));
}
#[test]
fn test_midi_polyphonic_channel() {
let outputs = expect_outputs("60 note 1 2 chan m.", 2);
assert!(outputs[0].contains("/note/60/") && outputs[0].contains("/chan/0"));
assert!(outputs[1].contains("/note/60/") && outputs[1].contains("/chan/1"));
}
#[test]
fn test_midi_polyphonic_cc() {
let outputs = expect_outputs("1 2 ccnum 64 127 ccout m.", 2);
assert!(outputs[0].contains("/midi/cc/1/64/"));
assert!(outputs[1].contains("/midi/cc/2/127/"));
}