Skip to main content

music21_rs/note/
mod.rs

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