1 /*
   2  * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.media.sound;
  27 
  28 import java.io.IOException;
  29 import java.io.InputStream;
  30 import java.util.ArrayList;
  31 import java.util.List;
  32 import java.util.Map;
  33 import java.util.WeakHashMap;
  34 
  35 import javax.sound.midi.ControllerEventListener;
  36 import javax.sound.midi.InvalidMidiDataException;
  37 import javax.sound.midi.MetaEventListener;
  38 import javax.sound.midi.MetaMessage;
  39 import javax.sound.midi.MidiDevice;
  40 import javax.sound.midi.MidiEvent;
  41 import javax.sound.midi.MidiMessage;
  42 import javax.sound.midi.MidiSystem;
  43 import javax.sound.midi.MidiUnavailableException;
  44 import javax.sound.midi.Receiver;
  45 import javax.sound.midi.Sequence;
  46 import javax.sound.midi.Sequencer;
  47 import javax.sound.midi.ShortMessage;
  48 import javax.sound.midi.Synthesizer;
  49 import javax.sound.midi.Track;
  50 import javax.sound.midi.Transmitter;
  51 
  52 /**
  53  * A Real Time Sequencer
  54  *
  55  * @author Florian Bomers
  56  */
  57 
  58 /* TODO:
  59  * - rename PlayThread to PlayEngine (because isn't a thread)
  60  */
  61 final class RealTimeSequencer extends AbstractMidiDevice
  62         implements Sequencer, AutoConnectSequencer {
  63 
  64     /**
  65      * Event Dispatcher thread. Should be using a shared event
  66      * dispatcher instance with a factory in EventDispatcher
  67      */
  68     private static final Map<ThreadGroup, EventDispatcher> dispatchers =
  69             new WeakHashMap<>();
  70 
  71     /**
  72      * All RealTimeSequencers share this info object.
  73      */
  74     static final MidiDevice.Info info = new RealTimeSequencerInfo();
  75 
  76 
  77     private static final Sequencer.SyncMode[] masterSyncModes = { Sequencer.SyncMode.INTERNAL_CLOCK };
  78     private static final Sequencer.SyncMode[] slaveSyncModes  = { Sequencer.SyncMode.NO_SYNC };
  79 
  80     private static final Sequencer.SyncMode masterSyncMode    = Sequencer.SyncMode.INTERNAL_CLOCK;
  81     private static final Sequencer.SyncMode slaveSyncMode     = Sequencer.SyncMode.NO_SYNC;
  82 
  83     /**
  84      * Sequence on which this sequencer is operating.
  85      */
  86     private Sequence sequence = null;
  87 
  88     // caches
  89 
  90     /**
  91      * Same for setTempoInMPQ...
  92      * -1 means not set.
  93      */
  94     private double cacheTempoMPQ = -1;
  95 
  96     /**
  97      * cache value for tempo factor until sequence is set
  98      * -1 means not set.
  99      */
 100     private float cacheTempoFactor = -1;
 101 
 102     /** if a particular track is muted */
 103     private boolean[] trackMuted = null;
 104     /** if a particular track is solo */
 105     private boolean[] trackSolo = null;
 106 
 107     /** tempo cache for getMicrosecondPosition */
 108     private final MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache();
 109 
 110     /**
 111      * True if the sequence is running.
 112      */
 113     private volatile boolean running;
 114 
 115     /**
 116      * the thread for pushing out the MIDI messages.
 117      */
 118     private PlayThread playThread;
 119 
 120     /**
 121      * True if we are recording.
 122      */
 123     private volatile boolean recording;
 124 
 125     /**
 126      * List of tracks to which we're recording.
 127      */
 128     private final List<RecordingTrack> recordingTracks = new ArrayList<>();
 129 
 130     private long loopStart = 0;
 131     private long loopEnd = -1;
 132     private int loopCount = 0;
 133 
 134     /**
 135      * Meta event listeners.
 136      */
 137     private final ArrayList<Object> metaEventListeners = new ArrayList<>();
 138 
 139     /**
 140      * Control change listeners.
 141      */
 142     private final ArrayList<ControllerListElement> controllerEventListeners = new ArrayList<>();
 143 
 144     /**
 145      * automatic connection support.
 146      */
 147     private boolean autoConnect = false;
 148 
 149     /**
 150      * if we need to autoconnect at next open.
 151      */
 152     private boolean doAutoConnectAtNextOpen = false;
 153 
 154     /**
 155      * the receiver that this device is auto-connected to.
 156      */
 157     Receiver autoConnectedReceiver = null;
 158 
 159 
 160     /* ****************************** CONSTRUCTOR ****************************** */
 161 
 162     RealTimeSequencer(){
 163         super(info);
 164     }
 165 
 166     /* ****************************** SEQUENCER METHODS ******************** */
 167 
 168     @Override
 169     public synchronized void setSequence(Sequence sequence)
 170         throws InvalidMidiDataException {
 171         if (sequence != this.sequence) {
 172             if (this.sequence != null && sequence == null) {
 173                 setCaches();
 174                 stop();
 175                 // initialize some non-cached values
 176                 trackMuted = null;
 177                 trackSolo = null;
 178                 loopStart = 0;
 179                 loopEnd = -1;
 180                 loopCount = 0;
 181                 if (getDataPump() != null) {
 182                     getDataPump().setTickPos(0);
 183                     getDataPump().resetLoopCount();
 184                 }
 185             }
 186 
 187             if (playThread != null) {
 188                 playThread.setSequence(sequence);
 189             }
 190 
 191             // store this sequence (do not copy - we want to give the possibility
 192             // of modifying the sequence at runtime)
 193             this.sequence = sequence;
 194 
 195             if (sequence != null) {
 196                 tempoCache.refresh(sequence);
 197                 // rewind to the beginning
 198                 setTickPosition(0);
 199                 // propagate caches
 200                 propagateCaches();
 201             }
 202         }
 203         else if (sequence != null) {
 204             tempoCache.refresh(sequence);
 205             if (playThread != null) {
 206                 playThread.setSequence(sequence);
 207             }
 208         }
 209     }
 210 
 211     @Override
 212     public synchronized void setSequence(InputStream stream) throws IOException, InvalidMidiDataException {
 213         if (stream == null) {
 214             setSequence((Sequence) null);
 215             return;
 216         }
 217 
 218         Sequence seq = MidiSystem.getSequence(stream); // can throw IOException, InvalidMidiDataException
 219 
 220         setSequence(seq);
 221     }
 222 
 223     @Override
 224     public Sequence getSequence() {
 225         return sequence;
 226     }
 227 
 228     @Override
 229     public synchronized void start() {
 230         // sequencer not open: throw an exception
 231         if (!isOpen()) {
 232             throw new IllegalStateException("sequencer not open");
 233         }
 234 
 235         // sequence not available: throw an exception
 236         if (sequence == null) {
 237             throw new IllegalStateException("sequence not set");
 238         }
 239 
 240         // already running: return quietly
 241         if (running == true) {
 242             return;
 243         }
 244 
 245         // start playback
 246         implStart();
 247     }
 248 
 249     @Override
 250     public synchronized void stop() {
 251         if (!isOpen()) {
 252             throw new IllegalStateException("sequencer not open");
 253         }
 254         stopRecording();
 255 
 256         // not running; just return
 257         if (running == false) {
 258             return;
 259         }
 260 
 261         // stop playback
 262         implStop();
 263     }
 264 
 265     @Override
 266     public boolean isRunning() {
 267         return running;
 268     }
 269 
 270     @Override
 271     public void startRecording() {
 272         if (!isOpen()) {
 273             throw new IllegalStateException("Sequencer not open");
 274         }
 275 
 276         start();
 277         recording = true;
 278     }
 279 
 280     @Override
 281     public void stopRecording() {
 282         if (!isOpen()) {
 283             throw new IllegalStateException("Sequencer not open");
 284         }
 285         recording = false;
 286     }
 287 
 288     @Override
 289     public boolean isRecording() {
 290         return recording;
 291     }
 292 
 293     @Override
 294     public void recordEnable(Track track, int channel) {
 295         if (!findTrack(track)) {
 296             throw new IllegalArgumentException("Track does not exist in the current sequence");
 297         }
 298 
 299         synchronized(recordingTracks) {
 300             RecordingTrack rc = RecordingTrack.get(recordingTracks, track);
 301             if (rc != null) {
 302                 rc.channel = channel;
 303             } else {
 304                 recordingTracks.add(new RecordingTrack(track, channel));
 305             }
 306         }
 307 
 308     }
 309 
 310     @Override
 311     public void recordDisable(Track track) {
 312         synchronized(recordingTracks) {
 313             RecordingTrack rc = RecordingTrack.get(recordingTracks, track);
 314             if (rc != null) {
 315                 recordingTracks.remove(rc);
 316             }
 317         }
 318 
 319     }
 320 
 321     private boolean findTrack(Track track) {
 322         boolean found = false;
 323         if (sequence != null) {
 324             Track[] tracks = sequence.getTracks();
 325             for (int i = 0; i < tracks.length; i++) {
 326                 if (track == tracks[i]) {
 327                     found = true;
 328                     break;
 329                 }
 330             }
 331         }
 332         return found;
 333     }
 334 
 335     @Override
 336     public float getTempoInBPM() {
 337         return (float) MidiUtils.convertTempo(getTempoInMPQ());
 338     }
 339 
 340     @Override
 341     public void setTempoInBPM(float bpm) {
 342         if (bpm <= 0) {
 343             // should throw IllegalArgumentException
 344             bpm = 1.0f;
 345         }
 346 
 347         setTempoInMPQ((float) MidiUtils.convertTempo((double) bpm));
 348     }
 349 
 350     @Override
 351     public float getTempoInMPQ() {
 352         if (needCaching()) {
 353             // if the sequencer is closed, return cached value
 354             if (cacheTempoMPQ != -1) {
 355                 return (float) cacheTempoMPQ;
 356             }
 357             // if sequence is set, return current tempo
 358             if (sequence != null) {
 359                 return tempoCache.getTempoMPQAt(getTickPosition());
 360             }
 361 
 362             // last resort: return a standard tempo: 120bpm
 363             return (float) MidiUtils.DEFAULT_TEMPO_MPQ;
 364         }
 365         return getDataPump().getTempoMPQ();
 366     }
 367 
 368     @Override
 369     public void setTempoInMPQ(float mpq) {
 370         if (mpq <= 0) {
 371             // should throw IllegalArgumentException
 372             mpq = 1.0f;
 373         }
 374         if (needCaching()) {
 375             // cache the value
 376             cacheTempoMPQ = mpq;
 377         } else {
 378             // set the native tempo in MPQ
 379             getDataPump().setTempoMPQ(mpq);
 380 
 381             // reset the tempoInBPM and tempoInMPQ values so we won't use them again
 382             cacheTempoMPQ = -1;
 383         }
 384     }
 385 
 386     @Override
 387     public void setTempoFactor(float factor) {
 388         if (factor <= 0) {
 389             // should throw IllegalArgumentException
 390             return;
 391         }
 392         if (needCaching()) {
 393             cacheTempoFactor = factor;
 394         } else {
 395             getDataPump().setTempoFactor(factor);
 396             // don't need cache anymore
 397             cacheTempoFactor = -1;
 398         }
 399     }
 400 
 401     @Override
 402     public float getTempoFactor() {
 403         if (needCaching()) {
 404             if (cacheTempoFactor != -1) {
 405                 return cacheTempoFactor;
 406             }
 407             return 1.0f;
 408         }
 409         return getDataPump().getTempoFactor();
 410     }
 411 
 412     @Override
 413     public long getTickLength() {
 414         if (sequence == null) {
 415             return 0;
 416         }
 417 
 418         return sequence.getTickLength();
 419     }
 420 
 421     @Override
 422     public synchronized long getTickPosition() {
 423         if (getDataPump() == null || sequence == null) {
 424             return 0;
 425         }
 426 
 427         return getDataPump().getTickPos();
 428     }
 429 
 430     @Override
 431     public synchronized void setTickPosition(long tick) {
 432         if (tick < 0) {
 433             // should throw IllegalArgumentException
 434             return;
 435         }
 436         if (getDataPump() == null) {
 437             if (tick != 0) {
 438                 // throw new InvalidStateException("cannot set position in closed state");
 439             }
 440         }
 441         else if (sequence == null) {
 442             if (tick != 0) {
 443                 // throw new InvalidStateException("cannot set position if sequence is not set");
 444             }
 445         } else {
 446             getDataPump().setTickPos(tick);
 447         }
 448     }
 449 
 450     @Override
 451     public long getMicrosecondLength() {
 452         if (sequence == null) {
 453             return 0;
 454         }
 455 
 456         return sequence.getMicrosecondLength();
 457     }
 458 
 459     @Override
 460     public long getMicrosecondPosition() {
 461         if (getDataPump() == null || sequence == null) {
 462             return 0;
 463         }
 464         synchronized (tempoCache) {
 465             return MidiUtils.tick2microsecond(sequence, getDataPump().getTickPos(), tempoCache);
 466         }
 467     }
 468 
 469     @Override
 470     public void setMicrosecondPosition(long microseconds) {
 471         if (microseconds < 0) {
 472             // should throw IllegalArgumentException
 473             return;
 474         }
 475         if (getDataPump() == null) {
 476             if (microseconds != 0) {
 477                 // throw new InvalidStateException("cannot set position in closed state");
 478             }
 479         }
 480         else if (sequence == null) {
 481             if (microseconds != 0) {
 482                 // throw new InvalidStateException("cannot set position if sequence is not set");
 483             }
 484         } else {
 485             synchronized(tempoCache) {
 486                 setTickPosition(MidiUtils.microsecond2tick(sequence, microseconds, tempoCache));
 487             }
 488         }
 489     }
 490 
 491     @Override
 492     public void setMasterSyncMode(Sequencer.SyncMode sync) {
 493         // not supported
 494     }
 495 
 496     @Override
 497     public Sequencer.SyncMode getMasterSyncMode() {
 498         return masterSyncMode;
 499     }
 500 
 501     @Override
 502     public Sequencer.SyncMode[] getMasterSyncModes() {
 503         Sequencer.SyncMode[] returnedModes = new Sequencer.SyncMode[masterSyncModes.length];
 504         System.arraycopy(masterSyncModes, 0, returnedModes, 0, masterSyncModes.length);
 505         return returnedModes;
 506     }
 507 
 508     @Override
 509     public void setSlaveSyncMode(Sequencer.SyncMode sync) {
 510         // not supported
 511     }
 512 
 513     @Override
 514     public Sequencer.SyncMode getSlaveSyncMode() {
 515         return slaveSyncMode;
 516     }
 517 
 518     @Override
 519     public Sequencer.SyncMode[] getSlaveSyncModes() {
 520         Sequencer.SyncMode[] returnedModes = new Sequencer.SyncMode[slaveSyncModes.length];
 521         System.arraycopy(slaveSyncModes, 0, returnedModes, 0, slaveSyncModes.length);
 522         return returnedModes;
 523     }
 524 
 525     int getTrackCount() {
 526         Sequence seq = getSequence();
 527         if (seq != null) {
 528             // $$fb wish there was a nicer way to get the number of tracks...
 529             return sequence.getTracks().length;
 530         }
 531         return 0;
 532     }
 533 
 534     @Override
 535     public synchronized void setTrackMute(int track, boolean mute) {
 536         int trackCount = getTrackCount();
 537         if (track < 0 || track >= getTrackCount()) return;
 538         trackMuted = ensureBoolArraySize(trackMuted, trackCount);
 539         trackMuted[track] = mute;
 540         if (getDataPump() != null) {
 541             getDataPump().muteSoloChanged();
 542         }
 543     }
 544 
 545     @Override
 546     public synchronized boolean getTrackMute(int track) {
 547         if (track < 0 || track >= getTrackCount()) return false;
 548         if (trackMuted == null || trackMuted.length <= track) return false;
 549         return trackMuted[track];
 550     }
 551 
 552     @Override
 553     public synchronized void setTrackSolo(int track, boolean solo) {
 554         int trackCount = getTrackCount();
 555         if (track < 0 || track >= getTrackCount()) return;
 556         trackSolo = ensureBoolArraySize(trackSolo, trackCount);
 557         trackSolo[track] = solo;
 558         if (getDataPump() != null) {
 559             getDataPump().muteSoloChanged();
 560         }
 561     }
 562 
 563     @Override
 564     public synchronized boolean getTrackSolo(int track) {
 565         if (track < 0 || track >= getTrackCount()) return false;
 566         if (trackSolo == null || trackSolo.length <= track) return false;
 567         return trackSolo[track];
 568     }
 569 
 570     @Override
 571     public boolean addMetaEventListener(MetaEventListener listener) {
 572         synchronized(metaEventListeners) {
 573             if (! metaEventListeners.contains(listener)) {
 574 
 575                 metaEventListeners.add(listener);
 576             }
 577             return true;
 578         }
 579     }
 580 
 581     @Override
 582     public void removeMetaEventListener(MetaEventListener listener) {
 583         synchronized(metaEventListeners) {
 584             int index = metaEventListeners.indexOf(listener);
 585             if (index >= 0) {
 586                 metaEventListeners.remove(index);
 587             }
 588         }
 589     }
 590 
 591     @Override
 592     public int[] addControllerEventListener(ControllerEventListener listener, int[] controllers) {
 593         synchronized(controllerEventListeners) {
 594 
 595             // first find the listener.  if we have one, add the controllers
 596             // if not, create a new element for it.
 597             ControllerListElement cve = null;
 598             boolean flag = false;
 599             for(int i=0; i < controllerEventListeners.size(); i++) {
 600 
 601                 cve = controllerEventListeners.get(i);
 602 
 603                 if (cve.listener.equals(listener)) {
 604                     cve.addControllers(controllers);
 605                     flag = true;
 606                     break;
 607                 }
 608             }
 609             if (!flag) {
 610                 cve = new ControllerListElement(listener, controllers);
 611                 controllerEventListeners.add(cve);
 612             }
 613 
 614             // and return all the controllers this listener is interested in
 615             return cve.getControllers();
 616         }
 617     }
 618 
 619     @Override
 620     public int[] removeControllerEventListener(ControllerEventListener listener, int[] controllers) {
 621         synchronized(controllerEventListeners) {
 622             ControllerListElement cve = null;
 623             boolean flag = false;
 624             for (int i=0; i < controllerEventListeners.size(); i++) {
 625                 cve = controllerEventListeners.get(i);
 626                 if (cve.listener.equals(listener)) {
 627                     cve.removeControllers(controllers);
 628                     flag = true;
 629                     break;
 630                 }
 631             }
 632             if (!flag) {
 633                 return new int[0];
 634             }
 635             if (controllers == null) {
 636                 int index = controllerEventListeners.indexOf(cve);
 637                 if (index >= 0) {
 638                     controllerEventListeners.remove(index);
 639                 }
 640                 return new int[0];
 641             }
 642             return cve.getControllers();
 643         }
 644     }
 645 
 646     ////////////////// LOOPING (added in 1.5) ///////////////////////
 647 
 648     @Override
 649     public void setLoopStartPoint(long tick) {
 650         if ((tick > getTickLength())
 651             || ((loopEnd != -1) && (tick > loopEnd))
 652             || (tick < 0)) {
 653             throw new IllegalArgumentException("invalid loop start point: "+tick);
 654         }
 655         loopStart = tick;
 656     }
 657 
 658     @Override
 659     public long getLoopStartPoint() {
 660         return loopStart;
 661     }
 662 
 663     @Override
 664     public void setLoopEndPoint(long tick) {
 665         if ((tick > getTickLength())
 666             || ((loopStart > tick) && (tick != -1))
 667             || (tick < -1)) {
 668             throw new IllegalArgumentException("invalid loop end point: "+tick);
 669         }
 670         loopEnd = tick;
 671     }
 672 
 673     @Override
 674     public long getLoopEndPoint() {
 675         return loopEnd;
 676     }
 677 
 678     @Override
 679     public void setLoopCount(int count) {
 680         if (count != LOOP_CONTINUOUSLY
 681             && count < 0) {
 682             throw new IllegalArgumentException("illegal value for loop count: "+count);
 683         }
 684         loopCount = count;
 685         if (getDataPump() != null) {
 686             getDataPump().resetLoopCount();
 687         }
 688     }
 689 
 690     @Override
 691     public int getLoopCount() {
 692         return loopCount;
 693     }
 694 
 695     /* *********************************** play control ************************* */
 696 
 697     @Override
 698     protected void implOpen() throws MidiUnavailableException {
 699         //openInternalSynth();
 700 
 701         // create PlayThread
 702         playThread = new PlayThread();
 703 
 704         //id = nOpen();
 705         //if (id == 0) {
 706         //    throw new MidiUnavailableException("unable to open sequencer");
 707         //}
 708         if (sequence != null) {
 709             playThread.setSequence(sequence);
 710         }
 711 
 712         // propagate caches
 713         propagateCaches();
 714 
 715         if (doAutoConnectAtNextOpen) {
 716             doAutoConnect();
 717         }
 718     }
 719 
 720     private void doAutoConnect() {
 721         Receiver rec = null;
 722         // first try to connect to the default synthesizer
 723         // IMPORTANT: this code needs to be synch'ed with
 724         //            MidiSystem.getSequencer(boolean), because the same
 725         //            algorithm needs to be used!
 726         try {
 727             Synthesizer synth = MidiSystem.getSynthesizer();
 728             if (synth instanceof ReferenceCountingDevice) {
 729                 rec = ((ReferenceCountingDevice) synth).getReceiverReferenceCounting();
 730             } else {
 731                 synth.open();
 732                 try {
 733                     rec = synth.getReceiver();
 734                 } finally {
 735                     // make sure that the synth is properly closed
 736                     if (rec == null) {
 737                         synth.close();
 738                     }
 739                 }
 740             }
 741         } catch (Exception e) {
 742             // something went wrong with synth
 743         }
 744         if (rec == null) {
 745             // then try to connect to the default Receiver
 746             try {
 747                 rec = MidiSystem.getReceiver();
 748             } catch (Exception e) {
 749                 // something went wrong. Nothing to do then!
 750             }
 751         }
 752         if (rec != null) {
 753             autoConnectedReceiver = rec;
 754             try {
 755                 getTransmitter().setReceiver(rec);
 756             } catch (Exception e) {}
 757         }
 758     }
 759 
 760     private synchronized void propagateCaches() {
 761         // only set caches if open and sequence is set
 762         if (sequence != null && isOpen()) {
 763             if (cacheTempoFactor != -1) {
 764                 setTempoFactor(cacheTempoFactor);
 765             }
 766             if (cacheTempoMPQ == -1) {
 767                 setTempoInMPQ((new MidiUtils.TempoCache(sequence)).getTempoMPQAt(getTickPosition()));
 768             } else {
 769                 setTempoInMPQ((float) cacheTempoMPQ);
 770             }
 771         }
 772     }
 773 
 774     /**
 775      * populate the caches with the current values.
 776      */
 777     private synchronized void setCaches() {
 778         cacheTempoFactor = getTempoFactor();
 779         cacheTempoMPQ = getTempoInMPQ();
 780     }
 781 
 782     @Override
 783     protected synchronized void implClose() {
 784         if (playThread == null) {
 785             if (Printer.err) Printer.err("RealTimeSequencer.implClose() called, but playThread not instanciated!");
 786         } else {
 787             // Interrupt playback loop.
 788             playThread.close();
 789             playThread = null;
 790         }
 791 
 792         super.implClose();
 793 
 794         sequence = null;
 795         running = false;
 796         cacheTempoMPQ = -1;
 797         cacheTempoFactor = -1;
 798         trackMuted = null;
 799         trackSolo = null;
 800         loopStart = 0;
 801         loopEnd = -1;
 802         loopCount = 0;
 803 
 804         /** if this sequencer is set to autoconnect, need to
 805          * re-establish the connection at next open!
 806          */
 807         doAutoConnectAtNextOpen = autoConnect;
 808 
 809         if (autoConnectedReceiver != null) {
 810             try {
 811                 autoConnectedReceiver.close();
 812             } catch (Exception e) {}
 813             autoConnectedReceiver = null;
 814         }
 815     }
 816 
 817     void implStart() {
 818         if (playThread == null) {
 819             if (Printer.err) Printer.err("RealTimeSequencer.implStart() called, but playThread not instanciated!");
 820             return;
 821         }
 822 
 823         tempoCache.refresh(sequence);
 824         if (!running) {
 825             running  = true;
 826             playThread.start();
 827         }
 828     }
 829 
 830     void implStop() {
 831         if (playThread == null) {
 832             if (Printer.err) Printer.err("RealTimeSequencer.implStop() called, but playThread not instanciated!");
 833             return;
 834         }
 835 
 836         recording = false;
 837         if (running) {
 838             running = false;
 839             playThread.stop();
 840         }
 841     }
 842 
 843     private static EventDispatcher getEventDispatcher() {
 844         // create and start the global event thread
 845         //TODO  need a way to stop this thread when the engine is done
 846         @SuppressWarnings("deprecation")
 847         final ThreadGroup tg = Thread.currentThread().getThreadGroup();
 848         synchronized (dispatchers) {
 849             EventDispatcher eventDispatcher = dispatchers.get(tg);
 850             if (eventDispatcher == null) {
 851                 eventDispatcher = new EventDispatcher();
 852                 dispatchers.put(tg, eventDispatcher);
 853                 eventDispatcher.start();
 854             }
 855             return eventDispatcher;
 856         }
 857     }
 858 
 859     /**
 860      * Send midi player events.
 861      * must not be synchronized on "this"
 862      */
 863     void sendMetaEvents(MidiMessage message) {
 864         if (metaEventListeners.size() == 0) return;
 865 
 866         getEventDispatcher().sendAudioEvents(message, metaEventListeners);
 867     }
 868 
 869     /**
 870      * Send midi player events.
 871      */
 872     void sendControllerEvents(MidiMessage message) {
 873         int size = controllerEventListeners.size();
 874         if (size == 0) return;
 875 
 876         if (! (message instanceof ShortMessage)) {
 877             return;
 878         }
 879         ShortMessage msg = (ShortMessage) message;
 880         int controller = msg.getData1();
 881         List<Object> sendToListeners = new ArrayList<>();
 882         for (int i = 0; i < size; i++) {
 883             ControllerListElement cve = controllerEventListeners.get(i);
 884             for(int j = 0; j < cve.controllers.length; j++) {
 885                 if (cve.controllers[j] == controller) {
 886                     sendToListeners.add(cve.listener);
 887                     break;
 888                 }
 889             }
 890         }
 891         getEventDispatcher().sendAudioEvents(message, sendToListeners);
 892     }
 893 
 894     private boolean needCaching() {
 895         return !isOpen() || (sequence == null) || (playThread == null);
 896     }
 897 
 898     /**
 899      * return the data pump instance, owned by play thread
 900      * if playthread is null, return null.
 901      * This method is guaranteed to return non-null if
 902      * needCaching returns false
 903      */
 904     private DataPump getDataPump() {
 905         if (playThread != null) {
 906             return playThread.getDataPump();
 907         }
 908         return null;
 909     }
 910 
 911     private MidiUtils.TempoCache getTempoCache() {
 912         return tempoCache;
 913     }
 914 
 915     private static boolean[] ensureBoolArraySize(boolean[] array, int desiredSize) {
 916         if (array == null) {
 917             return new boolean[desiredSize];
 918         }
 919         if (array.length < desiredSize) {
 920             boolean[] newArray = new boolean[desiredSize];
 921             System.arraycopy(array, 0, newArray, 0, array.length);
 922             return newArray;
 923         }
 924         return array;
 925     }
 926 
 927     // OVERRIDES OF ABSTRACT MIDI DEVICE METHODS
 928 
 929     @Override
 930     protected boolean hasReceivers() {
 931         return true;
 932     }
 933 
 934     // for recording
 935     @Override
 936     protected Receiver createReceiver() throws MidiUnavailableException {
 937         return new SequencerReceiver();
 938     }
 939 
 940     @Override
 941     protected boolean hasTransmitters() {
 942         return true;
 943     }
 944 
 945     @Override
 946     protected Transmitter createTransmitter() throws MidiUnavailableException {
 947         return new SequencerTransmitter();
 948     }
 949 
 950     // interface AutoConnectSequencer
 951     @Override
 952     public void setAutoConnect(Receiver autoConnectedReceiver) {
 953         this.autoConnect = (autoConnectedReceiver != null);
 954         this.autoConnectedReceiver = autoConnectedReceiver;
 955     }
 956 
 957     /**
 958      * An own class to distinguish the class name from
 959      * the transmitter of other devices.
 960      */
 961     private class SequencerTransmitter extends BasicTransmitter {
 962         private SequencerTransmitter() {
 963             super();
 964         }
 965     }
 966 
 967     final class SequencerReceiver extends AbstractReceiver {
 968 
 969         @Override
 970         void implSend(MidiMessage message, long timeStamp) {
 971             if (recording) {
 972                 long tickPos = 0;
 973 
 974                 // convert timeStamp to ticks
 975                 if (timeStamp < 0) {
 976                     tickPos = getTickPosition();
 977                 } else {
 978                     synchronized(tempoCache) {
 979                         tickPos = MidiUtils.microsecond2tick(sequence, timeStamp, tempoCache);
 980                     }
 981                 }
 982 
 983                 // and record to the first matching Track
 984                 Track track = null;
 985                 // do not record real-time events
 986                 // see 5048381: NullPointerException when saving a MIDI sequence
 987                 if (message.getLength() > 1) {
 988                     if (message instanceof ShortMessage) {
 989                         ShortMessage sm = (ShortMessage) message;
 990                         // all real-time messages have 0xF in the high nibble of the status byte
 991                         if ((sm.getStatus() & 0xF0) != 0xF0) {
 992                             track = RecordingTrack.get(recordingTracks, sm.getChannel());
 993                         }
 994                     } else {
 995                         // $$jb: where to record meta, sysex events?
 996                         // $$fb: the first recording track
 997                         track = RecordingTrack.get(recordingTracks, -1);
 998                     }
 999                     if (track != null) {
1000                         // create a copy of this message
1001                         if (message instanceof ShortMessage) {
1002                             message = new FastShortMessage((ShortMessage) message);
1003                         } else {
1004                             message = (MidiMessage) message.clone();
1005                         }
1006 
1007                         // create new MidiEvent
1008                         MidiEvent me = new MidiEvent(message, tickPos);
1009                         track.add(me);
1010                     }
1011                 }
1012             }
1013         }
1014     }
1015 
1016     private static class RealTimeSequencerInfo extends MidiDevice.Info {
1017 
1018         private static final String name = "Real Time Sequencer";
1019         private static final String vendor = "Oracle Corporation";
1020         private static final String description = "Software sequencer";
1021         private static final String version = "Version 1.0";
1022 
1023         RealTimeSequencerInfo() {
1024             super(name, vendor, description, version);
1025         }
1026     } // class Info
1027 
1028     private static class ControllerListElement {
1029 
1030         // $$jb: using an array for controllers b/c its
1031         //       easier to deal with than turning all the
1032         //       ints into objects to use a Vector
1033         int []  controllers;
1034         final ControllerEventListener listener;
1035 
1036         private ControllerListElement(ControllerEventListener listener, int[] controllers) {
1037 
1038             this.listener = listener;
1039             if (controllers == null) {
1040                 controllers = new int[128];
1041                 for (int i = 0; i < 128; i++) {
1042                     controllers[i] = i;
1043                 }
1044             }
1045             this.controllers = controllers;
1046         }
1047 
1048         private void addControllers(int[] c) {
1049 
1050             if (c==null) {
1051                 controllers = new int[128];
1052                 for (int i = 0; i < 128; i++) {
1053                     controllers[i] = i;
1054                 }
1055                 return;
1056             }
1057             int[] temp = new int[ controllers.length + c.length ];
1058             int elements;
1059 
1060             // first add what we have
1061             for(int i=0; i<controllers.length; i++) {
1062                 temp[i] = controllers[i];
1063             }
1064             elements = controllers.length;
1065             // now add the new controllers only if we don't already have them
1066             for(int i=0; i<c.length; i++) {
1067                 boolean flag = false;
1068 
1069                 for(int j=0; j<controllers.length; j++) {
1070                     if (c[i] == controllers[j]) {
1071                         flag = true;
1072                         break;
1073                     }
1074                 }
1075                 if (!flag) {
1076                     temp[elements++] = c[i];
1077                 }
1078             }
1079             // now keep only the elements we need
1080             int[] newc = new int[ elements ];
1081             for(int i=0; i<elements; i++){
1082                 newc[i] = temp[i];
1083             }
1084             controllers = newc;
1085         }
1086 
1087         private void removeControllers(int[] c) {
1088 
1089             if (c==null) {
1090                 controllers = new int[0];
1091             } else {
1092                 int[] temp = new int[ controllers.length ];
1093                 int elements = 0;
1094 
1095 
1096                 for(int i=0; i<controllers.length; i++){
1097                     boolean flag = false;
1098                     for(int j=0; j<c.length; j++) {
1099                         if (controllers[i] == c[j]) {
1100                             flag = true;
1101                             break;
1102                         }
1103                     }
1104                     if (!flag){
1105                         temp[elements++] = controllers[i];
1106                     }
1107                 }
1108                 // now keep only the elements remaining
1109                 int[] newc = new int[ elements ];
1110                 for(int i=0; i<elements; i++) {
1111                     newc[i] = temp[i];
1112                 }
1113                 controllers = newc;
1114 
1115             }
1116         }
1117 
1118         private int[] getControllers() {
1119 
1120             // return a copy of our array of controllers,
1121             // so others can't mess with it
1122             if (controllers == null) {
1123                 return null;
1124             }
1125 
1126             int[] c = new int[controllers.length];
1127 
1128             for(int i=0; i<controllers.length; i++){
1129                 c[i] = controllers[i];
1130             }
1131             return c;
1132         }
1133 
1134     } // class ControllerListElement
1135 
1136     static class RecordingTrack {
1137 
1138         private final Track track;
1139         private int channel;
1140 
1141         RecordingTrack(Track track, int channel) {
1142             this.track = track;
1143             this.channel = channel;
1144         }
1145 
1146         static RecordingTrack get(List<RecordingTrack> recordingTracks, Track track) {
1147 
1148             synchronized(recordingTracks) {
1149                 int size = recordingTracks.size();
1150 
1151                 for (int i = 0; i < size; i++) {
1152                     RecordingTrack current = recordingTracks.get(i);
1153                     if (current.track == track) {
1154                         return current;
1155                     }
1156                 }
1157             }
1158             return null;
1159         }
1160 
1161         static Track get(List<RecordingTrack> recordingTracks, int channel) {
1162 
1163             synchronized(recordingTracks) {
1164                 int size = recordingTracks.size();
1165                 for (int i = 0; i < size; i++) {
1166                     RecordingTrack current = recordingTracks.get(i);
1167                     if ((current.channel == channel) || (current.channel == -1)) {
1168                         return current.track;
1169                     }
1170                 }
1171             }
1172             return null;
1173 
1174         }
1175     }
1176 
1177     final class PlayThread implements Runnable {
1178         private Thread thread;
1179         private final Object lock = new Object();
1180 
1181         /** true if playback is interrupted (in close) */
1182         boolean interrupted = false;
1183         boolean isPumping = false;
1184 
1185         private final DataPump dataPump = new DataPump();
1186 
1187 
1188         PlayThread() {
1189             // nearly MAX_PRIORITY
1190             int priority = Thread.NORM_PRIORITY
1191                 + ((Thread.MAX_PRIORITY - Thread.NORM_PRIORITY) * 3) / 4;
1192             thread = JSSecurityManager.createThread(this,
1193                                                     "Java Sound Sequencer", // name
1194                                                     false,                  // daemon
1195                                                     priority,               // priority
1196                                                     true);                  // doStart
1197         }
1198 
1199         DataPump getDataPump() {
1200             return dataPump;
1201         }
1202 
1203         synchronized void setSequence(Sequence seq) {
1204             dataPump.setSequence(seq);
1205         }
1206 
1207 
1208         /** start thread and pump. Requires up-to-date tempoCache */
1209         synchronized void start() {
1210             // mark the sequencer running
1211             running = true;
1212 
1213             if (!dataPump.hasCachedTempo()) {
1214                 long tickPos = getTickPosition();
1215                 dataPump.setTempoMPQ(tempoCache.getTempoMPQAt(tickPos));
1216             }
1217             dataPump.checkPointMillis = 0; // means restarted
1218             dataPump.clearNoteOnCache();
1219             dataPump.needReindex = true;
1220 
1221             dataPump.resetLoopCount();
1222 
1223             // notify the thread
1224             synchronized(lock) {
1225                 lock.notifyAll();
1226             }
1227         }
1228 
1229         // waits until stopped
1230         synchronized void stop() {
1231             playThreadImplStop();
1232             long t = System.nanoTime() / 1000000l;
1233             while (isPumping) {
1234                 synchronized(lock) {
1235                     try {
1236                         lock.wait(2000);
1237                     } catch (InterruptedException ie) {
1238                         // ignore
1239                     }
1240                 }
1241                 // don't wait for more than 2 seconds
1242                 if ((System.nanoTime()/1000000l) - t > 1900) {
1243                     if (Printer.err) Printer.err("Waited more than 2 seconds in RealTimeSequencer.PlayThread.stop()!");
1244                     //break;
1245                 }
1246             }
1247         }
1248 
1249         void playThreadImplStop() {
1250             // mark the sequencer running
1251             running = false;
1252             synchronized(lock) {
1253                 lock.notifyAll();
1254             }
1255         }
1256 
1257         void close() {
1258             Thread oldThread = null;
1259             synchronized (this) {
1260                 // dispose of thread
1261                 interrupted = true;
1262                 oldThread = thread;
1263                 thread = null;
1264             }
1265             if (oldThread != null) {
1266                 // wake up the thread if it's in wait()
1267                 synchronized(lock) {
1268                     lock.notifyAll();
1269                 }
1270             }
1271             // wait for the thread to terminate itself,
1272             // but max. 2 seconds. Must not be synchronized!
1273             if (oldThread != null) {
1274                 try {
1275                     oldThread.join(2000);
1276                 } catch (InterruptedException ie) {}
1277             }
1278         }
1279 
1280         /**
1281          * Main process loop driving the media flow.
1282          *
1283          * Make sure to NOT synchronize on RealTimeSequencer
1284          * anywhere here (even implicit). That is a sure deadlock!
1285          */
1286         @Override
1287         public void run() {
1288 
1289             while (!interrupted) {
1290                 boolean EOM = false;
1291                 boolean wasRunning = running;
1292                 isPumping = !interrupted && running;
1293                 while (!EOM && !interrupted && running) {
1294                     EOM = dataPump.pump();
1295 
1296                     try {
1297                         Thread.sleep(1);
1298                     } catch (InterruptedException ie) {
1299                         // ignore
1300                     }
1301                 }
1302 
1303                 playThreadImplStop();
1304                 if (wasRunning) {
1305                     dataPump.notesOff(true);
1306                 }
1307                 if (EOM) {
1308                     dataPump.setTickPos(sequence.getTickLength());
1309 
1310                     // send EOT event (mis-used for end of media)
1311                     MetaMessage message = new MetaMessage();
1312                     try{
1313                         message.setMessage(MidiUtils.META_END_OF_TRACK_TYPE, new byte[0], 0);
1314                     } catch(InvalidMidiDataException e1) {}
1315                     sendMetaEvents(message);
1316                 }
1317                 synchronized (lock) {
1318                     isPumping = false;
1319                     // wake up a waiting stop() method
1320                     lock.notifyAll();
1321                     while (!running && !interrupted) {
1322                         try {
1323                             lock.wait();
1324                         } catch (Exception ex) {}
1325                     }
1326                 }
1327             } // end of while(!EOM && !interrupted && running)
1328         }
1329     }
1330 
1331     /**
1332      * class that does the actual dispatching of events,
1333      * used to be in native in MMAPI.
1334      */
1335     private class DataPump {
1336         private float currTempo;         // MPQ tempo
1337         private float tempoFactor;       // 1.0 is default
1338         private float inverseTempoFactor;// = 1.0 / tempoFactor
1339         private long ignoreTempoEventAt; // ignore next META tempo during playback at this tick pos only
1340         private int resolution;
1341         private float divisionType;
1342         private long checkPointMillis;   // microseconds at checkoint
1343         private long checkPointTick;     // ticks at checkpoint
1344         private int[] noteOnCache;       // bit-mask of notes that are currently on
1345         private Track[] tracks;
1346         private boolean[] trackDisabled; // if true, do not play this track
1347         private int[] trackReadPos;      // read index per track
1348         private long lastTick;
1349         private boolean needReindex = false;
1350         private int currLoopCounter = 0;
1351 
1352         //private sun.misc.Perf perf = sun.misc.Perf.getPerf();
1353         //private long perfFreq = perf.highResFrequency();
1354 
1355         DataPump() {
1356             init();
1357         }
1358 
1359         synchronized void init() {
1360             ignoreTempoEventAt = -1;
1361             tempoFactor = 1.0f;
1362             inverseTempoFactor = 1.0f;
1363             noteOnCache = new int[128];
1364             tracks = null;
1365             trackDisabled = null;
1366         }
1367 
1368         synchronized void setTickPos(long tickPos) {
1369             long oldLastTick = tickPos;
1370             lastTick = tickPos;
1371             if (running) {
1372                 notesOff(false);
1373             }
1374             if (running || tickPos > 0) {
1375                 // will also reindex
1376                 chaseEvents(oldLastTick, tickPos);
1377             } else {
1378                 needReindex = true;
1379             }
1380             if (!hasCachedTempo()) {
1381                 setTempoMPQ(getTempoCache().getTempoMPQAt(lastTick, currTempo));
1382                 // treat this as if it is a real time tempo change
1383                 ignoreTempoEventAt = -1;
1384             }
1385             // trigger re-configuration
1386             checkPointMillis = 0;
1387         }
1388 
1389         long getTickPos() {
1390             return lastTick;
1391         }
1392 
1393         // hasCachedTempo is only valid if it is the current position
1394         boolean hasCachedTempo() {
1395             if (ignoreTempoEventAt != lastTick) {
1396                 ignoreTempoEventAt = -1;
1397             }
1398             return ignoreTempoEventAt >= 0;
1399         }
1400 
1401         // this method is also used internally in the pump!
1402         synchronized void setTempoMPQ(float tempoMPQ) {
1403             if (tempoMPQ > 0 && tempoMPQ != currTempo) {
1404                 ignoreTempoEventAt = lastTick;
1405                 this.currTempo = tempoMPQ;
1406                 // re-calculate check point
1407                 checkPointMillis = 0;
1408             }
1409         }
1410 
1411         float getTempoMPQ() {
1412             return currTempo;
1413         }
1414 
1415         synchronized void setTempoFactor(float factor) {
1416             if (factor > 0 && factor != this.tempoFactor) {
1417                 tempoFactor = factor;
1418                 inverseTempoFactor = 1.0f / factor;
1419                 // re-calculate check point
1420                 checkPointMillis = 0;
1421             }
1422         }
1423 
1424         float getTempoFactor() {
1425             return tempoFactor;
1426         }
1427 
1428         synchronized void muteSoloChanged() {
1429             boolean[] newDisabled = makeDisabledArray();
1430             if (running) {
1431                 applyDisabledTracks(trackDisabled, newDisabled);
1432             }
1433             trackDisabled = newDisabled;
1434         }
1435 
1436         synchronized void setSequence(Sequence seq) {
1437             if (seq == null) {
1438                 init();
1439                 return;
1440             }
1441             tracks = seq.getTracks();
1442             muteSoloChanged();
1443             resolution = seq.getResolution();
1444             divisionType = seq.getDivisionType();
1445             trackReadPos = new int[tracks.length];
1446             // trigger re-initialization
1447             checkPointMillis = 0;
1448             needReindex = true;
1449         }
1450 
1451         synchronized void resetLoopCount() {
1452             currLoopCounter = loopCount;
1453         }
1454 
1455         void clearNoteOnCache() {
1456             for (int i = 0; i < 128; i++) {
1457                 noteOnCache[i] = 0;
1458             }
1459         }
1460 
1461         void notesOff(boolean doControllers) {
1462             int done = 0;
1463             for (int ch=0; ch<16; ch++) {
1464                 int channelMask = (1<<ch);
1465                 for (int i=0; i<128; i++) {
1466                     if ((noteOnCache[i] & channelMask) != 0) {
1467                         noteOnCache[i] ^= channelMask;
1468                         // send note on with velocity 0
1469                         getTransmitterList().sendMessage((ShortMessage.NOTE_ON | ch) | (i<<8), -1);
1470                         done++;
1471                     }
1472                 }
1473                 /* all notes off */
1474                 getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (123<<8), -1);
1475                 /* sustain off */
1476                 getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (64<<8), -1);
1477                 if (doControllers) {
1478                     /* reset all controllers */
1479                     getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (121<<8), -1);
1480                     done++;
1481                 }
1482             }
1483         }
1484 
1485         private boolean[] makeDisabledArray() {
1486             if (tracks == null) {
1487                 return null;
1488             }
1489             boolean[] newTrackDisabled = new boolean[tracks.length];
1490             boolean[] solo;
1491             boolean[] mute;
1492             synchronized(RealTimeSequencer.this) {
1493                 mute = trackMuted;
1494                 solo = trackSolo;
1495             }
1496             // if one track is solo, then only play solo
1497             boolean hasSolo = false;
1498             if (solo != null) {
1499                 for (int i = 0; i < solo.length; i++) {
1500                     if (solo[i]) {
1501                         hasSolo = true;
1502                         break;
1503                     }
1504                 }
1505             }
1506             if (hasSolo) {
1507                 // only the channels with solo play, regardless of mute
1508                 for (int i = 0; i < newTrackDisabled.length; i++) {
1509                     newTrackDisabled[i] = (i >= solo.length) || (!solo[i]);
1510                 }
1511             } else {
1512                 // mute the selected channels
1513                 for (int i = 0; i < newTrackDisabled.length; i++) {
1514                     newTrackDisabled[i] = (mute != null) && (i < mute.length) && (mute[i]);
1515                 }
1516             }
1517             return newTrackDisabled;
1518         }
1519 
1520         /**
1521          * chase all events from beginning of Track
1522          * and send note off for those events that are active
1523          * in noteOnCache array.
1524          * It is possible, of course, to catch notes from other tracks,
1525          * but better than more complicated logic to detect
1526          * which notes are really from this track
1527          */
1528         private void sendNoteOffIfOn(Track track, long endTick) {
1529             int size = track.size();
1530             int done = 0;
1531             try {
1532                 for (int i = 0; i < size; i++) {
1533                     MidiEvent event = track.get(i);
1534                     if (event.getTick() > endTick) break;
1535                     MidiMessage msg = event.getMessage();
1536                     int status = msg.getStatus();
1537                     int len = msg.getLength();
1538                     if (len == 3 && ((status & 0xF0) == ShortMessage.NOTE_ON)) {
1539                         int note = -1;
1540                         if (msg instanceof ShortMessage) {
1541                             ShortMessage smsg = (ShortMessage) msg;
1542                             if (smsg.getData2() > 0) {
1543                                 // only consider Note On with velocity > 0
1544                                 note = smsg.getData1();
1545                             }
1546                         } else {
1547                             byte[] data = msg.getMessage();
1548                             if ((data[2] & 0x7F) > 0) {
1549                                 // only consider Note On with velocity > 0
1550                                 note = data[1] & 0x7F;
1551                             }
1552                         }
1553                         if (note >= 0) {
1554                             int bit = 1<<(status & 0x0F);
1555                             if ((noteOnCache[note] & bit) != 0) {
1556                                 // the bit is set. Send Note Off
1557                                 getTransmitterList().sendMessage(status | (note<<8), -1);
1558                                 // clear the bit
1559                                 noteOnCache[note] &= (0xFFFF ^ bit);
1560                                 done++;
1561                             }
1562                         }
1563                     }
1564                 }
1565             } catch (ArrayIndexOutOfBoundsException aioobe) {
1566                 // this happens when messages are removed
1567                 // from the track while this method executes
1568             }
1569         }
1570 
1571         /**
1572          * Runtime application of mute/solo:
1573          * if a track is muted that was previously playing, send
1574          *    note off events for all currently playing notes.
1575          */
1576         private void applyDisabledTracks(boolean[] oldDisabled, boolean[] newDisabled) {
1577             byte[][] tempArray = null;
1578             synchronized(RealTimeSequencer.this) {
1579                 for (int i = 0; i < newDisabled.length; i++) {
1580                     if (((oldDisabled == null)
1581                          || (i >= oldDisabled.length)
1582                          || !oldDisabled[i])
1583                         && newDisabled[i]) {
1584                         // case that a track gets muted: need to
1585                         // send appropriate note off events to prevent
1586                         // hanging notes
1587 
1588                         if (tracks.length > i) {
1589                             sendNoteOffIfOn(tracks[i], lastTick);
1590                         }
1591                     }
1592                     else if ((oldDisabled != null)
1593                              && (i < oldDisabled.length)
1594                              && oldDisabled[i]
1595                              && !newDisabled[i]) {
1596                         // case that a track was muted and is now unmuted
1597                         // need to chase events and re-index this track
1598                         if (tempArray == null) {
1599                             tempArray = new byte[128][16];
1600                         }
1601                         chaseTrackEvents(i, 0, lastTick, true, tempArray);
1602                     }
1603                 }
1604             }
1605         }
1606 
1607         /** go through all events from startTick to endTick
1608          * chase the controller state and program change state
1609          * and then set the end-states at once.
1610          *
1611          * needs to be called in synchronized state
1612          * @param tempArray an byte[128][16] to hold controller messages
1613          */
1614         private void chaseTrackEvents(int trackNum,
1615                                       long startTick,
1616                                       long endTick,
1617                                       boolean doReindex,
1618                                       byte[][] tempArray) {
1619             if (startTick > endTick) {
1620                 // start from the beginning
1621                 startTick = 0;
1622             }
1623             byte[] progs = new byte[16];
1624             // init temp array with impossible values
1625             for (int ch = 0; ch < 16; ch++) {
1626                 progs[ch] = -1;
1627                 for (int co = 0; co < 128; co++) {
1628                     tempArray[co][ch] = -1;
1629                 }
1630             }
1631             Track track = tracks[trackNum];
1632             int size = track.size();
1633             try {
1634                 for (int i = 0; i < size; i++) {
1635                     MidiEvent event = track.get(i);
1636                     if (event.getTick() >= endTick) {
1637                         if (doReindex && (trackNum < trackReadPos.length)) {
1638                             trackReadPos[trackNum] = (i > 0)?(i-1):0;
1639                         }
1640                         break;
1641                     }
1642                     MidiMessage msg = event.getMessage();
1643                     int status = msg.getStatus();
1644                     int len = msg.getLength();
1645                     if (len == 3 && ((status & 0xF0) == ShortMessage.CONTROL_CHANGE)) {
1646                         if (msg instanceof ShortMessage) {
1647                             ShortMessage smsg = (ShortMessage) msg;
1648                             tempArray[smsg.getData1() & 0x7F][status & 0x0F] = (byte) smsg.getData2();
1649                         } else {
1650                             byte[] data = msg.getMessage();
1651                             tempArray[data[1] & 0x7F][status & 0x0F] = data[2];
1652                         }
1653                     }
1654                     if (len == 2 && ((status & 0xF0) == ShortMessage.PROGRAM_CHANGE)) {
1655                         if (msg instanceof ShortMessage) {
1656                             ShortMessage smsg = (ShortMessage) msg;
1657                             progs[status & 0x0F] = (byte) smsg.getData1();
1658                         } else {
1659                             byte[] data = msg.getMessage();
1660                             progs[status & 0x0F] = data[1];
1661                         }
1662                     }
1663                 }
1664             } catch (ArrayIndexOutOfBoundsException aioobe) {
1665                 // this happens when messages are removed
1666                 // from the track while this method executes
1667             }
1668             int numControllersSent = 0;
1669             // now send out the aggregated controllers and program changes
1670             for (int ch = 0; ch < 16; ch++) {
1671                 for (int co = 0; co < 128; co++) {
1672                     byte controllerValue = tempArray[co][ch];
1673                     if (controllerValue >= 0) {
1674                         int packedMsg = (ShortMessage.CONTROL_CHANGE | ch) | (co<<8) | (controllerValue<<16);
1675                         getTransmitterList().sendMessage(packedMsg, -1);
1676                         numControllersSent++;
1677                     }
1678                 }
1679                 // send program change *after* controllers, to
1680                 // correctly initialize banks
1681                 if (progs[ch] >= 0) {
1682                     getTransmitterList().sendMessage((ShortMessage.PROGRAM_CHANGE | ch) | (progs[ch]<<8), -1);
1683                 }
1684                 if (progs[ch] >= 0 || startTick == 0 || endTick == 0) {
1685                     // reset pitch bend on this channel (E0 00 40)
1686                     getTransmitterList().sendMessage((ShortMessage.PITCH_BEND | ch) | (0x40 << 16), -1);
1687                     // reset sustain pedal on this channel
1688                     getTransmitterList().sendMessage((ShortMessage.CONTROL_CHANGE | ch) | (64 << 8), -1);
1689                 }
1690             }
1691         }
1692 
1693         /**
1694          * chase controllers and program for all tracks.
1695          */
1696         synchronized void chaseEvents(long startTick, long endTick) {
1697             byte[][] tempArray = new byte[128][16];
1698             for (int t = 0; t < tracks.length; t++) {
1699                 if ((trackDisabled == null)
1700                     || (trackDisabled.length <= t)
1701                     || (!trackDisabled[t])) {
1702                     // if track is not disabled, chase the events for it
1703                     chaseTrackEvents(t, startTick, endTick, true, tempArray);
1704                 }
1705             }
1706         }
1707 
1708         // playback related methods (pumping)
1709 
1710         private long getCurrentTimeMillis() {
1711             return System.nanoTime() / 1000000l;
1712             //return perf.highResCounter() * 1000 / perfFreq;
1713         }
1714 
1715         private long millis2tick(long millis) {
1716             if (divisionType != Sequence.PPQ) {
1717                 double dTick = ((((double) millis) * tempoFactor)
1718                                 * ((double) divisionType)
1719                                 * ((double) resolution))
1720                     / ((double) 1000);
1721                 return (long) dTick;
1722             }
1723             return MidiUtils.microsec2ticks(millis * 1000,
1724                                             currTempo * inverseTempoFactor,
1725                                             resolution);
1726         }
1727 
1728         private long tick2millis(long tick) {
1729             if (divisionType != Sequence.PPQ) {
1730                 double dMillis = ((((double) tick) * 1000) /
1731                                   (tempoFactor * ((double) divisionType) * ((double) resolution)));
1732                 return (long) dMillis;
1733             }
1734             return MidiUtils.ticks2microsec(tick,
1735                                             currTempo * inverseTempoFactor,
1736                                             resolution) / 1000;
1737         }
1738 
1739         private void ReindexTrack(int trackNum, long tick) {
1740             if (trackNum < trackReadPos.length && trackNum < tracks.length) {
1741                 trackReadPos[trackNum] = MidiUtils.tick2index(tracks[trackNum], tick);
1742             }
1743         }
1744 
1745         /* returns if changes are pending */
1746         private boolean dispatchMessage(int trackNum, MidiEvent event) {
1747             boolean changesPending = false;
1748             MidiMessage message = event.getMessage();
1749             int msgStatus = message.getStatus();
1750             int msgLen = message.getLength();
1751             if (msgStatus == MetaMessage.META && msgLen >= 2) {
1752                 // a meta message. Do not send it to the device.
1753                 // 0xFF with length=1 is a MIDI realtime message
1754                 // which shouldn't be in a Sequence, but we play it
1755                 // nonetheless.
1756 
1757                 // see if this is a tempo message. Only on track 0.
1758                 if (trackNum == 0) {
1759                     int newTempo = MidiUtils.getTempoMPQ(message);
1760                     if (newTempo > 0) {
1761                         if (event.getTick() != ignoreTempoEventAt) {
1762                             setTempoMPQ(newTempo); // sets ignoreTempoEventAt!
1763                             changesPending = true;
1764                         }
1765                         // next loop, do not ignore anymore tempo events.
1766                         ignoreTempoEventAt = -1;
1767                     }
1768                 }
1769                 // send to listeners
1770                 sendMetaEvents(message);
1771 
1772             } else {
1773                 // not meta, send to device
1774                 getTransmitterList().sendMessage(message, -1);
1775 
1776                 switch (msgStatus & 0xF0) {
1777                 case ShortMessage.NOTE_OFF: {
1778                     // note off - clear the bit in the noteOnCache array
1779                     int note = ((ShortMessage) message).getData1() & 0x7F;
1780                     noteOnCache[note] &= (0xFFFF ^ (1<<(msgStatus & 0x0F)));
1781                     break;
1782                 }
1783 
1784                 case ShortMessage.NOTE_ON: {
1785                     // note on
1786                     ShortMessage smsg = (ShortMessage) message;
1787                     int note = smsg.getData1() & 0x7F;
1788                     int vel = smsg.getData2() & 0x7F;
1789                     if (vel > 0) {
1790                         // if velocity > 0 set the bit in the noteOnCache array
1791                         noteOnCache[note] |= 1<<(msgStatus & 0x0F);
1792                     } else {
1793                         // if velocity = 0 clear the bit in the noteOnCache array
1794                         noteOnCache[note] &= (0xFFFF ^ (1<<(msgStatus & 0x0F)));
1795                     }
1796                     break;
1797                 }
1798 
1799                 case ShortMessage.CONTROL_CHANGE:
1800                     // if controller message, send controller listeners
1801                     sendControllerEvents(message);
1802                     break;
1803 
1804                 }
1805             }
1806             return changesPending;
1807         }
1808 
1809         /** the main pump method
1810          * @return true if end of sequence is reached
1811          */
1812         synchronized boolean pump() {
1813             long currMillis;
1814             long targetTick = lastTick;
1815             MidiEvent currEvent;
1816             boolean changesPending = false;
1817             boolean doLoop = false;
1818             boolean EOM = false;
1819 
1820             currMillis = getCurrentTimeMillis();
1821             int finishedTracks = 0;
1822             do {
1823                 changesPending = false;
1824 
1825                 // need to re-find indexes in tracks?
1826                 if (needReindex) {
1827                     if (trackReadPos.length < tracks.length) {
1828                         trackReadPos = new int[tracks.length];
1829                     }
1830                     for (int t = 0; t < tracks.length; t++) {
1831                         ReindexTrack(t, targetTick);
1832                     }
1833                     needReindex = false;
1834                     checkPointMillis = 0;
1835                 }
1836 
1837                 // get target tick from current time in millis
1838                 if (checkPointMillis == 0) {
1839                     // new check point
1840                     currMillis = getCurrentTimeMillis();
1841                     checkPointMillis = currMillis;
1842                     targetTick = lastTick;
1843                     checkPointTick = targetTick;
1844                 } else {
1845                     // calculate current tick based on current time in milliseconds
1846                     targetTick = checkPointTick + millis2tick(currMillis - checkPointMillis);
1847                     if ((loopEnd != -1)
1848                         && ((loopCount > 0 && currLoopCounter > 0)
1849                             || (loopCount == LOOP_CONTINUOUSLY))) {
1850                         if (lastTick <= loopEnd && targetTick >= loopEnd) {
1851                             // need to loop!
1852                             // only play until loop end
1853                             targetTick = loopEnd - 1;
1854                             doLoop = true;
1855                         }
1856                     }
1857                     lastTick = targetTick;
1858                 }
1859 
1860                 finishedTracks = 0;
1861 
1862                 for (int t = 0; t < tracks.length; t++) {
1863                     try {
1864                         boolean disabled = trackDisabled[t];
1865                         Track thisTrack = tracks[t];
1866                         int readPos = trackReadPos[t];
1867                         int size = thisTrack.size();
1868                         // play all events that are due until targetTick
1869                         while (!changesPending && (readPos < size)
1870                                && (currEvent = thisTrack.get(readPos)).getTick() <= targetTick) {
1871 
1872                             if ((readPos == size -1) &&  MidiUtils.isMetaEndOfTrack(currEvent.getMessage())) {
1873                                 // do not send out this message. Finished with this track
1874                                 readPos = size;
1875                                 break;
1876                             }
1877                             // TODO: some kind of heuristics if the MIDI messages have changed
1878                             // significantly (i.e. deleted or inserted a bunch of messages)
1879                             // since last time. Would need to set needReindex = true then
1880                             readPos++;
1881                             // only play this event if the track is enabled,
1882                             // or if it is a tempo message on track 0
1883                             // Note: cannot put this check outside
1884                             //       this inner loop in order to detect end of file
1885                             if (!disabled ||
1886                                 ((t == 0) && (MidiUtils.isMetaTempo(currEvent.getMessage())))) {
1887                                 changesPending = dispatchMessage(t, currEvent);
1888                             }
1889                         }
1890                         if (readPos >= size) {
1891                             finishedTracks++;
1892                         }
1893                         trackReadPos[t] = readPos;
1894                     } catch(Exception e) {
1895                         if (Printer.err) e.printStackTrace();
1896                         if (e instanceof ArrayIndexOutOfBoundsException) {
1897                             needReindex = true;
1898                             changesPending = true;
1899                         }
1900                     }
1901                     if (changesPending) {
1902                         break;
1903                     }
1904                 }
1905                 EOM = (finishedTracks == tracks.length);
1906                 if (doLoop
1907                     || ( ((loopCount > 0 && currLoopCounter > 0)
1908                           || (loopCount == LOOP_CONTINUOUSLY))
1909                          && !changesPending
1910                          && (loopEnd == -1)
1911                          && EOM)) {
1912 
1913                     long oldCheckPointMillis = checkPointMillis;
1914                     long loopEndTick = loopEnd;
1915                     if (loopEndTick == -1) {
1916                         loopEndTick = lastTick;
1917                     }
1918 
1919                     // need to loop back!
1920                     if (loopCount != LOOP_CONTINUOUSLY) {
1921                         currLoopCounter--;
1922                     }
1923                     setTickPos(loopStart);
1924                     // now patch the checkPointMillis so that
1925                     // it points to the exact beginning of when the loop was finished
1926 
1927                     // $$fb TODO: although this is mathematically correct (i.e. the loop position
1928                     //            is correct, and doesn't drift away with several repetition,
1929                     //            there is a slight lag when looping back, probably caused
1930                     //            by the chasing.
1931 
1932                     checkPointMillis = oldCheckPointMillis + tick2millis(loopEndTick - checkPointTick);
1933                     checkPointTick = loopStart;
1934                     // no need for reindexing, is done in setTickPos
1935                     needReindex = false;
1936                     changesPending = false;
1937                     // reset doLoop flag
1938                     doLoop = false;
1939                     EOM = false;
1940                 }
1941             } while (changesPending);
1942 
1943             return EOM;
1944         }
1945     } // class DataPump
1946 }