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