/* * JFugue, an Application Programming Interface (API) for Music Programming * http://www.jfugue.org * * Copyright (C) 2003-2014 David Koelle * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jfugue.integration; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.Reader; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import javax.xml.parsers.ParserConfigurationException; import nu.xom.Builder; import nu.xom.Document; import nu.xom.Element; import nu.xom.Elements; import nu.xom.ParsingException; import nu.xom.ValidityException; import org.jfugue.midi.MidiDictionary; import org.jfugue.parser.Parser; import org.jfugue.theory.Chord; import org.jfugue.theory.Note; import org.staccato.DefaultNoteSettingsManager; /** * Parses a MusicXML file, and fires events for <code>ParserListener</code> * interfaces when tokens are interpreted. The <code>ParserListener</code> does * intelligent things with the resulting events, such as create music, draw * sheet music, or transform the data. * * MusicXmlParser.parse can be called with a file name, File, InputStream, or * Reader * * @author E.Philip Sobolik * @author David Koelle (updates for JFugue 5) * @author Richard Lavoie (Major rewriting) * */ public final class MusicXmlParser_R extends Parser { private static class MidiInstrument { private String id; private String channel; private String name; private String bank; private byte program; private String unpitched; public MidiInstrument(String id, String channel, String name , String bank, byte program, String unpitched) { this.id = id; this.channel = channel; this.name = name; this.bank = bank; this.program = program; this.unpitched = unpitched; } } /** * holds a MusicXML part-list entry */ private static class PartContext { public String id; public String name; public MidiInstrument[] instruments; // TODO : Find a good default, do we actually need a default ? private byte currentVolume = 90; public byte voice; public PartContext(String id, String name) { this.id = id; this.name = name; instruments = new MidiInstrument[16]; } }; private static class VoiceDefinition { public VoiceDefinition(int part, int voice) { this.part = part; this.voice = voice; } int part; int voice; } private static class KeySignature { private final byte key; private final byte scale; public KeySignature(byte key, byte scale) { this.key = key; this.scale = scale; } public byte getKey() { return key; } public byte getScale() { return scale; } @Override public boolean equals(Object o) { if (o instanceof KeySignature) { KeySignature other = (KeySignature) o; return other.key == key && other.scale == scale; } return false; } } private Builder xomBuilder; private Document xomDoc; private byte curVelocity = DefaultNoteSettingsManager.getInstance().getDefaultOnVelocity(); private byte beatsPerMeasure; private byte divisionsPerBeat; private int currentVoice; private byte currentLayer; private KeySignature keySignature = new KeySignature((byte) 0, (byte) 0); // next available voice # for a new voice private byte nextVoice; private VoiceDefinition[] voices; private PartContext currentPart; private static final Comparator<String> CHORD_COMPARATOR = new Comparator<String>() { @Override public int compare(String s1, String s2) { int result = compareLength(s1, s2); if (result == 0) { result = s1.compareTo(s2); } return result; } /** * Compare two strings and the bigger of the two is deemed to come * first in order */ private int compareLength(String s1, String s2) { if (s1.length() < s2.length()) { return 1; } else if (s1.length() > s2.length()) { return -1; } else { return 0; } } }; public static Map<String, String> XMLtoJFchordMap; static { // @formatter:off XMLtoJFchordMap = new TreeMap<String, String>(CHORD_COMPARATOR); // Major Chords XMLtoJFchordMap.put("major", "MAJ"); XMLtoJFchordMap.put("major-sixth", "MAJ6"); XMLtoJFchordMap.put("major-seventh", "MAJ7"); XMLtoJFchordMap.put("major-ninth", "MAJ9"); XMLtoJFchordMap.put("major-13th", "MAJ13"); // Minor Chords XMLtoJFchordMap.put("minor", "MIN"); XMLtoJFchordMap.put("minor-sixth", "MIN6"); XMLtoJFchordMap.put("minor-seventh", "MIN7"); XMLtoJFchordMap.put("minor-ninth", "MIN9"); XMLtoJFchordMap.put("minor-11th", "MIN11"); XMLtoJFchordMap.put("major-minor", "MINMAJ7"); // Dominant Chords XMLtoJFchordMap.put("dominant", "DOM7"); XMLtoJFchordMap.put("dominant-11th", "DOM7%11"); XMLtoJFchordMap.put("dominant-ninth", "DOM9"); XMLtoJFchordMap.put("dominant-13th", "DOM13"); // Augmented Chords XMLtoJFchordMap.put("augmented", "AUG"); XMLtoJFchordMap.put("augmented-seventh", "AUG7"); // Diminished Chords XMLtoJFchordMap.put("diminished", "DIM"); XMLtoJFchordMap.put("diminished-seventh", "DIM7"); // Suspended Chords XMLtoJFchordMap.put("suspended-fourth", "SUS4"); XMLtoJFchordMap.put("suspended-second", "SUS2"); // @formatter:on } // CONSTRUCTOR public MusicXmlParser_R() throws ParserConfigurationException { xomBuilder = new Builder(); // Set up MusicXML default values beatsPerMeasure = 1; divisionsPerBeat = 1; currentVoice = -1; nextVoice = 0; voices = new VoiceDefinition[32]; } public void parse(String musicXmlString) throws ValidityException, ParsingException, IOException { // URI is null when parsing a String as it's coming from somewhere else parse(xomBuilder.build(musicXmlString, (String) null)); } public void parse(File inputFile) throws ValidityException, ParsingException, IOException { parse(xomBuilder.build(inputFile)); } public void parse(FileInputStream inputStream) throws ValidityException, ParsingException, IOException { parse(xomBuilder.build(inputStream)); } public void parse(Reader reader) throws ValidityException, ParsingException, IOException { parse(xomBuilder.build(reader)); } private void parse(Document document) { xomDoc = document; parse(); } /** * Parses a MusicXML file and fires events to subscribed * <code>ParserListener</code> interfaces. As the file is parsed, events are * sent to <code>ParserListener</code> interfaces, which are responsible for * doing something interesting with the music data. * * the input is a XOM Document, which has been built previously * * @throws Exception * if there is an error parsing the pattern */ public void parse() { fireBeforeParsingStarts(); Element root = xomDoc.getRootElement(); if (root.getQualifiedName().equalsIgnoreCase("score-timewise")) { parseTimeWise(root); } else if (root.getQualifiedName().equalsIgnoreCase("score-partwise")) { parsePartWise(root); } // else Error document could not be parsed. } private void parsePartWise(Element root) { Element partlist = root.getFirstChildElement("part-list"); Elements parts = partlist.getChildElements(); Map<String, PartContext> partHeaders = parsePartList(parts); parts = root.getChildElements("part"); for (int childId = 0; childId < parts.size(); childId++) { Element partElement = parts.get(childId); String partId = partElement.getAttribute("id").getValue(); switchPart(partHeaders, partId, childId); Elements measures = partElement.getChildElements("measure"); for (int measure = 0; measure < measures.size(); measure++) { parseMusicData(childId, partId, partHeaders, measures.get(measure)); fireBarLineParsed(0); } } } private void parseTimeWise(Element root) { Element partlist = root.getFirstChildElement("part-list"); Elements scoreParts = partlist.getChildElements(); Map<String, PartContext> partHeaders = parsePartList(scoreParts); Elements measures = root.getChildElements("measure"); for (int measureIndex = 0; measureIndex < measures.size(); measureIndex++) { Element measureElement = measures.get(measureIndex); Elements parts = measureElement.getChildElements("part"); for (int partIndex = 0; partIndex < parts.size(); partIndex++) { Element partElement = parts.get(partIndex); String partId = partElement.getAttribute("id").getValue(); switchPart(partHeaders, partId, measureIndex); parseMusicData(partIndex, partId, partHeaders, partElement); fireBarLineParsed(0); } } } private Map<String, PartContext> parsePartList(Elements parts) { Map<String, PartContext> partHeaders = new HashMap<String, PartContext>(); for (int p = 0; p < parts.size(); ++p) { PartContext header = parsePartHeader(parts.get(p)); if (header != null) { partHeaders.put(header.id, header); } } return partHeaders; } /** * Parses a <code>part</code> element in the <code>part-list</code> section * * @param part * is the <code>part</code> element * @param partHeader * is the array of <code>XMLpart</code> classes that stores the * <code>part-list</code> elements */ private PartContext parsePartHeader(Element part) { // I added the following check to satisfy a MusicXML file that contained // a part-group, but I am not convinced that this is the proper way to // handle such an element. // - dmkoelle, 2 MAR 2011 // part-group is a notational convention and can be ignored - JWitzgall if (part.getLocalName().equals("part-group")) { return null; } PartContext partHeader = new PartContext(part.getAttribute("id").getValue(), part.getFirstChildElement("part-name").getValue()); // midi-instruments Elements midiInsts = part.getChildElements("midi-instrument"); for (int x = 0; x < midiInsts.size(); ++x) { Element midi_instrument = midiInsts.get(x); String instrumentId = midi_instrument.getAttribute("id").getValue(); String channel = getStringValueOrNull(midi_instrument.getFirstChildElement("midi-channel")); String name = getStringValueOrNull(midi_instrument.getFirstChildElement("midi-name")); String bank = getStringValueOrNull(midi_instrument.getFirstChildElement("midi-bank")); byte program = getByteValueOrDefault(midi_instrument.getFirstChildElement("midi-program"), (byte) 0); String unpitched = getStringValueOrNull(midi_instrument.getFirstChildElement("midi-unpitched")); partHeader.instruments[x] = new MidiInstrument(instrumentId, channel, name , bank, program, unpitched); } return partHeader; } /** * Returns the value if the object is not null, null otherwise. * @param element Element to return the String value of * @return String value of the element or not null, null otherwise */ private String getStringValueOrNull(Element element) { return element == null ? null : element.getValue(); } /** * Parses music data in under either a measure for timewise score or under a part for partwise scores. * * @param partIndex Index of the part * @param partId * @param partHeaders * @param musicDataRoot */ private void parseMusicData(int partIndex, String partId, Map<String, PartContext> partHeaders, Element musicDataRoot) { Element attributes = musicDataRoot.getFirstChildElement("attributes"); if (attributes != null) { KeySignature ks = parseKeySignature(attributes); if (!keySignature.equals(ks)) { keySignature = ks; fireKeySignatureParsed(keySignature.getKey(), keySignature.getScale()); } //Time-Signature this.divisionsPerBeat = getByteValueOrDefault(attributes.getFirstChildElement("divisions"), this.divisionsPerBeat); this.beatsPerMeasure = getByteValueOrDefault( getRecursiveFirstChildElement(attributes, "time", "beats"), this.beatsPerMeasure); } Elements childs = musicDataRoot.getChildElements(); for (int i = 0; i < childs.size(); i++) { Element el = childs.get(i); if (el.getLocalName().equals("harmony")) { parseGuitarChord(el); } else if (el.getLocalName().equals("note")) { parseNote(partIndex, el, partId, partHeaders); } else if (el.getLocalName().equals("direction")) { Element sound = el.getFirstChildElement("sound"); if (sound != null) { String value = sound.getAttributeValue("dynamics"); if (value != null) { currentPart.currentVolume = Byte.parseByte(value); } value = sound.getAttributeValue("tempo"); if (value != null) { for (MidiInstrument mi : currentPart.instruments) { System.out.println(mi); } fireTempoChanged(Integer.parseInt(value)); } } } } } private void switchPart(Map<String, PartContext> partHeaders, String partString, int partId) { currentPart = partHeaders.get(partString); // assigns a jfugue voice to the part if (currentPart.voice >= 0) { fireTrackChanged(currentPart.voice); } else { // if there are no midi instruments for the part ie // the midi-instruments string length is 0 // TODO : can that even be possible ? if (currentPart.instruments[0] == null) { parseVoice(partId, Integer.parseInt(currentPart.id)); // then pass the name of the part to the Instrument parser parseInstrumentNameAndFireChange(currentPart.name); currentLayer = 0; fireLayerChanged(currentLayer); } else { // TODO : channel ?? really ?? We need to validate this. if (currentPart.instruments[0].channel != null) { parseVoice(partId, Integer.parseInt(currentPart.instruments[0].channel)); } parseInstrumentAndFireChange(currentPart.instruments[0]); currentLayer = 0; fireLayerChanged(currentLayer); } } } private KeySignature parseKeySignature(Element attributes) { // scale 0 = minor, 1 = major byte key = keySignature.getKey(), scale = keySignature.getScale(); Element attr = attributes.getFirstChildElement("key"); if (attr != null) { key = getByteValueOrDefault(attr.getFirstChildElement("fifths"), key); Element eMode = attr.getFirstChildElement("mode"); if (eMode != null) { String mode = eMode.getValue(); if (mode.equalsIgnoreCase("major")) { scale = 0; } else if (mode.equalsIgnoreCase("minor")) { scale = 1; } else { throw new RuntimeException("Error in key signature: " + mode); } } else { scale = 0; } } return new KeySignature(key, scale); } private Element getRecursiveFirstChildElement(Element element, String... childs) { Element el = element; for (String c : childs) { if (el == null) { return null; } el = el.getFirstChildElement(c); } return el; } private byte getByteValueOrDefault(Element element, byte defaultValue) { if (element != null) { int value = Integer.parseInt(element.getValue()); return (byte) value; //return (byte) (value - 1); } return defaultValue; } private void parseGuitarChord(Element harmony) { StringBuilder chordString = new StringBuilder(" "); appendToChord(chordString, harmony, "root"); Element chord_kind = harmony.getFirstChildElement("kind"); if (chord_kind != null) { String chord_kind_str = XMLtoJFchordMap.get(chord_kind.getValue()); if (chord_kind_str != null) { chordString.append(chord_kind_str); } } Element chord_inv = harmony.getFirstChildElement("inversion"); if (chord_inv != null) { Integer inv_value = Integer.parseInt(chord_inv.getValue()); for (Integer i = 0; i < inv_value; i++) { chordString.append("^"); } } appendToChord(chordString, harmony, "bass"); fireChordParsed(new Chord(chordString.toString())); } private void appendToChord(StringBuilder chordString, Element harmony, String base) { Element chord_root = harmony.getFirstChildElement(base); if (chord_root != null) { Element chord_root_step = chord_root.getFirstChildElement(base + "-step"); if (chord_root_step != null) { chordString.append(chord_root_step.getValue()); } Element chord_root_alter = chord_root.getFirstChildElement(base + "-alter"); if (chord_root_alter != null) { if (chord_root_alter.getValue().equals("-1")) { chordString.append("b"); } if (chord_root_alter.getValue().equals("+1")) { chordString.append("#"); } } } } /** * parses MusicXML note Element * * @param noteElement * is the note Element to parse */ private void parseNote(int p, Element noteElement, String partId, Map<String, PartContext> partHeaders) { Note newNote = new Note(); newNote.setFirstNote(true); boolean isRest = false; boolean isStartOfTie = false; boolean isEndOfTie = false; byte noteNumber = 0; byte octaveNumber = 0; double decimalDuration; // skip grace notes // TODO : why do we skip grace notes ? if (noteElement.getFirstChildElement("grace") != null) { return; } Element voice = noteElement.getFirstChildElement("voice"); // TODO : !newNote.isHarmonicNote() is always true ... if (voice != null && !newNote.isHarmonicNote()) { if ((Byte.parseByte(voice.getValue()) - 1) != currentLayer) { currentLayer = Byte.parseByte(voice.getValue()); currentLayer = (byte) (currentLayer - 1); fireLayerChanged(currentLayer); } } enhanceFromChord(noteElement, newNote); Elements noteEls = noteElement.getChildElements(); // See if note is part of a chord for (int i = 0; i < noteEls.size(); i++) { Element element = noteEls.get(i); String tagName = element.getQualifiedName(); if (tagName.equals("instrument")) { PartContext header = partHeaders.get(partId); MidiInstrument[] instruments = header.instruments; for (int y = 0; y < instruments.length; ++y) { MidiInstrument ins = instruments[y]; if (ins != null && ins.id.equals(element.getAttributeValue("id"))) { parseVoice(p, findInstrument(ins)); parseInstrumentAndFireChange(ins); } } } else if (tagName.equals("unpitched")) { // To Determine if Note is Percussive newNote.setPercussionNote(true); Element display_note = element.getFirstChildElement("display-step"); if (display_note != null) { noteNumber = getNoteNumber(display_note.getValue().charAt(0)); } Element display_octave = element.getFirstChildElement("display-octave"); if (display_octave != null) { Byte octave_byte = new Byte(display_octave.getValue()); noteNumber += octave_byte * 12; } } else if (tagName.equals("pitch")) { String sStep = element.getFirstChildElement("step").getValue(); noteNumber = getNoteNumber(sStep.charAt(0)); Element alter = element.getFirstChildElement("alter"); if (alter != null) { noteNumber += Integer.parseInt(alter.getValue()); if (noteNumber > 11) { noteNumber = 0; } else if (noteNumber < 0) { noteNumber = 11; } } octaveNumber = getByteValueOrDefault(element.getFirstChildElement("octave"), octaveNumber); // Compute the actual note number, based on octave and note int intNoteNumber = ((octaveNumber) * 12) + noteNumber; if (intNoteNumber > 127) { throw new RuntimeException("Note value " + intNoteNumber + " is larger than 127"); } noteNumber = (byte) intNoteNumber; } else if (tagName.equals("rest")) { isRest = true; } } // duration Element element_duration = noteElement.getFirstChildElement("duration"); double durationValue = Double.parseDouble(element_duration.getValue()); decimalDuration = durationValue / (divisionsPerBeat * beatsPerMeasure); // Tied Note Element notations = noteElement.getFirstChildElement("notations"); if (notations != null) { Element tied = notations.getFirstChildElement("tied"); if (tied != null) { String tiedValue = tied.getAttributeValue("type"); if (tiedValue.equalsIgnoreCase("start")) { isStartOfTie = true; } else if (tiedValue.equalsIgnoreCase("stop")) { isEndOfTie = true; } } } byte attackVelocity = currentPart.currentVolume; byte decayVelocity = this.curVelocity; // Set up the note if (isRest) { newNote.setRest(true); newNote.setDuration(decimalDuration); // turn off sound for rest notes newNote.setOnVelocity((byte) 0); newNote.setOffVelocity((byte) 0); } else { newNote.setValue(noteNumber); newNote.setDuration(decimalDuration); newNote.setStartOfTie(isStartOfTie); newNote.setEndOfTie(isEndOfTie); newNote.setOnVelocity(attackVelocity); newNote.setOffVelocity(decayVelocity); } fireNoteParsed(newNote); // Add Lyric Element lyric = noteElement.getFirstChildElement("lyric"); if (lyric != null) { Element lyric_text_element = lyric.getFirstChildElement("text"); if (lyric_text_element != null) { fireLyricParsed(lyric_text_element.getValue()); } } } /** * Converts a step to it's note value. * @param step * Note step which is one of A,B,C,D,E,F or G * @return * Note number between 0 and 11 associated to it's MIDI equivalent note number */ private byte getNoteNumber(char step) { byte note = 0; switch (step) { case 'C': note = 0; break; case 'D': note = 2; break; case 'E': note = 4; break; case 'F': note = 5; break; case 'G': note = 7; break; case 'A': note = 9; break; case 'B': note = 11; break; } return note; } private int findInstrument(MidiInstrument instrument) { if (instrument.name != null) { return MidiDictionary.INSTRUMENT_STRING_TO_BYTE.get(instrument.name); } return 0; } private void enhanceFromChord(Element noteElement, Note note) { Elements note_elements = noteElement.getChildElements(); for (int i = 0; i < note_elements.size(); i++) { Element element = note_elements.get(i); String tagName = element.getQualifiedName(); if (tagName.equals("chord")) { note.setHarmonicNote(true); note.setFirstNote(false); } } } /** * Parses a voice and fires a voice element * * @param voice * is the voice number 1 - 16 * @throws JFugueException * if there is a problem parsing the element */ private void parseVoice(int part, int voice) { // This needs to be reworked as it probably should be stored in PartContext. if (voice == 10) { // TODO : Why does 10 fires a change right away ... ? fireTrackChanged((byte) voice); } else { // scroll through voiceDef objects looking for this particular // combination of p v // XML part ID's are 1-based, JFugue voice numbers are 0-based byte voiceNumber = -1; for (byte x = 0; x < this.nextVoice; ++x) { // class variable voices is an array of voiceDef objects. These // objects match a part index to a voice index. if (part == voices[x].part && voice == voices[x].voice) { voiceNumber = x; break; } } // if Voice not found, add a new voiceDef to the array if (voiceNumber == -1) { voiceNumber = nextVoice; voices[voiceNumber] = new VoiceDefinition(part, voice); ++nextVoice; } if (voiceNumber != this.currentVoice) { fireTrackChanged(voiceNumber); } currentVoice = voiceNumber; } } /** * parses <code>inst</code> and fires an Instrument Event * * @param name * is a String that represents the instrument. If it is a numeric * value, it is interpreted as a midi-bank or program. If it is * an instrument name, it is looked up in the Dictionary as an * instrument name. */ private void parseInstrumentNameAndFireChange(String name) { byte instrumentNumber; try { // if the inst string is a number ie the midi number instrumentNumber = Byte.parseByte(name); } catch (NumberFormatException e) { // otherwise map the midi_name to its byte code Object value = MidiDictionary.INSTRUMENT_STRING_TO_BYTE.get(name); instrumentNumber = (value == null) ? -1 : (Byte) value; } if (instrumentNumber > -1) { fireInstrumentParsed(instrumentNumber); } else throw new RuntimeException(); } private void parseInstrumentAndFireChange(MidiInstrument instrument) { if (instrument.program >= 0) { fireInstrumentParsed(instrument.program); } else if (instrument.name != null) { parseInstrumentNameAndFireChange(instrument.name); } else { throw new RuntimeException("Couldn't determine the instrument. Possibly and unhandled case. Please report with the musicXML data."); } } /** * converts beats per minute (BPM) to pulses per minute (PPM) assuming 240 * pulses per second In MusicXML, BPM can be fractional, so * <code>BPMtoPPM</code> takes a float argument * * @param bpm * @return ppm */ public static int BPMtoPPM(float bpm) { return (new Float((60.f * 240.f) / bpm).intValue()); } }