package org.herac.tuxguitar.io.midi;
import java.awt.Color;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.herac.tuxguitar.gui.editors.tab.TGBeatImpl;
import org.herac.tuxguitar.gui.editors.tab.TGMeasureHeaderImpl;
import org.herac.tuxguitar.gui.editors.tab.TGMeasureImpl;
import org.herac.tuxguitar.gui.editors.tab.TGNoteImpl;
import org.herac.tuxguitar.gui.editors.tab.TGTrackImpl;
import org.herac.tuxguitar.io.base.TGFileFormat;
import org.herac.tuxguitar.io.base.TGFileFormatException;
import org.herac.tuxguitar.io.base.TGLocalFileImporter;
import org.herac.tuxguitar.io.midi.base.MidiEvent;
import org.herac.tuxguitar.io.midi.base.MidiMessage;
import org.herac.tuxguitar.io.midi.base.MidiSequence;
import org.herac.tuxguitar.io.midi.base.MidiTrack;
import org.herac.tuxguitar.player.base.MidiControllers;
import org.herac.tuxguitar.song.managers.TGSongManager;
import org.herac.tuxguitar.song.models.TGBeat;
import org.herac.tuxguitar.song.models.TGChannel;
import org.herac.tuxguitar.song.models.TGDuration;
import org.herac.tuxguitar.song.models.TGMeasure;
import org.herac.tuxguitar.song.models.TGMeasureHeader;
import org.herac.tuxguitar.song.models.TGNote;
import org.herac.tuxguitar.song.models.TGSong;
import org.herac.tuxguitar.song.models.TGString;
import org.herac.tuxguitar.song.models.TGTempo;
import org.herac.tuxguitar.song.models.TGTimeSignature;
import org.herac.tuxguitar.song.models.TGTrack;
public class MidiSongImporter implements TGLocalFileImporter {
private class TempChannel {
private int balance;
private int channel;
private int instrument;
private int track;
private int volume;
public TempChannel(int channel) {
this.channel = channel;
this.instrument = 0;
this.volume = 127;
this.balance = 64;
this.track = -1;
}
public int getBalance() {
return this.balance;
}
public int getChannel() {
return this.channel;
}
public int getInstrument() {
return this.instrument;
}
public int getTrack() {
return this.track;
}
public int getVolume() {
return this.volume;
}
public void setBalance(int balance) {
this.balance = balance;
}
/*
* public void setChannel(int channel) { this.channel = channel; }
*/
public void setInstrument(int instrument) {
this.instrument = instrument;
}
public void setTrack(int track) {
this.track = track;
}
public void setVolume(int volume) {
this.volume = volume;
}
}
private class TempNote {
private int channel;
private long tick;
private int track;
private int value;
public TempNote(int track, int channel, int value, long tick) {
this.track = track;
this.channel = channel;
this.value = value;
this.tick = tick;
}
public int getChannel() {
return this.channel;
}
public long getTick() {
return this.tick;
}
public int getTrack() {
return this.track;
}
public int getValue() {
return this.value;
}
/*
* public void setChannel(int channel) { this.channel = channel; }
*/
/*
* public void setTick(long tick) { this.tick = tick; }
*/
/*
* public void setTrack(int track) { this.track = track; }
*/
/*
* public void setValue(int value) { this.value = value; }
*/
}
private class TrackTuningHelper {
private int maxValue;
private int minValue;
private int track;
public TrackTuningHelper(int track) {
this.track = track;
this.maxValue = -1;
this.minValue = -1;
}
public void checkValue(int value) {
if (this.minValue < 0 || value < this.minValue) {
this.minValue = value;
}
if (this.maxValue < 0 || value > this.maxValue) {
this.maxValue = value;
}
}
/*
* public int getMaxValue() { return this.maxValue; }
*/
/*
* public int getMinValue() { return this.minValue; }
*/
public List<TGString> getStrings() {
List<TGString> strings = new ArrayList<TGString>();
int maxFret = 24;
if (this.minValue >= 40 && this.maxValue <= 64 + maxFret) {
strings.add(new TGString(1, 64));
strings.add(new TGString(2, 59));
strings.add(new TGString(3, 55));
strings.add(new TGString(4, 50));
strings.add(new TGString(5, 45));
strings.add(new TGString(6, 40));
} else if (this.minValue >= 38 && this.maxValue <= 64 + maxFret) {
strings.add(new TGString(1, 64));
strings.add(new TGString(2, 59));
strings.add(new TGString(3, 55));
strings.add(new TGString(4, 50));
strings.add(new TGString(5, 45));
strings.add(new TGString(6, 38));
} else if (this.minValue >= 35 && this.maxValue <= 64 + maxFret) {
strings.add(new TGString(1, 64));
strings.add(new TGString(2, 59));
strings.add(new TGString(3, 55));
strings.add(new TGString(4, 50));
strings.add(new TGString(5, 45));
strings.add(new TGString(6, 40));
strings.add(new TGString(7, 35));
} else if (this.minValue >= 28 && this.maxValue <= 43 + maxFret) {
strings.add(new TGString(1, 43));
strings.add(new TGString(2, 38));
strings.add(new TGString(3, 33));
strings.add(new TGString(4, 28));
} else if (this.minValue >= 23 && this.maxValue <= 43 + maxFret) {
strings.add(new TGString(1, 43));
strings.add(new TGString(2, 38));
strings.add(new TGString(3, 33));
strings.add(new TGString(4, 28));
strings.add(new TGString(5, 23));
} else {
int stringCount = 6;
int stringSpacing = ((this.maxValue - (maxFret - 4) - this.minValue) / stringCount);
if (stringSpacing > 5) {
stringCount = 7;
stringSpacing = ((this.maxValue - (maxFret - 4) - this.minValue) / stringCount);
}
int maxStringValue = (this.minValue + (stringCount * stringSpacing));
while (strings.size() < stringCount) {
maxStringValue -= stringSpacing;
strings.add(new TGString(strings.size() + 1,
maxStringValue));
}
}
return strings;
}
public int getTrack() {
return this.track;
}
}
private static final int MIN_DURATION_VALUE = TGDuration.SIXTY_FOURTH;
// protected TGFactory factory;
private List<TGMeasureHeader> headers;
private int resolution;
private MidiSettings settings;
protected InputStream stream;
private List<TempChannel> tempChannels;
private List<TempNote> tempNotes;
private List<TGTrack> tracks;
private List<TrackTuningHelper> trackTuningHelpers;
public MidiSongImporter() {
super();
}
private void checkAll() throws Exception {
checkTracks();
int headerCount = this.headers.size();
for (int i = 0; i < this.tracks.size(); i++) {
TGTrack track = (TGTrack) this.tracks.get(i);
while (track.countMeasures() < headerCount) {
long start = TGDuration.QUARTER_TIME;
TGMeasure lastMeasure = ((track.countMeasures() > 0) ? track
.getMeasure(track.countMeasures() - 1) : null);
if (lastMeasure != null) {
start = (lastMeasure.getStart() + lastMeasure.getLength());
}
track.addMeasure(new TGMeasureImpl(getHeader(start)));
}
}
if (this.headers.isEmpty() || this.tracks.isEmpty()) {
throw new Exception("Empty Song");
}
}
private void checkTracks() {
for (final TGTrack track : this.tracks) {
for (final TempChannel tempChannel : this.tempChannels) {
if (tempChannel.getTrack() == track.getNumber()) {
if (track.getChannel().getChannel() < 0) {
track.getChannel().setChannel((short) tempChannel.getChannel());
track.getChannel().setInstrument(
(short) tempChannel.getInstrument());
track.getChannel().setVolume((short) tempChannel.getVolume());
track.getChannel().setBalance((short) tempChannel.getBalance());
} else if (track.getChannel().getEffectChannel() < 0) {
track.getChannel().setEffectChannel(
(short) tempChannel.getChannel());
}
}
}
if (track.getChannel().getChannel() < 0) {
track.getChannel().setChannel((short) (TGSongManager.MAX_CHANNELS - 1));
track.getChannel().setInstrument((short) 0);
track.getChannel().setVolume((short) 127);
track.getChannel().setBalance((short) 64);
}
if (track.getChannel().getEffectChannel() < 0) {
track.getChannel().setEffectChannel(track.getChannel().getChannel());
}
if (!track.isPercussionTrack()) {
track.setStrings(getTrackTuningHelper(track.getNumber()).getStrings());
} else {
track.setStrings(TGSongManager.createPercussionStrings(6));
}
}
}
public boolean configure(boolean setDefaults) {
this.settings = (setDefaults ? MidiSettings.getDefaults()
: new MidiSettingsDialog().open());
return (this.settings != null);
}
private TGBeat getBeat(TGMeasure measure, long start) {
int beatCount = measure.countBeats();
for (int i = 0; i < beatCount; i++) {
TGBeat beat = measure.getBeat(i);
if (beat.getStart() == start) {
return beat;
}
}
TGBeat beat = new TGBeatImpl();
beat.setStart(start);
measure.addBeat(beat);
return beat;
}
public TGFileFormat getFileFormat() {
return new TGFileFormat("Midi", "*.mid;*.midi");
}
private TGMeasureHeader getHeader(long tick) {
long realTick = (tick >= TGDuration.QUARTER_TIME) ? tick
: TGDuration.QUARTER_TIME;
for (final TGMeasureHeader header : this.headers) {
if (realTick >= header.getStart()
&& realTick < header.getStart() + header.getLength()) {
return header;
}
}
TGMeasureHeader last = getLastHeader();
TGMeasureHeader header = new TGMeasureHeaderImpl();
header.setNumber((last != null) ? last.getNumber() + 1 : 1);
header.setStart((last != null) ? (last.getStart() + last.getLength())
: TGDuration.QUARTER_TIME);
header.getTempo().setValue(
(last != null) ? last.getTempo().getValue() : 120);
if (last != null) {
header.setTimeSignature(last.getTimeSignature().clone());
} else {
header.getTimeSignature().setNumerator(4);
header.getTimeSignature().getDenominator().setValue(TGDuration.QUARTER);
}
this.headers.add(header);
if (realTick >= header.getStart()
&& realTick < header.getStart() + header.getLength()) {
return header;
}
return getHeader(realTick);
}
public String getImportName() {
return "Midi";
}
private TGMeasureHeader getLastHeader() {
if (!this.headers.isEmpty()) {
return (TGMeasureHeader) this.headers.get(this.headers.size() - 1);
}
return null;
}
private TGMeasure getMeasure(TGTrack track, long tick) {
long realTick = (tick >= TGDuration.QUARTER_TIME) ? tick
: TGDuration.QUARTER_TIME;
for (final TGMeasure measure : track.getMeasures()) {
if (realTick >= measure.getStart()
&& realTick < measure.getStart() + measure.getLength()) {
return measure;
}
}
getHeader(realTick);
for (int i = 0; i < this.headers.size(); i++) {
boolean exist = false;
TGMeasureHeader header = (TGMeasureHeader) this.headers.get(i);
int measureCount = track.countMeasures();
for (int j = 0; j < measureCount; j++) {
TGMeasure measure = track.getMeasure(j);
if (measure.getHeader().equals(header)) {
exist = true;
}
}
if (!exist) {
TGMeasure measure = new TGMeasureImpl(header);
track.addMeasure(measure);
}
}
return getMeasure(track, realTick);
}
private int getNextTrackNumber() {
return (this.tracks.size() + 1);
}
public TempChannel getTempChannel(int channel) {
for (final TempChannel tempChannel : this.tempChannels) {
if (tempChannel.getChannel() == channel) {
return tempChannel;
}
}
TempChannel tempChannel = new TempChannel(channel);
this.tempChannels.add(tempChannel);
return tempChannel;
}
private TempNote getTempNote(int track, int channel, int value, boolean purge) {
for (int i = 0; i < this.tempNotes.size(); i++) {
TempNote note = (TempNote) this.tempNotes.get(i);
if (note.getTrack() == track && note.getChannel() == channel
&& note.getValue() == value) {
if (purge) {
this.tempNotes.remove(i);
}
return note;
}
}
return null;
}
private TGTrack getTrack(int number) {
for (final TGTrack track : this.tracks) {
if (track.getNumber() == number) {
return track;
}
}
TGChannel channel = new TGChannel();
channel.setChannel((short) -1);
channel.setEffectChannel((short) -1);
channel.setInstrument((short) 0);
TGTrack track = new TGTrackImpl();
track.setNumber(number);
track.setChannel(channel);
track.setColor(Color.RED);
this.tracks.add(track);
return track;
}
protected TrackTuningHelper getTrackTuningHelper(int track) {
for (final TrackTuningHelper helper : this.trackTuningHelpers) {
if (helper.getTrack() == track) {
return helper;
}
}
TrackTuningHelper helper = new TrackTuningHelper(track);
this.trackTuningHelpers.add(helper);
return helper;
}
public TGSong importSong() throws TGFileFormatException {
try {
if (this.settings == null || this.stream == null) {
return null;
}
MidiSequence sequence = new MidiFileReader().getSequence(this.stream);
initFields(sequence);
for (int i = 0; i < sequence.countTracks(); i++) {
MidiTrack track = sequence.getTrack(i);
int trackNumber = getNextTrackNumber();
int events = track.size();
for (int j = 0; j < events; j++) {
MidiEvent event = track.get(j);
parseMessage(trackNumber, event.getTick(), event.getMessage());
}
}
checkAll();
TGSong song = new TGSong();
for (final TGMeasureHeader header : this.headers) {
song.addMeasureHeader(header);
}
for (final TGTrack track : this.tracks) {
song.addTrack(track);
}
return new SongAdjuster(song).adjustSong();
} catch (Throwable throwable) {
throw new TGFileFormatException(throwable);
}
}
public void init(InputStream stream) {
// this.factory = factory;
this.stream = stream;
}
private void initFields(MidiSequence sequence) {
this.resolution = sequence.getResolution();
this.headers = new ArrayList<TGMeasureHeader>();
this.tracks = new ArrayList<TGTrack>();
this.tempNotes = new ArrayList<TempNote>();
this.tempChannels = new ArrayList<TempChannel>();
this.trackTuningHelpers = new ArrayList<TrackTuningHelper>();
}
private void makeNote(long tick, int track, int channel, int value) {
TempNote tempNote = getTempNote(track, channel, value, true);
if (tempNote != null) {
int nString = 0;
int nValue = (tempNote.getValue() + this.settings.getTranspose());
int nVelocity = 64;
long nStart = tempNote.getTick();
TGDuration minDuration = newDuration(MIN_DURATION_VALUE);
TGDuration nDuration = TGDuration.fromTime(tick - tempNote.getTick(),
minDuration);
TGMeasure measure = getMeasure(getTrack(track), tempNote.getTick());
TGBeat beat = getBeat(measure, nStart);
beat.getVoice(0).setDuration(nDuration.clone());
TGNote note = new TGNoteImpl();
note.setValue(nValue);
note.setString(nString);
note.setVelocity(nVelocity);
beat.getVoice(0).addNote(note);
}
}
private void makeTempNotesBefore(long tick, int track) {
long nextTick = tick;
boolean check = true;
while (check) {
check = false;
for (int i = 0; i < this.tempNotes.size(); i++) {
TempNote note = (TempNote) this.tempNotes.get(i);
if (note.getTick() < nextTick && note.getTrack() == track) {
nextTick = note.getTick() + (TGDuration.QUARTER_TIME * 5); // First
// beat +
// 4/4
// measure;
makeNote(nextTick, track, note.getChannel(), note.getValue());
check = true;
break;
}
}
}
}
protected TGDuration newDuration(int value) {
TGDuration duration = new TGDuration();
duration.setValue(value);
return duration;
}
private void parseControlChange(byte[] data) {
int length = data.length;
int channel = (length > 0) ? ((data[0] & 0xFF) & 0x0F) : -1;
int control = (length > 1) ? (data[1] & 0xFF) : -1;
int value = (length > 2) ? (data[2] & 0xFF) : -1;
if (channel != -1 && control != -1 && value != -1) {
if (control == MidiControllers.VOLUME) {
getTempChannel(channel).setVolume(value);
} else if (control == MidiControllers.BALANCE) {
getTempChannel(channel).setBalance(value);
}
}
}
private void parseMessage(int trackNumber, long tick, MidiMessage message) {
long parsedTick = parseTick(tick + this.resolution);
// NOTE ON
if (message.getType() == MidiMessage.TYPE_SHORT
&& message.getCommand() == MidiMessage.NOTE_ON) {
parseNoteOn(trackNumber, parsedTick, message.getData());
}
// NOTE OFF
else if (message.getType() == MidiMessage.TYPE_SHORT
&& message.getCommand() == MidiMessage.NOTE_OFF) {
parseNoteOff(trackNumber, parsedTick, message.getData());
}
// PROGRAM CHANGE
else if (message.getType() == MidiMessage.TYPE_SHORT
&& message.getCommand() == MidiMessage.PROGRAM_CHANGE) {
parseProgramChange(message.getData());
}
// CONTROL CHANGE
else if (message.getType() == MidiMessage.TYPE_SHORT
&& message.getCommand() == MidiMessage.CONTROL_CHANGE) {
parseControlChange(message.getData());
}
// TIME SIGNATURE
else if (message.getType() == MidiMessage.TYPE_META
&& message.getCommand() == MidiMessage.TIME_SIGNATURE_CHANGE) {
parseTimeSignature(parsedTick, message.getData());
}
// TEMPO
else if (message.getType() == MidiMessage.TYPE_META
&& message.getCommand() == MidiMessage.TEMPO_CHANGE) {
parseTempo(parsedTick, message.getData());
}
}
private void parseNoteOff(int track, long tick, byte[] data) {
int length = data.length;
int channel = (length > 0) ? ((data[0] & 0xFF) & 0x0F) : 0;
int value = (length > 1) ? (data[1] & 0xFF) : 0;
makeNote(tick, track, channel, value);
}
private void parseNoteOn(int track, long tick, byte[] data) {
int length = data.length;
int channel = (length > 0) ? ((data[0] & 0xFF) & 0x0F) : 0;
int value = (length > 1) ? (data[1] & 0xFF) : 0;
int velocity = (length > 2) ? (data[2] & 0xFF) : 0;
if (velocity == 0) {
parseNoteOff(track, tick, data);
} else if (value > 0) {
makeTempNotesBefore(tick, track);
getTempChannel(channel).setTrack(track);
getTrackTuningHelper(track).checkValue(value);
this.tempNotes.add(new TempNote(track, channel, value, tick));
}
}
private void parseProgramChange(byte[] data) {
int length = data.length;
int channel = (length > 0) ? ((data[0] & 0xFF) & 0x0F) : -1;
int instrument = (length > 1) ? (data[1] & 0xFF) : -1;
if (channel != -1 && instrument != -1) {
getTempChannel(channel).setInstrument(instrument);
}
}
private void parseTempo(long tick, byte[] data) {
if (data.length >= 3) {
TGTempo tempo = TGTempo.fromUSQ((data[2] & 0xff)
| ((data[1] & 0xff) << 8) | ((data[0] & 0xff) << 16));
getHeader(tick).setTempo(tempo);
}
}
private long parseTick(long tick) {
return Math.abs(TGDuration.QUARTER_TIME * tick / this.resolution);
}
private void parseTimeSignature(long tick, byte[] data) {
if (data.length >= 2) {
TGTimeSignature timeSignature = new TGTimeSignature();
timeSignature.setNumerator(data[0]);
timeSignature.getDenominator().setValue(TGDuration.QUARTER);
if (data[1] == 0) {
timeSignature.getDenominator().setValue(TGDuration.WHOLE);
} else if (data[1] == 1) {
timeSignature.getDenominator().setValue(TGDuration.HALF);
} else if (data[1] == 2) {
timeSignature.getDenominator().setValue(TGDuration.QUARTER);
} else if (data[1] == 3) {
timeSignature.getDenominator().setValue(TGDuration.EIGHTH);
} else if (data[1] == 4) {
timeSignature.getDenominator().setValue(TGDuration.SIXTEENTH);
} else if (data[1] == 5) {
timeSignature.getDenominator().setValue(TGDuration.THIRTY_SECOND);
}
getHeader(tick).setTimeSignature(timeSignature);
}
}
}
class SongAdjuster {
// private TGFactory factory;
private long minDurationTime;
private TGSong song;
public SongAdjuster(TGSong song) {
// this.factory = factory;
this.song = song;
this.minDurationTime = 40;
}
public TGSong adjustSong() {
for (final TGTrack track : this.song.getTracks()) {
adjustTrack(track);
}
return this.song;
}
private void adjustStrings(TGBeat beat) {
TGTrack track = beat.getMeasure().getTrack();
List<TGString> freeStrings = new ArrayList<TGString>(track.getStrings());
List<TGNote> notesToRemove = new ArrayList<TGNote>();
// ajusto las cuerdas
for (final TGNote note : beat.getVoice(0).getNotes()) {
int string = getStringForValue(freeStrings, note.getValue());
for (int j = 0; j < freeStrings.size(); j++) {
TGString tempString = (TGString) freeStrings.get(j);
if (tempString.getNumber() == string) {
note.setValue(note.getValue() - tempString.getValue());
note.setString(tempString.getNumber());
freeStrings.remove(j);
break;
}
}
// Cannot have more notes on same string
if (note.getString() < 1) {
notesToRemove.add(note);
}
}
// Remove notes
while (notesToRemove.size() > 0) {
beat.getVoice(0).removeNote((TGNote) notesToRemove.get(0));
notesToRemove.remove(0);
}
}
private void adjustStrings(TGMeasure measure) {
for (int i = 0; i < measure.countBeats(); i++) {
TGBeat beat = measure.getBeat(i);
adjustStrings(beat);
}
}
private void adjustTrack(TGTrack track) {
for (final TGMeasure measure : track.getMeasures()) {
process(measure);
}
}
private int getStringForValue(List<TGString> strings, int value) {
int minFret = -1;
int stringForValue = 0;
for (int i = 0; i < strings.size(); i++) {
TGString string = strings.get(i);
int fret = value - string.getValue();
if (minFret < 0 || (fret >= 0 && fret < minFret)) {
stringForValue = string.getNumber();
minFret = fret;
}
}
return stringForValue;
}
public void joinBeats(TGMeasure measure) {
TGBeat previous = null;
boolean finish = true;
long measureStart = measure.getStart();
long measureEnd = (measureStart + measure.getLength());
for (int i = 0; i < measure.countBeats(); i++) {
TGBeat beat = measure.getBeat(i);
long beatStart = beat.getStart();
long beatLength = beat.getVoice(0).getDuration().getTime();
if (previous != null) {
long previousStart = previous.getStart();
long previousLength = previous.getVoice(0).getDuration().getTime();
// if(previousStart == beatStart){
if (beatStart >= previousStart
&& (previousStart + this.minDurationTime) > beatStart) {
// add beat notes to previous
for (int n = 0; n < beat.getVoice(0).getNotes().size(); n++) {
TGNote note = beat.getVoice(0).getNote(n);
previous.getVoice(0).addNote(note);
}
// add beat chord to previous
if (!previous.isChordBeat() && beat.isChordBeat()) {
previous.setChord(beat.getChord());
}
// add beat text to previous
if (!previous.isTextBeat() && beat.isTextBeat()) {
previous.setText(beat.getText());
}
// set the best duration
if (beatLength > previousLength
&& (beatStart + beatLength) <= measureEnd) {
previous.getVoice(0).setDuration(
beat.getVoice(0).getDuration().clone());
}
measure.removeBeat(beat);
finish = false;
break;
}
else if (previousStart < beatStart
&& (previousStart + previousLength) > beatStart) {
if (beat.getVoice(0).isRestVoice()) {
measure.removeBeat(beat);
finish = false;
break;
}
TGDuration duration = TGDuration
.fromTime((beatStart - previousStart));
previous.getVoice(0).setDuration(duration.clone());
}
}
if ((beatStart + beatLength) > measureEnd) {
if (beat.getVoice(0).isRestVoice()) {
measure.removeBeat(beat);
finish = false;
break;
}
TGDuration duration = TGDuration.fromTime(measureEnd - beatStart);
beat.getVoice(0).setDuration(duration.clone());
}
previous = beat;
}
if (!finish) {
joinBeats(measure);
}
}
public void orderBeats(TGMeasure measure) {
for (int i = 0; i < measure.countBeats(); i++) {
TGBeat minBeat = null;
for (int j = i; j < measure.countBeats(); j++) {
TGBeat beat = measure.getBeat(j);
if (minBeat == null || beat.getStart() < minBeat.getStart()) {
minBeat = beat;
}
}
measure.moveBeat(i, minBeat);
}
}
public void process(TGMeasure measure) {
orderBeats(measure);
joinBeats(measure);
adjustStrings(measure);
}
}