/* -*- c-basic-offset: 2; indent-tabs-mode: nil; -*- */
/*
* FreeDots -- MusicXML to braille music transcription
*
* Copyright 2008-2010 Mario Lang All Rights Reserved.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details (a copy is included in the LICENSE.txt file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License
* along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This file is maintained by Mario Lang <mlang@delysid.org>.
*/
package freedots.musicxml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
import java.util.Map;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import freedots.math.Fraction;
import freedots.music.Accidental;
import freedots.music.AccidentalContext;
import freedots.music.ClefChange;
import freedots.music.Event;
import freedots.music.GlobalKeyChange;
import freedots.music.KeyChange;
import freedots.music.KeySignature;
import freedots.music.MusicList;
import freedots.music.StartBar;
import freedots.music.EndBar;
import freedots.music.TimeSignature;
import freedots.music.TimeSignatureChange;
import freedots.musicxml.Note.TupletElementXML;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/** A wrapper around the MusicXML part element.
*/
public final class Part {
private static final Logger LOG = Logger.getLogger(Part.class.getName());
private final Element scorePart;
private final Score score;
/** Gets the parent {@link freedots.musicxml.Score} of this part.
*/
public Score getScore() { return score; }
private TimeSignature timeSignature = new TimeSignature(4, 4);
private final MusicList eventList = new MusicList();
/** Constructs a new part (and all its relevant child objects).
*
* @throws MusicXMLParseException if an unrecoverable error happens
*/
Part(final Element part, final Element scorePart, final Score score)
throws MusicXMLParseException {
this.scorePart = scorePart;
this.score = score;
final int divisions = score.getDivisions();
int durationMultiplier = 1;
int measureNumber = 1;
Fraction measureOffset = Fraction.ZERO;
TimeSignature lastTimeSignature = null;
Attributes.Transpose currentTranspose = null;
int staffCount = 1;
EndBar endbar = null;
boolean newMeasure = false;
final SlurBuilder slurBuilder = new SlurBuilder();
final TupletBuilder tupletBuilder = new TupletBuilder();
for (Node partNode = part.getFirstChild(); partNode != null;
partNode = partNode.getNextSibling()) {
if (partNode.getNodeType() == Node.ELEMENT_NODE
&& "measure".equals(partNode.getNodeName())) {
newMeasure = true;
final Element xmlMeasure = (Element)partNode;
StartBar startBar = new StartBar(measureOffset, measureNumber++);
startBar.setStaffCount(staffCount);
startBar.setTimeSignature(timeSignature);
eventList.add(startBar);
boolean repeatBackward = false;
int endingStop = 0;
Chord currentChord = null;
Fraction offset = Fraction.ZERO;
Fraction measureDuration = Fraction.ZERO;
for (Node measureNode = xmlMeasure.getFirstChild();
measureNode != null; measureNode = measureNode.getNextSibling()) {
if (measureNode.getNodeType() == Node.ELEMENT_NODE) {
Element musicdata = (Element)measureNode;
final String tagName = musicdata.getTagName();
if ("attributes".equals(tagName)) {
if (currentChord != null) {
offset = offset.add(currentChord.get(0).getDuration());
currentChord = null;
}
Attributes attributes = new Attributes(musicdata);
int newDivisions = attributes.getDivisions();
Attributes.Time newTimeSignature = attributes.getTime();
int newStaffCount = attributes.getStaves();
if (newDivisions > 0) {
durationMultiplier = divisions / newDivisions;
}
if (newStaffCount > 1 && newStaffCount != staffCount) {
staffCount = newStaffCount;
startBar.setStaffCount(staffCount);
}
if (newTimeSignature != null) {
if (lastTimeSignature == null) {
timeSignature = newTimeSignature;
}
lastTimeSignature = newTimeSignature;
eventList.add(new TimeSignatureChange(measureOffset.add(offset),
lastTimeSignature));
if (offset.equals(0)) {
startBar.setTimeSignature(newTimeSignature);
}
}
List<Attributes.Clef> clefs = attributes.getClefs();
if (clefs.size() > 0) {
for (Attributes.Clef clef:clefs) {
eventList.add(new ClefChange(measureOffset.add(offset),
clef, clef.getStaffNumber()));
}
}
List<Attributes.Key> keys = attributes.getKeys();
if (keys.size() > 0) {
for (Attributes.Key key: keys) {
if (key.getStaffName() == null)
eventList.add(new GlobalKeyChange(measureOffset.add(offset),
key));
else
eventList.add(new KeyChange(measureOffset.add(offset),
key,
Integer.parseInt(key.getStaffName()) - 1));
}
}
Attributes.Transpose transpose = attributes.getTranspose();
if (transpose != null) {
currentTranspose = transpose;
}
} else if ("note".equals(tagName)) {
Note note = new Note(musicdata, divisions, durationMultiplier,
currentTranspose, this);
if (currentChord != null
&& !elementHasChild(musicdata, Note.CHORD_ELEMENT)) {
offset = offset.add(currentChord.get(0).getDuration());
currentChord = null;
}
note.setMoment(measureOffset.add(offset));
boolean advanceTime = !note.isGrace();
boolean addNoteToEventList = true;
slurBuilder.visit(note);
tupletBuilder.visitNote(note,newMeasure);
if (note.getTimeModification()!=null)newMeasure=false;
if (currentChord != null) {
if (elementHasChild(musicdata, Note.CHORD_ELEMENT)) {
currentChord.add(note);
advanceTime = false;
addNoteToEventList = false;
}
}
if (currentChord == null && note.isStartOfChord()) {
currentChord = new Chord(note);
advanceTime = false;
eventList.add(currentChord);
addNoteToEventList = false;
}
if (addNoteToEventList) {
eventList.add(note);
}
if (advanceTime) {
offset = offset.add(note.getDuration());
}
} else if ("direction".equals(tagName)) {
Direction direction = new Direction(musicdata,
durationMultiplier, divisions,
measureOffset.add(offset));
eventList.add(direction);
} else if ("harmony".equals(tagName)) {
final Harmony harmony =
new Harmony(musicdata, durationMultiplier, divisions,
measureOffset.add(offset));
eventList.add(harmony);
} else if ("backup".equals(tagName)) {
if (currentChord != null) {
offset = offset.add(currentChord.get(0).getDuration());
currentChord = null;
}
final Backup backup =
new Backup(musicdata, divisions, durationMultiplier);
offset = offset.subtract(backup.getDuration());
} else if ("forward".equals(tagName)) {
if (currentChord != null) {
offset = offset.add(currentChord.get(0).getDuration());
currentChord = null;
}
Note invisibleRest = new Note(musicdata,
divisions, durationMultiplier,
currentTranspose, this);
invisibleRest.setMoment(measureOffset.add(offset));
eventList.add(invisibleRest);
offset = offset.add(invisibleRest.getDuration());
} else if ("print".equals(tagName)) {
Print print = new Print(musicdata);
if (print.isNewSystem()) startBar.setNewSystem(true);
} else if ("sound".equals(tagName)) {
Sound sound = new Sound(musicdata, measureOffset.add(offset));
eventList.add(sound);
} else if ("barline".equals(tagName)) {
Barline barline = new Barline(musicdata);
if (barline.getLocation() == Barline.Location.LEFT) {
if (barline.getRepeat() == Barline.Repeat.FORWARD) {
startBar.setRepeatForward(true);
}
if (barline.getEnding() > 0 &&
barline.getEndingType() == Barline.EndingType.START) {
startBar.setEndingStart(barline.getEnding());
}
} else if (barline.getLocation() == Barline.Location.RIGHT) {
if (barline.getRepeat() == Barline.Repeat.BACKWARD) {
repeatBackward = true;
}
if (barline.getEnding() > 0 &&
barline.getEndingType() == Barline.EndingType.STOP) {
endingStop = barline.getEnding();
}
}
} else
LOG.info("Unsupported musicdata element " + tagName);
if (offset.compareTo(measureDuration) > 0) measureDuration = offset;
}
}
if (currentChord != null) {
offset = offset.add(currentChord.get(0).getDuration());
if (offset.compareTo(measureDuration) > 0) measureDuration = offset;
currentChord = null;
}
TimeSignature activeTimeSignature = lastTimeSignature != null ? lastTimeSignature : timeSignature;
if (xmlMeasure.getAttribute("implicit").equalsIgnoreCase(Score.YES)
&& measureDuration.compareTo(timeSignature) < 0) {
measureOffset = measureOffset.add(measureDuration);
if (measureNumber == 2) {
measureNumber = 1;
startBar.setMeasureNumber(0);
}
} else {
if (measureDuration.compareTo(activeTimeSignature) != 0) {
LOG.warning("Incomplete measure "
+ xmlMeasure.getAttribute("number") + ": "
+ timeSignature + " " + measureDuration);
}
measureOffset = measureOffset.add(activeTimeSignature);
}
if (startBar.getTimeSignature() == null) {
startBar.setTimeSignature(lastTimeSignature);
}
endbar = new EndBar(measureOffset);
if (repeatBackward) endbar.setRepeat(true);
if (endingStop > 0) endbar.setEndingStop(endingStop);
eventList.add(endbar);
}
}
if (endbar != null) endbar.setEndOfMusic(true);
// Post processing
slurBuilder.buildSlurs();
tupletBuilder.buildTuplets();
if (!score.encodingSupports(Note.ACCIDENTAL_ELEMENT)) {
int staves = 1;
KeySignature defaultKeySignature = new KeySignature(0);
List<AccidentalContext> contexts = new ArrayList<AccidentalContext>();
for (int i = 0; i < staves; i++) {
contexts.add(new AccidentalContext(defaultKeySignature));
}
for (Event event: eventList) {
if (event instanceof StartBar) {
StartBar startBar = (StartBar)event;
if (startBar.getStaffCount() != staves) {
if (startBar.getStaffCount() > staves)
for (int i = 0; i < (startBar.getStaffCount() - staves); i++)
contexts.add(new AccidentalContext(defaultKeySignature));
else if (startBar.getStaffCount() < staves)
for (int i = 0; i < (staves - startBar.getStaffCount()); i++)
contexts.remove(contexts.size() - 1);
staves = startBar.getStaffCount();
}
for (AccidentalContext accidentalContext: contexts)
accidentalContext.resetToKeySignature();
} else if (event instanceof GlobalKeyChange) {
GlobalKeyChange globalKeyChange = (GlobalKeyChange)event;
defaultKeySignature = globalKeyChange.getKeySignature();
for (AccidentalContext accidentalContext: contexts)
accidentalContext.setKeySignature(defaultKeySignature);
} else if (event instanceof KeyChange) {
KeyChange keyChange = (KeyChange)event;
contexts.get(keyChange.getStaffNumber())
.setKeySignature(keyChange.getKeySignature());
} else if (event instanceof Note) {
calculateAccidental((Note)event, contexts);
} else if (event instanceof Chord) {
for (Note note: (Chord)event)
calculateAccidental(note, contexts);
}
}
}
}
private class SlurBuilder {
private final Collection<SlurBounds> slurs = new LinkedList<SlurBounds>();
private final Map<Integer, SlurBounds> slurMap =
new HashMap<Integer, SlurBounds>();
SlurBuilder() {
}
/** Checks if the given note is a slur curve point and remembers it if so.
*/
void visit(Note note) {
final Note.Notations notations = note.getNotations();
if (notations != null) {
for (Note.Notations.Slur slur: notations.getSlurs()) {
switch (slur.type()) {
case START:
slurMap.put(slur.number(), new SlurBounds(note));
break;
case CONTINUE:
if (slurMap.containsKey(slur.number())) {
slurMap.get(slur.number()).other().add(note);
}
break;
case STOP:
if (slurMap.containsKey(slur.number())) {
final SlurBounds bounds = slurMap.get(slur.number());
bounds.setEnd(note);
if (slurs.add(bounds)) {
slurMap.remove(slur.number());
}
}
break;
default: throw new AssertionError(slur.type());
}
}
}
}
/** Creates slurs from the collected start and end points.
*/
void buildSlurs() {
if (slurMap.size() != 0)
LOG.warning("Unterminated slurs: "+slurMap.size());
for (SlurBounds bounds: slurs) {
Note note = bounds.begin();
final Slur slur = new Slur(note);
while (note != bounds.end()) {
final Fraction nextMoment = note.getMoment().add(note.getDuration());
final Collection<Note> notes = notesAt(nextMoment);
if (notes.size() == 1) {
slur.add(note = notes.iterator().next());
} else if (notes.contains(bounds.end())) {
slur.add(note = bounds.end());
} else if (notes.size() == 0) {
LOG.warning("0 slur targets: '"+nextMoment+"','"
+note.getMoment()+"','"+note+"'");
break;
} else {
boolean found = false;
for (Note n: notes) {
if (bounds.other().contains(n)
|| n.getVoiceName().equals(note.getVoiceName())) {
slur.add(note = n);
found = true;
break;
}
}
if (!found) {
LOG.warning("Notes:"+notes);
slur.add(note = notes.iterator().next());
}
}
}
}
slurs.clear();
}
private class SlurBounds {
private Note begin, end;
private final Collection<Note> other = new ArrayList<Note>();
SlurBounds(Note begin) { this.begin = begin; }
void setEnd(Note end) { this.end = end; }
Note begin() { return begin; }
Note end() { return end; }
Collection<Note> other() { return other; }
}
}
/** Build tuplets
*/
private class TupletBuilder {
private final LinkedList<LinkedList<Note>> map = new LinkedList<LinkedList<Note>>();
public TupletBuilder() { }
/** Group note with TimeModification by measure in map according to newMeasure
*/
void visitNote(Note note, boolean newMeasure) {
if (note.getTimeModification() != null) {
if (newMeasure)
map.add(new LinkedList<Note>());
map.getLast().add(note);
}
}
/** Complete tuplet with note and if necessary with others notes in linkedListNotes
*/
void completeTuplet(Tuplet tuplet, Note note, LinkedList<Note> linkedListNotes){
if (note != null) {
final Note.Notations notations = note.getNotations();
if (notations != null && notations.tupletElementXMLMaxNumber()>1) { //nested tuplet
boolean hasStart = false;
boolean firstStop = true;
boolean hasStop = false;
Tuplet lastTuplet = tuplet;
for (TupletElementXML tupletElementXML: notations.getTupletElementsXML()) {
switch (tupletElementXML.type()) {
case START:
if (tupletElementXML.number() != 1) {
lastTuplet.addTuplet(new Tuplet());
lastTuplet = (Tuplet)lastTuplet.getLast();
}
lastTuplet.setActualType(tupletElementXML.getActualType());
lastTuplet.setNormalType(tupletElementXML.getNormalType());
hasStart = true;
break;
case STOP:
if (firstStop) {
tuplet.addNote(note);
firstStop = false;
hasStop = true;
}
lastTuplet = (Tuplet)tuplet.getParent();
break;
default: throw new AssertionError(tupletElementXML.type());
}
}
if (hasStart && hasStop)
throw new AssertionError("A note can't be at the beginning AND the end of tuplets. MusicXML file is corrupted.");
if (hasStart) lastTuplet.addNote(note);
if (lastTuplet != null) //tuplet is not yet complete
completeTuplet(lastTuplet, nextNoteOfTuplet(linkedListNotes, note),
linkedListNotes);
} else {
tuplet.addNote(note);
if (!tuplet.completed())
completeTuplet(tuplet, nextNoteOfTuplet(linkedListNotes, note),
linkedListNotes);
}
} else LOG.warning("Tuplet can't be completed, Notes:"+tuplet);
}
/** Build tuplets of the score
*/
void buildTuplets() {
for (LinkedList<Note> linkedListNote: map){
Note note=null;
List<String> voiceList=new LinkedList<String>();
while((note=firstNoteVoice(linkedListNote,voiceList))!=null){
while(note != null) {
Tuplet tuplet = new Tuplet();
completeTuplet(tuplet, note, linkedListNote);
while (!(tuplet.getLast() instanceof Note))
tuplet=(Tuplet)tuplet.getLast();
note = (Note)tuplet.getLast();
note = nextNoteVoice(linkedListNote, note);
}
}
}
}
/** @return next note, after note, of the tuplet which contains note
* (nextMoment, no tuplet, same voice) in linkedListNotes
* null otherwise
*/
public Note nextNoteOfTuplet(LinkedList<Note> linkedListNotes, Note note) {
final Fraction nextMoment = note.getMoment().add(note.getDuration());
for (Note n: linkedListNotes){
if (n.getMoment().equals(nextMoment)) {
if (n.getTuplet() == null
&& n.getVoiceName().equals(note.getVoiceName())){
return n;
}
}
}
return null;
}
/** @return the first note of the score in linkedListNotes which does not
* belong to a voice referenced in voiceList
* null otherwise
*/
public Note firstNoteVoice(final LinkedList<Note> linkedListNotes,
final List<String> voiceList) {
Note firstNote = null;
Fraction minMoment = null;
//select a note in a voice never seen before
for (Note note: linkedListNotes) {
if(!voiceList.contains(note.getVoiceName())){
firstNote = note;
minMoment = firstNote.getMoment();
voiceList.add(firstNote.getVoiceName());
break;
}
}
// select the first note in the same voice than firstNote
if (firstNote != null) {
for (Note note: linkedListNotes) {
if (note.getMoment().compareTo(minMoment)<0
&& firstNote.getVoiceName().equals(note.getVoiceName())) {
firstNote = note;
minMoment = firstNote.getMoment();
}
}
}
return firstNote;
}
/** @return the next note in linkedListNotes in the same voice than note
* null otherwise
*/
public Note nextNoteVoice(LinkedList<Note> linkedListNotes, Note note){
Fraction currentMoment = note.getMoment();
Note nextNote = null;
final Fraction nextMoment = note.getMoment().add(note.getDuration());
for (Note n: linkedListNotes) {
if (n.getTuplet() == null && (n.getMoment().compareTo(currentMoment)>0)
&& n.getVoiceName().equals(note.getVoiceName())) {
if (n.getMoment().equals(nextMoment))
return n;
if (nextNote == null)
nextNote = n;
else if (n.getMoment().compareTo(nextNote.getMoment())<0)
nextNote = n;
}
}
return nextNote;
}
}
/** Returns a list of all Note objects at a given musical offset.
* If a chord appears at that offset, all of its notes are returned
* separately.
*/
private Collection<Note> notesAt(final Fraction offset) {
Collection<Note> notes = new ArrayList<Note>();
for (Event event: eventList.eventsAt(offset)) {
if (event instanceof Chord) {
notes.addAll((Chord)event);
} else if (event instanceof Note) {
notes.add((Note)event);
}
}
return notes;
}
private void calculateAccidental (
Note note, List<AccidentalContext> contexts
) {
Pitch pitch = note.getPitch();
if (pitch != null) {
int staffNumber = note.getStaffNumber();
AccidentalContext accidentalContext = contexts.get(staffNumber);
Accidental accidental = null;
if (pitch.getAlter() != accidentalContext.getAlter(pitch.getOctave(),
pitch.getStep())) {
accidental = Accidental.fromAlter(pitch.getAlter());
if (accidental != null)
note.setAccidental(accidental);
}
accidentalContext.accept(pitch, accidental);
}
}
public MidiInstrument getMidiInstrument(String id) {
NodeList nodeList = scorePart.getElementsByTagName("midi-instrument");
for (int index = 0; index < nodeList.getLength(); index++) {
MidiInstrument instrument = new MidiInstrument((Element)nodeList.item(index));
if (id == null) return instrument;
if (id.equals(instrument.getId())) return instrument;
}
return null;
}
public TimeSignature getTimeSignature() { return timeSignature; }
public KeySignature getKeySignature() {
for (Object event:eventList) {
if (event instanceof GlobalKeyChange) {
GlobalKeyChange globalKeyChange = (GlobalKeyChange)event;
return globalKeyChange.getKeySignature();
}
}
return new KeySignature(0);
}
/** Gets a flat list of all events occuring in thsi part.
* Events are ordered from left to right with increasing time.
* Several events can have the same musical offset.
* Chords are represented with container objects.
* TODO: Group events by measure.
*/
public MusicList getMusicList () { return eventList; }
/** Gets the name of this part.
* @return the part name as a string, or null if printing is not adviced
*/
public String getName() {
for (Node node = scorePart.getFirstChild(); node != null;
node = node.getNextSibling()) {
if (node.getNodeType() == Node.ELEMENT_NODE
&& "part-name".equals(node.getNodeName())) {
Element partName = (Element)node;
if (!"no".equals(partName.getAttribute("print-object"))) {
return partName.getTextContent();
}
}
}
return null;
}
/** Gets a list of all directives contained in this part.
* Directives are special directions usually occuring at the start of
* the piece.
*/
public List<Direction> getDirectives() {
final List<Direction> directives = new ArrayList<Direction>();
for (Event event: eventList) {
if (event instanceof Direction) {
final Direction direction = (Direction)event;
if (direction.isDirective()) {
final String directive = direction.getWords();
if (directive != null && !directive.isEmpty()) {
directives.add(direction);
}
}
}
}
return directives;
}
static boolean elementHasChild(Element element, String tagName) {
for (Node node = element.getFirstChild(); node != null;
node = node.getNextSibling())
if (node.getNodeType() == Node.ELEMENT_NODE
&& node.getNodeName().equals(tagName)) return true;
return false;
}
}