redact_composer_musical/note/
iter.rs

1use crate::{Interval, Note, PitchClass};
2use std::collections::Bound;
3use std::ops::RangeBounds;
4
5/// Provides iteration over notes of specific patterns (e.g. scale/chord) within a given note range.
6///
7/// When implementing this trait, only [`Self::iter_notes_in_range`] is required. The default implementation of
8/// [`Self::notes_in_range`] simply collects these into a [`Vec`].
9pub trait NoteIterator {
10    /// Returns a note iterator ([`NoteIter`]) for notes of an interval pattern within the given note range.
11    fn iter_notes_in_range<R: RangeBounds<Note>>(&self, note_range: R) -> NoteIter<R>;
12    /// Returns all notes matching an interval pattern within the given note range.
13    fn notes_in_range<R: RangeBounds<Note>>(&self, note_range: R) -> Vec<Note> {
14        self.iter_notes_in_range(note_range).collect()
15    }
16}
17
18/// Iteration state for [`Note`]s according to an [`Interval`] pattern within a [`RangeBounds<Note>`].
19/// ```
20/// use redact_composer_musical::{Note, NoteIter};
21///
22/// let mut iter = NoteIter::from(Note(60)..Note(63));
23/// assert_eq!(iter.next(), Some(Note(60)));
24/// assert_eq!(iter.next(), Some(Note(61)));
25/// assert_eq!(iter.next(), Some(Note(62)));
26/// assert_eq!(iter.next(), None);
27/// ```
28#[derive(Debug, Clone)]
29pub struct NoteIter<R: RangeBounds<Note>> {
30    curr: Option<Note>,
31    intervals: Vec<Interval>,
32    interval_idx: usize,
33    bounds: R,
34}
35
36impl<R: RangeBounds<Note>> NoteIter<R> {
37    /// A chromatic note iterator over notes within a given note range.
38    pub fn chromatic(range: R) -> NoteIter<R> {
39        NoteIter {
40            curr: match range.start_bound() {
41                Bound::Included(n) => Some(*n),
42                Bound::Excluded(n) => Some(Note(n.0 + 1)),
43                Bound::Unbounded => Some(Note(0)),
44            },
45            intervals: vec![Interval(1)],
46            interval_idx: 0,
47            bounds: range,
48        }
49    }
50
51    fn next_interval(&mut self) -> Option<Interval> {
52        let returned = self
53            .intervals
54            .get(self.interval_idx)
55            .copied()
56            .and_then(|i| {
57                // Don't let the next interval push it past the maximum
58                if self.curr.map(|n| u8::MAX - n.0 >= i.0).unwrap_or(true) {
59                    Some(i)
60                } else {
61                    None
62                }
63            });
64
65        self.interval_idx = (self.interval_idx + 1) % self.intervals.len();
66
67        returned
68    }
69}
70
71impl<R: RangeBounds<Note>> From<R> for NoteIter<R> {
72    fn from(note_range: R) -> Self {
73        NoteIter::chromatic(note_range)
74    }
75}
76
77impl<R: RangeBounds<Note>> From<(PitchClass, Vec<Interval>, R)> for NoteIter<R> {
78    /// Constructs a note iterator ([`NoteIter`]) starting from a [`PitchClass`], over an ordered sequence of
79    /// [`Interval`]s relative to the starting pitch within the given note range.
80    fn from(value: (PitchClass, Vec<Interval>, R)) -> Self {
81        let (root, intervals, note_range) = value;
82        if let Some(first) = intervals.first() {
83            let start_pitch_class = root + *first;
84            let mut steps = intervals.windows(2).fold(vec![], |mut acc, i| {
85                acc.push(Interval(i[1].0 - i[0].0));
86
87                acc
88            });
89
90            if steps.is_empty() {
91                steps.push(Interval::Octave)
92            } else {
93                let sum_interval = steps.iter().copied().sum::<Interval>();
94                if sum_interval.to_simple() != Interval::P1 {
95                    steps.push(sum_interval.to_simple().inversion())
96                }
97            }
98
99            let first_range_note = match note_range.start_bound() {
100                Bound::Included(n) => *n,
101                Bound::Excluded(n) => Note(n.0 + 1),
102                Bound::Unbounded => Note(0),
103            };
104
105            // Find the first position within the note_range where the interval pattern can begin
106            // This will align the pattern such that the first pitch is as early as possible within the note_range
107            let (first_note, interval_idx_offset) = {
108                let mut interval_step_idx = steps.len() - 1;
109                let mut starting_pitch = start_pitch_class;
110                let mut interval_offset = Interval::P1;
111                let mut closest_starting_distance =
112                    first_range_note.pitch_class().interval_to(&starting_pitch);
113
114                // Walk backwards through the interval steps (up to an octave, exclusive) to find any pitches from the
115                // tail end of the interval pattern that will fit within the note_range
116                while interval_step_idx > 0
117                    && first_range_note.pitch_class().interval_to(
118                        &(starting_pitch + steps[interval_step_idx].inversion().to_simple()),
119                    ) < closest_starting_distance
120                    && interval_offset + steps[interval_step_idx] < Interval::Octave
121                {
122                    starting_pitch += steps[interval_step_idx].inversion().to_simple();
123                    interval_offset += steps[interval_step_idx];
124                    closest_starting_distance =
125                        first_range_note.pitch_class().interval_to(&starting_pitch);
126                    interval_step_idx -= 1;
127                }
128
129                let starting_note = starting_pitch.at_or_above(&first_range_note);
130                if note_range.contains(&starting_note) {
131                    (Some(starting_note), (interval_step_idx + 1) % steps.len())
132                } else {
133                    (None, 0)
134                }
135            };
136
137            NoteIter {
138                curr: first_note,
139                intervals: steps,
140                interval_idx: interval_idx_offset,
141                bounds: note_range,
142            }
143        } else {
144            // Return empty iterator
145            NoteIter {
146                curr: None,
147                intervals: Vec::new(),
148                interval_idx: Default::default(),
149                bounds: note_range,
150            }
151        }
152    }
153}
154
155impl<R> Iterator for NoteIter<R>
156where
157    R: RangeBounds<Note>,
158{
159    type Item = Note;
160
161    fn next(&mut self) -> Option<Self::Item> {
162        if let Some(note) = self.curr {
163            let return_note = if self.bounds.contains(&note) {
164                Some(note)
165            } else {
166                None
167            };
168
169            self.curr = self.next_interval().map(|interval| note + interval);
170
171            return_note
172        } else {
173            None
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use crate::{
181        Chord, ChordShape, Interval, IntervalCollection, Key, Mode, Note, NoteIterator, PitchClass,
182        PitchClassCollection, Scale,
183    };
184
185    #[test]
186    fn key_notes_boundary_test() {
187        let roots = vec![PitchClass(0), PitchClass(9), PitchClass(11)];
188        let scales = Scale::values();
189        let modes = vec![Mode::Ionian, Mode::Dorian, Mode::Aeolian, Mode::Locrian];
190        let lengths = [0, 1, 11, 12, 13, 23];
191        let offsets = [0, 1, 11, 12, 13, 23];
192
193        // let mut seq = 0_usize;
194        for root in roots.clone() {
195            for scale in scales.clone() {
196                for mode in modes.clone() {
197                    for length in lengths {
198                        for offset in offsets {
199                            let key = Key::from((root, scale, mode));
200                            let key_pitches = key.pitch_classes();
201
202                            let note_range = Note(offset)..Note(offset + length);
203                            let output = key.notes_in_range(note_range.clone());
204                            let out_of_key = output
205                                .iter()
206                                .filter(|n| !key_pitches.contains(&n.pitch_class()))
207                                .collect::<Vec<_>>();
208                            assert!(
209                                out_of_key.is_empty(),
210                                "`{:?}.notes_in_range({:?})` produced out of key notes.\nOutput: {:?}\nOut of key: {:?}",
211                                key, note_range, output, out_of_key
212                            )
213                        }
214                    }
215                }
216            }
217        }
218    }
219
220    #[test]
221    fn chord_note_iter_smoke_test() {
222        let roots = [PitchClass(0), PitchClass(7), PitchClass(11)];
223        let chord_shapes = ChordShape::all();
224        let range_lens = (0..=36).collect::<Vec<_>>();
225        let range_offsets = (0..=12).collect::<Vec<_>>();
226
227        for root in roots {
228            for shape in chord_shapes.clone() {
229                for range_len in range_lens.clone() {
230                    for range_offset in range_offsets.clone() {
231                        let first_range_note = Note(range_offset);
232                        let note_range = first_range_note..(first_range_note + Interval(range_len));
233                        let chord = Chord::from((root, shape));
234                        let chord_pitches = chord.pitch_classes();
235                        let chord_notes = chord.notes_in_range(note_range.clone());
236
237                        if note_range
238                            .clone()
239                            .contains(&chord.root.at_or_above(&first_range_note))
240                        {
241                            assert!(
242                                chord_notes.contains(&chord.root.at_or_above(&first_range_note)),
243                                "{:?} could contain chord root of {:?}, but it doesn't.",
244                                note_range,
245                                chord
246                            );
247                        }
248
249                        assert!(
250                            chord_notes
251                                .iter()
252                                .all(|n| chord_pitches.contains(&n.pitch_class())),
253                            "{:?}.notes_in_range({:?}) produced notes not within the chord.",
254                            chord,
255                            note_range
256                        );
257
258                        let note_count_lower_bound = range_len as usize
259                            / (first_range_note.pitch_class().interval_to(&chord.root)
260                                + *chord.intervals().last().unwrap()
261                                + chord.intervals().last().unwrap().inversion())
262                            .0 as usize
263                            * chord.intervals().len();
264
265                        assert!(chord_notes.len() >= note_count_lower_bound,
266                            "{:?} has room for at least {:?} notes of {:?}, but only {:?} were produced.",
267                            note_range, note_count_lower_bound, chord, chord_notes.len());
268
269                        if note_count_lower_bound >= chord.intervals().len() {
270                            assert!(
271                                chord_pitches.iter().all(|p| {
272                                    chord_notes.iter().any(|n| n.pitch_class() == *p)
273                                }),
274                                "{:?} has room for all notes of {:?}, but they weren't all produced.", note_range, chord);
275                        }
276                    }
277                }
278            }
279        }
280    }
281}