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 
091         *      for 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
116         *      stream to play.
117         * 
118         *      @param a 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         *      @param a The audio stream to play.
128         *      @param deviceName The device to play the audio to. 
129         */
130        public AudioPlayer( final AudioStream a, final String deviceName )
131        {
132                this.stream = a;
133                this.deviceName = deviceName;           
134                this.setTimecodeObject( new AudioTimecode(0) );
135        }
136        
137        /**
138         *      Set the length of the sound line's buffer in milliseconds. The longer
139         *      the buffer the less likely the soundline will be to pop but the shorter
140         *      the buffer the closer to real-time the sound output will be. This value
141         *      must be set before the audio line is opened otherwise it will have no
142         *      effect.
143         *      @param ms The length of the sound line in milliseconds.
144         */
145        public void setSoundLineBufferSize( final double ms )
146        {
147                this.soundLineBufferSize = ms;
148        }
149        
150        /**
151         *      Add the given audio event listener to this player.
152         *      @param l The listener to add.
153         */
154        public void addAudioEventListener( final AudioEventListener l )
155        {
156                this.listeners.add( l );
157        }
158        
159        /**
160         *      Remove the given event from the listeners on this player.
161         *      @param l The listener to remove.
162         */
163        public void removeAudioEventListener( final AudioEventListener l )
164        {
165                this.listeners.remove( l );
166        }
167
168        /**
169         *      Fires the audio ended event to the listeners.
170         *      @param as The audio stream that ended
171         */
172        protected void fireAudioEnded( final AudioStream as )
173        {
174                for( final AudioEventListener ael : this.listeners )
175                        ael.audioEnded();
176        }
177        
178        /**
179         *      Fires an event that says the samples will be played.
180         *      @param sc The samples to play
181         */
182        protected void fireBeforePlay( final SampleChunk sc )
183        {
184                for( final AudioEventListener ael: this.listeners )
185                        ael.beforePlay( sc );
186        }
187        
188        /**
189         *      Fires an event that says the samples have been played.
190         *      @param sc The sampled have been played
191         */
192        protected void fireAfterPlay( final SampleChunk sc )
193        {
194                for( final AudioEventListener ael: this.listeners )
195                        ael.afterPlay( this, sc );
196        }
197        
198        /**
199         *      Set the timecode object that is updated as the audio is played.
200         *  @param t The timecode object.
201         */
202        public void setTimecodeObject( final AudioTimecode t )
203        {
204                this.currentTimecode = t;
205        }
206        
207        /**
208         *      Returns the current timecode.
209         *      @return The timecode object.
210         */
211        public Timecode getTimecodeObject()
212        {
213                return this.currentTimecode;
214        }
215        
216        /**
217         *      {@inheritDoc}
218         *      @see java.lang.Runnable#run()
219         */
220        @Override
221        public void run()
222        {
223                this.setMode( Mode.PLAY );
224                this.timecodeReadAt = 0;
225                if( !this.started )
226                {
227                        this.started = true;
228                        try
229                        {
230                                // Open the sound system.
231                                this.openJavaSound();
232                        
233                                // Read samples until there are no more.
234                                SampleChunk samples = null;
235                                boolean ended = false;
236                                while( !ended && this.mode != Mode.STOP )
237                                {
238                                        if( this.mode == Mode.PLAY )
239                                        {
240                                                // Get the next sample chunk
241                                                samples = this.stream.nextSampleChunk();
242                                                
243                                                // Check if we've reached the end of the line
244                                                if( samples == null )
245                                                {
246                                                        ended = true;
247                                                        continue;
248                                                }
249                                                
250                                                // Fire the before event
251                                                this.fireBeforePlay( samples );
252                                                
253                                                // Play the samples
254                                                this.playJavaSound( samples );
255                                                
256                                                // Fire the after event
257                                                this.fireAfterPlay( samples );
258        
259                                                // If we have a timecode object to update, we'll update it here
260                                                if( this.currentTimecode != null )
261                                                {
262                                                        this.currentTimestamp = samples.getStartTimecode().
263                                                                        getTimecodeInMilliseconds(); 
264                                                        this.timecodeReadAt = System.currentTimeMillis();
265                                                        this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp );
266                                                }
267                                        }
268                                        else
269                                        {
270                                                // Let's be nice and not loop madly if we're not playing
271                                                // (we must be in PAUSE mode)
272                                                try
273                                                {
274                                                        Thread.sleep( 500 );
275                                                }
276                                                catch( final InterruptedException ie )
277                                                {
278                                                }
279                                        }
280                                }
281                                        
282                                // Fire the audio ended event
283                                this.fireAudioEnded( this.stream );
284                                this.setMode( Mode.STOP );
285                                this.reset();
286                        }
287                        catch( final Exception e )
288                        {
289                                e.printStackTrace();
290                        }
291                        finally
292                        {
293                                // Close the sound system
294                                this.closeJavaSound();                  
295                        }
296                }
297                else
298                {
299                        // Already playing something, so we just start going again
300                        this.setMode( Mode.PLAY );
301                }
302        }
303        
304        /**
305         *      Create a new audio player in a separate thread for playing audio.
306         * 
307         *      @param as The audio stream to play.
308         *      @return The audio player created.
309         */
310        public static AudioPlayer createAudioPlayer( final AudioStream as )
311        {
312                final AudioPlayer ap = new AudioPlayer( as );
313                new Thread( ap ).start();
314                return ap;
315        }
316
317        /**
318         *      Create a new audio player in a separate thread for playing audio.
319         *      To find out device names, use {@link AudioUtils#getDevices()}.
320         * 
321         *      @param as The audio stream to play.
322         *      @param device The name of the device to use.
323         *      @return The audio player created.
324         */
325        public static AudioPlayer createAudioPlayer( final AudioStream as, final String device )
326        {
327                final AudioPlayer ap = new AudioPlayer( as, device );
328                new Thread( ap ).start();
329                return ap;
330        }
331
332        /**
333         *      Open a line to the Java Sound APIs.
334         * 
335         *      @throws Exception if the Java sound system could not be initialised.
336         */
337        private void openJavaSound() throws Exception
338        {
339                try
340                {
341                        // Get a line (either the one we ask for, or any one).
342                        if( this.deviceName != null )
343                                        this.mLine = AudioUtils.getJavaOutputLine( this.deviceName, this.stream.getFormat() );
344                        else    this.mLine = AudioUtils.getAnyJavaOutputLine( this.stream.getFormat() );
345
346                        if( this.mLine == null )
347                                throw new Exception( "Cannot instantiate a sound line." );
348                        
349                        // If no exception has been thrown we open the line.
350                        this.mLine.open( this.mLine.getFormat(), (int)
351                                        (this.stream.getFormat().getSampleRateKHz() * this.soundLineBufferSize) );
352
353                        // If we've opened the line, we start it running
354                        this.mLine.start();
355                        
356                        System.out.println( "Opened Java Sound Line: "+this.mLine.getFormat() );
357                }
358                catch( final LineUnavailableException e )
359                {
360                        throw new Exception( "Could not open Java Sound audio line for" +
361                                        " the audio format "+this.stream.getFormat() );
362                }
363        }
364
365        /**
366         *      Play the given sample chunk to the Java sound line. The line should be
367         *      set up to accept the samples that we're going to give it, as we did
368         *      that in the {@link #openJavaSound()} method.
369         * 
370         *      @param chunk The chunk to play.
371         */
372        private void playJavaSound( final SampleChunk chunk )
373        {
374                final byte[] rawBytes = chunk.getSamples();
375                this.mLine.write( rawBytes, 0, rawBytes.length );
376        }
377
378        /**
379         *      Close down the Java sound APIs.
380         */
381        private void closeJavaSound()
382        {
383                if( this.mLine != null )
384                {
385                        // Wait for the buffer to empty...
386                        this.mLine.drain();
387                        
388                        // ...then close
389                        this.mLine.close();
390                        this.mLine = null;
391                }
392        }
393
394        /**
395         *      {@inheritDoc}
396         *      @see org.openimaj.time.TimeKeeper#getTime()
397         */
398        @Override
399        public AudioTimecode getTime()
400        {
401                // If we've not yet read any samples, just return the timecode
402                // object as it was first given to us.
403                if( this.timecodeReadAt == 0 )
404                        return this.currentTimecode;
405                
406                // Update the timecode if we're playing (otherwise we'll return the
407                // latest timecode)
408                if( this.mode == Mode.PLAY )
409                        this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp +
410                                (System.currentTimeMillis() - this.timecodeReadAt) );
411                
412                return this.currentTimecode;
413        }
414
415        /**
416         *      {@inheritDoc}
417         *      @see org.openimaj.time.TimeKeeper#stop()
418         */
419        @Override
420        public void stop()
421        {
422                this.setMode( Mode.STOP );
423        }
424
425        /**
426         *      Set the mode of the player.
427         *      @param m
428         */
429        public void setMode( final Mode m )
430        {
431                this.mode = m;
432        }
433        
434        /**
435         *      {@inheritDoc}
436         *      @see org.openimaj.time.TimeKeeper#supportsPause()
437         */
438        @Override
439        public boolean supportsPause()
440        {
441                return true;
442        }
443        
444        /**
445         *      {@inheritDoc}
446         *      @see org.openimaj.time.TimeKeeper#supportsSeek()
447         */
448        @Override
449        public boolean supportsSeek()
450        {
451                return true;
452        }
453
454        /**
455         *      {@inheritDoc}
456         *      @see org.openimaj.time.TimeKeeper#seek(long)
457         */
458        @Override
459        public void seek( final long timestamp )
460        {
461                this.stream.seek( timestamp );
462        }
463
464        /**
465         *      {@inheritDoc}
466         *      @see org.openimaj.time.TimeKeeper#reset()
467         */
468        @Override
469        public void reset()
470        {
471                this.timecodeReadAt = 0;
472                this.currentTimestamp = 0;
473                this.started = false;
474                this.currentTimecode.setTimecodeInMilliseconds( 0 );
475                this.stream.reset();
476        }
477
478        /**
479         *      {@inheritDoc}
480         *      @see org.openimaj.time.TimeKeeper#pause()
481         */
482        @Override
483        public void pause()
484        {
485                this.setMode( Mode.PAUSE );
486                
487                // Set the current timecode to the time at which we paused.
488                this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp +
489                                (System.currentTimeMillis() - this.timecodeReadAt) );
490        }
491}