1 /*
   2  * Copyright (c) 1999, 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.media.sound;
  27 
  28 import java.io.DataInputStream;
  29 import java.io.DataOutputStream;
  30 import java.io.PipedInputStream;
  31 import java.io.PipedOutputStream;
  32 import java.io.ByteArrayOutputStream;
  33 import java.io.ByteArrayInputStream;
  34 import java.io.SequenceInputStream;
  35 import java.io.File;
  36 import java.io.FileInputStream;
  37 import java.io.InputStream;
  38 import java.io.IOException;
  39 import java.io.EOFException;
  40 import java.io.OutputStream;
  41 import java.io.RandomAccessFile;
  42 import java.io.BufferedInputStream;
  43 import java.net.URL;
  44 import java.net.MalformedURLException;
  45 
  46 import javax.sound.midi.MidiFileFormat;
  47 import javax.sound.midi.InvalidMidiDataException;
  48 import javax.sound.midi.MetaMessage;
  49 import javax.sound.midi.MidiEvent;
  50 import javax.sound.midi.MidiMessage;
  51 import javax.sound.midi.Sequence;
  52 import javax.sound.midi.ShortMessage;
  53 import javax.sound.midi.SysexMessage;
  54 import javax.sound.midi.Track;
  55 import javax.sound.midi.spi.MidiFileReader;
  56 
  57 
  58 
  59 /**
  60  * MIDI file reader.
  61  *
  62  * @author Kara Kytle
  63  * @author Jan Borgersen
  64  * @author Florian Bomers
  65  */
  66 
  67 public class StandardMidiFileReader extends MidiFileReader {
  68 
  69     private static final int MThd_MAGIC = 0x4d546864;  // 'MThd'
  70 
  71     private static final int MIDI_TYPE_0 = 0;
  72     private static final int MIDI_TYPE_1 = 1;
  73 
  74     private static final int bisBufferSize = 1024; // buffer size in buffered input streams
  75 
  76     /**
  77      * MIDI parser types
  78      */
  79     private static final int types[] = {
  80         MIDI_TYPE_0,
  81         MIDI_TYPE_1
  82     };
  83 
  84     public MidiFileFormat getMidiFileFormat(InputStream stream) throws InvalidMidiDataException, IOException {
  85         return getMidiFileFormatFromStream(stream, MidiFileFormat.UNKNOWN_LENGTH, null);
  86     }
  87 
  88     // $$fb 2002-04-17: part of fix for 4635286: MidiSystem.getMidiFileFormat() returns format having invalid length
  89     private MidiFileFormat getMidiFileFormatFromStream(InputStream stream, int fileLength, SMFParser smfParser) throws InvalidMidiDataException, IOException {
  90         int maxReadLength = 16;
  91         int duration = MidiFileFormat.UNKNOWN_LENGTH;
  92         DataInputStream dis;
  93 
  94         if (stream instanceof DataInputStream) {
  95             dis = (DataInputStream) stream;
  96         } else {
  97             dis = new DataInputStream(stream);
  98         }
  99         if (smfParser == null) {
 100             dis.mark(maxReadLength);
 101         } else {
 102             smfParser.stream = dis;
 103         }
 104 
 105         int type;
 106         int numtracks;
 107         float divisionType;
 108         int resolution;
 109 
 110         try {
 111             int magic = dis.readInt();
 112             if( !(magic == MThd_MAGIC) ) {
 113                 // not MIDI
 114                 throw new InvalidMidiDataException("not a valid MIDI file");
 115             }
 116 
 117             // read header length
 118             int bytesRemaining = dis.readInt() - 6;
 119             type = dis.readShort();
 120             numtracks = dis.readShort();
 121             int timing = dis.readShort();
 122 
 123             // decipher the timing code
 124             if (timing > 0) {
 125                 // tempo based timing.  value is ticks per beat.
 126                 divisionType = Sequence.PPQ;
 127                 resolution = timing;
 128             } else {
 129                 // SMPTE based timing.  first decipher the frame code.
 130                 int frameCode = -1 * (timing >> 8);
 131                 switch(frameCode) {
 132                 case 24:
 133                     divisionType = Sequence.SMPTE_24;
 134                     break;
 135                 case 25:
 136                     divisionType = Sequence.SMPTE_25;
 137                     break;
 138                 case 29:
 139                     divisionType = Sequence.SMPTE_30DROP;
 140                     break;
 141                 case 30:
 142                     divisionType = Sequence.SMPTE_30;
 143                     break;
 144                 default:
 145                     throw new InvalidMidiDataException("Unknown frame code: " + frameCode);
 146                 }
 147                 // now determine the timing resolution in ticks per frame.
 148                 resolution = timing & 0xFF;
 149             }
 150             if (smfParser != null) {
 151                 // remainder of this chunk
 152                 dis.skip(bytesRemaining);
 153                 smfParser.tracks = numtracks;
 154             }
 155         } finally {
 156             // if only reading the file format, reset the stream
 157             if (smfParser == null) {
 158                 dis.reset();
 159             }
 160         }
 161         MidiFileFormat format = new MidiFileFormat(type, divisionType, resolution, fileLength, duration);
 162         return format;
 163     }
 164 
 165 
 166     public MidiFileFormat getMidiFileFormat(URL url) throws InvalidMidiDataException, IOException {
 167         InputStream urlStream = url.openStream(); // throws IOException
 168         BufferedInputStream bis = new BufferedInputStream( urlStream, bisBufferSize );
 169         MidiFileFormat fileFormat = null;
 170         try {
 171             fileFormat = getMidiFileFormat( bis ); // throws InvalidMidiDataException
 172         } finally {
 173             bis.close();
 174         }
 175         return fileFormat;
 176     }
 177 
 178 
 179     public MidiFileFormat getMidiFileFormat(File file) throws InvalidMidiDataException, IOException {
 180         FileInputStream fis = new FileInputStream(file); // throws IOException
 181         BufferedInputStream bis = new BufferedInputStream(fis, bisBufferSize);
 182 
 183         // $$fb 2002-04-17: part of fix for 4635286: MidiSystem.getMidiFileFormat() returns format having invalid length
 184         long length = file.length();
 185         if (length > Integer.MAX_VALUE) {
 186             length = MidiFileFormat.UNKNOWN_LENGTH;
 187         }
 188         MidiFileFormat fileFormat = null;
 189         try {
 190             fileFormat = getMidiFileFormatFromStream(bis, (int) length, null);
 191         } finally {
 192             bis.close();
 193         }
 194         return fileFormat;
 195     }
 196 
 197 
 198     public Sequence getSequence(InputStream stream) throws InvalidMidiDataException, IOException {
 199         SMFParser smfParser = new SMFParser();
 200         MidiFileFormat format = getMidiFileFormatFromStream(stream,
 201                                                             MidiFileFormat.UNKNOWN_LENGTH,
 202                                                             smfParser);
 203 
 204         // must be MIDI Type 0 or Type 1
 205         if ((format.getType() != 0) && (format.getType() != 1)) {
 206             throw new InvalidMidiDataException("Invalid or unsupported file type: "  + format.getType());
 207         }
 208 
 209         // construct the sequence object
 210         Sequence sequence = new Sequence(format.getDivisionType(), format.getResolution());
 211 
 212         // for each track, go to the beginning and read the track events
 213         for (int i = 0; i < smfParser.tracks; i++) {
 214             if (smfParser.nextTrack()) {
 215                 smfParser.readTrack(sequence.createTrack());
 216             } else {
 217                 break;
 218             }
 219         }
 220         return sequence;
 221     }
 222 
 223 
 224 
 225     public Sequence getSequence(URL url) throws InvalidMidiDataException, IOException {
 226         InputStream is = url.openStream();  // throws IOException
 227         is = new BufferedInputStream(is, bisBufferSize);
 228         Sequence seq = null;
 229         try {
 230             seq = getSequence(is);
 231         } finally {
 232             is.close();
 233         }
 234         return seq;
 235     }
 236 
 237 
 238     public Sequence getSequence(File file) throws InvalidMidiDataException, IOException {
 239         InputStream is = new FileInputStream(file); // throws IOException
 240         is = new BufferedInputStream(is, bisBufferSize);
 241         Sequence seq = null;
 242         try {
 243             seq = getSequence(is);
 244         } finally {
 245             is.close();
 246         }
 247         return seq;
 248     }
 249 }
 250 
 251 //=============================================================================================================
 252 
 253 /**
 254  * State variables during parsing of a MIDI file
 255  */
 256 class SMFParser {
 257     private static final int MTrk_MAGIC = 0x4d54726b;  // 'MTrk'
 258 
 259     // set to true to not allow corrupt MIDI files tombe loaded
 260     private static final boolean STRICT_PARSER = false;
 261 
 262     private static final boolean DEBUG = false;
 263 
 264     int tracks;                       // number of tracks
 265     DataInputStream stream;   // the stream to read from
 266 
 267     private int trackLength = 0;  // remaining length in track
 268     private byte[] trackData = null;
 269     private int pos = 0;
 270 
 271     public SMFParser() {
 272     }
 273 
 274     private int readUnsigned() throws IOException {
 275         return trackData[pos++] & 0xFF;
 276     }
 277 
 278     private void read(byte[] data) throws IOException {
 279         System.arraycopy(trackData, pos, data, 0, data.length);
 280         pos += data.length;
 281     }
 282 
 283     private long readVarInt() throws IOException {
 284         long value = 0; // the variable-lengh int value
 285         int currentByte = 0;
 286         do {
 287             currentByte = trackData[pos++] & 0xFF;
 288             value = (value << 7) + (currentByte & 0x7F);
 289         } while ((currentByte & 0x80) != 0);
 290         return value;
 291     }
 292 
 293     private int readIntFromStream() throws IOException {
 294         try {
 295             return stream.readInt();
 296         } catch (EOFException eof) {
 297             throw new EOFException("invalid MIDI file");
 298         }
 299     }
 300 
 301     boolean nextTrack() throws IOException, InvalidMidiDataException {
 302         int magic;
 303         trackLength = 0;
 304         do {
 305             // $$fb 2003-08-20: fix for 4910986: MIDI file parser breaks up on http connection
 306             if (stream.skipBytes(trackLength) != trackLength) {
 307                 if (!STRICT_PARSER) {
 308                     return false;
 309                 }
 310                 throw new EOFException("invalid MIDI file");
 311             }
 312             magic = readIntFromStream();
 313             trackLength = readIntFromStream();
 314         } while (magic != MTrk_MAGIC);
 315         if (!STRICT_PARSER) {
 316             if (trackLength < 0) {
 317                 return false;
 318             }
 319         }
 320         // now read track in a byte array
 321         trackData = new byte[trackLength];
 322         try {
 323             // $$fb 2003-08-20: fix for 4910986: MIDI file parser breaks up on http connection
 324             stream.readFully(trackData);
 325         } catch (EOFException eof) {
 326             if (!STRICT_PARSER) {
 327                 return false;
 328             }
 329             throw new EOFException("invalid MIDI file");
 330         }
 331         pos = 0;
 332         return true;
 333     }
 334 
 335     private boolean trackFinished() {
 336         return pos >= trackLength;
 337     }
 338 
 339     void readTrack(Track track) throws IOException, InvalidMidiDataException {
 340         try {
 341             // reset current tick to 0
 342             long tick = 0;
 343 
 344             // reset current status byte to 0 (invalid value).
 345             // this should cause us to throw an InvalidMidiDataException if we don't
 346             // get a valid status byte from the beginning of the track.
 347             int status = 0;
 348             boolean endOfTrackFound = false;
 349 
 350             while (!trackFinished() && !endOfTrackFound) {
 351                 MidiMessage message;
 352 
 353                 int data1 = -1;         // initialize to invalid value
 354                 int data2 = 0;
 355 
 356                 // each event has a tick delay and then the event data.
 357 
 358                 // first read the delay (a variable-length int) and update our tick value
 359                 tick += readVarInt();
 360 
 361                 // check for new status
 362                 int byteValue = readUnsigned();
 363 
 364                 if (byteValue >= 0x80) {
 365                     status = byteValue;
 366                 } else {
 367                     data1 = byteValue;
 368                 }
 369 
 370                 switch (status & 0xF0) {
 371                 case 0x80:
 372                 case 0x90:
 373                 case 0xA0:
 374                 case 0xB0:
 375                 case 0xE0:
 376                     // two data bytes
 377                     if (data1 == -1) {
 378                         data1 = readUnsigned();
 379                     }
 380                     data2 = readUnsigned();
 381                     message = new FastShortMessage(status | (data1 << 8) | (data2 << 16));
 382                     break;
 383                 case 0xC0:
 384                 case 0xD0:
 385                     // one data byte
 386                     if (data1 == -1) {
 387                         data1 = readUnsigned();
 388                     }
 389                     message = new FastShortMessage(status | (data1 << 8));
 390                     break;
 391                 case 0xF0:
 392                     // sys-ex or meta
 393                     switch(status) {
 394                     case 0xF0:
 395                     case 0xF7:
 396                         // sys ex
 397                         int sysexLength = (int) readVarInt();
 398                         byte[] sysexData = new byte[sysexLength];
 399                         read(sysexData);
 400 
 401                         SysexMessage sysexMessage = new SysexMessage();
 402                         sysexMessage.setMessage(status, sysexData, sysexLength);
 403                         message = sysexMessage;
 404                         break;
 405 
 406                     case 0xFF:
 407                         // meta
 408                         int metaType = readUnsigned();
 409                         int metaLength = (int) readVarInt();
 410 
 411                         byte[] metaData = new byte[metaLength];
 412                         read(metaData);
 413 
 414                         MetaMessage metaMessage = new MetaMessage();
 415                         metaMessage.setMessage(metaType, metaData, metaLength);
 416                         message = metaMessage;
 417                         if (metaType == 0x2F) {
 418                             // end of track means it!
 419                             endOfTrackFound = true;
 420                         }
 421                         break;
 422                     default:
 423                         throw new InvalidMidiDataException("Invalid status byte: " + status);
 424                     } // switch sys-ex or meta
 425                     break;
 426                 default:
 427                     throw new InvalidMidiDataException("Invalid status byte: " + status);
 428                 } // switch
 429                 track.add(new MidiEvent(message, tick));
 430             } // while
 431         } catch (ArrayIndexOutOfBoundsException e) {
 432             if (DEBUG) e.printStackTrace();
 433             // fix for 4834374
 434             throw new EOFException("invalid MIDI file");
 435         }
 436     }
 437 
 438 }