--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/com/sun/media/sound/RealTimeSequencer.java Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,2406 @@
+/*
+ * Copyright 2003-2007 Sun Microsystems, Inc. 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. Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package com.sun.media.sound;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.sound.midi.*;
+
+
+/**
+ * A Real Time Sequencer
+ *
+ * @author Florian Bomers
+ */
+
+/* TODO:
+ * - rename PlayThread to PlayEngine (because isn't a thread)
+ */
+class RealTimeSequencer extends AbstractMidiDevice implements Sequencer, AutoConnectSequencer {
+
+ // STATIC VARIABLES
+
+ /** debugging flags */
+ private final static boolean DEBUG_PUMP = false;
+ private final static boolean DEBUG_PUMP_ALL = false;
+
+
+ /** if true, we bridge RMF files over to the old MixerSequencer */
+ private final static boolean RMF = true;
+
+ /**
+ * Event Dispatcher thread. Should be using a shared event
+ * dispatcher instance with a factory in EventDispatcher
+ */
+ private static final EventDispatcher eventDispatcher;
+
+ /**
+ * All RealTimeSequencers share this info object.
+ */
+ static final RealTimeSequencerInfo info = new RealTimeSequencerInfo();
+
+
+ private static Sequencer.SyncMode[] masterSyncModes = { Sequencer.SyncMode.INTERNAL_CLOCK };
+ private static Sequencer.SyncMode[] slaveSyncModes = { Sequencer.SyncMode.NO_SYNC };
+
+ private static Sequencer.SyncMode masterSyncMode = Sequencer.SyncMode.INTERNAL_CLOCK;
+ private static Sequencer.SyncMode slaveSyncMode = Sequencer.SyncMode.NO_SYNC;
+
+
+ /**
+ * Sequence on which this sequencer is operating.
+ */
+ private Sequence sequence = null;
+
+ // caches
+
+ /**
+ * Same for setTempoInMPQ...
+ * -1 means not set.
+ */
+ private double cacheTempoMPQ = -1;
+
+
+ /**
+ * cache value for tempo factor until sequence is set
+ * -1 means not set.
+ */
+ private float cacheTempoFactor = -1;
+
+
+ /** if a particular track is muted */
+ private boolean[] trackMuted = null;
+ /** if a particular track is solo */
+ private boolean[] trackSolo = null;
+
+ /** tempo cache for getMicrosecondPosition */
+ private MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache();
+
+ /**
+ * True if the sequence is running.
+ */
+ private boolean running = false;
+
+
+ /** the thread for pushing out the MIDI messages */
+ private PlayThread playThread;
+
+
+ /**
+ * True if we are recording
+ */
+ private boolean recording = false;
+
+
+ /**
+ * List of tracks to which we're recording
+ */
+ private List recordingTracks = new ArrayList();
+
+
+ private long loopStart = 0;
+ private long loopEnd = -1;
+ private int loopCount = 0;
+
+
+ /**
+ * Meta event listeners
+ */
+ private ArrayList metaEventListeners = new ArrayList();
+
+
+ /**
+ * Control change listeners
+ */
+ private ArrayList controllerEventListeners = new ArrayList();
+
+
+ /** for RMF media we need the RMF sequencer */
+ private MixerSequencer seqBridge = null;
+
+ /** 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 */
+ Receiver autoConnectedReceiver = null;
+
+
+ static {
+ // create and start the global event thread
+ eventDispatcher = new EventDispatcher();
+ eventDispatcher.start();
+ }
+
+
+ /* ****************************** CONSTRUCTOR ****************************** */
+
+ protected 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 (RMF) {
+ if (seqBridge != null) {
+ seqBridge.close();
+ seqBridge = null;
+ }
+ // if previous file was an RMF, but this file is not RMF,
+ // then need to call implOpen again!
+ if (isOpen() && sequence != null && playThread == null) {
+ try {
+ implOpen();
+ } catch (MidiUnavailableException mue) {
+ if (Printer.err) mue.printStackTrace();
+ }
+ }
+ }
+
+ 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;
+ }
+
+ // need to be able to detect RMF
+ if (RMF) {
+ MidiFileFormat fileFormat = MidiSystem.getMidiFileFormat(stream); // can throw IOException, InvalidMidiDataException
+ int type = fileFormat.getType();
+ int resolution = fileFormat.getResolution();
+ if (Printer.debug) Printer.debug("Got file with type="+type+" and resolution="+resolution);
+ if (resolution == MidiFileFormat.UNKNOWN_LENGTH) {
+ // seems to be RMF
+ if (seqBridge == null) {
+ try {
+ seqBridge = new MixerSequencer();
+ if (isOpen()) {
+ seqBridge.open();
+ }
+ } catch (MidiUnavailableException mue) {
+ // uhum, strange situation. Need to cast to InvalidMidiDataException
+ throw new InvalidMidiDataException(mue.getMessage());
+ }
+ }
+ seqBridge.setSequence(stream);
+ // propagate state
+ seqBridge.setTempoFactor(getTempoFactor());
+
+ // propagate listeners
+ synchronized(metaEventListeners) {
+ for (int i = 0 ; i < metaEventListeners.size(); i++) {
+ seqBridge.addMetaEventListener((MetaEventListener) (metaEventListeners.get(i)));
+ }
+ }
+ synchronized(controllerEventListeners) {
+ for (int i = 0 ; i < controllerEventListeners.size(); i++) {
+ ControllerListElement cve = (ControllerListElement) (controllerEventListeners.get(i));
+ seqBridge.addControllerEventListener(cve.listener, cve.controllers);
+ }
+ }
+ // disable the current sequence of RealTimeSequencer
+ //setSequence((Sequence) null); -> will remove bridge again!
+ this.sequence = null;
+ return;
+ }
+ if (seqBridge != null) {
+ seqBridge.close();
+ seqBridge = null;
+ }
+ }
+
+ Sequence seq = MidiSystem.getSequence(stream); // can throw IOException, InvalidMidiDataException
+
+ setSequence(seq);
+
+ if (Printer.trace) Printer.trace("<< RealTimeSequencer: setSequence(" + stream +") completed");
+
+ }
+
+
+ public Sequence getSequence() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getSequence();
+ }
+ }
+ return sequence;
+ }
+
+
+ public synchronized void start() {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.start();
+ return;
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ seqBridge.stop();
+ return;
+ }
+ }
+ 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() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.isRunning();
+ }
+ }
+ return running;
+ }
+
+
+ public void startRecording() {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.startRecording();
+ return;
+ }
+ }
+
+ if (!isOpen()) {
+ throw new IllegalStateException("Sequencer not open");
+ }
+
+ start();
+ recording = true;
+ }
+
+
+ public void stopRecording() {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.stopRecording();
+ return;
+ }
+ }
+
+ if (!isOpen()) {
+ throw new IllegalStateException("Sequencer not open");
+ }
+ recording = false;
+ }
+
+
+ public boolean isRecording() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.isRecording();
+ }
+ }
+ return recording;
+ }
+
+
+ public void recordEnable(Track track, int channel) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.recordEnable(track, channel);
+ return;
+ }
+ }
+
+ 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) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.recordDisable(track);
+ return;
+ }
+ }
+
+ 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 (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getTempoInBPM();
+ }
+ }
+ if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoInBPM() ");
+
+ return (float) MidiUtils.convertTempo(getTempoInMPQ());
+ }
+
+
+ public void setTempoInBPM(float bpm) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setTempoInBPM(bpm);
+ return;
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.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 (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setTempoInMPQ(mpq);
+ return;
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setTempoFactor(factor);
+ return;
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.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 (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getTickLength();
+ }
+ }
+ if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTickLength() ");
+
+ if (sequence == null) {
+ return 0;
+ }
+
+ return sequence.getTickLength();
+ }
+
+
+ public synchronized long getTickPosition() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.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 (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setTickPosition(tick);
+ return;
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getMicrosecondLength();
+ }
+ }
+
+ if (Printer.trace) Printer.trace(">> RealTimeSequencer: getMicrosecondLength() ");
+
+ if (sequence == null) {
+ return 0;
+ }
+
+ return sequence.getMicrosecondLength();
+ }
+
+
+ public long getMicrosecondPosition() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.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 (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setMicrosecondPosition(microseconds);
+ return;
+ }
+ }
+
+ 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) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setMasterSyncMode(sync);
+ return;
+ }
+ }
+ // not supported
+ }
+
+
+ public Sequencer.SyncMode getMasterSyncMode() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getMasterSyncMode();
+ }
+ }
+ return masterSyncMode;
+ }
+
+
+ public Sequencer.SyncMode[] getMasterSyncModes() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.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) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setSlaveSyncMode(sync);
+ return;
+ }
+ }
+ // not supported
+ }
+
+
+ public Sequencer.SyncMode getSlaveSyncMode() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getSlaveSyncMode();
+ }
+ }
+ return slaveSyncMode;
+ }
+
+
+ public Sequencer.SyncMode[] getSlaveSyncModes() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getSlaveSyncModes();
+ }
+ }
+
+ Sequencer.SyncMode[] returnedModes = new Sequencer.SyncMode[slaveSyncModes.length];
+ System.arraycopy(slaveSyncModes, 0, returnedModes, 0, slaveSyncModes.length);
+ return returnedModes;
+ }
+
+ protected 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) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setTrackMute(track, mute);
+ return;
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getTrackMute(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) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setTrackSolo(track, solo);
+ return;
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getTrackSolo(track);
+ }
+ }
+ if (track < 0 || track >= getTrackCount()) return false;
+ if (trackSolo == null || trackSolo.length <= track) return false;
+ return trackSolo[track];
+ }
+
+
+ public boolean addMetaEventListener(MetaEventListener listener) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.addMetaEventListener(listener);
+ // do not return here!
+ }
+ }
+ synchronized(metaEventListeners) {
+ if (! metaEventListeners.contains(listener)) {
+
+ metaEventListeners.add(listener);
+ }
+ return true;
+ }
+ }
+
+
+ public void removeMetaEventListener(MetaEventListener listener) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.removeMetaEventListener(listener);
+ // do not return here!
+ }
+ }
+ synchronized(metaEventListeners) {
+ int index = metaEventListeners.indexOf(listener);
+ if (index >= 0) {
+ metaEventListeners.remove(index);
+ }
+ }
+ }
+
+
+ public int[] addControllerEventListener(ControllerEventListener listener, int[] controllers) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.addControllerEventListener(listener, controllers);
+ // do not return here!
+ }
+ }
+
+ 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) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.removeControllerEventListener(listener, controllers);
+ // do not return here!
+ }
+ }
+ 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 (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setLoopStartPoint(tick);
+ return;
+ }
+ }
+ if ((tick > getTickLength())
+ || ((loopEnd != -1) && (tick > loopEnd))
+ || (tick < 0)) {
+ throw new IllegalArgumentException("invalid loop start point: "+tick);
+ }
+ loopStart = tick;
+ }
+
+ public long getLoopStartPoint() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getLoopStartPoint();
+ }
+ }
+ return loopStart;
+ }
+
+ public void setLoopEndPoint(long tick) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setLoopEndPoint(tick);
+ return;
+ }
+ }
+ if ((tick > getTickLength())
+ || ((loopStart > tick) && (tick != -1))
+ || (tick < -1)) {
+ throw new IllegalArgumentException("invalid loop end point: "+tick);
+ }
+ loopEnd = tick;
+ }
+
+ public long getLoopEndPoint() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getLoopEndPoint();
+ }
+ }
+ return loopEnd;
+ }
+
+ public void setLoopCount(int count) {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.setLoopCount(count);
+ return;
+ }
+ }
+ 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() {
+ if (RMF) {
+ if (seqBridge != null) {
+ return seqBridge.getLoopCount();
+ }
+ }
+ return loopCount;
+ }
+
+
+ /* *********************************** play control ************************* */
+
+ /*
+ */
+ protected void implOpen() throws MidiUnavailableException {
+ if (Printer.trace) Printer.trace(">> RealTimeSequencer: implOpen()");
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.open();
+ if (Printer.trace) Printer.trace("<< RealTimeSequencer: -> called seqBridge.open");
+ return;
+ }
+ }
+
+ //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.getReceiver(boolean), because the same
+ // algorithm needs to be used!
+ try {
+ Synthesizer synth = MidiSystem.getSynthesizer();
+ synth.open();
+ if (synth instanceof ReferenceCountingDevice) {
+ rec = ((ReferenceCountingDevice) synth).getReceiverReferenceCounting();
+ if (synth.getClass().toString().contains("com.sun.media.sound.MixerSynth")
+ && (synth.getDefaultSoundbank() == null)) {
+ // don't use this receiver if no soundbank available
+ rec = null;
+ synth.close();
+ }
+ } else {
+ rec = synth.getReceiver();
+ }
+ } 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 */
+ private synchronized void setCaches() {
+ cacheTempoFactor = getTempoFactor();
+ cacheTempoMPQ = getTempoInMPQ();
+ }
+
+
+
+ protected synchronized void implClose() {
+ if (RMF) {
+ if (seqBridge != null) {
+ seqBridge.close();
+ // don't return here!
+ }
+ }
+ 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");
+ }
+
+ protected 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");
+ }
+
+
+ protected 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");
+ }
+
+
+ /**
+ * Send midi player events.
+ * must not be synchronized on "this"
+ */
+ protected void sendMetaEvents(MidiMessage message) {
+ if (metaEventListeners.size() == 0) return;
+
+ //if (Printer.debug) Printer.debug("sending a meta event");
+ eventDispatcher.sendAudioEvents(message, metaEventListeners);
+ }
+
+ /**
+ * Send midi player events.
+ */
+ protected 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;
+ }
+ }
+ }
+ eventDispatcher.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
+ */
+ 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() {
+ if (RMF) {
+ if (seqBridge != null) {
+ //RMF does not allow recording
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // for recording
+ protected Receiver createReceiver() throws MidiUnavailableException {
+ return new SequencerReceiver();
+ }
+
+
+ protected boolean hasTransmitters() {
+ if (RMF) {
+ if (seqBridge != null) {
+ //RMF does never allow setting own receivers
+ return false;
+ }
+ }
+ 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
+ */
+ private class SequencerTransmitter extends BasicTransmitter {
+ private SequencerTransmitter() {
+ super();
+ }
+ }
+
+
+ class SequencerReceiver extends AbstractReceiver {
+
+ protected 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 = "Sun Microsystems";
+ 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;
+ 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 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;
+
+ }
+ }
+
+
+ class PlayThread implements Runnable {
+ private Thread thread;
+ private Object lock = new Object();
+
+ /** true if playback is interrupted (in close) */
+ boolean interrupted = false;
+ boolean isPumping = false;
+
+ private 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 */
+ 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!
+ */
+ 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
+ */
+ 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
+ */
+ 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
+ */
+ 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
+ * @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 */
+ 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
+ * @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
+
+}