music21_rs/analysis/windowed.rs
1//! An analysis run over windows of a stream, from a quarter note long to
2//! the whole of it: music21's `analysis.windowed`.
3
4use crate::{
5 error::{Error, Result},
6 makenotation::{make_measures_by, make_ties},
7 meter::TimeSignature,
8 stream::{Stream, StreamEvent, StreamKind},
9};
10
11/// How a window of several quarters moves along the stream:
12/// [`WindowedAnalysis::analyze`]'s `windowType`.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum WindowType {
16 /// A window starting at every quarter: quarters one to three, then two
17 /// to four, and on.
18 #[default]
19 Overlap,
20 /// Windows side by side: quarters one to three, then four to six. Where
21 /// the quarters divide evenly into windows a last, empty window is
22 /// analysed too, as music21 does.
23 NoOverlap,
24 /// For each quarter, every overlapping window holding it, put end to
25 /// end: one answer per quarter.
26 AdjacentAverage,
27}
28
29/// How [`WindowedAnalysis::process`] goes from one window size to the next.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub enum WindowStep {
33 /// Each size this many quarters larger than the last: music21's
34 /// `windowStepSize` given as a number.
35 Add(usize),
36 /// Each size this many times the last, until past three quarters of the
37 /// largest: music21's `windowStepSize` given as text, `'2x'`.
38 Multiply(usize),
39}
40
41/// A stream cut into windows of a quarter note each, ready to have an
42/// analysis run over windows of any number of them: music21's
43/// `WindowedAnalysis`.
44///
45/// music21 hands each window to an analysis object's `process`; here the
46/// analysis is a function of the window, and a window is a stream of the
47/// quarter-note measures it spans, put end to end. A window holding nothing
48/// the analysis can use is the analysis's to answer for, as an `Option` or
49/// a `Result`.
50///
51/// ```
52/// use music21_rs::analysis::{KeyProfile, estimate_key_of_stream};
53/// use music21_rs::analysis::windowed::{WindowType, WindowedAnalysis};
54/// use music21_rs::tinynotation::from_tiny_notation;
55///
56/// let line = from_tiny_notation("4/4 c4 e g c' f a c' f'")?;
57/// let windowed = WindowedAnalysis::new(&line)?;
58/// assert_eq!(windowed.window_count(), 8);
59/// let keys = windowed.analyze(4, WindowType::Overlap, |window| {
60/// estimate_key_of_stream(KeyProfile::KrumhanslSchmuckler, window)
61/// .map(|estimates| estimates[0].key().tonic_pitch_name_with_case())
62/// });
63/// assert_eq!(keys.first(), Some(&Some("C".to_string())));
64/// assert_eq!(keys.last(), Some(&Some("F".to_string())));
65/// # Ok::<(), music21_rs::Error>(())
66/// ```
67#[derive(Clone, Debug)]
68pub struct WindowedAnalysis {
69 windows: Vec<Stream>,
70}
71
72impl WindowedAnalysis {
73 /// Cuts a stream into measures of one quarter note, a note running past
74 /// a barline tied into the next: music21's `getMinimumWindowStream`. A
75 /// stream holding parts is flattened first, its parts sounding as one.
76 ///
77 /// # Errors
78 ///
79 /// A stream that cannot be put into measures.
80 pub fn new(stream: &Stream) -> Result<Self> {
81 let source = if stream.has_part_like_streams() {
82 stream.flatten()
83 } else {
84 stream.clone()
85 };
86 let mut measured = make_measures_by(&source, &[(0.0, TimeSignature::new(1, 4)?)])?;
87 let measures: Vec<StreamEvent> = measured
88 .events()
89 .iter()
90 .filter(|event| {
91 event
92 .element()
93 .as_stream()
94 .is_some_and(|inner| inner.kind() == StreamKind::Measure)
95 })
96 .cloned()
97 .collect();
98 if measures.is_empty() {
99 return Err(Error::Analysis("making measures failed".to_string()));
100 }
101 measured = measured.with_events(measures);
102 make_ties(&mut measured)?;
103 let windows = measured
104 .events()
105 .iter()
106 .filter_map(|event| event.element().as_stream().cloned())
107 .collect();
108 Ok(Self { windows })
109 }
110
111 /// The quarter-note measures every window is made of.
112 pub fn minimum_windows(&self) -> &[Stream] {
113 &self.windows
114 }
115
116 /// How many quarter-note measures there are.
117 pub fn window_count(&self) -> usize {
118 self.windows.len()
119 }
120
121 /// The analysis run over every window of `window_size` quarters, in
122 /// order: music21's `analyze`, its answers without the colours.
123 pub fn analyze<T>(
124 &self,
125 window_size: usize,
126 window_type: WindowType,
127 mut analysis: impl FnMut(&Stream) -> T,
128 ) -> Vec<T> {
129 let count = self.windows.len();
130 match window_type {
131 WindowType::Overlap => (0..(count + 1).saturating_sub(window_size))
132 .map(|start| analysis(&self.window(start..start + window_size)))
133 .collect(),
134 WindowType::NoOverlap => {
135 let windows = count.checked_div(window_size).map_or(0, |whole| whole + 1);
136 (0..windows)
137 .map(|index| {
138 let start = (index * window_size).min(count);
139 let end = (start + window_size).min(count);
140 analysis(&self.window(start..end))
141 })
142 .collect()
143 }
144 WindowType::AdjacentAverage => {
145 let overlapped: Vec<std::ops::Range<usize>> = (0..(count + 1)
146 .saturating_sub(window_size))
147 .map(|start| start..start + window_size)
148 .collect();
149 (0..count)
150 .map(|quarter| {
151 let held: Vec<usize> = overlapped
152 .iter()
153 .filter(|range| range.contains(&quarter))
154 .flat_map(Clone::clone)
155 .collect();
156 analysis(&self.end_to_end(held))
157 })
158 .collect()
159 }
160 }
161 }
162
163 /// The analysis run over windows of each size from `min_window` to
164 /// `max_window` quarters, stepping by `step`, each size with its
165 /// answers: music21's `process`. A size left out is the whole stream,
166 /// and with `include_total_window` the whole stream is always among
167 /// the sizes.
168 ///
169 /// # Errors
170 ///
171 /// Multiplying by less than two, which never grows.
172 pub fn process<T>(
173 &self,
174 min_window: Option<usize>,
175 max_window: Option<usize>,
176 step: WindowStep,
177 window_type: WindowType,
178 include_total_window: bool,
179 mut analysis: impl FnMut(&Stream) -> T,
180 ) -> Result<Vec<(usize, Vec<T>)>> {
181 let total = self.windows.len();
182 let largest = max_window.unwrap_or(total);
183 let smallest = min_window.unwrap_or(total);
184 let mut sizes: Vec<usize> = match step {
185 WindowStep::Add(by) => {
186 if by == 0 {
187 return Err(Error::Analysis(
188 "a window step cannot be nought".to_string(),
189 ));
190 }
191 (smallest..=largest).step_by(by).collect()
192 }
193 WindowStep::Multiply(by) => {
194 if by < 2 {
195 return Err(Error::Analysis(
196 "a window size multiplied by less than two never grows".to_string(),
197 ));
198 }
199 let mut sizes = vec![smallest];
200 let mut size = smallest;
201 loop {
202 size *= by;
203 if size as f64 > largest as f64 * 0.75 || smallest == 0 {
204 break;
205 }
206 sizes.push(size);
207 }
208 sizes
209 }
210 };
211 if include_total_window && !sizes.contains(&total) {
212 sizes.push(total);
213 }
214 Ok(sizes
215 .into_iter()
216 .map(|size| (size, self.analyze(size, window_type, &mut analysis)))
217 .collect())
218 }
219
220 fn window(&self, range: std::ops::Range<usize>) -> Stream {
221 self.end_to_end(range.collect())
222 }
223
224 /// The measures at these indices, each starting where the last ended.
225 fn end_to_end(&self, indices: Vec<usize>) -> Stream {
226 let mut window = Stream::new();
227 for index in indices {
228 window.push(self.windows[index].clone());
229 }
230 window
231 }
232}