1mod 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45pub enum SearchLanguage {
46 All,
49 English,
51 French,
53 German,
55 Italian,
57 Russian,
59 Spanish,
61 Abbreviation,
63}
64
65impl SearchLanguage {
66 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 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 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#[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 headless: Option<bool>,
149 #[cfg_attr(feature = "serde", serde(default))]
150 pitched: Option<bool>,
151 #[cfg_attr(feature = "serde", serde(default))]
152 part_id: Option<String>,
153 #[cfg_attr(feature = "serde", serde(default))]
154 instrument_id: Option<String>,
155}
156
157impl Default for Instrument {
158 fn default() -> Self {
160 Self::of_kind("Instrument").expect("music21's base class is in the table")
161 }
162}
163
164fn kind_named(class: &str) -> Option<&'static Kind> {
165 KINDS.iter().find(|kind| kind.class == class)
166}
167
168impl Instrument {
169 pub fn new() -> Self {
171 Self::default()
172 }
173
174 pub fn of_kind(class: &str) -> Result<Self> {
177 let kind = kind_named(class)
178 .ok_or_else(|| Error::Instrument(format!("no instrument class {class}")))?;
179 let pitch = |name: Option<&str>| name.map(Pitch::from_name).transpose();
180 let flagged = |classes: &[&str]| {
183 std::iter::once(kind.class)
184 .chain(kind.parents.iter().copied())
185 .any(|class| classes.contains(&class))
186 .then_some(false)
187 };
188 Ok(Self {
189 kind: kind.class.to_string(),
190 name: kind.name.map(String::from),
191 abbreviation: kind.abbreviation.map(String::from),
192 part_name: None,
193 part_abbreviation: None,
194 sound: kind.sound.map(String::from),
195 midi_program: kind.midi_program,
196 midi_channel: kind.midi_channel,
197 lowest: pitch(kind.lowest)?,
198 highest: pitch(kind.highest)?,
199 transposition: kind.transposition.map(Interval::from_name).transpose()?,
200 percussion_map: kind.percussion_map,
201 percussion_pitch: kind.percussion_pitch,
202 string_pitches: tables::STRING_PITCHES
203 .iter()
204 .find(|(named, _)| *named == kind.class)
205 .map(|(_, names)| names.iter().map(Pitch::from_name).collect())
206 .transpose()?,
207 modifier: tables::MODIFIERS
208 .iter()
209 .find(|(named, ..)| *named == kind.class)
210 .map(|(_, modifier, ..)| modifier.to_string()),
211 headless: flagged(&tables::HEADLESS),
212 pitched: flagged(&tables::PITCHED),
213 part_id: None,
214 instrument_id: None,
215 })
216 }
217
218 pub fn part_id(&self) -> Option<&str> {
221 self.part_id.as_deref()
222 }
223
224 pub fn set_part_id(&mut self, id: Option<String>) {
226 self.part_id = id;
227 }
228
229 pub fn instrument_id(&self) -> Option<&str> {
232 self.instrument_id.as_deref()
233 }
234
235 pub fn set_instrument_id(&mut self, id: Option<String>) {
237 self.instrument_id = id;
238 }
239
240 pub fn kinds() -> impl Iterator<Item = &'static str> {
242 KINDS.iter().map(|kind| kind.class)
243 }
244
245 pub fn from_midi_program(program: u8) -> Result<Self> {
249 let class = tables::MIDI_PROGRAMS
250 .iter()
251 .find(|(number, _)| *number == program)
252 .map(|(_, class)| *class)
253 .ok_or_else(|| {
254 Error::Instrument(format!("No instrument found for MIDI program {program}"))
255 })?;
256 let mut made = Self::of_kind(class)?;
257 made.midi_program = Some(program);
258 Ok(made)
259 }
260
261 pub fn from_name(text: &str, language: SearchLanguage) -> Result<Self> {
272 let table = language.table();
273 let cleaned: String = text
274 .replace('.', " ")
275 .to_lowercase()
276 .chars()
277 .filter(|c| !c.is_ascii_punctuation())
278 .collect();
279 let runs = word_runs(&cleaned);
280
281 let mut best: Option<(Self, String)> = None;
282 for run in &runs {
283 let Some((_, class)) = table.iter().find(|(name, _)| name == run) else {
284 continue;
285 };
286 let candidate = Self::of_kind(class)?;
287 let Some(candidate_name) = candidate.best_name().map(str::to_lowercase) else {
288 continue;
289 };
290 let replaces = match &best {
291 None => true,
292 Some((found, found_name)) => {
293 candidate_name.split_whitespace().count()
294 >= found_name.split_whitespace().count()
295 && !found.is_a(class)
296 }
297 };
298 if replaces {
299 let mut named = candidate;
300 named.name = Some(text.to_string());
301 best = Some((named, candidate_name));
302 }
303 }
304 let Some((mut found, found_name)) = best else {
305 return Err(Error::Instrument(format!(
306 "Could not match string with instrument: {text}"
307 )));
308 };
309 let Some((_, keys)) = tables::TRANSPOSITIONS
310 .iter()
311 .find(|(name, _)| *name == found_name)
312 else {
313 return Ok(found);
314 };
315 for run in &runs {
316 let Some((_, pitch)) = tables::PITCH_NAMES.iter().find(|(name, _)| name == run) else {
317 continue;
318 };
319 if let Some((_, interval)) = keys.iter().find(|(key, _)| key == pitch) {
320 found.transposition = Some(Interval::from_name(interval)?);
321 break;
322 }
323 }
324 Ok(found)
325 }
326
327 pub fn kind(&self) -> &str {
329 &self.kind
330 }
331
332 pub fn class_name(&self) -> Option<&'static str> {
336 kind_named(&self.kind).map(|kind| kind.class)
337 }
338
339 pub fn families(&self) -> &'static [&'static str] {
342 kind_named(&self.kind).map_or(&[], |kind| kind.parents)
343 }
344
345 pub fn is_a(&self, class: &str) -> bool {
348 self.kind == class || self.families().contains(&class)
349 }
350
351 pub fn name(&self) -> Option<&str> {
353 self.name.as_deref()
354 }
355
356 pub fn set_name(&mut self, name: Option<String>) {
358 self.name = name;
359 }
360
361 pub fn abbreviation(&self) -> Option<&str> {
363 self.abbreviation.as_deref()
364 }
365
366 pub fn set_abbreviation(&mut self, abbreviation: Option<String>) {
368 self.abbreviation = abbreviation;
369 }
370
371 pub fn part_name(&self) -> Option<&str> {
374 self.part_name.as_deref()
375 }
376
377 pub fn set_part_name(&mut self, name: Option<String>) {
379 self.part_name = name;
380 }
381
382 pub fn part_abbreviation(&self) -> Option<&str> {
384 self.part_abbreviation.as_deref()
385 }
386
387 pub fn set_part_abbreviation(&mut self, abbreviation: Option<String>) {
389 self.part_abbreviation = abbreviation;
390 }
391
392 pub fn best_name(&self) -> Option<&str> {
396 self.part_name
397 .as_deref()
398 .or(self.part_abbreviation.as_deref())
399 .or(self.name.as_deref())
400 .or(self.abbreviation.as_deref())
401 }
402
403 pub fn sound(&self) -> Option<&str> {
406 self.sound.as_deref()
407 }
408
409 pub fn set_sound(&mut self, sound: Option<String>) {
411 self.sound = sound;
412 }
413
414 pub fn midi_program(&self) -> Option<u8> {
416 self.midi_program
417 }
418
419 pub fn set_midi_program(&mut self, program: Option<u8>) {
421 self.midi_program = program;
422 }
423
424 pub fn midi_channel(&self) -> Option<u8> {
427 self.midi_channel
428 }
429
430 pub fn set_midi_channel(&mut self, channel: Option<u8>) {
432 self.midi_channel = channel;
433 }
434
435 pub fn lowest(&self) -> Option<&Pitch> {
438 self.lowest.as_ref()
439 }
440
441 pub fn set_lowest(&mut self, pitch: Option<Pitch>) {
443 self.lowest = pitch;
444 }
445
446 pub fn highest(&self) -> Option<&Pitch> {
448 self.highest.as_ref()
449 }
450
451 pub fn set_highest(&mut self, pitch: Option<Pitch>) {
453 self.highest = pitch;
454 }
455
456 pub fn transposition(&self) -> Option<&Interval> {
460 self.transposition.as_ref()
461 }
462
463 pub fn set_transposition(&mut self, interval: Option<Interval>) {
465 self.transposition = interval;
466 }
467
468 pub fn sounding(&self, written: &Pitch) -> Result<Pitch> {
471 match &self.transposition {
472 Some(interval) => interval.transpose_pitch(written),
473 None => Ok(written.clone()),
474 }
475 }
476
477 pub fn written(&self, sounding: &Pitch) -> Result<Pitch> {
479 match &self.transposition {
480 Some(interval) => interval.reversed()?.transpose_pitch(sounding),
481 None => Ok(sounding.clone()),
482 }
483 }
484
485 pub fn in_percussion_map(&self) -> bool {
488 self.percussion_map
489 }
490
491 pub fn percussion_pitch(&self) -> Option<u8> {
494 self.percussion_pitch
495 }
496
497 pub fn set_percussion_pitch(&mut self, pitch: Option<u8>) {
499 self.percussion_pitch = pitch;
500 }
501
502 #[must_use]
509 pub fn muted(&self) -> Option<bool> {
510 let (muted, _, _) = Self::muting(&self.kind)?;
511 Some(self.percussion_pitch == Some(muted))
512 }
513
514 pub fn set_muted(&mut self, muted: bool) {
520 let Some((muted_key, open_key, only)) = Self::muting(&self.kind) else {
521 return;
522 };
523 if only.is_some_and(|modifier| self.modifier() != Some(modifier)) {
524 return;
525 }
526 self.percussion_pitch = Some(if muted { muted_key } else { open_key });
527 }
528
529 #[must_use]
538 pub fn headless(&self) -> Option<bool> {
539 self.headless
540 }
541
542 pub fn set_headless(&mut self, headless: bool) {
546 if self.headless.is_some() {
547 self.headless = Some(headless);
548 }
549 }
550
551 #[must_use]
555 pub fn pitched(&self) -> Option<bool> {
556 self.pitched
557 }
558
559 pub fn set_pitched(&mut self, pitched: bool) {
561 if self.pitched.is_some() {
562 self.pitched = Some(pitched);
563 }
564 }
565
566 fn muting(kind: &str) -> Option<(u8, u8, Option<&'static str>)> {
569 tables::MUTED
570 .iter()
571 .find(|(class, _, _, _)| *class == kind)
572 .map(|(_, muted, open, only)| (*muted, *open, *only))
573 }
574
575 pub fn set_in_percussion_map(&mut self, in_map: bool) {
577 self.percussion_map = in_map;
578 }
579
580 pub fn string_pitches(&self) -> Option<&[Pitch]> {
597 self.string_pitches.as_deref()
598 }
599
600 pub fn set_string_pitches(&mut self, pitches: Vec<Pitch>) {
602 self.string_pitches = Some(pitches);
603 }
604
605 pub fn modifier(&self) -> Option<&str> {
608 self.modifier.as_deref()
609 }
610
611 pub fn set_modifier(&mut self, modifier: &str) {
626 let mut modifier = modifier.trim().to_lowercase();
627 let table = std::iter::once(self.kind.as_str())
628 .chain(self.families().iter().copied())
629 .find_map(|class| tables::MODIFIERS.iter().find(|(named, ..)| *named == class));
630 if self.percussion_map
631 && let Some((_, _, to_pitch, to_name)) = table
632 && let Some((_, pitch)) = to_pitch.iter().find(|(named, _)| *named == modifier)
633 {
634 self.percussion_pitch = Some(*pitch);
635 if let Some((_, named)) = to_name.iter().find(|(drum, _)| drum == pitch) {
636 modifier = (*named).to_string();
637 }
638 }
639 self.modifier = Some(modifier);
640 }
641
642 pub fn auto_assign_midi_channel(&mut self, used: &[u8], max_channels: u8) -> Result<u8> {
650 let used: std::collections::BTreeSet<u8> = used.iter().copied().collect();
651 if self.is_a("UnpitchedPercussion") && !used.contains(&9) {
652 self.midi_channel = Some(9);
653 return Ok(9);
654 }
655 if used.is_empty() {
656 self.midi_channel = Some(0);
657 return Ok(0);
658 }
659 if used.len() >= usize::from(max_channels).saturating_sub(1) {
660 return Err(Error::Instrument(
661 "we are out of midi channels! help!".to_string(),
662 ));
663 }
664 for channel in 0..max_channels {
665 if used.contains(&channel) || channel % 16 == 9 {
666 continue;
667 }
668 self.midi_channel = Some(channel);
669 return Ok(channel);
670 }
671 Ok(0)
672 }
673
674 pub fn all_names(&self, language: SearchLanguage) -> Vec<(SearchLanguage, Vec<&'static str>)> {
682 let named = std::iter::once(self.kind.as_str())
685 .chain(self.families().iter().copied())
686 .find(|class| tables::ALL.iter().any(|(_, named)| named == class))
687 .or(self.name.as_deref());
688 let languages: &[SearchLanguage] = match language {
689 SearchLanguage::All => &SearchLanguage::EACH,
690 _ => std::slice::from_ref(&language),
691 };
692 languages
693 .iter()
694 .map(|language| {
695 let names = language
696 .table()
697 .iter()
698 .filter(|(_, class)| Some(*class) == named)
699 .map(|(name, _)| *name)
700 .collect();
701 (*language, names)
702 })
703 .collect()
704 }
705}
706
707impl fmt::Display for Instrument {
708 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
712 if let Some(part) = &self.part_name
713 && self.name.as_ref() != Some(part)
714 {
715 write!(f, "{part}: ")?;
716 }
717 if let Some(name) = &self.name {
718 f.write_str(name)?;
719 }
720 Ok(())
721 }
722}
723
724fn word_runs(text: &str) -> Vec<String> {
727 let words: Vec<&str> = text.split_whitespace().collect();
728 let mut runs = Vec::new();
729 for size in 1..=words.len() {
730 for start in 0..=words.len() - size {
731 runs.push(words[start..start + size].join(" "));
732 }
733 }
734 runs
735}
736
737pub fn ensemble_name_by_size(players: usize) -> Option<&'static str> {
740 tables::ENSEMBLE_NAMES.get(players).copied()
741}
742
743pub fn for_percussion_key(key: u8) -> Result<Instrument> {
785 percussion_key_instrument(key)
786 .ok_or_else(|| Error::Instrument(format!("{key} does not map to a valid instrument!")))?
787}
788
789pub fn for_percussion_key_or_unmapped(key: u8) -> Instrument {
803 percussion_key_instrument(key)
804 .unwrap_or_else(|| {
805 Instrument::of_kind("UnpitchedPercussion").map(|mut bare| {
806 bare.set_percussion_pitch(Some(key));
807 bare
808 })
809 })
810 .unwrap_or_default()
811}
812
813pub fn percussion_keys() -> impl Iterator<Item = u8> {
818 tables::PERCUSSION_KEYS.iter().map(|(key, _)| *key)
819}
820
821pub fn named_percussion_sounds() -> impl Iterator<Item = (u8, &'static str)> {
825 tables::NAMED_SOUNDS.iter().copied()
826}
827
828fn percussion_key_instrument(key: u8) -> Option<Result<Instrument>> {
832 if let Some((_, kind)) = tables::PERCUSSION_KEYS
833 .iter()
834 .find(|(named, _)| *named == key)
835 {
836 return Some(Instrument::of_kind(kind).map(|mut made| {
837 if let Some((_, _, _, by_key)) =
842 tables::MODIFIERS.iter().find(|(named, ..)| named == kind)
843 && let Some((_, modifier)) = by_key.iter().find(|(drum, _)| *drum == key)
844 {
845 made.set_modifier(modifier);
846 }
847 made.set_percussion_pitch(Some(key));
848 made
849 }));
850 }
851 let (_, sound) = tables::NAMED_SOUNDS
852 .iter()
853 .find(|(named, _)| *named == key)?;
854 Some(Instrument::of_kind("UnpitchedPercussion").map(|mut made| {
855 made.set_name(Some((*sound).to_string()));
856 made.set_percussion_pitch(Some(key));
857 made
858 }))
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 #[test]
868 fn an_instrument_of_no_named_kind_is_looked_up_by_its_name() {
869 let mut violin = Instrument::new();
870 violin.set_name(Some("Violin".to_string()));
871 assert_eq!(
872 violin.all_names(SearchLanguage::English),
873 [(SearchLanguage::English, vec!["violin", "violins"])]
874 );
875 }
876
877 #[test]
878 fn an_instrument_starts_out_as_its_class_does() {
879 let piano = Instrument::of_kind("Piano").unwrap();
880 assert_eq!(piano.name(), Some("Piano"));
881 assert_eq!(piano.midi_program(), Some(0));
882 assert_eq!(piano.lowest().unwrap().name_with_octave(), "A0");
883 assert!(piano.is_a("KeyboardInstrument"));
884 let saxophone = Instrument::of_kind("AltoSaxophone").unwrap();
885 assert_eq!(saxophone.transposition().unwrap().directed_name(), "M-6");
886 assert_eq!(
887 saxophone
888 .sounding(&Pitch::from_name("C5").unwrap())
889 .unwrap()
890 .name_with_octave(),
891 "Eb4"
892 );
893 assert!(Instrument::of_kind("Kazoo").is_err());
894 }
895
896 #[test]
897 fn a_score_s_name_for_an_instrument_is_read_as_music21_reads_it() {
898 let found = |text: &str| Instrument::from_name(text, SearchLanguage::All).unwrap();
899 assert_eq!(found("Contrabassoon").kind(), "Contrabassoon");
900 assert_eq!(found("Bb Piccolo Trumpet").kind(), "Trumpet");
901 let clarinet = found("Klarinette in B.");
902 assert_eq!(clarinet.kind(), "Clarinet");
903 assert_eq!(clarinet.name(), Some("Klarinette in B."));
904 assert_eq!(clarinet.transposition().unwrap().directed_name(), "M-2");
905 assert_eq!(found("Cl.").kind(), "Clarinet");
906 assert!(Instrument::from_name("kazoo concerto", SearchLanguage::All).is_err());
907 assert!(Instrument::from_name("Klarinette", SearchLanguage::French).is_err());
908 }
909
910 #[test]
911 fn a_channel_is_found_as_music21_finds_one() {
912 let mut violin = Instrument::of_kind("Violin").unwrap();
913 assert_eq!(
914 violin
915 .auto_assign_midi_channel(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 16)
916 .unwrap(),
917 12
918 );
919 let mut drum = Instrument::of_kind("Triangle").unwrap();
920 assert_eq!(drum.auto_assign_midi_channel(&[0, 1], 16).unwrap(), 9);
921 let full: Vec<u8> = (0..16).collect();
922 assert!(violin.auto_assign_midi_channel(&full, 16).is_err());
923 assert_eq!(violin.auto_assign_midi_channel(&full, 32).unwrap(), 16);
924 assert_eq!(ensemble_name_by_size(4), Some("quartet"));
925 }
926
927 #[test]
928 fn a_percussion_key_is_the_drum_that_key_strikes_and_not_the_class_s_own() {
929 for (key, kind, modifier, muted) in [
935 (62u8, "CongaDrum", Some("high"), Some(true)),
936 (63, "CongaDrum", Some("high"), Some(false)),
937 (64, "CongaDrum", Some("low"), Some(false)),
938 (78, "Cuica", None, Some(true)),
939 (79, "Cuica", None, Some(false)),
940 (86, "Surdo", None, Some(true)),
941 (87, "Surdo", None, Some(false)),
942 (35, "BassDrum", Some("acoustic"), None),
943 (36, "BassDrum", Some("midi-1"), None),
944 ] {
945 let drum = for_percussion_key(key).expect("the map names a drum for it");
946 assert_eq!(
947 (
948 drum.kind(),
949 drum.modifier(),
950 drum.percussion_pitch(),
951 drum.muted()
952 ),
953 (kind, modifier, Some(key), muted),
954 "key {key}"
955 );
956 }
957 }
958
959 #[test]
960 fn a_sound_the_map_names_no_class_for_is_named_percussion() {
961 for (key, name) in [(27u8, "High Q"), (34, "Metronome Bell"), (39, "Hand Clap")] {
965 let sound = for_percussion_key(key).expect("the map names the sound");
966 assert_eq!(sound.kind(), "UnpitchedPercussion");
967 assert_eq!(sound.name(), Some(name));
968 assert_eq!(sound.percussion_pitch(), Some(key));
969 assert_eq!(sound.modifier(), None);
970 }
971 assert!(for_percussion_key(22).is_err());
975 let bare = for_percussion_key_or_unmapped(22);
976 assert_eq!(bare.kind(), "UnpitchedPercussion");
977 assert_eq!(bare.name(), Some("Percussion"));
978 assert_eq!(bare.percussion_pitch(), Some(22));
979 }
980
981 #[test]
982 fn two_instrument_classes_carry_a_flag_of_their_own() {
983 let tambourine = Instrument::of_kind("Tambourine").unwrap();
986 assert_eq!(tambourine.headless(), Some(false));
987 assert_eq!(tambourine.pitched(), None);
988 let whistle = Instrument::of_kind("Whistle").unwrap();
989 assert_eq!(whistle.pitched(), Some(false));
990 assert_eq!(whistle.headless(), None);
991 let mut piano = Instrument::of_kind("Piano").unwrap();
992 assert_eq!(piano.headless(), None);
993 piano.set_headless(true);
994 assert_eq!(piano.headless(), None);
995 }
996}