package com.xenoage.zong.io.musicxml.in.readers;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.music.Pitch;
import com.xenoage.zong.core.music.VoiceElement;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.music.chord.Grace;
import com.xenoage.zong.core.music.chord.Note;
import com.xenoage.zong.core.music.chord.Stem;
import com.xenoage.zong.core.music.rest.Rest;
import com.xenoage.zong.io.musicxml.in.util.MusicReaderException;
import com.xenoage.zong.io.musicxml.in.util.OpenBeams;
import com.xenoage.zong.musicxml.types.MxlBeam;
import com.xenoage.zong.musicxml.types.MxlNote;
import com.xenoage.zong.musicxml.types.MxlPitch;
import com.xenoage.zong.musicxml.types.choice.MxlCueNote;
import com.xenoage.zong.musicxml.types.choice.MxlFullNoteContent;
import com.xenoage.zong.musicxml.types.choice.MxlFullNoteContent.MxlFullNoteContentType;
import com.xenoage.zong.musicxml.types.choice.MxlGraceNote;
import com.xenoage.zong.musicxml.types.choice.MxlNormalNote;
import com.xenoage.zong.musicxml.types.choice.MxlNoteContent.MxlNoteContentType;
import com.xenoage.zong.musicxml.types.enums.MxlYesNo;
import com.xenoage.zong.musicxml.types.groups.MxlEditorialVoice;
import com.xenoage.zong.musicxml.types.groups.MxlFullNote;
import lombok.RequiredArgsConstructor;
import java.util.List;
import static com.xenoage.utils.NullUtils.notNull;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.iterators.It.it;
import static com.xenoage.utils.math.Fraction._0;
import static com.xenoage.utils.math.Fraction.fr;
import static com.xenoage.zong.core.position.MP.getMP;
import static com.xenoage.zong.io.musicxml.in.readers.OtherReader.readDuration;
/**
* This class reads a {@link Chord} from a given list
* of note elements.
*
* @author Andreas Wenger
*/
@RequiredArgsConstructor
public final class ChordReader {
private final List<MxlNote> mxlNotes;
private Context context;
private MxlNote mxlFirstNote;
private VoiceElement chordOrRest;
private Chord chord;
private int staff;
private String mxlVoice;
/**
* Reads the given chord, consisting of the given list of note elements,
* including beams, notations and lyrics.
* All but the first given note must have a chord-element inside.
*/
public void readToContext(Context context) {
this.context = context;
readFirstNote();
//find staff
//(not supported yet: multi-staff chords)
staff = notNull(mxlFirstNote.getStaff(), 1) - 1;
//find voice
//TODO: might not exist! we have to use a helper algorithm to determine the right voice
//then, see MusicReader class documentation.
int staffVoice = 0;
mxlVoice = null;
MxlEditorialVoice editorialVoice = mxlFirstNote.getEditorialVoice();
if (editorialVoice != null) {
mxlVoice = editorialVoice.getVoice();
if (mxlVoice != null) {
try {
staffVoice = context.getVoice(staff, mxlVoice);
}
catch (MusicReaderException ex) {
context.reportError(ex.getMessage());
return;
}
}
}
//write chord or rest
boolean isWritten = false;
if (chordOrRest != null)
isWritten = context.writeVoiceElement(chordOrRest, staff, staffVoice);
//more details for chord
if (isWritten && chord != null) {
//check if chord could be written. if not, return
if (getMP(chord) == null)
return;
readFirstNoteNotations();
readOtherChordNotes();
readStem();
readBeams();
new LyricReader(mxlNotes).readToChord(chord);
}
if (chordOrRest != null)
context.moveCursorForward(chordOrRest.getDuration());
}
private void readFirstNote() {
mxlFirstNote = mxlNotes.get(0);
MxlNoteContentType mxlFirstNoteType = mxlFirstNote.getContent().getNoteContentType();
//type of chord/rest
//(unpitched is still unsupported)
MxlFullNote mxlFirstFullNote = mxlFirstNote.getContent().getFullNote();
MxlNormalNote mxlFirstNormalNote = null;
MxlCueNote mxlFirstCueNote = null;
MxlGraceNote mxlFirstGraceNote = null;
boolean isCue = false;
boolean isGrace = false;
if (mxlFirstNoteType == MxlNoteContentType.Normal) {
mxlFirstNormalNote = (MxlNormalNote) mxlNotes.get(0).getContent();
}
else if (mxlFirstNoteType == MxlNoteContentType.Cue) {
mxlFirstCueNote = (MxlCueNote) mxlNotes.get(0).getContent();
isCue = true;
}
else if (mxlFirstNoteType == MxlNoteContentType.Grace) {
mxlFirstGraceNote = (MxlGraceNote) mxlNotes.get(0).getContent();
isGrace = true;
isCue = false; //may also be true later, see (TODO) "Zong-Library/Discussions/MusicXML/Note - cue vs grace.txt"
}
MxlFullNoteContentType mxlFNCType = mxlFirstFullNote.getContent().getFullNoteContentType();
//duration. here, the first duration is the duration of the whole chord.
Fraction duration = _0;
if (mxlFirstNormalNote != null) {
duration = readDuration(mxlFirstNormalNote.getDuration(), context.getDivisions());
}
else if (mxlFirstCueNote != null) {
duration = readDuration(mxlFirstCueNote.getDuration(), context.getDivisions());
}
//when duration of normal note is 0, ignore the chord
if (false == isGrace && false == duration.isGreater0()) {
context.reportError("duration of chord is 0");
return;
}
//create new chord or rest
if (mxlFNCType == MxlFullNoteContentType.Pitch || mxlFNCType == MxlFullNoteContentType.Unpitched) {
//create a chord
Pitch pitch;
if (mxlFNCType == MxlFullNoteContentType.Pitch)
pitch = ((MxlPitch) mxlFirstFullNote.getContent()).getPitch();
else
pitch = Pitch.pi(0, 4); //TODO (ZONG-96): better support for unpitched notes
Grace grace = null;
if (mxlFirstGraceNote != null) {
//read grace duration from note-type ("eighth", "16th", ...)
Fraction graceDuration = fr(1, 8);
if (mxlFirstNote.getNoteType() != null)
graceDuration = mxlFirstNote.getNoteType().getDuration();
boolean slash = mxlFirstGraceNote.getSlash() == MxlYesNo.Yes;
grace = new Grace(slash, graceDuration);
chord = new Chord(alist(new Note(pitch)), grace);
}
else {
chord = new Chord(alist(new Note(pitch)), duration);
}
chord.setCue(isCue);
chordOrRest = chord;
}
else if (mxlFNCType == MxlFullNoteContentType.Rest) {
//create a rest
Rest rest = new Rest(duration);
rest.setCue(isCue);
chordOrRest = rest;
}
}
private void readStem() {
Stem stem = new StemReader(mxlFirstNote.getStem()).read(context, chord, staff);
if (stem != null)
chord.setStem(stem);
}
private void readBeams() {
OpenBeams openBeams = context.getOpenElements().getOpenBeams();
//we read only the beam elements with number 1
//beam subdivisions are computed by the program itself
for (MxlBeam mxlBeam : it(mxlFirstNote.getBeams())) {
int number = mxlBeam.getNumber();
//read only level 1 beams
if (number != 1)
continue;
switch (mxlBeam.getValue()) {
case Begin: {
//open new beam
openBeams.beginBeam(chord, mxlVoice, context);
break;
}
case ForwardHook:
case BackwardHook:
case Continue: {
//add chord to beam
openBeams.continueBeam(chord, mxlVoice, context);
break;
}
case End: {
//close the beam and create it
openBeams.endBeam(chord, mxlVoice, context);
}
}
}
}
private void readFirstNoteNotations() { //TIDY: merge with notations of other notes?
if (mxlFirstNote.getNotations() != null) {
new NotationsReader(mxlFirstNote.getNotations()).readToNote(
chord, 0, staff, context); //first note has index 0
}
}
private void readOtherChordNotes() {
//collect the following notes of this chord
for (int i = 1; i < mxlNotes.size(); i++) {
addChordNote(context, mxlNotes.get(i), chord, staff);
}
}
/**
* Reads the given note element, which is part of
* a chord (but not the first note element of the chord), and adds it to the given chord.
* Also the notations of this note are read.
*/
private static void addChordNote(Context context,
MxlNote mxlNote, Chord chord, int staffIndexInPart) {
//only pitch is interesting for us, since we do not allow
//different durations for notes within a chord or other strange stuff
MxlFullNoteContent mxlFNC = mxlNote.getContent().getFullNote().getContent();
if (mxlFNC.getFullNoteContentType() == MxlFullNoteContentType.Pitch) {
Pitch pitch = ((MxlPitch) mxlFNC).getPitch();
Note note = new Note(pitch);
chord.addNote(note);
//notations
if (mxlNote.getNotations() != null) {
new NotationsReader(mxlNote.getNotations()).readToNote(chord,
chord.getNotes().indexOf(note), staffIndexInPart, context);
}
}
}
}