jdk/src/share/classes/com/sun/media/sound/StandardMidiFileWriter.java
changeset 2 90ce3da70b43
child 3451 22d903fd3c5e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/com/sun/media/sound/StandardMidiFileWriter.java	Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,473 @@
+/*
+ * Copyright 1999-2007 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package com.sun.media.sound;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.SequenceInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.BufferedOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.IllegalArgumentException;
+import java.io.OutputStream;
+import java.util.Vector;
+
+import javax.sound.midi.MidiFileFormat;
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MidiEvent;
+import javax.sound.midi.MetaMessage;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.ShortMessage;
+import javax.sound.midi.SysexMessage;
+import javax.sound.midi.Track;
+import javax.sound.midi.spi.MidiFileWriter;
+
+
+/**
+ * MIDI file writer.
+ *
+ * @author Kara Kytle
+ * @author Jan Borgersen
+ */
+public class StandardMidiFileWriter extends MidiFileWriter {
+
+    private static final int MThd_MAGIC = 0x4d546864;  // 'MThd'
+    private static final int MTrk_MAGIC = 0x4d54726b;  // 'MTrk'
+
+    private static final int ONE_BYTE   = 1;
+    private static final int TWO_BYTE   = 2;
+    private static final int SYSEX      = 3;
+    private static final int META       = 4;
+    private static final int ERROR      = 5;
+    private static final int IGNORE     = 6;
+
+    private static final int MIDI_TYPE_0 = 0;
+    private static final int MIDI_TYPE_1 = 1;
+
+    private static final int bufferSize = 16384;  // buffersize for write
+    private DataOutputStream tddos;               // data output stream for track writing
+
+
+
+    /**
+     * MIDI parser types
+     */
+    public static final int types[] = {
+        MIDI_TYPE_0,
+        MIDI_TYPE_1
+    };
+
+
+    /**
+     * new
+     */
+    public int[] getMidiFileTypes() {
+        int[] localArray = new int[types.length];
+        System.arraycopy(types, 0, localArray, 0, types.length);
+        return localArray;
+    }
+
+    /**
+     * Obtains the file types that this provider can write from the
+     * sequence specified.
+     * @param sequence the sequence for which midi file type support
+     * is queried
+     * @return array of file types.  If no file types are supported,
+     * returns an array of length 0.
+     */
+    public int[] getMidiFileTypes(Sequence sequence){
+        int typesArray[];
+        Track tracks[] = sequence.getTracks();
+
+        if( tracks.length==1 ) {
+            typesArray = new int[2];
+            typesArray[0] = MIDI_TYPE_0;
+            typesArray[1] = MIDI_TYPE_1;
+        } else {
+            typesArray = new int[1];
+            typesArray[0] = MIDI_TYPE_1;
+        }
+
+        return typesArray;
+    }
+
+    public boolean isFileTypeSupported(int type) {
+        for(int i=0; i<types.length; i++) {
+            if( type == types[i] ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public int write(Sequence in, int type, OutputStream out) throws IOException {
+        byte [] buffer = null;
+
+        int bytesRead = 0;
+        long bytesWritten = 0;
+
+        if( !isFileTypeSupported(type,in) ) {
+            throw new IllegalArgumentException("Could not write MIDI file");
+        }
+        // First get the fileStream from this sequence
+        InputStream fileStream = getFileStream(type,in);
+        if (fileStream == null) {
+            throw new IllegalArgumentException("Could not write MIDI file");
+        }
+        buffer = new byte[bufferSize];
+
+        while( (bytesRead = fileStream.read( buffer )) >= 0 ) {
+            out.write( buffer, 0, (int)bytesRead );
+            bytesWritten += bytesRead;
+        }
+        // Done....return bytesWritten
+        return (int) bytesWritten;
+    }
+
+    public int write(Sequence in, int type, File out) throws IOException {
+        FileOutputStream fos = new FileOutputStream(out); // throws IOException
+        int bytesWritten = write( in, type, fos );
+        fos.close();
+        return bytesWritten;
+    }
+
+    //=================================================================================
+
+
+    private InputStream getFileStream(int type, Sequence sequence) throws IOException {
+        Track tracks[] = sequence.getTracks();
+        int bytesBuilt = 0;
+        int headerLength = 14;
+        int length = 0;
+        int timeFormat;
+        float divtype;
+
+        PipedOutputStream   hpos = null;
+        DataOutputStream    hdos = null;
+        PipedInputStream    headerStream = null;
+
+        InputStream         trackStreams [] = null;
+        InputStream         trackStream = null;
+        InputStream fStream = null;
+
+        // Determine the filetype to write
+        if( type==MIDI_TYPE_0 ) {
+            if (tracks.length != 1) {
+                return null;
+            }
+        } else if( type==MIDI_TYPE_1 ) {
+            if (tracks.length < 1) { // $$jb: 05.31.99: we _can_ write TYPE_1 if tracks.length==1
+                return null;
+            }
+        } else {
+            if(tracks.length==1) {
+                type = MIDI_TYPE_0;
+            } else if(tracks.length>1) {
+                type = MIDI_TYPE_1;
+            } else {
+                return null;
+            }
+        }
+
+        // Now build the file one track at a time
+        // Note that above we made sure that MIDI_TYPE_0 only happens
+        // if tracks.length==1
+
+        trackStreams = new InputStream[tracks.length];
+        int trackCount = 0;
+        for(int i=0; i<tracks.length; i++) {
+            try {
+                trackStreams[trackCount] = writeTrack( tracks[i], type );
+                trackCount++;
+            } catch (InvalidMidiDataException e) {
+                if(Printer.err) Printer.err("Exception in write: " + e.getMessage());
+            }
+            //bytesBuilt += trackStreams[i].getLength();
+        }
+
+        // Now seqence the track streams
+        if( trackCount == 1 ) {
+            trackStream = trackStreams[0];
+        } else if( trackCount > 1 ){
+            trackStream = trackStreams[0];
+            for(int i=1; i<tracks.length; i++) {
+                // fix for 5048381: NullPointerException when saving a MIDI sequence
+                // don't include failed track streams
+                if (trackStreams[i] != null) {
+                    trackStream = new SequenceInputStream( trackStream, trackStreams[i]);
+                }
+            }
+        } else {
+            throw new IllegalArgumentException("invalid MIDI data in sequence");
+        }
+
+        // Now build the header...
+        hpos = new PipedOutputStream();
+        hdos = new DataOutputStream(hpos);
+        headerStream = new PipedInputStream(hpos);
+
+        // Write the magic number
+        hdos.writeInt( MThd_MAGIC );
+
+        // Write the header length
+        hdos.writeInt( headerLength - 8 );
+
+        // Write the filetype
+        if(type==MIDI_TYPE_0) {
+            hdos.writeShort( 0 );
+        } else {
+            // MIDI_TYPE_1
+            hdos.writeShort( 1 );
+        }
+
+        // Write the number of tracks
+        hdos.writeShort( (short) trackCount );
+
+        // Determine and write the timing format
+        divtype = sequence.getDivisionType();
+        if( divtype == Sequence.PPQ ) {
+            timeFormat = sequence.getResolution();
+        } else if( divtype == Sequence.SMPTE_24) {
+            timeFormat = (24<<8) * -1;
+            timeFormat += (sequence.getResolution() & 0xFF);
+        } else if( divtype == Sequence.SMPTE_25) {
+            timeFormat = (25<<8) * -1;
+            timeFormat += (sequence.getResolution() & 0xFF);
+        } else if( divtype == Sequence.SMPTE_30DROP) {
+            timeFormat = (29<<8) * -1;
+            timeFormat += (sequence.getResolution() & 0xFF);
+        } else if( divtype == Sequence.SMPTE_30) {
+            timeFormat = (30<<8) * -1;
+            timeFormat += (sequence.getResolution() & 0xFF);
+        } else {
+            // $$jb: 04.08.99: What to really do here?
+            return null;
+        }
+        hdos.writeShort( timeFormat );
+
+        // now construct an InputStream to become the FileStream
+        fStream = new SequenceInputStream(headerStream, trackStream);
+        hdos.close();
+
+        length = bytesBuilt + headerLength;
+        return fStream;
+    }
+
+    /**
+     * Returns ONE_BYTE, TWO_BYTE, SYSEX, META,
+     * ERROR, or IGNORE (i.e. invalid for a MIDI file)
+     */
+    private int getType(int byteValue) {
+        if ((byteValue & 0xF0) == 0xF0) {
+            switch(byteValue) {
+            case 0xF0:
+            case 0xF7:
+                return SYSEX;
+            case 0xFF:
+                return META;
+            }
+            return IGNORE;
+        }
+
+        switch(byteValue & 0xF0) {
+        case 0x80:
+        case 0x90:
+        case 0xA0:
+        case 0xB0:
+        case 0xE0:
+            return TWO_BYTE;
+        case 0xC0:
+        case 0xD0:
+            return ONE_BYTE;
+        }
+        return ERROR;
+    }
+
+    private final static long mask = 0x7F;
+
+    private int writeVarInt(long value) throws IOException {
+        int len = 1;
+        int shift=63; // number of bitwise left-shifts of mask
+        // first screen out leading zeros
+        while ((shift > 0) && ((value & (mask << shift)) == 0)) shift-=7;
+        // then write actual values
+        while (shift > 0) {
+            tddos.writeByte((int) (((value & (mask << shift)) >> shift) | 0x80));
+            shift-=7;
+            len++;
+        }
+        tddos.writeByte((int) (value & mask));
+        return len;
+    }
+
+    private InputStream writeTrack( Track track, int type ) throws IOException, InvalidMidiDataException {
+        int bytesWritten = 0;
+        int lastBytesWritten = 0;
+        int size = track.size();
+        PipedOutputStream thpos = new PipedOutputStream();
+        DataOutputStream  thdos = new DataOutputStream(thpos);
+        PipedInputStream  thpis = new PipedInputStream(thpos);
+
+        ByteArrayOutputStream tdbos = new ByteArrayOutputStream();
+        tddos = new DataOutputStream(tdbos);
+        ByteArrayInputStream tdbis = null;
+
+        SequenceInputStream  fStream = null;
+
+        long currentTick = 0;
+        long deltaTick = 0;
+        long eventTick = 0;
+        int runningStatus = -1;
+
+        // -----------------------------
+        // Write each event in the track
+        // -----------------------------
+        for(int i=0; i<size; i++) {
+            MidiEvent event = track.get(i);
+
+            int status;
+            int eventtype;
+            int metatype;
+            int data1, data2;
+            int length;
+            byte data[] = null;
+            ShortMessage shortMessage = null;
+            MetaMessage  metaMessage  = null;
+            SysexMessage sysexMessage = null;
+
+            // get the tick
+            // $$jb: this gets easier if we change all system-wide time to delta ticks
+            eventTick = event.getTick();
+            deltaTick = event.getTick() - currentTick;
+            currentTick = event.getTick();
+
+            // get the status byte
+            status = event.getMessage().getStatus();
+            eventtype = getType( status );
+
+            switch( eventtype ) {
+            case ONE_BYTE:
+                shortMessage = (ShortMessage) event.getMessage();
+                data1 = shortMessage.getData1();
+                bytesWritten += writeVarInt( deltaTick );
+
+                if(status!=runningStatus) {
+                    runningStatus=status;
+                    tddos.writeByte(status);  bytesWritten += 1;
+                }
+                tddos.writeByte(data1);   bytesWritten += 1;
+                break;
+
+            case TWO_BYTE:
+                shortMessage = (ShortMessage) event.getMessage();
+                data1 = shortMessage.getData1();
+                data2 = shortMessage.getData2();
+
+                bytesWritten += writeVarInt( deltaTick );
+                if(status!=runningStatus) {
+                    runningStatus=status;
+                    tddos.writeByte(status);  bytesWritten += 1;
+                }
+                tddos.writeByte(data1);   bytesWritten += 1;
+                tddos.writeByte(data2);   bytesWritten += 1;
+                break;
+
+            case SYSEX:
+                sysexMessage = (SysexMessage) event.getMessage();
+                length     = sysexMessage.getLength();
+                data       = sysexMessage.getMessage();
+                bytesWritten += writeVarInt( deltaTick );
+
+                // $$jb: 04.08.99: always write status for sysex
+                runningStatus=status;
+                tddos.writeByte( data[0] ); bytesWritten += 1;
+
+                // $$jb: 10.18.99: we don't maintain length in
+                // the message data for SysEx (it is not transmitted
+                // over the line), so write the calculated length
+                // minus the status byte
+                bytesWritten += writeVarInt( (data.length-1) );
+
+                // $$jb: 10.18.99: now write the rest of the
+                // message
+                tddos.write(data, 1, (data.length-1));
+                bytesWritten += (data.length-1);
+                break;
+
+            case META:
+                metaMessage = (MetaMessage) event.getMessage();
+                length    = metaMessage.getLength();
+                data      = metaMessage.getMessage();
+                bytesWritten += writeVarInt( deltaTick );
+
+                // $$jb: 10.18.99: getMessage() returns the
+                // entire valid midi message for a file,
+                // including the status byte and the var-length-int
+                // length value, so we can just write the data
+                // here.  note that we must _always_ write the
+                // status byte, regardless of runningStatus.
+                runningStatus=status;
+                tddos.write( data, 0, data.length );
+                bytesWritten += data.length;
+                break;
+
+            case IGNORE:
+                // ignore this event
+                break;
+
+            case ERROR:
+                // ignore this event
+                break;
+
+            default:
+                throw new InvalidMidiDataException("internal file writer error");
+            }
+        }
+        // ---------------------------------
+        // End write each event in the track
+        // ---------------------------------
+
+        // Build Track header now that we know length
+        thdos.writeInt(MTrk_MAGIC);
+        thdos.writeInt(bytesWritten);
+        bytesWritten += 8;
+
+        // Now sequence them
+        tdbis = new ByteArrayInputStream( tdbos.toByteArray() );
+        fStream = new SequenceInputStream(thpis,tdbis);
+        thdos.close();
+        tddos.close();
+
+        return fStream;
+    }
+}