blob: e41c4482f1ea1585cfd264817a4f513b6838e682 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1999-2007 Sun Microsystems, Inc. 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. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package com.sun.media.sound;
27
28import java.io.DataInputStream;
29import java.io.DataOutputStream;
30import java.io.PipedInputStream;
31import java.io.PipedOutputStream;
32import java.io.ByteArrayOutputStream;
33import java.io.ByteArrayInputStream;
34import java.io.SequenceInputStream;
35import java.io.File;
36import java.io.FileInputStream;
37import java.io.InputStream;
38import java.io.IOException;
39import java.io.EOFException;
40import java.io.OutputStream;
41import java.io.RandomAccessFile;
42import java.io.BufferedInputStream;
43import java.net.URL;
44import java.net.MalformedURLException;
45
46import javax.sound.midi.MidiFileFormat;
47import javax.sound.midi.InvalidMidiDataException;
48import javax.sound.midi.MetaMessage;
49import javax.sound.midi.MidiEvent;
50import javax.sound.midi.MidiMessage;
51import javax.sound.midi.Sequence;
52import javax.sound.midi.ShortMessage;
53import javax.sound.midi.SysexMessage;
54import javax.sound.midi.Track;
55import 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
67public 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 */
256class 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}