package com.xenoage.zong.io.musicxml.in.util;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.io.musicxml.in.readers.Context;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.xenoage.utils.backports.ListBackports.sort;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.collections.CollectionUtils.getFirst;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.kernel.Range.rangeReverse;
/**
* Unclosed beams, needed during MusicXML import.
*
* Beams are distinguished by being in different voices and/or
* the presence or absence of grace and cue elements.
*
* Currently subdivisions in beams are not loaded from MusicXML,
* but computed by the program itself. Thus, only beam elements with
* number 1 (8th line) are read.
*
* @author Andreas Wenger
*/
public class OpenBeams {
//list index: category (see constants below)
//map key: voice (may be any string; null is permitted)
private List<Map<String, OpenBeam>> openBeams;
private static final int catsCount = 4;
private static final int catCue = 1 << 0;
private static final int catGrace = 1 << 1;
public OpenBeams() {
openBeams = alist(catsCount);
for (int i = 0; i < catsCount; i++)
openBeams.add(new HashMap<>());
}
/**
* Begins a new beam with the given chord.
* If a beam was already open with this context, it is tried to be overwritten.
*/
public void beginBeam(Chord chord, String voice, Context context) {
OpenBeam openBeam = getOpenBeam(voice, chord);
if (openBeam != null) {
context.reportError("Beginning a new beam, although there is still an open one");
//create open beam, as far as it is known
createBeam(context, openBeam);
}
openBeam = new OpenBeam();
openBeam.addChord(chord);
int category = getCategory(chord);
openBeams.get(category).put(voice, openBeam);
}
/**
* Adds a chord to an existing beam.
*/
public void continueBeam(Chord chord, String voice, Context context) {
OpenBeam openBeam = getOpenBeamTolerant(voice, chord);
if (openBeam == null) {
context.reportError("Can not continue beam which was not started");
beginBeam(chord, voice, context); //begin new beam instead
}
else {
openBeam.addChord(chord);
}
}
/**
* Ends an existing beam with the given chord and creates the beam.
*/
public void endBeam(Chord chord, String voice, Context context) {
OpenBeam openBeam = getOpenBeamTolerant(voice, chord);
if (openBeam == null) {
context.reportError("Can not end beam which was not started");
}
else {
openBeam.addChord(chord);
openBeams.get(getCategory(chord)).remove(voice);
createBeam(context, openBeam);
}
}
/**
* Creates a beam for the given beamed chords.
* Chords, that do not exist any more (when their {@link MP} is unknown)
* or which are not in the same measure are removed from the beam.
* The chords are sorted by beat. The chords may be mixed up beforehand
* because of wrong backup elements in the MusicXML score.
* Only beams with two or more notes are created.
*/
private void createBeam(Context context, OpenBeam openBeam) {
List<Chord> beamedChords = openBeam.getChords();
//remove missing chords
for (int i : rangeReverse(beamedChords)) {
if (beamedChords.get(i).getMP() == null)
beamedChords.remove(i);
}
//remove chords, which are in other measures
if (beamedChords.size() == 0)
return;
int measure = getFirst(beamedChords).getMP().measure;
for (int i : rangeReverse(beamedChords)) {
if (beamedChords.get(i).getMP().measure != measure)
beamedChords.remove(i);
}
//sort by beat
sort(beamedChords, c -> c.getMP().getBeat());
//create beam
if (beamedChords.size() > 1)
context.writeBeam(beamedChords);
}
/**
* Gets the existing {@link OpenBeam} for the given voice and grace/cue status.
* If it does not exist, another open beam with or without grace or cue is searched.
* If it can still not be found, null is returned.
*/
private OpenBeam getOpenBeamTolerant(String voice, Chord chord) {
//find perfect match
OpenBeam openBeam = getOpenBeam(voice, chord);
if (openBeam != null)
return openBeam;
//if not found, try to find an open beam with/without grace/cue
for (int category : range(catsCount)) {
openBeam = openBeams.get(category).get(voice);
if (openBeam != null)
return openBeam;
}
//still not found
return null;
}
/**
* Gets the existing {@link OpenBeam} for the given voice and grace/cue status.
* If it does not exist, null is returned.
*/
private OpenBeam getOpenBeam(String voice, Chord chord) {
int category = getCategory(chord);
return openBeams.get(category).get(voice);
}
private int getCategory(Chord chord) {
return (chord.isCue() ? catCue : 0) + (chord.isGrace() ? catGrace : 0);
}
}