/*
 * Copyright (c) 2003, 2016, Oracle and/or its affiliates. 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.  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.
 */

package com.sun.media.sound;

import java.io.IOException;
import java.io.InputStream;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import javax.sound.midi.*;


A Real Time Sequencer
Author:Florian Bomers
/** * A Real Time Sequencer * * @author Florian Bomers */
/* TODO: * - rename PlayThread to PlayEngine (because isn't a thread) */ final class RealTimeSequencer extends AbstractMidiDevice implements Sequencer, AutoConnectSequencer { // STATIC VARIABLES
debugging flags
/** 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
/** * Event Dispatcher thread. Should be using a shared event * dispatcher instance with a factory in EventDispatcher */
private static final Map<ThreadGroup, EventDispatcher> dispatchers = new WeakHashMap<>();
All RealTimeSequencers share this info object.
/** * All RealTimeSequencers share this info object. */
static final RealTimeSequencerInfo info = new RealTimeSequencerInfo(); private static final Sequencer.SyncMode[] masterSyncModes = { Sequencer.SyncMode.INTERNAL_CLOCK }; private static final Sequencer.SyncMode[] slaveSyncModes = { Sequencer.SyncMode.NO_SYNC }; private static final Sequencer.SyncMode masterSyncMode = Sequencer.SyncMode.INTERNAL_CLOCK; private static final Sequencer.SyncMode slaveSyncMode = Sequencer.SyncMode.NO_SYNC;
Sequence on which this sequencer is operating.
/** * Sequence on which this sequencer is operating. */
private Sequence sequence = null; // caches
Same for setTempoInMPQ... -1 means not set.
/** * Same for setTempoInMPQ... * -1 means not set. */
private double cacheTempoMPQ = -1;
cache value for tempo factor until sequence is set -1 means not set.
/** * cache value for tempo factor until sequence is set * -1 means not set. */
private float cacheTempoFactor = -1;
if a particular track is muted
/** if a particular track is muted */
private boolean[] trackMuted = null;
if a particular track is solo
/** if a particular track is solo */
private boolean[] trackSolo = null;
tempo cache for getMicrosecondPosition
/** tempo cache for getMicrosecondPosition */
private final MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache();
True if the sequence is running.
/** * True if the sequence is running. */
private volatile boolean running;
the thread for pushing out the MIDI messages
/** the thread for pushing out the MIDI messages */
private PlayThread playThread;
True if we are recording
/** * True if we are recording */
private volatile boolean recording;
List of tracks to which we're recording
/** * List of tracks to which we're recording */
private final List recordingTracks = new ArrayList(); private long loopStart = 0; private long loopEnd = -1; private int loopCount = 0;
Meta event listeners
/** * Meta event listeners */
private final ArrayList metaEventListeners = new ArrayList();
Control change listeners
/** * Control change listeners */
private final ArrayList controllerEventListeners = new ArrayList();
automatic connection support
/** automatic connection support */
private boolean autoConnect = false;
if we need to autoconnect at next open
/** if we need to autoconnect at next open */
private boolean doAutoConnectAtNextOpen = false;
the receiver that this device is auto-connected to
/** the receiver that this device is auto-connected to */
Receiver autoConnectedReceiver = null; /* ****************************** CONSTRUCTOR ****************************** */ RealTimeSequencer() throws MidiUnavailableException { super(info); if (Printer.trace) Printer.trace(">> RealTimeSequencer CONSTRUCTOR"); if (Printer.trace) Printer.trace("<< RealTimeSequencer CONSTRUCTOR completed"); } /* ****************************** SEQUENCER METHODS ******************** */ public synchronized void setSequence(Sequence sequence) throws InvalidMidiDataException { if (Printer.trace) Printer.trace(">> RealTimeSequencer: setSequence(" + sequence +")"); if (sequence != this.sequence) { if (this.sequence != null && sequence == null) { setCaches(); stop(); // initialize some non-cached values trackMuted = null; trackSolo = null; loopStart = 0; loopEnd = -1; loopCount = 0; if (getDataPump() != null) { getDataPump().setTickPos(0); getDataPump().resetLoopCount(); } } if (playThread != null) { playThread.setSequence(sequence); } // store this sequence (do not copy - we want to give the possibility // of modifying the sequence at runtime) this.sequence = sequence; if (sequence != null) { tempoCache.refresh(sequence); // rewind to the beginning setTickPosition(0); // propagate caches propagateCaches(); } } else if (sequence != null) { tempoCache.refresh(sequence); if (playThread != null) { playThread.setSequence(sequence); } } if (Printer.trace) Printer.trace("<< RealTimeSequencer: setSequence(" + sequence +") completed"); } public synchronized void setSequence(InputStream stream) throws IOException, InvalidMidiDataException { if (Printer.trace) Printer.trace(">> RealTimeSequencer: setSequence(" + stream +")"); if (stream == null) { setSequence((Sequence) null); return; } Sequence seq = MidiSystem.getSequence(stream); // can throw IOException, InvalidMidiDataException setSequence(seq); if (Printer.trace) Printer.trace("<< RealTimeSequencer: setSequence(" + stream +") completed"); } public Sequence getSequence() { return sequence; } public synchronized void start() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: start()"); // sequencer not open: throw an exception if (!isOpen()) { throw new IllegalStateException("sequencer not open"); } // sequence not available: throw an exception if (sequence == null) { throw new IllegalStateException("sequence not set"); } // already running: return quietly if (running == true) { return; } // start playback implStart(); if (Printer.trace) Printer.trace("<< RealTimeSequencer: start() completed"); } public synchronized void stop() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: stop()"); if (!isOpen()) { throw new IllegalStateException("sequencer not open"); } stopRecording(); // not running; just return if (running == false) { if (Printer.trace) Printer.trace("<< RealTimeSequencer: stop() not running!"); return; } // stop playback implStop(); if (Printer.trace) Printer.trace("<< RealTimeSequencer: stop() completed"); } 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; } public void recordEnable(Track track, int channel) { if (!findTrack(track)) { throw new IllegalArgumentException("Track does not exist in the current sequence"); } synchronized(recordingTracks) { RecordingTrack rc = RecordingTrack.get(recordingTracks, track); if (rc != null) { rc.channel = channel; } else { recordingTracks.add(new RecordingTrack(track, channel)); } } } public void recordDisable(Track track) { synchronized(recordingTracks) { RecordingTrack rc = RecordingTrack.get(recordingTracks, track); if (rc != null) { recordingTracks.remove(rc); } } } private boolean findTrack(Track track) { boolean found = false; if (sequence != null) { Track[] tracks = sequence.getTracks(); for (int i = 0; i < tracks.length; i++) { if (track == tracks[i]) { found = true; break; } } } return found; } public float getTempoInBPM() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoInBPM() "); return (float) MidiUtils.convertTempo(getTempoInMPQ()); } public void setTempoInBPM(float bpm) { if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoInBPM() "); if (bpm <= 0) { // should throw IllegalArgumentException bpm = 1.0f; } setTempoInMPQ((float) MidiUtils.convertTempo((double) bpm)); } public float getTempoInMPQ() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoInMPQ() "); if (needCaching()) { // if the sequencer is closed, return cached value if (cacheTempoMPQ != -1) { return (float) cacheTempoMPQ; } // if sequence is set, return current tempo if (sequence != null) { return tempoCache.getTempoMPQAt(getTickPosition()); } // last resort: return a standard tempo: 120bpm return (float) MidiUtils.DEFAULT_TEMPO_MPQ; } return (float)getDataPump().getTempoMPQ(); } public void setTempoInMPQ(float mpq) { if (mpq <= 0) { // should throw IllegalArgumentException mpq = 1.0f; } if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoInMPQ() "); if (needCaching()) { // cache the value cacheTempoMPQ = mpq; } else { // set the native tempo in MPQ getDataPump().setTempoMPQ(mpq); // reset the tempoInBPM and tempoInMPQ values so we won't use them again cacheTempoMPQ = -1; } } public void setTempoFactor(float factor) { if (factor <= 0) { // should throw IllegalArgumentException return; } if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoFactor() "); if (needCaching()) { cacheTempoFactor = factor; } else { getDataPump().setTempoFactor(factor); // don't need cache anymore cacheTempoFactor = -1; } } public float getTempoFactor() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoFactor() "); if (needCaching()) { if (cacheTempoFactor != -1) { return cacheTempoFactor; } return 1.0f; } return getDataPump().getTempoFactor(); } public long getTickLength() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTickLength() "); if (sequence == null) { return 0; } return sequence.getTickLength(); } public synchronized long getTickPosition() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTickPosition() "); if (getDataPump() == null || sequence == null) { return 0; } return getDataPump().getTickPos(); } public synchronized void setTickPosition(long tick) { if (tick < 0) { // should throw IllegalArgumentException return; } if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTickPosition("+tick+") "); if (getDataPump() == null) { if (tick != 0) { // throw new InvalidStateException("cannot set position in closed state"); } } else if (sequence == null) { if (tick != 0) { // throw new InvalidStateException("cannot set position if sequence is not set"); } } else { getDataPump().setTickPos(tick); } } public long getMicrosecondLength() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getMicrosecondLength() "); if (sequence == null) { return 0; } return sequence.getMicrosecondLength(); } public long getMicrosecondPosition() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getMicrosecondPosition() "); if (getDataPump() == null || sequence == null) { return 0; } synchronized (tempoCache) { return MidiUtils.tick2microsecond(sequence, getDataPump().getTickPos(), tempoCache); } } public void setMicrosecondPosition(long microseconds) { 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"); } } else if (sequence == null) { if (microseconds != 0) { // throw new InvalidStateException("cannot set position if sequence is not set"); } } else { synchronized(tempoCache) { setTickPosition(MidiUtils.microsecond2tick(sequence, microseconds, tempoCache)); } } } public void setMasterSyncMode(Sequencer.SyncMode sync) { // not supported } public Sequencer.SyncMode getMasterSyncMode() { return masterSyncMode; } public Sequencer.SyncMode[] getMasterSyncModes() { Sequencer.SyncMode[] returnedModes = new Sequencer.SyncMode[masterSyncModes.length]; System.arraycopy(masterSyncModes, 0, returnedModes, 0, masterSyncModes.length); return returnedModes; } public void setSlaveSyncMode(Sequencer.SyncMode sync) { // not supported } public Sequencer.SyncMode getSlaveSyncMode() { return slaveSyncMode; } public Sequencer.SyncMode[] getSlaveSyncModes() { Sequencer.SyncMode[] returnedModes = new Sequencer.SyncMode[slaveSyncModes.length]; System.arraycopy(slaveSyncModes, 0, returnedModes, 0, slaveSyncModes.length); return returnedModes; } int getTrackCount() { Sequence seq = getSequence(); if (seq != null) { // $$fb wish there was a nicer way to get the number of tracks... return sequence.getTracks().length; } return 0; } public synchronized void setTrackMute(int track, boolean mute) { int trackCount = getTrackCount(); if (track < 0 || track >= getTrackCount()) return; trackMuted = ensureBoolArraySize(trackMuted, trackCount); trackMuted[track] = mute; if (getDataPump() != null) { getDataPump().muteSoloChanged(); } } public synchronized boolean getTrackMute(int track) { if (track < 0 || track >= getTrackCount()) return false; if (trackMuted == null || trackMuted.length <= track) return false; return trackMuted[track]; } public synchronized void setTrackSolo(int track, boolean solo) { int trackCount = getTrackCount(); if (track < 0 || track >= getTrackCount()) return; trackSolo = ensureBoolArraySize(trackSolo, trackCount); trackSolo[track] = solo; if (getDataPump() != null) { getDataPump().muteSoloChanged(); } } public synchronized boolean getTrackSolo(int track) { if (track < 0 || track >= getTrackCount()) return false; if (trackSolo == null || trackSolo.length <= track) return false; return trackSolo[track]; } public boolean addMetaEventListener(MetaEventListener listener) { synchronized(metaEventListeners) { if (! metaEventListeners.contains(listener)) { metaEventListeners.add(listener); } return true; } } public void removeMetaEventListener(MetaEventListener listener) { synchronized(metaEventListeners) { int index = metaEventListeners.indexOf(listener); if (index >= 0) { metaEventListeners.remove(index); } } } public int[] addControllerEventListener(ControllerEventListener listener, int[] controllers) { synchronized(controllerEventListeners) { // first find the listener. if we have one, add the controllers // if not, create a new element for it. ControllerListElement cve = null; boolean flag = false; for(int i=0; i < controllerEventListeners.size(); i++) { cve = (ControllerListElement) controllerEventListeners.get(i); if (cve.listener.equals(listener)) { cve.addControllers(controllers); flag = true; break; } } if (!flag) { cve = new ControllerListElement(listener, controllers); controllerEventListeners.add(cve); } // and return all the controllers this listener is interested in return cve.getControllers(); } } public int[] removeControllerEventListener(ControllerEventListener listener, int[] controllers) { synchronized(controllerEventListeners) { ControllerListElement cve = null; boolean flag = false; for (int i=0; i < controllerEventListeners.size(); i++) { cve = (ControllerListElement) controllerEventListeners.get(i); if (cve.listener.equals(listener)) { cve.removeControllers(controllers); flag = true; break; } } if (!flag) { return new int[0]; } if (controllers == null) { int index = controllerEventListeners.indexOf(cve); if (index >= 0) { controllerEventListeners.remove(index); } return new int[0]; } return cve.getControllers(); } } ////////////////// LOOPING (added in 1.5) /////////////////////// public void setLoopStartPoint(long tick) { if ((tick > getTickLength()) || ((loopEnd != -1) && (tick > loopEnd)) || (tick < 0)) { throw new IllegalArgumentException("invalid loop start point: "+tick); } loopStart = tick; } public long getLoopStartPoint() { return loopStart; } public void setLoopEndPoint(long tick) { if ((tick > getTickLength()) || ((loopStart > tick) && (tick != -1)) || (tick < -1)) { throw new IllegalArgumentException("invalid loop end point: "+tick); } loopEnd = tick; } public long getLoopEndPoint() { return loopEnd; } public void setLoopCount(int count) { if (count != LOOP_CONTINUOUSLY && count < 0) { throw new IllegalArgumentException("illegal value for loop count: "+count); } loopCount = count; if (getDataPump() != null) { getDataPump().resetLoopCount(); } } public int getLoopCount() { return loopCount; } /* *********************************** play control ************************* */ /* */ protected void implOpen() throws MidiUnavailableException { if (Printer.trace) Printer.trace(">> RealTimeSequencer: implOpen()"); //openInternalSynth(); // create PlayThread playThread = new PlayThread(); //id = nOpen(); //if (id == 0) { // throw new MidiUnavailableException("unable to open sequencer"); //} if (sequence != null) { playThread.setSequence(sequence); } // propagate caches propagateCaches(); if (doAutoConnectAtNextOpen) { doAutoConnect(); } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implOpen() succeeded"); } private void doAutoConnect() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: doAutoConnect()"); Receiver rec = null; // 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 { Synthesizer synth = MidiSystem.getSynthesizer(); if (synth instanceof ReferenceCountingDevice) { rec = ((ReferenceCountingDevice) synth).getReceiverReferenceCounting(); } else { synth.open(); try { rec = synth.getReceiver(); } finally { // make sure that the synth is properly closed if (rec == null) { synth.close(); } } } } catch (Exception e) { // something went wrong with synth } if (rec == null) { // then try to connect to the default Receiver try { rec = MidiSystem.getReceiver(); } catch (Exception e) { // something went wrong. Nothing to do then! } } if (rec != null) { autoConnectedReceiver = rec; try { getTransmitter().setReceiver(rec); } catch (Exception e) {} } if (Printer.trace) Printer.trace("<< RealTimeSequencer: doAutoConnect() succeeded"); } private synchronized void propagateCaches() { // only set caches if open and sequence is set if (sequence != null && isOpen()) { if (cacheTempoFactor != -1) { setTempoFactor(cacheTempoFactor); } if (cacheTempoMPQ == -1) { setTempoInMPQ((new MidiUtils.TempoCache(sequence)).getTempoMPQAt(getTickPosition())); } else { setTempoInMPQ((float) cacheTempoMPQ); } } }
populate the caches with the current values
/** populate the caches with the current values */
private synchronized void setCaches() { cacheTempoFactor = getTempoFactor(); cacheTempoMPQ = getTempoInMPQ(); } protected synchronized void implClose() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: 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(); sequence = null; running = false; cacheTempoMPQ = -1; cacheTempoFactor = -1; trackMuted = null; trackSolo = null; loopStart = 0; loopEnd = -1; loopCount = 0; /** if this sequencer is set to autoconnect, need to * re-establish the connection at next open! */ doAutoConnectAtNextOpen = autoConnect; if (autoConnectedReceiver != null) { try { autoConnectedReceiver.close(); } catch (Exception e) {} autoConnectedReceiver = null; } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implClose() completed"); } void implStart() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: implStart()"); if (playThread == null) { if (Printer.err) Printer.err("RealTimeSequencer.implStart() called, but playThread not instanciated!"); return; } tempoCache.refresh(sequence); if (!running) { running = true; playThread.start(); } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implStart() completed"); } void implStop() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: 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(); } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implStop() completed"); } private static EventDispatcher getEventDispatcher() { // create and start the global event thread //TODO need a way to stop this thread when the engine is done final ThreadGroup tg = Thread.currentThread().getThreadGroup(); synchronized (dispatchers) { EventDispatcher eventDispatcher = dispatchers.get(tg); if (eventDispatcher == null) { eventDispatcher = new EventDispatcher(); dispatchers.put(tg, eventDispatcher); eventDispatcher.start(); } return eventDispatcher; } }
Send midi player events. must not be synchronized on "this"
/** * Send midi player events. * must not be synchronized on "this" */
void sendMetaEvents(MidiMessage message) { if (metaEventListeners.size() == 0) return; //if (Printer.debug) Printer.debug("sending a meta event"); getEventDispatcher().sendAudioEvents(message, metaEventListeners); }
Send midi player events.
/** * Send midi player events. */
void sendControllerEvents(MidiMessage message) { int size = controllerEventListeners.size(); if (size == 0) return; //if (Printer.debug) Printer.debug("sending a controller event"); if (! (message instanceof ShortMessage)) { if (Printer.debug) Printer.debug("sendControllerEvents: message is NOT instanceof ShortMessage!"); return; } ShortMessage msg = (ShortMessage) message; int controller = msg.getData1(); List sendToListeners = new ArrayList(); for (int i = 0; i < size; i++) { ControllerListElement cve = (ControllerListElement) controllerEventListeners.get(i); for(int j = 0; j < cve.controllers.length; j++) { if (cve.controllers[j] == controller) { sendToListeners.add(cve.listener); break; } } } getEventDispatcher().sendAudioEvents(message, sendToListeners); } private boolean needCaching() { return !isOpen() || (sequence == null) || (playThread == null); }
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
/** * 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 */
private DataPump getDataPump() { if (playThread != null) { return playThread.getDataPump(); } return null; } private MidiUtils.TempoCache getTempoCache() { return tempoCache; } private static boolean[] ensureBoolArraySize(boolean[] array, int desiredSize) { if (array == null) { return new boolean[desiredSize]; } if (array.length < desiredSize) { boolean[] newArray = new boolean[desiredSize]; System.arraycopy(array, 0, newArray, 0, array.length); return newArray; } return array; } // OVERRIDES OF ABSTRACT MIDI DEVICE METHODS protected boolean hasReceivers() { return true; } // for recording protected Receiver createReceiver() throws MidiUnavailableException { return new SequencerReceiver(); } protected boolean hasTransmitters() { return true; } protected Transmitter createTransmitter() throws MidiUnavailableException { return new SequencerTransmitter(); } // interface AutoConnectSequencer public void setAutoConnect(Receiver autoConnectedReceiver) { this.autoConnect = (autoConnectedReceiver != null); this.autoConnectedReceiver = autoConnectedReceiver; } // INNER CLASSES
An own class to distinguish the class name from the transmitter of other devices
/** * An own class to distinguish the class name from * the transmitter of other devices */
private class SequencerTransmitter extends BasicTransmitter { private SequencerTransmitter() { super(); } } final class SequencerReceiver extends AbstractReceiver { void implSend(MidiMessage message, long timeStamp) { if (recording) { long tickPos = 0; // convert timeStamp to ticks if (timeStamp < 0) { tickPos = getTickPosition(); } else { synchronized(tempoCache) { tickPos = MidiUtils.microsecond2tick(sequence, timeStamp, tempoCache); } } // and record to the first matching Track Track track = null; // do not record real-time events // see 5048381: NullPointerException when saving a MIDI sequence if (message.getLength() > 1) { if (message instanceof ShortMessage) { ShortMessage sm = (ShortMessage) message; // all real-time messages have 0xF in the high nibble of the status byte if ((sm.getStatus() & 0xF0) != 0xF0) { track = RecordingTrack.get(recordingTracks, sm.getChannel()); } } else { // $$jb: where to record meta, sysex events? // $$fb: the first recording track track = RecordingTrack.get(recordingTracks, -1); } if (track != null) { // create a copy of this message if (message instanceof ShortMessage) { message = new FastShortMessage((ShortMessage) message); } else { message = (MidiMessage) message.clone(); } // create new MidiEvent MidiEvent me = new MidiEvent(message, tickPos); track.add(me); } } } } } private static class RealTimeSequencerInfo extends MidiDevice.Info { private static final String name = "Real Time Sequencer"; private static final String vendor = "Oracle Corporation"; private static final String description = "Software sequencer"; private static final String version = "Version 1.0"; private RealTimeSequencerInfo() { super(name, vendor, description, version); } } // 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; final ControllerEventListener listener; private ControllerListElement(ControllerEventListener listener, int[] controllers) { this.listener = listener; 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 temp[] = new int[ controllers.length + c.length ]; int elements; // first add what we have for(int i=0; i<controllers.length; i++) { temp[i] = controllers[i]; } elements = controllers.length; // now add the new controllers only if we don't already have them for(int i=0; i<c.length; i++) { boolean flag = false; for(int j=0; j<controllers.length; j++) { if (c[i] == controllers[j]) { flag = true; break; } } if (!flag) { temp[elements++] = c[i]; } } // now keep only the elements we need int newc[] = new int[ elements ]; for(int i=0; i<elements; i++){ newc[i] = temp[i]; } controllers = newc; } private void removeControllers(int[] c) { if (c==null) { controllers = new int[0]; } else { int temp[] = new int[ controllers.length ]; int elements = 0; for(int i=0; i<controllers.length; i++){ boolean flag = false; for(int j=0; j<c.length; j++) { if (controllers[i] == c[j]) { flag = true; break; } } if (!flag){ temp[elements++] = controllers[i]; } } // now keep only the elements remaining int newc[] = new int[ elements ]; for(int i=0; i<elements; i++) { newc[i] = temp[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]; for(int i=0; i<controllers.length; i++){ c[i] = controllers[i]; } return c; } } // class ControllerListElement static class RecordingTrack { private final Track track; private int channel; RecordingTrack(Track track, int channel) { this.track = track; this.channel = channel; } static RecordingTrack get(List recordingTracks, Track track) { synchronized(recordingTracks) { int size = recordingTracks.size(); for (int i = 0; i < size; i++) { RecordingTrack current = (RecordingTrack)recordingTracks.get(i); if (current.track == track) { return current; } } } return null; } static Track get(List recordingTracks, int channel) { synchronized(recordingTracks) { int size = recordingTracks.size(); for (int i = 0; i < size; i++) { RecordingTrack current = (RecordingTrack)recordingTracks.get(i); if ((current.channel == channel) || (current.channel == -1)) { return current.track; } } } return null; } } final class PlayThread implements Runnable { private Thread thread; private final Object lock = new Object();
true if playback is interrupted (in close)
/** true if playback is interrupted (in close) */
boolean interrupted = false; boolean isPumping = false; private final DataPump dataPump = new DataPump(); PlayThread() { // nearly MAX_PRIORITY int priority = Thread.NORM_PRIORITY + ((Thread.MAX_PRIORITY - Thread.NORM_PRIORITY) * 3) / 4; thread = JSSecurityManager.createThread(this, "Java Sound Sequencer", // name false, // daemon priority, // priority true); // doStart } DataPump getDataPump() { return dataPump; } synchronized void setSequence(Sequence seq) { dataPump.setSequence(seq); }
start thread and pump. Requires up-to-date tempoCache
/** 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.setTempoMPQ(tempoCache.getTempoMPQAt(tickPos)); } dataPump.checkPointMillis = 0; // means restarted dataPump.clearNoteOnCache(); dataPump.needReindex = true; dataPump.resetLoopCount(); // notify the thread synchronized(lock) { lock.notifyAll(); } if (Printer.debug) Printer.debug(" ->Started MIDI play thread"); } // waits until stopped synchronized void stop() { playThreadImplStop(); long t = System.nanoTime() / 1000000l; while (isPumping) { synchronized(lock) { try { lock.wait(2000); } catch (InterruptedException ie) { // ignore } } // don't wait for more than 2 seconds if ((System.nanoTime()/1000000l) - t > 1900) { if (Printer.err) Printer.err("Waited more than 2 seconds in RealTimeSequencer.PlayThread.stop()!"); //break; } } } void playThreadImplStop() { // mark the sequencer running running = false; synchronized(lock) { lock.notifyAll(); } } void close() { Thread oldThread = null; synchronized (this) { // dispose of thread interrupted = true; oldThread = thread; thread = null; } if (oldThread != null) { // wake up the thread if it's in wait() synchronized(lock) { lock.notifyAll(); } } // wait for the thread to terminate itself, // but max. 2 seconds. Must not be synchronized! if (oldThread != null) { try { oldThread.join(2000); } 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!
/** * 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; isPumping = !interrupted && running; while (!EOM && !interrupted && running) { EOM = dataPump.pump(); try { Thread.sleep(1); } catch (InterruptedException ie) { // ignore } } if (Printer.debug) { Printer.debug("Exited main pump loop because: "); if (EOM) Printer.debug(" -> EOM is reached"); if (!running) Printer.debug(" -> running was set to false"); if (interrupted) Printer.debug(" -> interrupted was set to true"); } playThreadImplStop(); if (wasRunning) { dataPump.notesOff(true); } if (EOM) { dataPump.setTickPos(sequence.getTickLength()); // send EOT event (mis-used for end of media) MetaMessage message = new MetaMessage(); try{ message.setMessage(MidiUtils.META_END_OF_TRACK_TYPE, new byte[0], 0); } catch(InvalidMidiDataException e1) {} sendMetaEvents(message); } synchronized (lock) { isPumping = false; // wake up a waiting stop() method lock.notifyAll(); while (!running && !interrupted) { try { lock.wait(); } catch (Exception ex) {} } } } // end of while(!EOM && !interrupted && running) if (Printer.debug) Printer.debug("end of play thread"); } }
class that does the actual dispatching of events, used to be in native in MMAPI
/** * class that does the actual dispatching of events, * used to be in native in MMAPI */
private class DataPump { private float currTempo; // MPQ tempo private float tempoFactor; // 1.0 is default private float inverseTempoFactor;// = 1.0 / tempoFactor private long ignoreTempoEventAt; // ignore next META tempo during playback at this tick pos only private int resolution; private float divisionType; private long checkPointMillis; // microseconds at checkoint private long checkPointTick; // ticks at checkpoint private int[] noteOnCache; // bit-mask of notes that are currently on private Track[] tracks; private boolean[] trackDisabled; // if true, do not play this track private int[] trackReadPos; // read index per track private long lastTick; private boolean needReindex = false; private int currLoopCounter = 0; //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]; tracks = null; trackDisabled = null; } synchronized void setTickPos(long tickPos) { long oldLastTick = tickPos; lastTick = tickPos; if (running) { notesOff(false); } if (running || tickPos > 0) { // will also reindex chaseEvents(oldLastTick, tickPos); } else { needReindex = true; } if (!hasCachedTempo()) { setTempoMPQ(getTempoCache().getTempoMPQAt(lastTick, currTempo)); // 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! synchronized void setTempoMPQ(float tempoMPQ) { if (tempoMPQ > 0 && tempoMPQ != currTempo) { ignoreTempoEventAt = lastTick; this.currTempo = tempoMPQ; // re-calculate check point checkPointMillis = 0; } } float getTempoMPQ() { return currTempo; } synchronized void setTempoFactor(float factor) { if (factor > 0 && factor != this.tempoFactor) { tempoFactor = factor; inverseTempoFactor = 1.0f / factor; // re-calculate check point checkPointMillis = 0; } } float getTempoFactor() { return tempoFactor; } synchronized void muteSoloChanged() { boolean[] newDisabled = makeDisabledArray(); if (running) { applyDisabledTracks(trackDisabled, newDisabled); } trackDisabled = newDisabled; } synchronized void setSequence(Sequence seq) { if (seq == null) { init(); return; } tracks = seq.getTracks(); muteSoloChanged(); resolution = seq.getResolution(); divisionType = seq.getDivisionType(); trackReadPos = new int[tracks.length]; // trigger re-initialization checkPointMillis = 0; needReindex = true; } synchronized void resetLoopCount() { currLoopCounter = loopCount; } void clearNoteOnCache() { for (int i = 0; i < 128; i++) { noteOnCache[i] = 0; } } void notesOff(boolean doControllers) { int done = 0; for (int ch=0; ch<16; ch++) { int channelMask = (1<<ch); for (int i=0; i<128; i++) { if ((noteOnCache[i] & channelMask) != 0) { noteOnCache[i] ^= channelMask; // send note on with velocity 0 getTransmitterList().sendMessage((ShortMessage.NOTE_ON | ch) | (i<<8), -1); done++; } } /* all notes off */ getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (123<<8), -1); /* sustain off */ getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (64<<8), -1); if (doControllers) { /* reset all controllers */ getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (121<<8), -1); done++; } } if (DEBUG_PUMP) Printer.println(" noteOff: sent "+done+" messages."); } private boolean[] makeDisabledArray() { if (tracks == null) { return null; } boolean[] newTrackDisabled = new boolean[tracks.length]; boolean[] solo; boolean[] mute; synchronized(RealTimeSequencer.this) { mute = trackMuted; solo = trackSolo; } // if one track is solo, then only play solo boolean hasSolo = false; if (solo != null) { for (int i = 0; i < solo.length; i++) { if (solo[i]) { hasSolo = true; break; } } } if (hasSolo) { // only the channels with solo play, regardless of mute for (int i = 0; i < newTrackDisabled.length; i++) { newTrackDisabled[i] = (i >= solo.length) || (!solo[i]); } } else { // mute the selected channels for (int i = 0; i < newTrackDisabled.length; i++) { newTrackDisabled[i] = (mute != null) && (i < mute.length) && (mute[i]); } } 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
/** * 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 */
private void sendNoteOffIfOn(Track track, long endTick) { int size = track.size(); int done = 0; try { for (int i = 0; i < size; i++) { MidiEvent event = track.get(i); if (event.getTick() > endTick) break; MidiMessage msg = event.getMessage(); int status = msg.getStatus(); int len = msg.getLength(); if (len == 3 && ((status & 0xF0) == ShortMessage.NOTE_ON)) { int note = -1; if (msg instanceof ShortMessage) { ShortMessage smsg = (ShortMessage) msg; if (smsg.getData2() > 0) { // only consider Note On with velocity > 0 note = smsg.getData1(); } } else { byte[] data = msg.getMessage(); if ((data[2] & 0x7F) > 0) { // only consider Note On with velocity > 0 note = data[1] & 0x7F; } } if (note >= 0) { int bit = 1<<(status & 0x0F); if ((noteOnCache[note] & bit) != 0) { // the bit is set. Send Note Off getTransmitterList().sendMessage(status | (note<<8), -1); // clear the bit noteOnCache[note] &= (0xFFFF ^ bit); done++; } } } } } catch (ArrayIndexOutOfBoundsException aioobe) { // this happens when messages are removed // from the track while this method executes } if (DEBUG_PUMP) Printer.println(" sendNoteOffIfOn: sent "+done+" messages."); }
Runtime application of mute/solo: if a track is muted that was previously playing, send note off events for all currently playing notes
/** * Runtime application of mute/solo: * if a track is muted that was previously playing, send * note off events for all currently playing notes */
private void applyDisabledTracks(boolean[] oldDisabled, boolean[] newDisabled) { byte[][] tempArray = null; synchronized(RealTimeSequencer.this) { for (int i = 0; i < newDisabled.length; i++) { 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 if (tracks.length > i) { sendNoteOffIfOn(tracks[i], lastTick); } } 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 if (tempArray == null) { tempArray = new byte[128][16]; } chaseTrackEvents(i, 0, lastTick, true, tempArray); } } } }
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
Params:
  • tempArray – an byte[128][16] to hold controller messages
/** 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 */
private void chaseTrackEvents(int trackNum, long startTick, long endTick, boolean doReindex, byte[][] tempArray) { if (startTick > endTick) { // start from the beginning startTick = 0; } byte[] progs = new byte[16]; // init temp array with impossible values for (int ch = 0; ch < 16; ch++) { progs[ch] = -1; for (int co = 0; co < 128; co++) { tempArray[co][ch] = -1; } } Track track = tracks[trackNum]; int size = track.size(); try { for (int i = 0; i < size; i++) { MidiEvent event = track.get(i); if (event.getTick() >= endTick) { if (doReindex && (trackNum < trackReadPos.length)) { trackReadPos[trackNum] = (i > 0)?(i-1):0; if (DEBUG_PUMP) Printer.println(" chaseEvents: setting trackReadPos["+trackNum+"] = "+trackReadPos[trackNum]); } break; } MidiMessage msg = event.getMessage(); int status = msg.getStatus(); int len = msg.getLength(); if (len == 3 && ((status & 0xF0) == ShortMessage.CONTROL_CHANGE)) { if (msg instanceof ShortMessage) { ShortMessage smsg = (ShortMessage) msg; tempArray[smsg.getData1() & 0x7F][status & 0x0F] = (byte) smsg.getData2(); } else { byte[] data = msg.getMessage(); tempArray[data[1] & 0x7F][status & 0x0F] = data[2]; } } if (len == 2 && ((status & 0xF0) == ShortMessage.PROGRAM_CHANGE)) { if (msg instanceof ShortMessage) { ShortMessage smsg = (ShortMessage) msg; progs[status & 0x0F] = (byte) smsg.getData1(); } else { byte[] data = msg.getMessage(); progs[status & 0x0F] = data[1]; } } } } 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 for (int ch = 0; ch < 16; ch++) { for (int co = 0; co < 128; co++) { byte controllerValue = tempArray[co][ch]; if (controllerValue >= 0) { int packedMsg = (ShortMessage.CONTROL_CHANGE | ch) | (co<<8) | (controllerValue<<16); getTransmitterList().sendMessage(packedMsg, -1); numControllersSent++; } } // send program change *after* controllers, to // correctly initialize banks if (progs[ch] >= 0) { getTransmitterList().sendMessage((ShortMessage.PROGRAM_CHANGE | ch) | (progs[ch]<<8), -1); } if (progs[ch] >= 0 || startTick == 0 || endTick == 0) { // reset pitch bend on this channel (E0 00 40) getTransmitterList().sendMessage((ShortMessage.PITCH_BEND | ch) | (0x40 << 16), -1); // reset sustain pedal on this channel getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (64 << 8), -1); } } if (DEBUG_PUMP) Printer.println(" chaseTrackEvents track "+trackNum+": sent "+numControllersSent+" controllers."); }
chase controllers and program for all tracks
/** chase controllers and program for all tracks */
synchronized void chaseEvents(long startTick, long endTick) { if (DEBUG_PUMP) Printer.println(">> chaseEvents from tick "+startTick+".."+(endTick-1)); byte[][] tempArray = new byte[128][16]; for (int t = 0; t < tracks.length; t++) { if ((trackDisabled == null) || (trackDisabled.length <= t) || (!trackDisabled[t])) { // if track is not disabled, chase the events for it chaseTrackEvents(t, startTick, endTick, true, tempArray); } } if (DEBUG_PUMP) Printer.println("<< chaseEvents"); } // playback related methods (pumping) private long getCurrentTimeMillis() { return System.nanoTime() / 1000000l; //return perf.highResCounter() * 1000 / perfFreq; } private long millis2tick(long millis) { if (divisionType != Sequence.PPQ) { double dTick = ((((double) millis) * tempoFactor) * ((double) divisionType) * ((double) resolution)) / ((double) 1000); return (long) dTick; } return MidiUtils.microsec2ticks(millis * 1000, currTempo * inverseTempoFactor, resolution); } private long tick2millis(long tick) { if (divisionType != Sequence.PPQ) { double dMillis = ((((double) tick) * 1000) / (tempoFactor * ((double) divisionType) * ((double) resolution))); return (long) dMillis; } return MidiUtils.ticks2microsec(tick, currTempo * inverseTempoFactor, resolution) / 1000; } private void ReindexTrack(int trackNum, long tick) { if (trackNum < trackReadPos.length && trackNum < tracks.length) { trackReadPos[trackNum] = MidiUtils.tick2index(tracks[trackNum], tick); if (DEBUG_PUMP) Printer.println(" reindexTrack: setting trackReadPos["+trackNum+"] = "+trackReadPos[trackNum]); } } /* returns if changes are pending */ private boolean dispatchMessage(int trackNum, MidiEvent event) { boolean changesPending = false; MidiMessage message = event.getMessage(); int msgStatus = message.getStatus(); int msgLen = message.getLength(); if (msgStatus == MetaMessage.META && msgLen >= 2) { // 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) { int newTempo = MidiUtils.getTempoMPQ(message); if (newTempo > 0) { if (event.getTick() != ignoreTempoEventAt) { setTempoMPQ(newTempo); // sets ignoreTempoEventAt! changesPending = true; } // next loop, do not ignore anymore tempo events. ignoreTempoEventAt = -1; } } // send to listeners sendMetaEvents(message); } else { // not meta, send to device getTransmitterList().sendMessage(message, -1); switch (msgStatus & 0xF0) { case ShortMessage.NOTE_OFF: { // note off - clear the bit in the noteOnCache array int note = ((ShortMessage) message).getData1() & 0x7F; noteOnCache[note] &= (0xFFFF ^ (1<<(msgStatus & 0x0F))); break; } case ShortMessage.NOTE_ON: { // note on ShortMessage smsg = (ShortMessage) message; int note = smsg.getData1() & 0x7F; int vel = smsg.getData2() & 0x7F; if (vel > 0) { // if velocity > 0 set the bit in the noteOnCache array noteOnCache[note] |= 1<<(msgStatus & 0x0F); } else { // if velocity = 0 clear the bit in the noteOnCache array noteOnCache[note] &= (0xFFFF ^ (1<<(msgStatus & 0x0F))); } break; } case ShortMessage.CONTROL_CHANGE: // if controller message, send controller listeners sendControllerEvents(message); break; } } return changesPending; }
the main pump method
Returns:true if end of sequence is reached
/** the main pump method * @return true if end of sequence is reached */
synchronized boolean pump() { long currMillis; long targetTick = lastTick; MidiEvent currEvent; boolean changesPending = false; boolean doLoop = false; boolean EOM = false; currMillis = getCurrentTimeMillis(); 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); if (trackReadPos.length < tracks.length) { trackReadPos = new int[tracks.length]; } for (int t = 0; t < tracks.length; t++) { ReindexTrack(t, targetTick); if (DEBUG_PUMP_ALL) Printer.println(" Setting trackReadPos["+t+"]="+trackReadPos[t]); } needReindex = false; checkPointMillis = 0; } // get target tick from current time in millis if (checkPointMillis == 0) { // new check point currMillis = getCurrentTimeMillis(); checkPointMillis = currMillis; targetTick = lastTick; checkPointTick = targetTick; if (DEBUG_PUMP) Printer.println("New checkpoint to "+currMillis+" millis. " +"TargetTick="+targetTick +" new tempo="+MidiUtils.convertTempo(currTempo)+"bpm"); } else { // calculate current tick based on current time in milliseconds targetTick = checkPointTick + millis2tick(currMillis - checkPointMillis); if (DEBUG_PUMP_ALL) Printer.println("targetTick = "+targetTick+" at "+currMillis+" millis"); if ((loopEnd != -1) && ((loopCount > 0 && currLoopCounter > 0) || (loopCount == LOOP_CONTINUOUSLY))) { if (lastTick <= loopEnd && targetTick >= loopEnd) { // need to loop! // only play until loop end targetTick = loopEnd - 1; doLoop = true; if (DEBUG_PUMP) Printer.println("set doLoop to true. lastTick="+lastTick +" targetTick="+targetTick +" loopEnd="+loopEnd +" jumping to loopStart="+loopStart +" new currLoopCounter="+currLoopCounter); if (DEBUG_PUMP) Printer.println(" currMillis="+currMillis +" checkPointMillis="+checkPointMillis +" checkPointTick="+checkPointTick); } } lastTick = targetTick; } finishedTracks = 0; for (int t = 0; t < tracks.length; t++) { try { boolean disabled = trackDisabled[t]; Track thisTrack = tracks[t]; int readPos = trackReadPos[t]; int size = thisTrack.size(); // play all events that are due until targetTick while (!changesPending && (readPos < size) && (currEvent = thisTrack.get(readPos)).getTick() <= targetTick) { if ((readPos == size -1) && MidiUtils.isMetaEndOfTrack(currEvent.getMessage())) { // do not send out this message. Finished with this track readPos = size; 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 || ((t == 0) && (MidiUtils.isMetaTempo(currEvent.getMessage())))) { changesPending = dispatchMessage(t, currEvent); } } if (readPos >= size) { finishedTracks++; } if (DEBUG_PUMP_ALL) { System.out.print(" pumped track "+t+" ("+size+" events) " +" from index: "+trackReadPos[t] +" to "+(readPos-1)); System.out.print(" -> ticks: "); if (trackReadPos[t] < size) { System.out.print(""+(thisTrack.get(trackReadPos[t]).getTick())); } else { System.out.print("EOT"); } System.out.print(" to "); if (readPos < size) { System.out.print(""+(thisTrack.get(readPos-1).getTick())); } else { System.out.print("EOT"); } System.out.println(); } trackReadPos[t] = readPos; } catch(Exception e) { if (Printer.debug) Printer.debug("Exception in Sequencer pump!"); if (Printer.debug) e.printStackTrace(); if (e instanceof ArrayIndexOutOfBoundsException) { needReindex = true; changesPending = true; } } if (changesPending) { break; } } EOM = (finishedTracks == tracks.length); if (doLoop || ( ((loopCount > 0 && currLoopCounter > 0) || (loopCount == LOOP_CONTINUOUSLY)) && !changesPending && (loopEnd == -1) && EOM)) { long oldCheckPointMillis = checkPointMillis; long loopEndTick = loopEnd; if (loopEndTick == -1) { loopEndTick = lastTick; } // need to loop back! if (loopCount != LOOP_CONTINUOUSLY) { currLoopCounter--; } if (DEBUG_PUMP) Printer.println("Execute loop: lastTick="+lastTick +" loopEnd="+loopEnd +" jumping to loopStart="+loopStart +" new currLoopCounter="+currLoopCounter); setTickPos(loopStart); // 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. checkPointMillis = oldCheckPointMillis + tick2millis(loopEndTick - checkPointTick); checkPointTick = loopStart; if (DEBUG_PUMP) Printer.println(" Setting currMillis="+currMillis +" 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 }