Skip to content

Commit

Permalink
Merge pull request #110 from otsob/issue#32
Browse files Browse the repository at this point in the history
Issue #32: Add support for reading markings from MusicXML

Adds support for reading markings (slurs, glissando) from MusicXML.
The implementation only supports markings that occur between notes in the same voice.
Handling of slurs that cross parts and voices is more complex and is to be handled in issue #109
  • Loading branch information
otsob authored May 24, 2019
2 parents 16253ac + c77d2f0 commit 702cd6f
Show file tree
Hide file tree
Showing 5 changed files with 1,100 additions and 107 deletions.
246 changes: 211 additions & 35 deletions src/main/java/org/wmn4j/io/musicxml/MusicXmlReaderDom.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.wmn4j.notation.elements.Duration;
import org.wmn4j.notation.elements.KeySignature;
import org.wmn4j.notation.elements.KeySignatures;
import org.wmn4j.notation.elements.Marking;
import org.wmn4j.notation.elements.Part;
import org.wmn4j.notation.elements.Pitch;
import org.wmn4j.notation.elements.Score;
Expand All @@ -41,10 +42,15 @@
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
* A parser for MusicXML files.
Expand Down Expand Up @@ -248,15 +254,15 @@ private void readMeasuresIntoPartBuilder(PartBuilder partBuilder, Node partNode)
// Read measure node by node, create measure and add to list
final NodeList measureNodes = partNode.getChildNodes();

// Used for keeping track of possible tie beginnings.
final TieBeginningContainer tieBeginnings = new TieBeginningContainer();
// Used for keeping track of and resolving possible connected notations.
final ConnectedNotations connectedNotations = new ConnectedNotations();

