use crate::events::EventBody;
const MT_CHANNEL_VOICE_2: u8 = 0x4;
const MT_SYSEX_7: u8 = 0x3;
const MT_SYSEX_8: u8 = 0x5;
const SYSEX_STATUS_COMPLETE: u8 = 0x0;
const SYSEX_STATUS_START: u8 = 0x1;
const SYSEX_STATUS_CONTINUE: u8 = 0x2;
const SYSEX_STATUS_END: u8 = 0x3;
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn decode_ump_channel_voice_2(words: [u32; 4]) -> Option<EventBody> {
let w0 = words[0];
let w1 = words[1];
let mt = ((w0 >> 28) & 0xF) as u8;
if mt != MT_CHANNEL_VOICE_2 {
return None;
}
let group = ((w0 >> 24) & 0xF) as u8;
let status = ((w0 >> 20) & 0xF) as u8;
let channel = ((w0 >> 16) & 0xF) as u8;
let byte_a = ((w0 >> 8) & 0xFF) as u8; let byte_b = (w0 & 0xFF) as u8; let body = match status {
0x8 => EventBody::NoteOff2 {
group,
channel,
note: byte_a & 0x7F,
velocity: (w1 >> 16) as u16,
attribute_type: byte_b,
attribute: (w1 & 0xFFFF) as u16,
},
0x9 => EventBody::NoteOn2 {
group,
channel,
note: byte_a & 0x7F,
velocity: (w1 >> 16) as u16,
attribute_type: byte_b,
attribute: (w1 & 0xFFFF) as u16,
},
0xA => EventBody::PolyPressure2 {
group,
channel,
note: byte_a & 0x7F,
pressure: w1,
},
0x0 | 0x1 => EventBody::PerNoteCC {
group,
channel,
note: byte_a & 0x7F,
cc: byte_b,
value: w1,
registered: status == 0x0,
},
0x6 => EventBody::PerNotePitchBend {
group,
channel,
note: byte_a & 0x7F,
value: w1,
},
0xF => EventBody::PerNoteManagement {
group,
channel,
note: byte_a & 0x7F,
flags: byte_b,
},
0xB => EventBody::ControlChange2 {
group,
channel,
cc: byte_a & 0x7F,
value: w1,
},
0xD => EventBody::ChannelPressure2 {
group,
channel,
pressure: w1,
},
0xE => EventBody::PitchBend2 {
group,
channel,
value: w1,
},
0x2 => EventBody::RegisteredController {
group,
channel,
bank: byte_a & 0x7F,
index: byte_b & 0x7F,
value: w1,
},
0x3 => EventBody::AssignableController {
group,
channel,
bank: byte_a & 0x7F,
index: byte_b & 0x7F,
value: w1,
},
0xC => EventBody::ProgramChange2 {
group,
channel,
program: (w1 >> 24) as u8 & 0x7F,
bank: if w0 & 0x01 == 1 {
Some(((w1 >> 8) as u8 & 0x7F, w1 as u8 & 0x7F))
} else {
None
},
},
_ => return None,
};
Some(body)
}
#[must_use]
pub fn encode_ump_channel_voice_2(body: &EventBody) -> Option<[u32; 4]> {
let (status, byte_a, byte_b, w1): (u8, u8, u8, u32) = match *body {
EventBody::NoteOff2 {
note,
velocity,
attribute_type,
attribute,
..
} => (0x8, note, attribute_type, cv2_value16(velocity, attribute)),
EventBody::NoteOn2 {
note,
velocity,
attribute_type,
attribute,
..
} => (0x9, note, attribute_type, cv2_value16(velocity, attribute)),
EventBody::PolyPressure2 { note, pressure, .. } => (0xA, note, 0, pressure),
EventBody::PerNoteCC {
note,
cc,
value,
registered,
..
} => (u8::from(!registered), note, cc, value),
EventBody::PerNotePitchBend { note, value, .. } => (0x6, note, 0, value),
EventBody::PerNoteManagement { note, flags, .. } => (0xF, note, flags, 0),
EventBody::ControlChange2 { cc, value, .. } => (0xB, cc, 0, value),
EventBody::ChannelPressure2 { pressure, .. } => (0xD, 0, 0, pressure),
EventBody::PitchBend2 { value, .. } => (0xE, 0, 0, value),
EventBody::RegisteredController {
bank, index, value, ..
} => (0x2, bank, index & 0x7F, value),
EventBody::AssignableController {
bank, index, value, ..
} => (0x3, bank, index & 0x7F, value),
EventBody::ProgramChange2 { program, bank, .. } => {
let (option, w1) = match bank {
Some((msb, lsb)) => (
0x01,
(u32::from(program & 0x7F) << 24)
| (u32::from(msb & 0x7F) << 8)
| u32::from(lsb & 0x7F),
),
None => (0x00, u32::from(program & 0x7F) << 24),
};
(0xC, 0, option, w1)
}
_ => return None,
};
let (group, channel) = cv2_addr(body)?;
let w0 = (0x4 << 28)
| (u32::from(group & 0x0F) << 24)
| (u32::from(status) << 20)
| (u32::from(channel & 0x0F) << 16)
| (u32::from(byte_a & 0x7F) << 8)
| u32::from(byte_b);
Some([w0, w1, 0, 0])
}
const fn cv2_value16(hi: u16, lo: u16) -> u32 {
((hi as u32) << 16) | lo as u32
}
const MT_CHANNEL_VOICE_1: u8 = 0x2;
#[must_use]
pub fn encode_ump_channel_voice_1(body: &EventBody) -> Option<[u32; 4]> {
let (opcode, channel, group, data1, data2): (u8, u8, u8, u8, u8) = match *body {
EventBody::NoteOff {
group,
channel,
note,
velocity,
} => (0x8, channel, group, note, velocity),
EventBody::NoteOn {
group,
channel,
note,
velocity,
} => (0x9, channel, group, note, velocity),
EventBody::Aftertouch {
group,
channel,
note,
pressure,
} => (0xA, channel, group, note, pressure),
EventBody::ControlChange {
group,
channel,
cc,
value,
} => (0xB, channel, group, cc, value),
EventBody::ProgramChange {
group,
channel,
program,
} => (0xC, channel, group, program, 0),
EventBody::ChannelPressure {
group,
channel,
pressure,
} => (0xD, channel, group, pressure, 0),
EventBody::PitchBend {
group,
channel,
value,
} => {
let lsb = u8::try_from(value & 0x7F).unwrap_or(0);
let msb = u8::try_from((value >> 7) & 0x7F).unwrap_or(0);
(0xE, channel, group, lsb, msb)
}
_ => return None,
};
let w0 = (u32::from(MT_CHANNEL_VOICE_1) << 28)
| (u32::from(group & 0x0F) << 24)
| (u32::from((opcode << 4) | (channel & 0x0F)) << 16)
| (u32::from(data1 & 0x7F) << 8)
| u32::from(data2 & 0x7F);
Some([w0, 0, 0, 0])
}
const SYSEX_7_BYTES_PER_PACKET: usize = 6;
#[must_use]
pub const fn sysex7_packet_count(payload_len: usize) -> usize {
if payload_len == 0 {
1
} else {
payload_len.div_ceil(SYSEX_7_BYTES_PER_PACKET)
}
}
#[must_use]
pub fn encode_sysex7_packet(group: u8, payload: &[u8], packet_index: usize) -> Option<[u32; 2]> {
let total = sysex7_packet_count(payload.len());
if packet_index >= total {
return None;
}
let start = packet_index * SYSEX_7_BYTES_PER_PACKET;
let chunk = &payload[start..(start + SYSEX_7_BYTES_PER_PACKET).min(payload.len())];
let status = match (total, packet_index) {
(1, _) => SYSEX_STATUS_COMPLETE,
(_, 0) => SYSEX_STATUS_START,
(_, i) if i == total - 1 => SYSEX_STATUS_END,
_ => SYSEX_STATUS_CONTINUE,
};
let mut padded = [0u8; SYSEX_7_BYTES_PER_PACKET];
for (dst, src) in padded.iter_mut().zip(chunk) {
*dst = src & 0x7F;
}
#[allow(clippy::cast_possible_truncation)]
let n = chunk.len() as u32;
let w0 = (u32::from(MT_SYSEX_7) << 28)
| (u32::from(group & 0x0F) << 24)
| (u32::from(status) << 20)
| (n << 16)
| (u32::from(padded[0]) << 8)
| u32::from(padded[1]);
let w1 = (u32::from(padded[2]) << 24)
| (u32::from(padded[3]) << 16)
| (u32::from(padded[4]) << 8)
| u32::from(padded[5]);
Some([w0, w1])
}
fn cv2_addr(body: &EventBody) -> Option<(u8, u8)> {
Some(match *body {
EventBody::NoteOff2 { group, channel, .. }
| EventBody::NoteOn2 { group, channel, .. }
| EventBody::PolyPressure2 { group, channel, .. }
| EventBody::PerNoteCC { group, channel, .. }
| EventBody::PerNotePitchBend { group, channel, .. }
| EventBody::PerNoteManagement { group, channel, .. }
| EventBody::ControlChange2 { group, channel, .. }
| EventBody::ChannelPressure2 { group, channel, .. }
| EventBody::PitchBend2 { group, channel, .. }
| EventBody::RegisteredController { group, channel, .. }
| EventBody::AssignableController { group, channel, .. }
| EventBody::ProgramChange2 { group, channel, .. } => (group, channel),
_ => return None,
})
}
pub struct SysExPacket<'a> {
pub group: u8,
pub stream_id: u8,
pub bytes: &'a [u8],
}
pub enum SysExFeed<'a> {
Buffered,
Complete(SysExPacket<'a>),
Invalid,
Overflow,
}
pub const SYSEX_ASSEMBLER_SLOTS: usize = 4;
struct StreamSlot {
buffer: Vec<u8>,
group: u8,
stream_id: u8,
in_progress: bool,
in_use: bool,
last_touch: u64,
}
pub struct SysExAssembler {
slots: [StreamSlot; SYSEX_ASSEMBLER_SLOTS],
touch_counter: u64,
}
impl SysExAssembler {
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
let slots = std::array::from_fn(|_| StreamSlot {
buffer: Vec::with_capacity(capacity),
group: 0,
stream_id: 0,
in_progress: false,
in_use: false,
last_touch: 0,
});
Self {
slots,
touch_counter: 0,
}
}
pub fn reset(&mut self) {
for slot in &mut self.slots {
slot.buffer.clear();
slot.in_progress = false;
slot.in_use = false;
slot.last_touch = 0;
}
self.touch_counter = 0;
}
fn find_slot(&self, group: u8, stream_id: u8) -> Option<usize> {
self.slots
.iter()
.position(|s| s.in_use && s.group == group && s.stream_id == stream_id)
}
fn claim_slot(&mut self, group: u8, stream_id: u8) -> usize {
let idx = self
.slots
.iter()
.position(|s| !s.in_use)
.unwrap_or_else(|| {
self.slots
.iter()
.enumerate()
.min_by_key(|(_, s)| s.last_touch)
.map(|(i, _)| i)
.expect("non-empty slot table")
});
let slot = &mut self.slots[idx];
slot.buffer.clear();
slot.group = group;
slot.stream_id = stream_id;
slot.in_use = true;
slot.in_progress = false;
idx
}
#[allow(clippy::cast_possible_truncation)] pub fn push_sysex7_packet(&mut self, words: [u32; 2]) -> SysExFeed<'_> {
let w0 = words[0];
let w1 = words[1];
let mt = ((w0 >> 28) & 0xF) as u8;
if mt != MT_SYSEX_7 {
return SysExFeed::Invalid;
}
let group = ((w0 >> 24) & 0xF) as u8;
let status = ((w0 >> 20) & 0xF) as u8;
let n = ((w0 >> 16) & 0xF) as u8;
if n > 6 {
return SysExFeed::Invalid;
}
let raw = [
((w0 >> 8) & 0xFF) as u8,
(w0 & 0xFF) as u8,
((w1 >> 24) & 0xFF) as u8,
((w1 >> 16) & 0xFF) as u8,
((w1 >> 8) & 0xFF) as u8,
(w1 & 0xFF) as u8,
];
self.feed_payload(group, 0, status, &raw[..n as usize])
}
#[allow(clippy::cast_possible_truncation)] pub fn push_sysex8_packet(&mut self, words: [u32; 4]) -> SysExFeed<'_> {
let w0 = words[0];
let mt = ((w0 >> 28) & 0xF) as u8;
if mt != MT_SYSEX_8 {
return SysExFeed::Invalid;
}
let group = ((w0 >> 24) & 0xF) as u8;
let status = ((w0 >> 20) & 0xF) as u8;
let n = ((w0 >> 16) & 0xF) as u8;
let stream_id = ((w0 >> 8) & 0xFF) as u8;
if n == 0 || n > 14 {
return SysExFeed::Invalid;
}
let data_len = usize::from(n - 1);
let raw = [
(w0 & 0xFF) as u8, ((words[1] >> 24) & 0xFF) as u8,
((words[1] >> 16) & 0xFF) as u8,
((words[1] >> 8) & 0xFF) as u8,
(words[1] & 0xFF) as u8,
((words[2] >> 24) & 0xFF) as u8,
((words[2] >> 16) & 0xFF) as u8,
((words[2] >> 8) & 0xFF) as u8,
(words[2] & 0xFF) as u8,
((words[3] >> 24) & 0xFF) as u8,
((words[3] >> 16) & 0xFF) as u8,
((words[3] >> 8) & 0xFF) as u8,
(words[3] & 0xFF) as u8,
];
self.feed_payload(group, stream_id, status, &raw[..data_len])
}
fn feed_payload(
&mut self,
group: u8,
stream_id: u8,
status: u8,
bytes: &[u8],
) -> SysExFeed<'_> {
self.touch_counter += 1;
let now = self.touch_counter;
match status {
SYSEX_STATUS_COMPLETE => {
let idx = match self.find_slot(group, stream_id) {
Some(i) => i,
None => self.claim_slot(group, stream_id),
};
let slot = &mut self.slots[idx];
slot.buffer.clear();
if slot.buffer.capacity() < bytes.len() {
slot.in_progress = false;
slot.in_use = false;
slot.last_touch = now;
return SysExFeed::Overflow;
}
slot.buffer.extend_from_slice(bytes);
slot.in_progress = false;
slot.last_touch = now;
SysExFeed::Complete(SysExPacket {
group,
stream_id,
bytes: &slot.buffer,
})
}
SYSEX_STATUS_START => {
let idx = match self.find_slot(group, stream_id) {
Some(i) => i,
None => self.claim_slot(group, stream_id),
};
let slot = &mut self.slots[idx];
slot.buffer.clear();
if slot.buffer.capacity() < bytes.len() {
slot.in_progress = false;
slot.in_use = false;
slot.last_touch = now;
return SysExFeed::Overflow;
}
slot.buffer.extend_from_slice(bytes);
slot.in_progress = true;
slot.last_touch = now;
SysExFeed::Buffered
}
SYSEX_STATUS_CONTINUE | SYSEX_STATUS_END => {
let Some(idx) = self.find_slot(group, stream_id) else {
return SysExFeed::Invalid;
};
let slot = &mut self.slots[idx];
if !slot.in_progress {
return SysExFeed::Invalid;
}
if slot.buffer.len() + bytes.len() > slot.buffer.capacity() {
slot.buffer.clear();
slot.in_progress = false;
slot.in_use = false;
slot.last_touch = now;
return SysExFeed::Overflow;
}
slot.buffer.extend_from_slice(bytes);
slot.last_touch = now;
if status == SYSEX_STATUS_END {
slot.in_progress = false;
SysExFeed::Complete(SysExPacket {
group,
stream_id,
bytes: &slot.buffer,
})
} else {
SysExFeed::Buffered
}
}
_ => SysExFeed::Invalid,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_note_on_2() {
let w0 = (0x4u32 << 28) | (0x9u32 << 20) | (0x2u32 << 16) | (60u32 << 8) | 0x03;
let w1 = (0x8000u32 << 16) | 0x1234;
let decoded = decode_ump_channel_voice_2([w0, w1, 0, 0]).expect("decodes");
if let EventBody::NoteOn2 {
channel,
note,
velocity,
attribute_type,
attribute,
..
} = decoded
{
assert_eq!(channel, 2);
assert_eq!(note, 60);
assert_eq!(velocity, 0x8000);
assert_eq!(attribute_type, 3);
assert_eq!(attribute, 0x1234);
} else {
panic!("expected NoteOn2");
}
}
#[test]
fn non_channel_voice_packet_returns_none() {
assert!(decode_ump_channel_voice_2([0x0000_0000, 0, 0, 0]).is_none());
assert!(decode_ump_channel_voice_2([0x3000_0000, 0, 0, 0]).is_none());
}
#[track_caller]
fn cv2_round_trip(body: EventBody) {
let packet = encode_ump_channel_voice_2(&body).expect("2.0 channel voice encodes");
let decoded = decode_ump_channel_voice_2(packet).expect("decodes");
let re_encoded = encode_ump_channel_voice_2(&decoded).expect("re-encodes");
assert_eq!(packet, re_encoded, "round trip mismatch for {body:?}");
}
#[test]
fn encode_bits_match_spec() {
let packet = encode_ump_channel_voice_2(&EventBody::NoteOn2 {
group: 4,
channel: 2,
note: 64,
velocity: 0xBEEF,
attribute_type: 3,
attribute: 0x1234,
})
.unwrap();
assert_eq!(
packet,
[
(0x4 << 28) | (4 << 24) | (0x9 << 20) | (2 << 16) | (64 << 8) | 0x03,
(0xBEEF << 16) | 0x1234,
0,
0,
]
);
}
#[test]
fn channel_voice_2_round_trips() {
cv2_round_trip(EventBody::NoteOn2 {
group: 4,
channel: 2,
note: 64,
velocity: 0xBEEF,
attribute_type: 3,
attribute: 0x1234,
});
cv2_round_trip(EventBody::NoteOff2 {
group: 0,
channel: 15,
note: 127,
velocity: 0,
attribute_type: 0,
attribute: 0,
});
cv2_round_trip(EventBody::ControlChange2 {
group: 15,
channel: 0,
cc: 11,
value: 0xDEAD_BEEF,
});
cv2_round_trip(EventBody::PerNoteCC {
group: 1,
channel: 3,
note: 72,
cc: 5,
value: 0x0102_0304,
registered: true,
});
cv2_round_trip(EventBody::PerNoteCC {
group: 1,
channel: 3,
note: 72,
cc: 5,
value: 0x0102_0304,
registered: false,
});
cv2_round_trip(EventBody::PitchBend2 {
group: 1,
channel: 8,
value: 0x8000_0000,
});
cv2_round_trip(EventBody::RegisteredController {
group: 2,
channel: 4,
bank: 1,
index: 2,
value: 0xCAFE_0000,
});
cv2_round_trip(EventBody::PolyPressure2 {
group: 0,
channel: 0,
note: 60,
pressure: 0x1234_5678,
});
cv2_round_trip(EventBody::PerNoteManagement {
group: 0,
channel: 0,
note: 60,
flags: 0x03,
});
}
#[test]
fn program_change_2_bank_option_round_trips() {
cv2_round_trip(EventBody::ProgramChange2 {
group: 0,
channel: 0,
program: 10,
bank: Some((3, 7)),
});
cv2_round_trip(EventBody::ProgramChange2 {
group: 0,
channel: 0,
program: 10,
bank: None,
});
}
#[test]
fn group_nibble_survives_round_trip() {
for group in 0..=15u8 {
let packet = encode_ump_channel_voice_2(&EventBody::NoteOn2 {
group,
channel: 0,
note: 60,
velocity: 1,
attribute_type: 0,
attribute: 0,
})
.unwrap();
let Some(EventBody::NoteOn2 { group: g, .. }) = decode_ump_channel_voice_2(packet)
else {
panic!("expected NoteOn2");
};
assert_eq!(g, group);
}
}
#[test]
fn non_channel_voice_2_body_does_not_encode() {
assert!(
encode_ump_channel_voice_2(&EventBody::NoteOn {
group: 0,
channel: 0,
note: 60,
velocity: 100,
})
.is_none()
);
assert!(
encode_ump_channel_voice_2(&EventBody::ParamChange { id: 0, value: 0.0 }).is_none()
);
}
#[test]
fn channel_voice_1_encodes_mt2() {
let packet = encode_ump_channel_voice_1(&EventBody::NoteOn {
group: 3,
channel: 5,
note: 60,
velocity: 100,
})
.expect("note on encodes");
assert_eq!(
packet,
[
(0x2 << 28) | (0x3 << 24) | (0x95 << 16) | (0x3C << 8) | 0x64,
0,
0,
0
]
);
let bend = encode_ump_channel_voice_1(&EventBody::PitchBend {
group: 0,
channel: 0,
value: 0x2000,
})
.unwrap();
assert_eq!(bend[0], (0x2 << 28) | (0xE0 << 16) | 0x40);
assert!(
encode_ump_channel_voice_1(&EventBody::NoteOn2 {
group: 0,
channel: 0,
note: 60,
velocity: 1,
attribute_type: 0,
attribute: 0,
})
.is_none()
);
}
fn sysex7_packet(status: u8, bytes: &[u8]) -> [u32; 2] {
assert!(bytes.len() <= 6);
#[allow(clippy::cast_possible_truncation)]
let n = bytes.len() as u32;
let mut padded = [0u8; 6];
padded[..bytes.len()].copy_from_slice(bytes);
let w0 = (0x3u32 << 28)
| (u32::from(status) << 20)
| (n << 16)
| (u32::from(padded[0]) << 8)
| u32::from(padded[1]);
let w1 = (u32::from(padded[2]) << 24)
| (u32::from(padded[3]) << 16)
| (u32::from(padded[4]) << 8)
| u32::from(padded[5]);
[w0, w1]
}
#[test]
fn assembler_single_complete_packet() {
let mut a = SysExAssembler::with_capacity(64);
let packet = sysex7_packet(SYSEX_STATUS_COMPLETE, &[0x7E, 0x00, 0x06, 0x01]);
match a.push_sysex7_packet(packet) {
SysExFeed::Complete(p) => assert_eq!(p.bytes, &[0x7E, 0x00, 0x06, 0x01]),
_ => panic!("expected Complete"),
}
}
#[test]
fn assembler_multi_packet_reassembly() {
let mut a = SysExAssembler::with_capacity(64);
let start = sysex7_packet(SYSEX_STATUS_START, &[1, 2, 3, 4, 5, 6]);
assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[7, 8, 9, 10, 11, 12]);
assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Buffered));
let end = sysex7_packet(SYSEX_STATUS_END, &[13, 14, 15]);
match a.push_sysex7_packet(end) {
SysExFeed::Complete(p) => assert_eq!(
p.bytes,
&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
),
_ => panic!("expected Complete"),
}
}
#[test]
fn assembler_overflow_returns_overflow_and_drops_partial() {
let mut a = SysExAssembler::with_capacity(8); let start = sysex7_packet(SYSEX_STATUS_START, &[1, 2, 3, 4, 5, 6]);
assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[7, 8, 9, 10, 11, 12]);
assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Overflow));
let start2 = sysex7_packet(SYSEX_STATUS_COMPLETE, &[42]);
match a.push_sysex7_packet(start2) {
SysExFeed::Complete(p) => assert_eq!(p.bytes, &[42]),
_ => panic!("expected Complete after reset"),
}
}
#[test]
fn assembler_continue_without_start_is_invalid() {
let mut a = SysExAssembler::with_capacity(64);
let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[1, 2, 3]);
assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Invalid));
}
#[test]
fn assembler_complete_overflow_releases_slot() {
let mut a = SysExAssembler::with_capacity(4);
let oversize = sysex7_packet(SYSEX_STATUS_COMPLETE, &[1, 2, 3, 4, 5]);
assert!(matches!(
a.push_sysex7_packet(oversize),
SysExFeed::Overflow
));
for group in 1..=3u8 {
let p = sysex7_packet_for_group(group, SYSEX_STATUS_START, &[group]);
assert!(matches!(a.push_sysex7_packet(p), SysExFeed::Buffered));
}
let p = sysex7_packet_for_group(4, SYSEX_STATUS_START, &[4]);
assert!(matches!(a.push_sysex7_packet(p), SysExFeed::Buffered));
}
#[test]
fn assembler_reset_drops_partial() {
let mut a = SysExAssembler::with_capacity(64);
let start = sysex7_packet(SYSEX_STATUS_START, &[1, 2, 3]);
assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
a.reset();
let cont = sysex7_packet(SYSEX_STATUS_CONTINUE, &[4]);
assert!(matches!(a.push_sysex7_packet(cont), SysExFeed::Invalid));
}
#[test]
fn assembler_sysex8_complete_packet() {
let mut a = SysExAssembler::with_capacity(64);
let w0 = (0x5u32 << 28) | (5u32 << 16) | 0xAA;
let w1 = (0xBBu32 << 24) | (0xCCu32 << 16) | (0xDDu32 << 8);
match a.push_sysex8_packet([w0, w1, 0, 0]) {
SysExFeed::Complete(p) => {
assert_eq!(p.bytes, &[0xAA, 0xBB, 0xCC, 0xDD]);
assert_eq!(p.group, 0);
assert_eq!(p.stream_id, 0);
}
_ => panic!("expected Complete"),
}
}
#[test]
fn assembler_sysex8_full_packet_and_bounds() {
let mut a = SysExAssembler::with_capacity(64);
let data: [u8; 13] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
let w0 = (0x5u32 << 28)
| (14u32 << 16)
| (7u32 << 8) | u32::from(data[0]);
let word = |i: usize| {
(u32::from(data[i]) << 24)
| (u32::from(data[i + 1]) << 16)
| (u32::from(data[i + 2]) << 8)
| u32::from(data[i + 3])
};
match a.push_sysex8_packet([w0, word(1), word(5), word(9)]) {
SysExFeed::Complete(p) => {
assert_eq!(p.bytes, &data);
assert_eq!(p.stream_id, 7);
}
_ => panic!("expected Complete for a full conformant packet"),
}
let empty = (0x5u32 << 28) | (1u32 << 16);
assert!(matches!(
a.push_sysex8_packet([empty, 0, 0, 0]),
SysExFeed::Complete(_)
));
let zero = 0x5u32 << 28;
assert!(matches!(
a.push_sysex8_packet([zero, 0, 0, 0]),
SysExFeed::Invalid
));
let fifteen = (0x5u32 << 28) | (15u32 << 16);
assert!(matches!(
a.push_sysex8_packet([fifteen, 0, 0, 0]),
SysExFeed::Invalid
));
}
fn sysex7_packet_for_group(group: u8, status: u8, bytes: &[u8]) -> [u32; 2] {
assert!(bytes.len() <= 6);
#[allow(clippy::cast_possible_truncation)]
let n = bytes.len() as u32;
let mut padded = [0u8; 6];
padded[..bytes.len()].copy_from_slice(bytes);
let w0 = (0x3u32 << 28)
| (u32::from(group & 0xF) << 24)
| (u32::from(status) << 20)
| (n << 16)
| (u32::from(padded[0]) << 8)
| u32::from(padded[1]);
let w1 = (u32::from(padded[2]) << 24)
| (u32::from(padded[3]) << 16)
| (u32::from(padded[4]) << 8)
| u32::from(padded[5]);
[w0, w1]
}
#[test]
fn assembler_concurrent_streams_across_groups() {
let mut a = SysExAssembler::with_capacity(64);
let g3_start = sysex7_packet_for_group(3, SYSEX_STATUS_START, &[0x10, 0x11]);
assert!(matches!(
a.push_sysex7_packet(g3_start),
SysExFeed::Buffered
));
let g7_start = sysex7_packet_for_group(7, SYSEX_STATUS_START, &[0x20, 0x21, 0x22]);
assert!(matches!(
a.push_sysex7_packet(g7_start),
SysExFeed::Buffered
));
let g3_end = sysex7_packet_for_group(3, SYSEX_STATUS_END, &[0x12]);
match a.push_sysex7_packet(g3_end) {
SysExFeed::Complete(p) => {
assert_eq!(p.group, 3);
assert_eq!(p.bytes, &[0x10, 0x11, 0x12]);
}
_ => panic!("expected Complete on group 3"),
}
let g7_end = sysex7_packet_for_group(7, SYSEX_STATUS_END, &[0x23, 0x24]);
match a.push_sysex7_packet(g7_end) {
SysExFeed::Complete(p) => {
assert_eq!(p.group, 7);
assert_eq!(p.bytes, &[0x20, 0x21, 0x22, 0x23, 0x24]);
}
_ => panic!("expected Complete on group 7"),
}
}
#[test]
fn assembler_sysex8_stream_id_isolates_concurrent_streams() {
let mut a = SysExAssembler::with_capacity(64);
let mk = |status: u8, data_bytes: u32, stream_id: u8, bytes: [u8; 4]| -> [u32; 4] {
let w0 = (0x5u32 << 28)
| (u32::from(status) << 20)
| ((data_bytes + 1) << 16)
| (u32::from(stream_id) << 8)
| u32::from(bytes[0]);
let w1 = (u32::from(bytes[1]) << 24)
| (u32::from(bytes[2]) << 16)
| (u32::from(bytes[3]) << 8);
[w0, w1, 0, 0]
};
assert!(matches!(
a.push_sysex8_packet(mk(SYSEX_STATUS_START, 4, 5, [0xA0, 0xA1, 0xA2, 0xA3])),
SysExFeed::Buffered
));
assert!(matches!(
a.push_sysex8_packet(mk(SYSEX_STATUS_START, 4, 9, [0xB0, 0xB1, 0xB2, 0xB3])),
SysExFeed::Buffered
));
match a.push_sysex8_packet(mk(SYSEX_STATUS_END, 1, 5, [0xA4, 0, 0, 0])) {
SysExFeed::Complete(p) => {
assert_eq!(p.stream_id, 5);
assert_eq!(p.bytes, &[0xA0, 0xA1, 0xA2, 0xA3, 0xA4]);
}
_ => panic!("expected Complete on stream 5"),
}
match a.push_sysex8_packet(mk(SYSEX_STATUS_END, 2, 9, [0xB4, 0xB5, 0, 0])) {
SysExFeed::Complete(p) => {
assert_eq!(p.stream_id, 9);
assert_eq!(p.bytes, &[0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5]);
}
_ => panic!("expected Complete on stream 9"),
}
}
#[track_caller]
fn sysex7_encode_round_trip(group: u8, payload: &[u8]) {
let mut a = SysExAssembler::with_capacity(payload.len().max(1));
let total = sysex7_packet_count(payload.len());
for i in 0..total {
let packet = encode_sysex7_packet(group, payload, i).expect("in-range packet");
match a.push_sysex7_packet(packet) {
SysExFeed::Buffered => assert!(i + 1 < total, "premature Buffered"),
SysExFeed::Complete(p) => {
assert_eq!(i + 1, total, "Complete before the last packet");
assert_eq!(p.group, group);
assert_eq!(p.bytes, payload);
}
_ => panic!("assembler rejected encoder output"),
}
}
assert!(encode_sysex7_packet(group, payload, total).is_none());
}
#[test]
fn sysex7_encoder_round_trips_through_assembler() {
sysex7_encode_round_trip(0, &[]);
sysex7_encode_round_trip(0, &[0x7E]);
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<_>>()); }
#[test]
fn sysex7_encoder_bits_match_spec() {
let packet = encode_sysex7_packet(5, &[0x7E, 0x09], 0).unwrap();
assert_eq!(
packet,
[(0x3 << 28) | (5 << 24) | (2 << 16) | (0x7E << 8) | 0x09, 0]
);
let payload = [1, 2, 3, 4, 5, 6, 7];
let start = encode_sysex7_packet(0, &payload, 0).unwrap();
assert_eq!(
(start[0] >> 20) & 0xF,
u32::from(SYSEX_STATUS_START),
"first of a chain is Start"
);
let end = encode_sysex7_packet(0, &payload, 1).unwrap();
assert_eq!((end[0] >> 20) & 0xF, u32::from(SYSEX_STATUS_END));
assert_eq!((end[0] >> 16) & 0xF, 1, "End carries the 1 leftover byte");
assert_eq!((end[0] >> 8) & 0xFF, 7);
}
#[test]
fn sysex7_encoder_masks_to_7_bit() {
let packet = encode_sysex7_packet(0, &[0xFF], 0).unwrap();
assert_eq!((packet[0] >> 8) & 0xFF, 0x7F);
}
#[test]
fn assembler_lru_evicts_when_slots_exhausted() {
let slots_u8 = u8::try_from(SYSEX_ASSEMBLER_SLOTS).expect("slot count fits u8");
let mut a = SysExAssembler::with_capacity(64);
for group in 0..slots_u8 {
let start = sysex7_packet_for_group(group, SYSEX_STATUS_START, &[group]);
assert!(matches!(a.push_sysex7_packet(start), SysExFeed::Buffered));
}
let new_group = slots_u8;
let evictor = sysex7_packet_for_group(new_group, SYSEX_STATUS_START, &[new_group]);
assert!(matches!(a.push_sysex7_packet(evictor), SysExFeed::Buffered));
let g0_end = sysex7_packet_for_group(0, SYSEX_STATUS_END, &[0x99]);
assert!(matches!(a.push_sysex7_packet(g0_end), SysExFeed::Invalid));
let new_end = sysex7_packet_for_group(new_group, SYSEX_STATUS_END, &[0xEE]);
match a.push_sysex7_packet(new_end) {
SysExFeed::Complete(p) => {
assert_eq!(p.group, new_group);
assert_eq!(p.bytes, &[new_group, 0xEE]);
}
_ => panic!("expected Complete on evicting group"),
}
}
}