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 }