package com.xenoage.zong.musiclayout.spacer.beat;
import com.xenoage.utils.math.Delta;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.Measure;
import com.xenoage.zong.core.music.MeasureElement;
import com.xenoage.zong.core.music.Voice;
import com.xenoage.zong.core.music.VoiceElement;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.music.clef.Clef;
import com.xenoage.zong.core.music.clef.ClefType;
import com.xenoage.zong.core.music.key.TraditionalKey;
import com.xenoage.zong.core.music.time.TimeSignature;
import com.xenoage.zong.core.music.time.TimeType;
import com.xenoage.zong.io.musicxml.in.MusicXmlScoreFileInputTest;
import com.xenoage.zong.io.selection.Cursor;
import com.xenoage.zong.musiclayout.notation.ChordNotation;
import com.xenoage.zong.musiclayout.spacing.*;
import org.junit.Test;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.math.Delta.df;
import static com.xenoage.utils.math.Fraction._0;
import static com.xenoage.utils.math.Fraction.fr;
import static com.xenoage.zong.core.music.Pitch.pi;
import static com.xenoage.zong.core.music.chord.ChordFactory.chord;
import static com.xenoage.zong.core.music.chord.ChordFactory.graceChord;
import static com.xenoage.zong.core.music.time.TimeType.time_4_4;
import static com.xenoage.zong.core.position.MP.*;
import static com.xenoage.zong.musiclayout.spacer.beat.VoicesBeatOffsetter.voicesBeatOffsetter;
import static com.xenoage.zong.musiclayout.spacing.TestSpacing.spacing;
import static org.junit.Assert.*;
/**
* Tests for a {@link VoicesBeatOffsetter}.
*
* @author Andreas Wenger
*/
public class VoicesBeatOffsetterTest {
private VoicesBeatOffsetter testee = voicesBeatOffsetter;
private final float width_grace = 1f;
private final float width_1_8 = 1.5f;
private final float width_1_6 = 1.7f;
private final float width_1_4 = 2f;
private final float width_3_8 = 2.5f;
private final float width_1_2 = 3f;
private final float width_1_1 = 5f;
private final Fraction dur_1_8 = fr(1, 8);
private final Fraction dur_1_6 = fr(1, 6);
private final Fraction dur_1_4 = fr(1, 4);
private final Fraction dur_3_8 = fr(3, 8);
private final Fraction dur_1_2 = fr(1, 2);
private final Fraction dur_1_1 = fr(1, 1);
private final float minimalBeatsOffsetIs = 0.1f;
@Test public void computeVoicesBeatsTest() {
//must have 5 beats at 0, 2, 3, 4, 8.
List<Fraction> beats = testee.computeVoicesBeats(
createVoiceSpacings(createTestScore1Voice())).getLinkedList();
assertEquals(5, beats.size());
assertEquals(fr(0, 4), beats.get(0));
assertEquals(fr(2, 8), beats.get(1));
assertEquals(fr(3, 8), beats.get(2));
assertEquals(fr(4, 8), beats.get(3));
assertEquals(fr(8, 8), beats.get(4));
}
@Test public void computeDistanceTest() {
Voice voice = createTestScore1Voice().getVoice(mp0);
List<ElementSpacing> spacings = createTestElementSpacings1Voice();
LinkedList<BeatOffset> emptyList = new LinkedList<>();
//distance: the offsets of the notes and rests are interesting,
//not the ones of the clefs, key signatures and time signatures,
//so the method has to use the last occurrence of a beat.
//distance between beat 0 and 4: must be 6
float distance = testee.computeMinimalDistance(fr(0), fr(4, 4), false, voice, spacings,
emptyList, 1);
assertEquals(6, distance, Delta.DELTA_FLOAT);
//distance between beat 0 and 5: must be 0 (beat 5 isn't used)
distance = testee.computeMinimalDistance(fr(0), fr(5, 4), false, voice, spacings, emptyList,
1);
assertEquals(0, distance, Delta.DELTA_FLOAT);
//distance between beat 0 and 2: must be 2
distance = testee.computeMinimalDistance(fr(0), fr(2, 4), false, voice, spacings, emptyList,
1);
assertEquals(2, distance, Delta.DELTA_FLOAT);
//distance between beat 5 and 8: must be 0 (beat 5 isn't used)
distance = testee.computeMinimalDistance(fr(5), fr(8, 4), false, voice, spacings, emptyList,
1);
assertEquals(0, distance, Delta.DELTA_FLOAT);
}
/**
* Compute offsets of the common beats.
*/
@Test public void computeTest1() {
Score score = createTestScore3Voices();
BeatOffset[] beatOffsets = testee.compute(
createVoiceSpacings(score), fr(4, 4), minimalBeatsOffsetIs).toArray(new BeatOffset[0]);
float is = score.getFormat().getInterlineSpace();
//2: half note, 4: quarter note, 8: eighth note, 3: quarter triplet
//^: dominating voice
//voice 1: | 4 4 4 4 |
// ^^^^^
//voice 2: | 8 8 8 8 2 |
// ^^^^^^^^^^^^
//voice 3: | 3 3 3 8 8 4 |
// ^^^^^^
//used: * ** * ** * * * *
//checked: * * * * *
assertEquals(10, beatOffsets.length);
assertEquals(fr(0, 4), beatOffsets[0].getBeat());
assertEquals((0) * is, beatOffsets[0].getOffsetMm(), df);
assertEquals(fr(1, 4), beatOffsets[3].getBeat());
assertEquals((2 * width_1_8) * is, beatOffsets[3].getOffsetMm(), df);
assertEquals(fr(2, 4), beatOffsets[6].getBeat());
assertEquals((4 * width_1_8) * is, beatOffsets[6].getOffsetMm(), df);
assertEquals(fr(3, 4), beatOffsets[8].getBeat());
assertEquals((6 * width_1_8) * is, beatOffsets[8].getOffsetMm(), df);
assertEquals(fr(4, 4), beatOffsets[9].getBeat());
assertEquals((6 * width_1_8 + width_1_4) * is, beatOffsets[9].getOffsetMm(), df);
}
/**
* Compute offsets of the common beats,
* this time for an incomplete measure.
*/
@Test public void computeTest2() {
Score score = createTestScore3VoicesIncomplete();
BeatOffset[] beatOffsets = testee.compute(
createVoiceSpacings(score), fr(4, 4), minimalBeatsOffsetIs).toArray(new BeatOffset[0]);
float is = score.getFormat().getInterlineSpace();
//2: half note, 4: quarter note, 8: eighth note, 3: quarter triplet, x: missing (empty)
//^: dominating voice
//voice 1: | 4 4 4 8 xxx|
// ^^^^^^^^^^^^
//voice 2: | 8 8 8 8 4 xxxxxx|
// ^^^^^^^^^^^^
//voice 3: | 3 3 3 8 xxxxxxxxx|
//
//used: * ** * ** * * ° * (°: total final used beat)
//checked: * * * * * *
assertEquals(10, beatOffsets.length);
assertEquals(fr(0, 4), beatOffsets[0].getBeat());
assertEquals((0) * is, beatOffsets[0].getOffsetMm(), df);
assertEquals(fr(1, 4), beatOffsets[3].getBeat());
assertEquals((2 * width_1_8) * is, beatOffsets[3].getOffsetMm(), df);
assertEquals(fr(2, 4), beatOffsets[6].getBeat());
assertEquals((4 * width_1_8) * is, beatOffsets[6].getOffsetMm(), df);
assertEquals(fr(3, 4), beatOffsets[7].getBeat());
assertEquals((4 * width_1_8 + width_1_4) * is, beatOffsets[7].getOffsetMm(), df);
assertEquals(fr(7, 8), beatOffsets[8].getBeat());
assertEquals((4 * width_1_8 + width_1_4 + width_1_8) * is, beatOffsets[8].getOffsetMm(), df);
assertEquals(fr(4, 4), beatOffsets[9].getBeat());
assertEquals((4 * width_1_8 + width_1_4 + width_1_8 + minimalBeatsOffsetIs) * is,
beatOffsets[9].getOffsetMm(), df);
}
/**
* Compute offsets of the common beats, when also grace notes are used.
*/
@Test public void computeTestGrace() {
Score score = createTestScore3VoicesGrace();
BeatOffset[] beatOffsets = testee.compute(
createVoiceSpacings(score), fr(4, 4), minimalBeatsOffsetIs).toArray(new BeatOffset[0]);
float is = score.getFormat().getInterlineSpace();
//2: half note, 4: quarter note, 8: eighth note, 3: quarter triplet
//^: dominating voice
//voice 1: | 4 ...4 4 4 |
// ^^^^^^^^^ ^^^^^
//voice 2: | 8 8 8 8 ..2 |
// ^^^^^^^^
//voice 3: | 3 3 3 .8 8 4 |
// ^^^^^^
//used: * ** * ** * * * *
//checked: * * * * *
assertEquals(10, beatOffsets.length);
assertEquals(fr(0, 4), beatOffsets[0].getBeat());
assertEquals((0) * is, beatOffsets[0].getOffsetMm(), df);
assertEquals(fr(1, 4), beatOffsets[3].getBeat());
float offset1 = width_1_4 + 3 * width_grace;
assertEquals(offset1 * is, beatOffsets[3].getOffsetMm(), df);
assertEquals(fr(2, 4), beatOffsets[6].getBeat());
float offset2 = offset1 + 2 * width_1_8 + 2 * width_grace;
assertEquals(offset2 * is, beatOffsets[6].getOffsetMm(), df);
assertEquals(fr(3, 4), beatOffsets[8].getBeat());
float offset3 = offset2 + 2 * width_1_8;
assertEquals(offset3 * is, beatOffsets[8].getOffsetMm(), df);
assertEquals(fr(4, 4), beatOffsets[9].getBeat());
float offset4 = offset3 + width_1_4;
assertEquals(offset4 * is, beatOffsets[9].getOffsetMm(), df);
}
/**
* Test file "BeatOffsetsStrategyTest-1.xml".
*/
@Test public void computeBeatOffsets_File1() {
Score score = MusicXmlScoreFileInputTest.loadXMLTestScore("VoicesBeatOffsetterTest-1.xml");
LinkedList<VoiceSpacing> voiceSpacings = createVoiceSpacings(score);
BeatOffset[] beatOffsets = testee.compute(voiceSpacings,
fr(3, 4), minimalBeatsOffsetIs).toArray(new BeatOffset[0]);
//file must have 5 beat offsets with increasing mm offsets
assertEquals(5, beatOffsets.length);
assertEquals(fr(0, 4), beatOffsets[0].getBeat());
assertEquals(fr(1, 4), beatOffsets[1].getBeat());
assertEquals(fr(2, 4), beatOffsets[2].getBeat());
assertEquals(fr(5, 8), beatOffsets[3].getBeat());
assertEquals(fr(3, 4), beatOffsets[4].getBeat());
for (int i = 0; i < beatOffsets.length - 1; i++) {
assertTrue(beatOffsets[i].getOffsetMm() < beatOffsets[i + 1].getOffsetMm());
}
//distance between beat 1/4 and 2/4 must be width_1_4
float is = score.getFormat().getInterlineSpace();
assertEquals(width_1_4 * is, beatOffsets[2].getOffsetMm() - beatOffsets[1].getOffsetMm(), df);
}
/**
* Test file "BeatOffsetsStrategyTest-2.xml".
*/
@Test public void computeBeatOffsets_File2() {
Score score = MusicXmlScoreFileInputTest.loadXMLTestScore("VoicesBeatOffsetterTest-2.xml");
LinkedList<VoiceSpacing> voiceSpacings = createVoiceSpacings(score);
BeatOffset[] beatOffsets = testee.compute(voiceSpacings,
fr(3, 4), minimalBeatsOffsetIs).toArray(new BeatOffset[0]);
//file must have 6 beat offsets with increasing mm offsets
assertEquals(6, beatOffsets.length);
assertEquals(fr(0, 4), beatOffsets[0].getBeat());
assertEquals(fr(1, 8), beatOffsets[1].getBeat());
assertEquals(fr(1, 4), beatOffsets[2].getBeat());
assertEquals(fr(2, 4), beatOffsets[3].getBeat());
assertEquals(fr(5, 8), beatOffsets[4].getBeat());
assertEquals(fr(3, 4), beatOffsets[5].getBeat());
for (int i = 0; i < beatOffsets.length - 1; i++) {
assertTrue(beatOffsets[i].getOffsetMm() < beatOffsets[i + 1].getOffsetMm());
}
//distance between beat 1/4 and 2/4 must be width_1_4
float is = score.getFormat().getInterlineSpace();
assertEquals(width_1_4 * is, beatOffsets[3].getOffsetMm() - beatOffsets[2].getOffsetMm(),
df);
}
/**
* Test file "BeatOffsetsStrategyTest-3.xml".
*/
@Test public void computeBeatOffsets_File3() {
Score score = MusicXmlScoreFileInputTest.loadXMLTestScore("VoicesBeatOffsetterTest-3.xml");
LinkedList<VoiceSpacing> voiceSpacings = createVoiceSpacings(score);
BeatOffset[] beatOffsets = testee.compute(voiceSpacings,
fr(5, 4), minimalBeatsOffsetIs).toArray(new BeatOffset[0]);
//file must have 8 beat offsets with increasing mm offsets
//special difficulty: last eighth note must be further to the right as preceding quarter
//in other voice, even the distance between the whole note and the last eighth would be big enough
assertEquals(8, beatOffsets.length);
assertEquals(fr(0, 4), beatOffsets[0].getBeat());
assertEquals(fr(1, 8), beatOffsets[1].getBeat());
assertEquals(fr(1, 4), beatOffsets[2].getBeat());
assertEquals(fr(2, 4), beatOffsets[3].getBeat());
assertEquals(fr(3, 4), beatOffsets[4].getBeat());
assertEquals(fr(4, 4), beatOffsets[5].getBeat());
assertEquals(fr(9, 8), beatOffsets[6].getBeat());
assertEquals(fr(5, 4), beatOffsets[7].getBeat());
for (int i = 0; i < beatOffsets.length - 1; i++) {
assertTrue("beat " + (i + 1) + " wrong",
beatOffsets[i].getOffsetMm() < beatOffsets[i + 1].getOffsetMm());
}
//distance between beat 1/4 and 2/4 must be width_1_4
float is = score.getFormat().getInterlineSpace();
assertEquals(width_1_4 * is, beatOffsets[3].getOffsetMm() - beatOffsets[2].getOffsetMm(),
df);
}
/**
* Test file "BeatOffsetsStrategyTest-4.xml".
*/
@Test public void computeBeatOffsets_File4() {
Score score = MusicXmlScoreFileInputTest.loadXMLTestScore("VoicesBeatOffsetterTest-4.xml");
LinkedList<VoiceSpacing> voiceSpacings = createVoiceSpacings(score);
BeatOffset[] beatOffsets = testee.compute(voiceSpacings,
fr(3, 4), minimalBeatsOffsetIs).toArray(new BeatOffset[0]);
//distance between beat 1/4 and 2/4 must be width_1_4
float is = score.getFormat().getInterlineSpace();
assertEquals(width_1_4 * is, beatOffsets[3].getOffsetMm() - beatOffsets[2].getOffsetMm(), df);
}
private Score createTestScore1Voice() {
Score score = new Score();
score.getFormat().setInterlineSpace(1);
Cursor cursor = new Cursor(score, mp0, true);
cursor.write(new Clef(ClefType.clefTreble));
cursor.write((MeasureElement) new TraditionalKey(-3));
cursor.write(new TimeSignature(TimeType.timeType(6, 4)));
//beats: 0, 2, 3, 4, 8.
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 2)));
cursor.write(chord(pi(0, 0, 0), fr(1, 2)));
return score;
}
private List<ElementSpacing> createTestElementSpacings1Voice() {
return alist(
spacing(fr(0, 4), 1), //clef. width: 3
spacing(fr(0, 4), 4), //key. width: 2
spacing(fr(0, 4), 6), //time. width: 3
spacing(fr(0, 4), 9), //note. width: 2
spacing(fr(2, 4), 11), //note. width: 2
spacing(fr(3, 4), 13), //note. width: 2
spacing(fr(4, 4), 15), //note. width: 2
spacing(fr(8, 4), 17) //note. width: 2
);
}
private Score createTestScore3Voices() {
Score score = new Score();
score.getFormat().setInterlineSpace(10);
Cursor cursor = new Cursor(score, mp0, true);
cursor.write(new TimeSignature(time_4_4));
//2: half note, 4: quarter note, 8: eighth note, 3: quarter triplet
//voice 1: | 4 4 4 4 | (staff 1)
//voice 2: | 8 8 8 8 2 | (staff 1)
//voice 3: | 3 3 3 8 8 4 | (staff 2)
//voice 1 (staff 1)
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
//voice 2 (staff 1)
cursor.setMp(atElement(0, 0, 1, 0));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 2)));
//voice 3 (staff 2)
cursor.setMp(atElement(1, 0, 0, 0));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
return score;
}
private Score createTestScore3VoicesGrace() {
Score score = new Score();
score.getFormat().setInterlineSpace(10);
Cursor cursor = new Cursor(score, mp0, true);
cursor.write(new TimeSignature(time_4_4));
//2: half note, 4: quarter note, 8: eighth note, 3: quarter triplet, .: grace note
//voice 1: | 4 ...4 4 4 | (staff 1)
//voice 2: | 8 8 8 8 ..2 | (staff 1)
//voice 3: | 3 3 3 .8 8 4 | (staff 2)
//voice 1 (staff 1)
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(graceChord(pi(0, 0, 0)));
cursor.write(graceChord(pi(0, 0, 0)));
cursor.write(graceChord(pi(0, 0, 0)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
//voice 2 (staff 1)
cursor.setMp(atElement(0, 0, 1, 0));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(graceChord(pi(0, 0, 0)));
cursor.write(graceChord(pi(0, 0, 0)));
cursor.write(chord(pi(0, 0, 0), fr(1, 2)));
//voice 3 (staff 2)
cursor.setMp(atElement(1, 0, 0, 0));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(graceChord(pi(0, 0, 0)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
return score;
}
private Score createTestScore3VoicesIncomplete() {
Score score = new Score();
score.getFormat().setInterlineSpace(2);
Cursor cursor = new Cursor(score, mp0, true);
cursor.write(new TimeSignature(time_4_4));
//2: half note, 4: quarter note, 8: eighth note, 3: quarter triplet, x: missing (empty)
//voice 1: | 4 4 4 8 xxx| (staff 1)
//voice 2: | 8 8 8 8 4 xxxxxx| (staff 1)
//voice 3: | 3 3 3 8 xxxxxxxxx| (staff 2)
//voice 1
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
//voice 2
cursor.setMp(atElement(0, 0, 1, 0));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
cursor.write(chord(pi(0, 0, 0), fr(1, 4)));
//voice 3
cursor.setMp(atElement(0, 0, 2, 0));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 6)));
cursor.write(chord(pi(0, 0, 0), fr(1, 8)));
return score;
}
/**
* Create {@link VoiceSpacing}s for the first measure column
* of the given {@link Score}.
*/
private LinkedList<VoiceSpacing> createVoiceSpacings(Score score) {
LinkedList<VoiceSpacing> ret = new LinkedList<>();
for (int iStaff : range(0, score.getStavesCount() - 1)) {
Measure measure = score.getMeasure(atMeasure(iStaff, 0));
for (Voice voice : measure.getVoices()) {
Fraction beat = fr(0);
ArrayList<ElementSpacing> se = alist();
float offset = 0;
for (VoiceElement e : voice.getElements()) {
//compute width
float width = 0;
if (e.getDuration().equals(_0))
width = width_grace;
else if (e.getDuration().equals(dur_1_8))
width = width_1_8;
else if (e.getDuration().equals(dur_1_6))
width = width_1_6;
else if (e.getDuration().equals(dur_1_4))
width = width_1_4;
else if (e.getDuration().equals(dur_3_8))
width = width_3_8;
else if (e.getDuration().equals(dur_1_2))
width = width_1_2;
else if (e.getDuration().equals(dur_1_1))
width = width_1_1;
//create spacing element with offset
se.add(new ChordSpacing(new ChordNotation((Chord) e), beat, offset));
beat = beat.add(e.getDuration());
offset += width;
}
se.add(new BorderSpacing(beat, offset));
ret.add(new VoiceSpacing(voice, score.getFormat().getInterlineSpace(), se));
}
}
return ret;
}
}