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 }