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