/*
* JavaSequencer.java
*
* This file is part of Tritonus: http://www.tritonus.org/
*/
/*
* Copyright (c) 2000 - 2003 by Matthias Pfisterer
* Copyright (c) 2003 by Gabriele Mondada
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as published
* by the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
|<--- this code is formatted to fit into 80 columns --->|
*/
package org.tritonus.midi.device.java;
import java.util.Arrays;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;
import javax.sound.midi.InvalidMidiDataException;
import org.tritonus.share.TDebug;
import org.tritonus.share.midi.MidiUtils;
import org.tritonus.share.midi.TSequencer;
/** Sequencer implementation in pure Java.
*/
public class JavaSequencer
extends TSequencer
implements Runnable
{
private static final SyncMode[] MASTER_SYNC_MODES = {SyncMode.INTERNAL_CLOCK};
private static final SyncMode[] SLAVE_SYNC_MODES = {SyncMode.NO_SYNC};
// internal states
/** not running */
private static final int STATE_STOPPED = 0;
/** starting, awake thread */
private static final int STATE_STARTING = 1;
/** running */
private static final int STATE_STARTED = 2;
/** stopping */
private static final int STATE_STOPPING = 3;
/** closing, terminate thread */
private static final int STATE_CLOSING = 4;
private Thread m_thread;
private long m_lMicroSecondsPerTick;
private int[] m_anTrackPositions;
private long m_lTickPosition;
private long m_lStartTime;
/** Internal state of the sequencer.
As values, the symbolic constants STATE_*
are used.
*/
private int m_nPhase;
private boolean m_bTempoChanged;
/** The clock to use as time base for this sequencer.
This is commonly intialized in the constructor,
but can also be set with {@link #setClock}.
*/
private Clock m_clock;
/** How long to sleep in the main loop.
The value is initialized in the constructor by reading a
system property.
*/
private long m_lSleepInterval;
public JavaSequencer(MidiDevice.Info info)
{
super(info,
Arrays.asList(MASTER_SYNC_MODES),
Arrays.asList(SLAVE_SYNC_MODES));
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.<init>(): begin"); }
String strVersion = System.getProperty("java.version");
if (strVersion.indexOf("1.4.2") != -1)
{
setClock(new SunMiscPerfClock());
}
else
{
setClock(new SystemCurrentTimeMillisClock());
}
String strOS = System.getProperty("os.name");
if (strOS.equals("Linux"))
{
m_lSleepInterval = 0;
}
else
{
m_lSleepInterval = 1;
}
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.<init>(): end"); }
}
protected void openImpl()
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.openImpl(): begin"); }
m_nPhase = STATE_STOPPED;
m_thread = new Thread(this);
m_thread.setPriority(Thread.MAX_PRIORITY);
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.openImpl(): starting thread"); }
m_thread.start();
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.openImpl(): end"); }
}
protected void closeImpl()
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.closeImpl(): begin"); }
stop();
// terminate the thread
synchronized (this)
{
m_nPhase = STATE_CLOSING; // ask end of thread
this.notifyAll();
}
// now the thread should terminate
m_thread = null;
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.closeImpl(): end"); }
}
protected void startImpl()
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.startImpl(): begin"); }
synchronized (this)
{
if (m_nPhase == STATE_STOPPED)
{
// unlock thread waiting for start
m_nPhase = STATE_STARTING; // ask for start
this.notifyAll();
// wait until m_lStartTime is set
while (m_nPhase == STATE_STARTING)
{
try {
this.wait();
} catch (InterruptedException e) {
if (TDebug.TraceAllExceptions) { TDebug.out(e); }
}
}
}
}
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.startImpl(): end"); }
}
protected void stopImpl()
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.stopImpl(): begin"); }
synchronized (this)
{
// condition true if called from own run() method
if (Thread.currentThread() == m_thread)
{
if (m_nPhase != STATE_STOPPED)
{
m_nPhase = STATE_STOPPED;
notifyAll();
}
}
else
{
if (m_nPhase == STATE_STARTED)
{
m_nPhase = STATE_STOPPING; // ask for stop
while (m_nPhase == STATE_STOPPING)
{
try {
this.wait();
} catch (InterruptedException e) {
if (TDebug.TraceAllExceptions) { TDebug.out(e); }
}
}
}
}
}
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.stopImpl(): end"); }
}
public void run()
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): begin"); }
while (true)
{
synchronized (this)
{
while (m_nPhase == STATE_STOPPED)
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): waiting to become running"); }
try
{
this.wait();
}
catch (InterruptedException e)
{
if (TDebug.TraceAllExceptions) { TDebug.out(e); }
}
}
if (m_nPhase == STATE_CLOSING) {
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): end"); }
return;
}
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): now running"); }
//NOTE: all time calculations are done in microseconds
m_lStartTime = getTimeInMicroseconds() - m_lTickPosition * m_lMicroSecondsPerTick;
m_nPhase = STATE_STARTED;
this.notifyAll();
}
Sequence sequence = getSequence();
if (sequence == null)
{
stop();
continue;
}
Track[] aTracks = sequence.getTracks();
// this is used to get a useful time value for the end of track message
//long lHighestTime = 0;
while (m_nPhase == STATE_STARTED)
{
// searching for the next event
boolean bTrackPresent = false;
long lBestTick = Long.MAX_VALUE;
int nBestTrack = -1;
for (int nTrack = 0; nTrack < aTracks.length; nTrack++)
{
// TDebug.out("track " + nTrack);
// Track track = aTracks[nTrack];
if (m_anTrackPositions[nTrack] < aTracks[nTrack].size()
&& isTrackEnabled(nTrack))
{
bTrackPresent = true;
MidiEvent event = aTracks[nTrack].get(m_anTrackPositions[nTrack]);
long lTick = event.getTick();
if (lTick < lBestTick)
{
lBestTick = lTick;
nBestTrack = nTrack;
}
}
}
if (!bTrackPresent)
{
MetaMessage metaMessage = new MetaMessage();
try
{
metaMessage.setMessage(0x2F, new byte[0], 0);
}
catch (InvalidMidiDataException e)
{
if (TDebug.TraceAllExceptions) { TDebug.out(e); }
}
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): sending End of Track message with tick " + (m_lTickPosition + 1)); }
// TODO: calulate us
deliverEvent(metaMessage, m_lTickPosition + 1);
stop();
break;
}
MidiEvent event = aTracks[nBestTrack].get(m_anTrackPositions[nBestTrack]);
MidiMessage message = event.getMessage();
long lTick = event.getTick();
if (message instanceof MetaMessage && ((MetaMessage) message).getType() == 0x2F)
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.run(): ignoring End of Track message with tick " + lTick); }
m_anTrackPositions[nBestTrack]++;
synchronized (this)
{
m_lTickPosition = lTick;
}
}
else
{
if (deliverEvent(message, lTick))
{
m_anTrackPositions[nBestTrack]++;
synchronized(this) {
m_lTickPosition = lTick;
}
}
else
{
// be sure that the current position is before the next event
synchronized(this) {
m_lTickPosition = Math.min(lTick, (getTimeInMicroseconds() - m_lStartTime) / m_lMicroSecondsPerTick);
}
}
}
} // while (m_nPhase == STATE_STARTED)
stop();
} // while (true)
}
/** Deliver a message at a certain time.
@param lScheduledTick when to deliver the message in ticks
@return true if the event was sent, false otherwise
*/
private boolean deliverEvent(MidiMessage message, long lScheduledTick)
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.deliverEvent(): begin"); }
long lScheduledTime;
synchronized(this) {
lScheduledTime = lScheduledTick * m_lMicroSecondsPerTick + m_lStartTime;
}
// wait for scheduled time
while (getTimeInMicroseconds() < lScheduledTime)
{
if (m_nPhase != STATE_STARTED)
return false;
if (m_bTempoChanged) {
synchronized(this) {
lScheduledTime = lScheduledTick * m_lMicroSecondsPerTick + m_lStartTime;
m_bTempoChanged = false;
}
}
try
{
Thread.sleep(m_lSleepInterval);
}
catch (InterruptedException e)
{
if (TDebug.TraceAllExceptions) { TDebug.out(e); }
}
}
// send midi message
if (message instanceof MetaMessage)
{
MetaMessage metaMessage = (MetaMessage) message;
if (metaMessage.getType() == 0x51) // set tempo
{
byte[] abData = metaMessage.getData();
int nTempo = MidiUtils.getUnsignedInteger(abData[0]) * 65536 +
MidiUtils.getUnsignedInteger(abData[1]) * 256 +
MidiUtils.getUnsignedInteger(abData[2]);
// TDebug.out("tempo (us/quarter note): " + nTempo);
setTempoInMPQ(nTempo);
// TODO: setTempoInMPQ() seems to be not thread-safe
}
}
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.deliverEvent(): sending message: " + message + " at: " + lScheduledTime); }
// sendImpl(message, event.getTick());
sendImpl(message, -1);
// TODO: sendImpl() seems to be not thread-safe
notifyListeners(message);
// TODO: notifyListeners() seems to be not thread-safe
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.deliverEvent(): end"); }
return true; // success
}
protected void setMasterSyncModeImpl(SyncMode syncMode)
{
// DO NOTHING
}
protected void setSlaveSyncModeImpl(SyncMode syncMode)
{
// DO NOTHING
}
public void setSequence(Sequence sequence)
throws InvalidMidiDataException
{
boolean bWasRunning = isRunning();
if (bWasRunning)
{
stop();
}
super.setSequence(sequence);
m_lTickPosition = 0;
m_anTrackPositions = new int[sequence.getTracks().length];
for (int i = 0; i < m_anTrackPositions.length; i++)
{
m_anTrackPositions[i] = 0;
}
if (bWasRunning)
{
start();
}
}
public void setMicrosecondPosition(long lPosition)
{
setTickPosition(lPosition / m_lMicroSecondsPerTick);
}
public void setTickPosition(long lPosition)
{
if (getSequence() == null || m_anTrackPositions == null)
{
return;
}
boolean bWasRunning = isRunning();
if (bWasRunning)
stop();
if (lPosition > getSequence().getTickLength())
{
m_lTickPosition = getSequence().getTickLength();
}
else
{
m_lTickPosition = lPosition;
}
for (int i = 0; i < m_anTrackPositions.length; i++)
{
m_anTrackPositions[i] = getTrackPosition(getSequence().getTracks()[i], lPosition);
}
if (bWasRunning)
start();
}
public synchronized long getTickPosition()
{
if (m_nPhase == STATE_STARTED) {
return Math.max(m_lTickPosition, (getTimeInMicroseconds() - m_lStartTime) / m_lMicroSecondsPerTick);
} else {
return m_lTickPosition;
}
}
public void recordDisable(Track track)
{
}
public void recordEnable(Track track)
{
}
public void recordEnable(Track track, int nChannel)
{
}
public boolean isRecording()
{
return false;
}
public void stopRecording()
{
checkOpen();
}
public void startRecording()
{
checkOpen();
}
protected synchronized void setTempoImpl(float fMPQ)
{
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.setTempoImpl(): begin"); }
int nResolution = getResolution();
long currentTime = getTimeInMicroseconds();
long currentTickPosition = 0;
if (m_lMicroSecondsPerTick!= 0)
currentTickPosition = (currentTime - m_lStartTime) / m_lMicroSecondsPerTick;
m_lMicroSecondsPerTick = (long) fMPQ / nResolution;
m_lStartTime = currentTime - currentTickPosition * m_lMicroSecondsPerTick;
m_bTempoChanged = true;
// TODO: update m_lMicroSecondsPerTick and m_lStartTime only after the next event because the the event now waiting for its schedule is not updated
if (TDebug.TraceSequencer) { TDebug.out("JavaSequencer.setTempoImpl(): end"); }
}
/** Obtain the index of the event with the closest tick value.
*/
private int getTrackPosition(Track track, long tickPosition) {
// check params
if (track.size() == 0 || tickPosition <= track.get(0).getTick())
return 0;
if (tickPosition > track.get(track.size()-1).getTick())
return track.size(); // index out of track
// quick search
int idx1 = 0;
int idx2 = track.size() - 1;
for (;;)
{
if ((idx2 - idx1) == 1)
return idx1;
int idx3 = (int)(((long)idx1 + (long)idx2) / 2L);
if (tickPosition > track.get(idx3).getTick())
idx1 = idx3;
else
idx2 = idx3;
}
}
/** Retrieve system time in microseconds.
This method uses the clock as set with {@link #setClock}.
@return the system time in microseconds
*/
protected long getTimeInMicroseconds()
{
// temporary hack
if (getClock() == null)
{
return 0;
}
// end hack
return getClock().getMicroseconds();
}
/** Set the clock this sequencer should use.
@param clock the Clock to be used
@throws IllegalStateException if the sequencer is not closed
*/
public void setClock(Clock clock)
{
if (isOpen())
{
throw new IllegalStateException("closed state required to set the clock");
}
m_clock = clock;
}
/** Obtain the clock used by this sequencer.
@return the clock currently set for this sequencer
*/
public Clock getClock()
{
return m_clock;
}
/** Interface for sequencer clocks.
*/
public static interface Clock
{
public long getMicroseconds();
}
}
/*** JavaSequencer.java ***/