Skip to main content

music21_rs/
percussion.rs

1//! Notes with no pitch: music21's `note.Unpitched` and
2//! `percussion.PercussionChord`.
3//!
4//! A drum stroke is written on a staff line without sounding that line's
5//! pitch. What it keeps is where it is *displayed* -- a step and an octave,
6//! read as if the staff were in treble clef -- and everything a written note
7//! has besides a pitch: a length, a stem, a notehead, beams.
8
9use crate::chord::Chord;
10use crate::defaults::IntegerType;
11use crate::duration::Duration;
12use crate::error::Result;
13use crate::instrument::Instrument;
14use crate::note::Note;
15use crate::pitch::Pitch;
16
17/// A note with no pitch, as a drum stroke: music21's `note.Unpitched`.
18///
19/// ```
20/// use music21_rs::percussion::Unpitched;
21///
22/// let stroke = Unpitched::new();
23/// assert_eq!((stroke.display_step(), stroke.display_octave()), ('B', 4));
24///
25/// let low = Unpitched::from_display_name("D5")?;
26/// assert_eq!(low.display_name(), "D5");
27/// # Ok::<(), music21_rs::Error>(())
28/// ```
29#[derive(Clone, Debug)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[must_use]
32pub struct Unpitched {
33    /// A note standing where this one is displayed, carrying everything
34    /// written about it.
35    written: Note,
36}
37
38impl Default for Unpitched {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl PartialEq for Unpitched {
45    /// Where they are displayed, what plays them and how long they last,
46    /// as music21 compares them.
47    fn eq(&self, other: &Self) -> bool {
48        self.display_name() == other.display_name()
49            && self.stored_instrument() == other.stored_instrument()
50            && self.duration() == other.duration()
51    }
52}
53
54impl Unpitched {
55    /// A stroke displayed on the middle line, `B4`, which is where music21
56    /// puts one that says nothing.
57    pub fn new() -> Self {
58        Self::from_display_name("B4").expect("B4 is a pitch name")
59    }
60
61    /// A stroke displayed where this pitch would stand in treble clef. An
62    /// accidental in the name is dropped, since a display position has
63    /// none, and a name with no octave is displayed in the fourth.
64    pub fn from_display_name(name: &str) -> Result<Self> {
65        let named = Pitch::from_name(name)?;
66        Self::displayed_at(named.step().as_char(), named.implicit_octave())
67    }
68
69    /// A stroke displayed at this step and octave.
70    pub fn displayed_at(step: char, octave: IntegerType) -> Result<Self> {
71        Ok(Self {
72            written: Note::from_name(format!("{step}{octave}"))?,
73        })
74    }
75
76    /// The letter of the line or space it is displayed on: music21's
77    /// `displayStep`.
78    pub fn display_step(&self) -> char {
79        self.written.pitch().step().as_char()
80    }
81
82    /// The octave it is displayed in: music21's `displayOctave`.
83    pub fn display_octave(&self) -> IntegerType {
84        self.written.pitch().implicit_octave()
85    }
86
87    /// Where it is displayed, as a pitch, which never has an accidental:
88    /// music21's `displayPitch`.
89    pub fn display_pitch(&self) -> &Pitch {
90        self.written.pitch()
91    }
92
93    /// Where it is displayed, as a name: music21's `displayName`.
94    pub fn display_name(&self) -> String {
95        self.written.pitch().name_with_octave()
96    }
97
98    /// Moves it to another line or space.
99    pub fn set_display(&mut self, step: char, octave: IntegerType) -> Result<()> {
100        let pitch = Pitch::from_name(format!("{step}{octave}"))?;
101        self.written.set_pitch(pitch);
102        Ok(())
103    }
104
105    /// How long it lasts, where that has been said.
106    pub fn duration(&self) -> Option<&Duration> {
107        self.written.duration()
108    }
109
110    /// The same stroke lasting this long.
111    pub fn with_duration(mut self, duration: Duration) -> Self {
112        self.written.set_duration(duration);
113        self
114    }
115
116    /// The instrument that plays it, where one is kept on the stroke itself:
117    /// music21's `storedInstrument`.
118    pub fn stored_instrument(&self) -> Option<&Instrument> {
119        self.written.stored_instrument()
120    }
121
122    /// Everything written about the stroke -- its length, stem, notehead,
123    /// beams, tie, lyrics, expressions -- as a note standing where it is
124    /// displayed. The note's pitch is that position and nothing sounds it.
125    pub fn written(&self) -> &Note {
126        &self.written
127    }
128
129    /// What is written about the stroke, for editing. Changing the pitch
130    /// moves where it is displayed.
131    pub fn written_mut(&mut self) -> &mut Note {
132        &mut self.written
133    }
134}
135
136/// One member of a percussion chord: a stroke, or a note with a pitch.
137#[derive(Clone, Debug)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139pub enum PercussionNote {
140    /// A stroke with no pitch.
141    Unpitched(Unpitched),
142    /// A note with a pitch, as a timpani's or a glockenspiel's.
143    Note(Note),
144}
145
146impl From<Unpitched> for PercussionNote {
147    fn from(value: Unpitched) -> Self {
148        Self::Unpitched(value)
149    }
150}
151
152impl From<Note> for PercussionNote {
153    fn from(value: Note) -> Self {
154        Self::Note(value)
155    }
156}
157
158/// Several strokes at once, as a hi-hat over a bass drum: music21's
159/// `percussion.PercussionChord`.
160///
161/// It is not a harmony and answers none of a chord's harmonic questions;
162/// its members stay in the order given.
163///
164/// ```
165/// use music21_rs::percussion::{PercussionChord, PercussionNote, Unpitched};
166/// use music21_rs::Note;
167///
168/// let chord = PercussionChord::new(vec![
169///     Unpitched::from_display_name("E3")?.into(),
170///     Note::from_name("G4")?.into(),
171/// ])?;
172/// assert_eq!(chord.len(), 2);
173/// assert!(chord.is_unpitched(0) && !chord.is_unpitched(1));
174/// assert_eq!(chord.pitches().len(), 1);
175/// # Ok::<(), music21_rs::Error>(())
176/// ```
177#[derive(Clone, Debug)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
179#[must_use]
180pub struct PercussionChord {
181    /// The members as written notes, a stroke standing where it is
182    /// displayed, with what is written of the chord as a whole.
183    written: Chord,
184    /// Which members have no pitch.
185    unpitched: Vec<bool>,
186}
187
188impl PercussionChord {
189    /// A chord of these members, in this order.
190    pub fn new(members: Vec<PercussionNote>) -> Result<Self> {
191        let unpitched = members
192            .iter()
193            .map(|member| matches!(member, PercussionNote::Unpitched(_)))
194            .collect();
195        let notes: Vec<Note> = members
196            .into_iter()
197            .map(|member| match member {
198                PercussionNote::Unpitched(stroke) => stroke.written,
199                PercussionNote::Note(note) => note,
200            })
201            .collect();
202        Ok(Self {
203            written: Chord::new(notes)?,
204            unpitched,
205        })
206    }
207
208    /// How many members it has.
209    pub fn len(&self) -> usize {
210        self.unpitched.len()
211    }
212
213    /// Whether it has no members.
214    pub fn is_empty(&self) -> bool {
215        self.unpitched.is_empty()
216    }
217
218    /// Whether the member at this place has no pitch.
219    pub fn is_unpitched(&self, index: usize) -> bool {
220        self.unpitched.get(index).copied().unwrap_or(false)
221    }
222
223    /// The members, in order.
224    pub fn members(&self) -> Vec<PercussionNote> {
225        self.written
226            .notes()
227            .iter()
228            .zip(&self.unpitched)
229            .map(|(note, unpitched)| {
230                if *unpitched {
231                    PercussionNote::Unpitched(Unpitched {
232                        written: note.clone(),
233                    })
234                } else {
235                    PercussionNote::Note(note.clone())
236                }
237            })
238            .collect()
239    }
240
241    /// The pitches of the members that have one: music21's `pitches`.
242    pub fn pitches(&self) -> Vec<Pitch> {
243        self.written
244            .notes()
245            .iter()
246            .zip(&self.unpitched)
247            .filter(|(_, unpitched)| !**unpitched)
248            .map(|(note, _)| note.pitch().clone())
249            .collect()
250    }
251
252    /// How long it lasts, where that has been said.
253    pub fn duration(&self) -> Option<&Duration> {
254        self.written.duration()
255    }
256
257    /// The same chord lasting this long.
258    pub fn with_duration(mut self, duration: Duration) -> Self {
259        self.written.set_duration(duration);
260        self
261    }
262
263    /// Everything written about the chord as a whole -- its length, stem,
264    /// beams, expressions -- and about each member, as a chord of notes
265    /// standing where the members are displayed. It is a carrier of
266    /// notation: its harmony means nothing.
267    pub fn written(&self) -> &Chord {
268        &self.written
269    }
270
271    /// What is written about the chord, for editing. Adding or removing
272    /// notes here is not supported: build a new chord instead.
273    pub fn written_mut(&mut self) -> &mut Chord {
274        &mut self.written
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn a_stroke_is_displayed_without_an_accidental() {
284        // music21: Unpitched(displayName='D#5') keeps the step and octave.
285        let stroke = Unpitched::from_display_name("D#5").unwrap();
286        assert_eq!(stroke.display_name(), "D5");
287        assert_eq!(Unpitched::new().display_name(), "B4");
288    }
289
290    #[test]
291    fn strokes_compare_by_where_they_are_displayed() {
292        let one = Unpitched::from_display_name("B4").unwrap();
293        assert_eq!(one, Unpitched::new());
294        assert_ne!(one, Unpitched::from_display_name("A4").unwrap());
295    }
296
297    #[test]
298    fn a_percussion_chord_keeps_its_order() {
299        let chord = PercussionChord::new(vec![
300            Unpitched::from_display_name("B3").unwrap().into(),
301            Unpitched::from_display_name("E3").unwrap().into(),
302        ])
303        .unwrap();
304        let names: Vec<String> = chord
305            .written()
306            .notes()
307            .iter()
308            .map(|note| note.pitch().name_with_octave())
309            .collect();
310        assert_eq!(names, ["B3", "E3"]);
311        assert!(chord.pitches().is_empty());
312    }
313}