Skip to main content

music21/pitch/
names.rs

1//! What a pitch is called: its German, Italian, French and Spanish names,
2//! its full name, and its name in Unicode.
3
4use super::*;
5
6impl Pitch {
7    /// Returns the German name, where `B` is `H`, `B-` is `B`, sharps add
8    /// `is` and flats add `es` or `s`. Errors on a microtonal accidental.
9    pub fn german(&self) -> Result<String> {
10        let alter = self.whole_alteration("german")?;
11        let mut step = self.step.as_char().to_string();
12        let mut alter = alter;
13        if self.step == StepName::B {
14            if alter == -1 {
15                alter = 0;
16            } else {
17                step = "H".to_string();
18            }
19        }
20        Ok(match alter {
21            0 => step,
22            alter if alter > 0 => step + &"is".repeat(alter as usize),
23            alter => {
24                let first = if matches!(step.as_str(), "C" | "D" | "F" | "G" | "H") {
25                    "es"
26                } else {
27                    "s"
28                };
29                step + first + &"es".repeat(alter.unsigned_abs() as usize - 1)
30            }
31        })
32    }
33
34    /// Returns the Italian solfège name, such as `"do diesis"` or
35    /// `"si doppio bemolle"`. Errors on a microtonal accidental or more than
36    /// four sharps or flats.
37    pub fn italian(&self) -> Result<String> {
38        let alter = self.whole_alteration("italian")?;
39        let solfege = match self.step {
40            StepName::C => "do",
41            StepName::D => "re",
42            StepName::E => "mi",
43            StepName::F => "fa",
44            StepName::G => "sol",
45            StepName::A => "la",
46            StepName::B => "si",
47        };
48        let cardinality = match alter.unsigned_abs() {
49            0 => return Ok(solfege.to_string()),
50            1 => " ",
51            2 => " doppio ",
52            3 => " triplo ",
53            4 => " quadruplo ",
54            _ => {
55                return Err(Error::Pitch(format!(
56                    "entirely too many accidentals for an Italian name: {self}"
57                )));
58            }
59        };
60        let kind = if alter > 0 { "diesis" } else { "bemolle" };
61        Ok(format!("{solfege}{cardinality}{kind}"))
62    }
63
64    /// Returns the French solfège name, such as `"ré bémol"` or
65    /// `"fa double dièse"`. Errors on a microtonal accidental or more than
66    /// four sharps or flats.
67    pub fn french(&self) -> Result<String> {
68        let alter = self.whole_alteration("french")?;
69        let solfege = match self.step {
70            StepName::D => "ré",
71            other => romance_solfege(other),
72        };
73        let multiplier = match alter.unsigned_abs() {
74            0 => return Ok(solfege.to_string()),
75            1 => "",
76            2 => " double",
77            3 => " triple",
78            4 => " quadruple",
79            _ => {
80                return Err(Error::Pitch(format!(
81                    "entirely too many accidentals for a French name: {self}"
82                )));
83            }
84        };
85        let kind = if alter > 0 { "dièse" } else { "bémol" };
86        Ok(format!("{solfege}{multiplier} {kind}"))
87    }
88
89    /// Returns the Spanish solfège name, such as `"re bemol"` or
90    /// `"fa doble sostenido"`. Errors on a microtonal accidental or more than
91    /// four sharps or flats.
92    pub fn spanish(&self) -> Result<String> {
93        let alter = self.whole_alteration("spanish")?;
94        let solfege = romance_solfege(self.step);
95        let multiplier = match alter.unsigned_abs() {
96            0 => return Ok(solfege.to_string()),
97            1 => "",
98            2 => " doble",
99            3 => " triple",
100            4 => " cuádruple",
101            _ => {
102                return Err(Error::Pitch(format!(
103                    "entirely too many accidentals for a Spanish name: {self}"
104                )));
105            }
106        };
107        let kind = if alter > 0 { "sostenido" } else { "bemol" };
108        Ok(format!("{solfege}{multiplier} {kind}"))
109    }
110
111    /// Returns the name with the accidental as a Unicode symbol, such as
112    /// `"C♯"` or `"G𝄫"`.
113    pub fn unicode_name(&self) -> String {
114        if self.accidental().alter() == 0.0 {
115            return self.step.as_char().to_string();
116        }
117        format!("{}{}", self.step.as_char(), self.accidental().unicode())
118    }
119
120    /// Returns music21's `fullName`: the step, the accidental's full name, the
121    /// octave and any microtone, as in `E-flat in octave 4 (+20c)`.
122    pub fn full_name(&self) -> String {
123        let mut name = self.step.as_char().to_string();
124        // music21 asks whether the pitch carries an accidental object, not
125        // whether that accidental alters anything: a written natural is
126        // named, and a bare `C` — which carries none — is not.
127        if let Some(accidental) = &self.accidental {
128            name.push('-');
129            name.push_str(accidental.full_name());
130        }
131        if let Some(octave) = self.octave {
132            name.push_str(&format!(" in octave {octave}"));
133        }
134        if let Some(microtone) = &self.microtone
135            && microtone.cents() != 0.0
136        {
137            name.push(' ');
138            name.push_str(&microtone.to_string());
139        }
140        name
141    }
142
143    /// Returns the name with its octave and, when it has one that is not
144    /// zero, its microtone: music21's `str(Pitch)`, `A4(+20c)`.
145    pub fn name_with_octave_and_microtone(&self) -> String {
146        match &self.microtone {
147            Some(microtone) if microtone.cents() != 0.0 => {
148                format!("{}{microtone}", self.name_with_octave())
149            }
150            _ => self.name_with_octave(),
151        }
152    }
153
154    /// Returns [`Self::unicode_name`] followed by the octave when one is set.
155    pub fn unicode_name_with_octave(&self) -> String {
156        match self.octave {
157            Some(octave) => format!("{}{octave}", self.unicode_name()),
158            None => self.unicode_name(),
159        }
160    }
161
162    pub(super) fn whole_alteration(&self, language: &str) -> Result<IntegerType> {
163        let alter = self.accidental().alter();
164        if alter.fract() != 0.0 {
165            return Err(Error::Pitch(match language {
166                "german" => {
167                    "Es geht nicht \"german\" zu benutzen mit Microtönen.  Schade!".to_string()
168                }
169                "italian" => "Non si puo usare `italian` con microtoni".to_string(),
170                "french" => {
171                    "On ne peut pas utiliser les microtones avec \"french.\" Quelle Dommage!"
172                        .to_string()
173                }
174                "spanish" => "Unsupported accidental type.".to_string(),
175                other => {
176                    format!("{other} names cannot express the microtonal accidental of {self}")
177                }
178            }));
179        }
180        Ok(alter as IntegerType)
181    }
182}
183
184/// Canonical pitch names for chromatic pitch classes.
185pub const CHROMATIC_PITCH_CLASS_NAMES: [&str; 12] = [
186    "C", "Db", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B",
187];
188
189/// Returns a canonical pitch name for a chromatic pitch class.
190pub fn pitch_class_name(pitch_class: u8) -> &'static str {
191    CHROMATIC_PITCH_CLASS_NAMES[pitch_class as usize % 12]
192}
193
194pub(super) fn romance_solfege(step: StepName) -> &'static str {
195    match step {
196        StepName::C => "do",
197        StepName::D => "re",
198        StepName::E => "mi",
199        StepName::F => "fa",
200        StepName::G => "sol",
201        StepName::A => "la",
202        StepName::B => "si",
203    }
204}
205
206/// One of the languages a pitch has a name in: music21's
207/// `naturalLanguageObjects.SUPPORTED_LANGUAGES`.
208#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub enum Language {
211    /// German, where `B` is `H` and `B-` is `B`.
212    German,
213    /// French, whose names are solfège.
214    French,
215    /// Italian, whose names are solfège.
216    Italian,
217    /// Spanish, whose names are solfège.
218    Spanish,
219}
220
221impl Language {
222    /// Every one of them, in music21's order.
223    pub const ALL: [Language; 4] = [Self::German, Self::French, Self::Italian, Self::Spanish];
224
225    /// The language music21 names with this code -- `de`, `fr`, `it`, `es`
226    /// -- or nothing for a code it does not support.
227    #[must_use]
228    pub fn from_code(code: &str) -> Option<Self> {
229        match code {
230            "de" => Some(Self::German),
231            "fr" => Some(Self::French),
232            "it" => Some(Self::Italian),
233            "es" => Some(Self::Spanish),
234            _ => None,
235        }
236    }
237
238    /// music21's code for it.
239    #[must_use]
240    pub fn code(self) -> &'static str {
241        match self {
242            Self::German => "de",
243            Self::French => "fr",
244            Self::Italian => "it",
245            Self::Spanish => "es",
246        }
247    }
248
249    /// What a pitch is called in this language.
250    fn name_of(self, pitch: &Pitch) -> Result<String> {
251        match self {
252            Self::German => pitch.german(),
253            Self::French => pitch.french(),
254            Self::Italian => pitch.italian(),
255            Self::Spanish => pitch.spanish(),
256        }
257    }
258}
259
260/// Every name a pitch has in `language`, each with the name this crate
261/// spells it by: music21's `generateLanguageDictionary`.
262///
263/// The names are read off the pitches themselves, as music21 reads them --
264/// the seven letters, each with up to four flats and four sharps, written
265/// in the language and paired with what was written -- so there is one
266/// table and not two. A name two pitches share is the later of them, which
267/// is what music21's dictionary keeps.
268///
269/// ```
270/// use music21::pitch::{Language, language_dictionary};
271///
272/// let german = language_dictionary(Language::German);
273/// assert_eq!(german.len(), 63);
274/// assert!(german.contains(&("H".to_string(), "B".to_string())));
275/// assert!(german.contains(&("Es".to_string(), "E-".to_string())));
276/// ```
277#[must_use]
278pub fn language_dictionary(language: Language) -> Vec<(String, String)> {
279    let mut named: Vec<(String, String)> = Vec::new();
280    for step in ['A', 'B', 'C', 'D', 'E', 'F', 'G'] {
281        for accidental in ["----", "---", "--", "-", "", "#", "##", "###", "####"] {
282            let spelled = format!("{step}{accidental}");
283            let Ok(pitch) = Pitch::from_name(&spelled) else {
284                continue;
285            };
286            let Ok(name) = language.name_of(&pitch) else {
287                continue;
288            };
289            match named.iter_mut().find(|(known, _)| *known == name) {
290                // A name two pitches share is the later of them, as
291                // music21's dictionary is written over.
292                Some((_, kept)) => *kept = spelled,
293                None => named.push((name, spelled)),
294            }
295        }
296    }
297    named
298}
299
300/// The pitch a name in another language means, or a C natural where that
301/// language calls no pitch by that name: music21's `toPitch`.
302///
303/// ```
304/// use music21::pitch::{Language, from_language_name};
305///
306/// assert_eq!(from_language_name("Es", Language::German).name(), "Eb");
307/// assert_eq!(from_language_name("H", Language::German).name(), "B");
308/// // A name the language does not use is a C, as music21 answers.
309/// assert_eq!(from_language_name("Es", Language::Italian).name(), "C");
310/// ```
311pub fn from_language_name(name: &str, language: Language) -> Pitch {
312    language_dictionary(language)
313        .into_iter()
314        .find(|(known, _)| known == name)
315        .and_then(|(_, spelled)| Pitch::from_name(&spelled).ok())
316        .unwrap_or_else(|| Pitch::from_name("C").expect("a C natural is a pitch"))
317}