for (int i = 0; i < measureNodes.getLength(); ++i) {
final Node measureNode = measureNodes.item(i);

// Make sure that the node really is a measure node.
if (measureNode.getNodeName().equals(MusicXmlTags.MEASURE)) {
readMeasureIntoPartBuilder(partBuilder, measureNode, contexts, staves, tieBeginnings);
readMeasureIntoPartBuilder(partBuilder, measureNode, contexts, staves, connectedNotations);
}
}
}
Expand Down Expand Up @@ -292,7 +298,7 @@ private int getNumberOfStaves(Node partNode) {
* and add it to the PartBuilder.
*/
private void readMeasureIntoPartBuilder(PartBuilder partBuilder, Node measureNode, Map<Integer, Context> contexts,
int staves, TieBeginningContainer tieBuffer) {
int staves, ConnectedNotations connectedNotations) {

final int measureNumber = Integer
.parseInt(measureNode.getAttributes().getNamedItem(MusicXmlTags.MEASURE_NUM).getTextContent());
Expand Down Expand Up @@ -325,7 +331,8 @@ private void readMeasureIntoPartBuilder(PartBuilder partBuilder, Node measureNod

// Handle note element
if (node.getNodeName().equals(MusicXmlTags.NOTE) && !isGraceNote(node)) {
readNoteElementIntoMeasureBuilder(node, measureBuilders, chordBuffers, offsets, contexts, tieBuffer);
readNoteElementIntoMeasureBuilder(node, measureBuilders, chordBuffers, offsets, contexts,
connectedNotations);
}

// Handle clef changes
Expand Down Expand Up @@ -399,7 +406,7 @@ private void addBarline(Node node, Map<Integer, MeasureBuilder> measureBuilders)
*/
private void readNoteElementIntoMeasureBuilder(Node noteNode, Map<Integer, MeasureBuilder> measureBuilders,
Map<Integer, ChordBuffer> chordBuffers, Map<Integer, List<Duration>> offsets,
Map<Integer, Context> contexts, TieBeginningContainer tieBeginnings) {
Map<Integer, Context> contexts, ConnectedNotations connectedNotations) {
final int staffNumber = DocHelper.findChild(noteNode, MusicXmlTags.NOTE_STAFF)
.map(staffNode -> Integer.parseInt(staffNode.getTextContent()))
.orElse(MIN_STAFF_NUMBER);
Expand All @@ -418,22 +425,7 @@ private void readNoteElementIntoMeasureBuilder(Node noteNode, Map<Integer, Measu
final Pitch pitch = getPitch(noteNode);
final NoteBuilder noteBuilder = new NoteBuilder(pitch, duration);

// Handle ties
if (endsTie(noteNode)) {
final NoteBuilder tieBeginner = tieBeginnings.popMatchingBeginningFromStaff(staffNumber, noteBuilder);
if (tieBeginner != null) {
tieBeginner.addTieToFollowing(noteBuilder);
} else {
noteBuilder.setIsTiedFromPrevious(true);
}
}

if (startsTie(noteNode)) {
tieBeginnings.addToStaff(staffNumber, noteBuilder);
}

DocHelper.findChild(noteNode, MusicXmlTags.NOTATIONS)
.ifPresent(notationsNode -> addNotations(notationsNode, noteBuilder));
resolveTiesAndNotations(voice, noteNode, connectedNotations, staffNumber, noteBuilder);

if (hasChordTag(noteNode)) {
chordBuffers.get(staffNumber).addNote(noteBuilder, voice);
Expand All @@ -444,17 +436,99 @@ private void readNoteElementIntoMeasureBuilder(Node noteNode, Map<Integer, Measu
}
}

private void addNotations(Node notationsNode, NoteBuilder noteBuilder) {
private void resolveTiesAndNotations(int voiceNumber, Node noteNode, ConnectedNotations connectedNotations,
int staffNumber, NoteBuilder noteBuilder) {

// Handle ties
if (endsTie(noteNode)) {
final NoteBuilder tieBeginner = connectedNotations.popTieBeginningFromStaff(staffNumber, noteBuilder);
if (tieBeginner != null) {
tieBeginner.addTieToFollowing(noteBuilder);
} else {
noteBuilder.setIsTiedFromPrevious(true);
}
}

if (startsTie(noteNode)) {
connectedNotations.addTieBeginningToStaff(staffNumber, noteBuilder);
}

final Optional<Node> notationsNodeOptional = DocHelper.findChild(noteNode, MusicXmlTags.NOTATIONS);
notationsNodeOptional.ifPresent(notationsNode -> addArticulations(notationsNode, noteBuilder));

addMarkings(voiceNumber, notationsNodeOptional, noteBuilder, connectedNotations);
}

private void addMarkings(int voiceNumber, Optional<Node> notationsNode, NoteBuilder noteBuilder,
ConnectedNotations connectedNotations) {

final Set<UnresolvedMarking> startedMarkings = new HashSet<>();

// Handle the beginnings and ends of markings if the notations node is present
if (notationsNode.isPresent()) {
for (Node markingNode : getMarkingNodes(notationsNode.get())) {
final String markingPositionType = DocHelper.getAttributeValue(markingNode, MusicXmlTags.MARKING_TYPE)
.orElseThrow();

// Marking number can be omitted in MusicXML. In that case should default to 1.
final int markingNumber = DocHelper.getAttributeValue(markingNode, MusicXmlTags.MARKING_NUMBER)
.map(stringValue -> Integer.parseInt(stringValue)).orElse(1);

final Marking.Type markingType = getMarkingType(markingNode);

if (markingPositionType.equals(MusicXmlTags.MARKING_TYPE_START)) {
UnresolvedMarking startedMarking = connectedNotations
.createAndAddStartOfMarking(voiceNumber, markingNumber, markingType, noteBuilder);
startedMarkings.add(startedMarking);
} else if (markingPositionType.equals(MusicXmlTags.MARKING_TYPE_STOP)) {
connectedNotations.endMarking(voiceNumber, markingNumber, markingType, noteBuilder);
}
}
}

if (connectedNotations.hasUnresolvedMarkings(voiceNumber)) {
connectedNotations.addToUnresolvedMarkings(voiceNumber, noteBuilder, startedMarkings);
}
}

private Collection<Node> getMarkingNodes(Node notationsNode) {
Collection<Node> markingNodes = new ArrayList<>();
NodeList notationsChildren = notationsNode.getChildNodes();
for (int i = 0; i < notationsChildren.getLength(); ++i) {
Node childNode = notationsChildren.item(i);
if (MusicXmlTags.MARKING_NODE_NAMES.contains(childNode.getNodeName())) {
markingNodes.add(childNode);
}
}

return markingNodes;
}

private Marking.Type getMarkingType(Node markingNode) {
switch (markingNode.getNodeName()) {
case MusicXmlTags.SLUR:
return Marking.Type.SLUR;
case MusicXmlTags.GLISSANDO:
return Marking.Type.GLISSANDO;
}

LOG.warn("Tried to parse unsupported marking type: " + markingNode.getNodeName());
return null;
}

private void addArticulations(Node notationsNode, NoteBuilder noteBuilder) {
final Optional<Node> articulationsNode = DocHelper.findChild(notationsNode, MusicXmlTags.NOTE_ARTICULATIONS);

if (articulationsNode.isPresent()) {
for (int i = 0; i < articulationsNode.get().getChildNodes().getLength(); ++i) {
final Node articulationNode = articulationsNode.get().getChildNodes().item(i);
final Articulation articulation = getArticulation(articulationNode.getNodeName());
if (articulation != null) {
noteBuilder.addArticulation(articulation);
} else {
LOG.warn("Articulation of type " + articulationNode.getNodeName() + " not supported");
if (articulationNode.getNodeType() == Node.ELEMENT_NODE) {
final Articulation articulation = getArticulation(articulationNode.getNodeName());
if (articulation != null) {
noteBuilder.addArticulation(articulation);
} else {
LOG.warn("Articulation of type " + articulationNode.getNodeName() + " not supported");
}
}
}
}
Expand Down Expand Up @@ -853,27 +927,28 @@ void setClef(Clef clef) {
}

/**
* Class for keeping track of tie beginning NoteBuilders.
* Class for keeping track of connected notations such as ties, slurs etc.
*/
private class TieBeginningContainer {
private class ConnectedNotations {

private final Map<Integer, List<NoteBuilder>> tieStarts = new HashMap<>();
private final Map<Integer, Collection<UnresolvedMarking>> unresolvedMarkingsPerVoice = new HashMap<>();

void addToStaff(int staffNumber, NoteBuilder builder) {
void addTieBeginningToStaff(int staffNumber, NoteBuilder tieBeginning) {
if (!this.tieStarts.containsKey(staffNumber)) {
this.tieStarts.put(staffNumber, new ArrayList<>());
}

this.tieStarts.get(staffNumber).add(builder);
this.tieStarts.get(staffNumber).add(tieBeginning);
}

NoteBuilder popMatchingBeginningFromStaff(int staff, NoteBuilder builder) {
NoteBuilder popTieBeginningFromStaff(int staff, NoteBuilder tieEnd) {
NoteBuilder matching = null;

if (this.tieStarts.keySet().contains(staff)) {
for (int i = 0; i < this.tieStarts.get(staff).size(); ++i) {
final NoteBuilder b = this.tieStarts.get(staff).get(i);
if (b.getPitch().equals(builder.getPitch())) {
final NoteBuilder tieBeginning = this.tieStarts.get(staff).get(i);
if (tieBeginning.getPitch().equals(tieEnd.getPitch())) {
matching = this.tieStarts.get(staff).remove(i);
break;
}
Expand All @@ -882,5 +957,106 @@ NoteBuilder popMatchingBeginningFromStaff(int staff, NoteBuilder builder) {

return matching;
}

boolean hasUnresolvedMarkings(int voiceNumber) {
return unresolvedMarkingsPerVoice.containsKey(voiceNumber) && !unresolvedMarkingsPerVoice.get(voiceNumber)
.isEmpty();
}

UnresolvedMarking createAndAddStartOfMarking(int voiceNumber, int markingNumber, Marking.Type markingType,
NoteBuilder firstBuilder) {
if (!unresolvedMarkingsPerVoice.containsKey(voiceNumber)) {
unresolvedMarkingsPerVoice.put(voiceNumber, new ArrayList<>());
}

UnresolvedMarking unresolvedMarking = new UnresolvedMarking(markingNumber, markingType);
unresolvedMarking.addNoteBuilder(firstBuilder);
unresolvedMarkingsPerVoice.get(voiceNumber).add(unresolvedMarking);
return unresolvedMarking;
}

void addToUnresolvedMarkings(int voiceNumber, NoteBuilder builder, Set<UnresolvedMarking> markingsToIgnore) {
unresolvedMarkingsPerVoice.getOrDefault(voiceNumber, Collections.emptyList()).stream()
.filter(unresolvedMarking -> !markingsToIgnore.contains(unresolvedMarking))
.forEach(unresolvedMarking -> unresolvedMarking.addNoteBuilder(builder));
}

void endMarking(int voiceNumber, int markingNumber, Marking.Type markingType, NoteBuilder lastBuilder) {
final Collection<UnresolvedMarking> markingsForVoice = unresolvedMarkingsPerVoice
.getOrDefault(voiceNumber, Collections.emptyList());

Optional<UnresolvedMarking> unresolvedMarkingToEndOpt = markingsForVoice
.stream()
.filter(unresolvedMarking -> unresolvedMarking.getMarkingType().equals(markingType)
&& unresolvedMarking.getMarkingNumber() == markingNumber)
.findFirst();

if (unresolvedMarkingToEndOpt.isPresent()) {
UnresolvedMarking unresolvedMarkingToEnd = unresolvedMarkingToEndOpt.get();
unresolvedMarkingToEnd.addNoteBuilder(lastBuilder);
unresolvedMarkingToEnd.resolve();
markingsForVoice.remove(unresolvedMarkingToEnd);

} else {
LOG.warn("Cannot correctly resolve a marking that was started in a different voice."
+ " Trying to resolve marking in voice "
+ voiceNumber + " with type " + markingType + " and number " + markingNumber);
}
}
}

/*
* Class for keeping track of unresolved markings.
*/
private class UnresolvedMarking {

private final int markingNumber;
private final Marking.Type markingType;
private final List<NoteBuilder> connectedNoteBuilders;

UnresolvedMarking(int markingNumber, Marking.Type markingType) {
this.markingNumber = markingNumber;
this.markingType = markingType;
this.connectedNoteBuilders = new ArrayList<>();
}

int getMarkingNumber() {
return markingNumber;
}

Marking.Type getMarkingType() {
return markingType;
}

void addNoteBuilder(NoteBuilder builder) {
connectedNoteBuilders.add(builder);
}

void resolve() {
final Marking marking = Marking.of(markingType);
for (int i = 0; i < connectedNoteBuilders.size() - 1; ++i) {
connectedNoteBuilders.get(i).connectWith(marking, connectedNoteBuilders.get(i + 1));
}
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}

if (!(o instanceof UnresolvedMarking)) {
return false;
}

UnresolvedMarking other = (UnresolvedMarking) o;
return markingNumber == other.markingNumber
&& markingType.equals(other.markingType);
}

@Override
public int hashCode() {
return Objects.hash(markingNumber, markingType);
}
}
}
13 changes: 13 additions & 0 deletions src/main/java/org/wmn4j/io/musicxml/MusicXmlTags.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
*/
package org.wmn4j.io.musicxml;

import java.util.Collections;
import java.util.Set;

final class MusicXmlTags {

// Barline tags
Expand Down Expand Up @@ -70,6 +73,16 @@ final class MusicXmlTags {
static final String TENUTO = "tenuto";
static final String FERMATA = "fermata";

// Markings
static final String SLUR = "slur";
static final String GLISSANDO = "glissando";
static final String MARKING_NUMBER = "number";
static final String MARKING_TYPE = "type";
static final String MARKING_TYPE_START = "start";
static final String MARKING_TYPE_STOP = "stop";

static final Set<String> MARKING_NODE_NAMES = Collections.unmodifiableSet(Set.of(SLUR, GLISSANDO));

// Part tags
static final String PART = "part";
static final String PART_ID = "id";
Expand Down
Loading

0 comments on commit 702cd6f

Please sign in to comment.