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