|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
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 |
|
*/ |
|
|
|
|
|
|
|
*/ |
|
final class RealTimeSequencer extends AbstractMidiDevice |
|
implements Sequencer, AutoConnectSequencer { |
|
|
|
// STATIC VARIABLES |
|
|
|
|
|
private final static boolean DEBUG_PUMP = false; |
|
private final static boolean DEBUG_PUMP_ALL = false; |
|
|
|
|
|
|
|
|
|
*/ |
|
private static final Map<ThreadGroup, EventDispatcher> dispatchers = |
|
new WeakHashMap<>(); |
|
|
|
|
|
|
|
*/ |
|
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; |
|
|
|
|
|
|
|
|
|
*/ |
|
private Sequence sequence = null; |
|
|
|
// caches |
|
|
|
|
|
|
|
|
|
*/ |
|
private double cacheTempoMPQ = -1; |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private float cacheTempoFactor = -1; |
|
|
|
|
|
|
|
private boolean[] trackMuted = null; |
|
|
|
private boolean[] trackSolo = null; |
|
|
|
|
|
private final MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache(); |
|
|
|
|
|
|
|
*/ |
|
private volatile boolean running; |
|
|
|
|
|
|
|
private PlayThread playThread; |
|
|
|
|
|
|
|
|
|
*/ |
|
private volatile boolean recording; |
|
|
|
|
|
|
|
|
|
*/ |
|
private final List recordingTracks = new ArrayList(); |
|
|
|
|
|
private long loopStart = 0; |
|
private long loopEnd = -1; |
|
private int loopCount = 0; |
|
|
|
|
|
|
|
|
|
*/ |
|
private final ArrayList metaEventListeners = new ArrayList(); |
|
|
|
|
|
|
|
|
|
*/ |
|
private final ArrayList controllerEventListeners = new ArrayList(); |
|
|
|
|
|
|
|
private boolean autoConnect = false; |
|
|
|
|
|
private boolean doAutoConnectAtNextOpen = false; |
|
|
|
|
|
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(); |
|
|
|
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 |
|
|
|
this.sequence = sequence; |
|
|
|
if (sequence != null) { |
|
tempoCache.refresh(sequence); |
|
|
|
setTickPosition(0); |
|
|
|
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); |
|
|
|
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()"); |
|
|
|
|
|
if (!isOpen()) { |
|
throw new IllegalStateException("sequencer not open"); |
|
} |
|
|
|
|
|
if (sequence == null) { |
|
throw new IllegalStateException("sequence not set"); |
|
} |
|
|
|
|
|
if (running == true) { |
|
return; |
|
} |
|
|
|
|
|
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(); |
|
|
|
|
|
if (running == false) { |
|
if (Printer.trace) Printer.trace("<< RealTimeSequencer: stop() not running!"); |
|
return; |
|
} |
|
|
|
|
|
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) { |
|
|
|
bpm = 1.0f; |
|
} |
|
|
|
setTempoInMPQ((float) MidiUtils.convertTempo((double) bpm)); |
|
} |
|
|
|
|
|
public float getTempoInMPQ() { |
|
if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoInMPQ() "); |
|
|
|
if (needCaching()) { |
|
|
|
if (cacheTempoMPQ != -1) { |
|
return (float) cacheTempoMPQ; |
|
} |
|
|
|
if (sequence != null) { |
|
return tempoCache.getTempoMPQAt(getTickPosition()); |
|
} |
|
|
|
|
|
return (float) MidiUtils.DEFAULT_TEMPO_MPQ; |
|
} |
|
return (float)getDataPump().getTempoMPQ(); |
|
} |
|
|
|
|
|
public void setTempoInMPQ(float mpq) { |
|
if (mpq <= 0) { |
|
|
|
mpq = 1.0f; |
|
} |
|
|
|
if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoInMPQ() "); |
|
|
|
if (needCaching()) { |
|
|
|
cacheTempoMPQ = mpq; |
|
} else { |
|
|
|
getDataPump().setTempoMPQ(mpq); |
|
|
|
|
|
cacheTempoMPQ = -1; |
|
} |
|
} |
|
|
|
|
|
public void setTempoFactor(float factor) { |
|
if (factor <= 0) { |
|
|
|
return; |
|
} |
|
|
|
if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoFactor() "); |
|
|
|
if (needCaching()) { |
|
cacheTempoFactor = factor; |
|
} else { |
|
getDataPump().setTempoFactor(factor); |
|
|
|
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) { |
|
|
|
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) { |
|
|
|
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) { |
|
|
|
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 |
|
|
|
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); |
|
} |
|
|
|
|
|
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(); |
|
|
|
|
|
playThread = new PlayThread(); |
|
|
|
//id = nOpen(); |
|
//if (id == 0) { |
|
// throw new MidiUnavailableException("unable to open sequencer"); |
|
|
|
if (sequence != null) { |
|
playThread.setSequence(sequence); |
|
} |
|
|
|
|
|
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 |
|
|
|
try { |
|
Synthesizer synth = MidiSystem.getSynthesizer(); |
|
if (synth instanceof ReferenceCountingDevice) { |
|
rec = ((ReferenceCountingDevice) synth).getReceiverReferenceCounting(); |
|
} else { |
|
synth.open(); |
|
try { |
|
rec = synth.getReceiver(); |
|
} finally { |
|
|
|
if (rec == null) { |
|
synth.close(); |
|
} |
|
} |
|
} |
|
} catch (Exception e) { |
|
// something went wrong with synth |
|
} |
|
if (rec == null) { |
|
|
|
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() { |
|
|
|
if (sequence != null && isOpen()) { |
|
if (cacheTempoFactor != -1) { |
|
setTempoFactor(cacheTempoFactor); |
|
} |
|
if (cacheTempoMPQ == -1) { |
|
setTempoInMPQ((new MidiUtils.TempoCache(sequence)).getTempoMPQAt(getTickPosition())); |
|
} else { |
|
setTempoInMPQ((float) cacheTempoMPQ); |
|
} |
|
} |
|
} |
|
|
|
|
|
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 { |
|
|
|
playThread.close(); |
|
playThread = null; |
|
} |
|
|
|
super.implClose(); |
|
|
|
sequence = null; |
|
running = false; |
|
cacheTempoMPQ = -1; |
|
cacheTempoFactor = -1; |
|
trackMuted = null; |
|
trackSolo = null; |
|
loopStart = 0; |
|
loopEnd = -1; |
|
loopCount = 0; |
|
|
|
|
|
|
|
*/ |
|
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 |
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
void sendMetaEvents(MidiMessage message) { |
|
if (metaEventListeners.size() == 0) return; |
|
|
|
|
|
getEventDispatcher().sendAudioEvents(message, metaEventListeners); |
|
} |
|
|
|
|
|
|
|
*/ |
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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; |
|
} |
|
|
|
|
|
protected Receiver createReceiver() throws MidiUnavailableException { |
|
return new SequencerReceiver(); |
|
} |
|
|
|
|
|
protected boolean hasTransmitters() { |
|
return true; |
|
} |
|
|
|
|
|
protected Transmitter createTransmitter() throws MidiUnavailableException { |
|
return new SequencerTransmitter(); |
|
} |
|
|
|
|
|
|
|
public void setAutoConnect(Receiver autoConnectedReceiver) { |
|
this.autoConnect = (autoConnectedReceiver != null); |
|
this.autoConnectedReceiver = autoConnectedReceiver; |
|
} |
|
|
|
|
|
|
|
// INNER CLASSES |
|
|
|
|
|
|
|
|
|
*/ |
|
private class SequencerTransmitter extends BasicTransmitter { |
|
private SequencerTransmitter() { |
|
super(); |
|
} |
|
} |
|
|
|
|
|
final class SequencerReceiver extends AbstractReceiver { |
|
|
|
void implSend(MidiMessage message, long timeStamp) { |
|
if (recording) { |
|
long tickPos = 0; |
|
|
|
|
|
if (timeStamp < 0) { |
|
tickPos = getTickPosition(); |
|
} else { |
|
synchronized(tempoCache) { |
|
tickPos = MidiUtils.microsecond2tick(sequence, timeStamp, tempoCache); |
|
} |
|
} |
|
|
|
|
|
Track track = null; |
|
// do not record real-time events |
|
|
|
if (message.getLength() > 1) { |
|
if (message instanceof ShortMessage) { |
|
ShortMessage sm = (ShortMessage) message; |
|
|
|
if ((sm.getStatus() & 0xF0) != 0xF0) { |
|
track = RecordingTrack.get(recordingTracks, sm.getChannel()); |
|
} |
|
} else { |
|
// $$jb: where to record meta, sysex events? |
|
|
|
track = RecordingTrack.get(recordingTracks, -1); |
|
} |
|
if (track != null) { |
|
|
|
if (message instanceof ShortMessage) { |
|
message = new FastShortMessage((ShortMessage) message); |
|
} else { |
|
message = (MidiMessage) message.clone(); |
|
} |
|
|
|
|
|
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 |
|
|
|
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; |
|
|
|
|
|
for(int i=0; i<controllers.length; i++) { |
|
temp[i] = controllers[i]; |
|
} |
|
elements = controllers.length; |
|
|
|
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]; |
|
} |
|
} |
|
|
|
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]; |
|
} |
|
} |
|
|
|
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, |
|
|
|
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(); |
|
|
|
|
|
boolean interrupted = false; |
|
boolean isPumping = false; |
|
|
|
private final DataPump dataPump = new DataPump(); |
|
|
|
|
|
PlayThread() { |
|
|
|
int priority = Thread.NORM_PRIORITY |
|
+ ((Thread.MAX_PRIORITY - Thread.NORM_PRIORITY) * 3) / 4; |
|
thread = JSSecurityManager.createThread(this, |
|
"Java Sound Sequencer", |
|
false, |
|
priority, |
|
true); |
|
} |
|
|
|
DataPump getDataPump() { |
|
return dataPump; |
|
} |
|
|
|
synchronized void setSequence(Sequence seq) { |
|
dataPump.setSequence(seq); |
|
} |
|
|
|
|
|
|
|
synchronized void start() { |
|
|
|
running = true; |
|
|
|
if (!dataPump.hasCachedTempo()) { |
|
long tickPos = getTickPosition(); |
|
dataPump.setTempoMPQ(tempoCache.getTempoMPQAt(tickPos)); |
|
} |
|
dataPump.checkPointMillis = 0; |
|
dataPump.clearNoteOnCache(); |
|
dataPump.needReindex = true; |
|
|
|
dataPump.resetLoopCount(); |
|
|
|
|
|
synchronized(lock) { |
|
lock.notifyAll(); |
|
} |
|
|
|
if (Printer.debug) Printer.debug(" ->Started MIDI play thread"); |
|
|
|
} |
|
|
|
|
|
synchronized void stop() { |
|
playThreadImplStop(); |
|
long t = System.nanoTime() / 1000000l; |
|
while (isPumping) { |
|
synchronized(lock) { |
|
try { |
|
lock.wait(2000); |
|
} catch (InterruptedException ie) { |
|
// ignore |
|
} |
|
} |
|
|
|
if ((System.nanoTime()/1000000l) - t > 1900) { |
|
if (Printer.err) Printer.err("Waited more than 2 seconds in RealTimeSequencer.PlayThread.stop()!"); |
|
//break; |
|
} |
|
} |
|
} |
|
|
|
void playThreadImplStop() { |
|
|
|
running = false; |
|
synchronized(lock) { |
|
lock.notifyAll(); |
|
} |
|
} |
|
|
|
void close() { |
|
Thread oldThread = null; |
|
synchronized (this) { |
|
|
|
interrupted = true; |
|
oldThread = thread; |
|
thread = null; |
|
} |
|
if (oldThread != null) { |
|
|
|
synchronized(lock) { |
|
lock.notifyAll(); |
|
} |
|
} |
|
// wait for the thread to terminate itself, |
|
|
|
if (oldThread != null) { |
|
try { |
|
oldThread.join(2000); |
|
} catch (InterruptedException ie) {} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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()); |
|
|
|
|
|
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; |
|
|
|
lock.notifyAll(); |
|
while (!running && !interrupted) { |
|
try { |
|
lock.wait(); |
|
} catch (Exception ex) {} |
|
} |
|
} |
|
} |
|
if (Printer.debug) Printer.debug("end of play thread"); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private class DataPump { |
|
private float currTempo; |
|
private float tempoFactor; |
|
private float inverseTempoFactor; |
|
private long ignoreTempoEventAt; |
|
private int resolution; |
|
private float divisionType; |
|
private long checkPointMillis; |
|
private long checkPointTick; |
|
private int[] noteOnCache; |
|
private Track[] tracks; |
|
private boolean[] trackDisabled; |
|
private int[] trackReadPos; |
|
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) { |
|
|
|
chaseEvents(oldLastTick, tickPos); |
|
} else { |
|
needReindex = true; |
|
} |
|
if (!hasCachedTempo()) { |
|
setTempoMPQ(getTempoCache().getTempoMPQAt(lastTick, currTempo)); |
|
|
|
ignoreTempoEventAt = -1; |
|
} |
|
|
|
checkPointMillis = 0; |
|
} |
|
|
|
long getTickPos() { |
|
return lastTick; |
|
} |
|
|
|
|
|
boolean hasCachedTempo() { |
|
if (ignoreTempoEventAt != lastTick) { |
|
ignoreTempoEventAt = -1; |
|
} |
|
return ignoreTempoEventAt >= 0; |
|
} |
|
|
|
|
|
synchronized void setTempoMPQ(float tempoMPQ) { |
|
if (tempoMPQ > 0 && tempoMPQ != currTempo) { |
|
ignoreTempoEventAt = lastTick; |
|
this.currTempo = tempoMPQ; |
|
|
|
checkPointMillis = 0; |
|
} |
|
} |
|
|
|
float getTempoMPQ() { |
|
return currTempo; |
|
} |
|
|
|
synchronized void setTempoFactor(float factor) { |
|
if (factor > 0 && factor != this.tempoFactor) { |
|
tempoFactor = factor; |
|
inverseTempoFactor = 1.0f / factor; |
|
|
|
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]; |
|
|
|
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; |
|
|
|
getTransmitterList().sendMessage((ShortMessage.NOTE_ON | ch) | (i<<8), -1); |
|
done++; |
|
} |
|
} |
|
|
|
getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (123<<8), -1); |
|
|
|
getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (64<<8), -1); |
|
if (doControllers) { |
|
|
|
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; |
|
} |
|
|
|
boolean hasSolo = false; |
|
if (solo != null) { |
|
for (int i = 0; i < solo.length; i++) { |
|
if (solo[i]) { |
|
hasSolo = true; |
|
break; |
|
} |
|
} |
|
} |
|
if (hasSolo) { |
|
|
|
for (int i = 0; i < newTrackDisabled.length; i++) { |
|
newTrackDisabled[i] = (i >= solo.length) || (!solo[i]); |
|
} |
|
} else { |
|
|
|
for (int i = 0; i < newTrackDisabled.length; i++) { |
|
newTrackDisabled[i] = (mute != null) && (i < mute.length) && (mute[i]); |
|
} |
|
} |
|
return newTrackDisabled; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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) { |
|
|
|
note = smsg.getData1(); |
|
} |
|
} else { |
|
byte[] data = msg.getMessage(); |
|
if ((data[2] & 0x7F) > 0) { |
|
|
|
note = data[1] & 0x7F; |
|
} |
|
} |
|
if (note >= 0) { |
|
int bit = 1<<(status & 0x0F); |
|
if ((noteOnCache[note] & bit) != 0) { |
|
|
|
getTransmitterList().sendMessage(status | (note<<8), -1); |
|
|
|
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."); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
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 |
|
|
|
if (tempArray == null) { |
|
tempArray = new byte[128][16]; |
|
} |
|
chaseTrackEvents(i, 0, lastTick, true, tempArray); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private void chaseTrackEvents(int trackNum, |
|
long startTick, |
|
long endTick, |
|
boolean doReindex, |
|
byte[][] tempArray) { |
|
if (startTick > endTick) { |
|
|
|
startTick = 0; |
|
} |
|
byte[] progs = new byte[16]; |
|
|
|
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; |
|
|
|
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 |
|
|
|
if (progs[ch] >= 0) { |
|
getTransmitterList().sendMessage((ShortMessage.PROGRAM_CHANGE | ch) | (progs[ch]<<8), -1); |
|
} |
|
if (progs[ch] >= 0 || startTick == 0 || endTick == 0) { |
|
|
|
getTransmitterList().sendMessage((ShortMessage.PITCH_BEND | ch) | (0x40 << 16), -1); |
|
|
|
getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (64 << 8), -1); |
|
} |
|
} |
|
if (DEBUG_PUMP) Printer.println(" chaseTrackEvents track "+trackNum+": sent "+numControllersSent+" controllers."); |
|
} |
|
|
|
|
|
|
|
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])) { |
|
|
|
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]); |
|
} |
|
} |
|
|
|
|
|
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. |
|
|
|
|
|
if (trackNum == 0) { |
|
int newTempo = MidiUtils.getTempoMPQ(message); |
|
if (newTempo > 0) { |
|
if (event.getTick() != ignoreTempoEventAt) { |
|
setTempoMPQ(newTempo); |
|
changesPending = true; |
|
} |
|
|
|
ignoreTempoEventAt = -1; |
|
} |
|
} |
|
|
|
sendMetaEvents(message); |
|
|
|
} else { |
|
|
|
getTransmitterList().sendMessage(message, -1); |
|
|
|
switch (msgStatus & 0xF0) { |
|
case ShortMessage.NOTE_OFF: { |
|
|
|
int note = ((ShortMessage) message).getData1() & 0x7F; |
|
noteOnCache[note] &= (0xFFFF ^ (1<<(msgStatus & 0x0F))); |
|
break; |
|
} |
|
|
|
case ShortMessage.NOTE_ON: { |
|
|
|
ShortMessage smsg = (ShortMessage) message; |
|
int note = smsg.getData1() & 0x7F; |
|
int vel = smsg.getData2() & 0x7F; |
|
if (vel > 0) { |
|
|
|
noteOnCache[note] |= 1<<(msgStatus & 0x0F); |
|
} else { |
|
|
|
noteOnCache[note] &= (0xFFFF ^ (1<<(msgStatus & 0x0F))); |
|
} |
|
break; |
|
} |
|
|
|
case ShortMessage.CONTROL_CHANGE: |
|
|
|
sendControllerEvents(message); |
|
break; |
|
|
|
} |
|
} |
|
return changesPending; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
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; |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
if (checkPointMillis == 0) { |
|
|
|
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 { |
|
|
|
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! |
|
|
|
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(); |
|
|
|
while (!changesPending && (readPos < size) |
|
&& (currEvent = thisTrack.get(readPos)).getTick() <= targetTick) { |
|
|
|
if ((readPos == size -1) && MidiUtils.isMetaEndOfTrack(currEvent.getMessage())) { |
|
|
|
readPos = size; |
|
break; |
|
} |
|
// TODO: some kind of heuristics if the MIDI messages have changed |
|
// significantly (i.e. deleted or inserted a bunch of messages) |
|
|
|
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 |
|
|
|
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; |
|
} |
|
|
|
|
|
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); |
|
|
|
needReindex = false; |
|
changesPending = false; |
|
|
|
doLoop = false; |
|
EOM = false; |
|
} |
|
} while (changesPending); |
|
|
|
return EOM; |
|
} |
|
|
|
} // class DataPump |
|
|
|
} |