366 lines
11 KiB
Rust
366 lines
11 KiB
Rust
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 0.8 velocity 3 chan m.", 1);
|
||
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/2/dur/"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_midi_note_default_channel() {
|
||
let outputs = expect_outputs("72 note 0.6 velocity m.", 1);
|
||
assert!(outputs[0].starts_with("/midi/note/72/vel/76/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 0.8 velocity 0 chan m.", 1);
|
||
assert!(outputs[0].contains("/chan/0")); // 0 clamped to 1, then -1 = 0
|
||
|
||
let outputs = expect_outputs("60 note 0.8 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 0.8 velocity m.", 1);
|
||
assert!(outputs[0].contains("/note/0"));
|
||
|
||
let outputs = expect_outputs("200 note 0.8 velocity m.", 1);
|
||
assert!(outputs[0].contains("/note/127"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_midi_velocity_clamping() {
|
||
let outputs = expect_outputs("60 note -0.1 velocity m.", 1);
|
||
assert!(outputs[0].contains("/vel/0"));
|
||
|
||
let outputs = expect_outputs("60 note 2.0 velocity m.", 1);
|
||
assert!(outputs[0].contains("/vel/127"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_midi_defaults() {
|
||
// With only note specified, velocity defaults to 0.8 (101) and channel to 0
|
||
let outputs = expect_outputs("60 note m.", 1);
|
||
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/0/dur/"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_midi_full_defaults() {
|
||
// With nothing specified, defaults to note=60, velocity=0.8 (101), channel=0
|
||
let outputs = expect_outputs("m.", 1);
|
||
assert!(outputs[0].starts_with("/midi/note/60/vel/101/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 0.8 0.6 0.5 velocity m.", 3);
|
||
assert!(outputs[0].contains("/note/60/vel/101/"));
|
||
assert!(outputs[1].contains("/note/64/vel/76/"));
|
||
assert!(outputs[2].contains("/note/67/vel/63/"));
|
||
}
|
||
|
||
#[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/"));
|
||
}
|