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