Skip to main content

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}