package com.xenoage.zong.io.musicxml.in.readers;
import static com.xenoage.utils.collections.CollectionUtils.getFirst;
import static com.xenoage.utils.collections.CollectionUtils.getLast;
import static com.xenoage.zong.core.music.chord.Stem.defaultStem;
import java.util.List;
import lombok.RequiredArgsConstructor;
import com.xenoage.utils.math.VSide;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.MusicContext;
import com.xenoage.zong.core.music.Pitch;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.music.chord.Stem;
import com.xenoage.zong.core.music.chord.StemDirection;
import com.xenoage.zong.musicxml.types.MxlNote;
import com.xenoage.zong.musicxml.types.MxlStem;
import com.xenoage.zong.musicxml.types.attributes.MxlPosition;
/**
* Reads a {@link Stem} from the given {@link MxlNote} and the context.
*
* @author Andreas Wenger
*/
@RequiredArgsConstructor
public class StemReader {
private final MxlStem mxlStem;
/**
* Reads and returns the stem of the given chord.
* If not available, {@link Stem#defaultStem} is returned.
* @param context the global context
* @param chord the chord, whose notes are already collected
* @param staff the staff index of the current chord
*/
public Stem read(Context context, Chord chord, int staff) {
if (mxlStem == null)
return defaultStem;
//direction
StemDirection direction = readStemDirection();
//length
Float length = null;
MxlPosition yPos = mxlStem.getPosition();
if (yPos.getDefaultY() != null) {
//convert position in tenths relative to topmost staff line into
//a length in interline spaces measured from the outermost chord note on stem side
float stemEndLinePosition = convertDefaultYToLP(context, yPos.getDefaultY(), staff);
VSide side = (direction == StemDirection.Up ? VSide.Top : VSide.Bottom);
length = Math.abs(stemEndLinePosition -
getOuterNoteLp(context, chord, side, staff)) / 2;
}
//create stem
return new Stem(direction, length);
}
private StemDirection readStemDirection() {
switch (mxlStem.getValue()) {
case None:
return StemDirection.None;
case Up:
return StemDirection.Up;
case Down:
return StemDirection.Down;
case Double:
return StemDirection.Up; //currently double is not supported
}
return null;
}
/**
* Converts the given default-y position in global tenths (that is always
* relative to the topmost staff line) to a line position, using the
* musical context from the given staff.
*/
private float convertDefaultYToLP(Context context, float defaultY, int staff) {
Score score = context.getScore();
float defaultYInMm = defaultY * score.getFormat().getInterlineSpace() / 10;
float interlineSpace = score.getInterlineSpace(context.getPartStaffIndices().getStart() +
staff);
int linesCount = context.getStaffLinesCount(staff);
return 2 * (linesCount - 1) + 2 * defaultYInMm / interlineSpace;
}
/**
* Gets the line position of the note at the bottom or top side of the given chord.
*/
private static float getOuterNoteLp(Context context, Chord chord, VSide side, int staff) {
MusicContext mc = context.getMusicContext(staff);
List<Pitch> pitches = chord.getPitches();
return mc.getLp(side == VSide.Top ? getLast(pitches) : getFirst(pitches));
}
}