Skip to main content

music21_rs/note/
mod.rs

1pub(crate) mod generalnote;
2pub(crate) mod notrest;
3
4use crate::defaults::IntegerType;
5use crate::duration::Duration;
6use crate::error::Result;
7use crate::pitch::Pitch;
8
9use generalnote::GeneralNoteTrait;
10use notrest::NotRest;
11use std::collections::HashMap;
12use std::fmt::{Display, Formatter};
13use std::str::FromStr;
14use std::sync::{Arc, Mutex};
15
16#[derive(Clone, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18/// A pitched note.
19pub struct Note {
20    notrest: NotRest,
21    pub(crate) _pitch: Pitch,
22    #[cfg_attr(feature = "serde", serde(skip))]
23    _cache: Arc<Mutex<HashMap<String, String>>>,
24}
25
26impl Note {
27    /// Builds a note from a pitch name such as `"C#4"` or `"E-"`.
28    pub fn from_name(name: impl Into<String>) -> Result<Self> {
29        Self::new(Option::<Pitch>::None, None, None, Some(name.into()))
30    }
31
32    /// Builds a note from an existing [`Pitch`].
33    pub fn from_pitch(pitch: Pitch) -> Result<Self> {
34        Self::new(Some(pitch), None, None, None)
35    }
36
37    /// Returns the note's pitch.
38    pub fn pitch(&self) -> &Pitch {
39        &self._pitch
40    }
41
42    /// Returns the pitch name without an octave, such as `"C#"` or `"E-"`.
43    pub fn pitch_name(&self) -> String {
44        self._pitch.name()
45    }
46
47    /// Returns the pitch name with an octave when one is set.
48    pub fn pitch_name_with_octave(&self) -> String {
49        self._pitch.name_with_octave()
50    }
51
52    /// Returns the note duration when one has been assigned.
53    pub fn duration(&self) -> Option<&Duration> {
54        self.notrest.duration().as_ref()
55    }
56
57    /// Assigns a duration to the note.
58    pub fn set_duration(&mut self, duration: Duration) {
59        self.notrest.set_duration(&duration);
60    }
61
62    /// Returns a copy of this note with the supplied duration.
63    pub fn with_duration(mut self, duration: Duration) -> Self {
64        self.set_duration(duration);
65        self
66    }
67
68    pub(crate) fn new<T>(
69        pitch: Option<T>,
70        duration: Option<Duration>,
71        name: Option<String>,
72        name_with_octave: Option<String>,
73    ) -> Result<Self>
74    where
75        T: IntoPitch,
76    {
77        let _pitch = match pitch {
78            Some(pitch) => pitch.into_pitch(),
79            None => Ok({
80                let name = match name_with_octave {
81                    Some(name_with_octave) => name_with_octave,
82                    None => match name {
83                        Some(name) => name,
84                        None => "C4".to_string(),
85                    },
86                };
87
88                Pitch::new(
89                    Some(name),
90                    None,
91                    None,
92                    Option::<IntegerType>::None,
93                    Option::<IntegerType>::None,
94                    None,
95                    None,
96                    None,
97                    None,
98                )?
99            }),
100        }?;
101
102        Ok(Self {
103            notrest: NotRest::new(duration),
104            _pitch,
105            _cache: Arc::new(Mutex::new(HashMap::new())),
106        })
107    }
108
109    pub(crate) fn get_super(&self) -> &NotRest {
110        &self.notrest
111    }
112
113    pub(crate) fn pitch_changed(&self) {
114        {
115            let mut cache = match self._cache.lock() {
116                Ok(cache) => cache,
117                Err(err) => err.into_inner(),
118            };
119            cache.clear();
120        }
121
122        if let Some(chord) = &self.notrest._chord_attached {
123            chord.clear_cache();
124        }
125    }
126
127    #[cfg(test)]
128    fn insert_cache_value_for_test(&self, key: &str, value: &str) {
129        let mut cache = match self._cache.lock() {
130            Ok(cache) => cache,
131            Err(err) => err.into_inner(),
132        };
133        cache.insert(key.to_string(), value.to_string());
134    }
135
136    #[cfg(test)]
137    fn cache_len_for_test(&self) -> usize {
138        let cache = match self._cache.lock() {
139            Ok(cache) => cache,
140            Err(err) => err.into_inner(),
141        };
142        cache.len()
143    }
144}
145
146impl GeneralNoteTrait for Note {
147    fn duration(&self) -> &Option<Duration> {
148        self.notrest.duration()
149    }
150
151    fn set_duration(&mut self, duration: &Duration) {
152        self.notrest.set_duration(duration);
153    }
154}
155
156impl FromStr for Note {
157    type Err = crate::error::Error;
158
159    fn from_str(value: &str) -> Result<Self> {
160        Self::from_name(value)
161    }
162}
163
164impl TryFrom<&str> for Note {
165    type Error = crate::error::Error;
166
167    fn try_from(value: &str) -> Result<Self> {
168        Self::from_name(value)
169    }
170}
171
172impl TryFrom<String> for Note {
173    type Error = crate::error::Error;
174
175    fn try_from(value: String) -> Result<Self> {
176        Self::from_name(value)
177    }
178}
179
180impl TryFrom<Pitch> for Note {
181    type Error = crate::error::Error;
182
183    fn try_from(value: Pitch) -> Result<Self> {
184        Self::from_pitch(value)
185    }
186}
187
188impl TryFrom<&Pitch> for Note {
189    type Error = crate::error::Error;
190
191    fn try_from(value: &Pitch) -> Result<Self> {
192        Self::from_pitch(value.clone())
193    }
194}
195
196impl TryFrom<IntegerType> for Note {
197    type Error = crate::error::Error;
198
199    fn try_from(value: IntegerType) -> Result<Self> {
200        Note::new(Some(value), None, None, None)
201    }
202}
203
204impl Display for Note {
205    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
206        write!(f, "{}", self.pitch_name_with_octave())
207    }
208}
209
210/// Converts a single note-like value into a [`Note`].
211///
212/// This is useful when constructing vectors or other collections that are
213/// later passed to APIs such as `Chord::new`.
214pub trait IntoNote {
215    /// Whether this value came from an integer pitch class or MIDI-like number.
216    const FROM_INTEGER_PITCH: bool = false;
217
218    /// Converts the value into a note.
219    fn try_into_note(self) -> Result<Note>;
220}
221
222impl IntoNote for Note {
223    fn try_into_note(self) -> Result<Note> {
224        Ok(self)
225    }
226}
227
228impl IntoNote for &Note {
229    fn try_into_note(self) -> Result<Note> {
230        Ok(self.clone())
231    }
232}
233
234impl IntoNote for Pitch {
235    fn try_into_note(self) -> Result<Note> {
236        Note::new(Some(self), None, None, None)
237    }
238}
239
240impl IntoNote for &Pitch {
241    fn try_into_note(self) -> Result<Note> {
242        Note::new(Some(self.clone()), None, None, None)
243    }
244}
245
246impl IntoNote for String {
247    fn try_into_note(self) -> Result<Note> {
248        Note::new(Some(self), None, None, None)
249    }
250}
251
252impl IntoNote for &String {
253    fn try_into_note(self) -> Result<Note> {
254        Note::new(Some(self.to_string()), None, None, None)
255    }
256}
257
258impl IntoNote for &str {
259    fn try_into_note(self) -> Result<Note> {
260        Note::new(Some(self), None, None, None)
261    }
262}
263
264impl IntoNote for IntegerType {
265    const FROM_INTEGER_PITCH: bool = true;
266
267    fn try_into_note(self) -> Result<Note> {
268        Note::new(Some(self), None, None, None)
269    }
270}
271
272pub(crate) trait IntoPitch {
273    fn into_pitch(self) -> Result<Pitch>;
274}
275
276impl IntoPitch for Pitch {
277    fn into_pitch(self) -> Result<Pitch> {
278        Ok(self.clone())
279    }
280}
281
282impl IntoPitch for String {
283    fn into_pitch(self) -> Result<Pitch> {
284        Pitch::new(
285            Some(self),
286            None,
287            None,
288            Option::<IntegerType>::None,
289            Option::<IntegerType>::None,
290            None,
291            None,
292            None,
293            None,
294        )
295    }
296}
297
298impl IntoPitch for &str {
299    fn into_pitch(self) -> Result<Pitch> {
300        Pitch::new(
301            Some(self),
302            None,
303            None,
304            Option::<IntegerType>::None,
305            Option::<IntegerType>::None,
306            None,
307            None,
308            None,
309            None,
310        )
311    }
312}
313
314impl IntoPitch for IntegerType {
315    fn into_pitch(self) -> Result<Pitch> {
316        Pitch::new(
317            Some(self),
318            None,
319            None,
320            Option::<IntegerType>::None,
321            Option::<IntegerType>::None,
322            None,
323            None,
324            None,
325            None,
326        )
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::{IntoNote, Note};
333    use crate::chord::chordbase::ChordBase;
334    use crate::defaults::IntegerType;
335    use crate::pitch::Pitch;
336    use std::sync::Arc;
337
338    #[test]
339    fn pitch_changed_clears_note_cache() {
340        let note = Note::new(Some("C4"), None, None, None).unwrap();
341
342        note.insert_cache_value_for_test("pitchName", "C");
343        assert_eq!(note.cache_len_for_test(), 1);
344
345        note.pitch_changed();
346
347        assert_eq!(note.cache_len_for_test(), 0);
348    }
349
350    #[test]
351    fn pitch_changed_clears_attached_chord_cache() {
352        let chord = ChordBase::new(Some("C E G"), &None).unwrap();
353        chord.insert_cache_value_for_test("analysis", "major triad");
354        assert_eq!(chord.cache_len_for_test(), 1);
355
356        let mut note = Note::new(Some("E4"), None, None, None).unwrap();
357        note.notrest._chord_attached = Some(Arc::clone(&chord));
358
359        note.pitch_changed();
360
361        assert_eq!(chord.cache_len_for_test(), 0);
362    }
363
364    #[test]
365    fn into_note_accepts_note_like_inputs() {
366        fn from_integer_pitch<T: IntoNote>() -> bool {
367            T::FROM_INTEGER_PITCH
368        }
369
370        assert!(!from_integer_pitch::<&str>());
371        assert!(from_integer_pitch::<IntegerType>());
372
373        let note = Note::from_name("C4").unwrap();
374        assert_eq!(
375            note.clone()
376                .try_into_note()
377                .unwrap()
378                .pitch_name_with_octave(),
379            "C4"
380        );
381
382        let borrowed_note = Note::from_name("D4").unwrap();
383        assert_eq!(
384            (&borrowed_note)
385                .try_into_note()
386                .unwrap()
387                .pitch_name_with_octave(),
388            "D4"
389        );
390
391        let pitch = Pitch::from_name("E4").unwrap();
392        assert_eq!(
393            pitch.try_into_note().unwrap().pitch_name_with_octave(),
394            "E4"
395        );
396
397        let borrowed_pitch = Pitch::from_name("F4").unwrap();
398        assert_eq!(
399            (&borrowed_pitch)
400                .try_into_note()
401                .unwrap()
402                .pitch_name_with_octave(),
403            "F4"
404        );
405
406        assert_eq!(
407            "G4".to_string()
408                .try_into_note()
409                .unwrap()
410                .pitch_name_with_octave(),
411            "G4"
412        );
413
414        let owned_name = "A4".to_string();
415        assert_eq!(
416            (&owned_name)
417                .try_into_note()
418                .unwrap()
419                .pitch_name_with_octave(),
420            "A4"
421        );
422
423        assert_eq!("B4".try_into_note().unwrap().pitch_name_with_octave(), "B4");
424
425        assert_eq!(
426            (60 as IntegerType)
427                .try_into_note()
428                .unwrap()
429                .pitch_name_with_octave(),
430            "C4"
431        );
432    }
433
434    #[test]
435    fn note_supports_rust_conversion_traits() {
436        let parsed: Note = "C#4".parse().unwrap();
437        assert_eq!(parsed.to_string(), "C#4");
438
439        let from_pitch = Note::try_from(Pitch::from_name("D4").unwrap()).unwrap();
440        assert_eq!(from_pitch.pitch_name_with_octave(), "D4");
441
442        let from_integer = Note::try_from(60 as IntegerType).unwrap();
443        assert_eq!(from_integer.pitch_name_with_octave(), "C4");
444    }
445}