1use crate::cbin::{self, Cbin, Header};
31use crate::components::sparse_enum;
32use crate::error::{Error, ParseError};
33use crate::formats::ne5::{program, song};
34use crate::types::{RangedI8, RangedU8};
35use nord_bits_derive::bitbody;
36
37use std::fmt::{self, Debug, Display, Formatter};
38use std::io::{Read, Seek};
39
40pub const FORMAT: &str = "ne5s";
41pub const KNOWN_VERSIONS: &[u32] = &[0];
43pub const BODY_LEN: usize = 0x4e - 0x2c;
45pub const FILE_LEN: usize = 0x2c + BODY_LEN;
47
48pub fn new() -> Cbin<Settings> {
53 Cbin {
54 header: Header::new(FORMAT, (0, 0), 0),
55 body: Settings::default(),
56 }
57}
58
59pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Settings>, Error> {
60 let file: Cbin<Settings> = cbin::read(reader, FORMAT)?;
61 program::known_version(FORMAT, file.header.version, KNOWN_VERSIONS)?;
62 program::unset_aux(FORMAT, &file.header)?;
63 let (bank, slot) = file.header.slot();
66 if (bank, slot) != (0, 0) {
67 return Err(ParseError::AssertFail(format!(
68 "{FORMAT}: location is {bank} {slot}, and settings live at 0 0"
69 ))
70 .into());
71 }
72 Ok(file)
73}
74
75pub type GlobalTranspose = RangedI8<6, -6, 6>;
77
78pub type ResonanceLevel = RangedI8<6, -6, 6>;
80
81pub type FineTune = RangedI8<50, -50, 50>;
83
84#[bitbody(34)]
92pub struct Settings {
93 #[bits(52..=53)]
95 pub rotary_ctrl_type: RotaryCtrlType,
96 #[bits(54..=54)]
97 pub rotary_pedal_mode: RotaryPedalMode,
98 #[bits(134..=135)]
99 pub sustain_pedal_mode: SustainPedalMode,
100 #[bits(43..=44)]
101 pub sustain_pedal_type: SustainPedalType,
102 #[bits(45..=47)]
103 pub ctrl_pedal_type: CtrlPedalType,
104 #[bits(138..=141)]
105 pub ctrl_pedal_gain: CtrlPedalGain,
106 #[bits(117..=117)]
107 pub b3_trig_mode: B3TrigMode,
108 #[bits(128..=128)]
109 pub output_routing: OutputRouting,
110 #[bits(68..=71)]
111 pub global_transpose: GlobalTranspose,
112 #[bits(55..=61)]
116 pub fine_tune: FineTune,
117
118 #[bits(72..=76)]
120 pub global_channel: MidiChannel,
121 #[bits(118..=122)]
122 pub lower_receive_channel: MidiChannel,
123 #[bits(123..=127)]
124 pub upper_receive_channel: MidiChannel,
125 #[bits(129..=133)]
126 pub upper_split_channel: MidiChannel,
127 #[bits(38..=39)]
128 pub control_change_mode: MidiMessageMode,
129 #[bits(40..=41)]
130 pub program_change_mode: MidiMessageMode,
131 #[bits(137..=137)]
132 pub transpose_at: TransposeAt,
133
134 #[bits(64..=67)]
139 pub piano_string_resonance: ResonanceLevel,
140 #[bits(109..=111)]
141 pub b3_tonewheel_mode: TonewheelMode,
142 #[bits(113..=114)]
143 pub b3_key_click_level: KeyClickLevel,
144 #[bits(116..=116)]
145 pub b3_key_bounce: bool,
146 #[bits(112..=112)]
147 pub b3_perc_db9_mute: bool,
148 #[bits(97..=99)]
149 pub b3_perc_decay_fast: PercDecay,
150 #[bits(100..=102)]
151 pub b3_perc_decay_slow: PercDecay,
152 #[bits(103..=105)]
153 pub b3_perc_volume_normal: PercVolume,
154 #[bits(106..=108)]
155 pub b3_perc_volume_soft: PercVolume,
156 #[bits(79..=81)]
159 pub rotary_speaker_type: RotarySpeakerType,
160 #[bits(94..=96)]
161 pub rotary_balance: RotaryBalance,
162 #[bits(82..=84)]
163 pub rotary_horn_speed: RotaryRate,
164 #[bits(88..=90)]
165 pub rotary_horn_acceleration: RotaryRate,
166 #[bits(85..=87)]
167 pub rotary_rotor_speed: RotaryRate,
168 #[bits(91..=93)]
169 pub rotary_rotor_acceleration: RotaryRate,
170
171 #[bits(16..=16)]
176 pub startup_set_list_mode: bool,
177 #[bits(17..=17)]
178 pub startup_live_mode: bool,
179 #[bits(19..=20)]
180 pub startup_live_slot: LiveSlot,
181 #[bits(21..=29)]
182 pub startup_program: program::Location,
183 #[bits(30..=37)]
185 pub startup_song: song::Location,
186}
187
188sparse_enum!(
189 LiveSlot, 2, {
191 0 => Live1, "live 1";
192 1 => Live2, "live 2";
193 2 => Live3, "live 3";
194 }
195);
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum Menu {
200 System,
201 Midi,
202 Sound,
203}
204
205impl Menu {
206 pub fn title(&self) -> &'static str {
207 match self {
208 Menu::System => "System",
209 Menu::Midi => "MIDI",
210 Menu::Sound => "Sound",
211 }
212 }
213}
214
215pub struct Setting {
217 pub name: &'static str,
219 pub value: String,
222}
223
224impl Default for Settings {
227 fn default() -> Self {
228 Settings::try_from([0; BODY_LEN]).expect("every settings field decodes totally")
229 }
230}
231
232fn on_off(on: bool) -> String {
234 if on { "on" } else { "off" }.to_string()
235}
236
237impl Settings {
238 pub fn by_menu(&self) -> Vec<(Menu, Vec<Setting>)> {
246 let at = |name, value: String| Setting { name, value };
247 vec![
248 (
249 Menu::System,
250 vec![
251 at("rotary_ctrl_type", self.rotary_ctrl_type.to_string()),
252 at("rotary_pedal_mode", self.rotary_pedal_mode.to_string()),
253 at("sustain_pedal_mode", self.sustain_pedal_mode.to_string()),
254 at("sustain_pedal_type", self.sustain_pedal_type.to_string()),
255 at("ctrl_pedal_type", self.ctrl_pedal_type.to_string()),
256 at("ctrl_pedal_gain", self.ctrl_pedal_gain.to_string()),
257 at("b3_trig_mode", self.b3_trig_mode.to_string()),
258 at("output_routing", self.output_routing.to_string()),
259 at(
260 "global_transpose",
261 format!("{:+}", self.global_transpose.inner()),
262 ),
263 at("fine_tune", format!("{:+} cent", self.fine_tune.inner())),
264 ],
265 ),
266 (
267 Menu::Midi,
268 vec![
269 at("global_channel", self.global_channel.to_string()),
270 at(
271 "lower_receive_channel",
272 self.lower_receive_channel.to_string(),
273 ),
274 at(
275 "upper_receive_channel",
276 self.upper_receive_channel.to_string(),
277 ),
278 at("upper_split_channel", self.upper_split_channel.to_string()),
279 at("control_change_mode", self.control_change_mode.to_string()),
280 at("program_change_mode", self.program_change_mode.to_string()),
281 at("transpose_at", self.transpose_at.to_string()),
282 ],
283 ),
284 (
285 Menu::Sound,
286 vec![
287 at(
288 "piano_string_resonance",
289 format!("{:+} dB", self.piano_string_resonance.inner()),
290 ),
291 at("b3_tonewheel_mode", self.b3_tonewheel_mode.to_string()),
292 at("b3_key_click_level", self.b3_key_click_level.to_string()),
293 at("b3_key_bounce", on_off(self.b3_key_bounce)),
294 at("b3_perc_db9_mute", on_off(self.b3_perc_db9_mute)),
295 at("b3_perc_decay_fast", self.b3_perc_decay_fast.to_string()),
296 at("b3_perc_decay_slow", self.b3_perc_decay_slow.to_string()),
297 at(
298 "b3_perc_volume_normal",
299 self.b3_perc_volume_normal.to_string(),
300 ),
301 at("b3_perc_volume_soft", self.b3_perc_volume_soft.to_string()),
302 at("rotary_speaker_type", self.rotary_speaker_type.to_string()),
303 at("rotary_balance", self.rotary_balance.to_string()),
304 at("rotary_horn_speed", self.rotary_horn_speed.to_string()),
305 at(
306 "rotary_horn_acceleration",
307 self.rotary_horn_acceleration.to_string(),
308 ),
309 at("rotary_rotor_speed", self.rotary_rotor_speed.to_string()),
310 at(
311 "rotary_rotor_acceleration",
312 self.rotary_rotor_acceleration.to_string(),
313 ),
314 ],
315 ),
316 ]
317 }
318}
319
320sparse_enum!(
321 RotaryCtrlType, 2, {
323 0 => Closed, "closed";
324 1 => Open, "open";
325 2 => HalfMoon, "half moon";
326 }
327);
328
329sparse_enum!(
330 RotaryPedalMode, 1, {
332 0 => Hold, "hold";
333 1 => Toggle, "toggle";
334 }
335);
336
337sparse_enum!(
338 SustainPedalMode, 2, {
340 0 => Sustain, "sustain";
341 1 => SustainRotorHold, "sustain + rotor hold";
342 2 => SustainRotorToggle, "sustain + rotor toggle";
343 }
344);
345
346sparse_enum!(
347 SustainPedalType, 2, {
349 0 => Auto, "auto";
350 1 => Closed, "closed";
351 2 => Open, "open";
352 }
353);
354
355sparse_enum!(
356 CtrlPedalType, 3, {
358 0 => RolandEv7, "roland ev-7";
359 1 => YamahaFc7, "yamaha fc-7";
360 2 => KorgExp2, "korg exp-2";
361 3 => KorgXvp10, "korg xvp-10";
362 4 => BossFv500L, "boss fv-500l";
363 5 => FatarSl, "fatar sl";
364 }
365);
366
367sparse_enum!(
368 B3TrigMode, 1, {
370 0 => Normal, "normal";
371 1 => Fast, "fast";
372 }
373);
374
375sparse_enum!(
376 OutputRouting, 1, {
378 0 => Stereo, "stereo";
379 1 => LowerLeftUpperRight, "lower L / upper R";
380 }
381);
382
383sparse_enum!(
384 MidiMessageMode, 2, {
386 0 => Off, "off";
387 1 => Send, "send";
388 2 => Receive, "receive";
389 3 => SendReceive, "send/receive";
390 }
391);
392
393sparse_enum!(
394 TransposeAt, 1, {
396 0 => MidiIn, "midi in";
397 1 => MidiOut, "midi out";
398 }
399);
400
401sparse_enum!(
402 TonewheelMode, 3, {
404 0 => Clean, "clean";
405 1 => Vintage1, "vintage 1";
406 2 => Vintage2, "vintage 2";
407 3 => Vintage3, "vintage 3";
408 }
409);
410
411sparse_enum!(
412 KeyClickLevel, 2, {
414 0 => Low, "low";
415 1 => Normal, "normal";
416 2 => High, "high";
417 3 => Higher, "higher";
418 }
419);
420
421sparse_enum!(
422 PercDecay, 3, {
424 0 => Short, "short";
425 1 => Medium, "medium";
426 2 => Long, "long";
427 }
428);
429
430sparse_enum!(
431 PercVolume, 3, {
433 0 => Low, "low";
434 1 => Medium, "medium";
435 2 => High, "high";
436 }
437);
438
439sparse_enum!(
440 RotarySpeakerType, 3, {
442 0 => Rotary122, "122";
443 1 => Rotary122Close, "122 close";
444 }
445);
446
447sparse_enum!(
448 RotaryBalance, 3, {
450 0 => Bass70Horn30, "70/30";
451 1 => Bass60Horn40, "60/40";
452 2 => Bass50Horn50, "50/50";
453 3 => Bass40Horn60, "40/60";
454 4 => Bass30Horn70, "30/70";
455 }
456);
457
458sparse_enum!(
459 RotaryRate, 3, {
461 0 => Low, "low";
462 1 => Normal, "normal";
463 2 => High, "high";
464 }
465);
466
467#[derive(Copy, Clone, PartialEq, Eq)]
473pub struct ChannelNumber(u8);
474
475impl ChannelNumber {
476 pub fn get(self) -> u8 {
478 self.0
479 }
480}
481
482#[derive(Copy, Clone, PartialEq, Eq)]
487pub enum MidiChannel {
488 Channel(ChannelNumber),
490 Off,
491 Unknown(RangedU8<31>),
493}
494
495impl MidiChannel {
496 pub const CHANNELS: u8 = 16;
498
499 pub fn channel(number: u8) -> Result<Self, ParseError> {
501 if !(1..=Self::CHANNELS).contains(&number) {
502 return Err(ParseError::OutOfBounds {
503 value: format!("{number}"),
504 bound: format!("1..={}", Self::CHANNELS),
505 });
506 }
507 Ok(MidiChannel::Channel(ChannelNumber(number)))
508 }
509
510 pub fn number(&self) -> Option<u8> {
512 match self {
513 MidiChannel::Channel(n) => Some(n.get()),
514 _ => None,
515 }
516 }
517}
518
519impl crate::bits::Packed for MidiChannel {
520 const MAX_BITS: u32 = 5;
521 const DECODE_BITS: u32 = u8::BITS;
522 const CONTROL: crate::fields::ControlKind = crate::fields::ControlKind::Selector;
523 type Error = ParseError;
524
525 fn from_bits(bits: u64) -> Result<Self, ParseError> {
526 Ok(match bits as u8 {
527 n if n < Self::CHANNELS => MidiChannel::Channel(ChannelNumber(n + 1)),
528 16 => MidiChannel::Off,
529 other => MidiChannel::Unknown(other.try_into()?),
530 })
531 }
532
533 fn to_bits(&self) -> u64 {
534 match self {
535 MidiChannel::Channel(n) => u64::from(n.get() - 1),
537 MidiChannel::Off => 16,
538 MidiChannel::Unknown(raw) => u64::from(raw.as_u8()),
539 }
540 }
541}
542
543impl Debug for MidiChannel {
544 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
546 match self {
547 MidiChannel::Channel(n) => write!(f, "{}", n.get()),
548 MidiChannel::Off => f.write_str("off"),
549 MidiChannel::Unknown(raw) => write!(f, "unknown ({})", raw.as_u8()),
550 }
551 }
552}
553
554impl Display for MidiChannel {
555 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
556 write!(f, "{self:?}")
557 }
558}
559
560#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
562pub struct CtrlPedalGain(u8);
563
564impl CtrlPedalGain {
565 pub const MIN: u8 = 1;
566 pub const MAX: u8 = 10;
567
568 pub fn new(gain: u8) -> Result<Self, ParseError> {
569 if !(Self::MIN..=Self::MAX).contains(&gain) {
570 return Err(ParseError::OutOfBounds {
571 value: format!("{gain}"),
572 bound: format!("{}..={}", Self::MIN, Self::MAX),
573 });
574 }
575 Ok(CtrlPedalGain(gain))
576 }
577
578 pub fn as_u8(&self) -> u8 {
580 self.0
581 }
582}
583
584impl Default for CtrlPedalGain {
585 fn default() -> Self {
586 CtrlPedalGain(Self::MIN)
587 }
588}
589
590impl crate::bits::Packed for CtrlPedalGain {
591 const MAX_BITS: u32 = 4;
592 const DECODE_BITS: u32 = u8::BITS;
593 const CONTROL: crate::fields::ControlKind =
594 crate::fields::ControlKind::Knob(crate::fields::Unit::None);
595 type Error = ParseError;
596
597 fn from_bits(bits: u64) -> Result<Self, ParseError> {
598 CtrlPedalGain::new((bits as u8).saturating_add(1))
599 }
600
601 fn to_bits(&self) -> u64 {
602 u64::from(self.0 - Self::MIN)
603 }
604}
605
606impl Debug for CtrlPedalGain {
607 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
608 write!(f, "{}", self.0)
609 }
610}
611
612impl Display for CtrlPedalGain {
613 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
614 write!(f, "{}", self.0)
615 }
616}
617
618impl PartialEq<u8> for CtrlPedalGain {
619 fn eq(&self, other: &u8) -> bool {
620 self.0 == *other
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::bits::Packed;
628 use std::collections::{BTreeMap, BTreeSet};
629 use std::io::Cursor;
630
631 const fn body(abs: usize) -> usize {
633 abs - 0x2c
634 }
635
636 fn panel(bytes: &[(usize, u8)]) -> Settings {
638 let mut raw = [0u8; BODY_LEN];
639 for &(at, b) in bytes {
640 raw[body(at)] = b;
641 }
642 Settings::try_from(raw).expect("every settings field decodes totally")
643 }
644
645 #[test]
646 fn settings_round_trip_at_the_declared_length() {
647 let settings = new();
648 let mut bytes = Vec::new();
649 settings.write_to(&mut Cursor::new(&mut bytes)).unwrap();
650 assert_eq!(bytes.len(), FILE_LEN);
651 assert_eq!(&bytes[0x08..0x0c], FORMAT.as_bytes());
652
653 let back = read_from(&mut Cursor::new(&mut bytes)).unwrap();
654 let mut again = Vec::new();
655 back.write_to(&mut Cursor::new(&mut again)).unwrap();
656 assert_eq!(bytes, again);
657 }
658
659 #[test]
662 fn a_settings_file_addressed_to_a_slot_is_refused() {
663 let mut settings = new();
664 settings.header.set_slot((1, 0));
665 let mut bytes = Vec::new();
666 settings.write_to(&mut Cursor::new(&mut bytes)).unwrap();
667
668 let err = read_from(&mut Cursor::new(&bytes))
669 .expect_err("a located settings file must not decode");
670 assert!(
671 matches!(err, Error::Parse(ParseError::AssertFail(_))),
672 "refused for the wrong reason: {err}",
673 );
674 }
675
676 #[test]
677 fn a_settings_field_set_by_name_survives_a_round_trip() {
678 let mut settings = new();
679 settings.set_field("global_transpose", "-3").unwrap();
680
681 let mut bytes = Vec::new();
682 settings.write_to(&mut Cursor::new(&mut bytes)).unwrap();
683 let back = read_from(&mut Cursor::new(&mut bytes)).unwrap();
684 let listed = back
685 .fields()
686 .into_iter()
687 .find(|f| f.path == "global_transpose")
688 .expect("declared");
689 assert_eq!(listed.display, "-3");
690 }
691
692 #[test]
696 fn every_field_is_listed_under_one_menu() {
697 let declared: BTreeSet<String> = Settings::field_specs()
698 .into_iter()
699 .map(|f| f.name)
700 .filter(|name| !name.starts_with("startup_"))
701 .collect();
702
703 let mut grouped: BTreeSet<String> = BTreeSet::new();
704 for (menu, settings) in Settings::default().by_menu() {
705 for setting in settings {
706 assert!(
707 declared.contains(setting.name),
708 "{} lists {}, which the panel does not declare",
709 menu.title(),
710 setting.name,
711 );
712 assert!(
713 grouped.insert(setting.name.to_string()),
714 "{} is listed under two menus",
715 setting.name,
716 );
717 }
718 }
719 let missing: Vec<_> = declared.difference(&grouped).collect();
720 assert!(missing.is_empty(), "fields with no menu: {missing:?}");
721 }
722
723 #[test]
728 fn a_menu_renders_the_panels_own_wording() {
729 let p = panel(&[
731 (0x2e, 0x13),
732 (0x2f, 0x24),
733 (0x30, 0x03),
734 (0x31, 0xc1),
735 (0x32, 0x06),
736 (0x33, 0xdc),
737 (0x34, 0x66),
738 (0x35, 0x00),
739 (0x36, 0x09),
740 (0x37, 0x25),
741 (0x38, 0x12),
742 (0x39, 0x49),
743 (0x3a, 0x2d),
744 (0x3b, 0x61),
745 (0x3c, 0x06),
746 (0x3d, 0x24),
747 ]);
748 let rendered: BTreeMap<&str, String> = p
749 .by_menu()
750 .into_iter()
751 .flat_map(|(_, settings)| settings)
752 .map(|s| (s.name, s.value))
753 .collect();
754
755 for (field, want) in [
756 ("ctrl_pedal_type", "yamaha fc-7"),
757 ("ctrl_pedal_gain", "10"),
758 ("sustain_pedal_mode", "sustain + rotor toggle"),
759 ("output_routing", "stereo"),
760 ("global_transpose", "+0"),
761 ("fine_tune", "+5 cent"),
762 ("global_channel", "1"),
763 ("control_change_mode", "send/receive"),
764 ("transpose_at", "midi in"),
765 ("piano_string_resonance", "+0 dB"),
766 ("b3_tonewheel_mode", "vintage 1"),
767 ("b3_key_bounce", "on"),
768 ("b3_perc_db9_mute", "off"),
769 ("rotary_speaker_type", "122"),
770 ("rotary_balance", "50/50"),
771 ("rotary_rotor_acceleration", "normal"),
772 ] {
773 assert_eq!(rendered[field], want, "{field}");
774 }
775 }
776
777 #[test]
779 fn a_menu_names_an_unrecognized_value_as_unknown() {
780 let p = panel(&[(0x35, 0x01), (0x36, 0xc0)]);
782 let shown = p
783 .by_menu()
784 .into_iter()
785 .flat_map(|(_, settings)| settings)
786 .find(|s| s.name == "rotary_speaker_type")
787 .expect("declared")
788 .value;
789 assert_eq!(shown, "unknown (7)");
790 }
791
792 #[test]
795 fn global_transpose_stores_minus_six_as_zero() {
796 for (semitones, stored) in [(-6i8, 0x0u8), (-1, 0x5), (0, 0x6), (1, 0x7), (6, 0xc)] {
798 let p = panel(&[(0x34, stored)]);
799 assert_eq!(p.global_transpose, semitones, "stored {stored:#x}");
800 assert_eq!(
801 <[u8; BODY_LEN]>::from(&p)[body(0x34)],
802 stored,
803 "{semitones} did not write back"
804 );
805 }
806 assert_eq!(panel(&[(0x34, 0xc6)]).global_transpose, 0);
808 assert_eq!(panel(&[(0x34, 0xc6)]).piano_string_resonance, 6);
809 }
810
811 #[test]
812 fn fine_tune_stores_minus_fifty_cents_as_zero() {
813 for (cents, at32, at33) in [
815 (-50i8, 0x00u8, 0x00u8),
816 (0, 0x00, 0xc8),
817 (5, 0x00, 0xdc),
818 (50, 0x01, 0x90),
819 ] {
820 let p = panel(&[(0x32, at32), (0x33, at33)]);
821 assert_eq!(p.fine_tune, cents, "stored {at32:#04x} {at33:#04x}");
822 let back = <[u8; BODY_LEN]>::from(&p);
823 assert_eq!(
824 (back[body(0x32)], back[body(0x33)]),
825 (at32, at33),
826 "{cents} did not write back"
827 );
828 }
829 let mut raw = [0u8; BODY_LEN];
831 raw[body(0x32)] = 0x01;
832 raw[body(0x33)] = 0xfc;
833 assert!(
834 Settings::try_from(raw).is_err(),
835 "a stored 127 decoded, and biased by 50 that is +77 cents"
836 );
837 }
838
839 #[test]
842 fn a_midi_channel_is_stored_zero_based_with_sixteen_for_off() {
843 let unknown = |raw: u8| MidiChannel::Unknown(raw.try_into().unwrap());
844 for (bits, channel) in [
845 (0u64, MidiChannel::channel(1).unwrap()),
846 (1, MidiChannel::channel(2).unwrap()),
847 (15, MidiChannel::channel(16).unwrap()),
848 (16, MidiChannel::Off),
849 (17, unknown(17)),
850 (31, unknown(31)),
851 ] {
852 assert_eq!(MidiChannel::from_bits(bits).unwrap(), channel);
853 assert_eq!(channel.to_bits(), bits, "{channel:?} does not round-trip");
854 }
855 assert_eq!(format!("{:?}", MidiChannel::channel(7).unwrap()), "7");
856 assert_eq!(format!("{:?}", MidiChannel::Off), "off");
857 }
858
859 #[test]
862 fn only_the_panels_sixteen_channels_can_be_built() {
863 for number in 1..=MidiChannel::CHANNELS {
864 let channel = MidiChannel::channel(number).unwrap();
865 assert_eq!(channel.number(), Some(number));
866 assert_eq!(channel.to_bits(), u64::from(number) - 1);
867 }
868 assert!(MidiChannel::channel(0).is_err());
869 assert!(MidiChannel::channel(17).is_err());
870 assert!(RangedU8::<31>::new(32).is_err());
872 }
873
874 #[test]
876 fn ctrl_pedal_gain_is_stored_one_less_than_the_panel_reads() {
877 for gain in CtrlPedalGain::MIN..=CtrlPedalGain::MAX {
878 let stored = CtrlPedalGain::new(gain).unwrap().to_bits();
879 assert_eq!(stored, u64::from(gain) - 1);
880 assert_eq!(CtrlPedalGain::from_bits(stored).unwrap(), gain);
881 }
882 assert!(CtrlPedalGain::new(0).is_err());
883 assert!(CtrlPedalGain::new(11).is_err());
884 assert!(CtrlPedalGain::from_bits(10).is_err());
886 }
887
888 #[test]
890 fn the_default_panel_encodes_and_decodes() {
891 let p = Settings::default();
892 assert_eq!(<[u8; BODY_LEN]>::from(&p), [0; BODY_LEN]);
893 assert_eq!(p.global_transpose, -6);
894 assert_eq!(p.fine_tune, -50);
895 assert_eq!(p.ctrl_pedal_gain, 1);
896 assert_eq!(p.global_channel, MidiChannel::channel(1).unwrap());
897 }
898
899 #[test]
901 fn setting_a_field_moves_only_its_own_bytes() {
902 let mut p = panel(&[]);
903 p.b3_tonewheel_mode = TonewheelMode::Vintage3;
904 let raw = <[u8; BODY_LEN]>::from(&p);
905
906 let moved: Vec<usize> = (0..BODY_LEN).filter(|&i| raw[i] != 0).collect();
907 assert_eq!(moved, vec![body(0x39)], "{moved:x?}");
909 assert_eq!(raw[body(0x39)], 0x03);
910 }
911
912 #[test]
914 fn an_unrecognized_pattern_round_trips() {
915 let p = panel(&[(0x35, 0x01), (0x36, 0xc0)]);
917 assert!(p.rotary_speaker_type.is_unknown());
918 let raw = <[u8; BODY_LEN]>::from(&p);
919 assert_eq!((raw[body(0x35)], raw[body(0x36)]), (0x01, 0xc0));
920 }
921}