package com.xenoage.zong.musiclayout.spacer.beat; import static com.xenoage.utils.collections.CollectionUtils.alist; import static com.xenoage.utils.collections.SortedList.sortedListNoDuplicates; import static com.xenoage.utils.kernel.Range.range; import static com.xenoage.utils.math.Fraction._0; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import com.xenoage.utils.collections.SortedList; import com.xenoage.utils.math.Fraction; import com.xenoage.zong.core.music.MusicElement; import com.xenoage.zong.core.music.Voice; import com.xenoage.zong.core.music.VoiceElement; import com.xenoage.zong.musiclayout.spacing.BeatOffset; import com.xenoage.zong.musiclayout.spacing.ElementSpacing; import com.xenoage.zong.musiclayout.spacing.VoiceSpacing; /** * Computes the common {@link BeatOffset}s for the given {@link VoiceSpacing}s * within one measure column. * * Note, that the offsets of the precomputed voice spacings * must already include necessary space which is needed for * additonal elements like inner clefs, since this class * does not look at the musical content of the score, but is * just using the given spacings. * * A {@link BeatOffset} is created for each used beat. * * @author Andreas Wenger */ public class VoicesBeatOffsetter { public static final VoicesBeatOffsetter voicesBeatOffsetter = new VoicesBeatOffsetter(); /** * Computes the offsets of all the used beats, including * at least beat 0 and the beat at the end of the measure. * The beats containing the notes with the lowest valuation * (or that needs accidentals) dictate the spacing. * See "Ross: The Art of Music Engraving", page 79. */ public List<BeatOffset> compute(List<VoiceSpacing> voiceSpacings, Fraction measureBeats, float minimalBeatsOffsetIs) { //the list of all used beats of the measure //TODO: handle upbeat measures correctly! SortedList<Fraction> beats = computeVoicesBeats(voiceSpacings); beats.add(computeLastBeat(voiceSpacings)); //add final used beat beats.add(measureBeats); //add final beat in terms of time signature (only correct for non-upbeat measures) //the resulting offsets for each used beat ArrayList<BeatOffset> ret = alist(); //compute the offset of beat 0 float offsetMm = getOffsetBeat0InMm(voiceSpacings); Fraction lastBeat = _0; ret.add(new BeatOffset(lastBeat, offsetMm)); //if there is only one voice, it's easy to compute the offsets. //Otherwise we must find the dominant parts within the voices if (voiceSpacings.size() == 1) { //only one voice float interlineSpace = voiceSpacings.get(0).interlineSpace; for (ElementSpacing se : voiceSpacings.get(0).elements) { //if last beat offset has same beat, overwrite it if (ret.get(ret.size() - 1).getBeat().equals(se.beat)) ret.remove(ret.size() - 1); ret.add(new BeatOffset(se.beat, se.xIs * interlineSpace)); } } else { //more than one voice //use the following algorithm: //for each beat, compute the offset, by asking each voice how much space //it requires between the last computed beat offset and the current one. //each time, take the greatest distance required. Iterator<Fraction> beatsIterator = beats.iterator(); beatsIterator.next(); //ignore beat 0, we have handled it before while (beatsIterator.hasNext()) { Fraction beat = beatsIterator.next(); //find dominating voice and its minimal required distance float minimalDistance = 0; for (VoiceSpacing voiceSpacing : voiceSpacings) { float interlineSpace = voiceSpacing.interlineSpace; float voiceMinimalDistance = computeMinimalDistance(lastBeat, beat, beat.equals(measureBeats), voiceSpacing.voice, voiceSpacing.elements, ret, interlineSpace); minimalDistance = Math.max(minimalDistance, voiceMinimalDistance); //a minimal distance of 0 is possible, see "BeatOffsetsStrategyTest-3.xml" for an example. //but we do not want to have different beats at the same offset, so add a small distance. if (minimalDistance < minimalBeatsOffsetIs * interlineSpace) { minimalDistance = minimalBeatsOffsetIs * interlineSpace; } } //add beat offsetMm += minimalDistance; ret.add(new BeatOffset(beat, offsetMm)); lastBeat = beat; } } ret.trimToSize(); return ret; } /** * Returns a sorted list of all beats, where * chords or rests begin, from the given list of voice spacings. * There are no duplicate beats. The ending beats of the voices are not added. */ SortedList<Fraction> computeVoicesBeats(List<VoiceSpacing> voiceSpacings) { SortedList<Fraction> beats = sortedListNoDuplicates(); Fraction beat; for (VoiceSpacing voiceSpacing : voiceSpacings) { beat = Fraction._0; for (ElementSpacing spacingElement : voiceSpacing.elements) { MusicElement element = spacingElement.getElement(); if (element instanceof VoiceElement) { //add beat beats.add(beat); //find the next beat beat = beat.add(((VoiceElement) element).getDuration()); } } //do not add beat here, because the ending beat of an incomplete measure //is not interesting for computing beat offsets. } return beats; } /** * Returns the last beat of the given voice spacings. */ Fraction computeLastBeat(List<VoiceSpacing> voiceSpacings) { Fraction ret = _0; for (VoiceSpacing voiceSpacing : voiceSpacings) { Fraction lastBeat = voiceSpacing.getLast().beat; if (lastBeat.compareTo(ret) > 0) ret = lastBeat; } return ret; } /** * Computes and returns the minimal distance in mm * within the given spacing elements of the given voice * between the given starting and ending beat (ending beat * without its width). * * Beats may be multiused. The last element with the given start beat * and also the last element of the given end beat are used * (because the important offset of a beat is the position of the main note * or rest, not the position of a grace note or a clef or key signature). * * If both the starting and ending beat are used, * computing their minimal distance is simple. * * If the ending beat is unused, 0 is returned, since the given * voice does not need any space because it has no element to place there. * * If the starting beat is unused, we have to compute * the distance in the following way: * * The following example shows 2 voices: * * # # # ? { #: there the offsets are already known and given * 1/4 1/4 1/4 | * | | * 1/4 3/8 * 1/8 { this voice is given. *: startBeat is not used * | | * startBeat_| |_endBeat * * Because startBeat is not used, we compute the distance * from the last used beat to the end beat, which is known * from the given spacing elements: * * 1/4 3/8 * 1/8 * |_____________| * distanceToEndBeat * * And we subtract distance between the already computed offset of * the last used beat and the also already computed offset of * the starting beat (both given in the list of beat offsets): * * 1/4 3/8 * 1/8 * |_________| * distanceToLastUsedBeat * * The result is the distance between the starting beat * and the ending beat: * * 1/4 3/8 * 1/8 * |___| * return * * This value is the minimal distance the given voice needs to * place the elements up to the given ending beat. */ float computeMinimalDistance(Fraction startBeat, Fraction endBeat, boolean endBeatIsMeasureEnd, Voice voice, List<ElementSpacing> spacings, List<BeatOffset> alreadyComputedBeatOffsets, float interlineSpace) { //end beat used? (measure end beat is always used) if (endBeatIsMeasureEnd || voice.isBeatUsed(endBeat)) { //yes //when measure is incomplete: use last available beat if (endBeatIsMeasureEnd) { endBeat = voice.getLastUsedBeat(endBeat); } float endOffset = getLastOffset(spacings, endBeat) * interlineSpace; //start beat used? if (voice.isBeatUsed(startBeat)) { //yes float startOffset = getLastOffset(spacings, startBeat) * interlineSpace; //return the distance between this two beats return endOffset - startOffset; } else { //no, start beat is not used. use the algorithm described above Fraction lastUsedBeat = voice.getLastUsedBeat(startBeat); //get offset of the last used beat in the voice spacing float lastUsedBeatVoiceSpacingOffset = 0; for (ElementSpacing spacing : spacings) { if (spacing.beat.equals(lastUsedBeat)) { lastUsedBeatVoiceSpacingOffset = spacing.xIs * interlineSpace; break; } } //compute minimal distance from last used beat to end beat float distanceToEndBeat = endOffset - lastUsedBeatVoiceSpacingOffset; //get offset of the last computed beat from the list of already computed beat offsets float lastComputedBeatOffset = alreadyComputedBeatOffsets.get( alreadyComputedBeatOffsets.size() - 1).getOffsetMm(); //get offset of the last used beat from the list of already computed beat offsets float lastUsedBeatBeatOffsetsOffset = 0; for (BeatOffset beatOffset : alreadyComputedBeatOffsets) { if (beatOffset.getBeat().equals(lastUsedBeat)) { lastUsedBeatBeatOffsetsOffset = beatOffset.getOffsetMm(); break; } } //compute distance between these two offsets float distanceToLastUsedBeat = lastComputedBeatOffset - lastUsedBeatBeatOffsetsOffset; //return the distance between the last computed beat offset and the end beat return distanceToEndBeat - distanceToLastUsedBeat; } } else { //no, end beat is not used //since there is no element, we need no space return 0; } } /** * Computes and returns the maximum offset of beat 0 * for the given voice spacings in mm. * If no voice uses beat 0 or the list is empty, 0 is returned. */ private float getOffsetBeat0InMm(List<VoiceSpacing> voiceSpacings) { float maxOffset = 0; for (VoiceSpacing voiceSpacing : voiceSpacings) { float offset = getLastOffset(voiceSpacing.elements, _0) * voiceSpacing.interlineSpace; if (offset > maxOffset) maxOffset = offset; } return maxOffset; } /** * Computes and returns the offset of the last * occurrence of the given beat in interline spaces. * If the beat is not found, 0 is returned. */ private float getLastOffset(List<ElementSpacing> spacings, Fraction beat) { for (int i : range(spacings)) { //find first occurrence of the beat if (spacings.get(i).beat.equals(beat)) { //find last occurrence of the beat while (i + 1 < spacings.size() && spacings.get(i + 1).beat.equals(beat)) { i++; } return spacings.get(i).xIs; } } return 0; } }