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)
}
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 > 13 {
return SysExFeed::Invalid;
}
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[..n as usize])
}
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());
}
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) | (4u32 << 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"),
}
}
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, n: u32, stream_id: u8, bytes: [u8; 4]| -> [u32; 4] {
let w0 = (0x5u32 << 28)
| (u32::from(status) << 20)
| (n << 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"),
}
}
#[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"),
}
}
}