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 part_id: Option<String>,
149 #[cfg_attr(feature = "serde", serde(default))]
150 instrument_id: Option<String>,
151}
152
153impl Default for Instrument {
154 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 pub fn new() -> Self {
167 Self::default()
168 }
169
170 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 pub fn part_id(&self) -> Option<&str> {
207 self.part_id.as_deref()
208 }
209
210 pub fn set_part_id(&mut self, id: Option<String>) {
212 self.part_id = id;
213 }
214
215 pub fn instrument_id(&self) -> Option<&str> {
218 self.instrument_id.as_deref()
219 }
220
221 pub fn set_instrument_id(&mut self, id: Option<String>) {
223 self.instrument_id = id;
224 }
225
226 pub fn kinds() -> impl Iterator<Item = &'static str> {
228 KINDS.iter().map(|kind| kind.class)
229 }
230
231 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 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 pub fn kind(&self) -> &str {
315 &self.kind
316 }
317
318 pub fn class_name(&self) -> Option<&'static str> {
322 kind_named(&self.kind).map(|kind| kind.class)
323 }
324
325 pub fn families(&self) -> &'static [&'static str] {
328 kind_named(&self.kind).map_or(&[], |kind| kind.parents)
329 }
330
331 pub fn is_a(&self, class: &str) -> bool {
334 self.kind == class || self.families().contains(&class)
335 }
336
337 pub fn name(&self) -> Option<&str> {
339 self.name.as_deref()
340 }
341
342 pub fn set_name(&mut self, name: Option<String>) {
344 self.name = name;
345 }
346
347 pub fn abbreviation(&self) -> Option<&str> {
349 self.abbreviation.as_deref()
350 }
351
352 pub fn set_abbreviation(&mut self, abbreviation: Option<String>) {
354 self.abbreviation = abbreviation;
355 }
356
357 pub fn part_name(&self) -> Option<&str> {
360 self.part_name.as_deref()
361 }
362
363 pub fn set_part_name(&mut self, name: Option<String>) {
365 self.part_name = name;
366 }
367
368 pub fn part_abbreviation(&self) -> Option<&str> {
370 self.part_abbreviation.as_deref()
371 }
372
373 pub fn set_part_abbreviation(&mut self, abbreviation: Option<String>) {
375 self.part_abbreviation = abbreviation;
376 }
377
378 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 pub fn sound(&self) -> Option<&str> {
392 self.sound.as_deref()
393 }
394
395 pub fn set_sound(&mut self, sound: Option<String>) {
397 self.sound = sound;
398 }
399
400 pub fn midi_program(&self) -> Option<u8> {
402 self.midi_program
403 }
404
405 pub fn set_midi_program(&mut self, program: Option<u8>) {
407 self.midi_program = program;
408 }
409
410 pub fn midi_channel(&self) -> Option<u8> {
413 self.midi_channel
414 }
415
416 pub fn set_midi_channel(&mut self, channel: Option<u8>) {
418 self.midi_channel = channel;
419 }
420
421 pub fn lowest(&self) -> Option<&Pitch> {
424 self.lowest.as_ref()
425 }
426
427 pub fn set_lowest(&mut self, pitch: Option<Pitch>) {
429 self.lowest = pitch;
430 }
431
432 pub fn highest(&self) -> Option<&Pitch> {
434 self.highest.as_ref()
435 }
436
437 pub fn set_highest(&mut self, pitch: Option<Pitch>) {
439 self.highest = pitch;
440 }
441
442 pub fn transposition(&self) -> Option<&Interval> {
446 self.transposition.as_ref()
447 }
448
449 pub fn set_transposition(&mut self, interval: Option<Interval>) {
451 self.transposition = interval;
452 }
453
454 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 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 pub fn in_percussion_map(&self) -> bool {
474 self.percussion_map
475 }
476
477 pub fn percussion_pitch(&self) -> Option<u8> {
480 self.percussion_pitch
481 }
482
483 pub fn set_percussion_pitch(&mut self, pitch: Option<u8>) {
485 self.percussion_pitch = pitch;
486 }
487
488 pub fn set_in_percussion_map(&mut self, in_map: bool) {
490 self.percussion_map = in_map;
491 }
492
493 pub fn string_pitches(&self) -> Option<&[Pitch]> {
510 self.string_pitches.as_deref()
511 }
512
513 pub fn set_string_pitches(&mut self, pitches: Vec<Pitch>) {
515 self.string_pitches = Some(pitches);
516 }
517
518 pub fn modifier(&self) -> Option<&str> {
521 self.modifier.as_deref()
522 }
523
524 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 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 pub fn all_names(&self, language: SearchLanguage) -> Vec<(SearchLanguage, Vec<&'static str>)> {
595 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 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
637fn 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
650pub fn ensemble_name_by_size(players: usize) -> Option<&'static str> {
653 tables::ENSEMBLE_NAMES.get(players).copied()
654}
655
656pub 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 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 #[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}