package org.herac.tuxguitar.io.midi; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; 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; public class MidiFileReader implements MidiFileHeader{ public static boolean CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX = true; private static final int STATUS_NONE = 0; private static final int STATUS_ONE_BYTE = 1; private static final int STATUS_TWO_BYTES = 2; private static final int STATUS_SYSEX = 3; private static final int STATUS_META = 4; public MidiSequence getSequence(InputStream stream)throws MidiFileException, IOException{ DataInputStream in = new DataInputStream(stream); if (in.readInt() != HEADER_MAGIC){ throw new MidiFileException("not a MIDI file: wrong header magic"); } int headerLength = in.readInt(); if (headerLength < HEADER_LENGTH){ throw new MidiFileException("corrupt MIDI file: wrong header length"); } int type = in.readShort(); if (type < 0 || type > 2){ throw new MidiFileException("corrupt MIDI file: illegal type"); } if (type == 2){ throw new MidiFileException("this implementation doesn't support type 2 MIDI files"); } int trackCount = in.readShort(); if (trackCount <= 0){ throw new MidiFileException("corrupt MIDI file: number of tracks must be positive"); } if (type == 0 && trackCount != 1){ throw new MidiFileException("corrupt MIDI file: type 0 files must contain exactely one track"); } float divisionType = -1.0F; int resolution = -1; int division = in.readUnsignedShort(); if ((division & 0x8000) != 0){ int frameType = -((division >>> 8) & 0xFF); if(frameType == 24){ divisionType = MidiSequence.SMPTE_24; }else if(frameType == 25){ divisionType = MidiSequence.SMPTE_25; }else if(frameType == 29){ divisionType = MidiSequence.SMPTE_30DROP; }else if(frameType == 30){ divisionType = MidiSequence.SMPTE_30; }else{ throw new MidiFileException("corrupt MIDI file: illegal frame division type"); } resolution = division & 0xff; }else{ divisionType = MidiSequence.PPQ; resolution = division & 0x7fff; } in.skip(headerLength - HEADER_LENGTH); MidiSequence sequence = new MidiSequence(divisionType,resolution); for (int i = 0; i < trackCount; i++){ MidiTrack track = new MidiTrack(); sequence.addTrack(track); readTrack(in, track); } in.close(); return sequence; } private void readTrack(DataInputStream in, MidiTrack track)throws MidiFileException, IOException{ while (true){ if (in.readInt() == TRACK_MAGIC){ break; } int chunkLength = in.readInt(); if (chunkLength % 2 != 0){ chunkLength++; } in.skip(chunkLength); } MidiTrackReaderHelper helper = new MidiTrackReaderHelper(0,in.readInt(),-1); while (helper.remainingBytes > 0){ helper.ticks += readVariableLengthQuantity(in, helper); MidiEvent event = readEvent(in, helper); if(event != null){ track.add(event); } } } private static MidiEvent readEvent(DataInputStream in, MidiTrackReaderHelper helper)throws MidiFileException, IOException{ int statusByte = readUnsignedByte(in, helper); int savedByte = 0; boolean runningStatusApplies = false; if (statusByte < 0x80){ if (helper.runningStatusByte != -1){ runningStatusApplies = true; savedByte = statusByte; statusByte = helper.runningStatusByte; }else{ throw new MidiFileException("corrupt MIDI file: status byte missing"); } } int type = getType(statusByte); if(type == STATUS_ONE_BYTE){ int data = 0; if (runningStatusApplies){ data = savedByte; }else{ data = readUnsignedByte(in, helper); helper.runningStatusByte = statusByte; } return new MidiEvent(MidiMessage.shortMessage((statusByte & 0xF0),(statusByte & 0x0F) , data), helper.ticks); }else if(type == STATUS_TWO_BYTES){ int data1 = 0; if (runningStatusApplies){ data1 = savedByte; }else{ data1 = readUnsignedByte(in, helper); helper.runningStatusByte = statusByte; } return new MidiEvent(MidiMessage.shortMessage((statusByte & 0xF0),(statusByte & 0x0F) , data1, readUnsignedByte(in, helper)), helper.ticks); }else if(type == STATUS_SYSEX){ if (CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX){ helper.runningStatusByte = -1; } int dataLength = (int) readVariableLengthQuantity(in, helper); byte[] data = new byte[dataLength]; for (int i = 0; i < dataLength; i++){ data[i] = (byte) readUnsignedByte(in, helper); } }else if(type == STATUS_META){ if (CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX){ helper.runningStatusByte = -1; } int typeByte = readUnsignedByte(in, helper); int dataLength = (int) readVariableLengthQuantity(in, helper); byte[] data = new byte[dataLength]; for (int i = 0; i < dataLength; i++){ data[i] = (byte) readUnsignedByte(in, helper); } return new MidiEvent(MidiMessage.metaMessage(typeByte, data), helper.ticks); } return null; } private static int getType(int statusByte){ if (statusByte < 0xf0) { int command = statusByte & 0xf0; if(command == 0x80 || command == 0x90 || command == 0xa0 || command == 0xb0 || command == 0xe0){ return STATUS_TWO_BYTES; } else if(command == 0xc0 || command == 0xd0){ return STATUS_ONE_BYTE; } return STATUS_NONE; } else if (statusByte == 0xf0 || statusByte == 0xf7){ return STATUS_SYSEX; } else if (statusByte == 0xff){ return STATUS_META; } else{ return STATUS_NONE; } } public static long readVariableLengthQuantity(DataInputStream in, MidiTrackReaderHelper helper)throws MidiFileException, IOException{ int count = 0; long value = 0; while (count < 4){ int data = readUnsignedByte(in, helper); count++; value <<= 7; value |= (data & 0x7f); if (data < 128){ return value; } } throw new MidiFileException("not a MIDI file: unterminated variable-length quantity"); } public static int readUnsignedByte(DataInputStream dataInputStream, MidiTrackReaderHelper helper)throws IOException{ helper.remainingBytes--; return dataInputStream.readUnsignedByte(); } private class MidiTrackReaderHelper{ protected long ticks = 0; protected long remainingBytes; protected int runningStatusByte; protected MidiTrackReaderHelper(long ticks,long remainingBytes,int runningStatusByte){ this.ticks = ticks; this.remainingBytes = remainingBytes; this.runningStatusByte = runningStatusByte; } } }