const WHOLE: i64 = 1920;
const MAX_LEVEL: u8 = 8;
const MAX_CHAIN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BeamInput {
pub rtick: i64,
pub dur: i64,
pub beams: u8,
pub is_rest: bool,
pub tuplet_id: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BeamSegment {
pub level: u8,
pub text: BeamText,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BeamText {
Begin,
Continue,
End,
ForwardHook,
BackwardHook,
}
impl BeamText {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Begin => "begin",
Self::Continue => "continue",
Self::End => "end",
Self::ForwardHook => "forward hook",
Self::BackwardHook => "backward hook",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Meter {
pub numerator: u32,
pub denominator: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BeamPlan {
pub segments: Vec<Vec<BeamSegment>>,
pub groups: Vec<Vec<usize>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Grid {
Quarter,
Metered {
shape: Shape,
n: i64,
unit: i64,
bar: i64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
Compound,
LongUnit,
Short,
Irregular,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Place {
bar: i64,
beat: i64,
len: i64,
d1: i64,
offset: i64,
}
impl Place {
fn same_beat(self, other: Self) -> bool {
self.bar == other.bar && self.beat == other.beat
}
fn cell(self, level: u8) -> i64 {
self.offset.div_euclid(span(self.len, self.d1, level))
}
}
#[derive(Debug, Clone, Copy)]
struct Facts {
beamable: bool,
beams: u8,
place: Place,
}
fn grid(meter: Meter) -> Grid {
let n = i64::from(meter.numerator);
let d = i64::from(meter.denominator);
if n == 0 || d == 0 || WHOLE % d != 0 {
return Grid::Quarter;
}
let unit = WHOLE / d;
let shape = if n >= 6 && n % 3 == 0 {
Shape::Compound
} else if unit > 240 {
Shape::LongUnit
} else if n <= 4 {
Shape::Short
} else {
Shape::Irregular
};
Grid::Metered {
shape,
n,
unit,
bar: n * unit,
}
}
fn locate(grid: Grid, t: i64) -> Place {
let Grid::Metered {
shape,
n,
unit,
bar,
} = grid
else {
return Place {
bar: 0,
beat: t.div_euclid(480),
len: 480,
d1: 2,
offset: t.rem_euclid(480),
};
};
let bar_index = t.div_euclid(bar);
let local = t.rem_euclid(bar);
let p = local / unit;
let (len, d1, start) = match shape {
Shape::Compound => (3 * unit, 3, local - local % (3 * unit)),
Shape::LongUnit => (unit, 2, p * unit),
Shape::Short => {
let (len, d1) = match n {
1 => (unit, 2),
3 => (3 * unit, 3),
_ => (2 * unit, 2),
};
(len, d1, local - local % len)
}
Shape::Irregular => {
if n % 2 == 0 || p < n - 3 {
(2 * unit, 2, (p - p % 2) * unit)
} else {
(3 * unit, 3, (n - 3) * unit)
}
}
};
Place {
bar: bar_index,
beat: start,
len,
d1,
offset: local - start,
}
}
fn span(beat_len: i64, d1: i64, level: u8) -> i64 {
let limit = 2880_i64 >> level.min(MAX_LEVEL);
let mut element = beat_len.max(1);
if element <= limit {
return element;
}
let mut next = (d1 > 0 && element % d1 == 0).then(|| element / d1);
for _ in 1..MAX_CHAIN {
let Some(candidate) = next.filter(|&candidate| candidate >= 1) else {
break;
};
element = candidate;
if element <= limit {
return element;
}
next = (element % 2 == 0).then_some(element / 2);
}
element
}
pub(crate) fn plan(notes: &[BeamInput], meter: Meter) -> BeamPlan {
let mut segments: Vec<Vec<BeamSegment>> = vec![Vec::new(); notes.len()];
if notes.is_empty() {
return BeamPlan {
segments,
groups: Vec::new(),
};
}
let grid = grid(meter);
let facts: Vec<Facts> = notes
.iter()
.map(|note| {
let beams = note.beams.min(MAX_LEVEL);
Facts {
beamable: !note.is_rest && beams >= 1,
beams,
place: locate(grid, note.rtick),
}
})
.collect();
let provisional = provisional_groups(notes, &facts);
let merged = merge_eighth_beats(provisional, notes, &facts, grid);
let groups: Vec<Vec<usize>> = merged.into_iter().filter(|g| g.len() >= 2).collect();
for group in &groups {
emit_group(group, notes, &facts, &mut segments);
}
BeamPlan { segments, groups }
}
fn joins(a: (&BeamInput, &Facts), b: (&BeamInput, &Facts)) -> bool {
if b.0.rtick < a.0.rtick {
return false;
}
match (a.0.tuplet_id, b.0.tuplet_id) {
(None, None) => a.1.place.same_beat(b.1.place) && a.1.place.cell(1) == b.1.place.cell(1),
(Some(x), Some(y)) => x == y,
_ => false,
}
}
fn provisional_groups(notes: &[BeamInput], facts: &[Facts]) -> Vec<Vec<usize>> {
let mut groups = Vec::new();
let mut current: Vec<usize> = Vec::new();
let mut previous: Option<(&BeamInput, &Facts)> = None;
for (index, (note, fact)) in notes.iter().zip(facts).enumerate() {
if !fact.beamable {
if !current.is_empty() {
groups.push(std::mem::take(&mut current));
}
previous = None;
continue;
}
let continues = previous.is_some_and(|prev| joins(prev, (note, fact)));
if !continues && !current.is_empty() {
groups.push(std::mem::take(&mut current));
}
current.push(index);
previous = Some((note, fact));
}
if !current.is_empty() {
groups.push(current);
}
groups
}
fn complete_eighth_beat(
group: &[usize],
notes: &[BeamInput],
facts: &[Facts],
unit: i64,
) -> Option<(i64, i64)> {
let [first, second] = group else {
return None;
};
let (a_note, a) = (notes.get(*first)?, facts.get(*first)?);
let (b_note, b) = (notes.get(*second)?, facts.get(*second)?);
let pure_eighth = |note: &BeamInput, fact: &Facts| {
note.tuplet_id.is_none() && fact.beams == 1 && note.dur == 240
};
let on_the_beat = a.place.same_beat(b.place) && a.place.offset == 0 && b.place.offset == 240;
(pure_eighth(a_note, a) && pure_eighth(b_note, b) && on_the_beat)
.then(|| (a.place.bar, a.place.beat / unit))
}
fn merge_eighth_beats(
groups: Vec<Vec<usize>>,
notes: &[BeamInput],
facts: &[Facts],
grid: Grid,
) -> Vec<Vec<usize>> {
let Grid::Metered {
shape: Shape::LongUnit,
n: n @ 2..=4,
unit: 480,
..
} = grid
else {
return groups;
};
let span_len: usize = if n == 3 { 3 } else { 2 };
let beats: Vec<Option<(i64, i64)>> = groups
.iter()
.map(|group| complete_eighth_beat(group, notes, facts, 480))
.collect();
let mut out = Vec::with_capacity(groups.len());
let mut slots: Vec<Option<Vec<usize>>> = groups.into_iter().map(Some).collect();
let mut at = 0;
while at < slots.len() {
if merges_from(at, span_len, &beats, &slots) {
let mut merged = Vec::new();
for slot in slots.iter_mut().skip(at).take(span_len) {
merged.extend(slot.take().unwrap_or_default());
}
out.push(merged);
at += span_len;
} else {
if let Some(group) = slots.get_mut(at).and_then(Option::take) {
out.push(group);
}
at += 1;
}
}
out
}
fn merges_from(
at: usize,
span_len: usize,
beats: &[Option<(i64, i64)>],
slots: &[Option<Vec<usize>>],
) -> bool {
let Some(Some((bar, first_beat))) = beats.get(at).copied() else {
return false;
};
if first_beat.rem_euclid(span_len as i64) != 0 {
return false;
}
let mut previous_last: Option<usize> = None;
for (expected, index) in (first_beat..).zip(at..at + span_len) {
if beats.get(index).copied().flatten() != Some((bar, expected)) {
return false;
}
let Some(Some(group)) = slots.get(index) else {
return false;
};
if let Some(last) = previous_last
&& group.first().copied() != last.checked_add(1)
{
return false;
}
previous_last = group.last().copied();
}
true
}
fn emit_group(
group: &[usize],
notes: &[BeamInput],
facts: &[Facts],
segments: &mut [Vec<BeamSegment>],
) {
let Some(last) = group.len().checked_sub(1) else {
return;
};
let beams_of = |index: usize| facts.get(index).map_or(0, |fact| fact.beams);
let mut push = |index: usize, level: u8, text: BeamText| {
if let Some(list) = segments.get_mut(index) {
list.push(BeamSegment { level, text });
}
};
for (pos, &index) in group.iter().enumerate() {
let text = if pos == 0 {
BeamText::Begin
} else if pos == last {
BeamText::End
} else {
BeamText::Continue
};
push(index, 1, text);
}
let tuplet_group = group
.first()
.and_then(|&i| notes.get(i))
.is_some_and(|note| note.tuplet_id.is_some());
let mut written_pos = Vec::with_capacity(group.len());
let mut running = 0_i64;
for &index in group {
written_pos.push(running);
running = running.saturating_add(notes.get(index).map_or(0, |note| note.dur));
}
let top = group.iter().map(|&i| beams_of(i)).max().unwrap_or(0);
for level in 2..=top {
let connected = |a: usize, b: usize| {
if beams_of(a) < level || beams_of(b) < level {
return false;
}
if tuplet_group {
return true;
}
match (facts.get(a), facts.get(b)) {
(Some(x), Some(y)) => {
x.place.same_beat(y.place) && x.place.cell(level) == y.place.cell(level)
}
_ => false,
}
};
let mut pos = 0;
while let Some(&index) = group.get(pos) {
if beams_of(index) < level {
pos += 1;
continue;
}
let start = pos;
while let (Some(&a), Some(&b)) = (group.get(pos), group.get(pos + 1)) {
if !connected(a, b) {
break;
}
pos += 1;
}
if pos > start {
for (run_pos, &member) in group.iter().enumerate().take(pos + 1).skip(start) {
let text = if run_pos == start {
BeamText::Begin
} else if run_pos == pos {
BeamText::End
} else {
BeamText::Continue
};
push(member, level, text);
}
} else {
let position = if tuplet_group {
written_pos.get(pos).copied().unwrap_or(0)
} else {
facts.get(index).map_or(0, |fact| fact.place.offset)
};
push(index, level, hook(pos, last, level, position));
}
pos += 1;
}
}
}
fn hook(pos: usize, last: usize, level: u8, position: i64) -> BeamText {
if pos == 0 {
return BeamText::ForwardHook;
}
if pos == last {
return BeamText::BackwardHook;
}
let v = WHOLE >> (level.min(MAX_LEVEL) + 1);
if v > 0 && position.rem_euclid(v) == 0 {
BeamText::ForwardHook
} else {
BeamText::BackwardHook
}
}
pub(crate) fn tuplet_shows_bracket(
notes: &[BeamInput],
groups: &[Vec<usize>],
pair_id: u32,
) -> bool {
let members: Vec<usize> = notes
.iter()
.enumerate()
.filter(|(_, note)| note.tuplet_id == Some(pair_id))
.map(|(index, _)| index)
.collect();
if members.is_empty() {
return true;
}
!groups.contains(&members)
}
#[cfg(test)]
#[path = "beam_tests.rs"]
mod tests;