1/*
2 * Copyright (c) 1999, 2015, 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.ByteArrayInputStream;
29import java.io.ByteArrayOutputStream;
30import java.io.DataOutputStream;
31import java.io.File;
32import java.io.FileOutputStream;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.OutputStream;
36import java.io.PipedInputStream;
37import java.io.PipedOutputStream;
38import java.io.SequenceInputStream;
39import java.util.Objects;
40
41import javax.sound.midi.InvalidMidiDataException;
42import javax.sound.midi.MetaMessage;
43import javax.sound.midi.MidiEvent;
44import javax.sound.midi.Sequence;
45import javax.sound.midi.ShortMessage;
46import javax.sound.midi.SysexMessage;
47import javax.sound.midi.Track;
48import javax.sound.midi.spi.MidiFileWriter;
49
50/**
51 * MIDI file writer.
52 *
53 * @author Kara Kytle
54 * @author Jan Borgersen
55 */
56public final class StandardMidiFileWriter extends MidiFileWriter {
57
58    private static final int MThd_MAGIC = 0x4d546864;  // 'MThd'
59    private static final int MTrk_MAGIC = 0x4d54726b;  // 'MTrk'
60
61    private static final int ONE_BYTE   = 1;
62    private static final int TWO_BYTE   = 2;
63    private static final int SYSEX      = 3;
64    private static final int META       = 4;
65    private static final int ERROR      = 5;
66    private static final int IGNORE     = 6;
67
68    private static final int MIDI_TYPE_0 = 0;
69    private static final int MIDI_TYPE_1 = 1;
70
71    private static final int bufferSize = 16384;  // buffersize for write
72    private DataOutputStream tddos;               // data output stream for track writing
73
74    /**
75     * MIDI parser types.
76     */
77    private static final int types[] = {
78        MIDI_TYPE_0,
79        MIDI_TYPE_1
80    };
81
82    @Override
83    public int[] getMidiFileTypes() {
84        int[] localArray = new int[types.length];
85        System.arraycopy(types, 0, localArray, 0, types.length);
86        return localArray;
87    }
88
89    /**
90     * Obtains the file types that this provider can write from the
91     * sequence specified.
92     * @param sequence the sequence for which midi file type support
93     * is queried
94     * @return array of file types.  If no file types are supported,
95     * returns an array of length 0.
96     */
97    @Override
98    public int[] getMidiFileTypes(Sequence sequence){
99        int typesArray[];
100        Track tracks[] = sequence.getTracks();
101
102        if( tracks.length==1 ) {
103            typesArray = new int[2];
104            typesArray[0] = MIDI_TYPE_0;
105            typesArray[1] = MIDI_TYPE_1;
106        } else {
107            typesArray = new int[1];
108            typesArray[0] = MIDI_TYPE_1;
109        }
110
111        return typesArray;
112    }
113
114    @Override
115    public int write(Sequence in, int type, OutputStream out) throws IOException {
116        Objects.requireNonNull(out);
117        if (!isFileTypeSupported(type, in)) {
118            throw new IllegalArgumentException("Could not write MIDI file");
119        }
120        byte [] buffer = null;
121
122        int bytesRead = 0;
123        long bytesWritten = 0;
124
125        // First get the fileStream from this sequence
126        InputStream fileStream = getFileStream(type,in);
127        if (fileStream == null) {
128            throw new IllegalArgumentException("Could not write MIDI file");
129        }
130        buffer = new byte[bufferSize];
131
132        while( (bytesRead = fileStream.read( buffer )) >= 0 ) {
133            out.write( buffer, 0, bytesRead );
134            bytesWritten += bytesRead;
135        }
136        // Done....return bytesWritten
137        return (int) bytesWritten;
138    }
139
140    @Override
141    public int write(Sequence in, int type, File out) throws IOException {
142        Objects.requireNonNull(in);
143        FileOutputStream fos = new FileOutputStream(out); // throws IOException
144        int bytesWritten = write( in, type, fos );
145        fos.close();
146        return bytesWritten;
147    }
148
149    //=================================================================================
150
151    private InputStream getFileStream(int type, Sequence sequence) throws IOException {
152        Track tracks[] = sequence.getTracks();
153        int bytesBuilt = 0;
154        int headerLength = 14;
155        int length = 0;
156        int timeFormat;
157        float divtype;
158
159        PipedOutputStream   hpos = null;
160        DataOutputStream    hdos = null;
161        PipedInputStream    headerStream = null;
162
163        InputStream         trackStreams [] = null;
164        InputStream         trackStream = null;
165        InputStream fStream = null;
166
167        // Determine the filetype to write
168        if( type==MIDI_TYPE_0 ) {
169            if (tracks.length != 1) {
170                return null;
171            }
172        } else if( type==MIDI_TYPE_1 ) {
173            if (tracks.length < 1) { // $$jb: 05.31.99: we _can_ write TYPE_1 if tracks.length==1
174                return null;
175            }
176        } else {
177            if(tracks.length==1) {
178                type = MIDI_TYPE_0;
179            } else if(tracks.length>1) {
180                type = MIDI_TYPE_1;
181            } else {
182                return null;
183            }
184        }
185
186        // Now build the file one track at a time
187        // Note that above we made sure that MIDI_TYPE_0 only happens
188        // if tracks.length==1
189
190        trackStreams = new InputStream[tracks.length];
191        int trackCount = 0;
192        for(int i=0; i<tracks.length; i++) {
193            try {
194                trackStreams[trackCount] = writeTrack( tracks[i], type );
195                trackCount++;
196            } catch (InvalidMidiDataException e) {
197                if(Printer.err) Printer.err("Exception in write: " + e.getMessage());
198            }
199            //bytesBuilt += trackStreams[i].getLength();
200        }
201
202        // Now seqence the track streams
203        if( trackCount == 1 ) {
204            trackStream = trackStreams[0];
205        } else if( trackCount > 1 ){
206            trackStream = trackStreams[0];
207            for(int i=1; i<tracks.length; i++) {
208                // fix for 5048381: NullPointerException when saving a MIDI sequence
209                // don't include failed track streams
210                if (trackStreams[i] != null) {
211                    trackStream = new SequenceInputStream( trackStream, trackStreams[i]);
212                }
213            }
214        } else {
215            throw new IllegalArgumentException("invalid MIDI data in sequence");
216        }
217
218        // Now build the header...
219        hpos = new PipedOutputStream();
220        hdos = new DataOutputStream(hpos);
221        headerStream = new PipedInputStream(hpos);
222
223        // Write the magic number
224        hdos.writeInt( MThd_MAGIC );
225
226        // Write the header length
227        hdos.writeInt( headerLength - 8 );
228
229        // Write the filetype
230        if(type==MIDI_TYPE_0) {
231            hdos.writeShort( 0 );
232        } else {
233            // MIDI_TYPE_1
234            hdos.writeShort( 1 );
235        }
236
237        // Write the number of tracks
238        hdos.writeShort( (short) trackCount );
239
240        // Determine and write the timing format
241        divtype = sequence.getDivisionType();
242        if( divtype == Sequence.PPQ ) {
243            timeFormat = sequence.getResolution();
244        } else if( divtype == Sequence.SMPTE_24) {
245            timeFormat = (24<<8) * -1;
246            timeFormat += (sequence.getResolution() & 0xFF);
247        } else if( divtype == Sequence.SMPTE_25) {
248            timeFormat = (25<<8) * -1;
249            timeFormat += (sequence.getResolution() & 0xFF);
250        } else if( divtype == Sequence.SMPTE_30DROP) {
251            timeFormat = (29<<8) * -1;
252            timeFormat += (sequence.getResolution() & 0xFF);
253        } else if( divtype == Sequence.SMPTE_30) {
254            timeFormat = (30<<8) * -1;
255            timeFormat += (sequence.getResolution() & 0xFF);
256        } else {
257            // $$jb: 04.08.99: What to really do here?
258            return null;
259        }
260        hdos.writeShort( timeFormat );
261
262        // now construct an InputStream to become the FileStream
263        fStream = new SequenceInputStream(headerStream, trackStream);
264        hdos.close();
265
266        length = bytesBuilt + headerLength;
267        return fStream;
268    }
269
270    /**
271     * Returns ONE_BYTE, TWO_BYTE, SYSEX, META,
272     * ERROR, or IGNORE (i.e. invalid for a MIDI file)
273     */
274    private int getType(int byteValue) {
275        if ((byteValue & 0xF0) == 0xF0) {
276            switch(byteValue) {
277            case 0xF0:
278            case 0xF7:
279                return SYSEX;
280            case 0xFF:
281                return META;
282            }
283            return IGNORE;
284        }
285
286        switch(byteValue & 0xF0) {
287        case 0x80:
288        case 0x90:
289        case 0xA0:
290        case 0xB0:
291        case 0xE0:
292            return TWO_BYTE;
293        case 0xC0:
294        case 0xD0:
295            return ONE_BYTE;
296        }
297        return ERROR;
298    }
299
300    private static final long mask = 0x7F;
301
302    private int writeVarInt(long value) throws IOException {
303        int len = 1;
304        int shift=63; // number of bitwise left-shifts of mask
305        // first screen out leading zeros
306        while ((shift > 0) && ((value & (mask << shift)) == 0)) shift-=7;
307        // then write actual values
308        while (shift > 0) {
309            tddos.writeByte((int) (((value & (mask << shift)) >> shift) | 0x80));
310            shift-=7;
311            len++;
312        }
313        tddos.writeByte((int) (value & mask));
314        return len;
315    }
316
317    private InputStream writeTrack( Track track, int type ) throws IOException, InvalidMidiDataException {
318        int bytesWritten = 0;
319        int lastBytesWritten = 0;
320        int size = track.size();
321        PipedOutputStream thpos = new PipedOutputStream();
322        DataOutputStream  thdos = new DataOutputStream(thpos);
323        PipedInputStream  thpis = new PipedInputStream(thpos);
324
325        ByteArrayOutputStream tdbos = new ByteArrayOutputStream();
326        tddos = new DataOutputStream(tdbos);
327        ByteArrayInputStream tdbis = null;
328
329        SequenceInputStream  fStream = null;
330
331        long currentTick = 0;
332        long deltaTick = 0;
333        long eventTick = 0;
334        int runningStatus = -1;
335
336        // -----------------------------
337        // Write each event in the track
338        // -----------------------------
339        for(int i=0; i<size; i++) {
340            MidiEvent event = track.get(i);
341
342            int status;
343            int eventtype;
344            int metatype;
345            int data1, data2;
346            int length;
347            byte data[] = null;
348            ShortMessage shortMessage = null;
349            MetaMessage  metaMessage  = null;
350            SysexMessage sysexMessage = null;
351
352            // get the tick
353            // $$jb: this gets easier if we change all system-wide time to delta ticks
354            eventTick = event.getTick();
355            deltaTick = event.getTick() - currentTick;
356            currentTick = event.getTick();
357
358            // get the status byte
359            status = event.getMessage().getStatus();
360            eventtype = getType( status );
361
362            switch( eventtype ) {
363            case ONE_BYTE:
364                shortMessage = (ShortMessage) event.getMessage();
365                data1 = shortMessage.getData1();
366                bytesWritten += writeVarInt( deltaTick );
367
368                if(status!=runningStatus) {
369                    runningStatus=status;
370                    tddos.writeByte(status);  bytesWritten += 1;
371                }
372                tddos.writeByte(data1);   bytesWritten += 1;
373                break;
374
375            case TWO_BYTE:
376                shortMessage = (ShortMessage) event.getMessage();
377                data1 = shortMessage.getData1();
378                data2 = shortMessage.getData2();
379
380                bytesWritten += writeVarInt( deltaTick );
381                if(status!=runningStatus) {
382                    runningStatus=status;
383                    tddos.writeByte(status);  bytesWritten += 1;
384                }
385                tddos.writeByte(data1);   bytesWritten += 1;
386                tddos.writeByte(data2);   bytesWritten += 1;
387                break;
388
389            case SYSEX:
390                sysexMessage = (SysexMessage) event.getMessage();
391                length     = sysexMessage.getLength();
392                data       = sysexMessage.getMessage();
393                bytesWritten += writeVarInt( deltaTick );
394
395                // $$jb: 04.08.99: always write status for sysex
396                runningStatus=status;
397                tddos.writeByte( data[0] ); bytesWritten += 1;
398
399                // $$jb: 10.18.99: we don't maintain length in
400                // the message data for SysEx (it is not transmitted
401                // over the line), so write the calculated length
402                // minus the status byte
403                bytesWritten += writeVarInt( (data.length-1) );
404
405                // $$jb: 10.18.99: now write the rest of the
406                // message
407                tddos.write(data, 1, (data.length-1));
408                bytesWritten += (data.length-1);
409                break;
410
411            case META:
412                metaMessage = (MetaMessage) event.getMessage();
413                length    = metaMessage.getLength();
414                data      = metaMessage.getMessage();
415                bytesWritten += writeVarInt( deltaTick );
416
417                // $$jb: 10.18.99: getMessage() returns the
418                // entire valid midi message for a file,
419                // including the status byte and the var-length-int
420                // length value, so we can just write the data
421                // here.  note that we must _always_ write the
422                // status byte, regardless of runningStatus.
423                runningStatus=status;
424                tddos.write( data, 0, data.length );
425                bytesWritten += data.length;
426                break;
427
428            case IGNORE:
429                // ignore this event
430                break;
431
432            case ERROR:
433                // ignore this event
434                break;
435
436            default:
437                throw new InvalidMidiDataException("internal file writer error");
438            }
439        }
440        // ---------------------------------
441        // End write each event in the track
442        // ---------------------------------
443
444        // Build Track header now that we know length
445        thdos.writeInt(MTrk_MAGIC);
446        thdos.writeInt(bytesWritten);
447        bytesWritten += 8;
448
449        // Now sequence them
450        tdbis = new ByteArrayInputStream( tdbos.toByteArray() );
451        fStream = new SequenceInputStream(thpis,tdbis);
452        thdos.close();
453        tddos.close();
454
455        return fStream;
456    }
457}
458