Last active
December 20, 2021 08:36
-
-
Save alanwhite/4ac7a5f1dba69d21aad043e591dd3279 to your computer and use it in GitHub Desktop.
Algo that implements a common beam style for music, where the primary subdivision of the beat is used to direct hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package xyz.arwhite.music.helpers; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Optional; | |
import java.util.stream.Collectors; | |
import xyz.arwhite.music.models.BaseNoteModel; | |
import xyz.arwhite.music.models.BeamEnum; | |
import xyz.arwhite.music.models.MeasureModel; | |
import xyz.arwhite.music.models.PartModel; | |
import xyz.arwhite.music.models.StaffElementModel; | |
import xyz.arwhite.music.models.TimeSigModel; | |
/** | |
* Respects the primary subdivision of a beat, with hooks preferred over joining beams over a subdivision. | |
* This is the COMMON policy for automatic beam generation. | |
* | |
* @author Alan R. White | |
* | |
*/ | |
public class AutoBeamCommon extends AbstractAutoBeam { | |
/** | |
* Analyzes and updates notes in a part to set their beams appropriately. Note durations must be | |
* accurately set beforehand. | |
* | |
* @param mcx the musical context to apply containing time signature and other needed info | |
* @param part the part containing the notes to be managed | |
*/ | |
public void forPart(MusicalContext mcx, PartModel part) { | |
super.forPart(mcx, part); | |
// work out the divisions per beat | |
var divsPerBeat = Divisions.divisionsPerBeat(mcx); | |
// and the divisions per primary subdivision of the beat | |
var divsPerSubDiv = Divisions.divisionsPerPrimarySubDivOfBeat(mcx); | |
// break notes into beat groups | |
var beatList = noteElementsByBeat(part.getStaffElements(),divsPerBeat); | |
// work out beams in each beat | |
beatList.forEach(notes -> beamBeat(notes,divsPerBeat,divsPerSubDiv)); | |
} | |
/** | |
* Analyzes and updates notes in a beat within a measure | |
* | |
* @param notes in the beat | |
* @param divsPerBeat number of divisions in a beat | |
*/ | |
private void beamBeat(List<BaseNoteModel> notes, int divsPerBeat, int divsPerSubDiv) { | |
int beatCursor = 0; | |
int subDivCursor = 0; | |
for (int index = 0; index < notes.size(); index++) { | |
var note = notes.get(index); | |
if ( beatCursor >= divsPerBeat ) { | |
beatCursor = 0; | |
subDivCursor = 0; | |
} | |
if ( subDivCursor >= divsPerSubDiv ) | |
subDivCursor = subDivCursor - divsPerSubDiv; | |
if ( beatCursor == 0 ) | |
// it's the start of the beat | |
firstNoteInBeat(notes,index); | |
else if ( beatCursor + note.getDuration() >= divsPerBeat ) | |
// it's the end of a beat | |
lastNoteInBeat(notes,index); | |
else if ( subDivCursor + note.getDuration() >= divsPerSubDiv ) | |
// last note in a subdiv but not in beat | |
lastNoteInSubDiv(notes,index); | |
else if ( subDivCursor == 0 ) | |
// first note in a subdivision but not in beat | |
firstNoteInSubDiv(notes,index); | |
else | |
// otherwise we're mid beat | |
middleNoteInBeat(notes,index); | |
beatCursor += note.getDuration(); | |
subDivCursor += note.getDuration(); | |
} | |
} | |
/** | |
* Updates a note in the middle of a beat with the correct beams. | |
* | |
* @param notes | |
* @param index | |
*/ | |
private void middleNoteInBeat(List<BaseNoteModel> notes, int index) { | |
// see how many we have in common with prior and next | |
var note = notes.get(index); | |
var nextTailCount = this.getNextNoteTails(notes, index); | |
var priorTailCount = this.getPriorNoteTails(notes, index); | |
var noteTailCount = this.tailsPerNote(note); | |
var commonPrior = Math.min(priorTailCount,noteTailCount); | |
var commonNext = Math.min(nextTailCount,noteTailCount); | |
var commonAllTails = Math.min(nextTailCount,Math.min(priorTailCount,noteTailCount)); | |
if ( noteTailCount > 0 ) | |
note.setBeams(Optional.of(new ArrayList<BeamEnum>())); | |
var beamIndex = 0; | |
// do an CONTINUE for each one in common with next and prior | |
while( beamIndex < commonAllTails ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.CONTINUE); | |
if ( commonPrior > commonAllTails ) { | |
// if we have some in common with prior but not next do an end | |
while( beamIndex < commonPrior ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.END); | |
// and then if we have any leftover hook backwards | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BACKWARD_HOOK); | |
} else if ( commonNext > commonAllTails ) { | |
// else if we have some in common with next but not prior do a begin | |
while( beamIndex < commonNext ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BEGIN); | |
// and then if we have any leftover hook forward | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.FORWARD_HOOK); | |
} else if ( noteTailCount > priorTailCount ) { | |
// else if we still have more tails than prior do a hook backward | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BACKWARD_HOOK); | |
} | |
} | |
/** | |
* Updates a note at the start of a subdivision with the correct beams | |
* | |
* @param notes | |
* @param index | |
*/ | |
private void firstNoteInSubDiv(List<BaseNoteModel> notes, int index) { | |
var note = notes.get(index); | |
var nextTailCount = this.getNextNoteTails(notes, index); | |
var priorTailCount = this.getPriorNoteTails(notes, index); | |
var noteTailCount = this.tailsPerNote(note); | |
var commonPrior = Math.min(priorTailCount,noteTailCount); | |
var commonNext = Math.min(nextTailCount,noteTailCount); | |
var commonAllTails = Math.min(nextTailCount,Math.min(priorTailCount,noteTailCount)); | |
if ( noteTailCount > 0 ) | |
note.setBeams(Optional.of(new ArrayList<BeamEnum>())); | |
var beamIndex = 0; | |
// do an CONTINUE for each one in common with next and prior | |
/* | |
* Except this is a problem if the prior note had decided it was to hook back instead of begin | |
* It's almost like we need to know what decision the prior note took. | |
* | |
* We could inspect and if we want to do a continue, and the prior is hook back we don't incr beamIndex | |
* and don't do anything. Then the subsequent logic should kick in and we do a begin. Which won't work | |
* because the logic looks for beyond the common flags. | |
* | |
* So if we inspect the prior and see it's a hook back, we need to do a begin or hook forward depending on | |
* if we have a partner flag in the next note. Or refactor the thinking here. | |
* | |
* What if the next note isn't in our subdiv? That's a problem for all of this. Park for now. | |
*/ | |
while( beamIndex < commonAllTails ) { | |
if ( getPriorNoteBeam(notes,index,beamIndex) == BeamEnum.BACKWARD_HOOK ) { | |
// if next has a flag at beamIndex level then it's a BEGIN, otherwise forward hook | |
// it must have as it's within the common tails count | |
note.getBeams().get().add(beamIndex++,BeamEnum.BEGIN); | |
} else | |
note.getBeams().get().add(beamIndex++,BeamEnum.CONTINUE); | |
} | |
if ( commonNext > commonAllTails ) { | |
// else if we have some in common with next but not prior do a begin | |
while( beamIndex < commonNext ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BEGIN); | |
// and then if we have any leftover hook forward | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.FORWARD_HOOK); | |
} else if ( commonPrior > commonAllTails ) { | |
// if we have some in common with prior but not next do hook forward | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.FORWARD_HOOK); | |
} else if ( noteTailCount > priorTailCount ) { | |
// else if we still have more tails than prior do a hook forward | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.FORWARD_HOOK); | |
} | |
} | |
/** | |
* Updates a note that is at the end of a subdivision with the correct beams | |
* | |
* @param notes | |
* @param index | |
*/ | |
private void lastNoteInSubDiv(List<BaseNoteModel> notes, int index) { | |
var note = notes.get(index); | |
var nextTailCount = this.getNextNoteTails(notes, index); | |
var priorTailCount = this.getPriorNoteTails(notes, index); | |
var noteTailCount = this.tailsPerNote(note); | |
var commonPrior = Math.min(priorTailCount,noteTailCount); | |
var commonNext = Math.min(nextTailCount,noteTailCount); | |
var commonAllTails = Math.min(nextTailCount,Math.min(priorTailCount,noteTailCount)); | |
if ( noteTailCount > 0 ) | |
note.setBeams(Optional.of(new ArrayList<BeamEnum>())); | |
var beamIndex = 0; | |
// do an CONTINUE for each one in common with next and prior | |
while( beamIndex < commonAllTails ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.CONTINUE); | |
if ( commonPrior > commonAllTails ) { | |
// if we have some in common with prior but not next do an end | |
while( beamIndex < commonPrior ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.END); | |
// and then if we have any leftover hook backwards | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BACKWARD_HOOK); | |
} else if ( commonNext > commonAllTails ) { | |
// and then if we have any leftover hook forward | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BACKWARD_HOOK); | |
} else if ( noteTailCount > priorTailCount ) { | |
// else if we still have more tails than prior do a hook backward | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BACKWARD_HOOK); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment