/* -*- 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.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.logging.Logger; import java.util.Map; import java.util.Set; import freedots.math.AbstractFraction; import freedots.math.Fraction; import freedots.math.PowerOfTwo; import freedots.music.Accidental; import freedots.music.Articulation; import freedots.music.AugmentedPowerOfTwo; import freedots.music.Clef; import freedots.music.Event; import freedots.music.Fermata; import freedots.music.Fingering; import freedots.music.KeySignature; import freedots.music.Ornament; import freedots.music.Staff; import freedots.music.RhythmicElement; import freedots.music.Syllabic; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; /** A wrapper around (the most important) note element. */ public final class Note implements RhythmicElement, freedots.music.TupletElement { private static final Logger LOG = Logger.getLogger(Note.class.getName()); static final String ACCIDENTAL_ELEMENT = "accidental"; static final String CHORD_ELEMENT = "chord"; private static final String LYRIC_ELEMENT = "lyric"; private static final String NOTATIONS_ELEMENT = "notations"; private static final String STAFF_ELEMENT = "staff"; private static final String TIME_MODIFICATION_ELEMENT = "time-modification"; private static final String TUPLET_ELEMENT = "tuplet"; private final int divisions, durationMultiplier; private final Element element; private Part part; public Part getPart() { return part; } private Fraction moment; private Staff staff = null; private Element grace = null; private Pitch pitch = null; private Unpitched unpitched = null; private Text duration = null; private Text staffNumber, voiceName; private Element type; private static final Map<String, PowerOfTwo> TYPE_MAP = Collections.unmodifiableMap(new HashMap<String, PowerOfTwo>() { { put("long", AugmentedPowerOfTwo.LONGA); put("breve", AugmentedPowerOfTwo.BREVE); put("whole", AugmentedPowerOfTwo.SEMIBREVE); put("half", AugmentedPowerOfTwo.MINIM); put("quarter", AugmentedPowerOfTwo.CROTCHET); put("eighth", AugmentedPowerOfTwo.QUAVER); put("16th", AugmentedPowerOfTwo.SEMIQUAVER); put("32nd", AugmentedPowerOfTwo.DEMISEMIQUAVER); put("64th", AugmentedPowerOfTwo.HEMIDEMISEMIQUAVER); put("128th", AugmentedPowerOfTwo.SEMIHEMIDEMISEMIQUAVER); put("256th", new PowerOfTwo(-8)); } }); public static PowerOfTwo parseType(Element type) { assert type != null; final String sanitizedTypeName = type.getTextContent().trim().toLowerCase(); if (TYPE_MAP.containsKey(sanitizedTypeName)) return TYPE_MAP.get(sanitizedTypeName); else { LOG.warning("Illegal <type> content '" + type.getTextContent() + "', " + "guessing using the duration element"); return null; } } private List<Element> dot = new ArrayList<Element>(3); private Accidental accidental = null; private static final Map<String, Accidental> accidentalMap = Collections.unmodifiableMap(new HashMap<String, Accidental>() { { put("natural", Accidental.NATURAL); put("flat", Accidental.FLAT); put("flat-flat", Accidental.DOUBLE_FLAT); put("sharp", Accidental.SHARP); put("sharp-sharp", Accidental.DOUBLE_SHARP); put("double-sharp", Accidental.DOUBLE_SHARP); } }); private Element tie = null; private TimeModification timeModification = null; public TimeModification getTimeModification() { return timeModification; } private Lyric lyric = null; private Notations notations = null; private Tuplet tuplet = null; //A note is in only one tuplet which can be in others public Tuplet getTuplet() { return tuplet; } void addTuplet(Tuplet tuplet) { this.tuplet = tuplet; } private Attributes.Transpose transpose = null; Note(final Element element, final int divisions, final int durationMultiplier, final Attributes.Transpose transpose, final Part part) throws MusicXMLParseException { this.element = element; this.divisions = divisions; this.durationMultiplier = durationMultiplier; this.transpose = transpose; this.part = part; parseDOM(); } void setMoment(final Fraction moment) { this.moment = moment; } private void parseDOM() { for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element)node; if (child.getTagName().equals("grace")) { grace = child; } else if (child.getTagName().equals("pitch")) { pitch = new Pitch(child, transpose); } else if (child.getTagName().equals("unpitched")) { unpitched = new Unpitched(child); } else if (child.getTagName().equals("duration")) { duration = firstTextNode(child); } else if (child.getTagName().equals("tie")) { tie = child; } else if (child.getTagName().equals("voice")) { voiceName = firstTextNode(child); } else if (child.getTagName().equals("type")) { type = child; } else if (child.getTagName().equals("dot")) { dot.add(child); } else if (child.getTagName().equals(ACCIDENTAL_ELEMENT)) { if (part.getScore().encodingSupports(ACCIDENTAL_ELEMENT)) { final String accidentalName = child.getTextContent(); final String santizedName = accidentalName.trim().toLowerCase(); if (accidentalMap.containsKey(santizedName)) accidental = accidentalMap.get(santizedName); else throw new MusicXMLParseException("Illegal <accidental>" + accidentalName + "</accidental>"); } } else if (child.getTagName().equals(TIME_MODIFICATION_ELEMENT)) { timeModification = new TimeModification(child); } else if (child.getTagName().equals(STAFF_ELEMENT)) { staffNumber = firstTextNode(child); } else if (child.getTagName().equals(NOTATIONS_ELEMENT)) { notations = new Notations(child); } else if (child.getTagName().equals(LYRIC_ELEMENT)) { lyric = new Lyric(child); } } } } static Text firstTextNode(Node node) { for (Node child = node.getFirstChild(); node != null; node = node.getNextSibling()) { if (child.getNodeType() == Node.TEXT_NODE) return (Text)child; } return null; } public boolean isGrace() { return (grace != null); } public boolean isRest() { if ("forward".equals(element.getTagName()) || element.getElementsByTagName("rest").getLength() > 0) return true; return false; } public Pitch getPitch() { return pitch; } public Unpitched getUnpitched() { return unpitched; } public int getStaffNumber() { if (staffNumber != null) { return Integer.parseInt(staffNumber.getWholeText()) - 1; } return 0; } public String getVoiceName() { if (voiceName != null) { return voiceName.getWholeText(); } return null; } public void setVoiceName(String name) { if (voiceName != null) { voiceName.replaceWholeText(name); } } /** Gets the relative duration of this note. * @return the numerator, denominator, the amount of dots and the time * modification involved in the actual duration represented. */ public AugmentedPowerOfTwo getAugmentedFraction() { PowerOfTwo base = null; if (type != null) base = parseType(type); if (base != null) { int normalNotes = 1; int actualNotes = 1; if (timeModification != null) { normalNotes = timeModification.getNormalNotes(); actualNotes = timeModification.getActualNotes(); } return new AugmentedPowerOfTwo(base, dot.size(), normalNotes, actualNotes); } else { return AugmentedPowerOfTwo.valueOf(getDuration()); } } public Accidental getAccidental() { return accidental; } void setAccidental(Accidental accidental) { this.accidental = accidental; if (part.getScore().encodingSupports(ACCIDENTAL_ELEMENT)) { String accidentalName = null; if (accidental != null) { if (accidental.getAlter() == 0) accidentalName = "natural"; else if (accidental.getAlter() == -1) accidentalName = "flat"; else if (accidental.getAlter() == 1) accidentalName = "sharp"; } Node node; for (node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { if (node.getNodeName().equals(ACCIDENTAL_ELEMENT)) { if (accidental != null) { if (accidentalName != null) node.setTextContent(accidentalName); } else { element.removeChild(node); } return; } } } for (node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { if (node.getNodeName().equals(TIME_MODIFICATION_ELEMENT) || node.getNodeName().equals("stem") || node.getNodeName().equals("notehead") || node.getNodeName().equals(STAFF_ELEMENT) || node.getNodeName().equals("beam") || node.getNodeName().equals(NOTATIONS_ELEMENT) || node.getNodeName().equals(LYRIC_ELEMENT)) break; } } Element newElement = element.getOwnerDocument().createElement(ACCIDENTAL_ELEMENT); newElement.setTextContent(accidentalName); element.insertBefore(newElement, node); } } public boolean isTieStart() { if (tie != null && tie.getAttribute("type").equals("start")) return true; return false; } public Fraction getMoment() { return moment; } public Staff getStaff() { return staff; } public void setStaff(Staff staff) { this.staff = staff; } /** A wrapper around MusicXML element tuplet */ static class TupletElementXML { private Element element; enum Type { START, STOP; } private final Type type; private final Integer number; public TupletElementXML(Element element){ this.element=element; number = element.hasAttribute("number")? new Integer(element.getAttribute("number")): new Integer(1); type = Enum.valueOf(Type.class, element.getAttribute("type").trim().toUpperCase()); } public int number() { return number; } public Type type() { return type; } public Fraction getActualType() { for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { if (node.getNodeName().equals("tuplet-actual")){ return getType(node); } } } return null; } private Fraction getType (Node node) { int num=0; PowerOfTwo normalType=null; num = Integer.parseInt(Score.getTextNode((Element)node, "tuplet-number").getWholeText()); for (Node node2 = node.getFirstChild(); node2 != null; node2 = node2.getNextSibling()) if (node2.getNodeType() == Node.ELEMENT_NODE) if (node2.getNodeName().equals("tuplet-type")) normalType = Note.parseType((Element)node2); return new Fraction(num*normalType.numerator(),normalType.denominator()); } public Fraction getNormalType () { for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { if (node.getNodeName().equals("tuplet-normal")) { return getType(node); } } } return null; } } /** A wrapper around MusicXML element time-modification */ class TimeModification { private final Element element; private int actualNotes, normalNotes; private PowerOfTwo normalType = null; public TimeModification(final Element element) { this.element = element; for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element)node; if (child.getTagName().equals("normal-notes")) { normalNotes = Integer.parseInt(child.getTextContent()); } else if (child.getTagName().equals("actual-notes")) { actualNotes = Integer.parseInt(child.getTextContent()); } else if (child.getTagName().equals("normal-type")) { normalType = Note.parseType(child); } } } if (normalType == null) { if (type != null) { normalType = Note.parseType(type); } } } public int getActualNotes() { return actualNotes; } public int getNormalNotes() { return normalNotes; } public PowerOfTwo getNormalType() { return normalType; } } static class Lyric implements freedots.music.Lyric { Element element; Lyric(Element element) { this.element = element; } public String getText() { Text textNode = Score.getTextNode(element, "text"); if (textNode != null) return textNode.getWholeText(); return ""; } public Syllabic getSyllabic() { Text textNode = Score.getTextNode(element, "syllabic"); if (textNode != null) { return Enum.valueOf(Syllabic.class, textNode.getWholeText().toUpperCase()); } return null; } } public Lyric getLyric() { return lyric; } public boolean equalsIgnoreOffset(Event object) { if (object instanceof Note) { Note other = (Note)object; if (getAugmentedFraction().equals(other.getAugmentedFraction())) { if (getAccidental() == other.getAccidental()) { if (getPitch() == null) { return (other.getPitch() == null); } else { return getPitch().equals(other.getPitch()); } } } } return false; } @Override public boolean equals(Object object) { if (object instanceof Note) { Note other = (Note)object; if (getMoment().equals(other.getMoment())) { return equalsIgnoreOffset(other); } } return false; } public int getMidiChannel() { MidiInstrument instrument = part.getMidiInstrument(null); if (instrument != null) return instrument.getMidiChannel(); return 0; } public int getMidiProgram() { MidiInstrument instrument = part.getMidiInstrument(null); if (instrument != null) return instrument.getMidiProgram(); return 0; } public AbstractFraction getDuration() throws MusicXMLParseException { if (duration != null) { int value = Math.round(Float.parseFloat(duration.getNodeValue())); return new Fraction(value * durationMultiplier, 4 * divisions); } return getAugmentedFraction(); } public Notations getNotations() { return notations; } private Notations createNotations() { Node node; for (node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { if (node.getNodeName().equals(LYRIC_ELEMENT)) break; } } Element newElement = element.getOwnerDocument().createElement(NOTATIONS_ELEMENT); element.insertBefore(newElement, node); notations = new Notations(newElement); return notations; } private List<Slur> slurs = new ArrayList<Slur>(2); public List<Slur> getSlurs() { return slurs; } void addSlur(Slur slur) { slurs.add(slur); } public Fingering getFingering() { if (notations != null) { Notations.Technical technical = notations.getTechnical(); if (technical != null) { return technical.getFingering(); } } return new Fingering(); } public void setFingering(Fingering fingering) { if (notations == null) createNotations(); Notations.Technical technical = notations.getTechnical(); if (technical == null) { technical = notations.createTechnical(); } technical.setFingering(fingering); } public Fermata getFermata() { if (notations != null) { return notations.getFermata(); } return null; } public Set<Articulation> getArticulations() { if (notations != null) { return notations.getArticulations(); } return EnumSet.noneOf(Articulation.class); } public Set<Ornament> getOrnaments() { if (notations != null) { return notations.getOrnaments(); } return EnumSet.noneOf(Ornament.class); } public Clef getClef() { if (staff != null) { return staff.getClef(moment); } return null; } public KeySignature getActiveKeySignature() { if (staff != null) { return staff.getKeySignature(moment); } return null; } static class Notations { private static final Map<String, Articulation> ARTICULATION_MAP = Collections.unmodifiableMap(new HashMap<String, Articulation>() { { put("accent", Articulation.accent); put("strong-accent", Articulation.strongAccent); put("breath-mark", Articulation.breathMark); put("staccato", Articulation.staccato); put("staccatissimo", Articulation.staccatissimo); put("tenuto", Articulation.tenuto); } }); private static final String TECHNICAL_ELEMENT = "technical"; private static final String FERMATA_ELEMENT = "fermata"; private static final String ARTICULATIONS_ELEMENT = "articulations"; private Element element; private Technical technical = null; private Element fermata = null; private Set<Articulation> articulations = EnumSet.noneOf(Articulation.class); private List<TupletElementXML> tupletElementsXML=null; public List<TupletElementXML> getTupletElementsXML() {return tupletElementsXML;} public int tupletElementXMLMaxNumber(){ int max=0; if (tupletElementsXML!=null){ for(TupletElementXML tupletElementXML: tupletElementsXML){ if (tupletElementXML.number() > max) max = tupletElementXML.number(); } } return max; } Notations(Element element) { this.element = element; for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.ELEMENT_NODE) { Element child = (Element)node; if (child.getTagName().equals(FERMATA_ELEMENT)) { fermata = child; } else if (child.getTagName().equals(TECHNICAL_ELEMENT)) { technical = new Technical(child); } else if (child.getTagName().equals(TUPLET_ELEMENT)) { if (tupletElementsXML != null) tupletElementsXML.add(new TupletElementXML(child)); else { tupletElementsXML = new ArrayList<TupletElementXML>(); tupletElementsXML.add(new TupletElementXML(child)); } } else if (child.getTagName().equals(ARTICULATIONS_ELEMENT)) { for (Node articulationNode = child.getFirstChild(); articulationNode != null; articulationNode = articulationNode.getNextSibling()) { if (articulationNode.getNodeType() == Node.ELEMENT_NODE) { String nodeName = articulationNode.getNodeName(); if (ARTICULATION_MAP.containsKey(nodeName)) { articulations.add(ARTICULATION_MAP.get(nodeName)); } else { LOG.warning("Unhandled articulation " + nodeName); } } } } } } if (articulations.containsAll(Articulation.mezzoStaccatoSet)) { articulations.removeAll(Articulation.mezzoStaccatoSet); articulations.add(Articulation.mezzoStaccato); } } Fermata getFermata() { if (fermata != null) { Fermata.Type fermataType = Fermata.Type.UPRIGHT; Fermata.Shape fermataShape = Fermata.Shape.NORMAL; if (fermata.hasAttribute("type") && fermata.getAttribute("type").equals("inverted")) fermataType = Fermata.Type.INVERTED; return new Fermata(fermataType, fermataShape); } return null; } public List<Slur> getSlurs() { NodeList nodeList = element.getElementsByTagName("slur"); List<Slur> slurs = new ArrayList<Slur>(); for (int i = 0; i < nodeList.getLength(); i++) { slurs.add(new Slur((Element)nodeList.item(i))); } return slurs; } public Technical getTechnical() { return technical; } public Technical createTechnical() { Element newElement = element.getOwnerDocument() .createElement(TECHNICAL_ELEMENT); element.appendChild(newElement); technical = new Technical(newElement); return technical; } public Set<Articulation> getArticulations() { return articulations; } public Set<Ornament> getOrnaments() { Set<Ornament> ornaments = EnumSet.noneOf(Ornament.class); NodeList nodeList = element.getElementsByTagName("ornaments"); if (nodeList.getLength() >= 1) { nodeList = ((Element)nodeList.item(nodeList.getLength()-1)).getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { if (node.getNodeName().equals("mordent")) { ornaments.add(Ornament.mordent); } else if (node.getNodeName().equals("inverted-mordent")) { ornaments.add(Ornament.invertedMordent); } else if (node.getNodeName().equals("trill-mark")) { ornaments.add(Ornament.trill); } else if (node.getNodeName().equals("turn")) { ornaments.add(Ornament.turn); } else { LOG.warning("Unhandled ornament " + node.getNodeName()); } } } } return ornaments; } /** Most slurs are represented with two elements: one with a start type, * and one with a stop type. * <p> * Slurs can add more elements using a continue type. * This is typically used to specify the formatting of cross-system slurs, * or to specify the shape of very complex slurs. */ static final class Slur { private final Integer number; enum Type { START, STOP, CONTINUE; } private final Type type; Slur(final Element element) { number = element.hasAttribute("number")? new Integer(element.getAttribute("number")): new Integer(1); type = Enum.valueOf(Type.class, element.getAttribute("type").trim().toUpperCase()); } Integer number() { return number; } Type type() { return type; } } class Technical { private Element element; private Text fingering; Technical(Element element) { this.element = element; fingering = Score.getTextNode(element, "fingering"); } public Fingering getFingering() { Fingering result = new Fingering(); if (fingering != null) { String[] items = fingering.getWholeText().trim().split("[- \t\n]+"); List<Integer> fingers = new ArrayList<Integer>(items.length); for (String finger: items) { if (!finger.isEmpty()) fingers.add(Integer.valueOf(finger)); } if (fingers.size() > 0) { result.setFingers(fingers); } } return result; } public void setFingering(Fingering fingering) { String newValue = fingering.toString(" "); if (this.fingering != null) { this.fingering.replaceWholeText(newValue); } else { Document ownerDocument = element.getOwnerDocument(); Element newElement = ownerDocument.createElement("fingering"); this.fingering = ownerDocument.createTextNode(newValue); newElement.appendChild(this.fingering); element.appendChild(newElement); } } } } @Override public String toString() { StringBuilder sb = new StringBuilder(); if (getPitch() != null) sb.append(getPitch()); sb.append(getAugmentedFraction().toString()); Element measure = (Element)this.element.getParentNode(); Element part = (Element)measure.getParentNode(); sb.append(part.getAttribute("id")).append(" ").append(measure.getAttribute("number")); return sb.toString(); } /** Determines if this note is the start of a chord. * @return true if the next note elemnt has a chord child element. */ boolean isStartOfChord() { Node node = element; while ((node = node.getNextSibling()) != null) { if (node.getNodeType() == Node.ELEMENT_NODE) { String nodeName = node.getNodeName(); if ("note".equals(nodeName)) { return Part.elementHasChild((Element)node, CHORD_ELEMENT); } else if ("backup".equals(nodeName) || "forward".equals(nodeName)) return false; } } return false; } }