/*
* AlsaSequencer.java
*
* This file is part of Tritonus: http://www.tritonus.org/
*/
/*
* Copyright (c) 1999 - 2003 by Matthias Pfisterer
*
* 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.alsa;
import java.util.Arrays;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Track;
import javax.sound.midi.Transmitter;
import org.tritonus.lowlevel.alsa.AlsaSeq;
import org.tritonus.lowlevel.alsa.AlsaSeqPortSubscribe;
import org.tritonus.lowlevel.alsa.AlsaSeqQueueInfo;
import org.tritonus.lowlevel.alsa.AlsaSeqEvent;
import org.tritonus.lowlevel.alsa.AlsaSeqQueueStatus;
import org.tritonus.lowlevel.alsa.AlsaSeqQueueTempo;
import org.tritonus.share.TDebug;
import org.tritonus.share.midi.MidiUtils;
import org.tritonus.share.midi.TSequencer;
public class AlsaSequencer
// TODO: derive from TPreloadingSequencer
extends TSequencer
{
/** The syncronization modes the sequencer can sync to.
*/
private static final SyncMode[] MASTER_SYNC_MODES = {SyncMode.INTERNAL_CLOCK};
/** The syncronization modes the sequencer can send.
*/
private static final SyncMode[] SLAVE_SYNC_MODES = {SyncMode.NO_SYNC, SyncMode.MIDI_SYNC};
/** The ALSA event tag used for MIDI clock events
*/
private static final int CLOCK_EVENT_TAG = 255;
private AlsaSeq m_playbackAlsaSeq;
private AlsaSeq m_recordingAlsaSeq;
private int m_nRecordingPort;
private int m_nPlaybackPort;
private int m_nQueue;
private AlsaSeqQueueInfo m_queueInfo;
private AlsaSeqQueueStatus m_queueStatus;
private AlsaSeqQueueTempo m_queueTempo;
private AlsaMidiIn m_playbackAlsaMidiIn;
private AlsaMidiOut m_playbackAlsaMidiOut;
private AlsaMidiIn m_recordingAlsaMidiIn;
protected LoaderThread m_loaderThread;
private Thread m_syncThread;
private AlsaSeqEvent m_queueControlEvent;
protected AlsaSeqEvent m_clockEvent;
private boolean m_bRecording;
private Track m_track;
private AlsaSeqEvent m_allNotesOffEvent;
private Sequencer.SyncMode m_oldSlaveSyncMode;
private float m_fCachedRealMPQ;
public AlsaSequencer(MidiDevice.Info info)
{
super(info,
Arrays.asList(MASTER_SYNC_MODES),
Arrays.asList(SLAVE_SYNC_MODES));
// TODO: fetch from base class instead
m_fCachedRealMPQ = -1.0F;
}
protected int getPlaybackClient()
{
int nClient = getPlaybackAlsaSeq().getClientId();
return nClient;
}
protected int getPlaybackPort()
{
return m_nPlaybackPort;
}
protected int getRecordingClient()
{
int nClient = getRecordingAlsaSeq().getClientId();
return nClient;
}
protected int getRecordingPort()
{
return m_nRecordingPort;
}
protected int getQueue()
{
return m_nQueue;
}
private AlsaSeqQueueStatus getQueueStatus()
{
return m_queueStatus;
}
private AlsaSeqQueueTempo getQueueTempo()
{
return m_queueTempo;
}
private AlsaSeq getPlaybackAlsaSeq()
{
return m_playbackAlsaSeq;
}
protected AlsaSeq getRecordingAlsaSeq()
{
return m_recordingAlsaSeq;
}
private void updateQueueStatus()
{
// TODO: error handling
// getRecordingAlsaSeq().getQueueStatus(getQueue(), getQueueStatus());
getPlaybackAlsaSeq().getQueueStatus(getQueue(), getQueueStatus());
}
protected void openImpl()
{
m_recordingAlsaSeq = new AlsaSeq("Tritonus ALSA Sequencer (recording/synchronization)");
m_nRecordingPort = getRecordingAlsaSeq().createPort("recording/synchronization port", AlsaSeq.SND_SEQ_PORT_CAP_WRITE | AlsaSeq.SND_SEQ_PORT_CAP_SUBS_WRITE | AlsaSeq.SND_SEQ_PORT_CAP_READ | AlsaSeq.SND_SEQ_PORT_CAP_SUBS_READ, 0, AlsaSeq.SND_SEQ_PORT_TYPE_APPLICATION, 0, 0, 0);
m_playbackAlsaSeq = new AlsaSeq("Tritonus ALSA Sequencer (playback)");
m_nPlaybackPort = getPlaybackAlsaSeq().createPort("playback port", AlsaSeq.SND_SEQ_PORT_CAP_WRITE | AlsaSeq.SND_SEQ_PORT_CAP_SUBS_WRITE | AlsaSeq.SND_SEQ_PORT_CAP_READ | AlsaSeq.SND_SEQ_PORT_CAP_SUBS_READ, 0, AlsaSeq.SND_SEQ_PORT_TYPE_APPLICATION, 0, 0, 0);
m_nQueue = getPlaybackAlsaSeq().allocQueue();
m_queueInfo = new AlsaSeqQueueInfo();
m_queueStatus = new AlsaSeqQueueStatus();
m_queueTempo = new AlsaSeqQueueTempo();
getPlaybackAlsaSeq().getQueueInfo(getQueue(), m_queueInfo);
m_queueInfo.setLocked(false);
getPlaybackAlsaSeq().setQueueInfo(getQueue(), m_queueInfo);
m_playbackAlsaMidiOut = new AlsaMidiOut(getPlaybackAlsaSeq(), getPlaybackPort(), getQueue());
m_playbackAlsaMidiOut.setHandleMetaMessages(true);
getRecordingAlsaSeq().setQueueUsage(getQueue(), true);
// this establishes the subscription, too
AlsaMidiIn.AlsaMidiInListener playbackListener = new PlaybackAlsaMidiInListener();
m_playbackAlsaMidiIn = new AlsaMidiIn(getPlaybackAlsaSeq(), getPlaybackPort(), getPlaybackClient(), getPlaybackPort(), playbackListener);
// start the receiving thread
m_playbackAlsaMidiIn.start();
m_queueControlEvent = new AlsaSeqEvent();
m_clockEvent = new AlsaSeqEvent();
m_clockEvent.setCommon(
AlsaSeq.SND_SEQ_EVENT_CLOCK, // type
AlsaSeq.SND_SEQ_TIME_STAMP_TICK | AlsaSeq.SND_SEQ_TIME_MODE_ABS,
CLOCK_EVENT_TAG, // tag
getQueue(),
0L, // timestamp; not yet known
0, // source client
getRecordingPort(), // source port
AlsaSeq.SND_SEQ_ADDRESS_SUBSCRIBERS, // dest client
AlsaSeq.SND_SEQ_ADDRESS_UNKNOWN); // dest port
m_allNotesOffEvent = new AlsaSeqEvent();
m_oldSlaveSyncMode = getSlaveSyncMode();
if (m_fCachedRealMPQ != -1.0F)
{
setTempoImpl(m_fCachedRealMPQ);
m_fCachedRealMPQ = -1.0F;
}
m_loaderThread = new LoaderThread();
m_loaderThread.start();
// this is for sending clock events
// m_syncThread = new MasterSynchronizer();
// m_syncThread.start();
}
protected void closeImpl()
{
m_playbackAlsaMidiIn.interrupt();
m_playbackAlsaMidiIn = null;
getQueueStatus().free();
m_queueStatus = null;
getQueueTempo().free();
m_queueTempo = null;
// TODO:
// m_aSequencer.releaseQueue(getQueue());
// m_aSequencer.destroyPort(getPort());
getRecordingAlsaSeq().close();
m_recordingAlsaSeq = null;
getPlaybackAlsaSeq().close();
m_playbackAlsaSeq = null;
m_queueControlEvent.free();
m_queueControlEvent = null;
m_clockEvent.free();
m_clockEvent = null;
m_allNotesOffEvent.free();
m_allNotesOffEvent = null;
}
protected void startImpl()
{
if (getTickPosition() == 0)
{
startQueue();
}
else
{
continueQueue();
}
synchronized (m_loaderThread)
{
if (TDebug.TraceSequencer) TDebug.out("AlsaSequencer.startImpl(): notifying loader thread");
m_loaderThread.notify();
}
// TODO: should depend on sync mode
// synchronized (m_syncThread)
// {
// if (TDebug.TraceSequencer) TDebug.out("AlsaSequencer.startImpl(): notifying synchronizer thread");
// m_syncThread.notify();
// }
if (! getSlaveSyncMode(). equals(Sequencer.SyncMode.NO_SYNC))
{
sendStartEvent();
}
}
protected void stopImpl()
{
stopQueue();
sendAllNotesOff();
// should be in base class?
stopRecording();
if (! getSlaveSyncMode(). equals(Sequencer.SyncMode.NO_SYNC))
{
sendStopEvent();
}
}
protected void setSequenceImpl()
{
if (m_loaderThread != null)
{
m_loaderThread.setLoading(getSequence() != null);
}
}
/* TODO: can be implemented just with a flag?
*/
public boolean isRunning()
{
boolean bRunning = false;
if (isOpen())
{
updateQueueStatus();
int nStatus = getQueueStatus().getStatus();
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.isRunning(): queue status: " + nStatus); }
bRunning = (nStatus != 0);
}
return bRunning;
}
public void startRecording()
{
checkOpen(); // may throw IllegalStateException
m_bRecording = true;
start();
}
public void stopRecording()
{
checkOpen(); // may throw IllegalStateException
m_bRecording = false;
}
public boolean isRecording()
{
return m_bRecording;
}
// name should be: enableRecording
public void recordEnable(Track track, int nChannel)
{
// TODO: hacky
m_track = track;
}
// name should be: disableRecording
public void recordDisable(Track track)
{
// TODO:
}
protected void setTempoImpl(float fRealMPQ)
{
if (isOpen())
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.setTempoImpl(): setting tempo to " + (int) fRealMPQ); }
getQueueTempo().setTempo((int) fRealMPQ);
getQueueTempo().setPpq(getResolution());
getPlaybackAlsaSeq().setQueueTempo(getQueue(), getQueueTempo());
}
else
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.setTempoImpl(): ignoring because sequencer is not open"); }
m_fCachedRealMPQ = fRealMPQ;
}
}
public long getTickPosition()
{
long lPosition;
if (isOpen())
{
updateQueueStatus();
lPosition = getQueueStatus().getTickTime();
}
else
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.getTickPosition(): sequencer not open, returning 0"); }
lPosition = 0;
}
return lPosition;
}
public void setTickPosition(long lTick)
{
if (isOpen())
{
int nSourcePort = getRecordingPort();
int nQueue = getQueue();
long lTime = lTick;
sendQueueControlEvent(
AlsaSeq.SND_SEQ_EVENT_SETPOS_TICK,
AlsaSeq.SND_SEQ_TIME_STAMP_REAL | AlsaSeq.SND_SEQ_TIME_MODE_REL, 0, AlsaSeq.SND_SEQ_QUEUE_DIRECT, 0L,
nSourcePort, AlsaSeq.SND_SEQ_CLIENT_SYSTEM, AlsaSeq.SND_SEQ_PORT_SYSTEM_TIMER,
nQueue, 0, lTime);
}
else
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.setTickPosition(): ignored because sequencer is not open"); }
}
}
public long getMicrosecondPosition()
{
long lPosition;
if (isOpen())
{
updateQueueStatus();
long lNanoSeconds = getQueueStatus().getRealTime();
lPosition = lNanoSeconds / 1000;
}
else
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.getMicrosecondPosition(): sequencer not open, returning 0"); }
lPosition = 0;
}
return lPosition;
}
public void setMicrosecondPosition(long lMicroseconds)
{
if (isOpen())
{
long lNanoSeconds = lMicroseconds * 1000;
int nSourcePort = getRecordingPort();
int nQueue = getQueue();
long lTime = lNanoSeconds;
sendQueueControlEvent(
AlsaSeq.SND_SEQ_EVENT_SETPOS_TIME,
AlsaSeq.SND_SEQ_TIME_STAMP_REAL | AlsaSeq.SND_SEQ_TIME_MODE_REL, 0, AlsaSeq.SND_SEQ_QUEUE_DIRECT, 0L,
nSourcePort, AlsaSeq.SND_SEQ_CLIENT_SYSTEM, AlsaSeq.SND_SEQ_PORT_SYSTEM_TIMER,
nQueue, 0, lTime);
}
else
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.setMicrosecondPosition(): ignoring because sequencer is not open"); }
}
}
protected void setMasterSyncModeImpl(SyncMode syncMode)
{
// TODO:
}
protected void setSlaveSyncModeImpl(SyncMode syncMode)
{
if (isRunning())
{
if (m_oldSlaveSyncMode.equals(Sequencer.SyncMode.NO_SYNC) && (syncMode.equals(Sequencer.SyncMode.MIDI_SYNC) || syncMode.equals(Sequencer.SyncMode.MIDI_TIME_CODE)) )
{
sendStartEvent();
// TODO: notify sync thread
}
else if ((m_oldSlaveSyncMode.equals(Sequencer.SyncMode.MIDI_SYNC) || m_oldSlaveSyncMode.equals(Sequencer.SyncMode.MIDI_TIME_CODE)) && syncMode.equals(Sequencer.SyncMode.NO_SYNC) )
{
sendStopEvent();
// TODO: remove enqueued messages from queue (and buffer). perhaps do this by putting the code to do so after the main loop of the sync thread.
}
}
}
protected void setTrackEnabledImpl(int nTrack, boolean bEnabled)
{
/** If the enabled state changes to true, the events
between the current playback position and the current
loading position are enqueued.
If the enabled state of a track changed to false,
the events belonging to this track are removed from
the queue, besides the 'off'-events.
*/
if (bEnabled)
{
// TODO: reload events
}
else
{
// TODO: remove events
}
}
/*
This method has to be synchronized because it is called
from sendMessageTick() as well as from loadSequenceToNative().
*/
protected synchronized void enqueueMessage(MidiMessage message, long lTick)
{
m_playbackAlsaMidiOut.enqueueMessage(message, lTick);
}
/** Put a message into the queue.
This is Claus-Dieter's special method: it puts the message to
the ALSA queue for delivery at the specified time.
The time has to be given in ticks according to the resolution
of the currently active Sequence. For this method to work,
the Sequencer has to be started. The message is delivered
the same way as messages from a Sequence, i.e. to all
registered Transmitters. If the current queue position (as
returned by getTickPosition()) is
already behind the desired schedule time, the message is
ignored.
@param message the MidiMessage to put into the queue.
@param lTick the desired schedule time in ticks.
*/
public void sendMessageTick(MidiMessage message, long lTick)
{
enqueueMessage(message, lTick);
}
private void startQueue()
{
controlQueue(AlsaSeq.SND_SEQ_EVENT_START);
}
private void continueQueue()
{
controlQueue(AlsaSeq.SND_SEQ_EVENT_CONTINUE);
}
private void stopQueue()
{
controlQueue(AlsaSeq.SND_SEQ_EVENT_STOP);
}
private void controlQueue(int nType)
{
int nSourcePort = getPlaybackPort();
int nQueue = getQueue();
sendQueueControlEvent(
nType,
AlsaSeq.SND_SEQ_TIME_STAMP_REAL | AlsaSeq.SND_SEQ_TIME_MODE_REL,
0,
AlsaSeq.SND_SEQ_QUEUE_DIRECT,
0L,
nSourcePort,
AlsaSeq.SND_SEQ_CLIENT_SYSTEM,
AlsaSeq.SND_SEQ_PORT_SYSTEM_TIMER,
nQueue, 0, 0);
}
/** Send a real time START event to the subscribers immediately.
*/
private void sendStartEvent()
{
sendRealtimeEvent(AlsaSeq.SND_SEQ_EVENT_START);
}
/** Send a real time STOP event to the subscribers immediately.
*/
private void sendStopEvent()
{
sendRealtimeEvent(AlsaSeq.SND_SEQ_EVENT_STOP);
}
private void sendRealtimeEvent(int nType)
{
sendQueueControlEvent(
nType,
AlsaSeq.SND_SEQ_TIME_STAMP_REAL | AlsaSeq.SND_SEQ_TIME_MODE_REL,
0, // tag
AlsaSeq.SND_SEQ_QUEUE_DIRECT, // queue
0L, // time
getPlaybackPort(), // source
AlsaSeq.SND_SEQ_ADDRESS_SUBSCRIBERS, // dest client
AlsaSeq.SND_SEQ_ADDRESS_UNKNOWN, // dest port
0, 0, 0);
}
// NOTE: also used for setting position and start/stop RT
private void sendQueueControlEvent(
int nType, int nFlags, int nTag, int nQueue, long lTime,
int nSourcePort, int nDestClient, int nDestPort,
int nControlQueue, int nControlValue, long lControlTime)
{
m_queueControlEvent.setCommon(nType, nFlags, nTag, nQueue, lTime,
0, nSourcePort, nDestClient, nDestPort);
m_queueControlEvent.setQueueControl(nControlQueue, nControlValue, lControlTime);
getPlaybackAlsaSeq().eventOutputDirect(m_queueControlEvent);
}
private void sendAllNotesOffEvent(int nChannel)
{
int nSourcePort = getPlaybackPort();
m_allNotesOffEvent.setCommon(
AlsaSeq.SND_SEQ_EVENT_CONTROLLER,
AlsaSeq.SND_SEQ_TIME_STAMP_REAL | AlsaSeq.SND_SEQ_TIME_MODE_REL,
0, // tag
AlsaSeq.SND_SEQ_QUEUE_DIRECT, // queue
0L, // time
0, nSourcePort, // source
AlsaSeq.SND_SEQ_ADDRESS_SUBSCRIBERS, // dest client
AlsaSeq.SND_SEQ_ADDRESS_UNKNOWN); // dest port
m_allNotesOffEvent.setControl(nChannel, 0x78, 0);
getPlaybackAlsaSeq().eventOutputDirect(m_allNotesOffEvent);
}
private void sendAllNotesOff()
{
// TODO: check if [0..15] or [1..16]
for (int nChannel = 0; nChannel < 16; nChannel++)
{
sendAllNotesOffEvent(nChannel);
}
}
/** Receive a correctely timestamped event.
This method expects that the timestamp is in ticks,
appropriate for the Sequence currently running.
*/
protected void receiveTimestamped(MidiMessage message, long lTimestamp)
{
if (isRecording())
{
// TODO: this is hacky; should implement correct track mapping
Track track = m_track;
MidiEvent event = new MidiEvent(message, lTimestamp);
track.add(event);
}
// TODO: entering an event into the sequence
}
/** Receive an event from a Receiver.
This method is called by AlsaSequencer.AlsaSequencerReceiver
on receipt of a MidiMessage.
*/
protected void receive(MidiMessage message, long lTimestamp)
{
lTimestamp = getTickPosition();
receiveTimestamped(message, lTimestamp);
}
///////////////////////////////////////////////////
public Receiver getReceiver()
throws MidiUnavailableException
{
return new AlsaSequencerReceiver();
}
public Transmitter getTransmitter()
throws MidiUnavailableException
{
return new AlsaSequencerTransmitter();
}
/////////////////// INNER CLASSES //////////////////////////////////////
/*private*/ public class PlaybackAlsaMidiInListener
implements AlsaMidiIn.AlsaMidiInListener
{
public void dequeueEvent(MidiMessage message, long lTimestamp)
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.PlaybackAlsaMidiInListener.dequeueEvent(): message: " + message); }
if (message instanceof MetaMessage)
{
MetaMessage metaMessage = (MetaMessage) message;
byte[] abData = metaMessage.getData();
switch (metaMessage.getType())
{
case 6: // marker
String strMarkerText = new String(abData);
if (strMarkerText.equals("loopend"))
{
setTickPosition(getLoopStartPoint());
m_loaderThread.setStartPosition(getLoopStartPoint());
m_loaderThread.setLoading(true);
}
break;
case 0x51: // set tempo
int nTempo = MidiUtils.getUnsignedInteger(abData[0]) * 65536 +
MidiUtils.getUnsignedInteger(abData[1]) * 256 +
MidiUtils.getUnsignedInteger(abData[2]);
setTempoInMPQ(nTempo);
break;
}
}
// passes events to the receivers
sendImpl(message, -1L);
// calls control and meta listeners
notifyListeners(message);
}
}
/*private*/ public class RecordingAlsaMidiInListener
implements AlsaMidiIn.AlsaMidiInListener
{
public void dequeueEvent(MidiMessage message, long lTimestamp)
{
if (TDebug.TraceSequencer) { TDebug.out("AlsaSequencer.RecordingAlsaMidiInListener.dequeueEvent(): message: " + message); }
AlsaSequencer.this.receiveTimestamped(message, lTimestamp);
}
}
/*private*/ public class AlsaSequencerReceiver
extends TReceiver
implements AlsaReceiver
{
/** Subscribe to the passed port.
* This establishes a subscription in the ALSA sequencer
* so that the device this Receiver belongs to receives
* event from the client:port passed as parameters.
*
* @return true if subscription was established,
* false otherwise
*/
public boolean subscribeTo(int nClient, int nPort)
{
try
{
AlsaSeqPortSubscribe portSubscribe = new AlsaSeqPortSubscribe();
portSubscribe.setSender(nClient, nPort);
portSubscribe.setDest(AlsaSequencer.this.getRecordingClient(), AlsaSequencer.this.getRecordingPort());
portSubscribe.setQueue(AlsaSequencer.this.getQueue());
portSubscribe.setExclusive(false);
portSubscribe.setTimeUpdate(true);
portSubscribe.setTimeReal(false);
AlsaSequencer.this.getRecordingAlsaSeq().subscribePort(portSubscribe);
portSubscribe.free();
return true;
}
catch (RuntimeException e)
{
return false;
}
}
}
/*private*/ public class AlsaSequencerTransmitter
extends TTransmitter
{
private boolean m_bReceiverSubscribed;
public AlsaSequencerTransmitter()
{
super();
m_bReceiverSubscribed = false;
}
public void setReceiver(Receiver receiver)
{
super.setReceiver(receiver);
/*
* Try to establish a subscription of the Receiver
* to the ALSA seqencer client of the device this
* Transmitter belongs to.
*/
if (receiver instanceof AlsaReceiver)
{
// TDebug.out("AlsaSequencer.AlsaSequencerTransmitter.setReceiver(): trying to establish subscription");
m_bReceiverSubscribed = ((AlsaReceiver) receiver).subscribeTo(getPlaybackClient(), getPlaybackPort());
// TODO: similar subscription for the sequencer's own midi in listener!!
// this is necessary because sync messages are sent via the recording port
m_bReceiverSubscribed = ((AlsaReceiver) receiver).subscribeTo(getRecordingClient(), getRecordingPort());
// TDebug.out("AlsaSequencer.AlsaSequencerTransmitter.setReceiver(): subscription established: " + m_bReceiverSubscribed);
}
}
public void send(MidiMessage message, long lTimeStamp)
{
/*
* Send message via Java methods only if no
* subscription was established. If there is a
* subscription, the message is routed inside of
* the ALSA sequencer.
*/
if (! m_bReceiverSubscribed)
{
super.send(message, lTimeStamp);
}
}
public void close()
{
super.close();
// TODO: remove subscription
}
}
/** Pre-loading events to the sequencer queue.
*/
/*private*/ public class LoaderThread
extends Thread
{
/** Current position of loading in Ticks. This is used to get
a useful tick value for the end of track message.
*/
private long m_lLoadingPosition;
/** Position to start loading in Ticks.
This is used for Sequencer.seq[Tick|]Position().
*/
private long m_lStartPosition;
/** Loading activity. This flag shows if the LoaderThread is
currently loading events to the native queue. Loading may
be temporarily stopped if a position change in the
sequencer is executed, especially in case of a position
change for looping. This variable is only valid if
isRunning() is true. If the sequencer is stopped, it has
no significance (since loading is stopped anyway).
*/
private boolean m_bLoading;
private Track[] m_aTracks;
private int[] m_anTrackPositions;
public LoaderThread()
{
// TODO: monitor changes in the number of tracks
m_lLoadingPosition = 0;
initTracks();
/* If no sequence is set, we remain idle. We only start loading
if there is something to load.
If sequence has been set before open(), we set loading here
(LoaderThread is created in openImpl()).
If sequence is set after open, we do not set loading here,
but in setSequenceImpl().
*/
setLoading(getSequence() != null);
}
private void initTracks()
{
Sequence sequence = getSequence();
// TODO: reallocate if number of tracks has been changed.
if (m_aTracks == null && sequence != null)
{
m_aTracks = sequence.getTracks();
m_anTrackPositions = new int[m_aTracks.length];
}
}
public void setLoading(boolean bLoading)
{
if (TDebug.TraceSequencer) TDebug.out("LoaderThread.setLoading(): new value: " + bLoading);
m_bLoading = bLoading;
synchronized (this)
{
this.notify();
}
}
private boolean isLoading()
{
return m_bLoading;
}
public void setStartPosition(long lTicks)
{
// only to make sure...
setLoading(false);
m_lStartPosition = lTicks;
}
public void run()
{
while (isOpen())
{
do
{
synchronized (this)
{
try
{
this.wait();
}
catch (InterruptedException e)
{
}
}
}
while ( ! (isRunning() && isLoading()) );
if (isOpen())
{
loadSequenceToNative();
}
}
}
private void loadSequenceToNative()
{
initTracks();
/* For non-0 start positions, this works in conjunction with the
'continue' clause below. Not very efficient...
setStartPostion() shoult adapt m_anTrackPositions[].
*/
for (int i = 0; i < m_aTracks.length; i++)
{
m_anTrackPositions[i] = 0;
}
while (isRunning() && isLoading())
{
boolean bTrackPresent = false;
long lBestTick = Long.MAX_VALUE;
int nBestTrack = -1;
for (int nTrack = 0; nTrack < m_aTracks.length; nTrack++)
{
if (m_anTrackPositions[nTrack] < m_aTracks[nTrack].size())
{
bTrackPresent = true;
MidiEvent event = m_aTracks[nTrack].get(m_anTrackPositions[nTrack]);
long lTick = event.getTick();
if (lTick < m_lStartPosition)
{
// consider next event
continue;
}
if (lTick < lBestTick)
{
lBestTick = lTick;
nBestTrack = nTrack;
}
}
}
if (!bTrackPresent)
{
/* No more events; send
end-of-track event.
*/
MetaMessage metaMessage = new MetaMessage();
try
{
metaMessage.setMessage(0x2F, new byte[0], 0);
}
catch (InvalidMidiDataException e)
{
}
enqueueMessage(metaMessage, m_lLoadingPosition + 1);
// leave the while (isRunning() && isLoading())-loop
setLoading(false);
}
/** The normal case: deliver the event
found to be the next.
*/
MidiEvent event = m_aTracks[nBestTrack].get(m_anTrackPositions[nBestTrack]);
m_anTrackPositions[nBestTrack]++;
long lTick = event.getTick();
m_lLoadingPosition = Math.max(m_lLoadingPosition, lTick);
MidiMessage message = event.getMessage();
processMessage(message, lTick);
}
}
private void processMessage(MidiMessage message, long lTick)
{
boolean bMessageConsumed = false;
if (message instanceof MetaMessage)
{
MetaMessage metaMessage = (MetaMessage) message;
int nType = metaMessage.getType();
if (nType == 0x2F) // E.O.T.
{
bMessageConsumed = true;
if (TDebug.TraceSequencer) { TDebug.out("LoaderThread.loadSequenceToNative(): ignoring End of Track message with tick " + lTick); }
}
else if (nType == 6) // marker
{
String strMarkerText = new String(metaMessage.getData());
if (strMarkerText.equals("loopstart"))
{
setLoopStartPoint(lTick);
bMessageConsumed = true;
}
else if (strMarkerText.equals("loopend"))
{
setLoopEndPoint(lTick);
setLoopCount(-1 /*TODO: Sequencer.LOOP_CONTINUOUSLY */);
/* This one needs to be enqueued, because we do
a setPosition() once it is delivered. */
bMessageConsumed = false;
setLoading(false);
}
}
}
if (! bMessageConsumed)
{
if (TDebug.TraceSequencer) { TDebug.out("LoaderThread.loadSequenceToNative(): enqueueing event with tick " + lTick); }
enqueueMessage(message, lTick);
}
}
}
// TODO: start/stop; on/off
/*private*/ public class MasterSynchronizer
extends Thread
{
public void run()
{
while (isOpen())
{
do
{
synchronized (this)
{
try
{
this.wait();
}
catch (InterruptedException e)
{
}
}
}
while (! isRunning());
double dTickMin = getTickPosition();
double dTickMax = getSequence().getTickLength();
double dTickStep = getSequence().getResolution() / 24.0;
if (TDebug.TraceSequencer) { TDebug.out("MasterSynchronizer.run(): tick step: " + dTickStep); }
double dTick = dTickMin;
// TODO: ... && getS.Mode().equals(...)
while (dTick < dTickMax && isRunning())
{
long lTick = Math.round(dTick);
if (TDebug.TraceSequencer) { TDebug.out("MasterSynchronizer.run(): sending clock event with tick " + lTick); }
m_clockEvent.setTimestamp(lTick);
getRecordingAlsaSeq().eventOutput(m_clockEvent);
getRecordingAlsaSeq().drainOutput();
dTick += dTickStep;
}
}
}
}
}
/*** AlsaSequencer.java ***/