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/")); }