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