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                 // We need to disable autoclosing mechanism otherwise the clip
 177                 // can be closed after "!clip.isOpen()" check, because of
 178                 // previous inactivity.
 179                 clip.setAutoClosing(false);
 180                 try {
 181                     if (!clip.isOpen()) {
 182                         clip.open(loadedAudioFormat, loadedAudio, 0,
 183                                   loadedAudioByteLength);
 184                     } else {
 185                         clip.flush();
 186                         if (loop != clipLooping) {
 187                             // need to stop in case the looped status changed
 188                             clip.stop();
 189                         }
 190                     }
 191                     clip.setFramePosition(0);
 192                     if (loop) {
 193                         clip.loop(Clip.LOOP_CONTINUOUSLY);
 194                     } else {
 195                         clip.start();
 196                     }
 197                     clipLooping = loop;
 198                 } finally {
 199                     clip.setAutoClosing(true);
 200                 }
 201             } else if (datapusher != null ) {
 202                 datapusher.start(loop);
 203                 if (DEBUG || Printer.debug)Printer.debug("Stream should be playing/looping");
 204 
 205             } else if (sequencer != null) {
 206                 sequencerloop = loop;
 207                 if (sequencer.isRunning()) {
 208                     sequencer.setMicrosecondPosition(0);
 209                 }
 210                 if (!sequencer.isOpen()) {
 211                     try {
 212                         sequencer.open();
 213                         sequencer.setSequence(sequence);
 214 
 215                     } catch (InvalidMidiDataException e1) {
 216                         if (DEBUG || Printer.err)e1.printStackTrace();
 217                     } catch (MidiUnavailableException e2) {
 218                         if (DEBUG || Printer.err)e2.printStackTrace();
 219                     }
 220                 }
 221                 sequencer.addMetaEventListener(this);
 222                 try {
 223                     sequencer.start();
 224                 } catch (Exception e) {
 225                     if (DEBUG || Printer.err) e.printStackTrace();
 226                 }
 227                 if (DEBUG || Printer.debug)Printer.debug("Sequencer should be playing/looping");
 228             }
 229         } catch (Exception e) {
 230             if (DEBUG || Printer.err)e.printStackTrace();
 231         }
 232     }
 233 
 234     @Override
 235     public synchronized void stop() {
 236         if (!success) {
 237             return;
 238         }
 239 
 240         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip->stop()");
 241         lastPlayCall = 0;
 242 
 243         if (clip != null) {
 244             try {
 245                 if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.flush()");
 246                 clip.flush();
 247             } catch (Exception e1) {
 248                 if (Printer.err) e1.printStackTrace();
 249             }
 250             try {
 251                 if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip: clip.stop()");
 252                 clip.stop();
 253             } catch (Exception e2) {
 254                 if (Printer.err) e2.printStackTrace();
 255             }
 256             if (DEBUG || Printer.debug)Printer.debug("Clip should be stopped");
 257 
 258         } else if (datapusher != null) {
 259             datapusher.stop();
 260             if (DEBUG || Printer.debug)Printer.debug("Stream should be stopped");
 261 
 262         } else if (sequencer != null) {
 263             try {
 264                 sequencerloop = false;
 265                 sequencer.removeMetaEventListener(this);
 266                 sequencer.stop();
 267             } catch (Exception e3) {
 268                 if (Printer.err) e3.printStackTrace();
 269             }
 270             try {
 271                 sequencer.close();
 272             } catch (Exception e4) {
 273                 if (Printer.err) e4.printStackTrace();
 274             }
 275             if (DEBUG || Printer.debug)Printer.debug("Sequencer should be stopped");
 276         }
 277     }
 278 
 279     // Event handlers (for debugging)
 280 
 281     @Override
 282     public synchronized void update(LineEvent event) {
 283         if (DEBUG || Printer.debug) Printer.debug("line event received: "+event);
 284     }
 285 
 286     // handle MIDI track end meta events for looping
 287 
 288     @Override
 289     public synchronized void meta(MetaMessage message) {
 290 
 291         if (DEBUG || Printer.debug)Printer.debug("META EVENT RECEIVED!!!!! ");
 292 
 293         if( message.getType() == 47 ) {
 294             if (sequencerloop){
 295                 //notifyAll();
 296                 sequencer.setMicrosecondPosition(0);
 297                 loop();
 298             } else {
 299                 stop();
 300             }
 301         }
 302     }
 303 
 304     @Override
 305     public String toString() {
 306         return getClass().toString();
 307     }
 308 
 309     @Override
 310     protected void finalize() {
 311 
 312         if (clip != null) {
 313             if (DEBUG || Printer.trace)Printer.trace("JavaSoundAudioClip.finalize: clip.close()");
 314             clip.close();
 315         }
 316 
 317         //$$fb 2001-09-26: may improve situation related to bug #4302884
 318         if (datapusher != null) {
 319             datapusher.close();
 320         }
 321 
 322         if (sequencer != null) {
 323             sequencer.close();
 324         }
 325     }
 326 
 327     // FILE LOADING METHODS
 328 
 329     private boolean loadAudioData(AudioInputStream as)  throws IOException, UnsupportedAudioFileException {
 330         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip->openAsClip()");
 331 
 332         // first possibly convert this stream to PCM
 333         as = Toolkit.getPCMConvertedAudioInputStream(as);
 334         if (as == null) {
 335             return false;
 336         }
 337 
 338         loadedAudioFormat = as.getFormat();
 339         long frameLen = as.getFrameLength();
 340         int frameSize = loadedAudioFormat.getFrameSize();
 341         long byteLen = AudioSystem.NOT_SPECIFIED;
 342         if (frameLen != AudioSystem.NOT_SPECIFIED
 343             && frameLen > 0
 344             && frameSize != AudioSystem.NOT_SPECIFIED
 345             && frameSize > 0) {
 346             byteLen = frameLen * frameSize;
 347         }
 348         if (byteLen != AudioSystem.NOT_SPECIFIED) {
 349             // if the stream length is known, it can be efficiently loaded into memory
 350             readStream(as, byteLen);
 351         } else {
 352             // otherwise we use a ByteArrayOutputStream to load it into memory
 353             readStream(as);
 354         }
 355 
 356         // if everything went fine, we have now the audio data in
 357         // loadedAudio, and the byte length in loadedAudioByteLength
 358         return true;
 359     }
 360 
 361     private void readStream(AudioInputStream as, long byteLen) throws IOException {
 362         // arrays "only" max. 2GB
 363         int intLen;
 364         if (byteLen > 2147483647) {
 365             intLen = 2147483647;
 366         } else {
 367             intLen = (int) byteLen;
 368         }
 369         loadedAudio = new byte[intLen];
 370         loadedAudioByteLength = 0;
 371 
 372         // this loop may throw an IOException
 373         while (true) {
 374             int bytesRead = as.read(loadedAudio, loadedAudioByteLength, intLen - loadedAudioByteLength);
 375             if (bytesRead <= 0) {
 376                 as.close();
 377                 break;
 378             }
 379             loadedAudioByteLength += bytesRead;
 380         }
 381     }
 382 
 383     private void readStream(AudioInputStream as) throws IOException {
 384 
 385         DirectBAOS baos = new DirectBAOS();
 386         byte buffer[] = new byte[16384];
 387         int bytesRead = 0;
 388         int totalBytesRead = 0;
 389 
 390         // this loop may throw an IOException
 391         while( true ) {
 392             bytesRead = as.read(buffer, 0, buffer.length);
 393             if (bytesRead <= 0) {
 394                 as.close();
 395                 break;
 396             }
 397             totalBytesRead += bytesRead;
 398             baos.write(buffer, 0, bytesRead);
 399         }
 400         loadedAudio = baos.getInternalBuffer();
 401         loadedAudioByteLength = totalBytesRead;
 402     }
 403 
 404     // METHODS FOR CREATING THE DEVICE
 405 
 406     private boolean createClip() {
 407 
 408         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip.createClip()");
 409 
 410         try {
 411             DataLine.Info info = new DataLine.Info(Clip.class, loadedAudioFormat);
 412             if (!(AudioSystem.isLineSupported(info)) ) {
 413                 if (DEBUG || Printer.err)Printer.err("Clip not supported: "+loadedAudioFormat);
 414                 // fail silently
 415                 return false;
 416             }
 417             Object line = AudioSystem.getLine(info);
 418             if (!(line instanceof AutoClosingClip)) {
 419                 if (DEBUG || Printer.err)Printer.err("Clip is not auto closing!"+clip);
 420                 // fail -> will try with SourceDataLine
 421                 return false;
 422             }
 423             clip = (AutoClosingClip) line;
 424             clip.setAutoClosing(true);
 425             if (DEBUG || Printer.debug) clip.addLineListener(this);
 426         } catch (Exception e) {
 427             if (DEBUG || Printer.err)e.printStackTrace();
 428             // fail silently
 429             return false;
 430         }
 431 
 432         if (clip==null) {
 433             // fail silently
 434             return false;
 435         }
 436 
 437         if (DEBUG || Printer.debug)Printer.debug("Loaded clip.");
 438         return true;
 439     }
 440 
 441     private boolean createSourceDataLine() {
 442         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip.createSourceDataLine()");
 443         try {
 444             DataLine.Info info = new DataLine.Info(SourceDataLine.class, loadedAudioFormat);
 445             if (!(AudioSystem.isLineSupported(info)) ) {
 446                 if (DEBUG || Printer.err)Printer.err("Line not supported: "+loadedAudioFormat);
 447                 // fail silently
 448                 return false;
 449             }
 450             SourceDataLine source = (SourceDataLine) AudioSystem.getLine(info);
 451             datapusher = new DataPusher(source, loadedAudioFormat, loadedAudio, loadedAudioByteLength);
 452         } catch (Exception e) {
 453             if (DEBUG || Printer.err)e.printStackTrace();
 454             // fail silently
 455             return false;
 456         }
 457 
 458         if (datapusher==null) {
 459             // fail silently
 460             return false;
 461         }
 462 
 463         if (DEBUG || Printer.debug)Printer.debug("Created SourceDataLine.");
 464         return true;
 465     }
 466 
 467     private boolean createSequencer(BufferedInputStream in) throws IOException {
 468 
 469         if (DEBUG || Printer.debug)Printer.debug("JavaSoundAudioClip.createSequencer()");
 470 
 471         // get the sequencer
 472         try {
 473             sequencer = MidiSystem.getSequencer( );
 474         } catch(MidiUnavailableException me) {
 475             if (DEBUG || Printer.err)me.printStackTrace();
 476             return false;
 477         }
 478         if (sequencer==null) {
 479             return false;
 480         }
 481 
 482         try {
 483             sequence = MidiSystem.getSequence(in);
 484             if (sequence == null) {
 485                 return false;
 486             }
 487         } catch (InvalidMidiDataException e) {
 488             if (DEBUG || Printer.err)e.printStackTrace();
 489             return false;
 490         }
 491 
 492         if (DEBUG || Printer.debug)Printer.debug("Created Sequencer.");
 493         return true;
 494     }
 495 
 496     /*
 497      * private inner class representing a ByteArrayOutputStream
 498      * which allows retrieval of the internal array
 499      */
 500     private static class DirectBAOS extends ByteArrayOutputStream {
 501         DirectBAOS() {
 502             super();
 503         }
 504 
 505         public byte[] getInternalBuffer() {
 506             return buf;
 507         }
 508 
 509     } // class DirectBAOS
 510 }