Created
December 20, 2021 08:37
-
-
Save alanwhite/cc9759bd656f8594533a909416bba5ec to your computer and use it in GitHub Desktop.
Base class with utility methods for classes implementing automatic beaming algos
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.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; | |
/** | |
* Contains methods common to any autobeam algorithm implementation. | |
* | |
* @author Alan R. White | |
* | |
*/ | |
abstract class AbstractAutoBeam { | |
/** | |
* Analyzes and updates notes for all parts in a measure to set their beams appropriately. | |
* | |
* @param mcx the musical context to apply containing time signature and other needed info | |
* @param measure the measure containing the parts to be managed | |
*/ | |
public void forMeasure(MusicalContext mcx, MeasureModel measure) { | |
measure.getParts().forEach(part -> this.forPart(mcx, part)); | |
} | |
/** | |
* Clears any existing beams in a part, if not manually set, then sub class must | |
* analyze and update notes in the part to set their beams appropriately. | |
* | |
* @param mcx the musical context to apply containing time signature and other needed info | |
* @param part the part containing the notes to be managed | |
*/ | |
protected void forPart(MusicalContext mcx, PartModel part) { | |
// first clear beams on all notes in the part that are not manually set | |
part.getStaffElements().stream() | |
.filter(note -> note instanceof BaseNoteModel) | |
.filter(note -> !((BaseNoteModel) note).isManualBeams()) | |
.forEach(note -> ((BaseNoteModel) note).setBeams(Optional.empty())); | |
} | |
/** | |
* Updates a note at the start of a beat with the correct beams. | |
* | |
* @param notes | |
* @param index | |
*/ | |
protected void firstNoteInBeat(List<BaseNoteModel> notes, int index) { | |
var note = notes.get(index); | |
var nextTailCount = this.getNextNoteTails(notes, index); | |
var noteTailCount = this.tailsPerNote(notes.get(index)); | |
var commonTails = Math.min(nextTailCount,noteTailCount); | |
if ( noteTailCount > 0 ) | |
note.setBeams(Optional.of(new ArrayList<BeamEnum>())); | |
var beamIndex = 0; | |
// do a BEGIN for each one in common with next | |
while( beamIndex < commonTails ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BEGIN); | |
// do a hook forward for any left | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.FORWARD_HOOK); | |
} | |
/** | |
* Updates a note at the end of a beat with the correct beams | |
* | |
* @param notes | |
* @param index | |
*/ | |
protected void lastNoteInBeat(List<BaseNoteModel> notes, int index) { | |
var note = notes.get(index); | |
var priorTailCount = this.getPriorNoteTails(notes, index); | |
var noteTailCount = this.tailsPerNote(note); | |
var commonTails = Math.min(priorTailCount,noteTailCount); | |
if ( noteTailCount > 0 ) | |
note.setBeams(Optional.of(new ArrayList<BeamEnum>())); | |
var beamIndex = 0; | |
// do an END for each one in common with next | |
while( beamIndex < commonTails ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.END); | |
// do a hook forward for any left | |
while( beamIndex < noteTailCount ) | |
note.getBeams().get().add(beamIndex++,BeamEnum.BACKWARD_HOOK); | |
} | |
/** | |
* Obtain the tails the next note has. | |
* | |
* @param part contains the notes we need to work through | |
* @param index the index of the note we want to inspect forward from | |
* @return number of tails of next note or zero if no next note | |
*/ | |
protected int getNextNoteTails(List<BaseNoteModel> notes, int index) { | |
if ( notes.size() <= index+1 ) | |
return 0; | |
return tailsPerNote(notes.get(index+1)); | |
} | |
/** | |
* Obtain the tails the prior note has. | |
* | |
* @param part contains the notes we need to work through | |
* @param index the index of the note we want to inspect backward from | |
* @return number of tails of prior note or zero if no prior note | |
*/ | |
protected int getPriorNoteTails(List<BaseNoteModel> notes, int index) { | |
if ( index == 0 ) | |
return 0; | |
return tailsPerNote(notes.get(index-1)); | |
} | |
/** | |
* Determine the prior note's beam at a given level, protecting against non-existence | |
* | |
* @param notes | |
* @param index | |
* @param beamIndex | |
* @return | |
*/ | |
protected BeamEnum getPriorNoteBeam(List<BaseNoteModel> notes, int index, int beamIndex) { | |
// if no prior make logic behave like it needs to start a new beam or hook forward | |
if ( index == 0 ) | |
return BeamEnum.NONE; | |
if ( notes.get(index-1).getBeams().isEmpty() ) | |
return BeamEnum.NONE; | |
var beams = notes.get(index-1).getBeams().get(); | |
if ( beamIndex >= beams.size() ) | |
return BeamEnum.NONE; | |
return beams.get(beamIndex); | |
} | |
/** | |
* Breaks down a flat list of notes in a measure into groups by beat | |
* | |
* @param staffElements elements in a measure including notes | |
* @param divsPerBeat number of divisions set in a beat in this score | |
* @return a list of beats, each beat containing a list of notes | |
*/ | |
public List<List<BaseNoteModel>> noteElementsByBeat( | |
List<StaffElementModel> staffElements, int divsPerBeat) { | |
// ferret out any non-notes | |
List<BaseNoteModel> notes = staffElements.stream() | |
.filter(element -> (element instanceof BaseNoteModel)) | |
.map(element -> (BaseNoteModel) element) | |
.collect(Collectors.toList()); | |
int beatCursor = 0; | |
List<List<BaseNoteModel>> beats = new ArrayList<>(); | |
List<BaseNoteModel> currentBeat = null; | |
for (int noteIndex = 0; noteIndex < notes.size(); noteIndex++) { | |
var note = notes.get(noteIndex); | |
if ( beatCursor >= divsPerBeat ) | |
beatCursor = 0; | |
if ( beatCursor == 0 ) { | |
currentBeat = new ArrayList<BaseNoteModel>(); | |
beats.add(currentBeat); | |
} | |
currentBeat.add(note); | |
beatCursor += note.getDuration(); | |
} | |
return beats; | |
} | |
/** | |
* DRY helper to calculate the tails on a note. | |
* | |
* @param note that whose tail count we want | |
* @return the tail count | |
*/ | |
protected int tailsPerNote(BaseNoteModel note) { | |
var noteValue = note.getNoteValue(); | |
return switch(noteValue) { | |
case BaseNoteModel.quaver -> 1; | |
case BaseNoteModel.semiQuaver -> 2; | |
case BaseNoteModel.demiSemiQuaver -> 3; | |
case BaseNoteModel.hemiDemiSemiQuaver -> 4; | |
default -> 0; | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment