use crate::core::{Duration, Fraction};
use crate::notation::TimeSignature;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BeamType {
Start,
Continue,
Stop,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Beam {
indices: Vec<usize>,
}
impl Beam {
pub fn indices(&self) -> &[usize] {
&self.indices
}
pub fn beam_type_at(&self, position: usize) -> Option<BeamType> {
if position >= self.indices.len() {
return None;
}
Some(if position == 0 {
BeamType::Start
} else if position == self.indices.len() - 1 {
BeamType::Stop
} else {
BeamType::Continue
})
}
}
pub fn compute_beams(durations: &[Duration], time_signature: &TimeSignature) -> Vec<Beam> {
let beat_starts = time_signature.beat_offsets();
let beat_lengths = time_signature.beat_sequence();
let beat_index_for = |offset: Fraction| -> usize {
for (i, (&start, &length)) in beat_starts.iter().zip(beat_lengths.iter()).enumerate() {
if offset >= start && offset < start + length {
return i;
}
}
beat_starts.len().saturating_sub(1)
};
let mut groups: Vec<Beam> = Vec::new();
let mut current: Vec<usize> = Vec::new();
let mut current_beat: Option<usize> = None;
let mut offset = Fraction::new(0, 1);
for (i, duration) in durations.iter().enumerate() {
let length = duration.quarter_length();
let eligible = length > Fraction::new(0, 1) && length < Fraction::new(1, 1);
let beat = beat_index_for(offset);
if eligible && current_beat == Some(beat) {
current.push(i);
} else {
if current.len() > 1 {
groups.push(Beam {
indices: std::mem::take(&mut current),
});
} else {
current.clear();
}
if eligible {
current.push(i);
current_beat = Some(beat);
} else {
current_beat = None;
}
}
offset += length;
}
if current.len() > 1 {
groups.push(Beam { indices: current });
}
groups
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_beams_groups_within_a_beat_in_common_time() {
let durations: Vec<Duration> = (0..8).map(|_| Duration::eighth()).collect();
let beams = compute_beams(&durations, &TimeSignature::common_time());
assert_eq!(beams.len(), 4);
for (i, beam) in beams.iter().enumerate() {
assert_eq!(beam.indices(), &[i * 2, i * 2 + 1]);
assert_eq!(beam.beam_type_at(0), Some(BeamType::Start));
assert_eq!(beam.beam_type_at(1), Some(BeamType::Stop));
}
}
#[test]
fn test_compute_beams_quarter_notes_break_grouping() {
let durations = vec![
Duration::eighth(),
Duration::eighth(),
Duration::quarter(),
Duration::eighth(),
Duration::eighth(),
];
let beams = compute_beams(&durations, &TimeSignature::common_time());
assert_eq!(beams.len(), 2);
assert_eq!(beams[0].indices(), &[0, 1]);
assert_eq!(beams[1].indices(), &[3, 4]);
}
#[test]
fn test_compute_beams_compound_meter_groups_whole_beat() {
let durations: Vec<Duration> = (0..6).map(|_| Duration::eighth()).collect();
let beams = compute_beams(&durations, &TimeSignature::six_eight());
assert_eq!(beams.len(), 2);
assert_eq!(beams[0].indices(), &[0, 1, 2]);
assert_eq!(beams[1].indices(), &[3, 4, 5]);
}
#[test]
fn test_compute_beams_does_not_cross_beat_boundary() {
let durations: Vec<Duration> = (0..6).map(|_| Duration::eighth()).collect();
let beams = compute_beams(&durations, &TimeSignature::three_four());
assert_eq!(beams.len(), 3);
assert_eq!(beams[0].indices(), &[0, 1]);
assert_eq!(beams[1].indices(), &[2, 3]);
assert_eq!(beams[2].indices(), &[4, 5]);
}
}