Files
Cagire/tests/forth/midi.rs

359 lines
10 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 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_at_loop_notes() {
// at-loop with m. closer: 3 iterations, each emits one MIDI note
let outputs = expect_outputs("0 0.25 0.5 at 60 note m.", 3);
for o in &outputs {
assert!(o.contains("/note/60/"));
}
}
#[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/"));
}