package com.xenoage.zong.io.midi.out.channels;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.instrument.PitchedInstrument;
import com.xenoage.zong.core.music.Part;
import lombok.val;
import java.util.HashMap;
import static com.xenoage.utils.NullUtils.notNull;
import static com.xenoage.utils.collections.CollectionUtils.map;
import static com.xenoage.zong.io.midi.out.MidiConverter.channel10;
import static com.xenoage.zong.io.midi.out.channels.ChannelMap.unused;
/**
* Creates the mapping from staff indices to MIDI channel numbers.
*
* If possible, each part gets its own channel. If there are too many parts,
* the parts with the same MIDI program share the channel. If there are still
* too many parts, the remaining parts are ignored (i.e. they are mapped to
* the channel number {@link ChannelMap#unused}).
* Channel 10 is always reserved for drums/percussion/metronome.
*
* @author Andreas Wenger
*/
public class ChannelMapper {
private static final int maxChannelsCount = 16;
public static ChannelMap createChannelMap(Score score) {
int[] staffChannels;
//get number of parts which have not channel 10
int melodicPartsCount = 0;
for (Part part : score.getStavesList().getParts()) {
if (isPitched(part)) {
melodicPartsCount++;
}
}
//find out if there are enough channels for all parts
if (melodicPartsCount < maxChannelsCount)
//each part gets its own channel
staffChannels = createChannelForEachPart(score);
else
//try to reuse channels
staffChannels = createReusedChannels(score);
return new ChannelMap(staffChannels);
}
/**
* Each pitched part gets its own channel.
* All unpitched parts get channel 10.
*/
private static int[] createChannelForEachPart(Score score) {
int[] staffChannels = new int[score.getStavesCount()];
int iChannel = 0;
for (Part part : score.getStavesList().getParts()) {
boolean isPitched = isPitched(part);
int channel = (isPitched ? iChannel : channel10);
for (int iStaff : score.getStavesList().getPartStaffIndices(part).getRange())
staffChannels[iStaff] = channel;
//after pitched part: increment channel number
if (isPitched)
iChannel = iChannel + 1 + (iChannel + 1 == channel10 ? 1 : 0); //don't use channel 10
}
return staffChannels;
}
/**
* Share channel for parts with the same instrument (MIDI program).
* If none is left, the part is ignored (channel index = {@link ChannelMap#unused}).
* All unpitched parts get channel 10.
*/
private static int[] createReusedChannels(Score score) {
int[] staffChannels = new int[score.getStavesCount()];
int nextFreeChannel = 0;
HashMap<Integer, Integer> programToDeviceMap = map();
for (Part part : score.getStavesList().getParts()) {
boolean isPitched = isPitched(part);
//find channel
int channel;
if (isPitched) {
//pitched part: create new channel or reuse existing channel
val pitchedInstr = (PitchedInstrument) part.getFirstInstrument();
int program = pitchedInstr.getMidiProgram();
channel = notNull(programToDeviceMap.get(program), nextFreeChannel);
if (channel >= maxChannelsCount) {
//no more channel left for this part
channel = unused;
}
else if (channel == nextFreeChannel) {
//new channel created: increment next channel number and remember program
nextFreeChannel = nextFreeChannel + 1 + (nextFreeChannel + 1 == channel10 ? 1 : 0); //don't use channel 10
programToDeviceMap.put(program, channel);
}
}
else {
//unpitched part: always use channel 10
channel = channel10;
}
//apply channel to all staves
for (int iStaff : score.getStavesList().getPartStaffIndices(part).getRange()) {
staffChannels[iStaff] = channel;
}
}
return staffChannels;
}
/**
* Returns true, iff the given part starts with a pitched instrument.
*/
private static boolean isPitched(Part part) {
return (part.getFirstInstrument() instanceof PitchedInstrument);
}
}