use crate::core::Duration;
use crate::stream::{MusicElement, Part};
pub struct ChordReducer;
impl ChordReducer {
pub fn reduce(part: &Part, max_chords_per_measure: usize) -> Part {
let max_chords_per_measure = max_chords_per_measure.max(1);
let mut result = part.clone();
for i in 0..result.num_measures() {
let time_signature = result.time_signature_at(i).copied().unwrap_or_default();
let measure_duration = result.measure_duration(i);
let elements = match result.measure(i) {
Some(m) => m.elements().to_vec(),
None => continue,
};
if elements.len() <= max_chords_per_measure {
continue;
}
let mut scored: Vec<(usize, f64)> = elements
.iter()
.enumerate()
.map(|(idx, (offset, element))| {
let beat_weight = time_signature.beat_strength(*offset);
let consonance_bonus = match element {
MusicElement::Chord(c) if c.is_consonant() => 0.5,
MusicElement::Note(_) => 0.25,
_ => 0.0,
};
(idx, beat_weight + consonance_bonus)
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let mut keep_indices: Vec<usize> = scored
.into_iter()
.take(max_chords_per_measure)
.map(|(idx, _)| idx)
.collect();
keep_indices.sort_unstable();
if keep_indices.first() != Some(&0) {
keep_indices.insert(0, 0);
keep_indices.dedup();
}
let mut new_elements = Vec::with_capacity(keep_indices.len());
for (k, &idx) in keep_indices.iter().enumerate() {
let (offset, element) = &elements[idx];
let next_offset = keep_indices
.get(k + 1)
.map(|&next_idx| elements[next_idx].0)
.unwrap_or(measure_duration);
let new_duration = Duration::from_quarter_length(next_offset - *offset);
let mut element = element.clone();
match &mut element {
MusicElement::Note(n) => n.set_duration(new_duration),
MusicElement::Chord(c) => c.set_duration(new_duration),
MusicElement::Rest(r) => r.set_duration(new_duration),
}
new_elements.push((*offset, element));
}
if let Some(measure) = result.measure_mut(i) {
measure.clear();
for (offset, element) in new_elements {
measure.insert(offset, element);
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Chord, Fraction, Pitch, Step};
use crate::notation::TimeSignature;
use crate::stream::Measure;
#[test]
fn test_reduce_keeps_measures_already_under_the_limit() {
let mut part = Part::new();
let mut m1 = Measure::new(1);
m1.set_time_signature(TimeSignature::new(4, 4));
m1.insert(
Fraction::new(0, 1),
MusicElement::Chord(Chord::major_triad(Pitch::from_parts(Step::C, Some(4), None))),
);
part.add_measure(m1);
let reduced = ChordReducer::reduce(&part, 4);
assert_eq!(reduced.measure(0).unwrap().len(), 1);
}
#[test]
fn test_reduce_drops_weak_beats_and_extends_survivors() {
let mut part = Part::new();
let mut m1 = Measure::new(1);
m1.set_time_signature(TimeSignature::new(4, 4));
for (i, root) in [Step::C, Step::D, Step::E, Step::F].into_iter().enumerate() {
m1.insert(
Fraction::new(i as i64, 1),
MusicElement::Chord(Chord::major_triad(Pitch::from_parts(root, Some(4), None))),
);
}
part.add_measure(m1);
let reduced = ChordReducer::reduce(&part, 2);
let elements = reduced.measure(0).unwrap().elements().to_vec();
assert!(elements.len() <= 2);
assert_eq!(elements[0].0, Fraction::new(0, 1));
let total: Fraction = elements.iter().map(|(_, e)| e.quarter_length()).sum();
assert_eq!(total, Fraction::new(4, 1));
}
}