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