Skip to main content

croma_core/
model.rs

1use crate::{Diagnostic, Span};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct Tune {
5    pub reference: String,
6    pub title: String,
7    pub meter: String,
8    pub key: String,
9    pub divisions: u32,
10    pub events: Vec<Event>,
11    pub voices: Vec<VoiceTimeline>,
12    pub score_directives: Vec<ScoreDirectiveModel>,
13    pub preserved_directives: Vec<PreservedDirective>,
14    pub post_tune_lyrics: Vec<TextLine>,
15    pub score: Score,
16}
17
18/// Rational duration used by semantic lowering before MusicXML divisions are
19/// selected.
20pub type Rational = Fraction;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Score {
24    pub metadata: ScoreMetadata,
25    pub parts: Vec<Part>,
26    pub diagnostics: Vec<Diagnostic>,
27    pub divisions: u32,
28    pub source_span: Span,
29    pub accidental_policy: AccidentalPolicy,
30}
31
32impl Score {
33    /// A score with no parts: what a tune that fails to lower, or an empty
34    /// MusicXML document, produces.
35    pub(crate) fn empty(span: Span, diagnostics: Vec<Diagnostic>) -> Self {
36        Self {
37            metadata: ScoreMetadata::empty(span),
38            parts: Vec::new(),
39            diagnostics,
40            divisions: 1,
41            source_span: span,
42            accidental_policy: AccidentalPolicy::abc_default(span),
43        }
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ScoreMetadata {
49    pub reference: TextLine,
50    pub title: Option<TextLine>,
51    pub composers: Vec<TextLine>,
52    pub tempo: Option<TextLine>,
53    /// Structured interpretation of [`Self::tempo`] for `<metronome>` export.
54    pub tempo_model: Option<TempoModel>,
55    pub meter: Option<MeterModel>,
56    pub key: Option<KeySignatureModel>,
57    pub directives: Vec<ScoreDirectiveModel>,
58    pub preserved_directives: Vec<PreservedDirective>,
59    pub post_tune_lyrics: Vec<TextLine>,
60    pub source_span: Span,
61}
62
63impl ScoreMetadata {
64    /// No title, tempo, meter, key or directives; a blank `X:` reference.
65    pub(crate) fn empty(span: Span) -> Self {
66        Self {
67            reference: TextLine {
68                text: String::new(),
69                span,
70            },
71            title: None,
72            composers: Vec::new(),
73            tempo: None,
74            tempo_model: None,
75            meter: None,
76            key: None,
77            directives: Vec::new(),
78            preserved_directives: Vec::new(),
79            post_tune_lyrics: Vec::new(),
80            source_span: span,
81        }
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct PreservedDirective {
87    pub name: TextLine,
88    pub value: TextLine,
89    pub span: Span,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct MeterModel {
94    pub display: String,
95    /// MusicXML `<time symbol="...">` spelling when the source carried one.
96    /// ABC's `M:C`/`M:C|` can imply the common cases; this field preserves
97    /// foreign MusicXML symbols whose beat pair is not the ABC shorthand.
98    pub time_symbol: Option<String>,
99    pub duration: Option<Rational>,
100    pub free_meter: bool,
101    /// MusicXML-origin `<time>` restatement that must survive the ABC projection
102    /// even when it repeats the already-effective meter.
103    pub preserve_restatement: bool,
104    pub source_span: Span,
105}
106
107impl MeterModel {
108    pub(crate) fn structurally_matches(&self, other: &Self) -> bool {
109        self.display == other.display && self.time_symbol == other.time_symbol
110    }
111}
112
113/// A parsed ABC `Q:` tempo field, structured for MusicXML `<metronome>` export.
114///
115/// The raw field text is preserved in [`ScoreMetadata::tempo`]; this model adds
116/// the interpreted beat unit, beats-per-minute and any leading quoted text so
117/// the writer can emit a real metronome direction rather than plain words.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct TempoModel {
120    /// Optional quoted text accompanying the tempo (e.g. `"Allegro"`).
121    pub text: Option<String>,
122    /// Beat unit and bpm, when the field carries a numeric tempo.
123    pub beat: Option<TempoBeat>,
124    /// Whether the numeric tempo was a visible metronome or playback-only
125    /// MusicXML `<sound tempo>`. ABC `Q:` creates visible metronomes; imported
126    /// playback-only tempo uses a croma carrier so the round trip does not print
127    /// a metronome that was absent from the source.
128    pub beat_role: TempoBeatRole,
129    pub source_span: Span,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum TempoBeatRole {
134    PrintedMetronome,
135    PlaybackSoundOnly,
136}
137
138/// The numeric component of a tempo: a beat-unit fraction and its bpm.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct TempoBeat {
141    /// Beat-unit fraction numerator (e.g. `1` for `1/4`, `3` for `3/8`).
142    pub beat_numerator: u32,
143    /// Beat-unit fraction denominator (e.g. `4` for `1/4`, `8` for `3/8`).
144    pub beat_denominator: u32,
145    /// Beats per minute.
146    pub bpm: u32,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct KeySignatureModel {
151    pub display: String,
152    pub fifths: i8,
153    pub explicit_accidentals: Vec<KeyAccidentalModel>,
154    /// MusicXML-origin `<key>` restatement that must survive the ABC projection
155    /// even when it repeats the already-effective key (mirrors
156    /// [`MeterModel::preserve_restatement`]).
157    pub preserve_restatement: bool,
158    pub source_span: Span,
159}
160
161impl KeySignatureModel {
162    pub(crate) fn structurally_matches(&self, other: &Self) -> bool {
163        self.fifths == other.fifths && self.explicit_accidentals == other.explicit_accidentals
164    }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct ClefChangeModel {
169    pub clef: TextLine,
170    pub source_span: Span,
171    pub musicxml_cursor_pre_backup: Option<Fraction>,
172    pub musicxml_cursor_back: Option<Fraction>,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct KeyAccidentalModel {
177    pub step: char,
178    pub accidental: Accidental,
179    pub source_span: Span,
180}
181
182/// ABC accidentals are source-significant. Croma records the written accidental
183/// exactly when present, applies it to following matching pitches in the same
184/// measure, and clears that measure-local state at each barline. When the key
185/// signature or syntax leaves behavior ambiguous, diagnostics keep the policy
186/// decision source-spanned instead of silently changing notation.
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct AccidentalPolicy {
189    pub preserve_explicit_accidentals: bool,
190    pub reset_at_barlines: bool,
191    pub scope: AccidentalScope,
192    pub source_span: Span,
193}
194
195impl AccidentalPolicy {
196    /// ABC's rule: explicit accidentals are kept and last to the end of the
197    /// bar, per pitch and octave.
198    pub(crate) fn abc_default(span: Span) -> Self {
199        Self {
200            preserve_explicit_accidentals: true,
201            reset_at_barlines: true,
202            scope: AccidentalScope::PitchAndOctave,
203            source_span: span,
204        }
205    }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum AccidentalScope {
210    PitchAndOctave,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct Part {
215    pub id: PartId,
216    pub name: Option<TextLine>,
217    pub instruments: Vec<MusicXmlPartInstrumentModel>,
218    pub staves: Vec<Staff>,
219    pub voices: Vec<Voice>,
220    pub source_span: Span,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct PartId {
225    pub value: String,
226    pub span: Span,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct Staff {
231    pub id: StaffId,
232    pub voices: Vec<VoiceId>,
233    pub source_span: Span,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub struct StaffId {
238    pub value: u32,
239    pub span: Span,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct Voice {
244    pub id: VoiceId,
245    pub staff: StaffId,
246    /// MusicXML-origin part/voice initial key signature when it differs from the
247    /// score-level ABC `K:` header. ABC-origin scores keep this empty.
248    pub initial_key: Option<KeySignatureModel>,
249    /// MusicXML-origin part/voice initial meter spelling when it differs from
250    /// the score-level ABC `M:` header. Used for per-part `symbol` differences
251    /// such as header `M:C|` but a later part's initial numeric `2/2`.
252    pub initial_meter: Option<MeterModel>,
253    pub initial_properties: VoicePropertiesModel,
254    pub properties: VoicePropertiesModel,
255    pub measures: Vec<Measure>,
256    pub events: Vec<TimedEvent>,
257    /// Score-level MIDI instrument projected from this voice's `%%MIDI program`
258    /// / `%%MIDI channel` directives (see [`MidiInstrumentModel`]). `None` when
259    /// the voice declared no score-translatable MIDI instrument.
260    pub midi_instrument: Option<MidiInstrumentModel>,
261    /// Chromatic transposition (semitones) projected from this voice's
262    /// `%%MIDI transpose <n>` directive, emitted as MusicXML
263    /// `<attributes><transpose><chromatic>`. Distinct from the ABC `transpose=`
264    /// voice property (which is carried in [`VoicePropertiesModel`] and takes
265    /// precedence when both are present). `None` when no `%%MIDI transpose` was
266    /// declared.
267    pub midi_transpose: Option<i16>,
268    pub source_span: Span,
269}
270
271/// A score-level MIDI instrument projected from a voice's `%%MIDI program` /
272/// `channel` / `control` / `midi-unpitched` directives.
273///
274/// `%%MIDI` is an abc2midi convention, not part of ABC 2.1; the score-meaningful
275/// fields (instrument identity, channel, CC7 volume, CC10 pan, unpitched
276/// percussion key) are
277/// forward-translated to MusicXML `<score-instrument>` / `<midi-instrument>`
278/// while the raw directive text stays in [`ScoreMetadata::preserved_directives`]
279/// for round-trip / formatter use.
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub struct MidiInstrumentModel {
282    /// General MIDI program number exactly as written in the directive
283    /// (0-based, abc2midi/GM convention). The MusicXML `<midi-program>` is this
284    /// value plus one (1-based). `None` when no program was declared.
285    pub program: Option<u8>,
286    /// MIDI channel (1-16) from the two-integer `program <chan> <prog>` form or
287    /// a sibling `%%MIDI channel` directive in the same voice. `None` when no
288    /// channel was declared.
289    pub channel: Option<u8>,
290    /// MIDI CC7 (channel volume) value 0-127 from `%%MIDI control 7 <n>`,
291    /// rendered as `<midi-instrument><volume>` (`n / 1.27`). `None` when unset.
292    pub volume_cc: Option<u8>,
293    /// MIDI CC10 (pan) value 0-127 from `%%MIDI control 10 <n>`, rendered as
294    /// `<midi-instrument><pan>` (`n / 127 * 180 - 90`). `None` when unset.
295    pub pan_cc: Option<u8>,
296    /// MusicXML `<midi-unpitched>` value for unpitched percussion playback
297    /// (1-128). `None` when the instrument is pitched or unspecified.
298    pub midi_unpitched: Option<u8>,
299    /// Source span of the directive that last set this instrument.
300    pub span: Span,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct MusicXmlPartInstrumentModel {
305    pub id: String,
306    pub name: Option<TextLine>,
307    pub midi: Option<MidiInstrumentModel>,
308    pub span: Span,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct MusicXmlInstrumentRef {
313    pub id: String,
314    pub span: Span,
315}
316
317impl MidiInstrumentModel {
318    /// True when this projection carries at least one field worth emitting as a
319    /// MusicXML `<score-instrument>` / `<midi-instrument>`.
320    pub fn has_content(&self) -> bool {
321        self.program.is_some()
322            || self.channel.is_some()
323            || self.volume_cc.is_some()
324            || self.pan_cc.is_some()
325            || self.midi_unpitched.is_some()
326    }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub struct MeasureId {
331    pub index: u32,
332    pub number: u32,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct Measure {
337    pub id: MeasureId,
338    /// MusicXML-origin `<measure number>` display label. ABC-origin scores use
339    /// the numeric [`MeasureId::number`]; imported MusicXML can use labels such
340    /// as `0`, repeated movement-local numbers, or `X1`.
341    pub display_number: Option<String>,
342    pub source_span: Span,
343    pub expected_duration: Option<Rational>,
344    pub actual_duration: Rational,
345    /// Display hint for the first measure of an expanded ABC `Zn` rest run.
346    /// The measures still exist individually; this only requests the compact
347    /// MusicXML multi-rest glyph.
348    pub multiple_rest: Option<u32>,
349    pub pickup: bool,
350    pub complete: bool,
351    pub barlines: Vec<MeasureBarline>,
352    pub repeat_endings: Vec<RepeatEndingModel>,
353    /// MusicXML-origin explicit volta closes (`<ending type="stop|discontinue">`)
354    /// that cannot be inferred from ABC barline syntax, such as a close on an
355    /// implicit regular right barline. ABC-origin scores keep this empty.
356    pub repeat_ending_closes: Vec<RepeatEndingCloseModel>,
357    pub overlays: Vec<OverlaySegment>,
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361pub struct MeasureBarline {
362    pub kind: BarlineKind,
363    pub span: Span,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct RepeatEndingModel {
368    pub span: Span,
369    pub endings: Vec<RepeatEndingPartModel>,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq)]
373pub struct RepeatEndingCloseModel {
374    pub span: Span,
375    pub close_type: RepeatEndingCloseType,
376    pub location: RepeatEndingCloseLocation,
377    pub endings: Vec<RepeatEndingPartModel>,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
381pub enum RepeatEndingCloseType {
382    Stop,
383    Discontinue,
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
387pub enum RepeatEndingCloseLocation {
388    Left,
389    Right,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub enum RepeatEndingPartModel {
394    Single(u32),
395    Range { start: u32, end: u32 },
396    Text(String),
397}
398
399#[derive(Debug, Clone, PartialEq, Eq)]
400pub struct TimedEvent {
401    pub measure: MeasureId,
402    pub onset: Rational,
403    pub duration: Rational,
404    pub source: Span,
405    pub kind: TimedEventKind,
406    pub attachments: EventAttachments,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub enum TimedEventKind {
411    Note(NoteEvent),
412    Chord(ChordEvent),
413    Rest(RestEvent),
414    Spacer,
415    Barline(MeasureBarline),
416    RepeatEnding(RepeatEndingModel),
417    RepeatEndingClose(RepeatEndingCloseModel),
418    /// A mid-tune key change (`[K:..]` or a body `K:` line). Zero duration;
419    /// the new key's alters are already baked into later pitches — the event
420    /// records WHERE the change was written so exporters can reproduce it.
421    KeyChange(KeySignatureModel),
422    /// A mid-tune meter change (`[M:..]` or a body `M:` line). Zero duration.
423    MeterChange(MeterModel),
424    /// A mid-tune clef change carried by `[K:..]` or a body `K:` line. Zero
425    /// duration; following pitches are already lowered with the new voice
426    /// octave shift.
427    ClefChange(ClefChangeModel),
428    /// A mid-tune tempo change (`[Q:..]` or a body `Q:` line). Zero duration.
429    TempoChange(TempoModel),
430    /// A body/inline `P:` section label (ABC 2.1 §4.3), e.g. `P:A` or `[P:B]`.
431    /// Zero duration; emitted as a MusicXML `<rehearsal>`. The header `P:ABAB`
432    /// play-order macro is a different construct and never produces this.
433    SectionLabel(String),
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
437pub struct NoteEvent {
438    pub pitch: Pitch,
439    pub written_accidental: Option<AccidentalMark>,
440    pub chord_member: bool,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct ChordEvent {
445    pub members: Vec<ChordMemberEvent>,
446    pub source_span: Span,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq)]
450pub struct ChordMemberEvent {
451    pub pitch: Pitch,
452    pub duration: Rational,
453    pub written_accidental: Option<AccidentalMark>,
454    pub source_span: Span,
455    pub attachments: EventAttachments,
456}
457
458#[derive(Debug, Clone, PartialEq, Eq)]
459pub struct RestEvent {
460    pub visibility: RestVisibility,
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub struct Pitch {
465    pub step: char,
466    pub alter: i8,
467    pub octave: i8,
468    pub spelling_source: Span,
469}
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub struct AccidentalMark {
473    pub kind: Accidental,
474    pub explicit: bool,
475    pub courtesy: bool,
476    pub source: Span,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Default)]
480pub struct EventAttachments {
481    pub grace_groups: Vec<GraceGroupAttachment>,
482    pub after_grace_groups: Vec<GraceGroupAttachment>,
483    pub chord_symbols: Vec<TextAttachment>,
484    pub annotations: Vec<TextAttachment>,
485    pub decorations: Vec<DecorationAttachment>,
486    pub instrument: Option<MusicXmlInstrumentRef>,
487    pub lyrics: Vec<AlignedLyric>,
488    pub lyric_same_note_extends: Vec<u32>,
489    pub lyric_same_note_duplicates: Vec<AlignedLyric>,
490    pub musicxml_forward: bool,
491    pub musicxml_sequence_backup: Option<Fraction>,
492    pub symbols: Vec<AlignedSymbol>,
493    pub ties: Vec<TieAttachment>,
494    pub slurs: Vec<SlurAttachment>,
495    pub tuplets: Vec<TupletAttachment>,
496}
497
498impl EventAttachments {
499    pub fn is_empty(&self) -> bool {
500        self.grace_groups.is_empty()
501            && self.after_grace_groups.is_empty()
502            && self.chord_symbols.is_empty()
503            && self.annotations.is_empty()
504            && self.decorations.is_empty()
505            && self.instrument.is_none()
506            && self.lyrics.is_empty()
507            && self.lyric_same_note_extends.is_empty()
508            && self.lyric_same_note_duplicates.is_empty()
509            && !self.musicxml_forward
510            && self.musicxml_sequence_backup.is_none()
511            && self.symbols.is_empty()
512            && self.ties.is_empty()
513            && self.slurs.is_empty()
514            && self.tuplets.is_empty()
515    }
516
517    pub(crate) fn extend(&mut self, other: EventAttachments) {
518        self.grace_groups.extend(other.grace_groups);
519        self.after_grace_groups.extend(other.after_grace_groups);
520        self.chord_symbols.extend(other.chord_symbols);
521        self.annotations.extend(other.annotations);
522        self.decorations.extend(other.decorations);
523        if self.instrument.is_none() {
524            self.instrument = other.instrument;
525        }
526        self.lyrics.extend(other.lyrics);
527        self.lyric_same_note_extends
528            .extend(other.lyric_same_note_extends);
529        self.lyric_same_note_duplicates
530            .extend(other.lyric_same_note_duplicates);
531        self.musicxml_forward |= other.musicxml_forward;
532        if self.musicxml_sequence_backup.is_none() {
533            self.musicxml_sequence_backup = other.musicxml_sequence_backup;
534        }
535        self.symbols.extend(other.symbols);
536        self.ties.extend(other.ties);
537        self.slurs.extend(other.slurs);
538        self.tuplets.extend(other.tuplets);
539    }
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
543pub struct GraceGroupAttachment {
544    pub span: Span,
545    pub slash: Option<Span>,
546    pub note_count: u32,
547    pub events: Vec<GraceEvent>,
548    /// Slurs that bind to the FIRST grace note of this group, e.g. the `(` in
549    /// `({grace}note)` opens before the grace group, so the slur starts on the
550    /// grace note rather than the following main note (ABC 2.1 §4.11 + §4.20).
551    pub slurs: Vec<SlurAttachment>,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq)]
555pub struct GraceEvent {
556    pub source_span: Span,
557    pub kind: GraceEventKind,
558    pub slurs: Vec<SlurAttachment>,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq)]
562pub enum GraceEventKind {
563    Note(GraceNoteEvent),
564    Rest(RestEvent),
565    Chord(Vec<GraceNoteEvent>),
566}
567
568#[derive(Debug, Clone, PartialEq, Eq)]
569pub struct GraceNoteEvent {
570    pub pitch: Pitch,
571    pub written_accidental: Option<AccidentalMark>,
572    pub decorations: Vec<DecorationAttachment>,
573    /// Written length modifier of the grace note relative to the grace base
574    /// unit (`/` -> 1/2, `2` -> 2, etc.; `1` when no modifier is written).
575    pub length_multiplier: Fraction,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq)]
579pub struct TextAttachment {
580    pub text: String,
581    pub span: Span,
582    pub placement: Option<AnnotationPlacementModel>,
583    pub musicxml_harmony_text: HarmonyKindText,
584}
585
586/// Provenance of a chord symbol's MusicXML `<kind text="...">` attribute, kept so
587/// a foreign `<harmony>` re-exports byte-identically. The three states are
588/// distinct in MusicXML and must not be conflated: a textless `<kind>` (no `text=`
589/// attribute) is not the same as `<kind text="">` (an explicit empty attribute).
590#[derive(Debug, Clone, Default, PartialEq, Eq)]
591pub enum HarmonyKindText {
592    /// An ABC-native chord with no MusicXML provenance. The writer synthesises the
593    /// `text=` value from the chord's own ABC string.
594    #[default]
595    AbcNative,
596    /// A foreign `<kind>` with NO `text=` attribute (functional / "textless"). The
597    /// writer emits a bare `<kind>`.
598    Textless,
599    /// A foreign `<kind text="V">`; `V` may be empty. The writer emits it verbatim.
600    Text(String),
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub enum AnnotationPlacementModel {
605    Above,
606    Below,
607    Left,
608    Right,
609    Free,
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
613pub struct DecorationAttachment {
614    pub name: String,
615    pub span: Span,
616    pub source_kind: DecorationSourceKind,
617    pub placement: Option<AnnotationPlacementModel>,
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq)]
621pub enum DecorationSourceKind {
622    Named,
623    LegacyNamed,
624    Shorthand,
625    UserDefined,
626}
627
628#[derive(Debug, Clone, Copy, PartialEq, Eq)]
629pub struct TieAttachment {
630    pub pair_id: u32,
631    pub role: TieRole,
632    pub span: Span,
633    pub dotted: bool,
634}
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq)]
637pub enum TieRole {
638    Start,
639    Stop,
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq)]
643pub struct SlurAttachment {
644    pub pair_id: u32,
645    pub role: SlurRole,
646    pub span: Span,
647    pub dotted: bool,
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq)]
651pub enum SlurRole {
652    Start,
653    Stop,
654}
655
656/// Reserved low bound for the `pair_id` of a slur reconstructed from a
657/// cross-voice `[I:croma-xvoice-slur]` carrier (a slur whose start and stop live
658/// in different voices, which ABC `(`/`)` cannot span). Lowering allocates such
659/// `pair_id`s from this range so they never coincide with the small per-voice
660/// slur ids; the MusicXML writer relies on that disjointness to give a
661/// cross-voice slur's two ends a matching `<slur number>` even though they sit
662/// under different voice keys. See `lower` and `docs/carriers.md`.
663pub(crate) const XVOICE_SLUR_PAIR_ID_BASE: u32 = 1_000_000;
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
666pub struct TupletAttachment {
667    pub pair_id: u32,
668    pub actual_notes: u32,
669    pub normal_notes: u32,
670    pub role: TupletRole,
671    pub span: Span,
672}
673
674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
675pub enum TupletRole {
676    Start,
677    Continue,
678    Stop,
679}
680
681#[derive(Debug, Clone, PartialEq, Eq)]
682pub enum Event {
683    Note {
684        step: char,
685        octave: i8,
686        accidental: Option<Accidental>,
687        chord: bool,
688        duration: u32,
689        span: Span,
690    },
691    Rest {
692        visibility: RestVisibility,
693        duration: u32,
694        span: Span,
695    },
696    Spacer {
697        span: Span,
698    },
699    Barline {
700        kind: BarlineKind,
701        span: Span,
702    },
703}
704
705impl Event {
706    pub fn span(&self) -> Span {
707        match self {
708            Self::Note { span, .. }
709            | Self::Rest { span, .. }
710            | Self::Spacer { span }
711            | Self::Barline { span, .. } => *span,
712        }
713    }
714
715    pub(crate) fn is_time_bearing(&self) -> bool {
716        matches!(self, Self::Note { .. } | Self::Rest { .. })
717    }
718}
719
720#[derive(Debug, Clone, PartialEq, Eq)]
721pub struct VoiceTimeline {
722    pub id: VoiceId,
723    pub initial_key: Option<KeySignatureModel>,
724    pub initial_meter: Option<MeterModel>,
725    pub initial_properties: VoicePropertiesModel,
726    pub properties: VoicePropertiesModel,
727    pub measures: Vec<VoiceMeasureTimeline>,
728    /// MusicXML-origin part-list instruments carried through ABC as croma
729    /// `%%MIDI instrument-def` directives.
730    pub musicxml_instruments: Vec<MusicXmlPartInstrumentModel>,
731    /// Score-level MIDI instrument projected from this voice's `%%MIDI` program
732    /// / channel directives, carried through to the semantic [`Voice`].
733    pub midi_instrument: Option<MidiInstrumentModel>,
734    /// Chromatic transposition (semitones) projected from this voice's
735    /// `%%MIDI transpose` directive, carried through to the semantic [`Voice`].
736    pub midi_transpose: Option<i16>,
737    pub source_span: Span,
738}
739
740#[derive(Debug, Clone, PartialEq, Eq)]
741pub struct VoiceId {
742    pub value: String,
743    pub span: Span,
744}
745
746#[derive(Debug, Clone, PartialEq, Eq, Default)]
747pub struct VoicePropertiesModel {
748    pub name: Option<TextLine>,
749    pub nm: Option<TextLine>,
750    pub subname: Option<TextLine>,
751    pub snm: Option<TextLine>,
752    pub clef: Option<TextLine>,
753    pub stem: Option<StemDirectionModel>,
754    pub octave: Option<TextLine>,
755    pub transpose: Option<TextLine>,
756    pub middle: Option<TextLine>,
757}
758
759#[derive(Debug, Clone, Copy, PartialEq, Eq)]
760pub enum StemDirectionModel {
761    Up,
762    Down,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq)]
766pub struct VoiceMeasureTimeline {
767    pub index: u32,
768    pub display_number: Option<String>,
769    pub span: Span,
770    pub events: Vec<VoiceTimedEvent>,
771    pub overlays: Vec<OverlaySegment>,
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
775pub struct VoiceTimedEvent {
776    pub onset: Fraction,
777    pub duration: Fraction,
778    pub span: Span,
779    pub line_index: usize,
780    pub source_order: u32,
781    pub alignable: bool,
782    pub kind: TimelineEventKind,
783    pub attachments: EventAttachments,
784    pub lyrics: Vec<AlignedLyric>,
785    pub symbols: Vec<AlignedSymbol>,
786}
787
788#[derive(Debug, Clone, PartialEq, Eq)]
789pub enum TimelineEventKind {
790    Note {
791        step: char,
792        octave: i8,
793        accidental: Option<Accidental>,
794        effective_accidental: Option<Accidental>,
795        accidental_source: Option<Span>,
796        chord: bool,
797    },
798    Rest {
799        visibility: RestVisibility,
800        multiple_rest: Option<u32>,
801    },
802    Spacer,
803    Barline {
804        kind: BarlineKind,
805    },
806    VariantEnding {
807        endings: Vec<RepeatEndingPartModel>,
808    },
809    VariantEndingClose {
810        close_type: RepeatEndingCloseType,
811        location: RepeatEndingCloseLocation,
812        endings: Vec<RepeatEndingPartModel>,
813    },
814    KeyChange(KeySignatureModel),
815    MeterChange(MeterModel),
816    ClefChange(ClefChangeModel),
817    TempoChange(TempoModel),
818    /// A body/inline `P:` section label, mirrored from [`TimedEventKind`] so the
819    /// per-voice alignment timeline carries it (zero duration, non-alignable).
820    SectionLabel(String),
821}
822
823#[derive(Debug, Clone, PartialEq, Eq)]
824pub struct OverlaySegment {
825    pub id: VoiceId,
826    pub span: Span,
827    pub measure_index: u32,
828    pub expected_duration: Fraction,
829    pub actual_duration: Fraction,
830    pub events: Vec<VoiceTimedEvent>,
831}
832
833#[derive(Debug, Clone, PartialEq, Eq)]
834pub struct AlignedLyric {
835    pub verse: u32,
836    pub text: String,
837    pub span: Span,
838    pub control: LyricControl,
839    /// Foreign MusicXML can put `<text>` and `<extend/>` in the same `<lyric>`
840    /// element. ABC `w:` cannot spell that directly, so croma preserves it with
841    /// a private inline carrier when projecting MusicXML to ABC.
842    pub same_note_extend: bool,
843}
844
845#[derive(Debug, Clone, Copy, PartialEq, Eq)]
846pub enum LyricControl {
847    Syllable,
848    Hyphen,
849    Extender,
850    Skip,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq)]
854pub struct AlignedSymbol {
855    pub layer: u32,
856    pub text: String,
857    pub span: Span,
858    pub kind: AlignedSymbolKind,
859}
860
861#[derive(Debug, Clone, Copy, PartialEq, Eq)]
862pub enum AlignedSymbolKind {
863    Decoration,
864    ChordSymbol,
865    Annotation,
866    Raw,
867}
868
869#[derive(Debug, Clone, PartialEq, Eq)]
870pub struct ScoreDirectiveModel {
871    pub span: Span,
872    pub value: TextLine,
873    pub tokens: Vec<ScoreDirectiveTokenModel>,
874}
875
876#[derive(Debug, Clone, PartialEq, Eq)]
877pub struct ScoreDirectiveTokenModel {
878    pub span: Span,
879    pub kind: ScoreDirectiveTokenKindModel,
880}
881
882#[derive(Debug, Clone, PartialEq, Eq)]
883pub enum ScoreDirectiveTokenKindModel {
884    Voice(String),
885    GroupStart(char),
886    GroupEnd(char),
887    StaffSeparator,
888    MeasureSeparator,
889    FloatingVoiceMarker,
890}
891
892#[derive(Debug, Clone, PartialEq, Eq)]
893pub struct TextLine {
894    pub text: String,
895    pub span: Span,
896}
897
898#[derive(Debug, Clone, Copy, PartialEq, Eq)]
899pub enum RestVisibility {
900    Visible,
901    Invisible,
902}
903
904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
905pub enum Accidental {
906    DoubleFlat,
907    Flat,
908    Natural,
909    Sharp,
910    DoubleSharp,
911}
912
913impl Accidental {
914    /// The accidental for a semitone alteration in -2..=2, if any.
915    pub(crate) fn from_alter(alter: i8) -> Option<Self> {
916        match alter {
917            -2 => Some(Self::DoubleFlat),
918            -1 => Some(Self::Flat),
919            0 => Some(Self::Natural),
920            1 => Some(Self::Sharp),
921            2 => Some(Self::DoubleSharp),
922            _ => None,
923        }
924    }
925
926    /// The ABC sign written before a note or a key step: `__ _ = ^ ^^`.
927    pub(crate) fn abc_sign(self) -> &'static str {
928        match self {
929            Self::DoubleFlat => "__",
930            Self::Flat => "_",
931            Self::Natural => "=",
932            Self::Sharp => "^",
933            Self::DoubleSharp => "^^",
934        }
935    }
936
937    pub(crate) fn alter(self) -> i8 {
938        match self {
939            Self::DoubleFlat => -2,
940            Self::Flat => -1,
941            Self::Natural => 0,
942            Self::Sharp => 1,
943            Self::DoubleSharp => 2,
944        }
945    }
946
947    pub(crate) fn musicxml_name(self) -> &'static str {
948        match self {
949            Self::DoubleFlat => "flat-flat",
950            Self::Flat => "flat",
951            Self::Natural => "natural",
952            Self::Sharp => "sharp",
953            Self::DoubleSharp => "double-sharp",
954        }
955    }
956}
957
958#[derive(Debug, Clone, Copy, PartialEq, Eq)]
959pub enum BarlineKind {
960    Regular,
961    Double,
962    Final,
963    Initial,
964    RepeatStart,
965    RepeatEnd,
966    RepeatBoth,
967    Dotted,
968    Dashed,
969    Invisible,
970    Liberal,
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
974pub(crate) struct LoweredEventAtom {
975    pub kind: LoweredEventAtomKind,
976    pub duration: Fraction,
977}
978
979#[derive(Debug, Clone, Copy, PartialEq, Eq)]
980pub(crate) enum LoweredEventAtomKind {
981    Note {
982        step: char,
983        octave: i8,
984        accidental: Option<Accidental>,
985        effective_accidental: Option<Accidental>,
986        accidental_source: Option<Span>,
987        chord: bool,
988        span: Span,
989    },
990    Rest {
991        visibility: RestVisibility,
992        multiple_rest: Option<u32>,
993        span: Span,
994    },
995    Spacer {
996        span: Span,
997    },
998}
999
1000impl LoweredEventAtom {
1001    pub(crate) fn span(&self) -> Span {
1002        match self.kind {
1003            LoweredEventAtomKind::Note { span, .. }
1004            | LoweredEventAtomKind::Rest { span, .. }
1005            | LoweredEventAtomKind::Spacer { span } => span,
1006        }
1007    }
1008
1009    pub(crate) fn into_event(self, divisions: u32) -> Event {
1010        let duration = self.duration.to_divisions(divisions);
1011        match self.kind {
1012            LoweredEventAtomKind::Note {
1013                step,
1014                octave,
1015                accidental,
1016                effective_accidental: _,
1017                accidental_source: _,
1018                chord,
1019                span,
1020            } => Event::Note {
1021                step,
1022                octave,
1023                accidental,
1024                chord,
1025                duration,
1026                span,
1027            },
1028            LoweredEventAtomKind::Rest {
1029                visibility, span, ..
1030            } => Event::Rest {
1031                visibility,
1032                duration,
1033                span,
1034            },
1035            LoweredEventAtomKind::Spacer { span } => Event::Spacer { span },
1036        }
1037    }
1038}
1039
1040#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1041pub struct Fraction {
1042    pub numerator: u32,
1043    pub denominator: u32,
1044}
1045
1046impl Fraction {
1047    pub fn zero() -> Self {
1048        Self {
1049            numerator: 0,
1050            denominator: 1,
1051        }
1052    }
1053
1054    pub fn new(numerator: u32, denominator: u32) -> Self {
1055        let denominator = denominator.max(1);
1056        let gcd = gcd(numerator, denominator);
1057        Self {
1058            numerator: numerator / gcd,
1059            denominator: denominator / gcd,
1060        }
1061    }
1062
1063    pub fn one() -> Self {
1064        Self {
1065            numerator: 1,
1066            denominator: 1,
1067        }
1068    }
1069
1070    /// The largest representable duration; what the saturating operations clamp to.
1071    pub(crate) const MAX: Self = Self {
1072        numerator: u32::MAX,
1073        denominator: 1,
1074    };
1075
1076    /// The exact product, or `None` when the reduced result does not fit u32/u32.
1077    pub(crate) fn checked_mul(self, other: Self) -> Option<Self> {
1078        Self::reduced_wide(
1079            u128::from(self.numerator) * u128::from(other.numerator),
1080            u128::from(self.denominator) * u128::from(other.denominator),
1081        )
1082    }
1083
1084    /// The exact sum, or `None` when the reduced result does not fit u32/u32.
1085    pub(crate) fn checked_add(self, other: Self) -> Option<Self> {
1086        Self::reduced_wide(
1087            u128::from(self.numerator) * u128::from(other.denominator)
1088                + u128::from(other.numerator) * u128::from(self.denominator),
1089            u128::from(self.denominator) * u128::from(other.denominator),
1090        )
1091    }
1092
1093    /// For running totals (onsets, cursors): the exact sum, or [`Fraction::MAX`]
1094    /// when it does not fit. Lowering diagnoses an unrepresentable duration where
1095    /// it is formed, so a total that still overflows only clamps.
1096    pub(crate) fn saturating_add(self, other: Self) -> Self {
1097        self.checked_add(other).unwrap_or(Self::MAX)
1098    }
1099
1100    fn reduced_wide(numerator: u128, denominator: u128) -> Option<Self> {
1101        let denominator = denominator.max(1);
1102        let divisor = gcd_u128(numerator, denominator);
1103        Some(Self {
1104            numerator: u32::try_from(numerator / divisor).ok()?,
1105            denominator: u32::try_from(denominator / divisor).ok()?,
1106        })
1107    }
1108
1109    /// This value with `dots` augmentation dots, each adding half the previous
1110    /// increment.
1111    pub(crate) fn dotted(self, dots: usize) -> Self {
1112        let mut duration = self;
1113        let mut dot = self;
1114        for _ in 0..dots {
1115            dot = Self::new(dot.numerator, dot.denominator.saturating_mul(2));
1116            duration = duration.saturating_add(dot);
1117        }
1118        duration
1119    }
1120
1121    pub(crate) fn less_than(self, other: Self) -> bool {
1122        u64::from(self.numerator) * u64::from(other.denominator)
1123            < u64::from(other.numerator) * u64::from(self.denominator)
1124    }
1125
1126    pub(crate) fn divisions_requirement(self) -> u32 {
1127        let denominator = u64::from(self.denominator);
1128        let scaled_numerator = u64::from(self.numerator) * 4;
1129        let gcd = gcd_u64(denominator, scaled_numerator);
1130        u32::try_from(denominator / gcd).unwrap_or(u32::MAX)
1131    }
1132
1133    pub(crate) fn to_divisions(self, divisions: u32) -> u32 {
1134        // u128: `u32 * 4 * u32` can exceed u64. Results past u32 clamp.
1135        let numerator = u128::from(self.numerator) * 4 * u128::from(divisions);
1136        let denominator = u128::from(self.denominator.max(1));
1137        let value = numerator / denominator;
1138        u32::try_from(value.max(1)).unwrap_or(u32::MAX)
1139    }
1140}
1141
1142/// Key-signature order of sharps and flats (ABC 2.1 §3.1.14).
1143pub(crate) const SHARP_ORDER: [char; 7] = ['F', 'C', 'G', 'D', 'A', 'E', 'B'];
1144pub(crate) const FLAT_ORDER: [char; 7] = ['B', 'E', 'A', 'D', 'G', 'C', 'F'];
1145
1146/// The least common multiple, or `None` when it does not fit u32.
1147pub(crate) fn checked_lcm(left: u32, right: u32) -> Option<u32> {
1148    if left == 0 || right == 0 {
1149        return Some(left.max(right).max(1));
1150    }
1151    (left / gcd(left, right)).checked_mul(right)
1152}
1153
1154pub(crate) fn gcd(mut left: u32, mut right: u32) -> u32 {
1155    while right != 0 {
1156        let remainder = left % right;
1157        left = right;
1158        right = remainder;
1159    }
1160    left.max(1)
1161}
1162
1163pub(crate) fn gcd_u64(mut left: u64, mut right: u64) -> u64 {
1164    while right != 0 {
1165        let remainder = left % right;
1166        left = right;
1167        right = remainder;
1168    }
1169    left.max(1)
1170}
1171
1172/// The ratio `actual:normal` scaled by `factor_actual:factor_normal`, kept
1173/// unreduced when it fits u32 (tuplet ratios are written as given) and reduced
1174/// only when it must be; `None` when even the reduced ratio does not fit.
1175pub(crate) fn checked_ratio_product(
1176    actual: u32,
1177    normal: u32,
1178    factor_actual: u32,
1179    factor_normal: u32,
1180) -> Option<(u32, u32)> {
1181    let actual = u64::from(actual) * u64::from(factor_actual);
1182    let normal = u64::from(normal) * u64::from(factor_normal);
1183    ratio_to_u32(actual, normal)
1184}
1185
1186/// `numerator:denominator` as u32s: unchanged when both fit, else reduced, else
1187/// `None`.
1188pub(crate) fn ratio_to_u32(numerator: u64, denominator: u64) -> Option<(u32, u32)> {
1189    if numerator <= u64::from(u32::MAX) && denominator <= u64::from(u32::MAX) {
1190        return Some((numerator as u32, denominator as u32));
1191    }
1192    let gcd = gcd_u64(numerator, denominator);
1193    let numerator = numerator / gcd;
1194    let denominator = denominator / gcd;
1195    (numerator <= u64::from(u32::MAX) && denominator <= u64::from(u32::MAX))
1196        .then_some((numerator as u32, denominator as u32))
1197}
1198
1199fn gcd_u128(mut left: u128, mut right: u128) -> u128 {
1200    while right != 0 {
1201        let remainder = left % right;
1202        left = right;
1203        right = remainder;
1204    }
1205    left.max(1)
1206}
1207
1208#[cfg(test)]
1209#[path = "model_tests.rs"]
1210mod tests;