pub(crate) mod accidental;
mod align;
pub(crate) mod diagnostics;
mod semantic;
mod tempo;
pub(crate) mod tie;
pub(crate) mod timeline;
pub(crate) mod tuplet;
pub(crate) mod voice;
pub(crate) use crate::lower::diagnostics::*;
pub(crate) use crate::lower::voice::{
ActiveTuplet, CompletedTuplet, LoweredEvent, LoweringState, PendingTie, is_note_atom,
lowered_timed_note, note_signature,
};
use crate::diagnostic::{Diagnostic, Span};
use crate::lower::accidental::accidental_from_field_sign;
use crate::lower::align::{align_lyrics, align_symbols};
use crate::lower::semantic::semantic_voice_from_timeline;
use crate::lower::tempo::parse_tempo_model;
use std::collections::BTreeMap;
use crate::lower::timeline::build_voice_timeline;
use crate::model::{
AccidentalPolicy, AccidentalScope, BarlineKind, ClefChangeModel, Event, EventAttachments,
Fraction, KeyAccidentalModel, KeySignatureModel, LoweredEventAtom, LoweredEventAtomKind,
MeterModel, MidiInstrumentModel, Part, PartId, PreservedDirective, RestVisibility, Score,
ScoreDirectiveModel, ScoreDirectiveTokenKindModel, ScoreDirectiveTokenModel, ScoreMetadata,
Staff, StaffId, StemDirectionModel, TextLine, TimelineEventKind, VoiceId, VoicePropertiesModel,
VoiceTimeline, lcm,
};
use crate::parse::ParseReport;
use crate::parse::field::{
FieldState, KeyAccidental, KeyMode, KeySignature, KeyTonicAccidental, Meter, MeterKind,
Spanned, StemDirection, UnitNoteLength, VoiceDefinition, VoiceProperties,
};
use crate::syntax::tune::ScoreLineBreak;
use crate::syntax::{
AttachmentBundle, BarlineSyntax, InlineFieldSyntax, LyricLineSyntax, MusicFieldLine,
MusicFieldLineKind, MusicItem, MusicLine, ParsedTuneMusic, PreservedDirectiveSyntax,
ScoreDirectiveSyntax, SymbolLineSyntax,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoweredMusic {
pub events: Vec<Event>,
pub divisions: u32,
pub voices: Vec<VoiceTimeline>,
pub score_directives: Vec<ScoreDirectiveModel>,
pub preserved_directives: Vec<PreservedDirective>,
pub post_tune_lyrics: Vec<TextLine>,
}
pub(crate) fn lower_tune_music(
tune_music: &ParsedTuneMusic,
field_state: &FieldState,
) -> ParseReport<LoweredMusic> {
let unit = field_state.unit_note_length_fraction();
let mut lowering = MultiVoiceLowering::new(unit, field_state, tune_music.span);
let mut entries = Vec::new();
for (index, line) in tune_music.lines.iter().enumerate() {
entries.push((line.line_index, 1usize, index));
}
for (index, field) in tune_music.body_fields.iter().enumerate() {
entries.push((field.line_index, 0usize, index));
}
entries.sort_unstable();
for (_line_index, kind, index) in entries {
if kind == 0 {
lowering.apply_field(&tune_music.body_fields[index]);
} else {
lowering.apply_music_line(&tune_music.lines[index]);
}
}
let lyric_lines = lowering.lyric_lines.clone();
let symbol_lines = lowering.symbol_lines.clone();
let mut diagnostics = lowering.finish_open_constructs();
let all_lowered = lowering
.voices
.iter()
.flat_map(|voice| voice.lowered.iter())
.collect::<Vec<_>>();
let divisions = all_lowered.iter().fold(8, |divisions, event| match event {
LoweredEvent::Timed(timed) => lcm(divisions, timed.event.duration.divisions_requirement()),
LoweredEvent::Untimed(_)
| LoweredEvent::Overlay(_)
| LoweredEvent::VariantEnding(_)
| LoweredEvent::KeyChange(_)
| LoweredEvent::MeterChange(_)
| LoweredEvent::ClefChange(_)
| LoweredEvent::TempoChange(_)
| LoweredEvent::SectionLabel { .. } => divisions,
});
let events = all_lowered
.into_iter()
.filter_map(|event| match event {
LoweredEvent::Timed(timed) => Some(timed.event.into_event(divisions)),
LoweredEvent::Untimed(event) => Some(event.clone()),
LoweredEvent::Overlay(_)
| LoweredEvent::VariantEnding(_)
| LoweredEvent::KeyChange(_)
| LoweredEvent::MeterChange(_)
| LoweredEvent::ClefChange(_)
| LoweredEvent::TempoChange(_)
| LoweredEvent::SectionLabel { .. } => None,
})
.collect();
let meter_duration = lowering.meter_duration;
let mut voices = lowering.into_voice_timelines(meter_duration, &mut diagnostics);
align_lyrics(&mut voices, &lyric_lines, &mut diagnostics);
align_symbols(&mut voices, &symbol_lines, &mut diagnostics);
project_voice_midi(&mut voices, tune_music, field_state);
let score_directives = tune_music
.score_directives
.iter()
.map(score_directive_model)
.collect();
let preserved_directives = tune_music
.preserved_directives
.iter()
.map(preserved_directive_model)
.collect();
let post_tune_lyrics = tune_music
.body_fields
.iter()
.filter_map(|field| match &field.kind {
MusicFieldLineKind::PostTuneText(text) => Some(TextLine {
text: text.value.clone(),
span: text.span,
}),
_ => None,
})
.collect();
ParseReport::new(
LoweredMusic {
events,
divisions,
voices,
score_directives,
preserved_directives,
post_tune_lyrics,
},
diagnostics,
)
}
struct MultiVoiceLowering {
unit: Fraction,
key: Option<KeySignature>,
header_key_display: Option<String>,
header_meter_display: Option<String>,
meter_duration: Option<Fraction>,
voices: Vec<LoweringState>,
current_voice: String,
source_order: u32,
lyric_lines: Vec<VoicedLyricLine>,
symbol_lines: Vec<VoicedSymbolLine>,
diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct VoicedLyricLine {
voice_id: String,
line: LyricLineSyntax,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct VoicedSymbolLine {
voice_id: String,
line: SymbolLineSyntax,
}
impl MultiVoiceLowering {
fn new(unit: Fraction, field_state: &FieldState, fallback_span: Span) -> Self {
let mut lowering = Self {
unit,
key: field_state.key.as_ref().map(|key| key.value.clone()),
header_key_display: field_state.key.as_ref().map(|key| key.value.raw.clone()),
header_meter_display: field_state
.meter
.as_ref()
.map(|meter| meter.value.raw.clone()),
meter_duration: field_state
.meter
.as_ref()
.and_then(|meter| meter_duration(&meter.value)),
voices: Vec::new(),
current_voice: String::new(),
source_order: 0,
lyric_lines: Vec::new(),
symbol_lines: Vec::new(),
diagnostics: Vec::new(),
};
for voice in &field_state.voices {
lowering.ensure_voice(voice.clone());
}
let initial = field_state
.voices
.first()
.cloned()
.or_else(|| field_state.voice.clone())
.unwrap_or_else(|| default_voice_definition(fallback_span));
lowering.current_voice = initial.value.id.value.clone();
lowering.ensure_voice(initial);
if let Some(key) = field_state.key.as_ref() {
if key.value.compact_accidentals_ignored {
lowering
.diagnostics
.push(compact_key_accidentals_ignored_warning(key.span));
}
if key.value.tonic_trailing_junk_ignored {
lowering
.diagnostics
.push(key_tonic_trailing_junk_ignored_warning(key.span));
}
let key_props = key_clef_properties_model(&key.value.properties);
if key_props != VoicePropertiesModel::default() {
let state = lowering.current_state();
merge_voice_properties(&mut state.initial_properties, key_props.clone());
merge_voice_properties(&mut state.properties, key_props);
}
}
lowering
}
fn apply_field(&mut self, field: &MusicFieldLine) {
match &field.kind {
MusicFieldLineKind::Meter(meter) => self.apply_meter_change(meter),
MusicFieldLineKind::UnitNoteLength(unit) => self.apply_unit_change(unit),
MusicFieldLineKind::Key(key) => self.apply_key_change(key),
MusicFieldLineKind::Tempo(tempo) => self.apply_tempo_change(tempo),
MusicFieldLineKind::Voice(voice) => self.switch_voice(voice.clone()),
MusicFieldLineKind::Lyric(line) => self.lyric_lines.push(VoicedLyricLine {
voice_id: self.current_voice.clone(),
line: line.clone(),
}),
MusicFieldLineKind::Symbol(line) => self.symbol_lines.push(VoicedSymbolLine {
voice_id: self.current_voice.clone(),
line: line.clone(),
}),
MusicFieldLineKind::SectionLabel(label) => self.apply_section_label(label),
MusicFieldLineKind::PostTuneText(_)
| MusicFieldLineKind::Score(_)
| MusicFieldLineKind::Unknown(_)
| MusicFieldLineKind::Other => {}
}
}
fn apply_section_label(&mut self, label: &Spanned<String>) {
self.current_state()
.lowered
.push(LoweredEvent::SectionLabel {
label: label.value.clone(),
span: label.span,
});
}
fn apply_meter_change(&mut self, meter: &Spanned<Meter>) {
self.apply_current_voice_meter_change(meter);
}
fn apply_current_voice_meter_change(&mut self, meter: &Spanned<Meter>) {
if !self.validate_meter_change(meter) {
return;
}
let model = meter_model(meter);
let header = self.header_meter_display.clone();
let duration = meter_duration(&meter.value);
let voice = self.current_state();
voice.finish_open_tuplets_at_boundary();
voice.meter_duration = duration;
if effective_meter_display(voice, header.as_deref()) != Some(model.display.as_str()) {
voice.lowered.push(LoweredEvent::MeterChange(model));
}
}
fn apply_inline_meter_change(&mut self, meter: &Spanned<Meter>) {
self.apply_current_voice_meter_change(meter);
}
fn validate_meter_change(&mut self, meter: &Spanned<Meter>) -> bool {
if meter_is_invalid_for_lowering(&meter.value) {
self.diagnostics
.push(invalid_meter_change_warning(meter.span));
return false;
}
if matches!(meter.value.kind, MeterKind::Complex) && meter_duration(&meter.value).is_none()
{
self.diagnostics
.push(unsupported_complex_meter_warning(meter.span));
}
true
}
fn apply_tempo_change(&mut self, tempo: &Spanned<String>) {
let unit = self.unit;
if crate::lower::tempo::has_unterminated_quote(&tempo.value) {
self.diagnostics
.push(unterminated_tempo_quote_warning(tempo.span));
}
if let Some(model) = parse_tempo_model(&tempo.value, tempo.span, unit) {
self.current_state()
.lowered
.push(LoweredEvent::TempoChange(model));
}
}
fn apply_unit_change(&mut self, unit: &Spanned<UnitNoteLength>) {
self.current_state().unit = unit.value.fraction.to_model_fraction();
}
fn apply_key_change(&mut self, key: &Spanned<KeySignature>) {
self.apply_current_voice_key_change(key);
}
fn apply_current_voice_key_change(&mut self, key: &Spanned<KeySignature>) {
if key.value.tonic.is_none()
&& !key.value.accidentals.is_empty()
&& let Some(prevailing) = self
.current_state()
.current_key
.clone()
.filter(|prev| prev.tonic.is_some())
{
let merged = Spanned::new(merge_key_modification(&prevailing, &key.value), key.span);
self.apply_current_voice_key_change(&merged);
return;
}
if key_is_invalid_for_lowering(&key.value) {
self.diagnostics.push(invalid_key_change_warning(key.span));
return;
}
self.warn_if_key_field_recovered(key);
if !inline_key_changes_signature(&key.value) {
self.apply_current_voice_key_properties(&key.value.properties);
return;
}
let model = key_signature_model(key);
let header = self.header_key_display.clone();
{
let voice = self.current_state();
voice.set_key(Some(&key.value));
if effective_key_display(voice, header.as_deref()) != Some(model.display.as_str()) {
voice.lowered.push(LoweredEvent::KeyChange(model));
}
}
self.apply_current_voice_key_properties(&key.value.properties);
}
fn apply_inline_key_change(&mut self, key: &Spanned<KeySignature>) {
self.apply_current_voice_key_change(key);
}
fn apply_inline_key_clef_properties(&mut self, key: &KeySignature) {
self.apply_current_voice_key_properties(&key.properties);
}
fn apply_current_voice_key_properties(&mut self, properties: &VoiceProperties) {
let key_props = key_clef_properties_model(properties);
if key_props == VoicePropertiesModel::default() {
return;
}
let clef_change = key_clef_change_model(properties);
let voice = self.current_state();
if let Some(clef_change) = clef_change {
let current_clef = voice
.properties
.clef
.as_ref()
.map(|clef| clef.text.as_str());
if current_clef != Some(clef_change.clef.text.as_str()) {
voice.lowered.push(LoweredEvent::ClefChange(clef_change));
}
}
merge_voice_properties(&mut voice.properties, key_props);
}
fn warn_if_key_field_recovered(&mut self, key: &Spanned<KeySignature>) {
if key.value.compact_accidentals_ignored {
self.diagnostics
.push(compact_key_accidentals_ignored_warning(key.span));
}
if key.value.tonic_trailing_junk_ignored {
self.diagnostics
.push(key_tonic_trailing_junk_ignored_warning(key.span));
}
}
fn apply_inline_field(&mut self, inline: &InlineFieldSyntax) {
match inline.code {
'M' => {
let meter = crate::parse::field::parse_meter(&inline.value.value);
self.apply_inline_meter_change(&Spanned::new(meter, inline.value.span));
}
'L' => {
if let Some(unit) = crate::parse::field::parse_unit_note_length(&inline.value.value)
{
self.apply_unit_change(&Spanned::new(unit, inline.value.span));
} else {
self.diagnostics
.push(invalid_unit_change_warning(inline.value.span));
}
}
'K' => {
let key = crate::parse::field::parse_key(&inline.value.value, inline.value.span);
if inline_key_changes_signature(&key) {
self.apply_inline_key_change(&Spanned::new(key, inline.value.span));
} else if !key_is_invalid_for_lowering(&key) {
self.apply_inline_key_clef_properties(&key);
} else {
self.diagnostics
.push(invalid_key_change_warning(inline.value.span));
}
}
'Q' => {
self.apply_tempo_change(&Spanned::new(
inline.value.value.clone(),
inline.value.span,
));
}
'P' => {
self.apply_section_label(&Spanned::new(
inline.value.value.clone(),
inline.value.span,
));
}
'I' => {
let directive = inline
.value
.value
.split_whitespace()
.next()
.unwrap_or_default();
self.diagnostics
.push(inline_instruction_ignored_warning(directive, inline.span));
}
_ => {}
}
}
fn apply_music_line(&mut self, line: &MusicLine) {
let mut previous_item_end = None;
for item in &line.items {
let item_span = item.span();
let detached_from_previous = previous_item_end.is_some_and(|end| end < item_span.start);
match item {
MusicItem::Note(note) => {
let source_order = self.next_source_order();
self.current_state()
.push_note_group(note, line.line_index, source_order);
}
MusicItem::Rest(rest) => {
let source_order = self.next_source_order();
self.current_state()
.push_rest_group(rest, line.line_index, source_order);
}
MusicItem::MultiMeasureRest(rest) => {
let count = rest.count.map(|count| count.value).unwrap_or(1).max(1);
let voice_meter = self.current_state().meter_duration;
if let Some(meter_duration) = voice_meter {
let source_order = self.next_source_order();
let attachments = self
.current_state()
.take_timed_attachments(&AttachmentBundle::default());
for index in 0..count {
let rest_attachments = if index == 0 {
attachments.clone()
} else {
EventAttachments::default()
};
self.current_state().push_time_group(
vec![(
LoweredEventAtom {
kind: LoweredEventAtomKind::Rest {
visibility: rest.visibility,
multiple_rest: (index == 0
&& count > 1
&& rest.visibility == RestVisibility::Visible)
.then_some(count),
span: rest.span,
},
duration: meter_duration,
},
false,
rest_attachments,
)],
line.line_index,
source_order,
);
if index + 1 < count {
self.push_implicit_regular_barline(rest.span);
}
}
} else {
self.current_state()
.diagnostics
.push(free_meter_multirest_warning(rest.span));
let duration = self.unit.checked_mul_u32(count);
let source_order = self.next_source_order();
let attachments = self
.current_state()
.take_timed_attachments(&AttachmentBundle::default());
self.current_state().push_time_group(
vec![(
LoweredEventAtom {
kind: LoweredEventAtomKind::Rest {
visibility: rest.visibility,
multiple_rest: None,
span: rest.span,
},
duration,
},
false,
attachments,
)],
line.line_index,
source_order,
);
}
}
MusicItem::Spacer(spacer) => self
.current_state()
.lowered
.push(LoweredEvent::Untimed(Event::Spacer { span: spacer.span })),
MusicItem::Chord(chord) => {
let source_order = self.next_source_order();
self.current_state()
.push_chord_group(chord, line.line_index, source_order);
}
MusicItem::BrokenRhythm(marker) => {
self.current_state().apply_broken_rhythm(*marker)
}
MusicItem::Tuplet(tuplet) => self.current_state().start_tuplet(tuplet),
MusicItem::Slur(slur) => self.current_state().apply_slur(*slur),
MusicItem::Tie(tie) => self.current_state().apply_tie(*tie),
MusicItem::Overlay(overlay) => self
.current_state()
.lowered
.push(LoweredEvent::Overlay(*overlay)),
MusicItem::VariantEnding(ending) => {
if self.current_state().broken_left_available {
self.push_implicit_regular_barline(ending.span);
}
self.current_state()
.lowered
.push(LoweredEvent::VariantEnding(ending.clone()));
}
MusicItem::Barline(barline) => {
let source_order = self.next_source_order();
self.current_state().flush_pending_barline_directions(
line.line_index,
source_order,
barline.span,
barline.kind,
);
if matches!(barline.kind, BarlineKind::Dotted | BarlineKind::Invisible) {
self.current_state()
.diagnostics
.push(barline_export_policy_info(barline.span, barline.kind));
}
self.current_state()
.attach_pending_grace_groups_to_previous_note();
self.current_state()
.attach_pending_grace_groups_to_previous_note_if_measure_complete();
self.current_state().finish_pending_broken_at_boundary();
self.current_state().finish_open_tuplets_at_boundary();
self.current_state().reset_measure_accidentals_at_barline();
for kind in barline_lowering_kinds(barline) {
self.current_state()
.lowered
.push(LoweredEvent::Untimed(Event::Barline {
kind,
span: barline.span,
}));
}
}
MusicItem::InlineField(inline) if inline.code == 'V' => {
if let Some(voice) = &inline.voice {
self.switch_voice(voice.clone());
}
}
MusicItem::InlineField(inline) => self.apply_inline_field(inline),
MusicItem::GraceGroup(grace) => {
self.current_state()
.push_pending_grace_group(grace.clone(), detached_from_previous);
}
MusicItem::ChordSymbol(text) => {
self.current_state()
.pending_chord_symbols
.push(text.clone());
}
MusicItem::Annotation(text) => {
self.current_state().pending_annotations.push(text.clone());
}
MusicItem::Decoration(decoration) => {
self.current_state()
.pending_decorations
.push(decoration.clone());
}
MusicItem::Unsupported(_) | MusicItem::Malformed(_) => {}
}
previous_item_end = Some(item_span.end);
}
}
fn switch_voice(&mut self, voice: Spanned<VoiceDefinition>) {
let existing = self
.voices
.iter()
.position(|state| state.id.value == voice.value.id.value);
let previous_clef = existing
.and_then(|index| self.voices[index].properties.clef.as_ref())
.map(|clef| clef.text.clone());
let incoming = voice_properties_model(&voice.value);
self.current_voice = voice.value.id.value.clone();
let index = self.ensure_voice(voice);
if existing.is_some()
&& let Some(clef) = incoming.clef
&& previous_clef.as_deref() != Some(clef.text.as_str())
{
self.voices[index]
.lowered
.push(LoweredEvent::ClefChange(ClefChangeModel {
source_span: clef.span,
clef,
}));
}
}
fn next_source_order(&mut self) -> u32 {
let order = self.source_order;
self.source_order = self.source_order.saturating_add(1);
order
}
fn ensure_voice(&mut self, voice: Spanned<VoiceDefinition>) -> usize {
if let Some(index) = self
.voices
.iter()
.position(|state| state.id.value == voice.value.id.value)
{
let incoming = voice_properties_model(&voice.value);
merge_voice_properties(&mut self.voices[index].properties, incoming);
self.voices[index].source_span = voice.span;
return index;
}
let id = VoiceId {
value: voice.value.id.value.clone(),
span: voice.value.id.span,
};
let properties = voice_properties_model(&voice.value);
let state = LoweringState::new(
id,
properties,
self.unit,
self.key.as_ref(),
self.meter_duration,
);
self.voices.push(state);
self.voices.len() - 1
}
fn current_state(&mut self) -> &mut LoweringState {
let index = self
.voices
.iter()
.position(|state| state.id.value == self.current_voice)
.unwrap_or_else(|| {
let voice = default_voice_definition(Span::new(0, 0));
self.current_voice = voice.value.id.value.clone();
self.ensure_voice(voice)
});
&mut self.voices[index]
}
fn push_implicit_regular_barline(&mut self, span: Span) {
let voice = self.current_state();
voice.attach_pending_grace_groups_to_previous_note();
voice.attach_pending_grace_groups_to_previous_note_if_measure_complete();
voice.finish_pending_broken_at_boundary();
voice.finish_open_tuplets_at_boundary();
voice.reset_measure_accidentals_at_barline();
voice.lowered.push(LoweredEvent::Untimed(Event::Barline {
kind: BarlineKind::Regular,
span,
}));
}
fn finish_open_constructs(&mut self) -> Vec<Diagnostic> {
let mut diagnostics = std::mem::take(&mut self.diagnostics);
for voice in &mut self.voices {
voice.finish_open_constructs();
diagnostics.extend(std::mem::take(&mut voice.diagnostics));
}
diagnostics
}
fn into_voice_timelines(
self,
meter_duration: Option<Fraction>,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<VoiceTimeline> {
let single_voice = self.voices.len() == 1;
let mut voices = self
.voices
.into_iter()
.map(|voice| build_voice_timeline(voice, meter_duration, single_voice, diagnostics))
.collect::<Vec<_>>();
if voices.len() > 1 {
voices.retain(|voice| {
voice_has_timeline_content(voice)
|| voice.properties != VoicePropertiesModel::default()
|| voice.id.value != "1"
});
}
voices
}
}
fn voice_has_timeline_content(voice: &VoiceTimeline) -> bool {
voice.measures.iter().any(|measure| {
!measure.overlays.is_empty()
|| measure.events.iter().any(|event| {
matches!(
event.kind,
TimelineEventKind::Note { .. } | TimelineEventKind::Rest { .. }
)
})
})
}
fn default_voice_definition(span: Span) -> Spanned<VoiceDefinition> {
let id = Spanned::new("1".to_owned(), span);
Spanned::new(
VoiceDefinition {
id: id.clone(),
properties: Spanned::new(String::new(), Span::new(span.end, span.end)),
parsed_properties: Default::default(),
},
span,
)
}
fn merge_voice_properties(existing: &mut VoicePropertiesModel, incoming: VoicePropertiesModel) {
if incoming.name.is_some() {
existing.name = incoming.name;
}
if incoming.nm.is_some() {
existing.nm = incoming.nm;
}
if incoming.subname.is_some() {
existing.subname = incoming.subname;
}
if incoming.snm.is_some() {
existing.snm = incoming.snm;
}
if incoming.clef.is_some() {
existing.clef = incoming.clef;
}
if incoming.stem.is_some() {
existing.stem = incoming.stem;
}
if incoming.octave.is_some() {
existing.octave = incoming.octave;
}
if incoming.transpose.is_some() {
existing.transpose = incoming.transpose;
}
if incoming.middle.is_some() {
existing.middle = incoming.middle;
}
}
fn voice_properties_model(voice: &VoiceDefinition) -> VoicePropertiesModel {
VoicePropertiesModel {
name: voice
.parsed_properties
.name
.as_ref()
.map(text_line_from_spanned),
nm: voice
.parsed_properties
.nm
.as_ref()
.map(text_line_from_spanned),
subname: voice
.parsed_properties
.subname
.as_ref()
.map(text_line_from_spanned),
snm: voice
.parsed_properties
.snm
.as_ref()
.map(text_line_from_spanned),
clef: voice
.parsed_properties
.clef
.as_ref()
.map(text_line_from_spanned),
stem: voice
.parsed_properties
.stem
.as_ref()
.map(|stem| match stem.value {
StemDirection::Up => StemDirectionModel::Up,
StemDirection::Down => StemDirectionModel::Down,
}),
octave: voice
.parsed_properties
.octave
.as_ref()
.map(text_line_from_spanned),
transpose: voice
.parsed_properties
.transpose
.as_ref()
.map(text_line_from_spanned),
middle: voice
.parsed_properties
.middle
.as_ref()
.map(text_line_from_spanned),
}
}
fn key_clef_properties_model(properties: &VoiceProperties) -> VoicePropertiesModel {
VoicePropertiesModel {
clef: properties.clef.as_ref().map(text_line_from_spanned),
octave: properties.octave.as_ref().map(text_line_from_spanned),
transpose: properties.transpose.as_ref().map(text_line_from_spanned),
middle: properties.middle.as_ref().map(text_line_from_spanned),
..VoicePropertiesModel::default()
}
}
fn key_clef_change_model(properties: &VoiceProperties) -> Option<ClefChangeModel> {
properties.clef.as_ref().map(|clef| ClefChangeModel {
clef: text_line_from_spanned(clef),
source_span: clef.span,
})
}
fn text_line_from_spanned(value: &Spanned<String>) -> TextLine {
TextLine {
text: value.value.clone(),
span: value.span,
}
}
fn inline_key_changes_signature(key: &KeySignature) -> bool {
key.tonic.is_some() || !key.accidentals.is_empty() || key.mode != KeyMode::Major
}
pub(crate) fn extend_span(current: Span, next: Span) -> Span {
if current.is_empty() {
return next;
}
Span::new(current.start.min(next.start), current.end.max(next.end))
}
pub(crate) struct ScoreModelInput<'a> {
pub reference: TextLine,
pub title: Option<TextLine>,
pub composers: Vec<TextLine>,
pub tempo: Option<TextLine>,
pub source_span: Span,
pub field_state: &'a FieldState,
pub voices: &'a [VoiceTimeline],
pub score_directives: &'a [ScoreDirectiveModel],
pub preserved_directives: &'a [PreservedDirective],
pub post_tune_lyrics: &'a [TextLine],
pub diagnostics: &'a [Diagnostic],
pub divisions: u32,
}
fn part_voice_groups(
directives: &[ScoreDirectiveModel],
voices: &[VoiceTimeline],
) -> Vec<Vec<usize>> {
let one_per_voice = || {
(0..voices.len())
.map(|index| vec![index])
.collect::<Vec<_>>()
};
let Some(directive) = directives.iter().rev().find(|directive| {
directive
.tokens
.iter()
.any(|token| matches!(token.kind, ScoreDirectiveTokenKindModel::Voice(_)))
}) else {
return one_per_voice();
};
let index_of = |id: &str| voices.iter().position(|voice| voice.id.value == id);
let mut groups: Vec<Vec<usize>> = Vec::new();
let mut paren_depth = 0u32;
let mut current: Vec<usize> = Vec::new();
for token in &directive.tokens {
match &token.kind {
ScoreDirectiveTokenKindModel::GroupStart('(') => paren_depth += 1,
ScoreDirectiveTokenKindModel::GroupEnd(')') => {
paren_depth = paren_depth.saturating_sub(1);
if paren_depth == 0 && !current.is_empty() {
groups.push(std::mem::take(&mut current));
}
}
ScoreDirectiveTokenKindModel::Voice(id) => {
if let Some(index) = index_of(id) {
if paren_depth > 0 {
current.push(index);
} else {
groups.push(vec![index]);
}
}
}
_ => {}
}
}
if !current.is_empty() {
groups.push(current);
}
let mentioned: std::collections::HashSet<usize> = groups.iter().flatten().copied().collect();
for index in 0..voices.len() {
if !mentioned.contains(&index) {
groups.push(vec![index]);
}
}
if groups.is_empty() {
return one_per_voice();
}
groups
}
pub(crate) fn build_score_model(input: ScoreModelInput<'_>) -> Score {
let single_voice = input.voices.len() == 1;
let groups = part_voice_groups(input.score_directives, input.voices);
let mut parts = groups
.iter()
.enumerate()
.map(|(part_index, voice_indices)| {
let staff_id = StaffId {
value: 1,
span: input.source_span,
};
let semantic_voices = voice_indices
.iter()
.map(|&voice_index| {
semantic_voice_from_timeline(
&input.voices[voice_index],
staff_id,
input.field_state,
)
})
.collect::<Vec<_>>();
let staves = vec![Staff {
id: staff_id,
voices: semantic_voices
.iter()
.map(|voice| voice.id.clone())
.collect(),
source_span: input.source_span,
}];
let name = semantic_voices
.first()
.and_then(|voice| {
voice
.properties
.name
.clone()
.or_else(|| voice.properties.nm.clone())
})
.or_else(|| single_voice.then(|| input.title.clone()).flatten());
Part {
id: PartId {
value: format!("P{}", part_index + 1),
span: input.source_span,
},
name,
staves,
voices: semantic_voices,
source_span: input.source_span,
}
})
.collect::<Vec<_>>();
if parts.is_empty() {
parts.push(Part {
id: PartId {
value: "P1".to_owned(),
span: input.source_span,
},
name: input.title.clone(),
staves: vec![Staff {
id: StaffId {
value: 1,
span: input.source_span,
},
voices: Vec::new(),
source_span: input.source_span,
}],
voices: Vec::new(),
source_span: input.source_span,
});
}
Score {
metadata: ScoreMetadata {
reference: input.reference,
title: input.title.clone(),
composers: input.composers,
tempo_model: input.tempo.as_ref().and_then(|tempo| {
parse_tempo_model(
&tempo.text,
tempo.span,
input.field_state.unit_note_length_fraction(),
)
}),
tempo: input.tempo,
meter: input.field_state.meter.as_ref().map(meter_model),
key: input.field_state.key.as_ref().map(key_signature_model),
directives: input.score_directives.to_vec(),
preserved_directives: input.preserved_directives.to_vec(),
post_tune_lyrics: input.post_tune_lyrics.to_vec(),
source_span: input.source_span,
},
parts,
diagnostics: input.diagnostics.to_vec(),
divisions: input.divisions,
source_span: input.source_span,
accidental_policy: AccidentalPolicy {
preserve_explicit_accidentals: true,
reset_at_barlines: true,
scope: AccidentalScope::PitchAndOctave,
source_span: input.source_span,
},
}
}
fn meter_model(meter: &Spanned<Meter>) -> MeterModel {
let duration = meter_duration(&meter.value);
MeterModel {
display: meter.value.raw.clone(),
duration,
free_meter: duration.is_none(),
source_span: meter.span,
}
}
fn effective_key_display<'a>(voice: &'a LoweringState, header: Option<&'a str>) -> Option<&'a str> {
voice
.lowered
.iter()
.rev()
.find_map(|event| match event {
LoweredEvent::KeyChange(key) => Some(key.display.as_str()),
_ => None,
})
.or(header)
}
fn effective_meter_display<'a>(
voice: &'a LoweringState,
header: Option<&'a str>,
) -> Option<&'a str> {
voice
.lowered
.iter()
.rev()
.find_map(|event| match event {
LoweredEvent::MeterChange(meter) => Some(meter.display.as_str()),
_ => None,
})
.or(header)
}
fn key_signature_model(key: &Spanned<KeySignature>) -> KeySignatureModel {
KeySignatureModel {
display: key.value.raw.clone(),
fifths: key_fifths(&key.value),
explicit_accidentals: key
.value
.accidentals
.iter()
.map(|accidental| KeyAccidentalModel {
step: accidental.note.value.to_ascii_uppercase(),
accidental: accidental_from_field_sign(accidental.sign),
source_span: accidental.span,
})
.collect(),
source_span: key.span,
}
}
fn meter_is_invalid_for_lowering(meter: &Meter) -> bool {
matches!(meter.kind, MeterKind::Complex)
&& !meter.raw.contains('/')
&& !meter.raw.contains('+')
&& !meter.raw.contains('(')
}
fn key_is_invalid_for_lowering(key: &KeySignature) -> bool {
key.tonic.is_none()
&& key_clef_properties_model(&key.properties) == VoicePropertiesModel::default()
&& !matches!(
key.mode,
KeyMode::Explicit
| KeyMode::None
| KeyMode::HighlandPipes
| KeyMode::HighlandPipesMarked
)
}
fn merge_key_modification(prevailing: &KeySignature, body: &KeySignature) -> KeySignature {
let mut accidentals: Vec<KeyAccidental> = prevailing
.accidentals
.iter()
.filter(|base| {
!body
.accidentals
.iter()
.any(|added| added.note.value.eq_ignore_ascii_case(&base.note.value))
})
.cloned()
.collect();
accidentals.extend(body.accidentals.iter().cloned());
KeySignature {
raw: format!("{} {}", prevailing.raw.trim(), body.raw.trim()),
tonic: prevailing.tonic,
mode: prevailing.mode.clone(),
accidentals,
explicit: prevailing.explicit,
compact_accidentals_ignored: false,
tonic_trailing_junk_ignored: false,
properties: body.properties.clone(),
}
}
pub(crate) fn key_fifths(key: &KeySignature) -> i8 {
let Some(tonic) = key.tonic else {
return 0;
};
let base = match tonic.step {
'C' => 0,
'G' => 1,
'D' => 2,
'A' => 3,
'E' => 4,
'B' => 5,
'F' => -1,
_ => 0,
};
let accidental_shift = match tonic.accidental {
Some(KeyTonicAccidental::Sharp) => 7,
Some(KeyTonicAccidental::Flat) => -7,
None => 0,
};
let mode_shift = match key.mode {
KeyMode::Major | KeyMode::Ionian | KeyMode::Unknown(_) => 0,
KeyMode::Mixolydian => -1,
KeyMode::Dorian => -2,
KeyMode::Minor | KeyMode::Aeolian => -3,
KeyMode::Phrygian => -4,
KeyMode::Locrian => -5,
KeyMode::Lydian => 1,
KeyMode::Explicit
| KeyMode::None
| KeyMode::HighlandPipes
| KeyMode::HighlandPipesMarked => return 0,
};
(base + accidental_shift + mode_shift).clamp(-7, 7)
}
fn score_directive_model(syntax: &ScoreDirectiveSyntax) -> ScoreDirectiveModel {
ScoreDirectiveModel {
span: syntax.span,
value: TextLine {
text: syntax.value.value.clone(),
span: syntax.value.span,
},
tokens: syntax
.directive
.tokens
.iter()
.map(|token| ScoreDirectiveTokenModel {
span: token.span,
kind: match &token.kind {
crate::parse::field::ScoreDirectiveTokenKind::Voice(id) => {
ScoreDirectiveTokenKindModel::Voice(id.clone())
}
crate::parse::field::ScoreDirectiveTokenKind::GroupStart(ch) => {
ScoreDirectiveTokenKindModel::GroupStart(*ch)
}
crate::parse::field::ScoreDirectiveTokenKind::GroupEnd(ch) => {
ScoreDirectiveTokenKindModel::GroupEnd(*ch)
}
crate::parse::field::ScoreDirectiveTokenKind::StaffSeparator => {
ScoreDirectiveTokenKindModel::StaffSeparator
}
crate::parse::field::ScoreDirectiveTokenKind::MeasureSeparator => {
ScoreDirectiveTokenKindModel::MeasureSeparator
}
crate::parse::field::ScoreDirectiveTokenKind::FloatingVoiceMarker => {
ScoreDirectiveTokenKindModel::FloatingVoiceMarker
}
},
})
.collect(),
}
}
fn preserved_directive_model(syntax: &PreservedDirectiveSyntax) -> PreservedDirective {
PreservedDirective {
name: TextLine {
text: syntax.name.value.clone(),
span: syntax.name.span,
},
value: TextLine {
text: syntax.value.value.clone(),
span: syntax.value.span,
},
span: syntax.span,
}
}
fn project_voice_midi(
voices: &mut [VoiceTimeline],
tune_music: &ParsedTuneMusic,
field_state: &FieldState,
) {
if voices.is_empty() {
return;
}
let mut switches: Vec<(usize, &str)> = field_state
.voices
.iter()
.map(|voice| (voice.span.start, voice.value.id.value.as_str()))
.collect();
for field in &tune_music.body_fields {
if let MusicFieldLineKind::Voice(voice) = &field.kind {
switches.push((field.line_span.start, voice.value.id.value.as_str()));
}
}
switches.sort_by_key(|(position, _)| *position);
let default_voice = voices[0].id.value.clone();
let voice_at = |position: usize| -> &str {
switches
.iter()
.rev()
.find(|(switch_position, _)| *switch_position <= position)
.map_or(default_voice.as_str(), |(_, id)| *id)
};
let mut directives: Vec<(&str, &str, Span)> = Vec::new();
for directive in &tune_music.preserved_directives {
if directive.name.value.eq_ignore_ascii_case("MIDI") {
let voice_id = voice_at(directive.span.start);
directives.push((voice_id, directive.value.value.as_str(), directive.span));
}
}
for line in &tune_music.lines {
for item in &line.items {
let MusicItem::InlineField(inline) = item else {
continue;
};
if inline.code != 'I' {
continue;
}
let Some(rest) = inline.value.value.strip_prefix("MIDI") else {
continue;
};
if !rest.is_empty() && !rest.starts_with(['=', ' ', '\t']) {
continue;
}
let args = rest.trim_start_matches(['=', ' ', '\t']);
directives.push((voice_at(inline.span.start), args, inline.span));
}
}
directives.sort_by_key(|(_, _, span)| span.start);
let mut instrument_by_voice: BTreeMap<&str, MidiInstrumentModel> = BTreeMap::new();
let mut transpose_by_voice: BTreeMap<&str, i16> = BTreeMap::new();
for (voice_id, args, span) in directives {
if let Some(transpose) = parse_midi_transpose(args) {
transpose_by_voice.insert(voice_id, transpose);
continue;
}
let model = instrument_by_voice
.entry(voice_id)
.or_insert(MidiInstrumentModel {
program: None,
channel: None,
volume_cc: None,
pan_cc: None,
span,
});
apply_midi_instrument_directive(args, span, model);
}
for voice in voices.iter_mut() {
if let Some(model) = instrument_by_voice
.get(voice.id.value.as_str())
.filter(|model| model.has_content())
{
voice.midi_instrument = Some(*model);
}
if let Some(transpose) = transpose_by_voice.get(voice.id.value.as_str()) {
voice.midi_transpose = Some(*transpose);
}
}
}
fn apply_midi_instrument_directive(value: &str, span: Span, model: &mut MidiInstrumentModel) {
let head = value.split('%').next().unwrap_or(value);
let mut tokens = head.split_whitespace();
let Some(sub_directive) = tokens.next() else {
return;
};
match sub_directive {
"program" => {
let integers: Vec<u8> = tokens.map_while(leading_u8).collect();
let (channel, program) = match integers.as_slice() {
[program] => (None, Some(*program)),
[channel, program, ..] => (Some(*channel), Some(*program)),
[] => (None, None),
};
if let Some(program) = program.filter(|program| *program <= 127) {
model.program = Some(program);
if let Some(channel) = channel.filter(|channel| (1..=16).contains(channel)) {
model.channel = Some(channel);
}
model.span = span;
}
}
"channel" => {
if let Some(channel) = tokens
.next()
.and_then(leading_u8)
.filter(|channel| (1..=16).contains(channel))
{
model.channel = Some(channel);
model.span = span;
}
}
"control" => {
let integers: Vec<u8> = tokens.map_while(leading_u8).collect();
if let [controller, value, ..] = integers.as_slice() {
match controller {
7 => {
model.volume_cc = Some(*value);
model.span = span;
}
10 => {
model.pan_cc = Some(*value);
model.span = span;
}
_ => {}
}
}
}
_ => {}
}
}
fn leading_u8(token: &str) -> Option<u8> {
let digits: String = token.chars().take_while(char::is_ascii_digit).collect();
digits.parse::<u8>().ok()
}
fn parse_midi_transpose(value: &str) -> Option<i16> {
let head = value.split('%').next().unwrap_or(value);
let mut tokens = head.split_whitespace();
if tokens.next()? != "transpose" {
return None;
}
tokens.next()?.parse::<i16>().ok()
}
pub(crate) fn music_code_span(line: &crate::syntax::tune::ClassifiedLine) -> Span {
let mut end = line.text_span.end;
if let Some(comment_span) = line.trailing_comment {
end = end.min(comment_span.start);
}
if let ScoreLineBreak::Suppressed { marker_span } = line.score_line_break {
end = end.min(marker_span.start);
}
Span::new(line.text_span.start, end)
}
pub(crate) fn meter_duration(meter: &Meter) -> Option<Fraction> {
match &meter.kind {
MeterKind::CommonTime => Some(Fraction::new(4, 4)),
MeterKind::CutTime => Some(Fraction::new(2, 2)),
MeterKind::Fraction {
numerator,
denominator,
} => Some(Fraction::new(*numerator, *denominator)),
MeterKind::None => None,
MeterKind::Complex => complex_meter_duration(&meter.raw),
}
}
fn complex_meter_duration(raw: &str) -> Option<Fraction> {
let value = raw.trim();
if value.contains('+') && !value.trim_start().starts_with('(') {
let mut total = Fraction::zero();
let mut saw_part = false;
for part in value.split('+') {
let (numerator, denominator) = part.trim().split_once('/')?;
total = total.checked_add(Fraction::new(
numerator.trim().parse().ok()?,
denominator.trim().parse().ok()?,
));
saw_part = true;
}
return saw_part.then_some(total);
}
if let Some((beats, beat_type)) = value.split_once('/') {
let beats = beats
.trim()
.strip_prefix('(')
.and_then(|beats| beats.strip_suffix(')'))
.unwrap_or_else(|| beats.trim());
let denominator = beat_type.trim().parse::<u32>().ok()?;
let numerator = additive_u32(beats)?;
return Some(Fraction::new(numerator, denominator));
}
None
}
fn additive_u32(value: &str) -> Option<u32> {
let mut total = 0u32;
let mut saw_part = false;
for part in value.split('+') {
total = total.checked_add(part.trim().parse::<u32>().ok()?)?;
saw_part = true;
}
saw_part.then_some(total)
}
fn barline_lowering_kinds(barline: &BarlineSyntax) -> Vec<BarlineKind> {
let raw = barline.raw.strip_prefix('.').unwrap_or(&barline.raw);
if barline.kind == BarlineKind::RepeatStart {
if raw.contains(']') {
return vec![BarlineKind::Final, BarlineKind::RepeatStart];
}
if raw.starts_with("||") {
return vec![BarlineKind::Double, BarlineKind::RepeatStart];
}
if raw.starts_with("[|") {
return vec![BarlineKind::Initial, BarlineKind::RepeatStart];
}
}
vec![barline.kind]
}
pub(crate) fn default_tuplet_q(p: u32) -> u32 {
match p {
2 | 4 | 8 => 3,
_ => 2,
}
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;