1use std::ops::RangeInclusive;
9
10#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12pub struct MpeZone {
13 pub manager_channel: u8,
15 pub member_channels: RangeInclusive<u8>,
17 pub manager_pitch_bend_semitones: u8,
19 pub member_pitch_bend_semitones: u8,
21}
22
23impl MpeZone {
24 pub const DEFAULT_MEMBER_PITCH_BEND_SEMITONES: u8 = 48;
26 pub const DEFAULT_MANAGER_PITCH_BEND_SEMITONES: u8 = 2;
27
28 pub fn new(manager_channel: u8, member_count: u8) -> Option<Self> {
31 let manager_channel = manager_channel.min(15);
32 let member_count = member_count.min(15);
33 if member_count == 0 {
34 return None;
35 }
36 let member_channels = if manager_channel == 0 {
37 let end = 1_u8.saturating_add(member_count).min(15);
39 1..=end
40 } else if manager_channel == 15 {
41 let start = 15_u8.saturating_sub(member_count).max(1);
43 start..=14
44 } else {
45 return None;
47 };
48 Some(Self {
49 manager_channel,
50 member_channels,
51 manager_pitch_bend_semitones: Self::DEFAULT_MANAGER_PITCH_BEND_SEMITONES,
52 member_pitch_bend_semitones: Self::DEFAULT_MEMBER_PITCH_BEND_SEMITONES,
53 })
54 }
55
56 pub fn is_manager(&self, channel: u8) -> bool {
58 self.manager_channel == channel.min(15)
59 }
60
61 pub fn is_member(&self, channel: u8) -> bool {
63 self.member_channels.contains(&channel.min(15))
64 }
65
66 pub fn contains(&self, channel: u8) -> bool {
68 let channel = channel.min(15);
69 self.is_manager(channel) || self.is_member(channel)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
75pub struct MpeState {
76 lower: Option<MpeZone>,
77 upper: Option<MpeZone>,
78 #[serde(skip)]
79 pending_rpn_msb: [Option<u8>; 16],
80 #[serde(skip)]
81 pending_rpn_lsb: [Option<u8>; 16],
82}
83
84impl MpeState {
85 pub fn new() -> Self {
87 Self::default()
88 }
89
90 pub fn lower(&self) -> Option<&MpeZone> {
92 self.lower.as_ref()
93 }
94
95 pub fn upper(&self) -> Option<&MpeZone> {
97 self.upper.as_ref()
98 }
99
100 pub fn zone_for_channel(&self, channel: u8) -> Option<&MpeZone> {
102 let channel = channel.min(15);
103 if let Some(ref lower) = self.lower
104 && lower.contains(channel)
105 {
106 return Some(lower);
107 }
108 if let Some(ref upper) = self.upper
109 && upper.contains(channel)
110 {
111 return Some(upper);
112 }
113 None
114 }
115
116 pub fn is_manager_channel(&self, channel: u8) -> bool {
118 let channel = channel.min(15);
119 self.lower.as_ref().is_some_and(|z| z.is_manager(channel))
120 || self.upper.as_ref().is_some_and(|z| z.is_manager(channel))
121 }
122
123 pub fn is_member_channel(&self, channel: u8) -> bool {
125 let channel = channel.min(15);
126 self.lower.as_ref().is_some_and(|z| z.is_member(channel))
127 || self.upper.as_ref().is_some_and(|z| z.is_member(channel))
128 }
129
130 pub fn is_active(&self) -> bool {
132 self.lower.is_some() || self.upper.is_some()
133 }
134
135 pub fn active_zone(&self) -> Option<&MpeZone> {
140 self.lower.as_ref().or(self.upper.as_ref())
141 }
142
143 pub fn configure_zone(&mut self, manager_channel: u8, member_count: u8) {
153 let manager_channel = manager_channel.min(15);
154 if manager_channel != 0 && manager_channel != 15 {
155 return;
156 }
157
158 if member_count == 0 {
159 if manager_channel == 0 {
160 self.lower = None;
161 } else {
162 self.upper = None;
163 }
164 return;
165 }
166
167 let new_zone = match MpeZone::new(manager_channel, member_count) {
168 Some(z) => z,
169 None => return,
170 };
171
172 let other = if manager_channel == 0 {
176 &mut self.upper
177 } else {
178 &mut self.lower
179 };
180 if let Some(other_zone) = other.as_mut() {
181 let remaining_members: Vec<u8> = other_zone
182 .member_channels
183 .clone()
184 .filter(|ch| !new_zone.member_channels.contains(ch))
185 .collect();
186 if remaining_members.is_empty() {
187 *other = None;
188 } else {
189 let start = *remaining_members.first().unwrap();
190 let end = *remaining_members.last().unwrap();
191 other_zone.member_channels = start..=end;
192 }
193 }
194
195 if manager_channel == 0 {
196 self.lower = Some(new_zone);
197 } else {
198 self.upper = Some(new_zone);
199 }
200 }
201
202 pub fn set_pitch_bend_sensitivity(&mut self, channel: u8, semitones: u8) {
208 let channel = channel.min(15);
209 let semitones = semitones.min(96);
210
211 if let Some(ref mut lower) = self.lower {
212 if lower.is_manager(channel) {
213 lower.manager_pitch_bend_semitones = semitones;
214 } else if lower.is_member(channel) {
215 lower.member_pitch_bend_semitones = semitones;
216 }
217 }
218 if let Some(ref mut upper) = self.upper {
219 if upper.is_manager(channel) {
220 upper.manager_pitch_bend_semitones = semitones;
221 } else if upper.is_member(channel) {
222 upper.member_pitch_bend_semitones = semitones;
223 }
224 }
225 }
226
227 pub fn feed(&mut self, data: &[u8]) -> Option<u8> {
232 if data.len() < 3 {
233 return None;
234 }
235 let status = data[0];
236 let channel = status & 0x0F;
237 let msg_type = status & 0xF0;
238
239 if msg_type != 0xB0 {
241 return None;
242 }
243
244 let cc = data[1];
245 let value = data[2];
246
247 match cc {
248 101 => {
249 self.pending_rpn_msb[channel as usize] = Some(value);
251 self.pending_rpn_lsb[channel as usize] = None;
252 }
253 100 => {
254 if self.pending_rpn_msb[channel as usize].is_some() {
255 self.pending_rpn_lsb[channel as usize] = Some(value);
256 }
257 }
258 6 => {
259 if let (Some(msb), Some(lsb)) = (
260 self.pending_rpn_msb[channel as usize],
261 self.pending_rpn_lsb[channel as usize],
262 ) {
263 if msb == 0x00 && lsb == 0x00 {
264 self.set_pitch_bend_sensitivity(channel, value);
266 self.clear_rpn_state(channel);
267 return Some(channel);
268 } else if msb == 0x00 && lsb == 0x06 {
269 self.configure_zone(channel, value);
271 self.clear_rpn_state(channel);
272 return Some(channel);
273 }
274 }
275 }
276 38 => {
277 }
279 _ => {
280 self.clear_rpn_state(channel);
282 }
283 }
284
285 None
286 }
287
288 fn clear_rpn_state(&mut self, channel: u8) {
289 let idx = channel.min(15) as usize;
290 self.pending_rpn_msb[idx] = None;
291 self.pending_rpn_lsb[idx] = None;
292 }
293}
294
295#[derive(Debug, Clone)]
302pub struct MpeVoiceAllocator {
303 zone: MpeZone,
305 active: std::collections::HashMap<u8, Vec<u8>>,
309 channel_load: std::collections::HashMap<u8, usize>,
312}
313
314impl MpeVoiceAllocator {
315 pub fn new(zone: MpeZone) -> Self {
317 let mut channel_load = std::collections::HashMap::new();
318 for ch in zone.member_channels.clone() {
319 channel_load.insert(ch, 0);
320 }
321 Self {
322 zone,
323 active: std::collections::HashMap::new(),
324 channel_load,
325 }
326 }
327
328 pub fn feed(&mut self, data: &[u8]) -> Vec<Vec<u8>> {
338 if data.is_empty() {
339 return Vec::new();
340 }
341 let status = data[0];
342 let msg_type = status & 0xF0;
343 let channel = status & 0x0F;
344 let pitch = data.get(1).copied().unwrap_or(0);
345
346 match msg_type {
347 0x90 => {
348 if data.len() < 2 {
351 return vec![data.to_vec()];
352 }
353 let allocated = if self.zone.is_member(channel) {
354 channel
356 } else {
357 self.allocate(pitch)
358 };
359 self.active.entry(pitch).or_default().push(allocated);
360 *self.channel_load.entry(allocated).or_insert(0) += 1;
361 vec![
365 vec![0xE0 | allocated, 0x00, 0x40], vec![0xD0 | allocated, 0x00], vec![0xB0 | allocated, 74, 0x00], Self::with_channel(data, allocated),
369 ]
370 }
371 0x80 => {
372 if data.len() < 2 {
374 return vec![data.to_vec()];
375 }
376 let allocated = self.release(pitch);
377 let out_channel = allocated.unwrap_or(channel);
378 vec![Self::with_channel(data, out_channel)]
379 }
380 _ => {
381 vec![data.to_vec()]
383 }
384 }
385 }
386
387 pub fn feed_many(&mut self, data: &[Vec<u8>]) -> Vec<Vec<u8>> {
389 data.iter().flat_map(|d| self.feed(d)).collect()
390 }
391
392 fn allocate(&mut self, _pitch: u8) -> u8 {
393 self.zone
396 .member_channels
397 .clone()
398 .min_by_key(|ch| self.channel_load.get(ch).copied().unwrap_or(0))
399 .unwrap_or(*self.zone.member_channels.start())
400 }
401
402 fn release(&mut self, pitch: u8) -> Option<u8> {
403 let stack = self.active.get_mut(&pitch)?;
404 let ch = stack.pop();
405 if stack.is_empty() {
406 self.active.remove(&pitch);
407 }
408 if let Some(ch) = ch
409 && let Some(load) = self.channel_load.get_mut(&ch)
410 {
411 *load = load.saturating_sub(1);
412 }
413 ch
414 }
415
416 fn with_channel(data: &[u8], channel: u8) -> Vec<u8> {
417 let mut out = data.to_vec();
418 out[0] = (data[0] & 0xF0) | (channel & 0x0F);
419 out
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 fn cc(channel: u8, cc: u8, value: u8) -> Vec<u8> {
428 vec![0xB0 | (channel & 0x0F), cc, value]
429 }
430
431 #[test]
432 fn lower_zone_with_15_members() {
433 let mut state = MpeState::new();
434 state.feed(&cc(0, 101, 0));
436 state.feed(&cc(0, 100, 6));
437 state.feed(&cc(0, 6, 15));
438
439 let lower = state.lower().unwrap();
440 assert_eq!(lower.manager_channel, 0);
441 assert_eq!(lower.member_channels, 1..=15);
442 assert_eq!(lower.member_pitch_bend_semitones, 48);
443 assert_eq!(lower.manager_pitch_bend_semitones, 2);
444 assert!(state.upper().is_none());
445 }
446
447 #[test]
448 fn upper_zone_with_7_members() {
449 let mut state = MpeState::new();
450 state.feed(&cc(15, 101, 0));
452 state.feed(&cc(15, 100, 6));
453 state.feed(&cc(15, 6, 7));
454
455 let upper = state.upper().unwrap();
456 assert_eq!(upper.manager_channel, 15);
457 assert_eq!(upper.member_channels, 8..=14);
458 assert!(state.lower().is_none());
459 }
460
461 #[test]
462 fn zero_member_count_deactivates_zone() {
463 let mut state = MpeState::new();
464 state.feed(&cc(0, 101, 0));
465 state.feed(&cc(0, 100, 6));
466 state.feed(&cc(0, 6, 5));
467 assert!(state.lower().is_some());
468
469 state.feed(&cc(0, 101, 0));
470 state.feed(&cc(0, 100, 6));
471 state.feed(&cc(0, 6, 0));
472 assert!(state.lower().is_none());
473 }
474
475 #[test]
476 fn overlapping_zone_steals_channels() {
477 let mut state = MpeState::new();
478 state.feed(&cc(0, 101, 0));
480 state.feed(&cc(0, 100, 6));
481 state.feed(&cc(0, 6, 7));
482 assert!(state.lower().is_some());
483 assert!(state.upper().is_none());
484
485 state.feed(&cc(15, 101, 0));
489 state.feed(&cc(15, 100, 6));
490 state.feed(&cc(15, 6, 10));
491 let lower = state.lower().unwrap();
492 assert_eq!(lower.member_channels, 1..=4);
493 let upper = state.upper().unwrap();
494 assert_eq!(upper.member_channels, 5..=14);
495 }
496
497 #[test]
498 fn rpn_zero_sets_pitch_bend_sensitivity() {
499 let mut state = MpeState::new();
500 state.feed(&cc(0, 101, 0));
501 state.feed(&cc(0, 100, 6));
502 state.feed(&cc(0, 6, 7));
503
504 state.feed(&cc(0, 101, 0));
506 state.feed(&cc(0, 100, 0));
507 state.feed(&cc(0, 6, 5));
508 assert_eq!(state.lower().unwrap().manager_pitch_bend_semitones, 5);
509
510 state.feed(&cc(2, 101, 0));
512 state.feed(&cc(2, 100, 0));
513 state.feed(&cc(2, 6, 24));
514 assert_eq!(state.lower().unwrap().member_pitch_bend_semitones, 24);
515 }
516
517 #[test]
518 fn invalid_manager_channels_are_ignored() {
519 let mut state = MpeState::new();
520 for ch in 1..=14 {
521 state.feed(&cc(ch, 101, 0));
522 state.feed(&cc(ch, 100, 6));
523 state.feed(&cc(ch, 6, 5));
524 }
525 assert!(!state.is_active());
526 }
527
528 fn note_on(channel: u8, pitch: u8, velocity: u8) -> Vec<u8> {
529 vec![0x90 | (channel & 0x0F), pitch, velocity]
530 }
531
532 fn note_off(channel: u8, pitch: u8, velocity: u8) -> Vec<u8> {
533 vec![0x80 | (channel & 0x0F), pitch, velocity]
534 }
535
536 fn note_on_event(events: &[Vec<u8>]) -> Option<&Vec<u8>> {
537 events
538 .iter()
539 .find(|e| matches!(e.first().copied().unwrap_or(0) & 0xF0, 0x90))
540 }
541
542 #[test]
543 fn allocator_spreads_notes_across_member_channels() {
544 let zone = MpeZone::new(0, 3).unwrap();
545 let mut alloc = MpeVoiceAllocator::new(zone);
546
547 let out = alloc.feed_many(&[
548 note_on(0, 60, 100),
549 note_on(0, 64, 100),
550 note_on(0, 67, 100),
551 ]);
552
553 assert_eq!(note_on_event(&out[0..4]).unwrap()[0] & 0x0F, 1);
554 assert_eq!(note_on_event(&out[4..8]).unwrap()[0] & 0x0F, 2);
555 assert_eq!(note_on_event(&out[8..12]).unwrap()[0] & 0x0F, 3);
556 }
557
558 #[test]
559 fn allocator_reuses_released_channel() {
560 let zone = MpeZone::new(0, 2).unwrap();
561 let mut alloc = MpeVoiceAllocator::new(zone);
562
563 let _ = alloc.feed(¬e_on(0, 60, 100)); let _ = alloc.feed(¬e_off(0, 60, 100)); let out = alloc.feed(¬e_on(0, 64, 100));
566
567 assert_eq!(note_on_event(&out).unwrap()[0] & 0x0F, 1);
568 }
569
570 #[test]
571 fn allocator_matches_note_off_to_note_on_channel() {
572 let zone = MpeZone::new(0, 3).unwrap();
573 let mut alloc = MpeVoiceAllocator::new(zone);
574
575 let _ = alloc.feed(¬e_on(0, 60, 100));
576 let _ = alloc.feed(¬e_on(0, 64, 100));
577 let off = alloc.feed(¬e_off(0, 60, 100));
578
579 assert_eq!(off[0][0] & 0x0F, 1);
580 }
581
582 #[test]
583 fn allocator_resets_per_note_controllers_before_note_on() {
584 let zone = MpeZone::new(0, 3).unwrap();
585 let mut alloc = MpeVoiceAllocator::new(zone);
586
587 let out = alloc.feed(¬e_on(0, 60, 100));
588
589 assert_eq!(out.len(), 4);
590 assert_eq!(out[0], vec![0xE1, 0x00, 0x40]); assert_eq!(out[1], vec![0xD1, 0x00]); assert_eq!(out[2], vec![0xB1, 74, 0x00]); assert_eq!(out[3], vec![0x91, 60, 100]); }
595
596 #[test]
597 fn allocator_keeps_member_channel_events() {
598 let zone = MpeZone::new(0, 3).unwrap();
599 let mut alloc = MpeVoiceAllocator::new(zone);
600
601 let pb = vec![0xE0 | 2, 0x00, 0x40];
602 let out = alloc.feed(&pb);
603 assert_eq!(out[0][0] & 0x0F, 2);
604 }
605
606 #[test]
607 fn allocator_leaves_manager_controllers_unchanged() {
608 let zone = MpeZone::new(0, 3).unwrap();
609 let mut alloc = MpeVoiceAllocator::new(zone);
610
611 let cc_msg = vec![0xB0, 74, 64];
612 let out = alloc.feed(&cc_msg);
613 assert_eq!(out[0][0] & 0x0F, 0);
614 }
615}