001package jmri.jmrit.audio;
002
003import com.jogamp.openal.AL;
004import com.jogamp.openal.ALExt;
005import java.util.Queue;
006import javax.vecmath.Vector3f;
007
008/**
009 * JOAL implementation of the Audio Source sub-class.
010 * <p>
011 * For now, no system-specific implementations are forseen - this will remain
012 * internal-only
013 * <br><br><hr><br><b>
014 * This software is based on or using the JOAL Library available from
015 * <a href="http://jogamp.org/joal/www/">http://jogamp.org/joal/www/</a>
016 * </b><br><br>
017 * JOAL is released under the BSD license. The full license terms follow:
018 * <br><i>
019 * Copyright (c) 2003-2006 Sun Microsystems, Inc. All Rights Reserved.
020 * <br>
021 * Redistribution and use in source and binary forms, with or without
022 * modification, are permitted provided that the following conditions are
023 * met:
024 * <br>
025 * - Redistribution of source code must retain the above copyright
026 *   notice, this list of conditions and the following disclaimer.
027 * <br>
028 * - Redistribution in binary form must reproduce the above copyright
029 *   notice, this list of conditions and the following disclaimer in the
030 *   documentation and/or other materials provided with the distribution.
031 * <br>
032 * Neither the name of Sun Microsystems, Inc. or the names of
033 * contributors may be used to endorse or promote products derived from
034 * this software without specific prior written permission.
035 * <br>
036 * This software is provided "AS IS," without a warranty of any kind. ALL
037 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
038 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
039 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
040 * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
041 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
042 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
043 * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
044 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
045 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
046 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
047 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
048 * <br>
049 * You acknowledge that this software is not designed or intended for use
050 * in the design, construction, operation or maintenance of any nuclear
051 * facility.
052 * <br><br><br></i>
053 * <hr>
054 * This file is part of JMRI.
055 * <p>
056 * JMRI is free software; you can redistribute it and/or modify it under the
057 * terms of version 2 of the GNU General Public License as published by the Free
058 * Software Foundation. See the "COPYING" file for a copy of this license.
059 * <p>
060 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
061 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
062 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
063 *
064 * @author Matthew Harris copyright (c) 2009
065 */
066public class JoalAudioSource extends AbstractAudioSource {
067
068    private static AL al = JoalAudioFactory.getAL();
069
070    private boolean _initialised = false;
071
072    private int[] _source = new int[1];
073
074    private int[] _alState = new int[1];
075
076    /**
077     * Constructor for new JoalAudioSource with system name
078     *
079     * @param systemName AudioSource object system name (e.g. IAS1)
080     */
081    public JoalAudioSource(String systemName) {
082        super(systemName);
083        log.debug("New JoalAudioSource: {}", systemName);
084        _initialised = init();
085    }
086
087    /**
088     * Constructor for new JoalAudioSource with system name and user name
089     *
090     * @param systemName AudioSource object system name (e.g. IAS1)
091     * @param userName   AudioSource object user name
092     */
093    public JoalAudioSource(String systemName, String userName) {
094        super(systemName, userName);
095        if (log.isDebugEnabled()) {
096            log.debug("New JoalAudioSource: {} ({})", userName, systemName);
097        }
098        _initialised = init();
099    }
100
101    /**
102     * Initialise this AudioSource
103     *
104     * @return True if initialised
105     */
106    private boolean init() {
107        // Check that the JoalAudioFactory exists
108        if (al == null) {
109            log.warn("Al Factory not yet initialised");
110            return false;
111        }
112
113        // Now, check that the audio command thread exists
114        if (!isAudioAlive()) {
115            log.debug("Command thread not yet alive...");
116            return false;
117        } else {
118            log.debug("Command thread is alive - continue.");
119        }
120
121        // Generate the AudioSource
122        al.alGenSources(1, _source, 0);
123        if (JoalAudioFactory.checkALError()) {
124            log.warn("Error creating JoalSource ({})", this.getSystemName());
125            _source = null;
126            return false;
127        }
128        return true;
129    }
130
131    /**
132     * Queue a single AudioBuffer on this source.
133     *
134     * (called from DefaultAudioFactory command queue)
135     *
136     * @param audioBuffer AudioBuffer to queue
137     * @return True if successfully queued.
138     */
139    @Override
140    public boolean queueAudioBuffer(AudioBuffer audioBuffer) {
141        // First check we've been initialised
142        if (!_initialised || !isAudioAlive()) {
143            log.error("Source Not Initialized: {}", this.getSystemName());
144            return false;
145        }
146
147        if (audioBuffer instanceof JoalAudioBuffer) {
148            int[] bids = new int[1];
149            // Make an int[] of the buffer ids
150            bids[0] = ((JoalAudioBuffer) audioBuffer).getDataStorageBuffer()[0];
151            if (log.isDebugEnabled()) {
152                log.debug("Queueing Buffer: {} bid: {} Source: {}", audioBuffer.getSystemName(), ((JoalAudioBuffer) audioBuffer).getDataStorageBuffer()[0], this.getSystemName());
153            }
154
155            // Bind this AudioSource to the specified AudioBuffer
156            al.alSourceQueueBuffers(_source[0], 1, bids, 0);
157            if (JoalAudioFactory.checkALError()) {
158                log.warn("Error queueing JoalSource ({}) to AudioBuffers ({})", this.getSystemName(), audioBuffer.getDisplayName());
159                return false;
160            }
161
162            if (log.isDebugEnabled()) {
163                log.debug("Queue JoalAudioBuffer ({}) to JoalAudioSource ({})", audioBuffer.getSystemName(), this.getSystemName());
164            }
165            return true;
166        } else {
167            throw new IllegalArgumentException(audioBuffer.getSystemName() + " is not a JoalAudioBuffer");
168        }
169    }
170
171    /**
172     * Queue a list of AudioBuffers on this source.
173     *
174     * (called from DefaultAudioFactory command queue)
175     *
176     * @param audioBuffers AudioBuffers to queue
177     * @return True if successfully queued.
178     */
179    @Override
180    public boolean queueAudioBuffers(Queue<AudioBuffer> audioBuffers) {
181        // First check we've been initialised
182        if (!_initialised || !isAudioAlive()) {
183            return false;
184        }
185
186        // Make an int[] of the buffer ids
187        int[] bids = new int[1];
188        int i = 0;
189        // While the list isn't empty, pop elements and process.
190        AudioBuffer b;
191        while ((b = audioBuffers.poll()) != null) {
192            if (b instanceof JoalAudioBuffer) {
193                bids[0] = ((JoalAudioBuffer) b).getDataStorageBuffer()[0];
194            } else {
195                throw new IllegalArgumentException(b.getSystemName() + " is not a JoalAudioBuffer");
196            }
197            al.alSourceQueueBuffers(_source[0], 1, bids, 0);
198            if (log.isDebugEnabled()) {
199                log.debug("Queueing Buffer [{}] {}", i, b.getSystemName());
200            }
201            i++;
202            if (JoalAudioFactory.checkALError()) {
203                log.warn("Error queueing JoalSource ({}) to AudioBuffers ({}) etc.", this.getSystemName(), b.getDisplayName());
204                return false;
205            }
206        }
207
208        // Bind this AudioSource to the specified AudioBuffer
209        //al.alSourceQueueBuffers(_source[0], bids.length, bids, 0);
210        //al.alSourcei(_source[0], AL.AL_BUFFER, ((JoalAudioBuffer)audioBuffer).getDataStorageBuffer()[0]);
211        return true;
212    }
213
214    /**
215     * Remove all processed AudioBuffers from this Source.
216     *
217     * @return True if successful.
218     */
219    @Override
220    public boolean unqueueAudioBuffers() {
221        // First check we've been initialised
222        if (!_initialised || !isAudioAlive()) {
223            return false;
224        }
225
226        int[] num_processed = new int[1];
227
228        // How many processed buffers are there?
229        al.alGetSourcei(_source[0], AL.AL_BUFFERS_PROCESSED, num_processed, 0);
230        if (JoalAudioFactory.checkALError()) {
231            log.warn("Error getting # processed buffers from  JoalSource ({})", this.getSystemName());
232            return false;
233        }
234
235        // Try to unqueue them all.
236        if (num_processed[0] > 0) {
237            int[] bids = new int[num_processed[0]];
238            al.alSourceUnqueueBuffers(_source[0], num_processed[0], bids, 0);
239            if (JoalAudioFactory.checkALError()) {
240                log.warn("Error removing {} buffers from  JoalSource ({})", num_processed[0], this.getSystemName());
241                return false;
242            }
243        }
244        if (log.isDebugEnabled()) {
245            log.debug("Removed {} buffers from JoalAudioSource ({})", num_processed[0], this.getSystemName());
246        }
247        return (numQueuedBuffers() != 0);
248    }
249
250    @Override
251    public int numProcessedBuffers() {
252        if (!isAudioAlive()) {
253            return 0;
254        }
255
256        int[] num_processed = new int[1];
257
258        // How many processed buffers are there?
259        al.alGetSourcei(_source[0], AL.AL_BUFFERS_PROCESSED, num_processed, 0);
260        if (JoalAudioFactory.checkALError()) {
261            log.warn("Error getting # processed buffers from  JoalSource ({})", this.getSystemName());
262            return 0;
263        }
264        return (num_processed[0]);
265    }
266
267    /**
268     * Report the number of AudioBuffers queued to this source.
269     *
270     * @return number of queued buffers.
271     */
272    @Override
273    public int numQueuedBuffers() {
274        // First check we've been initialised
275        if (!_initialised || !isAudioAlive()) {
276            return 0;
277        }
278
279        int[] num_queued = new int[1];
280        // How many queued buffers are there?
281        al.alGetSourcei(_source[0], AL.AL_BUFFERS_QUEUED, num_queued, 0);
282        if (JoalAudioFactory.checkALError()) {
283            log.warn("Error getting # queued buffers from  JoalSource ({})", this.getSystemName());
284            return 0;
285        }
286
287        if (log.isDebugEnabled()) {
288            log.debug("Queued {} buffers on JoalAudioSource ({})", num_queued[0], this.getSystemName());
289        }
290        return (num_queued[0]);
291    }
292
293    @Override
294    boolean bindAudioBuffer(AudioBuffer audioBuffer) {
295        // First check we've been initialised
296        if (!_initialised || !isAudioAlive()) {
297            return false;
298        }
299
300        // Bind this AudioSource to the specified AudioBuffer
301        al.alSourcei(_source[0], AL.AL_BUFFER, ((JoalAudioBuffer) audioBuffer).getDataStorageBuffer()[0]);
302        if (JoalAudioFactory.checkALError()) {
303            log.warn("Error binding JoalSource ({}) to AudioBuffer ({})", this.getSystemName(), this.getAssignedBufferName());
304            return false;
305        }
306
307        if (log.isDebugEnabled()) {
308            log.debug("Bind JoalAudioSource ({}) to JoalAudioBuffer ({})", this.getSystemName(), audioBuffer.getSystemName());
309        }
310        return true;
311    }
312
313    @Override
314    protected void changePosition(Vector3f pos) {
315        if (_initialised && isAudioAlive()) {
316            if (_source != null) {
317                al.alSource3f(_source[0], AL.AL_POSITION, pos.x, pos.y, pos.z);
318                if (JoalAudioFactory.checkALError()) {
319                    log.warn("Error updating position of JoalAudioSource ({})", this.getSystemName());
320                }
321            }
322        }
323    }
324
325    @Override
326    public void setPositionRelative(boolean relative) {
327        super.setPositionRelative(relative);
328        if (_initialised && isAudioAlive()) {
329            al.alSourcei(_source[0], AL.AL_SOURCE_RELATIVE, relative ? AL.AL_TRUE : AL.AL_FALSE);
330            if (JoalAudioFactory.checkALError()) {
331                log.warn("Error updating relative position property of JoalAudioSource ({})", this.getSystemName());
332            }
333        }
334    }
335
336    @Override
337    public void setVelocity(Vector3f vel) {
338        super.setVelocity(vel);
339        if (_initialised && isAudioAlive()) {
340            al.alSource3f(_source[0], AL.AL_VELOCITY, vel.x, vel.y, vel.z);
341            if (JoalAudioFactory.checkALError()) {
342                log.warn("Error updating velocity of JoalAudioSource ({})", this.getSystemName());
343            }
344        }
345    }
346
347    @Override
348    public void setGain(float gain) {
349        super.setGain(gain);
350        if (_initialised && isAudioAlive()) {
351            calculateGain();
352        }
353    }
354
355    @Override
356    public void setPitch(float pitch) {
357        super.setPitch(pitch);
358        if (_initialised && isAudioAlive()) {
359            al.alSourcef(_source[0], AL.AL_PITCH, pitch);
360            if (JoalAudioFactory.checkALError()) {
361                log.warn("Error updating pitch of JoalAudioSource ({})", this.getSystemName());
362            }
363        }
364    }
365
366    @Override
367    public void setReferenceDistance(float referenceDistance) {
368        super.setReferenceDistance(referenceDistance);
369        if (_initialised && isAudioAlive()) {
370            al.alSourcef(_source[0], AL.AL_REFERENCE_DISTANCE, referenceDistance);
371            if (JoalAudioFactory.checkALError()) {
372                log.warn("Error updating reference distance of JoalAudioSource ({})", this.getSystemName());
373            }
374        }
375    }
376
377    @Override
378    public void setOffset(long offset) {
379        super.setOffset(offset);
380        if (_initialised && isAudioAlive()) {
381            al.alSourcei(_source[0], AL.AL_SAMPLE_OFFSET, (int) getOffset());
382            if (JoalAudioFactory.checkALError()) {
383                log.warn("Error updating Sample Offset of JoalAudioSource ({})", this.getSystemName());
384             }
385        }
386    }
387
388    @Override
389    public void setMaximumDistance(float maximumDistance) {
390        super.setMaximumDistance(maximumDistance);
391        if (_initialised && isAudioAlive()) {
392            al.alSourcef(_source[0], AL.AL_MAX_DISTANCE, maximumDistance);
393            if (JoalAudioFactory.checkALError()) {
394                log.warn("Error updating maximum distance of JoalAudioSource ({})", this.getSystemName());
395            }
396        }
397    }
398
399    @Override
400    public void setRollOffFactor(float rollOffFactor) {
401        super.setRollOffFactor(rollOffFactor);
402        if (_initialised && isAudioAlive()) {
403            al.alSourcef(_source[0], AL.AL_ROLLOFF_FACTOR, rollOffFactor);
404            if (JoalAudioFactory.checkALError()) {
405                log.warn("Error updating roll-off factor of JoalAudioSource ({})", this.getSystemName());
406            }
407        }
408    }
409
410    @Override
411    public int getState() {
412        if (_source != null && isAudioAlive()) {
413            int old = _alState[0];
414            al.alGetSourcei(_source[0], AL.AL_SOURCE_STATE, _alState, 0);
415            if (_alState[0] != old) {
416                if (_alState[0] == AL.AL_PLAYING) {
417                    this.setState(STATE_PLAYING);
418                } else {
419                    this.setState(STATE_STOPPED);
420                }
421            }
422            return super.getState();
423        } else {
424            return STATE_STOPPED;
425        }
426    }
427
428    @Override
429    public void stateChanged(int oldState) {
430        super.stateChanged(oldState);
431        if (_initialised && isAudioAlive()) {
432            if (_source != null) {
433                al.alSourcef(_source[0], AL.AL_PITCH, this.getPitch());
434                al.alSourcef(_source[0], AL.AL_GAIN, this.getGain());
435                al.alSource3f(_source[0], AL.AL_POSITION, this.getCurrentPosition().x, this.getCurrentPosition().y, this.getCurrentPosition().z);
436                al.alSource3f(_source[0], AL.AL_VELOCITY, this.getVelocity().x, this.getVelocity().y, this.getVelocity().z);
437                al.alSourcei(_source[0], AL.AL_LOOPING, this.isLooped() ? AL.AL_TRUE : AL.AL_FALSE);
438                if (JoalAudioFactory.checkALError()) {
439                    log.warn("Error updating JoalAudioSource ({})", this.getSystemName());
440                }
441            }
442        } else {
443            _initialised = init();
444        }
445    }
446
447    @Override
448    public int attachSourcesToEffects() {
449        if (!isAudioAlive()) {
450            return 0;
451        }
452
453        // Connect the source to the effect slot
454        al.alSource3i(_source[0], ALExt.AL_AUXILIARY_SEND_FILTER, 1, 0, 0);
455        if (JoalAudioFactory.checkALError()) {
456            log.warn("Failed to configure source send 1");
457            return 0;
458        }
459        return 1;  
460    }
461
462    @Override
463    public int detachSourcesToEffects() {
464        if (!isAudioAlive()) {
465            return 0;
466        }
467        // Disconnect the source
468        al.alSource3i(_source[0], ALExt.AL_AUXILIARY_SEND_FILTER, 0, 0, 0);
469
470        // Remove filter from source
471        if (JoalAudioFactory.checkALError()) {
472            log.warn("Failed to detach");
473            return 0;
474        }
475        return 1;
476    }
477
478    @Override
479    protected void doPlay() {
480        log.debug("Play JoalAudioSource ({})", this.getSystemName());
481        if (_initialised && isAudioAlive() && (isBound() || isQueued())) {
482            doRewind();
483            doResume();
484        }
485    }
486
487//    @SuppressWarnings("SleepWhileInLoop")
488    @Override
489    protected void doStop() {
490        log.debug("Stop JoalAudioSource ({})", this.getSystemName());
491        if (_source != null) {
492            if (_initialised && isAudioAlive() && (isBound() || isQueued())) {
493                al.alSourceStop(_source[0]);
494                doRewind();
495            }
496            int[] myState = new int[1];
497            al.alGetSourcei(_source[0], AL.AL_SOURCE_STATE, myState, 0);
498            boolean stopped = myState[0] != AL.AL_LOOPING;
499            while (!stopped) {
500                try {
501                    Thread.sleep(5);
502                } catch (InterruptedException ex) {
503                }
504                al.alGetSourcei(_source[0], AL.AL_SOURCE_STATE, myState, 0);
505                stopped = myState[0] != AL.AL_LOOPING;
506            }
507            this.setState(STATE_STOPPED);
508        }
509    }
510
511    @Override
512    protected void doPause() {
513        log.debug("Pause JoalAudioSource ({})", this.getSystemName());
514        if (_initialised && isAudioAlive() && (isBound() || isQueued())) {
515            al.alSourcePause(_source[0]);
516        }
517        this.setState(STATE_STOPPED);
518    }
519
520    @Override
521    protected void doResume() {
522        log.debug("Resume JoalAudioSource ({})", this.getSystemName());
523        if (_initialised && isAudioAlive() && (isBound() || isQueued())) {
524            calculateGain();
525            al.alSourcei(_source[0], AL.AL_LOOPING, this.isLooped() ? AL.AL_TRUE : AL.AL_FALSE);
526            al.alSourcePlay(_source[0]);
527            int numLoops = this.getNumLoops();
528            if (numLoops > 0) {
529                if (log.isDebugEnabled()) {
530                    log.debug("Create LoopThread for JoalAudioSource {}", this.getSystemName());
531                }
532                AudioSourceLoopThread aslt = new AudioSourceLoopThread(this, numLoops);
533                aslt.start();
534            }
535        }
536        this.setState(STATE_PLAYING);
537    }
538
539    @Override
540    protected void doRewind() {
541        log.debug("Rewind JoalAudioSource ({})", this.getSystemName());
542        if (_initialised && isAudioAlive() && (isBound() || isQueued())) {
543            al.alSourceRewind(_source[0]);
544        }
545    }
546
547    @Override
548    protected void doFadeIn() {
549        log.debug("Fade-in JoalAudioSource ({})", this.getSystemName());
550        if (_initialised && isAudioAlive() && (isBound() || isQueued())) {
551            doPlay();
552            AudioSourceFadeThread asft = new AudioSourceFadeThread(this);
553            asft.start();
554        }
555    }
556
557    @Override
558    protected void doFadeOut() {
559        log.debug("Fade-out JoalAudioSource ({})", this.getSystemName());
560        if (_initialised && isAudioAlive() && (isBound() || isQueued())) {
561            AudioSourceFadeThread asft = new AudioSourceFadeThread(this);
562            asft.start();
563        }
564    }
565
566    @Override
567    protected void cleanup() {
568        log.debug("Cleanup JoalAudioSource ({})", this.getSystemName());
569        int[] source_type = new int[1];
570        al.alGetSourcei(_source[0], AL.AL_SOURCE_TYPE, source_type, 0);
571        if (_initialised && (isBound() || isQueued() || source_type[0] == AL.AL_UNDETERMINED) || source_type[0] == AL.AL_STREAMING) {
572            al.alSourceStop(_source[0]);
573            al.alDeleteSources(1, _source, 0);
574            this._source = null;
575            log.debug("...done cleanup");
576        }
577    }
578
579    @Override
580    protected void calculateGain() {
581        // Adjust gain based on master gain for this source and any
582        // calculated fade gains
583        float currentGain = this.getGain() * this.getFadeGain();
584
585        // If playing, update the gain
586        if (_initialised && isAudioAlive()) {
587            al.alSourcef(_source[0], AL.AL_GAIN, currentGain);
588            if (JoalAudioFactory.checkALError()) {
589                log.warn("Error updating gain setting of JoalAudioSource ({})", this.getSystemName());
590            }
591            if (log.isDebugEnabled()) {
592                log.debug("Set current gain of JoalAudioSource {} to {}", this.getSystemName(), currentGain);
593            }
594        }
595    }
596
597    /**
598     * Internal method to return a reference to the OpenAL source buffer
599     *
600     * @return source buffer
601     */
602    private int[] getSourceBuffer() {
603        return this._source;
604    }
605
606    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JoalAudioSource.class);
607
608    /**
609     * An internal class used to create a new thread to monitor looping as,
610     * unlike JavaSound, OpenAL (and, therefore, JOAL) do not provide a
611     * convenient method to loop a sound a specific number of times.
612     */
613    private static class AudioSourceLoopThread extends AbstractAudioThread {
614
615        /**
616         * Number of times to loop this source
617         */
618        private int numLoops;
619
620        /**
621         * Reference to the OpenAL source buffer
622         */
623        private int[] sourceBuffer;
624
625        /**
626         * Constructor that takes handle to looping AudioSource to monitor
627         *
628         * @param audioSource looping AudioSource to monitor
629         * @param numLoops    number of loops for this AudioSource to make
630         */
631        AudioSourceLoopThread(JoalAudioSource audioSource, int numLoops) {
632            super();
633            this.setName("loopsrc-" + super.getName());
634            this.sourceBuffer = audioSource.getSourceBuffer();
635            this.numLoops = numLoops;
636            if (log.isDebugEnabled()) {
637                log.debug("Created AudioSourceLoopThread for AudioSource {} loopcount {}",
638                        audioSource.getSystemName(), this.numLoops);
639            }
640        }
641
642        /**
643         * Main processing loop
644         */
645        @Override
646        public void run() {
647
648            // Current loop count
649            int loopCount = 0;
650
651            // Previous position
652            float oldPos = 0;
653
654            // Current position
655            float[] newPos = new float[1];
656
657            // Turn on looping
658            JoalAudioSource.al.alSourcei(sourceBuffer[0], AL.AL_LOOPING, AL.AL_TRUE);
659
660            while (!dying()) {
661
662                // Determine current position
663                JoalAudioSource.al.alGetSourcef(sourceBuffer[0], AL.AL_SEC_OFFSET, newPos, 0);
664
665                // Check if it is smaller than the previous position
666                // If so, we've looped so increment the loop counter
667                if (oldPos > newPos[0]) {
668                    loopCount++;
669                    log.debug("Loop count {}", loopCount);
670                }
671                oldPos = newPos[0];
672
673                // Check if we've performed sufficient iterations
674                if (loopCount >= numLoops) {
675                    die();
676                }
677
678                // sleep for a while so as not to overload CPU
679                snooze(20);
680            }
681
682            // Turn off looping
683            JoalAudioSource.al.alSourcei(sourceBuffer[0], AL.AL_LOOPING, AL.AL_FALSE);
684
685            // Finish up
686            if (log.isDebugEnabled()) {
687                log.debug("Clean up thread {}", this.getName());
688            }
689            cleanup();
690        }
691
692        /**
693         * Shuts this thread down and clears references to created objects
694         */
695        @Override
696        protected void cleanup() {
697            // Thread is to shutdown
698            die();
699
700            // Clear references to objects
701            this.sourceBuffer = null;
702
703            // Finalise cleanup in super-class
704            super.cleanup();
705        }
706    }
707}