/* * Copyright (c) 2003, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.media.sound; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import javax.sound.midi.ControllerEventListener; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MetaEventListener; import javax.sound.midi.MetaMessage; import javax.sound.midi.MidiDevice; import javax.sound.midi.MidiEvent; import javax.sound.midi.MidiMessage; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.Receiver; import javax.sound.midi.Sequence; import javax.sound.midi.Sequencer; import javax.sound.midi.ShortMessage; import javax.sound.midi.Synthesizer; import javax.sound.midi.Track; import javax.sound.midi.Transmitter; /** * A Real Time Sequencer * * @author Florian Bomers */ /* TODO: * - rename PlayThread to PlayEngine (because isn't a thread) */ final class RealTimeSequencer extends AbstractMidiDevice implements Sequencer, AutoConnectSequencer { /** debugging flags */ private static final boolean DEBUG_PUMP = false; private static final boolean DEBUG_PUMP_ALL = false; /** * Event Dispatcher thread. Should be using a shared event * dispatcher instance with a factory in EventDispatcher */ private static final Map dispatchers = new WeakHashMap<>(); /** * All RealTimeSequencers share this info object. */ static final MidiDevice.Info info = new RealTimeSequencerInfo(); private static final Sequencer.SyncMode[] masterSyncModes = { Sequencer.SyncMode.INTERNAL_CLOCK }; private static final Sequencer.SyncMode[] slaveSyncModes = { Sequencer.SyncMode.NO_SYNC }; private static final Sequencer.SyncMode masterSyncMode = Sequencer.SyncMode.INTERNAL_CLOCK; private static final Sequencer.SyncMode slaveSyncMode = Sequencer.SyncMode.NO_SYNC; /** * Sequence on which this sequencer is operating. */ 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 final MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache(); /** * True if the sequence is running. */ private volatile boolean running; /** * the thread for pushing out the MIDI messages. */ private PlayThread playThread; /** * True if we are recording. */ private volatile boolean recording; /** * List of tracks to which we're recording. */ private final List recordingTracks = new ArrayList<>(); private long loopStart = 0; private long loopEnd = -1; private int loopCount = 0; /** * Meta event listeners. */ private final ArrayList metaEventListeners = new ArrayList<>(); /** * Control change listeners. */ private final ArrayList controllerEventListeners = new ArrayList<>(); /** * 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; /* ****************************** CONSTRUCTOR ****************************** */ RealTimeSequencer(){ super(info); if (Printer.trace) Printer.trace(">> RealTimeSequencer CONSTRUCTOR"); if (Printer.trace) Printer.trace("<< RealTimeSequencer CONSTRUCTOR completed"); } /* ****************************** SEQUENCER METHODS ******************** */ @Override public synchronized void setSequence(Sequence sequence) throws InvalidMidiDataException { if (Printer.trace) Printer.trace(">> RealTimeSequencer: setSequence(" + sequence +")"); if (sequence != this.sequence) { if (this.sequence != null && sequence == null) { setCaches(); stop(); // initialize some non-cached values trackMuted = null; trackSolo = null; loopStart = 0; loopEnd = -1; loopCount = 0; if (getDataPump() != null) { getDataPump().setTickPos(0); getDataPump().resetLoopCount(); } } if (playThread != null) { playThread.setSequence(sequence); } // store this sequence (do not copy - we want to give the possibility // of modifying the sequence at runtime) this.sequence = sequence; if (sequence != null) { tempoCache.refresh(sequence); // rewind to the beginning setTickPosition(0); // propagate caches propagateCaches(); } } else if (sequence != null) { tempoCache.refresh(sequence); if (playThread != null) { playThread.setSequence(sequence); } } if (Printer.trace) Printer.trace("<< RealTimeSequencer: setSequence(" + sequence +") completed"); } @Override public synchronized void setSequence(InputStream stream) throws IOException, InvalidMidiDataException { if (Printer.trace) Printer.trace(">> RealTimeSequencer: setSequence(" + stream +")"); if (stream == null) { setSequence((Sequence) null); return; } Sequence seq = MidiSystem.getSequence(stream); // can throw IOException, InvalidMidiDataException setSequence(seq); if (Printer.trace) Printer.trace("<< RealTimeSequencer: setSequence(" + stream +") completed"); } @Override public Sequence getSequence() { return sequence; } @Override public synchronized void start() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: start()"); // sequencer not open: throw an exception if (!isOpen()) { throw new IllegalStateException("sequencer not open"); } // sequence not available: throw an exception if (sequence == null) { throw new IllegalStateException("sequence not set"); } // already running: return quietly if (running == true) { return; } // start playback implStart(); if (Printer.trace) Printer.trace("<< RealTimeSequencer: start() completed"); } @Override public synchronized void stop() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: stop()"); if (!isOpen()) { throw new IllegalStateException("sequencer not open"); } stopRecording(); // not running; just return if (running == false) { if (Printer.trace) Printer.trace("<< RealTimeSequencer: stop() not running!"); return; } // stop playback implStop(); if (Printer.trace) Printer.trace("<< RealTimeSequencer: stop() completed"); } @Override public boolean isRunning() { return running; } @Override public void startRecording() { if (!isOpen()) { throw new IllegalStateException("Sequencer not open"); } start(); recording = true; } @Override public void stopRecording() { if (!isOpen()) { throw new IllegalStateException("Sequencer not open"); } recording = false; } @Override public boolean isRecording() { return recording; } @Override 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)); } } } @Override 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; } @Override public float getTempoInBPM() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoInBPM() "); return (float) MidiUtils.convertTempo(getTempoInMPQ()); } @Override public void setTempoInBPM(float bpm) { if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoInBPM() "); if (bpm <= 0) { // should throw IllegalArgumentException bpm = 1.0f; } setTempoInMPQ((float) MidiUtils.convertTempo((double) bpm)); } @Override public float getTempoInMPQ() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoInMPQ() "); if (needCaching()) { // if the sequencer is closed, return cached value if (cacheTempoMPQ != -1) { return (float) cacheTempoMPQ; } // if sequence is set, return current tempo if (sequence != null) { return tempoCache.getTempoMPQAt(getTickPosition()); } // last resort: return a standard tempo: 120bpm return (float) MidiUtils.DEFAULT_TEMPO_MPQ; } return getDataPump().getTempoMPQ(); } @Override public void setTempoInMPQ(float mpq) { if (mpq <= 0) { // should throw IllegalArgumentException mpq = 1.0f; } if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoInMPQ() "); if (needCaching()) { // cache the value cacheTempoMPQ = mpq; } else { // set the native tempo in MPQ getDataPump().setTempoMPQ(mpq); // reset the tempoInBPM and tempoInMPQ values so we won't use them again cacheTempoMPQ = -1; } } @Override public void setTempoFactor(float factor) { if (factor <= 0) { // should throw IllegalArgumentException return; } if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTempoFactor() "); if (needCaching()) { cacheTempoFactor = factor; } else { getDataPump().setTempoFactor(factor); // don't need cache anymore cacheTempoFactor = -1; } } @Override public float getTempoFactor() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTempoFactor() "); if (needCaching()) { if (cacheTempoFactor != -1) { return cacheTempoFactor; } return 1.0f; } return getDataPump().getTempoFactor(); } @Override public long getTickLength() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTickLength() "); if (sequence == null) { return 0; } return sequence.getTickLength(); } @Override public synchronized long getTickPosition() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getTickPosition() "); if (getDataPump() == null || sequence == null) { return 0; } return getDataPump().getTickPos(); } @Override public synchronized void setTickPosition(long tick) { if (tick < 0) { // should throw IllegalArgumentException return; } if (Printer.trace) Printer.trace(">> RealTimeSequencer: setTickPosition("+tick+") "); if (getDataPump() == null) { if (tick != 0) { // throw new InvalidStateException("cannot set position in closed state"); } } else if (sequence == null) { if (tick != 0) { // throw new InvalidStateException("cannot set position if sequence is not set"); } } else { getDataPump().setTickPos(tick); } } @Override public long getMicrosecondLength() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: getMicrosecondLength() "); if (sequence == null) { return 0; } return sequence.getMicrosecondLength(); } @Override 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); } } @Override public void setMicrosecondPosition(long microseconds) { if (microseconds < 0) { // should throw IllegalArgumentException return; } if (Printer.trace) Printer.trace(">> RealTimeSequencer: setMicrosecondPosition("+microseconds+") "); if (getDataPump() == null) { if (microseconds != 0) { // throw new InvalidStateException("cannot set position in closed state"); } } else if (sequence == null) { if (microseconds != 0) { // throw new InvalidStateException("cannot set position if sequence is not set"); } } else { synchronized(tempoCache) { setTickPosition(MidiUtils.microsecond2tick(sequence, microseconds, tempoCache)); } } } @Override public void setMasterSyncMode(Sequencer.SyncMode sync) { // not supported } @Override public Sequencer.SyncMode getMasterSyncMode() { return masterSyncMode; } @Override public Sequencer.SyncMode[] getMasterSyncModes() { Sequencer.SyncMode[] returnedModes = new Sequencer.SyncMode[masterSyncModes.length]; System.arraycopy(masterSyncModes, 0, returnedModes, 0, masterSyncModes.length); return returnedModes; } @Override public void setSlaveSyncMode(Sequencer.SyncMode sync) { // not supported } @Override public Sequencer.SyncMode getSlaveSyncMode() { return slaveSyncMode; } @Override public Sequencer.SyncMode[] getSlaveSyncModes() { Sequencer.SyncMode[] returnedModes = new Sequencer.SyncMode[slaveSyncModes.length]; System.arraycopy(slaveSyncModes, 0, returnedModes, 0, slaveSyncModes.length); return returnedModes; } int getTrackCount() { Sequence seq = getSequence(); if (seq != null) { // $$fb wish there was a nicer way to get the number of tracks... return sequence.getTracks().length; } return 0; } @Override 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(); } } @Override public synchronized boolean getTrackMute(int track) { if (track < 0 || track >= getTrackCount()) return false; if (trackMuted == null || trackMuted.length <= track) return false; return trackMuted[track]; } @Override 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(); } } @Override public synchronized boolean getTrackSolo(int track) { if (track < 0 || track >= getTrackCount()) return false; if (trackSolo == null || trackSolo.length <= track) return false; return trackSolo[track]; } @Override public boolean addMetaEventListener(MetaEventListener listener) { synchronized(metaEventListeners) { if (! metaEventListeners.contains(listener)) { metaEventListeners.add(listener); } return true; } } @Override public void removeMetaEventListener(MetaEventListener listener) { synchronized(metaEventListeners) { int index = metaEventListeners.indexOf(listener); if (index >= 0) { metaEventListeners.remove(index); } } } @Override public int[] addControllerEventListener(ControllerEventListener listener, int[] controllers) { synchronized(controllerEventListeners) { // first find the listener. if we have one, add the controllers // if not, create a new element for it. ControllerListElement cve = null; boolean flag = false; for(int i=0; i < controllerEventListeners.size(); i++) { cve = 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(); } } @Override public int[] removeControllerEventListener(ControllerEventListener listener, int[] controllers) { synchronized(controllerEventListeners) { ControllerListElement cve = null; boolean flag = false; for (int i=0; i < controllerEventListeners.size(); i++) { cve = 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) /////////////////////// @Override public void setLoopStartPoint(long tick) { if ((tick > getTickLength()) || ((loopEnd != -1) && (tick > loopEnd)) || (tick < 0)) { throw new IllegalArgumentException("invalid loop start point: "+tick); } loopStart = tick; } @Override public long getLoopStartPoint() { return loopStart; } @Override public void setLoopEndPoint(long tick) { if ((tick > getTickLength()) || ((loopStart > tick) && (tick != -1)) || (tick < -1)) { throw new IllegalArgumentException("invalid loop end point: "+tick); } loopEnd = tick; } @Override public long getLoopEndPoint() { return loopEnd; } @Override 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(); } } @Override public int getLoopCount() { return loopCount; } /* *********************************** play control ************************* */ @Override protected void implOpen() throws MidiUnavailableException { if (Printer.trace) Printer.trace(">> RealTimeSequencer: implOpen()"); //openInternalSynth(); // create PlayThread playThread = new PlayThread(); //id = nOpen(); //if (id == 0) { // throw new MidiUnavailableException("unable to open sequencer"); //} if (sequence != null) { playThread.setSequence(sequence); } // propagate caches propagateCaches(); if (doAutoConnectAtNextOpen) { doAutoConnect(); } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implOpen() succeeded"); } private void doAutoConnect() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: doAutoConnect()"); Receiver rec = null; // first try to connect to the default synthesizer // IMPORTANT: this code needs to be synch'ed with // MidiSystem.getSequencer(boolean), because the same // algorithm needs to be used! try { Synthesizer synth = MidiSystem.getSynthesizer(); if (synth instanceof ReferenceCountingDevice) { rec = ((ReferenceCountingDevice) synth).getReceiverReferenceCounting(); } else { synth.open(); try { rec = synth.getReceiver(); } finally { // make sure that the synth is properly closed if (rec == null) { synth.close(); } } } } catch (Exception e) { // something went wrong with synth } if (rec == null) { // then try to connect to the default Receiver try { rec = MidiSystem.getReceiver(); } catch (Exception e) { // something went wrong. Nothing to do then! } } if (rec != null) { autoConnectedReceiver = rec; try { getTransmitter().setReceiver(rec); } catch (Exception e) {} } if (Printer.trace) Printer.trace("<< RealTimeSequencer: doAutoConnect() succeeded"); } private synchronized void propagateCaches() { // only set caches if open and sequence is set if (sequence != null && isOpen()) { if (cacheTempoFactor != -1) { setTempoFactor(cacheTempoFactor); } if (cacheTempoMPQ == -1) { setTempoInMPQ((new MidiUtils.TempoCache(sequence)).getTempoMPQAt(getTickPosition())); } else { setTempoInMPQ((float) cacheTempoMPQ); } } } /** * populate the caches with the current values. */ private synchronized void setCaches() { cacheTempoFactor = getTempoFactor(); cacheTempoMPQ = getTempoInMPQ(); } @Override protected synchronized void implClose() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: implClose() "); if (playThread == null) { if (Printer.err) Printer.err("RealTimeSequencer.implClose() called, but playThread not instanciated!"); } else { // Interrupt playback loop. playThread.close(); playThread = null; } super.implClose(); sequence = null; running = false; cacheTempoMPQ = -1; cacheTempoFactor = -1; trackMuted = null; trackSolo = null; loopStart = 0; loopEnd = -1; loopCount = 0; /** if this sequencer is set to autoconnect, need to * re-establish the connection at next open! */ doAutoConnectAtNextOpen = autoConnect; if (autoConnectedReceiver != null) { try { autoConnectedReceiver.close(); } catch (Exception e) {} autoConnectedReceiver = null; } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implClose() completed"); } void implStart() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: implStart()"); if (playThread == null) { if (Printer.err) Printer.err("RealTimeSequencer.implStart() called, but playThread not instanciated!"); return; } tempoCache.refresh(sequence); if (!running) { running = true; playThread.start(); } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implStart() completed"); } void implStop() { if (Printer.trace) Printer.trace(">> RealTimeSequencer: implStop()"); if (playThread == null) { if (Printer.err) Printer.err("RealTimeSequencer.implStop() called, but playThread not instanciated!"); return; } recording = false; if (running) { running = false; playThread.stop(); } if (Printer.trace) Printer.trace("<< RealTimeSequencer: implStop() completed"); } private static EventDispatcher getEventDispatcher() { // create and start the global event thread //TODO need a way to stop this thread when the engine is done final ThreadGroup tg = Thread.currentThread().getThreadGroup(); synchronized (dispatchers) { EventDispatcher eventDispatcher = dispatchers.get(tg); if (eventDispatcher == null) { eventDispatcher = new EventDispatcher(); dispatchers.put(tg, eventDispatcher); eventDispatcher.start(); } return eventDispatcher; } } /** * Send midi player events. * must not be synchronized on "this" */ void sendMetaEvents(MidiMessage message) { if (metaEventListeners.size() == 0) return; //if (Printer.debug) Printer.debug("sending a meta event"); getEventDispatcher().sendAudioEvents(message, metaEventListeners); } /** * Send midi player events. */ 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 = controllerEventListeners.get(i); for(int j = 0; j < cve.controllers.length; j++) { if (cve.controllers[j] == controller) { sendToListeners.add(cve.listener); break; } } } getEventDispatcher().sendAudioEvents(message, sendToListeners); } private boolean needCaching() { return !isOpen() || (sequence == null) || (playThread == null); } /** * return the data pump instance, owned by play thread * if playthread is null, return null. * This method is guaranteed to return non-null if * needCaching returns false */ 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 @Override protected boolean hasReceivers() { return true; } // for recording @Override protected Receiver createReceiver() throws MidiUnavailableException { return new SequencerReceiver(); } @Override protected boolean hasTransmitters() { return true; } @Override protected Transmitter createTransmitter() throws MidiUnavailableException { return new SequencerTransmitter(); } // interface AutoConnectSequencer @Override public void setAutoConnect(Receiver autoConnectedReceiver) { this.autoConnect = (autoConnectedReceiver != null); this.autoConnectedReceiver = autoConnectedReceiver; } /** * An own class to distinguish the class name from * the transmitter of other devices. */ private class SequencerTransmitter extends BasicTransmitter { private SequencerTransmitter() { super(); } } final class SequencerReceiver extends AbstractReceiver { @Override void implSend(MidiMessage message, long timeStamp) { if (recording) { long tickPos = 0; // convert timeStamp to ticks if (timeStamp < 0) { tickPos = getTickPosition(); } else { synchronized(tempoCache) { tickPos = MidiUtils.microsecond2tick(sequence, timeStamp, tempoCache); } } // and record to the first matching Track Track track = null; // do not record real-time events // see 5048381: NullPointerException when saving a MIDI sequence if (message.getLength() > 1) { if (message instanceof ShortMessage) { ShortMessage sm = (ShortMessage) message; // all real-time messages have 0xF in the high nibble of the status byte if ((sm.getStatus() & 0xF0) != 0xF0) { track = RecordingTrack.get(recordingTracks, sm.getChannel()); } } else { // $$jb: where to record meta, sysex events? // $$fb: the first recording track track = RecordingTrack.get(recordingTracks, -1); } if (track != null) { // create a copy of this message if (message instanceof ShortMessage) { message = new FastShortMessage((ShortMessage) message); } else { message = (MidiMessage) message.clone(); } // create new MidiEvent MidiEvent me = new MidiEvent(message, tickPos); track.add(me); } } } } } private static class RealTimeSequencerInfo extends MidiDevice.Info { private static final String name = "Real Time Sequencer"; private static final String vendor = "Oracle Corporation"; private static final String description = "Software sequencer"; private static final String version = "Version 1.0"; RealTimeSequencerInfo() { super(name, vendor, description, version); } } // class Info private class ControllerListElement { // $$jb: using an array for controllers b/c its // easier to deal with than turning all the // ints into objects to use a Vector int [] controllers; final ControllerEventListener listener; private ControllerListElement(ControllerEventListener listener, int[] controllers) { this.listener = listener; if (controllers == null) { controllers = new int[128]; for (int i = 0; i < 128; i++) { controllers[i] = i; } } this.controllers = controllers; } private void addControllers(int[] c) { if (c==null) { controllers = new int[128]; for (int i = 0; i < 128; i++) { controllers[i] = i; } return; } int temp[] = new int[ controllers.length + c.length ]; int elements; // first add what we have for(int i=0; i recordingTracks, Track track) { synchronized(recordingTracks) { int size = recordingTracks.size(); for (int i = 0; i < size; i++) { RecordingTrack current = 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 = recordingTracks.get(i); if ((current.channel == channel) || (current.channel == -1)) { return current.track; } } } return null; } } final class PlayThread implements Runnable { private Thread thread; private final Object lock = new Object(); /** true if playback is interrupted (in close) */ boolean interrupted = false; boolean isPumping = false; private final DataPump dataPump = new DataPump(); PlayThread() { // nearly MAX_PRIORITY int priority = Thread.NORM_PRIORITY + ((Thread.MAX_PRIORITY - Thread.NORM_PRIORITY) * 3) / 4; thread = JSSecurityManager.createThread(this, "Java Sound Sequencer", // name false, // daemon priority, // priority true); // doStart } DataPump getDataPump() { return dataPump; } synchronized void setSequence(Sequence seq) { dataPump.setSequence(seq); } /** start thread and pump. Requires up-to-date tempoCache */ 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! */ @Override 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<= 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 }