001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030/**
031 *
032 */
033package org.openimaj.audio;
034
035import java.util.ArrayList;
036import java.util.List;
037
038import javax.sound.sampled.LineUnavailableException;
039import javax.sound.sampled.SourceDataLine;
040
041import org.openimaj.audio.timecode.AudioTimecode;
042import org.openimaj.audio.util.AudioUtils;
043import org.openimaj.time.TimeKeeper;
044import org.openimaj.time.Timecode;
045
046/**
047 * Wraps the Java Sound APIs into the OpenIMAJ audio core for playing sounds.
048 * <p>
049 * The {@link AudioPlayer} supports the {@link TimeKeeper} interface so that
050 * other methods can synchronise to the audio timestamps.
051 * <p>
052 * The Audio Player as a {@link TimeKeeper} supports seeking but it may be
053 * possible that the underlying stream does not support seeking so the seek
054 * method may not affect the time keeper as expected.
055 *
056 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
057 * @created 8 Jun 2011
058 *
059 */
060public class AudioPlayer implements Runnable, TimeKeeper<AudioTimecode>
061{
062        /** The audio stream being played */
063        private AudioStream stream = null;
064
065        /** The java audio output stream line */
066        private SourceDataLine mLine = null;
067
068        /** The current timecode being played */
069        private AudioTimecode currentTimecode = null;
070
071        /** The current audio timestamp */
072        private long currentTimestamp = 0;
073
074        /** At what timestamp the current timecode was read at */
075        private long timecodeReadAt = 0;
076
077        /** The device name on which to play */
078        private String deviceName = null;
079
080        /** The mode of the player */
081        private Mode mode = Mode.PLAY;
082
083        /** Listeners for events */
084        private final List<AudioEventListener> listeners = new ArrayList<AudioEventListener>();
085
086        /** Whether the system has been started */
087        private boolean started = false;
088
089        /**
090         * Number of milliseconds in the sound line buffer. < 100ms is good for
091         * real-time whereas the bigger the better for smooth sound reproduction
092         */
093        private double soundLineBufferSize = 100;
094
095        /**
096         * Enumerator for the current state of the audio player.
097         *
098         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
099         *
100         * @created 29 Nov 2011
101         */
102        public enum Mode
103        {
104                /** The audio player is playing */
105                PLAY,
106
107                /** The audio player is paused */
108                PAUSE,
109
110                /** The audio player is stopped */
111                STOP
112        }
113
114        /**
115         * Default constructor that takes an audio stream to play.
116         *
117         * @param a
118         *            The audio stream to play
119         */
120        public AudioPlayer(final AudioStream a)
121        {
122                this(a, null);
123        }
124
125        /**
126         * Play the given stream to a specific device.
127         *
128         * @param a
129         *            The audio stream to play.
130         * @param deviceName
131         *            The device to play the audio to.
132         */
133        public AudioPlayer(final AudioStream a, final String deviceName)
134        {
135                this.stream = a;
136                this.deviceName = deviceName;
137                this.setTimecodeObject(new AudioTimecode(0));
138        }
139
140        /**
141         * Set the length of the sound line's buffer in milliseconds. The longer the
142         * buffer the less likely the soundline will be to pop but the shorter the
143         * buffer the closer to real-time the sound output will be. This value must
144         * be set before the audio line is opened otherwise it will have no effect.
145         *
146         * @param ms
147         *            The length of the sound line in milliseconds.
148         */
149        public void setSoundLineBufferSize(final double ms)
150        {
151                this.soundLineBufferSize = ms;
152        }
153
154        /**
155         * Add the given audio event listener to this player.
156         *
157         * @param l
158         *            The listener to add.
159         */
160        public void addAudioEventListener(final AudioEventListener l)
161        {
162                this.listeners.add(l);
163        }
164
165        /**
166         * Remove the given event from the listeners on this player.
167         *
168         * @param l
169         *            The listener to remove.
170         */
171        public void removeAudioEventListener(final AudioEventListener l)
172        {
173                this.listeners.remove(l);
174        }
175
176        /**
177         * Fires the audio ended event to the listeners.
178         *
179         * @param as
180         *            The audio stream that ended
181         */
182        protected void fireAudioEnded(final AudioStream as)
183        {
184                for (final AudioEventListener ael : this.listeners)
185                        ael.audioEnded();
186        }
187
188        /**
189         * Fires an event that says the samples will be played.
190         *
191         * @param sc
192         *            The samples to play
193         */
194        protected void fireBeforePlay(final SampleChunk sc)
195        {
196                for (final AudioEventListener ael : this.listeners)
197                        ael.beforePlay(sc);
198        }
199
200        /**
201         * Fires an event that says the samples have been played.
202         *
203         * @param sc
204         *            The sampled have been played
205         */
206        protected void fireAfterPlay(final SampleChunk sc)
207        {
208                for (final AudioEventListener ael : this.listeners)
209                        ael.afterPlay(this, sc);
210        }
211
212        /**
213         * Set the timecode object that is updated as the audio is played.
214         *
215         * @param t
216         *            The timecode object.
217         */
218        public void setTimecodeObject(final AudioTimecode t)
219        {
220                this.currentTimecode = t;
221        }
222
223        /**
224         * Returns the current timecode.
225         *
226         * @return The timecode object.
227         */
228        public Timecode getTimecodeObject()
229        {
230                return this.currentTimecode;
231        }
232
233        /**
234         * {@inheritDoc}
235         *
236         * @see java.lang.Runnable#run()
237         */
238        @Override
239        public void run()
240        {
241                this.setMode(Mode.PLAY);
242                this.timecodeReadAt = 0;
243                if (!this.started)
244                {
245                        this.started = true;
246                        try
247                        {
248                                // Open the sound system.
249                                this.openJavaSound();
250
251                                // Read samples until there are no more.
252                                SampleChunk samples = null;
253                                boolean ended = false;
254                                while (!ended && this.mode != Mode.STOP)
255                                {
256                                        if (this.mode == Mode.PLAY)
257                                        {
258                                                // System.out.println("loop");
259                                                // Get the next sample chunk
260                                                samples = this.stream.nextSampleChunk();
261
262                                                // Check if we've reached the end of the line
263                                                if (samples == null)
264                                                {
265                                                        ended = true;
266                                                        continue;
267                                                }
268
269                                                // Fire the before event
270                                                this.fireBeforePlay(samples);
271
272                                                // Play the samples
273                                                this.playJavaSound(samples);
274
275                                                // Fire the after event
276                                                this.fireAfterPlay(samples);
277
278                                                // If we have a timecode object to update, we'll update
279                                                // it here
280                                                if (this.currentTimecode != null)
281                                                {
282                                                        this.currentTimestamp = samples.getStartTimecode().
283                                                                        getTimecodeInMilliseconds();
284                                                        this.timecodeReadAt = System.currentTimeMillis();
285                                                        this.currentTimecode.setTimecodeInMilliseconds(this.currentTimestamp);
286                                                }
287                                        }
288                                        else
289                                        {
290                                                // Let's be nice and not loop madly if we're not playing
291                                                // (we must be in PAUSE mode)
292                                                try
293                                                {
294                                                        Thread.sleep(500);
295                                                } catch (final InterruptedException ie)
296                                                {
297                                                }
298                                        }
299                                }
300
301                                // Fire the audio ended event
302                                this.fireAudioEnded(this.stream);
303                                this.setMode(Mode.STOP);
304                                this.reset();
305                        } catch (final Exception e)
306                        {
307                                e.printStackTrace();
308                        } finally
309                        {
310                                // Close the sound system
311                                this.closeJavaSound();
312                        }
313                }
314                else
315                {
316                        // Already playing something, so we just start going again
317                        this.setMode(Mode.PLAY);
318                }
319        }
320
321        /**
322         * Create a new audio player in a separate thread for playing audio.
323         *
324         * @param as
325         *            The audio stream to play.
326         * @return The audio player created.
327         */
328        public static AudioPlayer createAudioPlayer(final AudioStream as)
329        {
330                final AudioPlayer ap = new AudioPlayer(as);
331                new Thread(ap).start();
332                return ap;
333        }
334
335        /**
336         * Create a new audio player in a separate thread for playing audio. To find
337         * out device names, use {@link AudioUtils#getDevices()}.
338         *
339         * @param as
340         *            The audio stream to play.
341         * @param device
342         *            The name of the device to use.
343         * @return The audio player created.
344         */
345        public static AudioPlayer createAudioPlayer(final AudioStream as, final String device)
346        {
347                final AudioPlayer ap = new AudioPlayer(as, device);
348                new Thread(ap).start();
349                return ap;
350        }
351
352        /**
353         * Open a line to the Java Sound APIs.
354         *
355         * @throws Exception
356         *             if the Java sound system could not be initialised.
357         */
358        private void openJavaSound() throws Exception
359        {
360                try
361                {
362                        // Get a line (either the one we ask for, or any one).
363                        if (this.deviceName != null)
364                                this.mLine = AudioUtils.getJavaOutputLine(this.deviceName, this.stream.getFormat());
365                        else
366                                this.mLine = AudioUtils.getAnyJavaOutputLine(this.stream.getFormat());
367
368                        if (this.mLine == null)
369                                throw new Exception("Cannot instantiate a sound line.");
370
371                        // If no exception has been thrown we open the line.
372                        this.mLine.open(this.mLine.getFormat(), (int)
373                                        (this.stream.getFormat().getSampleRateKHz() * this.soundLineBufferSize));
374
375                        // If we've opened the line, we start it running
376                        this.mLine.start();
377
378                        System.out.println("Opened Java Sound Line: " + this.mLine.getFormat());
379                } catch (final LineUnavailableException e)
380                {
381                        throw new Exception("Could not open Java Sound audio line for" +
382                                        " the audio format " + this.stream.getFormat());
383                }
384        }
385
386        /**
387         * Play the given sample chunk to the Java sound line. The line should be
388         * set up to accept the samples that we're going to give it, as we did that
389         * in the {@link #openJavaSound()} method.
390         *
391         * @param chunk
392         *            The chunk to play.
393         */
394        private void playJavaSound(final SampleChunk chunk)
395        {
396                final byte[] rawBytes = chunk.getSamples();
397                this.mLine.write(rawBytes, 0, rawBytes.length);
398        }
399
400        /**
401         * Close down the Java sound APIs.
402         */
403        private void closeJavaSound()
404        {
405                if (this.mLine != null)
406                {
407                        // Wait for the buffer to empty...
408                        this.mLine.drain();
409
410                        // ...then close
411                        this.mLine.close();
412                        this.mLine = null;
413                }
414        }
415
416        /**
417         * {@inheritDoc}
418         *
419         * @see org.openimaj.time.TimeKeeper#getTime()
420         */
421        @Override
422        public AudioTimecode getTime()
423        {
424                // If we've not yet read any samples, just return the timecode
425                // object as it was first given to us.
426                if (this.timecodeReadAt == 0)
427                        return this.currentTimecode;
428
429                // Update the timecode if we're playing (otherwise we'll return the
430                // latest timecode)
431                if (this.mode == Mode.PLAY)
432                        this.currentTimecode.setTimecodeInMilliseconds(this.currentTimestamp +
433                                        (System.currentTimeMillis() - this.timecodeReadAt));
434
435                return this.currentTimecode;
436        }
437
438        /**
439         * {@inheritDoc}
440         *
441         * @see org.openimaj.time.TimeKeeper#stop()
442         */
443        @Override
444        public void stop()
445        {
446                this.setMode(Mode.STOP);
447        }
448
449        /**
450         * Set the mode of the player.
451         *
452         * @param m
453         */
454        public void setMode(final Mode m)
455        {
456                this.mode = m;
457        }
458
459        /**
460         * {@inheritDoc}
461         *
462         * @see org.openimaj.time.TimeKeeper#supportsPause()
463         */
464        @Override
465        public boolean supportsPause()
466        {
467                return true;
468        }
469
470        /**
471         * {@inheritDoc}
472         *
473         * @see org.openimaj.time.TimeKeeper#supportsSeek()
474         */
475        @Override
476        public boolean supportsSeek()
477        {
478                return true;
479        }
480
481        /**
482         * {@inheritDoc}
483         *
484         * @see org.openimaj.time.TimeKeeper#seek(long)
485         */
486        @Override
487        public void seek(final long timestamp)
488        {
489                this.stream.seek(timestamp);
490        }
491
492        /**
493         * {@inheritDoc}
494         *
495         * @see org.openimaj.time.TimeKeeper#reset()
496         */
497        @Override
498        public void reset()
499        {
500                this.timecodeReadAt = 0;
501                this.currentTimestamp = 0;
502                this.started = false;
503                this.currentTimecode.setTimecodeInMilliseconds(0);
504                this.stream.reset();
505        }
506
507        /**
508         * {@inheritDoc}
509         *
510         * @see org.openimaj.time.TimeKeeper#pause()
511         */
512        @Override
513        public void pause()
514        {
515                this.setMode(Mode.PAUSE);
516
517                // Set the current timecode to the time at which we paused.
518                this.currentTimecode.setTimecodeInMilliseconds(this.currentTimestamp +
519                                (System.currentTimeMillis() - this.timecodeReadAt));
520        }
521}