package com.xenoage.zong.io.musicxml.in.readers;
import static com.xenoage.utils.CheckUtils.checkNotNull;
import static com.xenoage.utils.NullUtils.notNull;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.collections.CollectionUtils.map;
import java.util.HashMap;
import java.util.List;
import lombok.RequiredArgsConstructor;
import com.xenoage.utils.annotations.MaybeEmpty;
import com.xenoage.utils.math.MathUtils;
import com.xenoage.zong.core.instrument.Instrument;
import com.xenoage.zong.core.instrument.PitchedInstrument;
import com.xenoage.zong.core.instrument.Transpose;
import com.xenoage.zong.core.instrument.UnpitchedInstrument;
import com.xenoage.zong.musicxml.types.MxlAttributes;
import com.xenoage.zong.musicxml.types.MxlMidiInstrument;
import com.xenoage.zong.musicxml.types.MxlNote;
import com.xenoage.zong.musicxml.types.MxlScoreInstrument;
import com.xenoage.zong.musicxml.types.MxlScorePart;
import com.xenoage.zong.musicxml.types.choice.MxlMusicDataContent;
import com.xenoage.zong.musicxml.types.choice.MxlMusicDataContent.MxlMusicDataContentType;
import com.xenoage.zong.musicxml.types.partwise.MxlMeasure;
import com.xenoage.zong.musicxml.types.partwise.MxlPart;
/**
* This class reads the {@link Instrument}s from a
* given {@link MxlScorePart}.
*
* TODO: more information like midi channel, bank etc.
*
* TODO: include better guesses about the instruments,
* e.g. which MIDI program to take if nothing is specified,
* use localized names and so on.
*
* @author Andreas Wenger
*/
@RequiredArgsConstructor
public class InstrumentsReader {
private final MxlScorePart mxlScorePart;
private final MxlPart mxlPart;
private class Info {
String id;
String name;
String abbreviation;
Transpose transpose = Transpose.noTranspose;
Integer midiProgram;
Integer midiChannel;
Float volume;
Float pan;
}
private HashMap<String, Info> infos = map();
private Transpose partTranspose = Transpose.noTranspose;
/**
* Reads the instruments from the given {@link ScorePart}.
* Not only the header ({@link MxlScorePart})
* must be given, but also the contents ({@link MxlPart}),
* which is needed to find transposition information.
*/
@MaybeEmpty public List<Instrument> read() {
readScoreInstruments();
readTranspositions();
readMidiInstruments();
List<Instrument> ret = createInstruments();
return ret;
}
private Info getInfo(String id) {
return checkNotNull(infos.get(id), "Unknown instrument: " + id);
}
private void readScoreInstruments() {
for (MxlScoreInstrument mxlScoreInstr : mxlScorePart.getScoreInstruments()) {
String id = mxlScoreInstr.getId();
Info info = new Info();
info.id = id;
info.name = checkNotNull(mxlScoreInstr.getInstrumentName());
info.abbreviation = mxlScoreInstr.getInstrumentAbbreviation();
infos.put(id, info);
}
}
private void readTranspositions() {
List<MxlScoreInstrument> mxlScoreInstruments = mxlScorePart.getScoreInstruments();
if (mxlScoreInstruments.size() == 0) {
//no instrument defined, but maybe we have a transposition anyway
partTranspose = findFirstTranspose();
}
if (mxlScoreInstruments.size() == 1) {
//only one instrument: find transposition (if any) in first attributes of first measure
getInfo(mxlScoreInstruments.get(0).getId()).transpose = findFirstTranspose();
}
else if (mxlScoreInstruments.size() > 1) {
//more than one instrument in this part:
//for each instrument, find its first note and the last transposition change before that note
for (MxlScoreInstrument mxlScoreInstr : mxlScoreInstruments) {
getInfo(mxlScoreInstr.getId()).transpose =
findLastTransposeBeforeFirstNote(mxlScoreInstr.getId());
}
}
}
/**
* Returns the {@link Transpose} of the first measure of this part,
* or {@link Transpose#noTranspose} if there is none.
*/
private Transpose findFirstTranspose() {
List<MxlMeasure> mxlMeasures = mxlPart.getMeasures();
if (mxlMeasures.size() > 0) {
MxlMeasure mxlMeasure = mxlMeasures.get(0);
for (MxlMusicDataContent c : mxlMeasure.getMusicData().getContent()) {
if (c.getMusicDataContentType() == MxlMusicDataContentType.Attributes) {
MxlAttributes a = (MxlAttributes) c;
return new TransposeReader(a.getTranspose()).read();
}
}
}
return Transpose.noTranspose;
}
/**
* Returns the last {@link Transpose} of this part that can be found
* before the first note that is played by the instrument with the given ID,
* or {@link Transpose#noTranspose} if there is none.
*/
private Transpose findLastTransposeBeforeFirstNote(String instrumentID) {
for (MxlMeasure mxlMeasure : mxlPart.getMeasures()) {
MxlAttributes lastAttributes = null;
for (MxlMusicDataContent c : mxlMeasure.getMusicData().getContent()) {
if (c.getMusicDataContentType() == MxlMusicDataContentType.Attributes) {
lastAttributes = (MxlAttributes) c;
}
else if (c.getMusicDataContentType() == MxlMusicDataContentType.Note) {
MxlNote n = (MxlNote) c;
if (n.getInstrument() != null && n.getInstrument().getId().equals(instrumentID) &&
lastAttributes != null)
return new TransposeReader(lastAttributes.getTranspose()).read();
}
}
}
return Transpose.noTranspose;
}
private void readMidiInstruments() {
for (MxlMidiInstrument mxlMidiInstr : mxlScorePart.getMidiInstruments()) {
Info info = getInfo(mxlMidiInstr.id);
//midi program
info.midiProgram = mxlMidiInstr.getMidiProgram();
//midi channel
info.midiChannel = mxlMidiInstr.getMidiChannel();
//global volume
info.volume = mxlMidiInstr.getVolume();
if (info.volume != null)
info.volume /= 100; //to 0..1
//global panning
info.pan = mxlMidiInstr.getPan();
if (info.pan != null) {
if (info.pan > 90)
info.pan = 90 - (info.pan - 90); //e.g. convert 120° to 60°
else if (info.pan < -90)
info.pan = -90 - (info.pan + 90); //e.g. convert -120° to -60°
info.pan /= 90f; //to -1..1
}
}
}
private List<Instrument> createInstruments() {
List<Instrument> ret = alist();
for (MxlScoreInstrument mxlScoreInstr : mxlScorePart.getScoreInstruments()) {
Instrument instrument = readInstrument(getInfo(mxlScoreInstr.getId()));
ret.add(instrument);
}
//when no instrument was created, but a transposition was found, create
//a default instrument with this transposition
if (ret.size() == 0 && partTranspose != Transpose.noTranspose) {
PitchedInstrument instrument = new PitchedInstrument(mxlPart.getId(), 0);
instrument.setTranspose(partTranspose);
ret.add(instrument);
}
return ret;
}
private Instrument readInstrument(Info info) {
Instrument instrument = null;
if (info.midiChannel != null && info.midiChannel == 10) {
//unpitched instrument
instrument = new UnpitchedInstrument(info.id);
}
else {
//pitched instrument
//midi-program is 1-based in MusicXML but 0-based in MIDI
int midiProgram = notNull(info.midiProgram, 1) - 1; //TODO: find value that matches instrument name
midiProgram = MathUtils.clamp(midiProgram, 0, 127);
PitchedInstrument pitchedInstrument;
instrument = pitchedInstrument = new PitchedInstrument(info.id, midiProgram);
pitchedInstrument.setTranspose(info.transpose);
}
instrument.setName(info.name);
instrument.setAbbreviation(info.abbreviation);
if (info.volume != null)
instrument.setVolume(info.volume);
if (info.pan != null)
instrument.setPan(info.pan);
return instrument;
}
}