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