1use std::collections::BTreeSet;
19
20use crate::error::{Error, Result};
21use crate::pitch::Pitch;
22use crate::scale::{DegreeComparison, Scale};
23use crate::stream::{Stream, StreamElement};
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum RunDirection {
29 Ascending,
31 Descending,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub enum PitchAttribute {
40 #[default]
42 Name,
43 NameWithOctave,
45 PitchClass,
47 Step,
49}
50
51impl PitchAttribute {
52 fn key(self, pitch: &Pitch) -> String {
53 match self {
54 Self::Name => pitch.name(),
55 Self::NameWithOctave => pitch.name_with_octave(),
56 Self::PitchClass => pitch.pitch_class().number().to_string(),
57 Self::Step => pitch.step().as_char().to_string(),
58 }
59 }
60
61 fn degree_comparison(self) -> DegreeComparison {
65 match self {
66 Self::Name | Self::NameWithOctave => DegreeComparison::Name,
67 Self::PitchClass => DegreeComparison::PitchClass,
68 Self::Step => DegreeComparison::Step,
69 }
70 }
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub struct ScaleRunOptions {
77 pub degrees_required: usize,
79 pub step_size: usize,
81 pub comparison: PitchAttribute,
83 pub repeats_allowed: bool,
85}
86
87impl Default for ScaleRunOptions {
88 fn default() -> Self {
89 Self {
90 degrees_required: 5,
91 step_size: 1,
92 comparison: PitchAttribute::Name,
93 repeats_allowed: true,
94 }
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct ScaleRun {
103 pub elements: Vec<usize>,
105 pub direction: Option<RunDirection>,
107}
108
109fn degree(
112 scale: &Scale,
113 pitch: &Pitch,
114 direction: Option<RunDirection>,
115 comparison: PitchAttribute,
116) -> Result<Option<usize>> {
117 let comparison = comparison.degree_comparison();
118 match direction {
119 Some(RunDirection::Ascending) => scale.degree_of_by(pitch, comparison),
120 Some(RunDirection::Descending) => scale.descending().degree_of_by(pitch, comparison),
121 None => match scale.degree_of_by(pitch, comparison)? {
122 Some(found) => Ok(Some(found)),
123 None => scale.descending().degree_of_by(pitch, comparison),
124 },
125 }
126}
127
128fn is_next(
132 scale: &Scale,
133 other: Option<&Pitch>,
134 origin: &Pitch,
135 direction: Option<RunDirection>,
136 steps: usize,
137 comparison: PitchAttribute,
138) -> Result<bool> {
139 let Some(other) = other else {
140 return Ok(false);
141 };
142 let next = match direction {
143 Some(RunDirection::Ascending) => scale.next_pitch_beside(origin, steps as _, true)?,
144 Some(RunDirection::Descending) => scale.descending().next_pitch_beside(
145 origin,
146 -(steps as crate::defaults::IntegerType),
147 false,
148 )?,
149 None => {
150 return Err(Error::Scale(
151 "cannot match direction specification: None".to_string(),
152 ));
153 }
154 };
155 Ok(comparison.key(&next) == comparison.key(other))
156}
157
158pub fn find_consecutive_scale(
174 source: &Stream,
175 scale: &Scale,
176 options: &ScaleRunOptions,
177) -> Result<Vec<ScaleRun>> {
178 let notes: Vec<(usize, Pitch)> = source
179 .events()
180 .iter()
181 .enumerate()
182 .filter_map(|(place, event)| match event.element() {
183 StreamElement::Note(note) => Some((place, note.pitch().clone())),
184 _ => None,
185 })
186 .collect();
187 let mut order: Vec<usize> = (0..notes.len()).collect();
188 order.sort_by(|&left, &right| {
189 source.events()[notes[left].0]
190 .offset()
191 .total_cmp(&source.events()[notes[right].0].offset())
192 });
193 let notes: Vec<(usize, Pitch)> = order
194 .into_iter()
195 .map(|index| notes[index].clone())
196 .collect();
197
198 let comparison = options.comparison;
199 let step = options.step_size;
200 let mut degree_last: Option<usize> = None;
201 let mut pitch_last: Option<Pitch> = None;
202 let mut direction_last: Option<RunDirection> = None;
203 let mut degrees: BTreeSet<usize> = BTreeSet::new();
204 let mut elements: Vec<usize> = Vec::new();
205 let mut clear = false;
206 let mut clear_keeping_last = false;
207 let mut runs = Vec::new();
208
209 for (index, (place, pitch)) in notes.iter().enumerate() {
210 let next = notes.get(index + 1).map(|(_, next)| next);
211 let found_degree = degree(scale, pitch, direction_last, comparison)?;
212 match found_degree {
213 None => clear = true,
214 Some(found) => {
215 let collect = match (degree_last, &pitch_last) {
216 (None, _) | (_, None) => true,
217 (Some(last), _) if options.repeats_allowed && last == found => true,
218 (Some(_), Some(previous)) => {
219 if matches!(direction_last, None | Some(RunDirection::Ascending))
220 && is_next(
221 scale,
222 Some(pitch),
223 previous,
224 Some(RunDirection::Ascending),
225 step,
226 comparison,
227 )?
228 {
229 direction_last = Some(RunDirection::Ascending);
230 true
231 } else if matches!(direction_last, None | Some(RunDirection::Descending))
232 && is_next(
233 scale,
234 Some(pitch),
235 previous,
236 Some(RunDirection::Descending),
237 step,
238 comparison,
239 )?
240 {
241 direction_last = Some(RunDirection::Descending);
242 true
243 } else {
244 clear_keeping_last = true;
245 false
246 }
247 }
248 };
249 if collect {
250 degrees.insert(found);
251 elements.push(*place);
252 }
253 }
254 }
255 degree_last = found_degree;
256 pitch_last = Some(pitch.clone());
257
258 if degrees.len() >= options.degrees_required
259 && !is_next(scale, next, pitch, direction_last, 1, comparison)?
260 {
261 runs.push(ScaleRun {
262 elements: elements.clone(),
263 direction: direction_last,
264 });
265 if !clear && !clear_keeping_last {
266 let continues = is_next(
267 scale,
268 next,
269 pitch,
270 Some(RunDirection::Descending),
271 step,
272 comparison,
273 )? || is_next(
274 scale,
275 next,
276 pitch,
277 Some(RunDirection::Ascending),
278 step,
279 comparison,
280 )?;
281 if continues {
282 clear_keeping_last = true;
283 } else {
284 clear = true;
285 }
286 }
287 }
288 if clear {
289 degree_last = None;
290 direction_last = None;
291 degrees.clear();
292 elements.clear();
293 clear = false;
294 }
295 if clear_keeping_last {
296 direction_last = None;
297 degrees.clear();
298 if let Some(found) = found_degree {
301 degrees.insert(found);
302 }
303 elements = vec![*place];
304 clear_keeping_last = false;
305 }
306 }
307 Ok(runs)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::note::Note;
314 use crate::scale::ScaleType;
315
316 fn line(names: &[&str]) -> Stream {
317 let mut stream = Stream::new();
318 for name in names {
319 stream.push(Note::from_name(name).unwrap());
320 }
321 stream
322 }
323
324 fn runs(names: &[&str], required: usize, comparison: PitchAttribute) -> Vec<(usize, String)> {
325 let source = line(names);
326 let a_major = Scale::new(ScaleType::Major, Pitch::from_name("A4").unwrap());
327 let options = ScaleRunOptions {
328 degrees_required: required,
329 comparison,
330 ..ScaleRunOptions::default()
331 };
332 find_consecutive_scale(&source, &a_major, &options)
333 .unwrap()
334 .into_iter()
335 .map(|run| {
336 let StreamElement::Note(first) = source.events()[run.elements[0]].element() else {
337 unreachable!("a run holds notes")
338 };
339 (run.elements.len(), first.pitch().name_with_octave())
340 })
341 .collect()
342 }
343
344 #[test]
346 fn runs_are_found_as_music21_finds_them() {
347 use PitchAttribute::{Name, NameWithOctave};
348 let scale = ["a4", "b4", "c#4", "d4", "e4", "f#4", "g#4", "a4"];
349 assert_eq!(runs(&scale, 4, Name).len(), 1);
350 assert_eq!(runs(&scale, 4, Name)[0].0, 8);
351 assert_eq!(runs(&scale, 4, NameWithOctave)[0].0, 6);
352 let twice = ["a4", "b4", "c#5", "d5", "e5", "a4", "b4", "c#5", "d5", "e5"];
353 assert_eq!(
354 runs(&twice, 4, NameWithOctave)
355 .iter()
356 .map(|r| r.0)
357 .collect::<Vec<_>>(),
358 [5, 5]
359 );
360 let shifted = [
361 "a4", "b8", "c#3", "d3", "e4", "a4", "b9", "c#2", "d4", "e12",
362 ];
363 assert_eq!(
364 runs(&shifted, 4, Name)
365 .iter()
366 .map(|r| r.0)
367 .collect::<Vec<_>>(),
368 [5, 5]
369 );
370 let thrice = [
371 "a4", "b4", "c#5", "d-3", "a4", "b4", "c#5", "d-3", "a4", "b4", "c#5", "d-3",
372 ];
373 assert!(runs(&thrice, 4, NameWithOctave).is_empty());
374 assert_eq!(
375 runs(&thrice, 3, NameWithOctave)
376 .iter()
377 .map(|r| r.0)
378 .collect::<Vec<_>>(),
379 [3, 3, 3]
380 );
381
382 let up = ["c#5", "d3", "e4", "f#4", "g#4"];
383 let down = ["g#4", "f#4", "e4", "d3", "c#5"];
384 let turning: Vec<&str> = [up, down, up, down, down].concat();
385 assert!(runs(&turning, 5, NameWithOctave).is_empty());
386 let found = runs(&turning, 5, Name);
387 assert_eq!(
388 found.iter().map(|r| r.1.as_str()).collect::<Vec<_>>(),
389 ["C#5", "G#4", "C#5", "G#4", "G#4"]
390 );
391 assert!(found.iter().all(|run| run.0 == 5));
392
393 let mingled: Vec<&str> = [
394 &up[..],
395 &down,
396 &["g2", "e#7"],
397 &up,
398 &["a-2"],
399 &down,
400 &["a", "b"],
401 &down,
402 ]
403 .concat();
404 assert!(runs(&mingled, 5, NameWithOctave).is_empty());
405 let found = runs(&mingled, 5, Name);
406 assert_eq!(
407 found.iter().map(|r| r.1.as_str()).collect::<Vec<_>>(),
408 ["C#5", "G#4", "C#5", "G#4", "G#4"]
409 );
410 }
411}