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 }