package com.xenoage.zong.io.midi.out.dynamics;
import com.xenoage.utils.annotations.MaybeNull;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.MusicElementType;
import com.xenoage.zong.core.music.direction.Dynamic;
import com.xenoage.zong.core.music.direction.DynamicValue;
import com.xenoage.zong.core.music.direction.Wedge;
import com.xenoage.zong.core.music.direction.WedgeEnd;
import com.xenoage.zong.core.position.Time;
import com.xenoage.zong.io.midi.out.dynamics.type.DynamicsType;
import com.xenoage.zong.io.midi.out.dynamics.type.FixedDynamics;
import com.xenoage.zong.io.midi.out.dynamics.type.GradientDynamics;
import com.xenoage.zong.io.midi.out.repetitions.Repetitions;
import com.xenoage.zong.io.midi.out.score.MeasureBeats;
import com.xenoage.zong.io.midi.out.score.PartStaves;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.val;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.zong.core.music.direction.DynamicValue.mf;
import static com.xenoage.zong.core.position.MP.atMeasure;
import static com.xenoage.zong.core.position.Time.time;
/**
* Finds the {@link Dynamics} in a {@link Score}.
*
* Each staff has its own dynamics, represented by {@link DynamicsPeriod}s.
* Additionally, dynamics may appear in voices. Then, the voice dynamics
* are valid until the next voice or staff dynamics appears or when the voice ends,
* i.e. when the following measure does not contain this voice any more.
*
* TODO (ZONG-100): Playback dynamics in voices
* TODO (ZONG-98): Also support dynamics/wedges in column header
* TODO (ZONG-99): Playback sfz, ftp and other dynamic accents
*
* @author Andreas Wenger
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class DynamicsFinder {
private final Score score;
private final DynamicsInterpretation interpretation;
private final Repetitions repetitions;
private final MeasureBeats measureBeats;
private final PartStaves partStaves;
private DynamicsPeriodsBuilder periods = new DynamicsPeriodsBuilder();
public static Dynamics findDynamics(
Score score, DynamicsInterpretation interpretation, Repetitions repetitions) {
return new DynamicsFinder(score, interpretation, repetitions,
MeasureBeats.findMeasureBeats(score), PartStaves.findPartStaves(score)).find();
}
private Dynamics find() {
//staves: find dynamics for each staff and repetition
for (int iStaff : range(score.getStavesCount())) {
DynamicValue currentValue = mf;
for (int iRep : range(repetitions)) {
currentValue = findStaffDynamics(iStaff, iRep, currentValue);
}
}
//voices: find dynamics for each staff, voice and repetition
for (int iStaff : range(score.getStavesCount())) {
for (int iVoice : range(score.getStaff(iStaff).getVoicesCount())) {
for (int iRep : range(repetitions)) {
//TODO: ZONG-100 findVoiceDynamics(iStaff, iVoice, iRep);
}
}
}
return new Dynamics(periods.build(), interpretation, measureBeats, partStaves);
}
/**
* Walk through the measures of the given staff and repetition and find the dynamics.
* Starts with the given initial dynamic. Returns the dynamic value at the end of the
* repetition.
*/
private DynamicValue findStaffDynamics(int staff, int repetition, DynamicValue startDynamics) {
val rep = repetitions.get(repetition);
//walk through the measures
Time currentStartTime = rep.start;
DynamicsType currentDynamic = new FixedDynamics(startDynamics);
for (int iMeasure : range(rep.start.measure, rep.end.measure)) {
if (iMeasure >= score.getMeasuresCount())
break;
val measure = score.getMeasure(atMeasure(staff, iMeasure));
for (val beat : measure.getDirections().getBeats()) {
val newTime = time(iMeasure, beat);
DynamicsType newDynamic = null;
//if beat is out of the repetition range, ignore it
if (false == rep.contains(newTime))
continue;
//find dynamics change
if (currentDynamic instanceof GradientDynamics) {
//wedge ending at this beat?
val closedWedge = getWedgeEndAt(staff, newTime, (GradientDynamics) currentDynamic);
if (closedWedge != null) {
currentDynamic = closedWedge;
newDynamic = new FixedDynamics(closedWedge.end); //continue with the end dynamic of the wedge
}
}
else if (newDynamic == null) {
//new dynamic starting? then close currently open period and open new one
newDynamic = getStaffDynamicStartAt(staff, newTime, currentDynamic.getEndValue());
}
//when change was found, apply it
if (newDynamic != null) {
if (false == currentStartTime.equals(newTime) && false == currentDynamic.equals(newDynamic)) {
val period = new DynamicsPeriod(currentStartTime, newTime, currentDynamic);
periods.addPeriodToStaff(period, staff, repetition);
currentStartTime = newTime;
}
currentDynamic = newDynamic;
}
}
}
//close the dynamics period at the end of the repetition
val period = new DynamicsPeriod(currentStartTime, rep.end, currentDynamic);
periods.addPeriodToStaff(period, staff, repetition);
return currentDynamic.getEndValue();
}
/**
* Returns the {@link DynamicsType} starting at the given time within the given staff (not a voice),
* or null, if nothing starts here.
*/
@MaybeNull private DynamicsType getStaffDynamicStartAt(int staff, Time time, DynamicValue currentDynamicValue) {
val measure = score.getMeasure(atMeasure(staff, time.measure));
//when there is a Wedge (possible with a Dynamic as the start volume), create
//a gradient dynamic, when there is only a Dynamic, create a fixed dynamic
val foundDynamic = (Dynamic) measure.getDirections().get(time.beat, MusicElementType.Dynamic);
val foundWedge = (Wedge) measure.getDirections().get(time.beat, MusicElementType.Wedge);
if (foundWedge != null) {
//gradient
val startDynamicValue = (foundDynamic != null ? foundDynamic.getValue() : currentDynamicValue);
val endDynamicValue = startDynamicValue.getWedgeEndValue(foundWedge.getType());
return new GradientDynamics(startDynamicValue, endDynamicValue); //can be replaced later, when end dynamic is found
}
else if (foundDynamic != null) {
//fixed value
return new FixedDynamics(foundDynamic.getValue());
}
return null;
}
/**
* Checks, if there is a {@link WedgeEnd} at the given time within the given staff (not a voice).
* If yes, the given open wedge is closed and returned.
*/
@MaybeNull private GradientDynamics getWedgeEndAt(int staff, Time time, GradientDynamics openWedge) {
val measure = score.getMeasure(atMeasure(staff, time.measure));
val foundDynamic = (Dynamic) measure.getDirections().get(time.beat, MusicElementType.Dynamic);
val foundWedgeEnd = (WedgeEnd) measure.getDirections().get(time.beat, MusicElementType.WedgeEnd);
if (foundWedgeEnd != null) {
val endDynamicValue = (foundDynamic != null ? foundDynamic.getValue() : openWedge.end);
return new GradientDynamics(openWedge.start, endDynamicValue);
}
return null;
}
}