1use crate::events::EventBody;
31
32const MT_CHANNEL_VOICE_2: u8 = 0x4;
34const MT_SYSEX_7: u8 = 0x3;
36const MT_SYSEX_8: u8 = 0x5;
38
39const SYSEX_STATUS_COMPLETE: u8 = 0x0;
42const SYSEX_STATUS_START: u8 = 0x1;
43const SYSEX_STATUS_CONTINUE: u8 = 0x2;
44const SYSEX_STATUS_END: u8 = 0x3;
45
46#[must_use]
53#[allow(clippy::cast_possible_truncation)] pub fn decode_ump_channel_voice_2(words: [u32; 4]) -> Option<EventBody> {
55 let w0 = words[0];
62 let w1 = words[1];
63 let mt = ((w0 >> 28) & 0xF) as u8;
64 if mt != MT_CHANNEL_VOICE_2 {
65 return None;
66 }
67 let group = ((w0 >> 24) & 0xF) as u8;
68 let status = ((w0 >> 20) & 0xF) as u8;
69 let channel = ((w0 >> 16) & 0xF) as u8;
70 let byte_a = ((w0 >> 8) & 0xFF) as u8; let byte_b = (w0 & 0xFF) as u8; let body = match status {
73 0x8 => EventBody::NoteOff2 {
74 group,
75 channel,
76 note: byte_a & 0x7F,
77 velocity: (w1 >> 16) as u16,
78 attribute_type: byte_b,
79 attribute: (w1 & 0xFFFF) as u16,
80 },
81 0x9 => EventBody::NoteOn2 {
82 group,
83 channel,
84 note: byte_a & 0x7F,
85 velocity: (w1 >> 16) as u16,
86 attribute_type: byte_b,
87 attribute: (w1 & 0xFFFF) as u16,
88 },
89 0xA => EventBody::PolyPressure2 {
90 group,
91 channel,
92 note: byte_a & 0x7F,
93 pressure: w1,
94 },
95 0x0 | 0x1 => EventBody::PerNoteCC {
99 group,
100 channel,
101 note: byte_a & 0x7F,
102 cc: byte_b,
103 value: w1,
104 registered: status == 0x0,
105 },
106 0x6 => EventBody::PerNotePitchBend {
108 group,
109 channel,
110 note: byte_a & 0x7F,
111 value: w1,
112 },
113 0xF => EventBody::PerNoteManagement {
116 group,
117 channel,
118 note: byte_a & 0x7F,
119 flags: byte_b,
120 },
121 0xB => EventBody::ControlChange2 {
122 group,
123 channel,
124 cc: byte_a & 0x7F,
125 value: w1,
126 },
127 0xD => EventBody::ChannelPressure2 {
128 group,
129 channel,
130 pressure: w1,
131 },
132 0xE => EventBody::PitchBend2 {
133 group,
134 channel,
135 value: w1,
136 },
137 0x2 => EventBody::RegisteredController {
141 group,
142 channel,
143 bank: byte_a & 0x7F,
144 index: byte_b & 0x7F,
145 value: w1,
146 },
147 0x3 => EventBody::AssignableController {
148 group,
149 channel,
150 bank: byte_a & 0x7F,
151 index: byte_b & 0x7F,
152 value: w1,
153 },
154 0xC => EventBody::ProgramChange2 {
155 group,
156 channel,
157 program: (w1 >> 24) as u8 & 0x7F,
158 bank: if w0 & 0x01 == 1 {
162 Some(((w1 >> 8) as u8 & 0x7F, w1 as u8 & 0x7F))
163 } else {
164 None
165 },
166 },
167 _ => return None,
168 };
169 Some(body)
170}
171
172#[must_use]
182pub fn encode_ump_channel_voice_2(body: &EventBody) -> Option<[u32; 4]> {
183 let (status, byte_a, byte_b, w1): (u8, u8, u8, u32) = match *body {
187 EventBody::NoteOff2 {
188 note,
189 velocity,
190 attribute_type,
191 attribute,
192 ..
193 } => (0x8, note, attribute_type, cv2_value16(velocity, attribute)),
194 EventBody::NoteOn2 {
195 note,
196 velocity,
197 attribute_type,
198 attribute,
199 ..
200 } => (0x9, note, attribute_type, cv2_value16(velocity, attribute)),
201 EventBody::PolyPressure2 { note, pressure, .. } => (0xA, note, 0, pressure),
202 EventBody::PerNoteCC {
203 note,
204 cc,
205 value,
206 registered,
207 ..
208 } => (u8::from(!registered), note, cc, value),
210 EventBody::PerNotePitchBend { note, value, .. } => (0x6, note, 0, value),
211 EventBody::PerNoteManagement { note, flags, .. } => (0xF, note, flags, 0),
212 EventBody::ControlChange2 { cc, value, .. } => (0xB, cc, 0, value),
213 EventBody::ChannelPressure2 { pressure, .. } => (0xD, 0, 0, pressure),
214 EventBody::PitchBend2 { value, .. } => (0xE, 0, 0, value),
215 EventBody::RegisteredController {
216 bank, index, value, ..
217 } => (0x2, bank, index & 0x7F, value),
218 EventBody::AssignableController {
219 bank, index, value, ..
220 } => (0x3, bank, index & 0x7F, value),
221 EventBody::ProgramChange2 { program, bank, .. } => {
222 let (option, w1) = match bank {
225 Some((msb, lsb)) => (
226 0x01,
227 (u32::from(program & 0x7F) << 24)
228 | (u32::from(msb & 0x7F) << 8)
229 | u32::from(lsb & 0x7F),
230 ),
231 None => (0x00, u32::from(program & 0x7F) << 24),
232 };
233 (0xC, 0, option, w1)
234 }
235 _ => return None,
236 };
237 let (group, channel) = cv2_addr(body)?;
238 let w0 = (0x4 << 28)
244 | (u32::from(group & 0x0F) << 24)
245 | (u32::from(status) << 20)
246 | (u32::from(channel & 0x0F) << 16)
247 | (u32::from(byte_a & 0x7F) << 8)
248 | u32::from(byte_b);
249 Some([w0, w1, 0, 0])
250}
251
252const fn cv2_value16(hi: u16, lo: u16) -> u32 {
253 ((hi as u32) << 16) | lo as u32
254}
255
256const MT_CHANNEL_VOICE_1: u8 = 0x2;
258
259#[must_use]
267pub fn encode_ump_channel_voice_1(body: &EventBody) -> Option<[u32; 4]> {
268 let (opcode, channel, group, data1, data2): (u8, u8, u8, u8, u8) = match *body {
269 EventBody::NoteOff {
270 group,
271 channel,
272 note,
273 velocity,
274 } => (0x8, channel, group, note, velocity),
275 EventBody::NoteOn {
276 group,
277 channel,
278 note,
279 velocity,
280 } => (0x9, channel, group, note, velocity),
281 EventBody::Aftertouch {
282 group,
283 channel,
284 note,
285 pressure,
286 } => (0xA, channel, group, note, pressure),
287 EventBody::ControlChange {
288 group,
289 channel,
290 cc,
291 value,
292 } => (0xB, channel, group, cc, value),
293 EventBody::ProgramChange {
294 group,
295 channel,
296 program,
297 } => (0xC, channel, group, program, 0),
298 EventBody::ChannelPressure {
299 group,
300 channel,
301 pressure,
302 } => (0xD, channel, group, pressure, 0),
303 EventBody::PitchBend {
304 group,
305 channel,
306 value,
307 } => {
308 let lsb = u8::try_from(value & 0x7F).unwrap_or(0);
311 let msb = u8::try_from((value >> 7) & 0x7F).unwrap_or(0);
312 (0xE, channel, group, lsb, msb)
313 }
314 _ => return None,
315 };
316 let w0 = (u32::from(MT_CHANNEL_VOICE_1) << 28)
317 | (u32::from(group & 0x0F) << 24)
318 | (u32::from((opcode << 4) | (channel & 0x0F)) << 16)
319 | (u32::from(data1 & 0x7F) << 8)
320 | u32::from(data2 & 0x7F);
321 Some([w0, 0, 0, 0])
322}
323
324const SYSEX_7_BYTES_PER_PACKET: usize = 6;
327
328#[must_use]
331pub const fn sysex7_packet_count(payload_len: usize) -> usize {
332 if payload_len == 0 {
333 1
334 } else {
335 payload_len.div_ceil(SYSEX_7_BYTES_PER_PACKET)
336 }
337}
338
339#[must_use]
349pub fn encode_sysex7_packet(group: u8, payload: &[u8], packet_index: usize) -> Option<[u32; 2]> {
350 let total = sysex7_packet_count(payload.len());
351 if packet_index >= total {
352 return None;
353 }
354 let start = packet_index * SYSEX_7_BYTES_PER_PACKET;
355 let chunk = &payload[start..(start + SYSEX_7_BYTES_PER_PACKET).min(payload.len())];
356 let status = match (total, packet_index) {
357 (1, _) => SYSEX_STATUS_COMPLETE,
358 (_, 0) => SYSEX_STATUS_START,
359 (_, i) if i == total - 1 => SYSEX_STATUS_END,
360 _ => SYSEX_STATUS_CONTINUE,
361 };
362 let mut padded = [0u8; SYSEX_7_BYTES_PER_PACKET];
363 for (dst, src) in padded.iter_mut().zip(chunk) {
364 *dst = src & 0x7F;
365 }
366 #[allow(clippy::cast_possible_truncation)]
368 let n = chunk.len() as u32;
369 let w0 = (u32::from(MT_SYSEX_7) << 28)
370 | (u32::from(group & 0x0F) << 24)
371 | (u32::from(status) << 20)
372 | (n << 16)
373 | (u32::from(padded[0]) << 8)
374 | u32::from(padded[1]);
375 let w1 = (u32::from(padded[2]) << 24)
376 | (u32::from(padded[3]) << 16)
377 | (u32::from(padded[4]) << 8)
378 | u32::from(padded[5]);
379 Some([w0, w1])
380}
381
382fn cv2_addr(body: &EventBody) -> Option<(u8, u8)> {
386 Some(match *body {
387 EventBody::NoteOff2 { group, channel, .. }
388 | EventBody::NoteOn2 { group, channel, .. }
389 | EventBody::PolyPressure2 { group, channel, .. }
390 | EventBody::PerNoteCC { group, channel, .. }
391 | EventBody::PerNotePitchBend { group, channel, .. }
392 | EventBody::PerNoteManagement { group, channel, .. }
393 | EventBody::ControlChange2 { group, channel, .. }
394 | EventBody::ChannelPressure2 { group, channel, .. }
395 | EventBody::PitchBend2 { group, channel, .. }
396 | EventBody::RegisteredController { group, channel, .. }
397 | EventBody::AssignableController { group, channel, .. }
398 | EventBody::ProgramChange2 { group, channel, .. } => (group, channel),
399 _ => return None,
400 })
401}
402
403pub struct SysExPacket<'a> {
410 pub group: u8,
412 pub stream_id: u8,
415 pub bytes: &'a [u8],
418}
419
420pub enum SysExFeed<'a> {
423 Buffered,
426 Complete(SysExPacket<'a>),
430 Invalid,
434 Overflow,
438}
439
440pub const SYSEX_ASSEMBLER_SLOTS: usize = 4;
450
451struct StreamSlot {
452 buffer: Vec<u8>,
455 group: u8,
456 stream_id: u8,
457 in_progress: bool,
461 in_use: bool,
466 last_touch: u64,
469}
470
471pub struct SysExAssembler {
483 slots: [StreamSlot; SYSEX_ASSEMBLER_SLOTS],
484 touch_counter: u64,
487}
488
489impl SysExAssembler {
490 #[must_use]
500 pub fn with_capacity(capacity: usize) -> Self {
501 let slots = std::array::from_fn(|_| StreamSlot {
504 buffer: Vec::with_capacity(capacity),
505 group: 0,
506 stream_id: 0,
507 in_progress: false,
508 in_use: false,
509 last_touch: 0,
510 });
511 Self {
512 slots,
513 touch_counter: 0,
514 }
515 }
516
517 pub fn reset(&mut self) {
522 for slot in &mut self.slots {
523 slot.buffer.clear();
524 slot.in_progress = false;
525 slot.in_use = false;
526 slot.last_touch = 0;
527 }
528 self.touch_counter = 0;
529 }
530
531 fn find_slot(&self, group: u8, stream_id: u8) -> Option<usize> {
534 self.slots
535 .iter()
536 .position(|s| s.in_use && s.group == group && s.stream_id == stream_id)
537 }
538
539 fn claim_slot(&mut self, group: u8, stream_id: u8) -> usize {
545 let idx = self
549 .slots
550 .iter()
551 .position(|s| !s.in_use)
552 .unwrap_or_else(|| {
553 self.slots
554 .iter()
555 .enumerate()
556 .min_by_key(|(_, s)| s.last_touch)
557 .map(|(i, _)| i)
558 .expect("non-empty slot table")
559 });
560 let slot = &mut self.slots[idx];
561 slot.buffer.clear();
562 slot.group = group;
563 slot.stream_id = stream_id;
564 slot.in_use = true;
565 slot.in_progress = false;
566 idx
567 }
568
569 #[allow(clippy::cast_possible_truncation)] pub fn push_sysex7_packet(&mut self, words: [u32; 2]) -> SysExFeed<'_> {
574 let w0 = words[0];
575 let w1 = words[1];
576 let mt = ((w0 >> 28) & 0xF) as u8;
577 if mt != MT_SYSEX_7 {
578 return SysExFeed::Invalid;
579 }
580 let group = ((w0 >> 24) & 0xF) as u8;
581 let status = ((w0 >> 20) & 0xF) as u8;
582 let n = ((w0 >> 16) & 0xF) as u8;
583 if n > 6 {
584 return SysExFeed::Invalid;
585 }
586 let raw = [
589 ((w0 >> 8) & 0xFF) as u8,
590 (w0 & 0xFF) as u8,
591 ((w1 >> 24) & 0xFF) as u8,
592 ((w1 >> 16) & 0xFF) as u8,
593 ((w1 >> 8) & 0xFF) as u8,
594 (w1 & 0xFF) as u8,
595 ];
596 self.feed_payload(group, 0, status, &raw[..n as usize])
597 }
598
599 #[allow(clippy::cast_possible_truncation)] pub fn push_sysex8_packet(&mut self, words: [u32; 4]) -> SysExFeed<'_> {
605 let w0 = words[0];
606 let mt = ((w0 >> 28) & 0xF) as u8;
607 if mt != MT_SYSEX_8 {
608 return SysExFeed::Invalid;
609 }
610 let group = ((w0 >> 24) & 0xF) as u8;
611 let status = ((w0 >> 20) & 0xF) as u8;
612 let n = ((w0 >> 16) & 0xF) as u8;
613 let stream_id = ((w0 >> 8) & 0xFF) as u8;
614 if n == 0 || n > 14 {
620 return SysExFeed::Invalid;
621 }
622 let data_len = usize::from(n - 1);
623 let raw = [
626 (w0 & 0xFF) as u8, ((words[1] >> 24) & 0xFF) as u8,
628 ((words[1] >> 16) & 0xFF) as u8,
629 ((words[1] >> 8) & 0xFF) as u8,
630 (words[1] & 0xFF) as u8,
631 ((words[2] >> 24) & 0xFF) as u8,
632 ((words[2] >> 16) & 0xFF) as u8,
633 ((words[2] >> 8) & 0xFF) as u8,
634 (words[2] & 0xFF) as u8,
635 ((words[3] >> 24) & 0xFF) as u8,
636 ((words[3] >> 16) & 0xFF) as u8,
637 ((words[3] >> 8) & 0xFF) as u8,
638 (words[3] & 0xFF) as u8,
639 ];
640 self.feed_payload(group, stream_id, status, &raw[..data_len])
641 }
642
643 fn feed_payload(
644 &mut self,
645 group: u8,
646 stream_id: u8,
647 status: u8,
648 bytes: &[u8],
649 ) -> SysExFeed<'_> {
650 self.touch_counter += 1;
651 let now = self.touch_counter;
652
653 match status {
654 SYSEX_STATUS_COMPLETE => {
655 let idx = match self.find_slot(group, stream_id) {
661 Some(i) => i,
662 None => self.claim_slot(group, stream_id),
663 };
664 let slot = &mut self.slots[idx];
665 slot.buffer.clear();
666 if slot.buffer.capacity() < bytes.len() {
667 slot.in_progress = false;
673 slot.in_use = false;
674 slot.last_touch = now;
675 return SysExFeed::Overflow;
676 }
677 slot.buffer.extend_from_slice(bytes);
678 slot.in_progress = false;
679 slot.last_touch = now;
680 SysExFeed::Complete(SysExPacket {
681 group,
682 stream_id,
683 bytes: &slot.buffer,
684 })
685 }
686 SYSEX_STATUS_START => {
687 let idx = match self.find_slot(group, stream_id) {
688 Some(i) => i,
689 None => self.claim_slot(group, stream_id),
690 };
691 let slot = &mut self.slots[idx];
692 slot.buffer.clear();
693 if slot.buffer.capacity() < bytes.len() {
694 slot.in_progress = false;
695 slot.in_use = false;
696 slot.last_touch = now;
697 return SysExFeed::Overflow;
698 }
699 slot.buffer.extend_from_slice(bytes);
700 slot.in_progress = true;
701 slot.last_touch = now;
702 SysExFeed::Buffered
703 }
704 SYSEX_STATUS_CONTINUE | SYSEX_STATUS_END => {
705 let Some(idx) = self.find_slot(group, stream_id) else {
706 return SysExFeed::Invalid;
708 };
709 let slot = &mut self.slots[idx];
710 if !slot.in_progress {
711 return SysExFeed::Invalid;
712 }
713 if slot.buffer.len() + bytes.len() > slot.buffer.capacity() {
714 slot.buffer.clear();
715 slot.in_progress = false;
716 slot.in_use = false;
717 slot.last_touch = now;
718 return SysExFeed::Overflow;
719 }
720 slot.buffer.extend_from_slice(bytes);
721 slot.last_touch = now;
722 if status == SYSEX_STATUS_END {
723 slot.in_progress = false;
724 SysExFeed::Complete(SysExPacket {
725 group,
726 stream_id,
727 bytes: &slot.buffer,
728 })
729 } else {
730 SysExFeed::Buffered
731 }
732 }
733 _ => SysExFeed::Invalid,
734 }
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741
742 #[test]
743 fn decode_note_on_2() {
744 let w0 = (0x4u32 << 28) | (0x9u32 << 20) | (0x2u32 << 16) | (60u32 << 8) | 0x03;
748 let w1 = (0x8000u32 << 16) | 0x1234;
749 let decoded = decode_ump_channel_voice_2([w0, w1, 0, 0]).expect("decodes");
750 if let EventBody::NoteOn2 {
751 channel,
752 note,
753 velocity,
754 attribute_type,
755 attribute,
756 ..
757 } = decoded
758 {
759 assert_eq!(channel, 2);
760 assert_eq!(note, 60);
761 assert_eq!(velocity, 0x8000);
762 assert_eq!(attribute_type, 3);
763 assert_eq!(attribute, 0x1234);
764 } else {
765 panic!("expected NoteOn2");
766 }
767 }
768
769 #[test]
770 fn non_channel_voice_packet_returns_none() {
771 assert!(decode_ump_channel_voice_2([0x0000_0000, 0, 0, 0]).is_none());
773 assert!(decode_ump_channel_voice_2([0x3000_0000, 0, 0, 0]).is_none());
775 }
776
777 #[track_caller]
783 fn cv2_round_trip(body: EventBody) {
784 let packet = encode_ump_channel_voice_2(&body).expect("2.0 channel voice encodes");
785 let decoded = decode_ump_channel_voice_2(packet).expect("decodes");
786 let re_encoded = encode_ump_channel_voice_2(&decoded).expect("re-encodes");
787 assert_eq!(packet, re_encoded, "round trip mismatch for {body:?}");
788 }
789
790 #[test]
791 fn encode_bits_match_spec() {
792 let packet = encode_ump_channel_voice_2(&EventBody::NoteOn2 {
795 group: 4,
796 channel: 2,
797 note: 64,
798 velocity: 0xBEEF,
799 attribute_type: 3,
800 attribute: 0x1234,
801 })
802 .unwrap();
803 assert_eq!(
804 packet,
805 [
806 (0x4 << 28) | (4 << 24) | (0x9 << 20) | (2 << 16) | (64 << 8) | 0x03,
807 (0xBEEF << 16) | 0x1234,
808 0,
809 0,
810 ]
811 );
812 }
813
814 #[test]
815 fn channel_voice_2_round_trips() {
816 cv2_round_trip(EventBody::NoteOn2 {
817 group: 4,
818 channel: 2,
819 note: 64,
820 velocity: 0xBEEF,
821 attribute_type: 3,
822 attribute: 0x1234,
823 });
824 cv2_round_trip(EventBody::NoteOff2 {
825 group: 0,
826 channel: 15,
827 note: 127,
828 velocity: 0,
829 attribute_type: 0,
830 attribute: 0,
831 });
832 cv2_round_trip(EventBody::ControlChange2 {
833 group: 15,
834 channel: 0,
835 cc: 11,
836 value: 0xDEAD_BEEF,
837 });
838 cv2_round_trip(EventBody::PerNoteCC {
839 group: 1,
840 channel: 3,
841 note: 72,
842 cc: 5,
843 value: 0x0102_0304,
844 registered: true,
845 });
846 cv2_round_trip(EventBody::PerNoteCC {
847 group: 1,
848 channel: 3,
849 note: 72,
850 cc: 5,
851 value: 0x0102_0304,
852 registered: false,
853 });
854 cv2_round_trip(EventBody::PitchBend2 {
855 group: 1,
856 channel: 8,
857 value: 0x8000_0000,
858 });
859 cv2_round_trip(EventBody::RegisteredController {
860 group: 2,
861 channel: 4,
862 bank: 1,
863 index: 2,
864 value: 0xCAFE_0000,
865 });
866 cv2_round_trip(EventBody::PolyPressure2 {
867 group: 0,
868 channel: 0,
869 note: 60,
870 pressure: 0x1234_5678,
871 });
872 cv2_round_trip(EventBody::PerNoteManagement {
873 group: 0,
874 channel: 0,
875 note: 60,
876 flags: 0x03,
877 });
878 }
879
880 #[test]
881 fn program_change_2_bank_option_round_trips() {
882 cv2_round_trip(EventBody::ProgramChange2 {
883 group: 0,
884 channel: 0,
885 program: 10,
886 bank: Some((3, 7)),
887 });
888 cv2_round_trip(EventBody::ProgramChange2 {
889 group: 0,
890 channel: 0,
891 program: 10,
892 bank: None,
893 });
894 }
895
896 #[test]
897 fn group_nibble_survives_round_trip() {
898 for group in 0..=15u8 {
899 let packet = encode_ump_channel_voice_2(&EventBody::NoteOn2 {
900 group,
901 channel: 0,
902 note: 60,
903 velocity: 1,
904 attribute_type: 0,
905 attribute: 0,
906 })
907 .unwrap();
908 let Some(EventBody::NoteOn2 { group: g, .. }) = decode_ump_channel_voice_2(packet)
909 else {
910 panic!("expected NoteOn2");
911 };
912 assert_eq!(g, group);
913 }
914 }
915
916 #[test]
917 fn non_channel_voice_2_body_does_not_encode() {
918 assert!(
920 encode_ump_channel_voice_2(&EventBody::NoteOn {
921 group: 0,
922 channel: 0,
923 note: 60,
924 velocity: 100,
925 })
926 .is_none()
927 );
928 assert!(
929 encode_ump_channel_voice_2(&EventBody::ParamChange { id: 0, value: 0.0 }).is_none()
930 );
931 }
932
933 #[test]
934 fn channel_voice_1_encodes_mt2() {
935 let packet = encode_ump_channel_voice_1(&EventBody::NoteOn {
937 group: 3,
938 channel: 5,
939 note: 60,
940 velocity: 100,
941 })
942 .expect("note on encodes");
943 assert_eq!(
944 packet,
945 [
946 (0x2 << 28) | (0x3 << 24) | (0x95 << 16) | (0x3C << 8) | 0x64,
947 0,
948 0,
949 0
950 ]
951 );
952
953 let bend = encode_ump_channel_voice_1(&EventBody::PitchBend {
955 group: 0,
956 channel: 0,
957 value: 0x2000,
958 })
959 .unwrap();
960 assert_eq!(bend[0], (0x2 << 28) | (0xE0 << 16) | 0x40);
961
962 assert!(
964 encode_ump_channel_voice_1(&EventBody::NoteOn2 {
965 group: 0,
966 channel: 0,
967 note: 60,
968 velocity: 1,
969 attribute_type: 0,
970 attribute: 0,
971 })
972 .is_none()
973 );
974 }
975
976 fn sysex7_packet(status: u8, bytes: &[u8]) -> [u32; 2] {
979 assert!(bytes.len() <= 6);
980 #[allow(clippy::cast_possible_truncation)]
982 let n = bytes.len() as u32;
983 let mut padded = [0u8; 6];
984 padded[..bytes.len()].copy_from_slice(bytes);
985 let w0 = (0x3u32 << 28)
987 | (u32::from(status) << 20)
988 | (n << 16)
989 | (u32::from(padded[0]) << 8)
990 | u32::from(padded[1]);
991 let w1 = (u32::from(padded[2]) << 24)
992 | (u32::from(padded[3]) << 16)
993 | (u32::from(padded[4]) << 8)
994 | u32::from(padded[5]);
995 [w0, w1]
996 }
997
998 #[test]
999 fn assembler_single_complete_packet() {
1000 let mut a = SysExAssembler::with_capacity(64);
1001 let packet = sysex7_packet(SYSEX_STATUS_COMPLETE, &[0x7E, 0x00, 0x06, 0x01]);
1002 match a.push_sysex7_packet(packet) {
1003 SysExFeed::Complete(p) => assert_eq!(p.bytes, &[0x7E, 0x00, 0x06, 0x01]),
1004 _ => panic!("expected Complete"),
1005 }
1006 }
1007
1008 #[test]
1009 fn assembler_multi_packet_reassembly() {
1010 let mut a = SysExAssembler::with_capacity(64);
1011 let start = sysex7_packet(SYSEX_STATUS_START, &[1, 2, 3, 4, 5, 6]);
1013 assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
1014 let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[7, 8, 9, 10, 11, 12]);
1016 assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Buffered));
1017 let end = sysex7_packet(SYSEX_STATUS_END, &[13, 14, 15]);
1019 match a.push_sysex7_packet(end) {
1020 SysExFeed::Complete(p) => assert_eq!(
1021 p.bytes,
1022 &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
1023 ),
1024 _ => panic!("expected Complete"),
1025 }
1026 }
1027
1028 #[test]
1029 fn assembler_overflow_returns_overflow_and_drops_partial() {
1030 let mut a = SysExAssembler::with_capacity(8); let start = sysex7_packet(SYSEX_STATUS_START, &[1, 2, 3, 4, 5, 6]);
1032 assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
1033 let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[7, 8, 9, 10, 11, 12]);
1035 assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Overflow));
1036 let start2 = sysex7_packet(SYSEX_STATUS_COMPLETE, &[42]);
1038 match a.push_sysex7_packet(start2) {
1039 SysExFeed::Complete(p) => assert_eq!(p.bytes, &[42]),
1040 _ => panic!("expected Complete after reset"),
1041 }
1042 }
1043
1044 #[test]
1045 fn assembler_continue_without_start_is_invalid() {
1046 let mut a = SysExAssembler::with_capacity(64);
1047 let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[1, 2, 3]);
1048 assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Invalid));
1049 }
1050
1051 #[test]
1052 fn assembler_complete_overflow_releases_slot() {
1053 let mut a = SysExAssembler::with_capacity(4);
1058 let oversize = sysex7_packet(SYSEX_STATUS_COMPLETE, &[1, 2, 3, 4, 5]);
1059 assert!(matches!(
1060 a.push_sysex7_packet(oversize),
1061 SysExFeed::Overflow
1062 ));
1063 for group in 1..=3u8 {
1066 let p = sysex7_packet_for_group(group, SYSEX_STATUS_START, &[group]);
1067 assert!(matches!(a.push_sysex7_packet(p), SysExFeed::Buffered));
1068 }
1069 let p = sysex7_packet_for_group(4, SYSEX_STATUS_START, &[4]);
1072 assert!(matches!(a.push_sysex7_packet(p), SysExFeed::Buffered));
1073 }
1074
1075 #[test]
1076 fn assembler_reset_drops_partial() {
1077 let mut a = SysExAssembler::with_capacity(64);
1078 let start = sysex7_packet(SYSEX_STATUS_START, &[1, 2, 3]);
1079 assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
1080 a.reset();
1081 let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[4]);
1083 assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Invalid));
1084 }
1085
1086 #[test]
1087 fn assembler_sysex8_complete_packet() {
1088 let mut a = SysExAssembler::with_capacity(64);
1089 let w0 = (0x5u32 << 28) | (5u32 << 16) | 0xAA;
1094 let w1 = (0xBBu32 << 24) | (0xCCu32 << 16) | (0xDDu32 << 8);
1095 match a.push_sysex8_packet([w0, w1, 0, 0]) {
1096 SysExFeed::Complete(p) => {
1097 assert_eq!(p.bytes, &[0xAA, 0xBB, 0xCC, 0xDD]);
1098 assert_eq!(p.group, 0);
1099 assert_eq!(p.stream_id, 0);
1100 }
1101 _ => panic!("expected Complete"),
1102 }
1103 }
1104
1105 #[test]
1106 fn assembler_sysex8_full_packet_and_bounds() {
1107 let mut a = SysExAssembler::with_capacity(64);
1112 let data: [u8; 13] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
1113 let w0 = (0x5u32 << 28)
1114 | (14u32 << 16)
1115 | (7u32 << 8) | u32::from(data[0]);
1117 let word = |i: usize| {
1118 (u32::from(data[i]) << 24)
1119 | (u32::from(data[i + 1]) << 16)
1120 | (u32::from(data[i + 2]) << 8)
1121 | u32::from(data[i + 3])
1122 };
1123 match a.push_sysex8_packet([w0, word(1), word(5), word(9)]) {
1124 SysExFeed::Complete(p) => {
1125 assert_eq!(p.bytes, &data);
1126 assert_eq!(p.stream_id, 7);
1127 }
1128 _ => panic!("expected Complete for a full conformant packet"),
1129 }
1130
1131 let empty = (0x5u32 << 28) | (1u32 << 16);
1133 assert!(matches!(
1134 a.push_sysex8_packet([empty, 0, 0, 0]),
1135 SysExFeed::Complete(_)
1136 ));
1137 let zero = 0x5u32 << 28;
1140 assert!(matches!(
1141 a.push_sysex8_packet([zero, 0, 0, 0]),
1142 SysExFeed::Invalid
1143 ));
1144 let fifteen = (0x5u32 << 28) | (15u32 << 16);
1145 assert!(matches!(
1146 a.push_sysex8_packet([fifteen, 0, 0, 0]),
1147 SysExFeed::Invalid
1148 ));
1149 }
1150
1151 fn sysex7_packet_for_group(group: u8, status: u8, bytes: &[u8]) -> [u32; 2] {
1153 assert!(bytes.len() <= 6);
1154 #[allow(clippy::cast_possible_truncation)]
1155 let n = bytes.len() as u32;
1156 let mut padded = [0u8; 6];
1157 padded[..bytes.len()].copy_from_slice(bytes);
1158 let w0 = (0x3u32 << 28)
1159 | (u32::from(group & 0xF) << 24)
1160 | (u32::from(status) << 20)
1161 | (n << 16)
1162 | (u32::from(padded[0]) << 8)
1163 | u32::from(padded[1]);
1164 let w1 = (u32::from(padded[2]) << 24)
1165 | (u32::from(padded[3]) << 16)
1166 | (u32::from(padded[4]) << 8)
1167 | u32::from(padded[5]);
1168 [w0, w1]
1169 }
1170
1171 #[test]
1172 fn assembler_concurrent_streams_across_groups() {
1173 let mut a = SysExAssembler::with_capacity(64);
1177
1178 let g3_start = sysex7_packet_for_group(3, SYSEX_STATUS_START, &[0x10, 0x11]);
1180 assert!(matches!(
1181 a.push_sysex7_packet(g3_start),
1182 SysExFeed::Buffered
1183 ));
1184
1185 let g7_start = sysex7_packet_for_group(7, SYSEX_STATUS_START, &[0x20, 0x21, 0x22]);
1187 assert!(matches!(
1188 a.push_sysex7_packet(g7_start),
1189 SysExFeed::Buffered
1190 ));
1191
1192 let g3_end = sysex7_packet_for_group(3, SYSEX_STATUS_END, &[0x12]);
1194 match a.push_sysex7_packet(g3_end) {
1195 SysExFeed::Complete(p) => {
1196 assert_eq!(p.group, 3);
1197 assert_eq!(p.bytes, &[0x10, 0x11, 0x12]);
1198 }
1199 _ => panic!("expected Complete on group 3"),
1200 }
1201
1202 let g7_end = sysex7_packet_for_group(7, SYSEX_STATUS_END, &[0x23, 0x24]);
1204 match a.push_sysex7_packet(g7_end) {
1205 SysExFeed::Complete(p) => {
1206 assert_eq!(p.group, 7);
1207 assert_eq!(p.bytes, &[0x20, 0x21, 0x22, 0x23, 0x24]);
1208 }
1209 _ => panic!("expected Complete on group 7"),
1210 }
1211 }
1212
1213 #[test]
1214 fn assembler_sysex8_stream_id_isolates_concurrent_streams() {
1215 let mut a = SysExAssembler::with_capacity(64);
1218
1219 let mk = |status: u8, data_bytes: u32, stream_id: u8, bytes: [u8; 4]| -> [u32; 4] {
1224 let w0 = (0x5u32 << 28)
1225 | (u32::from(status) << 20)
1226 | ((data_bytes + 1) << 16)
1227 | (u32::from(stream_id) << 8)
1228 | u32::from(bytes[0]);
1229 let w1 = (u32::from(bytes[1]) << 24)
1230 | (u32::from(bytes[2]) << 16)
1231 | (u32::from(bytes[3]) << 8);
1232 [w0, w1, 0, 0]
1233 };
1234
1235 assert!(matches!(
1237 a.push_sysex8_packet(mk(SYSEX_STATUS_START, 4, 5, [0xA0, 0xA1, 0xA2, 0xA3])),
1238 SysExFeed::Buffered
1239 ));
1240 assert!(matches!(
1242 a.push_sysex8_packet(mk(SYSEX_STATUS_START, 4, 9, [0xB0, 0xB1, 0xB2, 0xB3])),
1243 SysExFeed::Buffered
1244 ));
1245 match a.push_sysex8_packet(mk(SYSEX_STATUS_END, 1, 5, [0xA4, 0, 0, 0])) {
1247 SysExFeed::Complete(p) => {
1248 assert_eq!(p.stream_id, 5);
1249 assert_eq!(p.bytes, &[0xA0, 0xA1, 0xA2, 0xA3, 0xA4]);
1250 }
1251 _ => panic!("expected Complete on stream 5"),
1252 }
1253 match a.push_sysex8_packet(mk(SYSEX_STATUS_END, 2, 9, [0xB4, 0xB5, 0, 0])) {
1255 SysExFeed::Complete(p) => {
1256 assert_eq!(p.stream_id, 9);
1257 assert_eq!(p.bytes, &[0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5]);
1258 }
1259 _ => panic!("expected Complete on stream 9"),
1260 }
1261 }
1262
1263 #[track_caller]
1268 fn sysex7_encode_round_trip(group: u8, payload: &[u8]) {
1269 let mut a = SysExAssembler::with_capacity(payload.len().max(1));
1270 let total = sysex7_packet_count(payload.len());
1271 for i in 0..total {
1272 let packet = encode_sysex7_packet(group, payload, i).expect("in-range packet");
1273 match a.push_sysex7_packet(packet) {
1274 SysExFeed::Buffered => assert!(i + 1 < total, "premature Buffered"),
1275 SysExFeed::Complete(p) => {
1276 assert_eq!(i + 1, total, "Complete before the last packet");
1277 assert_eq!(p.group, group);
1278 assert_eq!(p.bytes, payload);
1279 }
1280 _ => panic!("assembler rejected encoder output"),
1281 }
1282 }
1283 assert!(encode_sysex7_packet(group, payload, total).is_none());
1284 }
1285
1286 #[test]
1287 fn sysex7_encoder_round_trips_through_assembler() {
1288 sysex7_encode_round_trip(0, &[]);
1289 sysex7_encode_round_trip(0, &[0x7E]);
1290 sysex7_encode_round_trip(3, &[1, 2, 3, 4, 5, 6]); sysex7_encode_round_trip(7, &[1, 2, 3, 4, 5, 6, 7]); sysex7_encode_round_trip(15, &(0..=40u8).collect::<Vec<_>>()); }
1294
1295 #[test]
1296 fn sysex7_encoder_bits_match_spec() {
1297 let packet = encode_sysex7_packet(5, &[0x7E, 0x09], 0).unwrap();
1299 assert_eq!(
1300 packet,
1301 [(0x3 << 28) | (5 << 24) | (2 << 16) | (0x7E << 8) | 0x09, 0]
1302 );
1303 let payload = [1, 2, 3, 4, 5, 6, 7];
1305 let start = encode_sysex7_packet(0, &payload, 0).unwrap();
1306 assert_eq!(
1307 (start[0] >> 20) & 0xF,
1308 u32::from(SYSEX_STATUS_START),
1309 "first of a chain is Start"
1310 );
1311 let end = encode_sysex7_packet(0, &payload, 1).unwrap();
1312 assert_eq!((end[0] >> 20) & 0xF, u32::from(SYSEX_STATUS_END));
1313 assert_eq!((end[0] >> 16) & 0xF, 1, "End carries the 1 leftover byte");
1314 assert_eq!((end[0] >> 8) & 0xFF, 7);
1315 }
1316
1317 #[test]
1318 fn sysex7_encoder_masks_to_7_bit() {
1319 let packet = encode_sysex7_packet(0, &[0xFF], 0).unwrap();
1320 assert_eq!((packet[0] >> 8) & 0xFF, 0x7F);
1321 }
1322
1323 #[test]
1324 fn assembler_lru_evicts_when_slots_exhausted() {
1325 let slots_u8 = u8::try_from(SYSEX_ASSEMBLER_SLOTS).expect("slot count fits u8");
1328 let mut a = SysExAssembler::with_capacity(64);
1329 for group in 0..slots_u8 {
1330 let start = sysex7_packet_for_group(group, SYSEX_STATUS_START, &[group]);
1331 assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
1332 }
1333 let new_group = slots_u8;
1335 let evictor = sysex7_packet_for_group(new_group, SYSEX_STATUS_START, &[new_group]);
1336 assert!(matches!(a.push_sysex7_packet(evictor), SysExFeed::Buffered));
1337 let g0_end = sysex7_packet_for_group(0, SYSEX_STATUS_END, &[0x99]);
1339 assert!(matches!(a.push_sysex7_packet(g0_end), SysExFeed::Invalid));
1340 let new_end = sysex7_packet_for_group(new_group, SYSEX_STATUS_END, &[0xEE]);
1342 match a.push_sysex7_packet(new_end) {
1343 SysExFeed::Complete(p) => {
1344 assert_eq!(p.group, new_group);
1345 assert_eq!(p.bytes, &[new_group, 0xEE]);
1346 }
1347 _ => panic!("expected Complete on evicting group"),
1348 }
1349 }
1350}