1 /*
   2  * Copyright (c) 1999, 2016, 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.applet.AudioClip;
  29 import java.io.BufferedInputStream;
  30 import java.io.ByteArrayOutputStream;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 
  34 import javax.sound.midi.InvalidMidiDataException;
  35 import javax.sound.midi.MetaEventListener;
  36 import javax.sound.midi.MetaMessage;
  37 import javax.sound.midi.MidiFileFormat;
  38 import javax.sound.midi.MidiSystem;
  39 import javax.sound.midi.MidiUnavailableException;
  40 import javax.sound.midi.Sequence;
  41 import javax.sound.midi.Sequencer;
  42 import javax.sound.sampled.AudioFormat;
  43 import javax.sound.sampled.AudioInputStream;
  44 import javax.sound.sampled.AudioSystem;
  45 import javax.sound.sampled.Clip;
  46 import javax.sound.sampled.DataLine;
  47 import javax.sound.sampled.LineEvent;
  48 import javax.sound.sampled.LineListener;
  49 import javax.sound.sampled.SourceDataLine;
  50 import javax.sound.sampled.UnsupportedAudioFileException;
  51 
  52 /**
  53  * Java Sound audio clip;
  54  *
  55  * @author Arthur van Hoff, Kara Kytle, Jan Borgersen
  56  * @author Florian Bomers
  57  */
  58 @SuppressWarnings("deprecation")
  59 public final class JavaSoundAudioClip implements AudioClip, MetaEventListener, LineListener {
  60 
  61     private static final boolean DEBUG = false;
  62     private static final int BUFFER_SIZE = 16384; // number of bytes written each time to the source data line
  63 
  64     private long lastPlayCall = 0;
  65     private static final int MINIMUM_PLAY_DELAY = 30;
  66 
  67     private byte loadedAudio[] = null;
  68     private int loadedAudioByteLength = 0;
  69     private AudioFormat loadedAudioFormat = null;
  70 
  71     private AutoClosingClip clip = null;
  72     private boolean clipLooping = false;
  73 
  74     private DataPusher datapusher = null;
  75 
  76     private Sequencer sequencer = null;
  77     private Sequence sequence = null;
  78     private boolean sequencerloop = false;
  79 
  80     /**
  81      * used for determining how many samples is the
  82      * threshhold between playing as a Clip and streaming
  83      * from the file.
  84      *
  85      * $$jb: 11.07.99: the engine has a limit of 1M
  86      * samples to play as a Clip, so compare this number
  87      * with the number of samples in the stream.
  88      *
  89      */
  90     private static final long CLIP_THRESHOLD = 1048576;
  91     //private final static long CLIP_THRESHOLD = 1;
  92     private static final int STREAM_BUFFER_SIZE = 1024;
  93 
  94     public JavaSoundAudioClip(InputStream in) throws IOException {
  95         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip.<init>");
  96 
  97         BufferedInputStream bis = new BufferedInputStream(in, STREAM_BUFFER_SIZE);
  98         bis.mark(STREAM_BUFFER_SIZE);
  99         boolean success = false;
 100         try {
 101             AudioInputStream as = AudioSystem.getAudioInputStream(bis);
 102             // load the stream data into memory
 103             success = loadAudioData(as);
 104 
 105             if (success) {
 106                 success = false;
 107                 if (loadedAudioByteLength < CLIP_THRESHOLD) {
 108                     success = createClip();
 109                 }
 110                 if (!success) {
 111                     success = createSourceDataLine();
 112                 }
 113             }
 114         } catch (UnsupportedAudioFileException e) {
 115             // not an audio file
 116             try {
 117                 MidiFileFormat mff = MidiSystem.getMidiFileFormat(bis);
 118                 success = createSequencer(bis);
 119             } catch (InvalidMidiDataException e1) {
 120                 success = false;
 121             }
 122         }
 123         if (!success) {
 124             throw new IOException("Unable to create AudioClip from input stream");
 125         }
 126     }
 127 
 128     @Override
 129     public synchronized void play() {
 130         startImpl(false);
 131     }
 132 
 133     @Override
 134     public synchronized void loop() {
 135         startImpl(true);
 136     }
 137 
 138     private synchronized void startImpl(boolean loop) {
 139         // hack for some applets that call the start method very rapidly...
 140         long currentTime = System.currentTimeMillis();
 141         long diff = currentTime - lastPlayCall;
 142         if (diff < MINIMUM_PLAY_DELAY) {
 143             if (DEBUG || Printer.debug) Printer.debug("JavaSoundAudioClip.startImpl(loop="+loop+"): abort - too rapdly");
 144             return;
 145         }
 146         lastPlayCall = currentTime;
 147 
 148         if (DEBUG || Printer.debug) Printer.debug("JavaSoundAudioClip.startImpl(loop="+loop+")");
 149         try {
 150             if (clip != null) {
 151                 if (!clip.isOpen()) {
 152                     if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.open()");
 153                     clip.open(loadedAudioFormat, loadedAudio, 0, loadedAudioByteLength);
 154                 } else {
 155                     if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.flush()");
 156                     clip.flush();
 157                     if (loop != clipLooping) {
 158                         // need to stop in case the looped status changed
 159                         if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.stop()");
 160                         clip.stop();
 161                     }
 162                 }
 163                 clip.setFramePosition(0);
 164                 if (loop) {
 165                     if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.loop()");
 166                     clip.loop(Clip.LOOP_CONTINUOUSLY);
 167                 } else {
 168                     if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.start()");
 169                     clip.start();
 170                 }
 171                 clipLooping = loop;
 172                 if (DEBUG || Printer.debug)Printer.debug("Clip should be playing/looping");
 173 
 174             } else if (datapusher != null ) {
 175                 datapusher.start(loop);
 176                 if (DEBUG || Printer.debug)Printer.debug("Stream should be playing/looping");
 177 
 178             } else if (sequencer != null) {
 179                 sequencerloop = loop;
 180                 if (sequencer.isRunning()) {
 181                     sequencer.setMicrosecondPosition(0);
 182                 }
 183                 if (!sequencer.isOpen()) {
 184                     try {
 185                         sequencer.open();
 186                         sequencer.setSequence(sequence);
 187 
 188                     } catch (InvalidMidiDataException e1) {
 189                         if (DEBUG || Printer.err)e1.printStackTrace();
 190                     } catch (MidiUnavailableException e2) {
 191                         if (DEBUG || Printer.err)e2.printStackTrace();
 192                     }
 193                 }
 194                 sequencer.addMetaEventListener(this);
 195                 try {
 196                     sequencer.start();
 197                 } catch (Exception e) {
 198                     if (DEBUG || Printer.err) e.printStackTrace();
 199                 }
 200                 if (DEBUG || Printer.debug)Printer.debug("Sequencer should be playing/looping");
 201             }
 202         } catch (Exception e) {
 203             if (DEBUG || Printer.err)e.printStackTrace();
 204         }
 205     }
 206 
 207     @Override
 208     public synchronized void stop() {
 209 
 210         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip->stop()");
 211         lastPlayCall = 0;
 212 
 213         if (clip != null) {
 214             try {
 215                 if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.flush()");
 216                 clip.flush();
 217             } catch (Exception e1) {
 218                 if (Printer.err) e1.printStackTrace();
 219             }
 220             try {
 221                 if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.stop()");
 222                 clip.stop();
 223             } catch (Exception e2) {
 224                 if (Printer.err) e2.printStackTrace();
 225             }
 226             if (DEBUG || Printer.debug)Printer.debug("Clip should be stopped");
 227 
 228         } else if (datapusher != null) {
 229             datapusher.stop();
 230             if (DEBUG || Printer.debug)Printer.debug("Stream should be stopped");
 231 
 232         } else if (sequencer != null) {
 233             try {
 234                 sequencerloop = false;
 235                 sequencer.removeMetaEventListener(this);
 236                 sequencer.stop();
 237             } catch (Exception e3) {
 238                 if (Printer.err) e3.printStackTrace();
 239             }
 240             try {
 241                 sequencer.close();
 242             } catch (Exception e4) {
 243                 if (Printer.err) e4.printStackTrace();
 244             }
 245             if (DEBUG || Printer.debug)Printer.debug("Sequencer should be stopped");
 246         }
 247     }
 248 
 249     // Event handlers (for debugging)
 250 
 251     @Override
 252     public synchronized void update(LineEvent event) {
 253         if (DEBUG || Printer.debug) Printer.debug("line event received: "+event);
 254     }
 255 
 256     // handle MIDI track end meta events for looping
 257 
 258     @Override
 259     public synchronized void meta(MetaMessage message) {
 260 
 261         if (DEBUG || Printer.debug)Printer.debug("META EVENT RECEIVED!!!!! ");
 262 
 263         if( message.getType() == 47 ) {
 264             if (sequencerloop){
 265                 //notifyAll();
 266                 sequencer.setMicrosecondPosition(0);
 267                 loop();
 268             } else {
 269                 stop();
 270             }
 271         }
 272     }
 273 
 274     @Override
 275     public String toString() {
 276         return getClass().toString();
 277     }
 278 
 279     @Override
 280     protected void finalize() {
 281 
 282         if (clip != null) {
 283             if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip.finalize: clip.close()");
 284             clip.close();
 285         }
 286 
 287         //$$fb 2001-09-26: may improve situation related to bug #4302884
 288         if (datapusher != null) {
 289             datapusher.close();
 290         }
 291 
 292         if (sequencer != null) {
 293             sequencer.close();
 294         }
 295     }
 296 
 297     // FILE LOADING METHODS
 298 
 299     private boolean loadAudioData(AudioInputStream as)  throws IOException, UnsupportedAudioFileException {
 300         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip->openAsClip()");
 301 
 302         // first possibly convert this stream to PCM
 303         as = Toolkit.getPCMConvertedAudioInputStream(as);
 304         if (as == null) {
 305             return false;
 306         }
 307 
 308         loadedAudioFormat = as.getFormat();
 309         long frameLen = as.getFrameLength();
 310         int frameSize = loadedAudioFormat.getFrameSize();
 311         long byteLen = AudioSystem.NOT_SPECIFIED;
 312         if (frameLen != AudioSystem.NOT_SPECIFIED
 313             && frameLen > 0
 314             && frameSize != AudioSystem.NOT_SPECIFIED
 315             && frameSize > 0) {
 316             byteLen = frameLen * frameSize;
 317         }
 318         if (byteLen != AudioSystem.NOT_SPECIFIED) {
 319             // if the stream length is known, it can be efficiently loaded into memory
 320             readStream(as, byteLen);
 321         } else {
 322             // otherwise we use a ByteArrayOutputStream to load it into memory
 323             readStream(as);
 324         }
 325 
 326         // if everything went fine, we have now the audio data in
 327         // loadedAudio, and the byte length in loadedAudioByteLength
 328         return true;
 329     }
 330 
 331     private void readStream(AudioInputStream as, long byteLen) throws IOException {
 332         // arrays "only" max. 2GB
 333         int intLen;
 334         if (byteLen > 2147483647) {
 335             intLen = 2147483647;
 336         } else {
 337             intLen = (int) byteLen;
 338         }
 339         loadedAudio = new byte[intLen];
 340         loadedAudioByteLength = 0;
 341 
 342         // this loop may throw an IOException
 343         while (true) {
 344             int bytesRead = as.read(loadedAudio, loadedAudioByteLength, intLen - loadedAudioByteLength);
 345             if (bytesRead <= 0) {
 346                 as.close();
 347                 break;
 348             }
 349             loadedAudioByteLength += bytesRead;
 350         }
 351     }
 352 
 353     private void readStream(AudioInputStream as) throws IOException {
 354 
 355         DirectBAOS baos = new DirectBAOS();
 356         byte buffer[] = new byte[16384];
 357         int bytesRead = 0;
 358         int totalBytesRead = 0;
 359 
 360         // this loop may throw an IOException
 361         while( true ) {
 362             bytesRead = as.read(buffer, 0, buffer.length);
 363             if (bytesRead <= 0) {
 364                 as.close();
 365                 break;
 366             }
 367             totalBytesRead += bytesRead;
 368             baos.write(buffer, 0, bytesRead);
 369         }
 370         loadedAudio = baos.getInternalBuffer();
 371         loadedAudioByteLength = totalBytesRead;
 372     }
 373 
 374     // METHODS FOR CREATING THE DEVICE
 375 
 376     private boolean createClip() {
 377 
 378         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip.createClip()");
 379 
 380         try {
 381             DataLine.Info info = new DataLine.Info(Clip.class, loadedAudioFormat);
 382             if (!(AudioSystem.isLineSupported(info)) ) {
 383                 if (DEBUG || Printer.err)Printer.err("Clip not supported: "+loadedAudioFormat);
 384                 // fail silently
 385                 return false;
 386             }
 387             Object line = AudioSystem.getLine(info);
 388             if (!(line instanceof AutoClosingClip)) {
 389                 if (DEBUG || Printer.err)Printer.err("Clip is not auto closing!"+clip);
 390                 // fail -> will try with SourceDataLine
 391                 return false;
 392             }
 393             clip = (AutoClosingClip) line;
 394             clip.setAutoClosing(true);
 395             if (DEBUG || Printer.debug) clip.addLineListener(this);
 396         } catch (Exception e) {
 397             if (DEBUG || Printer.err)e.printStackTrace();
 398             // fail silently
 399             return false;
 400         }
 401 
 402         if (clip==null) {
 403             // fail silently
 404             return false;
 405         }
 406 
 407         if (DEBUG || Printer.debug)Printer.debug("Loaded clip.");
 408         return true;
 409     }
 410 
 411     private boolean createSourceDataLine() {
 412         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip.createSourceDataLine()");
 413         try {
 414             DataLine.Info info = new DataLine.Info(SourceDataLine.class, loadedAudioFormat);
 415             if (!(AudioSystem.isLineSupported(info)) ) {
 416                 if (DEBUG || Printer.err)Printer.err("Line not supported: "+loadedAudioFormat);
 417                 // fail silently
 418                 return false;
 419             }
 420             SourceDataLine source = (SourceDataLine) AudioSystem.getLine(info);
 421             datapusher = new DataPusher(source, loadedAudioFormat, loadedAudio, loadedAudioByteLength);
 422         } catch (Exception e) {
 423             if (DEBUG || Printer.err)e.printStackTrace();
 424             // fail silently
 425             return false;
 426         }
 427 
 428         if (datapusher==null) {
 429             // fail silently
 430             return false;
 431         }
 432 
 433         if (DEBUG || Printer.debug)Printer.debug("Created SourceDataLine.");
 434         return true;
 435     }
 436 
 437     private boolean createSequencer(BufferedInputStream in) throws IOException {
 438 
 439         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip.createSequencer()");
 440 
 441         // get the sequencer
 442         try {
 443             sequencer = MidiSystem.getSequencer( );
 444         } catch(MidiUnavailableException me) {
 445             if (DEBUG || Printer.err)me.printStackTrace();
 446             return false;
 447         }
 448         if (sequencer==null) {
 449             return false;
 450         }
 451 
 452         try {
 453             sequence = MidiSystem.getSequence(in);
 454             if (sequence == null) {
 455                 return false;
 456             }
 457         } catch (InvalidMidiDataException e) {
 458             if (DEBUG || Printer.err)e.printStackTrace();
 459             return false;
 460         }
 461 
 462         if (DEBUG || Printer.debug)Printer.debug("Created Sequencer.");
 463         return true;
 464     }
 465 
 466     /*
 467      * private inner class representing a ByteArrayOutputStream
 468      * which allows retrieval of the internal array
 469      */
 470     private static class DirectBAOS extends ByteArrayOutputStream {
 471         DirectBAOS() {
 472             super();
 473         }
 474 
 475         public byte[] getInternalBuffer() {
 476             return buf;
 477         }
 478 
 479     } // class DirectBAOS
 480 }