Skip to main content

music21_rs/instrument/
mod.rs

1//! Instruments: music21's `instrument` module.
2//!
3//! An [`Instrument`] is one of music21's instrument classes -- a piano, a
4//! clarinet in B-flat, a triangle -- with what an instrument of that class
5//! starts out as: its name and abbreviation, its General MIDI program and
6//! sound, the range it plays, the interval from what is written for it to
7//! what sounds, and for unpitched percussion the General MIDI drum it plays.
8//! The class survives as the instrument's *kind*, and a kind belongs to the
9//! families above it, so a clarinet is a woodwind and a triangle unpitched
10//! percussion ([`Instrument::is_a`]).
11//!
12//! An instrument is found by kind ([`Instrument::of_kind`]), by MIDI program
13//! ([`Instrument::from_midi_program`]) or by what a score calls it, in six
14//! languages and their abbreviations ([`Instrument::from_name`], music21's
15//! `fromString`), which also reads the key a transposing instrument is in:
16//! `"Clarinet in A"` is a clarinet sounding a minor third below what is
17//! written.
18//!
19//! What an instrument does to a stream it stands in is here too: moving the
20//! instruments notes keep as their own into the stream and back
21//! ([`unbundle_instruments`], [`bundle_instruments`]), folding together the
22//! instruments a part says twice ([`deduplicate`]) and splitting a score into
23//! a part for each instrument ([`partition_by_instrument`]).
24
25mod streams;
26mod tables;
27
28pub(crate) use streams::settle_by_offset;
29pub use streams::{bundle_instruments, deduplicate, partition_by_instrument, unbundle_instruments};
30
31use std::fmt;
32
33use crate::{
34    error::{Error, Result},
35    interval::Interval,
36    pitch::Pitch,
37};
38
39use tables::{KINDS, Kind};
40
41/// Which of music21's name tables [`Instrument::from_name`] reads: music21's
42/// `SearchLanguage`.
43#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45pub enum SearchLanguage {
46    /// Every language and the abbreviations, English winning where two
47    /// disagree.
48    All,
49    /// English names.
50    English,
51    /// French names.
52    French,
53    /// German names.
54    German,
55    /// Italian names.
56    Italian,
57    /// Russian names, transliterated.
58    Russian,
59    /// Spanish names.
60    Spanish,
61    /// The standard abbreviations, an honorary language.
62    Abbreviation,
63}
64
65impl SearchLanguage {
66    /// Every language on its own, in music21's order, without
67    /// [`SearchLanguage::All`].
68    pub const EACH: [SearchLanguage; 7] = [
69        SearchLanguage::English,
70        SearchLanguage::French,
71        SearchLanguage::German,
72        SearchLanguage::Italian,
73        SearchLanguage::Russian,
74        SearchLanguage::Spanish,
75        SearchLanguage::Abbreviation,
76    ];
77
78    /// music21's name for the language: `"all"`, `"english"` and so on.
79    pub fn as_str(self) -> &'static str {
80        match self {
81            Self::All => "all",
82            Self::English => "english",
83            Self::French => "french",
84            Self::German => "german",
85            Self::Italian => "italian",
86            Self::Russian => "russian",
87            Self::Spanish => "spanish",
88            Self::Abbreviation => "abbreviation",
89        }
90    }
91
92    /// The language music21 calls by that name, in any case.
93    pub fn from_name(name: &str) -> Result<Self> {
94        let lower = name.to_lowercase();
95        std::iter::once(Self::All)
96            .chain(Self::EACH)
97            .find(|language| language.as_str() == lower)
98            .ok_or_else(|| {
99                Error::Instrument(format!("Chosen language {lower} not currently supported."))
100            })
101    }
102
103    fn table(self) -> &'static [(&'static str, &'static str)] {
104        match self {
105            Self::All => &tables::ALL,
106            Self::English => &tables::ENGLISH,
107            Self::French => &tables::FRENCH,
108            Self::German => &tables::GERMAN,
109            Self::Italian => &tables::ITALIAN,
110            Self::Russian => &tables::RUSSIAN,
111            Self::Spanish => &tables::SPANISH,
112            Self::Abbreviation => &tables::ABBREVIATION,
113        }
114    }
115}
116
117/// An instrument of one of music21's kinds: music21's `Instrument`.
118///
119/// ```
120/// use music21_rs::instrument::{Instrument, SearchLanguage};
121///
122/// let clarinet = Instrument::from_name("Clarinet in A", SearchLanguage::All)?;
123/// assert_eq!(clarinet.kind(), "Clarinet");
124/// assert!(clarinet.is_a("WoodwindInstrument"));
125/// assert_eq!(clarinet.transposition().unwrap().directed_name(), "m-3");
126/// # Ok::<(), music21_rs::Error>(())
127/// ```
128#[derive(Clone, Debug, PartialEq)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130#[must_use]
131pub struct Instrument {
132    kind: String,
133    name: Option<String>,
134    abbreviation: Option<String>,
135    part_name: Option<String>,
136    part_abbreviation: Option<String>,
137    sound: Option<String>,
138    midi_program: Option<u8>,
139    midi_channel: Option<u8>,
140    lowest: Option<Pitch>,
141    highest: Option<Pitch>,
142    transposition: Option<Interval>,
143    percussion_map: bool,
144    percussion_pitch: Option<u8>,
145    string_pitches: Option<Vec<Pitch>>,
146    modifier: Option<String>,
147    #[cfg_attr(feature = "serde", serde(default))]
148    part_id: Option<String>,
149    #[cfg_attr(feature = "serde", serde(default))]
150    instrument_id: Option<String>,
151}
152
153impl Default for Instrument {
154    /// music21's bare `Instrument`, which says nothing about itself.
155    fn default() -> Self {
156        Self::of_kind("Instrument").expect("music21's base class is in the table")
157    }
158}
159
160fn kind_named(class: &str) -> Option<&'static Kind> {
161    KINDS.iter().find(|kind| kind.class == class)
162}
163
164impl Instrument {
165    /// music21's bare `Instrument`, which says nothing about itself.
166    pub fn new() -> Self {
167        Self::default()
168    }
169
170    /// An instrument of the kind music21 names by that class, as a new one of
171    /// that class starts out: `"Piano"`, `"BassClarinet"`, `"Triangle"`.
172    pub fn of_kind(class: &str) -> Result<Self> {
173        let kind = kind_named(class)
174            .ok_or_else(|| Error::Instrument(format!("no instrument class {class}")))?;
175        let pitch = |name: Option<&str>| name.map(Pitch::from_name).transpose();
176        Ok(Self {
177            kind: kind.class.to_string(),
178            name: kind.name.map(String::from),
179            abbreviation: kind.abbreviation.map(String::from),
180            part_name: None,
181            part_abbreviation: None,
182            sound: kind.sound.map(String::from),
183            midi_program: kind.midi_program,
184            midi_channel: kind.midi_channel,
185            lowest: pitch(kind.lowest)?,
186            highest: pitch(kind.highest)?,
187            transposition: kind.transposition.map(Interval::from_name).transpose()?,
188            percussion_map: kind.percussion_map,
189            percussion_pitch: kind.percussion_pitch,
190            string_pitches: tables::STRING_PITCHES
191                .iter()
192                .find(|(named, _)| *named == kind.class)
193                .map(|(_, names)| names.iter().map(Pitch::from_name).collect())
194                .transpose()?,
195            modifier: tables::MODIFIERS
196                .iter()
197                .find(|(named, ..)| *named == kind.class)
198                .map(|(_, modifier, ..)| modifier.to_string()),
199            part_id: None,
200            instrument_id: None,
201        })
202    }
203
204    /// The id of the part this instrument plays, as a score writes it:
205    /// music21's `partId`, `P1` in a score read from MusicXML.
206    pub fn part_id(&self) -> Option<&str> {
207        self.part_id.as_deref()
208    }
209
210    /// Sets the id of the part this instrument plays.
211    pub fn set_part_id(&mut self, id: Option<String>) {
212        self.part_id = id;
213    }
214
215    /// The instrument's own id within its part, `P1-I1`: music21's
216    /// `instrumentId`.
217    pub fn instrument_id(&self) -> Option<&str> {
218        self.instrument_id.as_deref()
219    }
220
221    /// Sets the instrument's own id.
222    pub fn set_instrument_id(&mut self, id: Option<String>) {
223        self.instrument_id = id;
224    }
225
226    /// Every kind of instrument music21 has a class for, families first.
227    pub fn kinds() -> impl Iterator<Item = &'static str> {
228        KINDS.iter().map(|kind| kind.class)
229    }
230
231    /// The instrument music21 makes of a General MIDI program, 0 to 127,
232    /// with its program set to that number: music21's
233    /// `instrumentFromMidiProgram`.
234    pub fn from_midi_program(program: u8) -> Result<Self> {
235        let class = tables::MIDI_PROGRAMS
236            .iter()
237            .find(|(number, _)| *number == program)
238            .map(|(_, class)| *class)
239            .ok_or_else(|| {
240                Error::Instrument(format!("No instrument found for MIDI program {program}"))
241            })?;
242        let mut made = Self::of_kind(class)?;
243        made.midi_program = Some(program);
244        Ok(made)
245    }
246
247    /// The instrument a score's name for one means, read the way music21's
248    /// `fromString` reads it.
249    ///
250    /// The name is taken apart into every run of its words, and the longest
251    /// run naming an instrument wins, a later one breaking a tie unless the
252    /// one found is already of its kind -- so `"Bb Piccolo Trumpet"` is a
253    /// trumpet, not a piccolo. The instrument is named with the text as
254    /// given. For an instrument that comes in several keys, a run
255    /// naming a pitch sets its transposition: `"Horn in F"`, `"Klarinette in
256    /// B."`.
257    pub fn from_name(text: &str, language: SearchLanguage) -> Result<Self> {
258        let table = language.table();
259        let cleaned: String = text
260            .replace('.', " ")
261            .to_lowercase()
262            .chars()
263            .filter(|c| !c.is_ascii_punctuation())
264            .collect();
265        let runs = word_runs(&cleaned);
266
267        let mut best: Option<(Self, String)> = None;
268        for run in &runs {
269            let Some((_, class)) = table.iter().find(|(name, _)| name == run) else {
270                continue;
271            };
272            let candidate = Self::of_kind(class)?;
273            let Some(candidate_name) = candidate.best_name().map(str::to_lowercase) else {
274                continue;
275            };
276            let replaces = match &best {
277                None => true,
278                Some((found, found_name)) => {
279                    candidate_name.split_whitespace().count()
280                        >= found_name.split_whitespace().count()
281                        && !found.is_a(class)
282                }
283            };
284            if replaces {
285                let mut named = candidate;
286                named.name = Some(text.to_string());
287                best = Some((named, candidate_name));
288            }
289        }
290        let Some((mut found, found_name)) = best else {
291            return Err(Error::Instrument(format!(
292                "Could not match string with instrument: {text}"
293            )));
294        };
295        let Some((_, keys)) = tables::TRANSPOSITIONS
296            .iter()
297            .find(|(name, _)| *name == found_name)
298        else {
299            return Ok(found);
300        };
301        for run in &runs {
302            let Some((_, pitch)) = tables::PITCH_NAMES.iter().find(|(name, _)| name == run) else {
303                continue;
304            };
305            if let Some((_, interval)) = keys.iter().find(|(key, _)| key == pitch) {
306                found.transposition = Some(Interval::from_name(interval)?);
307                break;
308            }
309        }
310        Ok(found)
311    }
312
313    /// The kind of instrument this is: music21's class name for it.
314    pub fn kind(&self) -> &str {
315        &self.kind
316    }
317
318    /// music21's own name for this kind of instrument, where it is one
319    /// music21 has a class for: the name as the table spells it, which is
320    /// what `is_a` and the class lineage answer with.
321    pub fn class_name(&self) -> Option<&'static str> {
322        kind_named(&self.kind).map(|kind| kind.class)
323    }
324
325    /// The families this kind belongs to, nearest first and ending with
326    /// `Instrument`: a clarinet is a `WoodwindInstrument`.
327    pub fn families(&self) -> &'static [&'static str] {
328        kind_named(&self.kind).map_or(&[], |kind| kind.parents)
329    }
330
331    /// Whether this is an instrument of that kind or of a kind in that
332    /// family, as music21's `isinstance` answers.
333    pub fn is_a(&self, class: &str) -> bool {
334        self.kind == class || self.families().contains(&class)
335    }
336
337    /// The instrument's name: music21's `instrumentName`.
338    pub fn name(&self) -> Option<&str> {
339        self.name.as_deref()
340    }
341
342    /// Sets the instrument's name, or takes it away.
343    pub fn set_name(&mut self, name: Option<String>) {
344        self.name = name;
345    }
346
347    /// The instrument's abbreviation: music21's `instrumentAbbreviation`.
348    pub fn abbreviation(&self) -> Option<&str> {
349        self.abbreviation.as_deref()
350    }
351
352    /// Sets the abbreviation, or takes it away.
353    pub fn set_abbreviation(&mut self, abbreviation: Option<String>) {
354        self.abbreviation = abbreviation;
355    }
356
357    /// What the part this instrument plays is called, where that is not
358    /// the instrument's name: music21's `partName`.
359    pub fn part_name(&self) -> Option<&str> {
360        self.part_name.as_deref()
361    }
362
363    /// Sets what the part is called.
364    pub fn set_part_name(&mut self, name: Option<String>) {
365        self.part_name = name;
366    }
367
368    /// The part's abbreviation: music21's `partAbbreviation`.
369    pub fn part_abbreviation(&self) -> Option<&str> {
370        self.part_abbreviation.as_deref()
371    }
372
373    /// Sets the part's abbreviation.
374    pub fn set_part_abbreviation(&mut self, abbreviation: Option<String>) {
375        self.part_abbreviation = abbreviation;
376    }
377
378    /// The best name there is for this instrument: the part's name, the
379    /// part's abbreviation, the instrument's name or its abbreviation, the
380    /// first of those given. music21's `bestName`.
381    pub fn best_name(&self) -> Option<&str> {
382        self.part_name
383            .as_deref()
384            .or(self.part_abbreviation.as_deref())
385            .or(self.name.as_deref())
386            .or(self.abbreviation.as_deref())
387    }
388
389    /// The sound's MusicXML id, `"wind.reed.clarinet.bflat"` and the like:
390    /// music21's `instrumentSound`.
391    pub fn sound(&self) -> Option<&str> {
392        self.sound.as_deref()
393    }
394
395    /// Sets the sound's id.
396    pub fn set_sound(&mut self, sound: Option<String>) {
397        self.sound = sound;
398    }
399
400    /// The General MIDI program, counted from nought: music21's `midiProgram`.
401    pub fn midi_program(&self) -> Option<u8> {
402        self.midi_program
403    }
404
405    /// Sets the General MIDI program.
406    pub fn set_midi_program(&mut self, program: Option<u8>) {
407        self.midi_program = program;
408    }
409
410    /// The MIDI channel, counted from nought, 9 being the drums: music21's
411    /// `midiChannel`.
412    pub fn midi_channel(&self) -> Option<u8> {
413        self.midi_channel
414    }
415
416    /// Sets the MIDI channel.
417    pub fn set_midi_channel(&mut self, channel: Option<u8>) {
418        self.midi_channel = channel;
419    }
420
421    /// The lowest note the instrument plays, as it sounds: music21's
422    /// `lowestNote`.
423    pub fn lowest(&self) -> Option<&Pitch> {
424        self.lowest.as_ref()
425    }
426
427    /// Sets the lowest note.
428    pub fn set_lowest(&mut self, pitch: Option<Pitch>) {
429        self.lowest = pitch;
430    }
431
432    /// The highest note the instrument plays: music21's `highestNote`.
433    pub fn highest(&self) -> Option<&Pitch> {
434        self.highest.as_ref()
435    }
436
437    /// Sets the highest note.
438    pub fn set_highest(&mut self, pitch: Option<Pitch>) {
439        self.highest = pitch;
440    }
441
442    /// The interval from what is written for the instrument to what sounds:
443    /// a B-flat clarinet sounds a major second below, `M-2`. music21's
444    /// `transposition`.
445    pub fn transposition(&self) -> Option<&Interval> {
446        self.transposition.as_ref()
447    }
448
449    /// Sets the transposition, or takes it away.
450    pub fn set_transposition(&mut self, interval: Option<Interval>) {
451        self.transposition = interval;
452    }
453
454    /// What sounds when `written` is played: the pitch moved by the
455    /// transposition, or itself for an instrument that does not transpose.
456    pub fn sounding(&self, written: &Pitch) -> Result<Pitch> {
457        match &self.transposition {
458            Some(interval) => interval.transpose_pitch(written),
459            None => Ok(written.clone()),
460        }
461    }
462
463    /// What is written for `sounding` to be heard.
464    pub fn written(&self, sounding: &Pitch) -> Result<Pitch> {
465        match &self.transposition {
466            Some(interval) => interval.reversed()?.transpose_pitch(sounding),
467            None => Ok(sounding.clone()),
468        }
469    }
470
471    /// Whether the instrument is one of General MIDI's percussion map:
472    /// music21's `inGMPercMap`.
473    pub fn in_percussion_map(&self) -> bool {
474        self.percussion_map
475    }
476
477    /// The General MIDI drum the instrument plays, for unpitched percussion:
478    /// music21's `percMapPitch`.
479    pub fn percussion_pitch(&self) -> Option<u8> {
480        self.percussion_pitch
481    }
482
483    /// Sets the General MIDI drum the instrument plays.
484    pub fn set_percussion_pitch(&mut self, pitch: Option<u8>) {
485        self.percussion_pitch = pitch;
486    }
487
488    /// Says whether the instrument is in the General MIDI percussion map.
489    pub fn set_in_percussion_map(&mut self, in_map: bool) {
490        self.percussion_map = in_map;
491    }
492
493    /// The open strings, lowest first unless the tuning is reentrant, for a
494    /// string instrument music21 tunes: music21's `stringPitches`.
495    ///
496    /// ```
497    /// use music21_rs::Instrument;
498    ///
499    /// let violin = Instrument::of_kind("Violin")?;
500    /// let open: Vec<String> = violin
501    ///     .string_pitches()
502    ///     .unwrap()
503    ///     .iter()
504    ///     .map(|pitch| pitch.name_with_octave())
505    ///     .collect();
506    /// assert_eq!(open, ["G3", "D4", "A4", "E5"]);
507    /// # Ok::<(), music21_rs::Error>(())
508    /// ```
509    pub fn string_pitches(&self) -> Option<&[Pitch]> {
510        self.string_pitches.as_deref()
511    }
512
513    /// Retunes the open strings, a scordatura.
514    pub fn set_string_pitches(&mut self, pitches: Vec<Pitch>) {
515        self.string_pitches = Some(pitches);
516    }
517
518    /// How an unpitched percussion instrument is played, such as `"low"` for
519    /// a bongo or `"rimshot"` for a snare: music21's `modifier`.
520    pub fn modifier(&self) -> Option<&str> {
521        self.modifier.as_deref()
522    }
523
524    /// Sets how the instrument is played. A modifier its drum map knows
525    /// changes the drum it plays, and is written back as that drum's own name
526    /// for it, so a woodblock set to `"LO"` plays the low block and says
527    /// `"low"`.
528    ///
529    /// ```
530    /// use music21_rs::Instrument;
531    ///
532    /// let mut woodblock = Instrument::of_kind("Woodblock")?;
533    /// woodblock.set_modifier("LO");
534    /// assert_eq!(woodblock.modifier(), Some("low"));
535    /// assert_eq!(woodblock.percussion_pitch(), Some(77));
536    /// # Ok::<(), music21_rs::Error>(())
537    /// ```
538    pub fn set_modifier(&mut self, modifier: &str) {
539        let mut modifier = modifier.trim().to_lowercase();
540        let table = std::iter::once(self.kind.as_str())
541            .chain(self.families().iter().copied())
542            .find_map(|class| tables::MODIFIERS.iter().find(|(named, ..)| *named == class));
543        if self.percussion_map
544            && let Some((_, _, to_pitch, to_name)) = table
545            && let Some((_, pitch)) = to_pitch.iter().find(|(named, _)| *named == modifier)
546        {
547            self.percussion_pitch = Some(*pitch);
548            if let Some((_, named)) = to_name.iter().find(|(drum, _)| drum == pitch) {
549                modifier = (*named).to_string();
550            }
551        }
552        self.modifier = Some(modifier);
553    }
554
555    /// Takes a MIDI channel no instrument in `used` has, and returns it:
556    /// music21's `autoAssignMidiChannel`.
557    ///
558    /// Unpitched percussion goes to channel 9 when that is free; otherwise
559    /// the lowest free channel below `max_channels` that is not a drum
560    /// channel is taken. It is an error when every channel is in use; where
561    /// none is free the answer is 0 and no channel is set, as music21 answers.
562    pub fn auto_assign_midi_channel(&mut self, used: &[u8], max_channels: u8) -> Result<u8> {
563        let used: std::collections::BTreeSet<u8> = used.iter().copied().collect();
564        if self.is_a("UnpitchedPercussion") && !used.contains(&9) {
565            self.midi_channel = Some(9);
566            return Ok(9);
567        }
568        if used.is_empty() {
569            self.midi_channel = Some(0);
570            return Ok(0);
571        }
572        if used.len() >= usize::from(max_channels).saturating_sub(1) {
573            return Err(Error::Instrument(
574                "we are out of midi channels! help!".to_string(),
575            ));
576        }
577        for channel in 0..max_channels {
578            if used.contains(&channel) || channel % 16 == 9 {
579                continue;
580            }
581            self.midi_channel = Some(channel);
582            return Ok(channel);
583        }
584        Ok(0)
585    }
586
587    /// Every name a score may call this kind of instrument in one language,
588    /// or in each of them: music21's `getAllNamesForInstrument`.
589    ///
590    /// The names are those of the nearest kind in the instrument's family
591    /// that the tables name at all -- its own, or for a guitar, which they do
592    /// not name, a string instrument's -- whatever the instrument itself is
593    /// called.
594    pub fn all_names(&self, language: SearchLanguage) -> Vec<(SearchLanguage, Vec<&'static str>)> {
595        // An instrument no table names by its kind is looked up by its own
596        // name, as the class it would be.
597        let named = std::iter::once(self.kind.as_str())
598            .chain(self.families().iter().copied())
599            .find(|class| tables::ALL.iter().any(|(_, named)| named == class))
600            .or(self.name.as_deref());
601        let languages: &[SearchLanguage] = match language {
602            SearchLanguage::All => &SearchLanguage::EACH,
603            _ => std::slice::from_ref(&language),
604        };
605        languages
606            .iter()
607            .map(|language| {
608                let names = language
609                    .table()
610                    .iter()
611                    .filter(|(_, class)| Some(*class) == named)
612                    .map(|(name, _)| *name)
613                    .collect();
614                (*language, names)
615            })
616            .collect()
617    }
618}
619
620impl fmt::Display for Instrument {
621    /// music21's `str` of an instrument, less the part id a score gives it:
622    /// the part's name where it differs from the instrument's, then the
623    /// instrument's name.
624    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625        if let Some(part) = &self.part_name
626            && self.name.as_ref() != Some(part)
627        {
628            write!(f, "{part}: ")?;
629        }
630        if let Some(name) = &self.name {
631            f.write_str(name)?;
632        }
633        Ok(())
634    }
635}
636
637/// Every run of consecutive words in `text`, shortest first and left to
638/// right within a length: music21's `_combinations`.
639fn word_runs(text: &str) -> Vec<String> {
640    let words: Vec<&str> = text.split_whitespace().collect();
641    let mut runs = Vec::new();
642    for size in 1..=words.len() {
643        for start in 0..=words.len() - size {
644            runs.push(words[start..start + size].join(" "));
645        }
646    }
647    runs
648}
649
650/// What an ensemble of so many players is called, from `"no performers"` to
651/// `"centet"`: music21's `ensembleNameBySize`, which is `None` past a hundred.
652pub fn ensemble_name_by_size(players: usize) -> Option<&'static str> {
653    tables::ENSEMBLE_NAMES.get(players).copied()
654}
655
656/// The instrument a key of the General MIDI percussion map plays, played
657/// the way that key says: music21's
658/// `midi.percussion.PercussionMapper.midiPitchToInstrument`.
659///
660/// A key is 0 to 127 and the map runs from 35 to 81; a key outside it, or
661/// one of the eleven inside it music21 has no instrument for, is an error,
662/// as it raises there. Where the instrument is played more than one way the
663/// modifier is the one that key plays, so key 36 is a bass drum played
664/// `"1"` and key 35 the same drum played `"acoustic"`.
665///
666/// ```
667/// use music21_rs::instrument::for_percussion_key;
668///
669/// let drum = for_percussion_key(36)?;
670/// assert_eq!(drum.kind(), "BassDrum");
671/// assert_eq!(drum.modifier(), Some("1"));
672/// assert_eq!(drum.percussion_pitch(), Some(36));
673/// assert!(for_percussion_key(69).is_err());
674/// # Ok::<(), music21_rs::Error>(())
675/// ```
676///
677/// # Errors
678///
679/// A key the map does not name an instrument for.
680pub fn for_percussion_key(key: u8) -> Result<Instrument> {
681    let (_, kind) = tables::PERCUSSION_KEYS
682        .iter()
683        .find(|(named, _)| *named == key)
684        .ok_or_else(|| Error::Instrument(format!("{key} does not map to a valid instrument!")))?;
685    let mut instrument = Instrument::of_kind(kind)?;
686    // music21 writes the modifier that key plays onto the instrument it
687    // built, which moves the drum it plays with it.
688    if let Some((_, _, _, by_key)) = tables::MODIFIERS.iter().find(|(named, ..)| named == kind)
689        && let Some((_, modifier)) = by_key.iter().find(|(drum, _)| *drum == key)
690    {
691        instrument.set_modifier(modifier);
692    }
693    Ok(instrument)
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699
700    // Every expectation here was read off music21 11.0.0b9.
701
702    #[test]
703    fn an_instrument_of_no_named_kind_is_looked_up_by_its_name() {
704        let mut violin = Instrument::new();
705        violin.set_name(Some("Violin".to_string()));
706        assert_eq!(
707            violin.all_names(SearchLanguage::English),
708            [(SearchLanguage::English, vec!["violin", "violins"])]
709        );
710    }
711
712    #[test]
713    fn an_instrument_starts_out_as_its_class_does() {
714        let piano = Instrument::of_kind("Piano").unwrap();
715        assert_eq!(piano.name(), Some("Piano"));
716        assert_eq!(piano.midi_program(), Some(0));
717        assert_eq!(piano.lowest().unwrap().name_with_octave(), "A0");
718        assert!(piano.is_a("KeyboardInstrument"));
719        let saxophone = Instrument::of_kind("AltoSaxophone").unwrap();
720        assert_eq!(saxophone.transposition().unwrap().directed_name(), "M-6");
721        assert_eq!(
722            saxophone
723                .sounding(&Pitch::from_name("C5").unwrap())
724                .unwrap()
725                .name_with_octave(),
726            "Eb4"
727        );
728        assert!(Instrument::of_kind("Kazoo").is_err());
729    }
730
731    #[test]
732    fn a_score_s_name_for_an_instrument_is_read_as_music21_reads_it() {
733        let found = |text: &str| Instrument::from_name(text, SearchLanguage::All).unwrap();
734        assert_eq!(found("Contrabassoon").kind(), "Contrabassoon");
735        assert_eq!(found("Bb Piccolo Trumpet").kind(), "Trumpet");
736        let clarinet = found("Klarinette in B.");
737        assert_eq!(clarinet.kind(), "Clarinet");
738        assert_eq!(clarinet.name(), Some("Klarinette in B."));
739        assert_eq!(clarinet.transposition().unwrap().directed_name(), "M-2");
740        assert_eq!(found("Cl.").kind(), "Clarinet");
741        assert!(Instrument::from_name("kazoo concerto", SearchLanguage::All).is_err());
742        assert!(Instrument::from_name("Klarinette", SearchLanguage::French).is_err());
743    }
744
745    #[test]
746    fn a_channel_is_found_as_music21_finds_one() {
747        let mut violin = Instrument::of_kind("Violin").unwrap();
748        assert_eq!(
749            violin
750                .auto_assign_midi_channel(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 16)
751                .unwrap(),
752            12
753        );
754        let mut drum = Instrument::of_kind("Triangle").unwrap();
755        assert_eq!(drum.auto_assign_midi_channel(&[0, 1], 16).unwrap(), 9);
756        let full: Vec<u8> = (0..16).collect();
757        assert!(violin.auto_assign_midi_channel(&full, 16).is_err());
758        assert_eq!(violin.auto_assign_midi_channel(&full, 32).unwrap(), 16);
759        assert_eq!(ensemble_name_by_size(4), Some("quartet"));
760    }
761}