music21_rs/analysis/metrical.rs
1//! Metrical and melodic accent: music21's `analysis.metrical`.
2
3use crate::{
4 defaults::FloatType,
5 error::{Error, Result},
6 meter::TimeSignature,
7 notation::TieType,
8 stream::{Stream, StreamElement, StreamKind},
9};
10
11/// How many levels of the metrical hierarchy start at each note and rest of
12/// a part's measures, as positions in [`Stream::leaves`] beside the count:
13/// the depths music21's `labelBeatDepth` marks.
14///
15/// Each measure is read in the meter it states, else the one in force where
16/// it starts, else four-four, nested three levels deep, and every note,
17/// chord and rest it holds itself -- not those in its voices -- is counted
18/// where it starts. A note ending a tie is passed over.
19///
20/// # Errors
21///
22/// A note standing outside its measure's bar.
23pub fn beat_depths(part: &Stream) -> Result<Vec<(usize, u8)>> {
24 let mut depths = Vec::new();
25 let mut position = 0;
26 for event in part.events() {
27 let StreamElement::Stream(measure) = event.element() else {
28 position += 1;
29 continue;
30 };
31 let leaves = measure.leaves().len();
32 if measure.kind() != StreamKind::Measure {
33 position += leaves;
34 continue;
35 }
36 let mut meter = measure_meter(part, measure, event.offset())?;
37 meter
38 .beat_sequence_mut()
39 .subdivide_nested_hierarchy(3, None, true)?;
40 let mut inside = position;
41 for own in measure.events() {
42 match own.element() {
43 StreamElement::Stream(nested) => inside += nested.leaves().len(),
44 element => {
45 if is_note_or_rest(element) && !ends_tie(element) {
46 depths.push((inside, meter.beat_depth(own.offset())?));
47 }
48 inside += 1;
49 }
50 }
51 }
52 position += leaves;
53 }
54 Ok(depths)
55}
56
57/// Marks each note and rest of a part's measures with a lyric of a star for
58/// every level of the metrical hierarchy starting where it does: music21's
59/// `labelBeatDepth`. The depths are [`beat_depths`]'.
60///
61/// ```
62/// use music21_rs::analysis::metrical::label_beat_depth;
63/// use music21_rs::stream::StreamElement;
64/// use music21_rs::tinynotation::from_tiny_notation;
65///
66/// let mut part = from_tiny_notation("4/4 c8 c c c c c c c")?;
67/// label_beat_depth(&mut part)?;
68/// let stars: Vec<String> = part
69/// .leaves()
70/// .into_iter()
71/// .filter_map(|(_, element)| match element {
72/// StreamElement::Note(note) => {
73/// Some(note.lyrics().iter().map(|lyric| lyric.text()).collect())
74/// }
75/// _ => None,
76/// })
77/// .collect();
78/// assert_eq!(stars, ["****", "*", "**", "*", "***", "*", "**", "*"]);
79/// # Ok::<(), music21_rs::Error>(())
80/// ```
81///
82/// # Errors
83///
84/// As [`beat_depths`], or a chord symbol to mark, which carries no lyric
85/// here.
86pub fn label_beat_depth(part: &mut Stream) -> Result<()> {
87 let depths = beat_depths(part)?;
88 let mut position = 0;
89 let mut next = depths.iter().peekable();
90 let mut refused = None;
91 part.for_each_mut(&mut |_, element| {
92 if let Some(&&(at, depth)) = next.peek()
93 && at == position
94 {
95 next.next();
96 for _ in 0..depth {
97 let added = match element {
98 StreamElement::Note(note) => note.add_lyric("*", None, false),
99 StreamElement::Chord(chord) => chord.add_lyric("*", None, false),
100 StreamElement::Rest(rest) => {
101 rest.add_lyric("*", None, false);
102 Ok(())
103 }
104 StreamElement::Unpitched(stroke) => {
105 stroke.written_mut().add_lyric("*", None, false)
106 }
107 StreamElement::PercussionChord(chord) => {
108 chord.written_mut().add_lyric("*", None, false)
109 }
110 _ => Err(Error::Analysis(
111 "a chord symbol carries no lyric to mark its beat depth with".to_string(),
112 )),
113 };
114 if let Err(error) = added {
115 refused.get_or_insert(error);
116 }
117 }
118 }
119 position += 1;
120 });
121 refused.map_or(Ok(()), Err)
122}
123
124/// The meter a measure is read in: the first it holds, else the one in
125/// force in the part where it starts, else four-four.
126fn measure_meter(part: &Stream, measure: &Stream, offset: FloatType) -> Result<TimeSignature> {
127 if let Some(meter) = measure
128 .recurse()
129 .into_iter()
130 .find_map(|(_, element)| match element {
131 StreamElement::TimeSignature(meter) => Some(meter.clone()),
132 _ => None,
133 })
134 {
135 return Ok(meter);
136 }
137 match part.time_signature_at(offset) {
138 Some(meter) => Ok(meter),
139 None => TimeSignature::new(4, 4),
140 }
141}
142
143/// music21's `notesAndRests`: anything that sounds or rests.
144fn is_note_or_rest(element: &StreamElement) -> bool {
145 matches!(
146 element,
147 StreamElement::Note(_)
148 | StreamElement::Chord(_)
149 | StreamElement::Rest(_)
150 | StreamElement::Unpitched(_)
151 | StreamElement::PercussionChord(_)
152 | StreamElement::ChordSymbol(_)
153 )
154}
155
156fn ends_tie(element: &StreamElement) -> bool {
157 let tie = match element {
158 StreamElement::Note(note) => note.tie(),
159 StreamElement::Chord(chord) => chord.tie(),
160 StreamElement::Rest(rest) => rest.tie(),
161 StreamElement::Unpitched(stroke) => stroke.written().tie(),
162 StreamElement::PercussionChord(chord) => chord.written().tie(),
163 _ => None,
164 };
165 tie.is_some_and(|tie| tie.tie_type() == TieType::Stop)
166}
167
168/// The melodic accent of each note of a line, after Thomassen (1982) as
169/// Huron and Royal (1996) apply it: music21's `thomassenMelodicAccent`.
170///
171/// The first note takes an accent of one. Each note after it is accented by
172/// the contour around it -- a peak or a valley, a turn, a run on or a note
173/// repeated -- times what the contour before it left over for it, and the
174/// last note takes what is left over. Only pitch heights are compared, so
175/// enharmonic notes are one. The line is the notes the stream holds itself,
176/// in order.
177///
178/// ```
179/// use music21_rs::Stream;
180/// use music21_rs::analysis::metrical::thomassen_melodic_accent;
181/// use music21_rs::stream::StreamElement;
182/// use music21_rs::tinynotation::from_tiny_notation;
183///
184/// let line = from_tiny_notation("7/4 c4 c c d e d d")?.flatten();
185/// let notes = Stream::from_events(
186/// line.events()
187/// .iter()
188/// .filter(|event| matches!(event.element(), StreamElement::Note(_)))
189/// .cloned(),
190/// );
191/// let accents = thomassen_melodic_accent(¬es)?;
192/// assert_eq!(accents, [1.0, 0.0, 0.0, 0.33, 0.67 * 0.83, 0.17, 0.0]);
193/// # Ok::<(), music21_rs::Error>(())
194/// ```
195///
196/// # Errors
197///
198/// Anything in the stream but a note, once there are three elements to
199/// compare.
200pub fn thomassen_melodic_accent(line: &Stream) -> Result<Vec<FloatType>> {
201 let events = line.events();
202 let height = |index: usize| -> Result<FloatType> {
203 match events[index].element() {
204 StreamElement::Note(note) => Ok(note.pitch().ps()),
205 _ => Err(Error::Analysis(
206 "melodic accent is read off notes alone".to_string(),
207 )),
208 }
209 };
210 let last = events.len().saturating_sub(1);
211 let mut accents = Vec::with_capacity(events.len());
212 let mut carried = 1.0;
213 for index in 0..events.len() {
214 if index == 0 {
215 accents.push(1.0);
216 continue;
217 }
218 if index == last {
219 accents.push(carried);
220 continue;
221 }
222 let (before, here, after) = (height(index - 1)?, height(index)?, height(index + 1)?);
223 let (this, next) = if before == here && here == after {
224 (0.0, 0.0)
225 } else if before != here && here == after {
226 (1.0, 0.0)
227 } else if before == here {
228 (0.0, 1.0)
229 } else if before < here && here > after {
230 (0.83, 0.17)
231 } else if before > here && here < after {
232 (0.71, 0.29)
233 } else if before < here && here < after {
234 (0.33, 0.67)
235 } else if before > here && here > after {
236 (0.5, 0.5)
237 } else {
238 (0.0, 0.0)
239 };
240 accents.push(this * carried);
241 carried = next;
242 }
243 Ok(accents)
244}