Skip to main content

nord_format/formats/ne5/program/
center.rs

1//! The center panel — part selection, split, transpose, gain, and the organ selector.
2
3use crate::components::sparse_enum;
4use crate::components::PartMix;
5use crate::formats::ne5::{Instrument, Level, OctaveShift, SplitPoint, Transpose};
6use nord_bits_derive::bitbody;
7
8// 0x2e..=0x34 — the center panel.
9
10/// The center panel: part selection, split, transpose, gain, and the organ
11/// selector.
12#[bitbody(7)]
13#[derive(Default)]
14pub struct CenterPanel {
15    #[bits(0..=2)]
16    pub lower_part: Instrument,
17    #[bits(3..=5)]
18    pub upper_part: Instrument,
19    #[bits(6..=9)]
20    pub lower_octave_shift: OctaveShift,
21    #[bits(10..=13)]
22    pub upper_octave_shift: OctaveShift,
23    #[bits(14..=14)]
24    pub lower_sustain: bool,
25    #[bits(15..=15)]
26    pub upper_sustain: bool,
27    #[bits(16..=16)]
28    pub lower_control: bool,
29    #[bits(17..=17)]
30    pub upper_control: bool,
31    /// Zero in every specimen. Inferred from specimens; not confirmed on hardware.
32    #[bits(18..=18)]
33    pub unknown_boolean1: bool,
34    #[bits(19..=19)]
35    pub split: bool,
36    #[bits(20..=22)]
37    pub split_point: SplitPoint,
38    /// Sticky: the instrument sets this the first time transposition is changed and never
39    /// clears it again, so it stays true after the value is put back to 0. It marks that
40    /// transposition has been *touched*, not that the program is transposed.
41    ///
42    /// The transpose light is on for `transpose_enabled && transpose != 0`. Neither field
43    /// answers on its own — a caller reporting or editing transposition must read both.
44    /// Confirmed on hardware.
45    #[bits(23..=23)]
46    pub transpose_enabled: bool,
47
48    /// Half-step transposition, `-6..=6`, stored biased by 6.
49    ///
50    /// Carries no meaning while [`transpose_enabled`](Self::transpose_enabled) is clear: an
51    /// untouched program stores `+1` there rather than `0`. Inferred from specimens; not
52    /// confirmed on hardware. Every specimen with the enable clear holds `+1`.
53    #[bits(24..=27)]
54    pub transpose: Transpose,
55    #[bits(28..=34)]
56    pub part_mix: PartMix,
57    #[bits(35..=41)]
58    pub gain: Level,
59    #[bits(42..=44)]
60    pub organ_type: OrganType,
61    #[bits(45..=45)]
62    pub lower_enabled: bool,
63    #[bits(46..=46)]
64    pub upper_enabled: bool,
65    #[bits(47..=47)]
66    pub drawbar_live: bool,
67}
68
69sparse_enum!(
70    /// Which organ the program has selected.
71    ///
72    /// b3+bass shares the B3's storage rather than being a fifth model, and its preset 1
73    /// is the bass manual — see [`Self::is_b3_bass`].
74    OrganType, 3, {
75        0 => B3, "b3";
76        1 => B3Bass, "b3+bass";
77        2 => Pipe, "pipe";
78        3 => Vox, "vox";
79        4 => Farfisa, "farfisa";
80    }
81);
82
83impl OrganType {
84    /// Which model's storage slots this selection reads from. b3 and b3+bass share
85    /// [`OrganModel::B3`](crate::formats::ne5::program::OrganModel::B3); `None` for an
86    /// unknown selection.
87    pub fn storage(&self) -> Option<crate::formats::ne5::program::OrganModel> {
88        use crate::formats::ne5::program::OrganModel;
89        match self {
90            OrganType::B3 | OrganType::B3Bass => Some(OrganModel::B3),
91            OrganType::Pipe => Some(OrganModel::Pipe),
92            OrganType::Vox => Some(OrganModel::Vox),
93            OrganType::Farfisa => Some(OrganModel::Farfisa),
94            OrganType::Unknown(_) => None,
95        }
96    }
97
98    /// Whether preset 1 is the bass manual.
99    ///
100    /// **b3+bass is a selection, not a fifth model.** It shares the B3's storage, but its
101    /// two presets are different instruments: preset 1 is the bass manual, where only
102    /// drawbars 1-2 do anything and they live outside the nine-nibble block, and preset 2
103    /// is an ordinary B3. Reading preset 1's nine nibbles in that mode shows stale
104    /// values —
105    /// [`OrganPanel::b3_bass_drawbars`](crate::formats::ne5::program::OrganPanel::b3_bass_drawbars)
106    /// is the only correct source for bars 1-2.
107    pub fn is_b3_bass(&self) -> bool {
108        matches!(self, OrganType::B3Bass)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::super::{self as program, OrganModel, FILE_LEN};
115    use super::*;
116    use crate::bits::Packed;
117    use crate::types::RangedU8;
118    use std::io::Cursor;
119
120    /// An out-of-range value is not something a field can hold, so the refusal happens
121    /// at construction rather than part-way through a write.
122    #[test]
123    fn an_out_of_range_value_is_refused_where_it_is_written() {
124        // `panel.gain = 200;` does not compile: 200 is not a `RangedU8<127>`.
125        let too_wide: Result<RangedU8<127>, _> = 200u8.try_into();
126        assert!(too_wide.is_err(), "200 must not be a valid seven-bit gain");
127        assert!(
128            RangedU8::<127>::new(127).is_ok(),
129            "127 is the largest that fits"
130        );
131
132        let mut program = program::new((0, 0).try_into().unwrap());
133        program.center_panel.gain = 96u8.try_into().unwrap();
134
135        let mut bytes = Vec::new();
136        program
137            .write_to(&mut Cursor::new(&mut bytes))
138            .expect("a panel built from ranged values always writes");
139        assert_eq!(bytes.len(), FILE_LEN);
140    }
141
142    /// A default panel has to encode, which zeroed bytes alone would not: an octave
143    /// shift of zero is stored as 7, so all-zero bits decode as -7 — out of range.
144    #[test]
145    fn the_default_panel_encodes_and_decodes() {
146        let panel = CenterPanel::default();
147        let raw = <[u8; 7]>::from(&panel);
148        let back = CenterPanel::try_from(raw).expect("default panel must decode");
149
150        assert_eq!(back.lower_octave_shift, 0);
151        assert_eq!(back.upper_octave_shift, 0);
152        assert_eq!(back.transpose, 0);
153        assert_eq!(back.lower_part, Instrument::Organ);
154    }
155
156    /// Every organ the panel can select round-trips through its stored value.
157    #[test]
158    fn organ_type_values_round_trip() {
159        for bits in 0..5u64 {
160            assert_eq!(OrganType::from_bits(bits).unwrap().to_bits(), bits);
161        }
162        assert!(OrganType::from_bits(6).unwrap().is_unknown());
163        assert_eq!(OrganType::B3Bass.storage(), Some(OrganModel::B3));
164        assert!(OrganType::B3Bass.is_b3_bass());
165        assert!(!OrganType::B3.is_b3_bass());
166    }
167}