croma-core 1.0.0

Library-first ABC to MusicXML exporter
Documentation
use crate::diagnostic::{Diagnostic, RecoveryNote, Severity, Span};
use crate::lower::{
    ActiveTuplet, CompletedTuplet, LoweredEvent, LoweringState, abc_tuplet_reference,
    invalid_tuplet_warning,
};
use crate::model::{Fraction, TupletAttachment, TupletRole};
use crate::syntax::TupletSyntax;

impl LoweringState {
    pub(crate) fn record_tuplet_group(&mut self, group: &[usize]) {
        let mut completed = Vec::new();
        for tuplet in &mut self.active_tuplets {
            if tuplet.remaining == 0 {
                continue;
            }
            tuplet.groups.push(group.to_vec());
            tuplet.remaining -= 1;
            if tuplet.remaining == 0 {
                completed.push(CompletedTuplet {
                    pair_id: tuplet.pair_id,
                    span: tuplet.span,
                    actual_notes: tuplet.actual_notes,
                    normal_notes: tuplet.normal_notes,
                    groups: tuplet.groups.clone(),
                });
            }
        }
        self.active_tuplets.retain(|tuplet| tuplet.remaining > 0);
        for tuplet in completed {
            self.attach_completed_tuplet(tuplet);
        }
    }

    pub(crate) fn start_tuplet(&mut self, tuplet: &TupletSyntax) {
        let p = tuplet.p.value;
        let q = tuplet.q_value();
        let r = tuplet.r_value();
        if !(2..=9).contains(&p) || q == 0 || r == 0 {
            self.diagnostics.push(invalid_tuplet_warning(tuplet.span));
            return;
        }
        let pair_id = self.next_tuplet_id;
        self.next_tuplet_id = self.next_tuplet_id.saturating_add(1);
        self.active_tuplets.push(ActiveTuplet {
            pair_id,
            span: tuplet.span,
            remaining: r,
            actual_notes: p,
            normal_notes: q,
            multiplier: Fraction::new(q, p),
            groups: Vec::new(),
        });
    }

    pub(crate) fn finish_open_tuplets_at_boundary(&mut self) {
        for tuplet in std::mem::take(&mut self.active_tuplets) {
            if tuplet.remaining > 0 {
                self.diagnostics
                    .push(tuplet_too_few_notes_warning(tuplet.span));
            }
        }
    }

    fn attach_completed_tuplet(&mut self, tuplet: CompletedTuplet) {
        let groups_len = tuplet.groups.len();
        for (index, group) in tuplet.groups.iter().enumerate() {
            let roles: &[TupletRole] = if groups_len == 1 {
                &[TupletRole::Start, TupletRole::Stop]
            } else if index == 0 {
                &[TupletRole::Start]
            } else if index + 1 == groups_len {
                &[TupletRole::Stop]
            } else {
                &[TupletRole::Continue]
            };
            // The role goes on the group's first timed event — the single note
            // or rest, or a chord's first member (never both, so no
            // double-attach). Rests count: `(3zBA` starts on the rest, `(3BAz`
            // stops on it, exactly as the abc2xml oracle notates them.
            if let Some(event_index) = group
                .iter()
                .copied()
                .find(|index| matches!(self.lowered.get(*index), Some(LoweredEvent::Timed(_))))
            {
                for role in roles {
                    self.attach_tuplet(event_index, &tuplet, *role);
                }
            }
        }
    }

    fn attach_tuplet(&mut self, event_index: usize, tuplet: &CompletedTuplet, role: TupletRole) {
        if let Some(LoweredEvent::Timed(timed)) = self.lowered.get_mut(event_index) {
            timed.attachments.tuplets.push(TupletAttachment {
                pair_id: tuplet.pair_id,
                actual_notes: tuplet.actual_notes,
                normal_notes: tuplet.normal_notes,
                role,
                span: tuplet.span,
            });
        }
    }
}

fn tuplet_too_few_notes_warning(span: Span) -> Diagnostic {
    Diagnostic::new(
        Severity::Warning,
        "abc.music.tuplet.too_few_notes",
        "Tuplet does not have enough following time-bearing note groups before the boundary",
        span,
    )
    .with_spec_reference(abc_tuplet_reference())
    .with_recovery_note(RecoveryNote::new(
        "The tuplet ratio was applied only to the available groups and was not carried across the boundary.",
    ))
}