1 /*
2  * Copyright (c) 1999, 2018, 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.ByteArrayInputStream;
29 import java.io.ByteArrayOutputStream;
30 import java.io.DataOutputStream;
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.OutputStream;
36 import java.io.PipedInputStream;
37 import java.io.PipedOutputStream;
38 import java.io.SequenceInputStream;
39 import java.util.Objects;
40 
41 import javax.sound.midi.InvalidMidiDataException;
42 import javax.sound.midi.MetaMessage;
43 import javax.sound.midi.MidiEvent;
44 import javax.sound.midi.Sequence;
45 import javax.sound.midi.ShortMessage;
46 import javax.sound.midi.SysexMessage;
47 import javax.sound.midi.Track;
48 import javax.sound.midi.spi.MidiFileWriter;
49 
50 /**
51  * MIDI file writer.
52  *
53  * @author Kara Kytle
54  * @author Jan Borgersen
55  */
56 public 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
getMidiFileTypes()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
getMidiFileTypes(Sequence sequence)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
write(Sequence in, int type, OutputStream out)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
write(Sequence in, int type, File out)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 
getFileStream(int type, Sequence sequence)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      */
getType(int byteValue)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 
writeVarInt(long value)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 
writeTrack( Track track, int type )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