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