package com.xenoage.zong.core.music.key;
import com.xenoage.utils.math.MathUtils;
import com.xenoage.zong.core.music.MusicElementType;
import com.xenoage.zong.core.music.Pitch;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.core.position.MPContainer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import static com.xenoage.utils.NullUtils.notNull;
import static com.xenoage.zong.core.music.Pitch.pi;
/**
* Traditional key signature in the circle of fifth and a mode (like major or
* minor).
*
* @author Andreas Wenger
* @author Uli Teschemacher
*/
@EqualsAndHashCode(exclude="parent")
public final class TraditionalKey
implements Key {
/** Major, minor and other modes. */
public enum Mode {
Major,
Minor,
Ionian,
Dorian,
Phrygian,
Lydian,
Mixolydian,
Aeolian,
Locrian;
};
/** The number within the circle of fifths, e.g. 1 for G major or E minor. Between -7 and +7. */
@Getter private int fifths;
/** The mode, e.g. major, or null for an unknown mode. */
@Getter @Setter private Mode mode;
/** Back reference: the parent element, or null, if not part of a score. */
@Getter @Setter private MPContainer parent;
//alterations for the circle of fifths (from -7 to +7, represented by the indices 0 to 14)
private static final int[][] alterations = { { -1, -1, -1, -1, -1, -1, -1 },
{ -1, -1, -1, 0, -1, -1, -1 }, { 0, -1, -1, 0, -1, -1, -1 }, { 0, -1, -1, 0, 0, -1, -1 },
{ 0, 0, -1, 0, 0, -1, -1 }, { 0, 0, -1, 0, 0, 0, -1 }, { 0, 0, 0, 0, 0, 0, -1 },
{ 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, +1, 0, 0, 0 }, { +1, 0, 0, +1, 0, 0, 0 },
{ +1, 0, 0, +1, +1, 0, 0 }, { +1, +1, 0, +1, +1, 0, 0 }, { +1, +1, 0, +1, +1, +1, 0 },
{ +1, +1, +1, +1, +1, +1, 0 }, { +1, +1, +1, +1, +1, +1, +1 } };
public TraditionalKey(int fifth, Mode mode) {
setFifth(fifth);
this.mode = notNull(mode, Mode.Major);
}
public TraditionalKey(int fifth) {
setFifth(fifth);
this.mode = Mode.Major;
}
public void setFifth(int fifth) {
if (fifth < -7 || fifth > +7)
throw new IllegalArgumentException("fifth must be between -7 and +7");
this.fifths = fifth;
}
@Override public int[] getAlterations() {
return alterations[fifths + 7];
}
/**
* Gets the step of the pitch of the flat/sharp with the given index. For
* example index = 0 at Eb flat will return Pitch.B.
*/
public int getStep(int index) {
if (fifths < 0) {
switch (index) {
case 0:
return Pitch.B;
case 1:
return Pitch.E;
case 2:
return Pitch.A;
case 3:
return Pitch.D;
case 4:
return Pitch.G;
case 5:
return Pitch.C;
case 6:
return Pitch.F;
}
}
else {
switch (index) {
case 0:
return Pitch.F;
case 1:
return Pitch.C;
case 2:
return Pitch.G;
case 3:
return Pitch.D;
case 4:
return Pitch.A;
case 5:
return Pitch.E;
case 6:
return Pitch.B;
}
}
return 0;
}
/**
* Returns the nearest higher {@link Pitch} in the current key.
*/
@Override public Pitch getNearestHigherPitch(Pitch pitch) {
int step = (pitch.getStep() + 1) % 7;
int octave = pitch.getOctave() + (pitch.getStep() + 1) / 7;
int alter = getAlterations()[step];
return pi(step, alter, octave);
}
/**
* Returns the nearest lower {@link Pitch} in the current key.
*/
@Override public Pitch getNearestLowerPitch(Pitch pitch) {
byte step;
byte alter;
byte octave;
step = (byte) ((pitch.getStep() - 1 + 7) % 7);
octave = (byte) (pitch.getOctave() + (pitch.getStep() - 1 + 7) / 7 - 1);
alter = (byte) getAlterations()[step];
return pi(step, alter, octave);
}
/**
* Gets the line position for the sharp or flat
* with the given 0-based index, when C4 is on the given line position.
*/
public static int getLinePosition(int index, boolean sharp, int linePositionC4,
int linePositionMin) {
int ret = linePositionC4 + 2 +
(sharp ? getSharpLinePositionGKey(index) : getFlatLinePositionGKey(index));
ret = MathUtils.modMin(ret, 7, linePositionMin);
return ret;
}
/**
* Gets the line position for the flat
* with the given 0-based index when there is a normal G key.
*/
public static int getFlatLinePositionGKey(int index) {
switch (index) {
case 0:
return 4; //Bb
case 1:
return 7; //Eb
case 2:
return 3; //Ab
case 3:
return 6; //Db
case 4:
return 2; //Gb
case 5:
return 5; //Cb
case 6:
return 1; //Fb
default:
throw new IllegalArgumentException("Invalid index: " + index);
}
}
/**
* Gets the line position for the sharp
* with the given 0-based index when there is a normal G key.
*/
public static int getSharpLinePositionGKey(int index) {
switch (index) {
case 0:
return 8; //F#
case 1:
return 5; //C#
case 2:
return 9; //G#
case 3:
return 6; //D#
case 4:
return 3; //A#
case 5:
return 7; //E#
case 6:
return 4; //H#
default:
throw new IllegalArgumentException("Invalid index: " + index);
}
}
@Override public MusicElementType getMusicElementType() {
return MusicElementType.TraditionalKey;
}
@Override public MP getMP() {
return MP.getMPFromParent(this);
}
}