/* * StandardMidiFileReader.java * * This file is part of Tritonus: http://www.tritonus.org/ */ /* * Copyright (c) 1999, 2000 by Matthias Pfisterer * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as published * by the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* |<--- this code is formatted to fit into 80 columns --->| */ package org.tritonus.midi.file; import java.io.FileInputStream; import java.io.InputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.File; import java.net.URL; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MetaMessage; import javax.sound.midi.MidiFileFormat; import javax.sound.midi.MidiEvent; import javax.sound.midi.MidiMessage; import javax.sound.midi.Sequence; import javax.sound.midi.ShortMessage; import javax.sound.midi.SysexMessage; import javax.sound.midi.Track; import javax.sound.midi.spi.MidiFileReader; import org.tritonus.share.TDebug; import org.tritonus.share.midi.TMidiFileFormat; /** TODO: */ public class StandardMidiFileReader extends MidiFileReader { /** TODO: */ 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; /** TODO: */ public MidiFileFormat getMidiFileFormat(InputStream inputStream) throws InvalidMidiDataException, IOException { DataInputStream dataInputStream = new DataInputStream(inputStream); int nHeaderMagic = dataInputStream.readInt(); if (nHeaderMagic != MidiConstants.HEADER_MAGIC) { throw new InvalidMidiDataException("not a MIDI file: wrong header magic"); } int nHeaderLength = dataInputStream.readInt(); if (nHeaderLength < 6) { throw new InvalidMidiDataException("corrupt MIDI file: wrong header length"); } int nType = dataInputStream.readShort(); if (nType < 0 || nType > 2) { throw new InvalidMidiDataException("corrupt MIDI file: illegal type"); } if (nType == 2) { throw new InvalidMidiDataException("this implementation doesn't support type 2 MIDI files"); } int nNumTracks = dataInputStream.readShort(); if (nNumTracks <= 0) { throw new InvalidMidiDataException("corrupt MIDI file: number of tracks must be positive"); } if (nType == 0 && nNumTracks != 1) { throw new InvalidMidiDataException("corrupt MIDI file: type 0 files must contain exactely one track"); } int nDivision = dataInputStream.readUnsignedShort(); float fDivisionType = -1.0F; int nResolution = -1; if ((nDivision & 0x8000) != 0) //frame division { // TODO: int nFrameType = -((nDivision >>> 8) & 0xFF); switch (nFrameType) { case 24: fDivisionType = Sequence.SMPTE_24; break; case 25: fDivisionType = Sequence.SMPTE_25; break; case 29: fDivisionType = Sequence.SMPTE_30DROP; break; case 30: fDivisionType = Sequence.SMPTE_30; break; default: throw new InvalidMidiDataException("corrupt MIDI file: illegal frame division type"); } nResolution = nDivision & 0xff; } else // BPM division { fDivisionType = Sequence.PPQ; nResolution = nDivision & 0x7fff; } // skip additional bytes in the header dataInputStream.skip(nHeaderLength - 6); MidiFileFormat midiFileFormat = new TMidiFileFormat( nType, fDivisionType, nResolution, MidiFileFormat.UNKNOWN_LENGTH, MidiFileFormat.UNKNOWN_LENGTH, nNumTracks); return midiFileFormat; } /** TODO: */ public MidiFileFormat getMidiFileFormat(URL url) throws InvalidMidiDataException, IOException { InputStream inputStream = url.openStream(); try { return getMidiFileFormat(inputStream); } finally { inputStream.close(); } } /** TODO: */ public MidiFileFormat getMidiFileFormat(File file) throws InvalidMidiDataException, IOException { InputStream inputStream = new FileInputStream(file); //inputStream = new BufferedInputStream(inputStream, 1024); try { return getMidiFileFormat(inputStream); } finally { inputStream.close(); } } /** TODO: */ public Sequence getSequence(URL url) throws InvalidMidiDataException, IOException { InputStream inputStream = url.openStream(); try { return getSequence(inputStream); } catch (InvalidMidiDataException e) { if (TDebug.TraceAllExceptions) { TDebug.out(e); } inputStream.close(); throw e; } catch (IOException e) { if (TDebug.TraceAllExceptions) { TDebug.out(e); } inputStream.close(); throw e; } } /** TODO: */ public Sequence getSequence(File file) throws InvalidMidiDataException, IOException { InputStream inputStream = new FileInputStream(file); // inputStream = new BufferedInputStream(inputStream, 1024); try { return getSequence(inputStream); } catch (InvalidMidiDataException e) { if (TDebug.TraceAllExceptions) { TDebug.out(e); } inputStream.close(); throw e; } catch (IOException e) { if (TDebug.TraceAllExceptions) { TDebug.out(e); } inputStream.close(); throw e; } } /** TODO: */ public Sequence getSequence(InputStream inputStream) throws InvalidMidiDataException, IOException { MidiFileFormat midiFileFormat = getMidiFileFormat(inputStream); Sequence sequence = new Sequence( midiFileFormat.getDivisionType(), midiFileFormat.getResolution()); DataInputStream dataInputStream = new DataInputStream(inputStream); int nNumTracks = ((TMidiFileFormat) midiFileFormat).getTrackCount(); for (int nTrack = 0; nTrack < nNumTracks; nTrack++) { Track track = sequence.createTrack(); readTrack(dataInputStream, track); } return sequence; } /** TODO: */ private void readTrack(DataInputStream dataInputStream, Track track) throws InvalidMidiDataException, IOException { // search for a "MTrk" chunk while (true) { int nMagic = dataInputStream.readInt(); if (nMagic == MidiConstants.TRACK_MAGIC) { break; } int nChunkLength = dataInputStream.readInt(); if (nChunkLength % 2 != 0) { nChunkLength++; } dataInputStream.skip(nChunkLength); } int nTrackChunkLength = dataInputStream.readInt(); long lTicks = 0; long[] alRemainingBytes = new long[1]; alRemainingBytes[0] = nTrackChunkLength; int[] anRunningStatusByte = new int[1]; // indicates no running status in effect anRunningStatusByte[0] = -1; while (alRemainingBytes[0] > 0) { long lDeltaTicks = readVariableLengthQuantity(dataInputStream, alRemainingBytes); // TDebug.out("delta ticks: " + lDeltaTicks); lTicks += lDeltaTicks; MidiEvent event = readEvent(dataInputStream, alRemainingBytes, anRunningStatusByte, lTicks); track.add(event); } } /** TODO: */ private static MidiEvent readEvent(DataInputStream dataInputStream, long[] alRemainingBytes, int[] anRunningStatusByte, long lTicks) throws InvalidMidiDataException, IOException { int nStatusByte = readUnsignedByte(dataInputStream, alRemainingBytes); // TDebug.out("status byte: " + nStatusByte); MidiMessage message = null; boolean bRunningStatusApplies = false; int nSavedByte = 0; if (nStatusByte < 0x80) { if (anRunningStatusByte[0] != -1) { bRunningStatusApplies = true; nSavedByte = nStatusByte; nStatusByte = anRunningStatusByte[0]; } else { throw new InvalidMidiDataException("corrupt MIDI file: status byte missing"); } } switch (getType(nStatusByte)) { case STATUS_ONE_BYTE: int nByte = 0; if (bRunningStatusApplies) { nByte = nSavedByte; } else { nByte = readUnsignedByte(dataInputStream, alRemainingBytes); anRunningStatusByte[0] = nStatusByte; } ShortMessage shortMessage1 = new ShortMessage(); shortMessage1.setMessage(nStatusByte, nByte, 0); message = shortMessage1; break; case STATUS_TWO_BYTES: int nByte1 = 0; if (bRunningStatusApplies) { nByte1 = nSavedByte; } else { nByte1 = readUnsignedByte(dataInputStream, alRemainingBytes); anRunningStatusByte[0] = nStatusByte; } int nByte2 = readUnsignedByte(dataInputStream, alRemainingBytes); ShortMessage shortMessage2 = new ShortMessage(); shortMessage2.setMessage(nStatusByte, nByte1, nByte2); message = shortMessage2; break; case STATUS_SYSEX: if (CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX) { anRunningStatusByte[0] = -1; } int nSysexDataLength = (int) readVariableLengthQuantity(dataInputStream, alRemainingBytes); byte[] abSysexData = new byte[nSysexDataLength]; for (int i = 0; i < nSysexDataLength; i++) { int nDataByte = readUnsignedByte(dataInputStream, alRemainingBytes); abSysexData[i] = (byte) nDataByte; } SysexMessage sysexMessage = new SysexMessage(); sysexMessage.setMessage(nStatusByte, abSysexData, nSysexDataLength); message = sysexMessage; break; case STATUS_META: if (CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX) { anRunningStatusByte[0] = -1; } int nTypeByte = readUnsignedByte(dataInputStream, alRemainingBytes); int nMetaDataLength = (int) readVariableLengthQuantity(dataInputStream, alRemainingBytes); byte[] abMetaData = new byte[nMetaDataLength]; for (int i = 0; i < nMetaDataLength; i++) { int nDataByte = readUnsignedByte(dataInputStream, alRemainingBytes); abMetaData[i] = (byte) nDataByte; } MetaMessage metaMessage = new MetaMessage(); metaMessage.setMessage(nTypeByte, abMetaData, nMetaDataLength); message = metaMessage; break; default: } MidiEvent event = new MidiEvent(message, lTicks); return event; } // TODO: use table /** TODO: */ private static int getType(int nStatusByte) { if (nStatusByte < 0xf0) // channel voice or mode command { int nCommand = nStatusByte & 0xf0; switch (nCommand) { case 0x80: // note off case 0x90: // note on case 0xa0: // polyphonic key pressure case 0xb0: // control change case 0xe0: // pitch wheel change return STATUS_TWO_BYTES; case 0xc0: // program change case 0xd0: // channel pressure return STATUS_ONE_BYTE; default: return STATUS_NONE; } } else if (nStatusByte == 0xf0 || nStatusByte == 0xf7) { return STATUS_SYSEX; } else if (nStatusByte == 0xff) { return STATUS_META; } else { return STATUS_NONE; } } /** TODO: */ public static long readVariableLengthQuantity(DataInputStream dataInputStream, long[] alRemainingBytes) throws InvalidMidiDataException, IOException { long lValue = 0; int nByteCount = 0; while (nByteCount < 4) { int nByte = readUnsignedByte(dataInputStream, alRemainingBytes); nByteCount++; lValue <<= 7; lValue |= (nByte & 0x7f); if (nByte < 128) // MSB is 0: last byte { return lValue; } } throw new InvalidMidiDataException("not a MIDI file: unterminated variable-length quantity"); } /** TODO: */ public static int readUnsignedByte(DataInputStream dataInputStream, long[] alRemainingBytes) throws IOException { int nByte = dataInputStream.readUnsignedByte(); // already done in DataInputStream.readUnsignedByte(); // if (nByte < 0) // { // throw new EOFException(); // } alRemainingBytes[0]--; return nByte; } } /*** StandardMidiFileReader.java ***/