package com.xenoage.zong.io.musicxml.in.readers; import com.xenoage.zong.core.music.Part; import com.xenoage.zong.core.music.Staff; import com.xenoage.zong.core.music.StavesList; import com.xenoage.zong.core.music.group.BarlineGroup; import com.xenoage.zong.core.music.group.BracketGroup; import com.xenoage.zong.core.music.group.StavesRange; import com.xenoage.zong.io.musicxml.in.util.ErrorHandling; import com.xenoage.zong.musicxml.types.*; import com.xenoage.zong.musicxml.types.choice.MxlMusicDataContent; import com.xenoage.zong.musicxml.types.choice.MxlMusicDataContent.MxlMusicDataContentType; import com.xenoage.zong.musicxml.types.choice.MxlPartListContent; import com.xenoage.zong.musicxml.types.choice.MxlPartListContent.PartListContentType; import com.xenoage.zong.musicxml.types.enums.MxlStartStop; import com.xenoage.zong.musicxml.types.partwise.MxlMeasure; import com.xenoage.zong.musicxml.types.partwise.MxlPart; import lombok.Getter; import lombok.RequiredArgsConstructor; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.xenoage.utils.collections.CollectionUtils.alist; import static com.xenoage.utils.collections.CollectionUtils.map; import static com.xenoage.utils.iterators.MultiListIt.multiListIt; import static com.xenoage.utils.kernel.Range.range; import static com.xenoage.zong.io.musicxml.Equivalents.bracketGroupStyles; /** * This reads an empty {@link StavesList} from the * score-part and part-group elements of a partwise MusicXML 2.0 document. * * This class also creates a map for matching MusicXML part-IDs * and staff indices to the correct application's staff indices. * * @author Andreas Wenger */ @RequiredArgsConstructor public class StavesListReader { private final MxlScorePartwise mxlScore; private final ErrorHandling errorHandling; private StavesList stavesList; @Getter private Map<String, Integer> partsIDtoIndex; private static abstract class PartsGroup { public int startPartIndex = -1, stopPartIndex = -1; @Override public String toString() { return "[" + startPartIndex + " to " + stopPartIndex + "]"; } } private static final class PartsBarlineGroup extends PartsGroup { public BarlineGroup.Style style = BarlineGroup.Style.Single; } private static final class PartsBracketGroup extends PartsGroup { public BracketGroup.Style style = BracketGroup.Style.None; } private static final class PartsGroups { public PartsBarlineGroup barlineGroup = null; public PartsBracketGroup bracketsGroup = null; } public StavesList read() { //list of parts List<Part> parts = alist(); partsIDtoIndex = map(); //list of groups List<PartsBarlineGroup> barlineGroups = alist(); List<PartsBracketGroup> bracketGroups = alist(); //open groups with number as index Map<String, PartsBarlineGroup> openBarlineGroups = map(); Map<String, PartsBracketGroup> openBracketGroups = map(); //read score-part and part-group elements //each score-part is a part in our application MxlPartList mxlPartList = mxlScore.getScoreHeader().getPartList(); int currentPartIndex = 0; for (MxlPartListContent mxlItem : mxlPartList.getContent()) { //score-part if (mxlItem.getPartListContentType() == PartListContentType.ScorePart) { MxlScorePart mxlScorePart = (MxlScorePart) mxlItem; Part part = PartReader.read(mxlScorePart, findCorrespondingPart(mxlScorePart, mxlScore)); parts.add(part); partsIDtoIndex.put(mxlScorePart.getId(), currentPartIndex); currentPartIndex++; } //part-group else if (mxlItem.getPartListContentType() == PartListContentType.PartGroup) { PartsGroups group = readPartGroup(currentPartIndex, (MxlPartGroup) mxlItem, openBarlineGroups, openBracketGroups); if (group != null) { //a group was closed, add it if (group.barlineGroup != null) barlineGroups.add(group.barlineGroup); if (group.bracketsGroup != null) bracketGroups.add(group.bracketsGroup); } } } //if there are unclosed score-groups, report a problem if (openBarlineGroups.size() > 0 || openBracketGroups.size() > 0) { errorHandling.reportError("There are unclosed score-groups"); } //count the number of staves and measures used by each part HashMap<String, Integer> partsStaves = countStaves(mxlScore); for (String partID : partsStaves.keySet()) { Integer partIndex = partsIDtoIndex.get(partID); if (partIndex == null) throw new IllegalStateException("Unknown part \"" + partID + "\""); Integer partStaves = partsStaves.get(partID); if (partStaves == null) throw new IllegalStateException("Unused part \"" + partID + "\""); if (partStaves > 1) parts.get(partIndex).setStavesCount(partStaves); } //creates the final StavesList for this document stavesList = createStavesList(parts, barlineGroups, bracketGroups); return stavesList; } /** * Reads a part-group element. * If a group was closed, it is returned. If a group was opened or it can not be read, * null is returned. While MusicXML groups can be combined barline and bracket groups, * these are separated values in Zong!. This is why they are returned as a tuple * (with null if not set). */ private PartsGroups readPartGroup(int currentPartIndex, MxlPartGroup mxlPartGroup, Map<String, PartsBarlineGroup> openBarlineGroups, Map<String, PartsBracketGroup> openBracketGroups) { String number = mxlPartGroup.getNumber(); MxlStartStop type = mxlPartGroup.getType(); PartsBarlineGroup openBarlineGroup = openBarlineGroups.get(number); PartsBracketGroup openBracketGroup = openBracketGroups.get(number); if (type == MxlStartStop.Start) { //group begins here if (openBarlineGroup != null || openBracketGroup != null) { errorHandling.reportError("part-group \"" + number + "\" was already opened"); } //read group-barline and group-symbol (bracket) BarlineGroup.Style barlineStyle = readBarlineGroupStyle(mxlPartGroup.getGroupBarline()); if (barlineStyle != BarlineGroup.Style.Single) { openBarlineGroups.put(number, openBarlineGroup = new PartsBarlineGroup()); openBarlineGroup.startPartIndex = currentPartIndex; openBarlineGroup.style = barlineStyle; } BracketGroup.Style bracketStyle = readBracketGroupStyle(mxlPartGroup.getGroupSymbol()); if (bracketStyle != BracketGroup.Style.None) { openBracketGroups.put(number, openBracketGroup = new PartsBracketGroup()); openBracketGroup.startPartIndex = currentPartIndex; openBracketGroup.style = bracketStyle; } return null; } else if (type == MxlStartStop.Stop) { //group ends here if (openBarlineGroup == null && openBracketGroup == null) { errorHandling.reportError("score-group \"" + number + "\" was closed before it was opened"); } //close open barline group and/or bracket group PartsBarlineGroup closedBarlineGroup = null; if (openBarlineGroup != null) { closedBarlineGroup = openBarlineGroup; openBarlineGroups.remove(number); closedBarlineGroup.stopPartIndex = currentPartIndex - 1; } PartsBracketGroup closedBracketGroup = null; if (openBracketGroup != null) { closedBracketGroup = openBracketGroup; openBracketGroups.remove(number); closedBracketGroup.stopPartIndex = currentPartIndex - 1; } PartsGroups ret = new PartsGroups(); ret.barlineGroup = closedBarlineGroup; ret.bracketsGroup = closedBracketGroup; return ret; } return null; } /** * Counts the number of staves used in each part and returns them. * @return a hashmap which maps a part ID to the number of staves in this part */ private HashMap<String, Integer> countStaves(MxlScorePartwise mxlScore) { HashMap<String, Integer> ret = map(); //check all parts for (MxlPart mxlPart : mxlScore.getParts()) { String id = mxlPart.getId(); //heck all measures for attributes with staves-element and store the greatest value int maxStaves = 1; for (MxlMeasure mxlMeasure : mxlPart.getMeasures()) { for (MxlMusicDataContent content : mxlMeasure.getMusicData().getContent()) { if (content.getMusicDataContentType() == MxlMusicDataContentType.Attributes) { Integer xmlStaves = ((MxlAttributes) content).getStaves(); if (xmlStaves != null) { maxStaves = Math.max(maxStaves, xmlStaves); } } } } //set the number of staves of the part ret.put(id, maxStaves); } return ret; } /** * Creates the (still empty) {@link StavesList} for this document. */ private StavesList createStavesList(List<Part> parts, List<PartsBarlineGroup> barlineGroups, List<PartsBracketGroup> bracketGroups) { StavesList ret = new StavesList(); //add parts for (Part part : parts) { ret.getParts().add(part); for (int i = 0; i < part.getStavesCount(); i++) { Staff staff = Staff.staffMinimal(); staff.setParent(ret); ret.getStaves().add(staff); } } //add groups for (PartsBarlineGroup barlineGroup : barlineGroups) { int startIndex = getFirstStaffIndex(barlineGroup.startPartIndex, parts); int endIndex = getLastStaffIndex(barlineGroup.stopPartIndex, parts); ret.addBarlineGroup(new StavesRange(startIndex, endIndex), barlineGroup.style); } for (PartsBracketGroup bracketGroup : bracketGroups) { int startIndex = getFirstStaffIndex(bracketGroup.startPartIndex, parts); int endIndex = getLastStaffIndex(bracketGroup.stopPartIndex, parts); ret.addBracketGroup(new StavesRange(startIndex, endIndex), bracketGroup.style); } //add implicit brace- and barline-groups for ungrouped //parts with more than one staff for (int i : range(parts)) { if (parts.get(i).getStavesCount() > 1 && !isPartInGroup(i, barlineGroups, bracketGroups)) { int startIndex = getFirstStaffIndex(i, parts); int endIndex = getLastStaffIndex(i, parts); ret.addBarlineGroup(new StavesRange(startIndex, endIndex), BarlineGroup.Style.Common); ret.addBracketGroup(new StavesRange(startIndex, endIndex), BracketGroup.Style.Brace); } } return ret; } /** * Gets the index of the first staff of the given part. */ private int getFirstStaffIndex(int partIndex, List<Part> parts) { int ret = 0; for (int i : range(partIndex)) ret += parts.get(i).getStavesCount(); return ret; } /** * Gets the index of the last staff of the given part. */ private int getLastStaffIndex(int partIndex, List<Part> parts) { return getFirstStaffIndex(partIndex, parts) + parts.get(partIndex).getStavesCount() - 1; } /** * Returns true, if the part with the given index * is in at least one barline- or bracket-group. */ @SuppressWarnings("unchecked") private static boolean isPartInGroup(int partIndex, List<PartsBarlineGroup> barlineGroups, List<PartsBracketGroup> bracketGroups) { for (PartsGroup group : multiListIt(barlineGroups, bracketGroups)) { if (group.startPartIndex >= partIndex && group.stopPartIndex <= partIndex) return true; } return false; } private BracketGroup.Style readBracketGroupStyle(MxlGroupSymbol mxlGroupSymbol) { if (mxlGroupSymbol == null) return BracketGroup.Style.None; return bracketGroupStyles.getBy2(mxlGroupSymbol.getValue()); } private BarlineGroup.Style readBarlineGroupStyle(MxlGroupBarline mxlGroupBarline) { if (mxlGroupBarline != null) { switch (mxlGroupBarline.getValue()) { case Yes: return BarlineGroup.Style.Common; case No: return BarlineGroup.Style.Single; case Mensurstrich: return BarlineGroup.Style.Mensurstrich; } } return BarlineGroup.Style.Single; } /** * Returns the {@link MxlPart}, which belongs to the given {@link MxlScorePart}, * i.e. the part with an equal ID. */ private MxlPart findCorrespondingPart(MxlScorePart mxlScorePart, MxlScorePartwise mxlScorePartwise) { for (MxlPart part : mxlScorePartwise.getParts()) { if (part.getId().equals(mxlScorePart.getId())) return part; } throw new IllegalStateException("There is no part for score-part \"" + mxlScorePart.getId() + "\""); } }