Skip to main content

nord_format/formats/ne5/program/
piano.rs

1//! The piano panel.
2
3use crate::components::{sparse_enum, PianoRef};
4use crate::types::RangedU8;
5use nord_bits_derive::bitbody;
6
7// 0x3a..=0x41 — the piano panel.
8
9/// The piano panel: category and model slot, plus the clav and acoustic
10/// playing options.
11#[bitbody(8)]
12#[derive(Default)]
13pub struct PianoPanel {
14    #[bits(0..=2)]
15    pub category: PianoCategory,
16    /// Zero-based model slot *within* [`category`](Self::category) — the panel's
17    /// Model dial. A slot coordinate, not an identity; see [`id`](Self::id).
18    #[bits(5..=9)]
19    pub piano_model: RangedU8<31>,
20    #[bits(15..=16)]
21    pub clav_model: RangedU8<3>,
22    #[bits(17..=18)]
23    pub acoustics: RangedU8<3>,
24    #[bits(19..=20)]
25    pub touch: RangedU8<3>,
26    #[bits(21..=21)]
27    pub mono: bool,
28    /// The piano (`.npno`) this program depends on: a stable id, independent of where
29    /// the piano sits in the library, and `0` when none is referenced. Use this — not
30    /// [`category`](Self::category)/[`piano_model`](Self::piano_model), which are slot
31    /// coordinates — to resolve the song → program → piano chain.
32    ///
33    /// It is the same id the instrument reports for this program over USB in a
34    /// `DEPENDENCIES` reply, which is what lets a file on disk be matched to the library
35    /// content it needs. The wire carries the piano's *name* too; the file does not, so
36    /// resolving one to the other needs the device or a bundle manifest.
37    #[bits(22..=53)]
38    pub id: PianoRef,
39}
40sparse_enum!(
41    /// The piano panel's Type dial — which library category the model comes from.
42    PianoCategory, 3, {
43        0 => Grand, "grand";
44        1 => Upright, "upright";
45        2 => EPiano1, "epiano1";
46        3 => EPiano2, "epiano2";
47        4 => Clavinet, "clavinet";
48        5 => Harpsichord, "harps";
49    }
50);
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::bits::Packed;
56    use std::array;
57
58    /// Bits no field names survive a re-encode, because each panel keeps the bytes it
59    /// was decoded from.
60    #[test]
61    fn unnamed_bits_survive_a_re_encode() {
62        // `PianoPanel` bits 3..=4 and 10..=14 are named by nothing.
63        const GAPS: [u8; 8] = [0b0001_1000, 0b0011_1110, 0, 0, 0, 0, 0, 0];
64        fn gap_bits(raw: [u8; 8]) -> [u8; 8] {
65            array::from_fn(|i| raw[i] & GAPS[i])
66        }
67
68        let mut panel = PianoPanel::try_from(GAPS).unwrap();
69        panel.category = PianoCategory::EPiano1;
70        panel.id = PianoRef::from_bits(0xdead_beef).expect("a 32-bit id decodes totally");
71        let out = <[u8; 8]>::from(&panel);
72        assert_eq!(gap_bits(out), GAPS, "a re-encode cleared a gap bit");
73
74        let mut panel = PianoPanel::try_from([0; 8]).unwrap();
75        panel.category = PianoCategory::Unknown(7);
76        panel.piano_model = 31u8.try_into().unwrap();
77        let out = <[u8; 8]>::from(&panel);
78        assert_eq!(gap_bits(out), [0; 8], "a re-encode set a gap bit");
79    }
80}