alda-lang / alda

A music programming language for musicians. :notes:
https://alda.io
Eclipse Public License 2.0
5.59k stars 286 forks source link

MusicXML Importer - Optimization #364

Closed Scowluga closed 3 years ago

Scowluga commented 3 years ago

Due to the differences between MusicXML and Alda representation, while importing we must maintain some extra information that is not present in idiomatic Alda. We want to apply post processing on the importer state to remove redundant information and generate more idiomatic Alda.

This PR adds a post processing step in the MusicXML importer with the following key method:

func (processor *postProcessor) processAll(
    updates []model.ScoreUpdate,
) []model.ScoreUpdate {
    // We standardize barlines in post processing to improve duration removal
    updates = standardizeBarlines(updates)
    updates = processor.removeRedundantAccidentals(updates)
    updates = processor.removeRedundantDurations(updates)
    return updates
}

Standardize Barlines

Barlines are introduced into score updates with duration components (rest, note). This is because we might have to deal with ties, and follows the Alda convention. However after all is said and done, we will have many updates that have barlines as the last element in their duration components. Then the barline can just as easily be extracted. This extraction process is not necessary, but it creates equivalent Alda and serves to make testing and removing durations easier. So we do it.

Remove Redundant Accidentals

In Alda, a key signature automatically applies accidentals to each note. In MusicXML, the key signature is just visual, and each note must have the correct pitch. So in the importer Alda there are many redundant accidentals that should be removed.

However we shouldn't just naively remove all accidentals. In normal sheet music, after a pitch has diverged from it's normal state as defined by a key signature, when it returns back to the key signature it should have accidentals to re-iterate this return. For example, in G major if we have an F natural followed by an F sharp, that second F should have the sharp, even if in Alda it is not necessary since it would be automatically applied by the key signature.

So in this step we do some extra work by maintaining currentNoteState which represents if a note has diverged from its key signature.

Remove Redundant Durations

The most difficult of all is removing redundant durations. In Alda a note/rest can have nil durations, and this will simply copy the last encountered duration.

Doing this in post processing is not as easy as it seems. Originally I just processed the []model.ScoreUpdates linearly and maintained a duration. But it turns out this does not work since we have repeats. To properly remove durations we would have to follow the execution of the score itself, which is non-trivial.

Instead I have applied a heuristic in removing durations. I only remove durations that are super unnecessary. These are durations that happen within the same measure, and are not at different nesting levels (chords, repeats). This leads to Alda code that does not have every possible duration removed, but has most unnecessary ones removed. I think this is quite nice, and the resulting test Alda code looks very readable.

Essentially, to remove all possible durations is very difficult. But removing this subset is very doable, and leads to Alda code that I'd argue is more readable. So I've gone ahead and done that.

Scowluga commented 3 years ago

I agree optimizer is a better term, I didn't quite make that connection. I will rename and add an additional step to map midi note numbers to pitch identifiers.

Scowluga commented 3 years ago

I have added a few commits.

Midi Note Number Translation (Commit 1, 4)

I've extracted the NoteLetterIntervals: map[NoteLetter]int32 in pitch.go and used it to define the func (mnn MidiNoteNumber) ToNoteAndOctave() (LetterAndAccidentals, int32) mapping method.

In commit 4 I added documentation and renamed this to a better name ToLetterAndAccidentalsAndOctave.

I didn't add any tests for this because I think it is tested with the MusicXML TestPercussion test which calls post processor and tests it.

Renaming (Commit 2, 3)

I renamed post processing to optimizer. This is a much better name.

Let me know and I'll merge both PRs.