Skip to main content

nord_format/
accept.rs

1//! Which format tags an instrument family takes, and the class it keeps each of them in.
2//!
3//! A file names its model in its four-character tag; an instrument names its own in the
4//! USB product string. [`Family`] is the vocabulary the two meet in, and
5//! [`Family::accepts`] is the table: for one storage class and one tag, whether that
6//! family takes it and how that is known.
7//!
8//! ⚠️ Schema versions are not in the table. Which revisions of a tag an instrument
9//! accepts has not been measured, so a version is never a reason to refuse here.
10
11use crate::fields::Library;
12use crate::formats::{
13    nc2, nc2d, nd2, nd3, ne3, ne4, ne5, ne6, ne7, ng2, nl4, nla1, no3, np, np2, np3, np4, np5,
14    npip, npno, ns2, ns3, ns4, nsclassic, nsmp, nw, nw2,
15};
16
17/// One instrument family: the unit both a file's tag and an instrument's product string
18/// name.
19///
20/// Models whose files carry one set of tags are one family — the Electro 3 and 3 HP, the
21/// Electro 4 and 4D, the Stage Classic and Stage EX — because nothing in a file says
22/// which of the pair wrote it.
23///
24/// Two families are absent, because their files carry no tag to name them by: the older
25/// Leads ride a SysEx or MIDI carrier shared across four models, and the Electro 2's
26/// sample library is its own container under no CBIN tag at all.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Family {
29    Electro3,
30    Electro4,
31    Electro5,
32    Electro6,
33    Electro7,
34    StageClassic,
35    Stage2,
36    Stage3,
37    Stage4,
38    Piano,
39    Piano2,
40    Piano3,
41    Piano4,
42    Piano5,
43    Grand,
44    Wave,
45    Wave2,
46    C2,
47    C2D,
48    Organ3,
49    Lead4,
50    LeadA1,
51    Drum2,
52    Drum3,
53}
54
55/// A class of storage on the instrument: the four libraries a decoded body can refer
56/// into, plus the two singleton buffers no reference points at.
57///
58/// The wire codes live on `nord-usb`'s `ObjectClass`, which takes its library codes from
59/// [`Library`]. This is that same vocabulary without the wire, so a caller holding both
60/// converts rather than keeping a second table.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum Slot {
63    Piano,
64    Sample,
65    Program,
66    SetList,
67    /// The live buffer — the panel as it stands.
68    Live,
69    /// The global settings singleton.
70    Settings,
71}
72
73impl From<Library> for Slot {
74    fn from(library: Library) -> Slot {
75        match library {
76            Library::Piano => Slot::Piano,
77            Library::Sample => Slot::Sample,
78            Library::Program => Slot::Program,
79            Library::SetList => Slot::SetList,
80        }
81    }
82}
83
84impl Slot {
85    pub const ALL: [Slot; 6] = [
86        Slot::Piano,
87        Slot::Sample,
88        Slot::Program,
89        Slot::SetList,
90        Slot::Live,
91        Slot::Settings,
92    ];
93}
94
95/// Whether a family takes a tag in a class, and how that is known.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Acceptance {
98    /// A file under this tag has been written into this class and read back.
99    Confirmed,
100    /// The tag is this family's own and belongs in this class, but no such write has
101    /// been made.
102    Inferred,
103    /// The tag names a family and this class is not where it goes: another family's
104    /// tag, or this family's own in the wrong class.
105    Refused,
106    /// Nothing here says either way: a tag no family's own files carry — the shared
107    /// library formats and the carriers — or one this crate does not read.
108    Unknown,
109}
110
111/// How a row of [`TAKES`] is known.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113enum Evidence {
114    /// Confirmed on hardware.
115    Hardware,
116    /// Inferred from specimens; not confirmed on hardware.
117    Specimens,
118}
119
120impl Evidence {
121    fn acceptance(self) -> Acceptance {
122        match self {
123            Evidence::Hardware => Acceptance::Confirmed,
124            Evidence::Specimens => Acceptance::Inferred,
125        }
126    }
127}
128
129/// What each family keeps in each of its classes.
130///
131/// The Electro 5's rows are `Hardware` because this project has written each of them to
132/// the instrument and read the body back byte-exact: programs and set lists into slots,
133/// a sample instrument and a trimmed piano library into their partitions, and the live
134/// and settings singletons written in place. Every other row is the tag its family's
135/// format module declares, in the class the module's own name gives it.
136const TAKES: &[(Family, Slot, &str, Evidence)] = &[
137    (
138        Family::Electro3,
139        Slot::Program,
140        ne3::program::FORMAT,
141        Evidence::Specimens,
142    ),
143    (
144        Family::Electro4,
145        Slot::Program,
146        ne4::program::FORMAT,
147        Evidence::Specimens,
148    ),
149    (
150        Family::Electro4,
151        Slot::Live,
152        ne4::live::FORMAT,
153        Evidence::Specimens,
154    ),
155    (
156        Family::Electro4,
157        Slot::Settings,
158        ne4::settings::FORMAT,
159        Evidence::Specimens,
160    ),
161    (
162        Family::Electro5,
163        Slot::Program,
164        ne5::program::FORMAT,
165        Evidence::Hardware,
166    ),
167    (
168        Family::Electro5,
169        Slot::SetList,
170        ne5::song::FORMAT,
171        Evidence::Hardware,
172    ),
173    (
174        Family::Electro5,
175        Slot::Live,
176        ne5::live::FORMAT,
177        Evidence::Hardware,
178    ),
179    (
180        Family::Electro5,
181        Slot::Settings,
182        ne5::settings::FORMAT,
183        Evidence::Hardware,
184    ),
185    (
186        Family::Electro5,
187        Slot::Sample,
188        nsmp::FORMAT,
189        Evidence::Hardware,
190    ),
191    (
192        Family::Electro5,
193        Slot::Piano,
194        npno::FORMAT,
195        Evidence::Hardware,
196    ),
197    (
198        Family::Electro6,
199        Slot::Program,
200        ne6::program::FORMAT,
201        Evidence::Specimens,
202    ),
203    (
204        Family::Electro6,
205        Slot::Live,
206        ne6::live::FORMAT,
207        Evidence::Specimens,
208    ),
209    (
210        Family::Electro6,
211        Slot::Settings,
212        ne6::settings::FORMAT,
213        Evidence::Specimens,
214    ),
215    (
216        Family::Electro7,
217        Slot::Program,
218        ne7::program::FORMAT,
219        Evidence::Specimens,
220    ),
221    (
222        Family::Electro7,
223        Slot::Live,
224        ne7::live::FORMAT,
225        Evidence::Specimens,
226    ),
227    (
228        Family::Electro7,
229        Slot::Settings,
230        ne7::settings::FORMAT,
231        Evidence::Specimens,
232    ),
233    (
234        Family::StageClassic,
235        Slot::Program,
236        nsclassic::program::FORMAT,
237        Evidence::Specimens,
238    ),
239    (
240        Family::StageClassic,
241        Slot::Piano,
242        nsclassic::piano_library::FORMAT,
243        Evidence::Specimens,
244    ),
245    (
246        Family::Stage2,
247        Slot::Program,
248        ns2::program::FORMAT,
249        Evidence::Specimens,
250    ),
251    (
252        Family::Stage2,
253        Slot::Live,
254        ns2::live::FORMAT,
255        Evidence::Specimens,
256    ),
257    (
258        Family::Stage2,
259        Slot::Settings,
260        ns2::settings::FORMAT,
261        Evidence::Specimens,
262    ),
263    (
264        Family::Stage3,
265        Slot::Program,
266        ns3::program::FORMAT,
267        Evidence::Specimens,
268    ),
269    (
270        Family::Stage3,
271        Slot::Live,
272        ns3::live::FORMAT,
273        Evidence::Specimens,
274    ),
275    (
276        Family::Stage3,
277        Slot::SetList,
278        ns3::song::FORMAT,
279        Evidence::Specimens,
280    ),
281    (
282        Family::Stage3,
283        Slot::Settings,
284        ns3::settings::FORMAT,
285        Evidence::Specimens,
286    ),
287    (
288        Family::Stage4,
289        Slot::Program,
290        ns4::program::FORMAT,
291        Evidence::Specimens,
292    ),
293    (
294        Family::Stage4,
295        Slot::Live,
296        ns4::live::FORMAT,
297        Evidence::Specimens,
298    ),
299    (
300        Family::Stage4,
301        Slot::Settings,
302        ns4::settings::FORMAT,
303        Evidence::Specimens,
304    ),
305    (
306        Family::Piano,
307        Slot::Program,
308        np::program::FORMAT,
309        Evidence::Specimens,
310    ),
311    (
312        Family::Piano,
313        Slot::Live,
314        np::live::FORMAT,
315        Evidence::Specimens,
316    ),
317    (
318        Family::Piano,
319        Slot::Settings,
320        np::settings::FORMAT,
321        Evidence::Specimens,
322    ),
323    (
324        Family::Piano2,
325        Slot::Program,
326        np2::program::FORMAT,
327        Evidence::Specimens,
328    ),
329    (
330        Family::Piano2,
331        Slot::Live,
332        np2::live::FORMAT,
333        Evidence::Specimens,
334    ),
335    (
336        Family::Piano2,
337        Slot::Settings,
338        np2::settings::FORMAT,
339        Evidence::Specimens,
340    ),
341    (
342        Family::Piano3,
343        Slot::Program,
344        np3::program::FORMAT,
345        Evidence::Specimens,
346    ),
347    (
348        Family::Piano3,
349        Slot::Live,
350        np3::live::FORMAT,
351        Evidence::Specimens,
352    ),
353    (
354        Family::Piano3,
355        Slot::Settings,
356        np3::settings::FORMAT,
357        Evidence::Specimens,
358    ),
359    (
360        Family::Piano4,
361        Slot::Program,
362        np4::program::FORMAT,
363        Evidence::Specimens,
364    ),
365    (
366        Family::Piano4,
367        Slot::Live,
368        np4::live::FORMAT,
369        Evidence::Specimens,
370    ),
371    (
372        Family::Piano4,
373        Slot::Settings,
374        np4::settings::FORMAT,
375        Evidence::Specimens,
376    ),
377    (
378        Family::Piano5,
379        Slot::Program,
380        np5::program::FORMAT,
381        Evidence::Specimens,
382    ),
383    (
384        Family::Piano5,
385        Slot::Live,
386        np5::live::FORMAT,
387        Evidence::Specimens,
388    ),
389    (
390        Family::Piano5,
391        Slot::Settings,
392        np5::settings::FORMAT,
393        Evidence::Specimens,
394    ),
395    (
396        Family::Grand,
397        Slot::Program,
398        ng2::program::FORMAT,
399        Evidence::Specimens,
400    ),
401    (
402        Family::Grand,
403        Slot::Live,
404        ng2::live::FORMAT,
405        Evidence::Specimens,
406    ),
407    (
408        Family::Grand,
409        Slot::Settings,
410        ng2::settings::FORMAT,
411        Evidence::Specimens,
412    ),
413    (
414        Family::Wave,
415        Slot::Program,
416        nw::program::FORMAT,
417        Evidence::Specimens,
418    ),
419    (
420        Family::Wave,
421        Slot::Settings,
422        nw::settings::FORMAT,
423        Evidence::Specimens,
424    ),
425    (
426        Family::Wave2,
427        Slot::Program,
428        nw2::program::FORMAT,
429        Evidence::Specimens,
430    ),
431    (
432        Family::Wave2,
433        Slot::Live,
434        nw2::live::FORMAT,
435        Evidence::Specimens,
436    ),
437    (
438        Family::Wave2,
439        Slot::Settings,
440        nw2::settings::FORMAT,
441        Evidence::Specimens,
442    ),
443    (
444        Family::C2,
445        Slot::Program,
446        nc2::program::FORMAT,
447        Evidence::Specimens,
448    ),
449    (
450        Family::C2,
451        Slot::Settings,
452        nc2::settings::FORMAT,
453        Evidence::Specimens,
454    ),
455    (
456        Family::C2D,
457        Slot::Program,
458        nc2d::program::FORMAT,
459        Evidence::Specimens,
460    ),
461    (
462        Family::C2D,
463        Slot::Settings,
464        nc2d::settings::FORMAT,
465        Evidence::Specimens,
466    ),
467    (
468        Family::Organ3,
469        Slot::Program,
470        no3::program::FORMAT,
471        Evidence::Specimens,
472    ),
473    (
474        Family::Organ3,
475        Slot::Settings,
476        no3::settings::FORMAT,
477        Evidence::Specimens,
478    ),
479    (
480        Family::Lead4,
481        Slot::Program,
482        nl4::program::FORMAT,
483        Evidence::Specimens,
484    ),
485    (
486        Family::Lead4,
487        Slot::Settings,
488        nl4::settings::FORMAT,
489        Evidence::Specimens,
490    ),
491    (
492        Family::LeadA1,
493        Slot::Program,
494        nla1::program::FORMAT,
495        Evidence::Specimens,
496    ),
497    (
498        Family::LeadA1,
499        Slot::Settings,
500        nla1::settings::FORMAT,
501        Evidence::Specimens,
502    ),
503    (
504        Family::Drum2,
505        Slot::Program,
506        nd2::program::FORMAT,
507        Evidence::Specimens,
508    ),
509    (
510        Family::Drum3,
511        Slot::Program,
512        nd3::kit::FORMAT,
513        Evidence::Specimens,
514    ),
515];
516
517/// Which family's files carry a tag.
518///
519/// ⚠️ The shared library formats are deliberately absent: a sample instrument and a
520/// piano library are one file several families read, so a tag missing here is *not*
521/// evidence that an instrument refuses it. Only a tag listed here can refuse another
522/// family's instrument.
523const CARRIES: &[(&str, Family)] = &[
524    (ne3::program::FORMAT, Family::Electro3),
525    (ne3::organ_preset::FORMAT, Family::Electro3),
526    (ne4::program::FORMAT, Family::Electro4),
527    (ne4::live::FORMAT, Family::Electro4),
528    (ne4::settings::FORMAT, Family::Electro4),
529    (ne5::program::FORMAT, Family::Electro5),
530    (ne5::live::FORMAT, Family::Electro5),
531    (ne5::song::FORMAT, Family::Electro5),
532    (ne5::settings::FORMAT, Family::Electro5),
533    (ne6::program::FORMAT, Family::Electro6),
534    (ne6::live::FORMAT, Family::Electro6),
535    (ne6::settings::FORMAT, Family::Electro6),
536    (ne7::program::FORMAT, Family::Electro7),
537    (ne7::live::FORMAT, Family::Electro7),
538    (ne7::settings::FORMAT, Family::Electro7),
539    (nsclassic::program::FORMAT, Family::StageClassic),
540    (nsclassic::synth::FORMAT, Family::StageClassic),
541    (nsclassic::piano_library::FORMAT, Family::StageClassic),
542    (ns2::program::FORMAT, Family::Stage2),
543    (ns2::live::FORMAT, Family::Stage2),
544    (ns2::synth::FORMAT, Family::Stage2),
545    (ns2::settings::FORMAT, Family::Stage2),
546    (ns3::program::FORMAT, Family::Stage3),
547    (ns3::live::FORMAT, Family::Stage3),
548    (ns3::song::FORMAT, Family::Stage3),
549    (ns3::synth::FORMAT, Family::Stage3),
550    (ns3::settings::FORMAT, Family::Stage3),
551    (ns4::program::FORMAT, Family::Stage4),
552    (ns4::live::FORMAT, Family::Stage4),
553    (ns4::synth::FORMAT, Family::Stage4),
554    (ns4::piano_preset::FORMAT, Family::Stage4),
555    (ns4::organ_preset::FORMAT, Family::Stage4),
556    (ns4::settings::FORMAT, Family::Stage4),
557    (np::program::FORMAT, Family::Piano),
558    (np::live::FORMAT, Family::Piano),
559    (np::settings::FORMAT, Family::Piano),
560    (np2::program::FORMAT, Family::Piano2),
561    (np2::live::FORMAT, Family::Piano2),
562    (np2::settings::FORMAT, Family::Piano2),
563    (np3::program::FORMAT, Family::Piano3),
564    (np3::live::FORMAT, Family::Piano3),
565    (np3::settings::FORMAT, Family::Piano3),
566    (np4::program::FORMAT, Family::Piano4),
567    (np4::live::FORMAT, Family::Piano4),
568    (np4::settings::FORMAT, Family::Piano4),
569    (np5::program::FORMAT, Family::Piano5),
570    (np5::live::FORMAT, Family::Piano5),
571    (np5::settings::FORMAT, Family::Piano5),
572    (ng2::program::FORMAT, Family::Grand),
573    (ng2::live::FORMAT, Family::Grand),
574    (ng2::settings::FORMAT, Family::Grand),
575    (nw::program::FORMAT, Family::Wave),
576    (nw::settings::FORMAT, Family::Wave),
577    (nw2::program::FORMAT, Family::Wave2),
578    (nw2::live::FORMAT, Family::Wave2),
579    (nw2::settings::FORMAT, Family::Wave2),
580    (nc2::program::FORMAT, Family::C2),
581    (nc2::settings::FORMAT, Family::C2),
582    (npip::pipe_library::FORMAT, Family::C2),
583    (nc2d::program::FORMAT, Family::C2D),
584    (nc2d::settings::FORMAT, Family::C2D),
585    (no3::program::FORMAT, Family::Organ3),
586    (no3::settings::FORMAT, Family::Organ3),
587    (nl4::program::FORMAT, Family::Lead4),
588    (nl4::performance::FORMAT, Family::Lead4),
589    (nl4::settings::FORMAT, Family::Lead4),
590    (nla1::program::FORMAT, Family::LeadA1),
591    (nla1::performance::FORMAT, Family::LeadA1),
592    (nla1::settings::FORMAT, Family::LeadA1),
593    (nd2::program::FORMAT, Family::Drum2),
594    (nd3::kit::FORMAT, Family::Drum3),
595];
596
597// Every hand-written `FORMAT` reaches one of these two tables, and the stub macro
598// asserts its own — so a tag of the wrong length fails the build rather than the
599// first `cbin::tag` call.
600const _: () = {
601    let mut i = 0;
602    while i < TAKES.len() {
603        assert!(TAKES[i].2.len() == 4, "a CBIN tag is four bytes");
604        i += 1;
605    }
606    let mut i = 0;
607    while i < CARRIES.len() {
608        assert!(CARRIES[i].0.len() == 4, "a CBIN tag is four bytes");
609        i += 1;
610    }
611};
612
613impl Family {
614    pub const ALL: [Family; 24] = [
615        Family::Electro3,
616        Family::Electro4,
617        Family::Electro5,
618        Family::Electro6,
619        Family::Electro7,
620        Family::StageClassic,
621        Family::Stage2,
622        Family::Stage3,
623        Family::Stage4,
624        Family::Piano,
625        Family::Piano2,
626        Family::Piano3,
627        Family::Piano4,
628        Family::Piano5,
629        Family::Grand,
630        Family::Wave,
631        Family::Wave2,
632        Family::C2,
633        Family::C2D,
634        Family::Organ3,
635        Family::Lead4,
636        Family::LeadA1,
637        Family::Drum2,
638        Family::Drum3,
639    ];
640
641    /// The model, spelled as [`Identity::kind`](crate::Identity::kind) spells it.
642    pub fn label(self) -> &'static str {
643        match self {
644            Family::Electro3 => "Electro 3",
645            Family::Electro4 => "Electro 4",
646            Family::Electro5 => "Electro 5",
647            Family::Electro6 => "Electro 6",
648            Family::Electro7 => "Electro 7",
649            Family::StageClassic => "Stage Classic",
650            Family::Stage2 => "Stage 2",
651            Family::Stage3 => "Stage 3",
652            Family::Stage4 => "Stage 4",
653            Family::Piano => "Piano",
654            Family::Piano2 => "Piano 2",
655            Family::Piano3 => "Piano 3",
656            Family::Piano4 => "Piano 4",
657            Family::Piano5 => "Piano 5",
658            Family::Grand => "Grand",
659            Family::Wave => "Wave",
660            Family::Wave2 => "Wave 2",
661            Family::C2 => "C2",
662            Family::C2D => "C2D",
663            Family::Organ3 => "no3 organ",
664            Family::Lead4 => "Lead 4",
665            Family::LeadA1 => "Lead A1",
666            Family::Drum2 => "Drum 2",
667            Family::Drum3 => "Drum 3P",
668        }
669    }
670
671    /// What this family's USB product string contains, where the model's own name is
672    /// known.
673    ///
674    /// Usually the [`label`](Self::label), and separate from it because two rows differ:
675    /// the Stage Classic calls itself `Nord Stage`, "Classic" being the name this
676    /// project gives it to tell it from the numbered Stages, and the `no3` organ's model
677    /// name is not known at all — so no product string names it.
678    fn product_name(self) -> Option<&'static str> {
679        match self {
680            Family::StageClassic => Some("Stage"),
681            Family::Organ3 => None,
682            named => Some(named.label()),
683        }
684    }
685
686    /// The family a USB product string names.
687    ///
688    /// The string is the model and then the keybed — an Electro 5 reads
689    /// `Nord Electro 5`, and a 73-key 5D reads `Nord Electro 5D 73` — so the family is
690    /// the **longest** [`product_name`](Self::product_name) the string contains. Longest
691    /// because `Stage` sits inside `Stage 3`, `Piano` inside `Piano 5` and `C2` inside
692    /// `C2D`.
693    ///
694    /// For the Electro 5, `Nord Electro 5` is the descriptor string the recorded
695    /// exchanges in `nord-usb`'s replay scripts carry. Confirmed on hardware.
696    pub fn from_product(product: &str) -> Option<Family> {
697        Family::ALL
698            .into_iter()
699            .filter_map(|family| Some((family, family.product_name()?)))
700            .filter(|(_, name)| product.contains(name))
701            .max_by_key(|(_, name)| name.len())
702            .map(|(family, _)| family)
703    }
704
705    /// The family whose files carry `tag`, where one family's do. `None` for the shared
706    /// library formats, the carriers, and anything this crate does not read.
707    pub fn of_tag(tag: &str) -> Option<Family> {
708        CARRIES
709            .iter()
710            .find(|(held, _)| *held == tag)
711            .map(|(_, family)| *family)
712    }
713
714    /// Whether this family keeps files under `tag` in `slot`.
715    pub fn accepts(self, slot: Slot, tag: &str) -> Acceptance {
716        if let Some((_, _, _, evidence)) = TAKES
717            .iter()
718            .find(|(family, held, format, _)| (*family, *held, *format) == (self, slot, tag))
719        {
720            return evidence.acceptance();
721        }
722        // A tag naming a family, this one included, does not belong in a class the
723        // table does not list it under: a `ns4p` is refused from the piano partition by
724        // the Stage 4 as surely as by an Electro 5.
725        match Family::of_tag(tag) {
726            Some(_) => Acceptance::Refused,
727            None => Acceptance::Unknown,
728        }
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    /// The four outcomes, on the one family whose rows are hardware.
737    #[test]
738    fn an_electro_5_takes_its_own_program_and_refuses_a_stage_4s() {
739        let e5 = Family::Electro5;
740        assert_eq!(
741            e5.accepts(Slot::Program, ne5::program::FORMAT),
742            Acceptance::Confirmed
743        );
744        assert_eq!(
745            e5.accepts(Slot::Program, ns4::program::FORMAT),
746            Acceptance::Refused
747        );
748        assert_eq!(
749            Family::Stage4.accepts(Slot::Program, ns4::program::FORMAT),
750            Acceptance::Inferred
751        );
752        assert_eq!(e5.accepts(Slot::Program, "zzzz"), Acceptance::Unknown);
753    }
754
755    /// A family's own tag still belongs in one class: the Stage 4 keeps programs in the
756    /// program slots, so a `ns4p` offered to the piano partition is refused rather than
757    /// left an open question.
758    #[test]
759    fn a_familys_own_tag_is_refused_outside_its_class() {
760        assert_eq!(
761            Family::Stage4.accepts(Slot::Piano, ns4::program::FORMAT),
762            Acceptance::Refused
763        );
764        for (tag, owner) in CARRIES {
765            let open: Vec<Slot> = Slot::ALL
766                .into_iter()
767                .filter(|slot| owner.accepts(*slot, tag) != Acceptance::Refused)
768                .collect();
769            assert!(
770                open.len() <= 1,
771                "{} leaves its own {tag} unrefused in {open:?}",
772                owner.label(),
773            );
774        }
775    }
776
777    /// A shared library format names no family, so it can never refuse an instrument
778    /// this table says nothing about.
779    #[test]
780    fn a_shared_library_format_refuses_nobody() {
781        assert_eq!(Family::of_tag(nsmp::FORMAT), None);
782        assert_eq!(Family::of_tag(npno::FORMAT), None);
783        assert_eq!(
784            Family::Electro5.accepts(Slot::Sample, nsmp::FORMAT),
785            Acceptance::Confirmed
786        );
787        assert_eq!(
788            Family::Stage4.accepts(Slot::Sample, nsmp::FORMAT),
789            Acceptance::Unknown
790        );
791    }
792
793    /// Every accepted tag is its family's own or is shared, and lands in exactly one of
794    /// that family's classes: two classes for one tag would let a file be sent to the
795    /// wrong partition and still be called accepted.
796    #[test]
797    fn every_tag_a_family_takes_lands_in_one_class() {
798        for family in Family::ALL {
799            let mine: Vec<&str> = TAKES
800                .iter()
801                .filter(|(held, _, _, _)| *held == family)
802                .map(|(_, _, tag, _)| *tag)
803                .collect();
804            for tag in &mine {
805                assert_eq!(
806                    mine.iter().filter(|held| *held == tag).count(),
807                    1,
808                    "{} lists {tag} in more than one class",
809                    family.label()
810                );
811                assert!(
812                    Family::of_tag(tag).is_none_or(|held| held == family),
813                    "{} takes {tag}, which is another family's",
814                    family.label()
815                );
816            }
817        }
818    }
819
820    /// A tag another family's files carry is refused in every class.
821    #[test]
822    fn a_foreign_tag_is_refused_in_every_class() {
823        for family in Family::ALL {
824            for (tag, owner) in CARRIES.iter().filter(|(_, owner)| *owner != family) {
825                for slot in Slot::ALL {
826                    assert_eq!(
827                        family.accepts(slot, tag),
828                        Acceptance::Refused,
829                        "{} should refuse {}'s {tag}",
830                        family.label(),
831                        owner.label()
832                    );
833                }
834            }
835        }
836    }
837
838    /// The keybed follows the model in the product string, and a shorter model name is a
839    /// substring of a longer one.
840    #[test]
841    fn the_product_string_names_the_model_and_then_the_keybed() {
842        assert_eq!(
843            Family::from_product("Nord Electro 5"),
844            Some(Family::Electro5)
845        );
846        assert_eq!(
847            Family::from_product("Nord Electro 5D 73"),
848            Some(Family::Electro5)
849        );
850        assert_eq!(Family::from_product("Nord Piano 88"), Some(Family::Piano));
851        assert_eq!(
852            Family::from_product("Nord Piano 5 73"),
853            Some(Family::Piano5)
854        );
855        assert_eq!(Family::from_product("Nord C2D"), Some(Family::C2D));
856        assert_eq!(Family::from_product("Nord Wave 2"), Some(Family::Wave2));
857        // The instrument calls itself `Nord Stage`; "Classic" is this project's word.
858        assert_eq!(
859            Family::from_product("Nord Stage 88"),
860            Some(Family::StageClassic)
861        );
862        assert_eq!(
863            Family::from_product("Nord Stage EX 76"),
864            Some(Family::StageClassic)
865        );
866        assert_eq!(
867            Family::from_product("Nord Stage 3 88"),
868            Some(Family::Stage3)
869        );
870        // No product string is known for the `no3` organ, so its label is not one.
871        assert_eq!(Family::from_product("Nord no3 organ"), None);
872        assert_eq!(Family::from_product("Some other keyboard"), None);
873    }
874
875    /// Two families under one name would make [`Family::from_product`] pick between them
876    /// arbitrarily.
877    #[test]
878    fn no_two_families_share_a_name() {
879        for family in Family::ALL {
880            assert_eq!(
881                Family::ALL
882                    .iter()
883                    .filter(|held| held.label() == family.label())
884                    .count(),
885                1,
886                "{} is not a unique name",
887                family.label()
888            );
889            let Some(name) = family.product_name() else {
890                continue;
891            };
892            assert_eq!(
893                Family::ALL
894                    .iter()
895                    .filter(|held| held.product_name() == Some(name))
896                    .count(),
897                1,
898                "{} is not a unique product name",
899                family.label()
900            );
901        }
902    }
903}