/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/**
* A Real Time Sequencer
*
* @author Florian Bomers
*/
/* TODO:
* - rename PlayThread to PlayEngine (because isn't a thread)
*/
implements Sequencer, AutoConnectSequencer {
// STATIC VARIABLES
/** debugging flags */
private final static boolean DEBUG_PUMP = false;
private final static boolean DEBUG_PUMP_ALL = false;
/**
* Event Dispatcher thread. Should be using a shared event
* dispatcher instance with a factory in EventDispatcher
*/
new WeakHashMap<>();
/**
* All RealTimeSequencers share this info object.
*/
/**
* Sequence on which this sequencer is operating.
*/
// caches
/**
* Same for setTempoInMPQ...
* -1 means not set.
*/
/**
* cache value for tempo factor until sequence is set
* -1 means not set.
*/
/** if a particular track is muted */
/** if a particular track is solo */
/** tempo cache for getMicrosecondPosition */
/**
* True if the sequence is running.
*/
private boolean running = false;
/** the thread for pushing out the MIDI messages */
/**
* True if we are recording
*/
private boolean recording = false;
/**
* List of tracks to which we're recording
*/
/**
* Meta event listeners
*/
/**
* Control change listeners
*/
/** automatic connection support */
private boolean autoConnect = false;
/** if we need to autoconnect at next open */
private boolean doAutoConnectAtNextOpen = false;
/** the receiver that this device is auto-connected to */
/* ****************************** CONSTRUCTOR ****************************** */
super(info);
}
/* ****************************** SEQUENCER METHODS ******************** */
throws InvalidMidiDataException {
setCaches();
stop();
// initialize some non-cached values
trackMuted = null;
loopStart = 0;
loopEnd = -1;
loopCount = 0;
if (getDataPump() != null) {
}
}
if (playThread != null) {
}
// store this sequence (do not copy - we want to give the possibility
// of modifying the sequence at runtime)
// rewind to the beginning
setTickPosition(0);
// propagate caches
}
}
if (playThread != null) {
}
}
}
public synchronized void setSequence(InputStream stream) throws IOException, InvalidMidiDataException {
return;
}
}
return sequence;
}
public synchronized void start() {
// sequencer not open: throw an exception
if (!isOpen()) {
throw new IllegalStateException("sequencer not open");
}
// sequence not available: throw an exception
throw new IllegalStateException("sequence not set");
}
// already running: return quietly
if (running == true) {
return;
}
// start playback
implStart();
}
public synchronized void stop() {
if (!isOpen()) {
throw new IllegalStateException("sequencer not open");
}
// not running; just return
if (running == false) {
return;
}
// stop playback
implStop();
}
public boolean isRunning() {
return running;
}
public void startRecording() {
if (!isOpen()) {
throw new IllegalStateException("Sequencer not open");
}
start();
recording = true;
}
public void stopRecording() {
if (!isOpen()) {
throw new IllegalStateException("Sequencer not open");
}
recording = false;
}
public boolean isRecording() {
return recording;
}
throw new IllegalArgumentException("Track does not exist in the current sequence");
}
synchronized(recordingTracks) {
} else {
}
}
}
synchronized(recordingTracks) {
}
}
}
boolean found = false;
found = true;
break;
}
}
}
return found;
}
public float getTempoInBPM() {
}
if (bpm <= 0) {
// should throw IllegalArgumentException
bpm = 1.0f;
}
}
public float getTempoInMPQ() {
if (needCaching()) {
// if the sequencer is closed, return cached value
if (cacheTempoMPQ != -1) {
return (float) cacheTempoMPQ;
}
// if sequence is set, return current tempo
}
// last resort: return a standard tempo: 120bpm
return (float) MidiUtils.DEFAULT_TEMPO_MPQ;
}
return (float)getDataPump().getTempoMPQ();
}
if (mpq <= 0) {
// should throw IllegalArgumentException
mpq = 1.0f;
}
if (needCaching()) {
// cache the value
cacheTempoMPQ = mpq;
} else {
// set the native tempo in MPQ
// reset the tempoInBPM and tempoInMPQ values so we won't use them again
cacheTempoMPQ = -1;
}
}
if (factor <= 0) {
// should throw IllegalArgumentException
return;
}
if (needCaching()) {
} else {
// don't need cache anymore
cacheTempoFactor = -1;
}
}
public float getTempoFactor() {
if (needCaching()) {
if (cacheTempoFactor != -1) {
return cacheTempoFactor;
}
return 1.0f;
}
return getDataPump().getTempoFactor();
}
public long getTickLength() {
return 0;
}
return sequence.getTickLength();
}
public synchronized long getTickPosition() {
return 0;
}
return getDataPump().getTickPos();
}
if (tick < 0) {
// should throw IllegalArgumentException
return;
}
if (getDataPump() == null) {
if (tick != 0) {
// throw new InvalidStateException("cannot set position in closed state");
}
}
if (tick != 0) {
// throw new InvalidStateException("cannot set position if sequence is not set");
}
} else {
}
}
public long getMicrosecondLength() {
return 0;
}
return sequence.getMicrosecondLength();
}
public long getMicrosecondPosition() {
return 0;
}
synchronized (tempoCache) {
}
}
if (microseconds < 0) {
// should throw IllegalArgumentException
return;
}
if (Printer.trace) Printer.trace(">> RealTimeSequencer: setMicrosecondPosition("+microseconds+") ");
if (getDataPump() == null) {
if (microseconds != 0) {
// throw new InvalidStateException("cannot set position in closed state");
}
}
if (microseconds != 0) {
// throw new InvalidStateException("cannot set position if sequence is not set");
}
} else {
synchronized(tempoCache) {
}
}
}
// not supported
}
return masterSyncMode;
}
return returnedModes;
}
// not supported
}
return slaveSyncMode;
}
return returnedModes;
}
int getTrackCount() {
// $$fb wish there was a nicer way to get the number of tracks...
}
return 0;
}
int trackCount = getTrackCount();
if (getDataPump() != null) {
}
}
return trackMuted[track];
}
int trackCount = getTrackCount();
if (getDataPump() != null) {
}
}
}
synchronized(metaEventListeners) {
}
return true;
}
}
synchronized(metaEventListeners) {
if (index >= 0) {
}
}
}
synchronized(controllerEventListeners) {
// first find the listener. if we have one, add the controllers
// if not, create a new element for it.
boolean flag = false;
flag = true;
break;
}
}
if (!flag) {
}
// and return all the controllers this listener is interested in
return cve.getControllers();
}
}
synchronized(controllerEventListeners) {
boolean flag = false;
flag = true;
break;
}
}
if (!flag) {
return new int[0];
}
if (controllers == null) {
if (index >= 0) {
}
return new int[0];
}
return cve.getControllers();
}
}
////////////////// LOOPING (added in 1.5) ///////////////////////
if ((tick > getTickLength())
|| (tick < 0)) {
}
}
public long getLoopStartPoint() {
return loopStart;
}
if ((tick > getTickLength())
|| (tick < -1)) {
}
}
public long getLoopEndPoint() {
return loopEnd;
}
if (count != LOOP_CONTINUOUSLY
&& count < 0) {
}
if (getDataPump() != null) {
}
}
public int getLoopCount() {
return loopCount;
}
/* *********************************** play control ************************* */
/*
*/
//openInternalSynth();
// create PlayThread
playThread = new PlayThread();
//id = nOpen();
//if (id == 0) {
// throw new MidiUnavailableException("unable to open sequencer");
//}
}
// propagate caches
if (doAutoConnectAtNextOpen) {
}
}
private void doAutoConnect() {
// first try to connect to the default synthesizer
// IMPORTANT: this code needs to be synch'ed with
// MidiSystem.getSequencer(boolean), because the same
// algorithm needs to be used!
try {
if (synth instanceof ReferenceCountingDevice) {
} else {
try {
} finally {
// make sure that the synth is properly closed
}
}
}
} catch (Exception e) {
// something went wrong with synth
}
// then try to connect to the default Receiver
try {
} catch (Exception e) {
// something went wrong. Nothing to do then!
}
}
try {
} catch (Exception e) {}
}
}
private synchronized void propagateCaches() {
// only set caches if open and sequence is set
if (cacheTempoFactor != -1) {
}
if (cacheTempoMPQ == -1) {
} else {
setTempoInMPQ((float) cacheTempoMPQ);
}
}
}
/** populate the caches with the current values */
private synchronized void setCaches() {
}
protected synchronized void implClose() {
if (playThread == null) {
if (Printer.err) Printer.err("RealTimeSequencer.implClose() called, but playThread not instanciated!");
} else {
// Interrupt playback loop.
playThread.close();
playThread = null;
}
super.implClose();
running = false;
cacheTempoMPQ = -1;
cacheTempoFactor = -1;
trackMuted = null;
loopStart = 0;
loopEnd = -1;
loopCount = 0;
/** if this sequencer is set to autoconnect, need to
* re-establish the connection at next open!
*/
if (autoConnectedReceiver != null) {
try {
} catch (Exception e) {}
}
}
void implStart() {
if (playThread == null) {
if (Printer.err) Printer.err("RealTimeSequencer.implStart() called, but playThread not instanciated!");
return;
}
if (!running) {
running = true;
playThread.start();
}
}
void implStop() {
if (playThread == null) {
if (Printer.err) Printer.err("RealTimeSequencer.implStop() called, but playThread not instanciated!");
return;
}
recording = false;
if (running) {
running = false;
playThread.stop();
}
}
// create and start the global event thread
//TODO need a way to stop this thread when the engine is done
synchronized (dispatchers) {
if (eventDispatcher == null) {
eventDispatcher = new EventDispatcher();
}
return eventDispatcher;
}
}
/**
* Send midi player events.
* must not be synchronized on "this"
*/
//if (Printer.debug) Printer.debug("sending a meta event");
}
/**
* Send midi player events.
*/
if (size == 0) return;
//if (Printer.debug) Printer.debug("sending a controller event");
if (! (message instanceof ShortMessage)) {
return;
}
for (int i = 0; i < size; i++) {
break;
}
}
}
}
private boolean needCaching() {
}
/**
* return the data pump instance, owned by play thread
* if playthread is null, return null.
* This method is guaranteed to return non-null if
* needCaching returns false
*/
if (playThread != null) {
return playThread.getDataPump();
}
return null;
}
return tempoCache;
}
return new boolean[desiredSize];
}
boolean[] newArray = new boolean[desiredSize];
return newArray;
}
return array;
}
// OVERRIDES OF ABSTRACT MIDI DEVICE METHODS
protected boolean hasReceivers() {
return true;
}
// for recording
return new SequencerReceiver();
}
protected boolean hasTransmitters() {
return true;
}
return new SequencerTransmitter();
}
// interface AutoConnectSequencer
}
// INNER CLASSES
/**
* An own class to distinguish the class name from
* the transmitter of other devices
*/
private SequencerTransmitter() {
super();
}
}
if (recording) {
long tickPos = 0;
// convert timeStamp to ticks
if (timeStamp < 0) {
tickPos = getTickPosition();
} else {
synchronized(tempoCache) {
}
}
// and record to the first matching Track
// do not record real-time events
// see 5048381: NullPointerException when saving a MIDI sequence
if (message instanceof ShortMessage) {
// all real-time messages have 0xF in the high nibble of the status byte
}
} else {
// $$jb: where to record meta, sysex events?
// $$fb: the first recording track
}
// create a copy of this message
if (message instanceof ShortMessage) {
} else {
}
// create new MidiEvent
}
}
}
}
}
private RealTimeSequencerInfo() {
}
} // class Info
private class ControllerListElement {
// $$jb: using an array for controllers b/c its
// easier to deal with than turning all the
// ints into objects to use a Vector
int [] controllers;
if (controllers == null) {
controllers = new int[128];
for (int i = 0; i < 128; i++) {
controllers[i] = i;
}
}
this.controllers = controllers;
}
private void addControllers(int[] c) {
if (c==null) {
controllers = new int[128];
for (int i = 0; i < 128; i++) {
controllers[i] = i;
}
return;
}
int elements;
// first add what we have
temp[i] = controllers[i];
}
// now add the new controllers only if we don't already have them
for(int i=0; i<c.length; i++) {
boolean flag = false;
if (c[i] == controllers[j]) {
flag = true;
break;
}
}
if (!flag) {
}
}
// now keep only the elements we need
for(int i=0; i<elements; i++){
}
controllers = newc;
}
private void removeControllers(int[] c) {
if (c==null) {
controllers = new int[0];
} else {
int elements = 0;
boolean flag = false;
for(int j=0; j<c.length; j++) {
if (controllers[i] == c[j]) {
flag = true;
break;
}
}
if (!flag){
}
}
// now keep only the elements remaining
for(int i=0; i<elements; i++) {
}
controllers = newc;
}
}
private int[] getControllers() {
// return a copy of our array of controllers,
// so others can't mess with it
if (controllers == null) {
return null;
}
int c[] = new int[controllers.length];
c[i] = controllers[i];
}
return c;
}
} // class ControllerListElement
static class RecordingTrack {
private int channel;
}
synchronized(recordingTracks) {
for (int i = 0; i < size; i++) {
return current;
}
}
}
return null;
}
synchronized(recordingTracks) {
for (int i = 0; i < size; i++) {
}
}
}
return null;
}
}
/** true if playback is interrupted (in close) */
boolean interrupted = false;
boolean isPumping = false;
PlayThread() {
// nearly MAX_PRIORITY
"Java Sound Sequencer", // name
false, // daemon
priority, // priority
true); // doStart
}
return dataPump;
}
}
/** start thread and pump. Requires up-to-date tempoCache */
synchronized void start() {
// mark the sequencer running
running = true;
if (!dataPump.hasCachedTempo()) {
long tickPos = getTickPosition();
}
dataPump.needReindex = true;
// notify the thread
synchronized(lock) {
}
}
// waits until stopped
synchronized void stop() {
while (isPumping) {
synchronized(lock) {
try {
} catch (InterruptedException ie) {
// ignore
}
}
// don't wait for more than 2 seconds
//break;
}
}
}
void playThreadImplStop() {
// mark the sequencer running
running = false;
synchronized(lock) {
}
}
void close() {
synchronized (this) {
// dispose of thread
interrupted = true;
}
// wake up the thread if it's in wait()
synchronized(lock) {
}
}
// wait for the thread to terminate itself,
// but max. 2 seconds. Must not be synchronized!
try {
} catch (InterruptedException ie) {}
}
}
/**
* Main process loop driving the media flow.
*
* Make sure to NOT synchronize on RealTimeSequencer
* anywhere here (even implicit). That is a sure deadlock!
*/
public void run() {
while (!interrupted) {
boolean EOM = false;
boolean wasRunning = running;
try {
} catch (InterruptedException ie) {
// ignore
}
}
}
if (wasRunning) {
}
if (EOM) {
// send EOT event (mis-used for end of media)
try{
} catch(InvalidMidiDataException e1) {}
}
synchronized (lock) {
isPumping = false;
// wake up a waiting stop() method
while (!running && !interrupted) {
try {
}
}
} // end of while(!EOM && !interrupted && running)
}
}
/**
* class that does the actual dispatching of events,
* used to be in native in MMAPI
*/
private class DataPump {
private int resolution;
private float divisionType;
private long lastTick;
private boolean needReindex = false;
//private sun.misc.Perf perf = sun.misc.Perf.getPerf();
//private long perfFreq = perf.highResFrequency();
DataPump() {
init();
}
synchronized void init() {
ignoreTempoEventAt = -1;
tempoFactor = 1.0f;
inverseTempoFactor = 1.0f;
noteOnCache = new int[128];
}
long oldLastTick = tickPos;
if (running) {
notesOff(false);
}
// will also reindex
} else {
needReindex = true;
}
if (!hasCachedTempo()) {
// treat this as if it is a real time tempo change
ignoreTempoEventAt = -1;
}
// trigger re-configuration
checkPointMillis = 0;
}
long getTickPos() {
return lastTick;
}
// hasCachedTempo is only valid if it is the current position
boolean hasCachedTempo() {
if (ignoreTempoEventAt != lastTick) {
ignoreTempoEventAt = -1;
}
return ignoreTempoEventAt >= 0;
}
// this method is also used internally in the pump!
// re-calculate check point
checkPointMillis = 0;
}
}
float getTempoMPQ() {
return currTempo;
}
// re-calculate check point
checkPointMillis = 0;
}
}
float getTempoFactor() {
return tempoFactor;
}
synchronized void muteSoloChanged() {
boolean[] newDisabled = makeDisabledArray();
if (running) {
}
}
init();
return;
}
// trigger re-initialization
checkPointMillis = 0;
needReindex = true;
}
synchronized void resetLoopCount() {
}
void clearNoteOnCache() {
for (int i = 0; i < 128; i++) {
noteOnCache[i] = 0;
}
}
int done = 0;
for (int i=0; i<128; i++) {
noteOnCache[i] ^= channelMask;
// send note on with velocity 0
done++;
}
}
/* all notes off */
/* sustain off */
if (doControllers) {
/* reset all controllers */
done++;
}
}
}
private boolean[] makeDisabledArray() {
return null;
}
boolean[] solo;
boolean[] mute;
synchronized(RealTimeSequencer.this) {
mute = trackMuted;
}
// if one track is solo, then only play solo
boolean hasSolo = false;
if (solo[i]) {
hasSolo = true;
break;
}
}
}
if (hasSolo) {
// only the channels with solo play, regardless of mute
}
} else {
// mute the selected channels
}
}
return newTrackDisabled;
}
/**
* chase all events from beginning of Track
* and send note off for those events that are active
* in noteOnCache array.
* It is possible, of course, to catch notes from other tracks,
* but better than more complicated logic to detect
* which notes are really from this track
*/
int done = 0;
try {
for (int i = 0; i < size; i++) {
int note = -1;
if (msg instanceof ShortMessage) {
// only consider Note On with velocity > 0
}
} else {
// only consider Note On with velocity > 0
}
}
if (note >= 0) {
// the bit is set. Send Note Off
// clear the bit
done++;
}
}
}
}
} catch (ArrayIndexOutOfBoundsException aioobe) {
// this happens when messages are removed
// from the track while this method executes
}
}
/**
* if a track is muted that was previously playing, send
* note off events for all currently playing notes
*/
synchronized(RealTimeSequencer.this) {
if (((oldDisabled == null)
|| (i >= oldDisabled.length)
|| !oldDisabled[i])
&& newDisabled[i]) {
// case that a track gets muted: need to
// send appropriate note off events to prevent
// hanging notes
}
}
else if ((oldDisabled != null)
&& (i < oldDisabled.length)
&& oldDisabled[i]
&& !newDisabled[i]) {
// case that a track was muted and is now unmuted
// need to chase events and re-index this track
}
}
}
}
}
/** go through all events from startTick to endTick
* chase the controller state and program change state
* and then set the end-states at once.
*
* needs to be called in synchronized state
* @param tempArray an byte[128][16] to hold controller messages
*/
long startTick,
long endTick,
boolean doReindex,
byte[][] tempArray) {
// start from the beginning
startTick = 0;
}
byte[] progs = new byte[16];
// init temp array with impossible values
}
}
try {
for (int i = 0; i < size; i++) {
if (DEBUG_PUMP) Printer.println(" chaseEvents: setting trackReadPos["+trackNum+"] = "+trackReadPos[trackNum]);
}
break;
}
if (msg instanceof ShortMessage) {
} else {
}
}
if (msg instanceof ShortMessage) {
} else {
}
}
}
} catch (ArrayIndexOutOfBoundsException aioobe) {
// this happens when messages are removed
// from the track while this method executes
}
int numControllersSent = 0;
// now send out the aggregated controllers and program changes
if (controllerValue >= 0) {
}
}
// send program change *after* controllers, to
// correctly initialize banks
}
// reset pitch bend on this channel (E0 00 40)
// reset sustain pedal on this channel
}
}
if (DEBUG_PUMP) Printer.println(" chaseTrackEvents track "+trackNum+": sent "+numControllersSent+" controllers.");
}
/** chase controllers and program for all tracks */
if ((trackDisabled == null)
|| (trackDisabled.length <= t)
|| (!trackDisabled[t])) {
// if track is not disabled, chase the events for it
}
}
}
// playback related methods (pumping)
private long getCurrentTimeMillis() {
//return perf.highResCounter() * 1000 / perfFreq;
}
* ((double) divisionType)
* ((double) resolution))
/ ((double) 1000);
return (long) dTick;
}
}
return (long) dMillis;
}
resolution) / 1000;
}
if (DEBUG_PUMP) Printer.println(" reindexTrack: setting trackReadPos["+trackNum+"] = "+trackReadPos[trackNum]);
}
}
/* returns if changes are pending */
boolean changesPending = false;
// a meta message. Do not send it to the device.
// 0xFF with length=1 is a MIDI realtime message
// which shouldn't be in a Sequence, but we play it
// nonetheless.
// see if this is a tempo message. Only on track 0.
if (trackNum == 0) {
if (newTempo > 0) {
changesPending = true;
}
// next loop, do not ignore anymore tempo events.
ignoreTempoEventAt = -1;
}
}
// send to listeners
} else {
// not meta, send to device
switch (msgStatus & 0xF0) {
case ShortMessage.NOTE_OFF: {
// note off - clear the bit in the noteOnCache array
break;
}
case ShortMessage.NOTE_ON: {
// note on
if (vel > 0) {
// if velocity > 0 set the bit in the noteOnCache array
} else {
// if velocity = 0 clear the bit in the noteOnCache array
}
break;
}
case ShortMessage.CONTROL_CHANGE:
// if controller message, send controller listeners
break;
}
}
return changesPending;
}
/** the main pump method
* @return true if end of sequence is reached
*/
synchronized boolean pump() {
long currMillis;
long targetTick = lastTick;
boolean changesPending = false;
boolean doLoop = false;
boolean EOM = false;
int finishedTracks = 0;
do {
changesPending = false;
// need to re-find indexes in tracks?
if (needReindex) {
if (DEBUG_PUMP) Printer.println("Need to re-index at "+currMillis+" millis. TargetTick="+targetTick);
}
ReindexTrack(t, targetTick);
}
needReindex = false;
checkPointMillis = 0;
}
// get target tick from current time in millis
if (checkPointMillis == 0) {
// new check point
+"TargetTick="+targetTick
} else {
// calculate current tick based on current time in milliseconds
if ((loopEnd != -1)
|| (loopCount == LOOP_CONTINUOUSLY))) {
// need to loop!
// only play until loop end
doLoop = true;
+" targetTick="+targetTick
+" loopEnd="+loopEnd
+" jumping to loopStart="+loopStart
+" new currLoopCounter="+currLoopCounter);
+" checkPointMillis="+checkPointMillis
+" checkPointTick="+checkPointTick);
}
}
}
finishedTracks = 0;
try {
boolean disabled = trackDisabled[t];
int readPos = trackReadPos[t];
// play all events that are due until targetTick
// do not send out this message. Finished with this track
break;
}
// TODO: some kind of heuristics if the MIDI messages have changed
// significantly (i.e. deleted or inserted a bunch of messages)
// since last time. Would need to set needReindex = true then
readPos++;
// only play this event if the track is enabled,
// or if it is a tempo message on track 0
// Note: cannot put this check outside
// this inner loop in order to detect end of file
if (!disabled ||
}
}
}
if (DEBUG_PUMP_ALL) {
+" from index: "+trackReadPos[t]
if (trackReadPos[t] < size) {
} else {
}
} else {
}
}
trackReadPos[t] = readPos;
} catch(Exception e) {
if (e instanceof ArrayIndexOutOfBoundsException) {
needReindex = true;
changesPending = true;
}
}
if (changesPending) {
break;
}
}
if (doLoop
|| (loopCount == LOOP_CONTINUOUSLY))
&& !changesPending
&& (loopEnd == -1)
&& EOM)) {
long oldCheckPointMillis = checkPointMillis;
long loopEndTick = loopEnd;
if (loopEndTick == -1) {
}
// need to loop back!
if (loopCount != LOOP_CONTINUOUSLY) {
}
+" loopEnd="+loopEnd
+" jumping to loopStart="+loopStart
+" new currLoopCounter="+currLoopCounter);
// now patch the checkPointMillis so that
// it points to the exact beginning of when the loop was finished
// $$fb TODO: although this is mathematically correct (i.e. the loop position
// is correct, and doesn't drift away with several repetition,
// there is a slight lag when looping back, probably caused
// by the chasing.
+" new checkPointMillis="+checkPointMillis
+" new checkPointTick="+checkPointTick);
// no need for reindexing, is done in setTickPos
needReindex = false;
changesPending = false;
// reset doLoop flag
doLoop = false;
EOM = false;
}
} while (changesPending);
return EOM;
}
} // class DataPump
}