//////////////////////////////////////////////////////////////////////////////
// Copyright 2011 Alex Leffelman
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//////////////////////////////////////////////////////////////////////////////
package com.leff.midi;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.TreeSet;
import com.leff.midi.event.MidiEvent;
import com.leff.midi.event.NoteOn;
import com.leff.midi.event.meta.EndOfTrack;
import com.leff.midi.event.meta.Tempo;
import com.leff.midi.event.meta.TimeSignature;
import com.leff.midi.util.MidiUtil;
import com.leff.midi.util.VariableLengthInt;
public class MidiTrack
{
private static final boolean VERBOSE = false;
public static final byte[] IDENTIFIER = { 'M', 'T', 'r', 'k' };
private int mSize;
private boolean mSizeNeedsRecalculating;
private boolean mClosed;
private long mEndOfTrackDelta;
private TreeSet<MidiEvent> mEvents;
public static MidiTrack createTempoTrack()
{
MidiTrack T = new MidiTrack();
T.insertEvent(new TimeSignature());
T.insertEvent(new Tempo());
return T;
}
public MidiTrack()
{
mEvents = new TreeSet<MidiEvent>();
mSize = 0;
mSizeNeedsRecalculating = false;
mClosed = false;
mEndOfTrackDelta = 0;
}
public MidiTrack(InputStream in) throws IOException
{
this();
byte[] buffer = new byte[4];
in.read(buffer);
if(!MidiUtil.bytesEqual(buffer, IDENTIFIER, 0, 4))
{
System.err.println("Track identifier did not match MTrk!");
return;
}
in.read(buffer);
mSize = MidiUtil.bytesToInt(buffer, 0, 4);
buffer = new byte[mSize];
in.read(buffer);
this.readTrackData(buffer);
}
private void readTrackData(byte[] data) throws IOException
{
InputStream in = new ByteArrayInputStream(data);
long totalTicks = 0;
while(in.available() > 0)
{
VariableLengthInt delta = new VariableLengthInt(in);
totalTicks += delta.getValue();
MidiEvent E = MidiEvent.parseEvent(totalTicks, delta.getValue(), in);
if(E == null)
{
System.out.println("Event skipped!");
continue;
}
if(VERBOSE)
{
System.out.println(E);
}
// Not adding the EndOfTrack event here allows the track to be
// edited
// after being read in from file.
if(E.getClass().equals(EndOfTrack.class))
{
mEndOfTrackDelta = E.getDelta();
break;
}
mEvents.add(E);
}
}
public TreeSet<MidiEvent> getEvents()
{
return mEvents;
}
public int getEventCount()
{
return mEvents.size();
}
public int getSize()
{
if(mSizeNeedsRecalculating)
{
recalculateSize();
}
return mSize;
}
public long getLengthInTicks()
{
if(mEvents.size() == 0)
{
return 0;
}
MidiEvent E = mEvents.last();
return E.getTick();
}
public long getEndOfTrackDelta()
{
return mEndOfTrackDelta;
}
public void setEndOfTrackDelta(long delta)
{
mEndOfTrackDelta = delta;
}
public void insertNote(int channel, int pitch, int velocity, long tick, long duration)
{
insertEvent(new NoteOn(tick, channel, pitch, velocity));
insertEvent(new NoteOn(tick + duration, channel, pitch, 0));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public void insertEvent(MidiEvent newEvent)
{
if(newEvent == null)
{
return;
}
if(mClosed)
{
System.err.println("Error: Cannot add an event to a closed track.");
return;
}
MidiEvent prev = null, next = null;
// floor() and ceiling() are not supported on Android before API Level 9
// (Gingerbread)
try
{
Class treeSet = Class.forName("java.util.TreeSet");
Method floor = treeSet.getMethod("floor", Object.class);
Method ceiling = treeSet.getMethod("ceiling", Object.class);
prev = (MidiEvent) floor.invoke(mEvents, newEvent);
next = (MidiEvent) ceiling.invoke(mEvents, newEvent);
}
catch(Exception e)
{
// methods are not supported - must perform linear search
Iterator<MidiEvent> it = mEvents.iterator();
while(it.hasNext())
{
next = it.next();
if(next.getTick() > newEvent.getTick())
{
break;
}
prev = next;
next = null;
}
}
mEvents.add(newEvent);
mSizeNeedsRecalculating = true;
// Set its delta time based on the previous event (or itself if no
// previous event exists)
if(prev != null)
{
newEvent.setDelta(newEvent.getTick() - prev.getTick());
}
else
{
newEvent.setDelta(newEvent.getTick());
}
// Update the next event's delta time relative to the new event.
if(next != null)
{
next.setDelta(next.getTick() - newEvent.getTick());
}
mSize += newEvent.getSize();
if(newEvent.getClass().equals(EndOfTrack.class))
{
if(next != null)
{
throw new IllegalArgumentException("Attempting to insert EndOfTrack before an existing event. Use closeTrack() when finished with MidiTrack.");
}
mClosed = true;
}
}
public boolean removeEvent(MidiEvent E)
{
Iterator<MidiEvent> it = mEvents.iterator();
MidiEvent prev = null, curr = null, next = null;
while(it.hasNext())
{
next = it.next();
if(E.equals(curr))
{
break;
}
prev = curr;
curr = next;
next = null;
}
if(next == null)
{
// Either the event was not found in the track,
// or this is the last event in the track.
// Either way, we won't need to update any delta times
return mEvents.remove(curr);
}
if(!mEvents.remove(curr))
{
return false;
}
if(prev != null)
{
next.setDelta(next.getTick() - prev.getTick());
}
else
{
next.setDelta(next.getTick());
}
return true;
}
public void closeTrack()
{
long lastTick = 0;
if(mEvents.size() > 0)
{
MidiEvent last = mEvents.last();
lastTick = last.getTick();
}
EndOfTrack eot = new EndOfTrack(lastTick + mEndOfTrackDelta, 0);
insertEvent(eot);
}
public void dumpEvents()
{
Iterator<MidiEvent> it = mEvents.iterator();
while(it.hasNext())
{
System.out.println(it.next());
}
}
private void recalculateSize()
{
mSize = 0;
Iterator<MidiEvent> it = mEvents.iterator();
MidiEvent last = null;
while(it.hasNext())
{
MidiEvent E = it.next();
mSize += E.getSize();
// If an event is of the same type as the previous event,
// no status byte is written.
if(last != null && !E.requiresStatusByte(last))
{
mSize--;
}
last = E;
}
mSizeNeedsRecalculating = false;
}
public void writeToFile(OutputStream out) throws IOException
{
if(!mClosed)
{
closeTrack();
}
if(mSizeNeedsRecalculating)
{
recalculateSize();
}
out.write(IDENTIFIER);
out.write(MidiUtil.intToBytes(mSize, 4));
Iterator<MidiEvent> it = mEvents.iterator();
MidiEvent lastEvent = null;
while(it.hasNext())
{
MidiEvent event = it.next();
if(VERBOSE)
{
System.out.println("Writing: " + event);
}
event.writeToFile(out, event.requiresStatusByte(lastEvent));
lastEvent = event;
}
}
}