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