001package jmri.jmrit.audio;
002
003import java.util.LinkedList;
004import java.util.Queue;
005import java.util.concurrent.ThreadLocalRandom;
006import javax.annotation.Nonnull;
007import javax.vecmath.Vector3f;
008import jmri.Audio;
009import jmri.AudioManager;
010import jmri.InstanceManager;
011import jmri.implementation.AbstractAudio;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015/**
016 * Base implementation of the AudioSource class.
017 * <p>
018 * Specific implementations will extend this base class.
019 * <br>
020 * <hr>
021 * This file is part of JMRI.
022 * <p>
023 * JMRI is free software; you can redistribute it and/or modify it under the
024 * terms of version 2 of the GNU General Public License as published by the Free
025 * Software Foundation. See the "COPYING" file for a copy of this license.
026 * <p>
027 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
028 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
029 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
030 *
031 * @author Matthew Harris copyright (c) 2009
032 */
033public abstract class AbstractAudioSource extends AbstractAudio implements AudioSource {
034
035    private Vector3f position = new Vector3f(0.0f, 0.0f, 0.0f);
036    private Vector3f currentPosition = new Vector3f(0.0f, 0.0f, 0.0f);
037    private Vector3f velocity = new Vector3f(0.0f, 0.0f, 0.0f);
038    private float gain = 1.0f;
039    private float pitch = 1.0f;
040    private float referenceDistance = 1.0f;
041    private float maximumDistance = Audio.MAX_DISTANCE;
042    private float rollOffFactor = 1.0f;
043    private int minLoops = LOOP_NONE;
044    private int maxLoops = LOOP_NONE;
045    private int numLoops = 0;
046    // private int minLoopDelay = 0;
047    // private int maxLoopDelay = 0;
048    // private int loopDelay = 0;
049    private int fadeInTime = 1000;
050    private int fadeOutTime = 1000;
051    private float fadeGain = 1.0f;
052    private long timeOfLastFadeCheck = 0;
053    private long timeOfLastPositionCheck = 0;
054    private int fading = Audio.FADE_NONE;
055    private boolean bound = false;
056    private boolean positionRelative = false;
057    private boolean queued = false;
058    private long offset = 0;
059    private AudioBuffer buffer;
060    // private AudioSourceDelayThread asdt = null;
061    private LinkedList<AudioBuffer> pendingBufferQueue = new LinkedList<>();
062
063    private static final AudioFactory activeAudioFactory = InstanceManager.getDefault(jmri.AudioManager.class).getActiveAudioFactory();
064    private static float metersPerUnit;
065
066    /**
067     * Abstract constructor for new AudioSource with system name
068     *
069     * @param systemName AudioSource object system name (e.g. IAS1)
070     */
071    public AbstractAudioSource(String systemName) {
072        super(systemName);
073        AudioListener al = activeAudioFactory.getActiveAudioListener();
074        if (al != null) {
075            storeMetersPerUnit(al.getMetersPerUnit());
076        }
077    }
078
079    /**
080     * Abstract constructor for new AudioSource with system name and user name
081     *
082     * @param systemName AudioSource object system name (e.g. IAS1)
083     * @param userName   AudioSource object user name
084     */
085    public AbstractAudioSource(String systemName, String userName) {
086        super(systemName, userName);
087        AudioListener al = activeAudioFactory.getActiveAudioListener();
088        if (al != null) {
089            storeMetersPerUnit(al.getMetersPerUnit());
090        }
091    }
092
093    private static void storeMetersPerUnit(float newVal) {
094        metersPerUnit = newVal;
095    }
096
097    public boolean isAudioAlive() {
098        return ((AudioThread) activeAudioFactory.getCommandThread()).isThreadAlive();
099    }
100
101    @Override
102    public char getSubType() {
103        return SOURCE;
104    }
105
106    @Override
107    public boolean queueBuffers(Queue<AudioBuffer> audioBuffers) {
108        // Note: Cannot queue buffers to a Source that has a bound buffer.
109        if (!bound) {
110            this.pendingBufferQueue = new LinkedList<>(audioBuffers);
111            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_QUEUE_BUFFERS));
112            activeAudioFactory.getCommandThread().interrupt();
113            if (log.isDebugEnabled() && (audioBuffers.peek() != null)) {
114                log.debug("Queued Buffer {} to Source {}", audioBuffers.peek().getSystemName(), this.getSystemName());
115            }
116            return true;
117        } else {
118            if (audioBuffers.peek() != null) {
119                log.error("Attempted to queue buffers {} (etc) to Bound Source {}", audioBuffers.peek().getSystemName(), this.getSystemName());
120            }
121            return false;
122        }
123    }
124
125    @Override
126    public boolean queueBuffer(AudioBuffer audioBuffer) {
127        if (!bound) {
128            //this.pendingBufferQueue.clear();
129            this.pendingBufferQueue.add(audioBuffer);
130            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_QUEUE_BUFFERS));
131            activeAudioFactory.getCommandThread().interrupt();
132            if (log.isDebugEnabled()) {
133                log.debug("Queued Buffer {} to Source {}", audioBuffer.getSystemName(), this.getSystemName());
134            }
135            return true;
136        } else {
137            log.error("Attempted to queue buffer {} to Bound Source {}", audioBuffer.getSystemName(), this.getSystemName());
138            return false;
139        }
140    }
141
142    @Override
143    public boolean unqueueBuffers() {
144        if (bound) {
145            log.error("Attempted to unqueue buffers on Bound Source {}", this.getSystemName());
146            return false;
147        } else if (queued) {
148            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_UNQUEUE_BUFFERS));
149            activeAudioFactory.getCommandThread().interrupt();
150            if (log.isDebugEnabled()) {
151                log.debug("Unqueued Processed Buffers on Source {}", this.getSystemName());
152            }
153            return true;
154        } else {
155            log.debug("Source neither queued nor bound. Not an error. {}", this.getSystemName());
156            return false;
157        }
158    }
159
160    public Queue<AudioBuffer> getQueuedBuffers() {
161        return this.pendingBufferQueue;
162    }
163
164    @Override
165    public void setAssignedBuffer(AudioBuffer audioBuffer) {
166        if (!queued) {
167            this.buffer = audioBuffer;
168            // Ensure that the source is stopped
169            this.stop(false);
170            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_BIND_BUFFER));
171            activeAudioFactory.getCommandThread().interrupt();
172            if (log.isDebugEnabled()) {
173                log.debug("Assigned Buffer {} to Source {}", audioBuffer.getSystemName(), this.getSystemName());
174            }
175        } else {
176            log.error("Attempted to assign buffer {} to Queued Source {}", audioBuffer.getSystemName(), this.getSystemName());
177        }
178    }
179
180    @Override
181    public void setAssignedBuffer(String bufferSystemName) {
182        if (!queued) {
183            AudioManager am = InstanceManager.getDefault(jmri.AudioManager.class);
184            Audio a = am.getBySystemName(bufferSystemName);
185            if (a != null && a.getSubType() == Audio.BUFFER) {
186                setAssignedBuffer((AudioBuffer) a);
187            } else {
188                log.warn("Attempt to assign incorrect object type to buffer - AudioBuffer expected.");
189                this.buffer = null;
190                this.bound = false;
191            }
192        } else {
193            log.error("Attempted to assign buffer {} to Queued Source {}", bufferSystemName, this.getSystemName());
194        }
195    }
196
197    @Override
198    public AudioBuffer getAssignedBuffer() {
199        return this.buffer;
200    }
201
202    @Override
203    public String getAssignedBufferName() {
204        return (buffer != null) ? buffer.getSystemName() : "[none]";
205    }
206
207    @Override
208    public void setPosition(Vector3f pos) {
209        this.position = pos;
210        this.currentPosition = pos;
211        changePosition(pos);
212        if (log.isDebugEnabled()) {
213            log.debug("Set position of Source {} to {}", this.getSystemName(), pos);
214        }
215    }
216
217    @Override
218    public void setPosition(float x, float y, float z) {
219        this.setPosition(new Vector3f(x, y, z));
220    }
221
222    @Override
223    public void setPosition(float x, float y) {
224        this.setPosition(new Vector3f(x, y, 0.0f));
225    }
226
227    @Override
228    public Vector3f getPosition() {
229        return this.position;
230    }
231
232    @Override
233    public Vector3f getCurrentPosition() {
234        return this.currentPosition;
235    }
236
237    @Override
238    public void setPositionRelative(boolean relative) {
239        this.positionRelative = relative;
240    }
241
242    @Override
243    public boolean isPositionRelative() {
244        return this.positionRelative;
245    }
246
247    @Override
248    public void setVelocity(Vector3f vel) {
249        this.velocity = vel;
250        if (log.isDebugEnabled()) {
251            log.debug("Set velocity of Source {} to {}", this.getSystemName(), vel);
252        }
253    }
254
255    @Override
256    public Vector3f getVelocity() {
257        return this.velocity;
258    }
259
260    /**
261     * Calculate current position based on velocity.
262     */
263    protected void calculateCurrentPosition() {
264
265        // Calculate how long it's been since we lasted checked position
266        long currentTime = System.currentTimeMillis();
267        float timePassed = (currentTime - this.timeOfLastPositionCheck);
268        this.timeOfLastPositionCheck = currentTime;
269
270        log.debug("timePassed = {} metersPerUnit = {} source = {} state = {}", timePassed, metersPerUnit, this.getSystemName(), this.getState());
271        if (this.velocity.length() != 0) {
272            this.currentPosition.scaleAdd((timePassed / 1000) * metersPerUnit,
273                    this.velocity,
274                    this.currentPosition);
275            changePosition(this.currentPosition);
276            if (log.isDebugEnabled()) {
277                log.debug("Set current position of Source {} to {}", this.getSystemName(), this.currentPosition);
278            }
279        }
280    }
281
282    @Override
283    public void resetCurrentPosition() {
284        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_RESET_POSITION));
285        activeAudioFactory.getCommandThread().interrupt();
286    }
287
288    /**
289     * Reset the current position.
290     */
291    protected void doResetCurrentPosition() {
292        this.currentPosition = this.position;
293    }
294
295    /**
296     * Change the current position of this source.
297     *
298     * @param pos new position
299     */
300    abstract protected void changePosition(Vector3f pos);
301
302    @Override
303    public void setGain(float gain) {
304        this.gain = gain;
305        if (log.isDebugEnabled()) {
306            log.debug("Set gain of Source {} to {}", this.getSystemName(), gain);
307        }
308    }
309
310    @Override
311    public float getGain() {
312        return this.gain;
313    }
314
315    /**
316     * Calculate the gain of this AudioSource based on distance from
317     * listener and fade levels.
318     */
319    abstract protected void calculateGain();
320
321    @Override
322    public void setPitch(float pitch) {
323        if (pitch < 0.5f) {
324            pitch = 0.5f;
325        }
326        if (pitch > 2.0f) {
327            pitch = 2.0f;
328        }
329        this.pitch = pitch;
330        if (log.isDebugEnabled()) {
331            log.debug("Set pitch of Source {} to {}", this.getSystemName(), pitch);
332        }
333    }
334
335    @Override
336    public float getPitch() {
337        return this.pitch;
338    }
339
340    @Override
341    public void setReferenceDistance(float referenceDistance) {
342        if (referenceDistance < 0.0f) {
343            referenceDistance = 0.0f;
344        }
345        this.referenceDistance = referenceDistance;
346        if (log.isDebugEnabled()) {
347            log.debug("Set reference distance of Source {} to {}", this.getSystemName(), referenceDistance);
348        }
349    }
350
351    @Override
352    public float getReferenceDistance() {
353        return this.referenceDistance;
354    }
355
356    @Override
357    public void setOffset(long offset) {
358        if (offset < 0) {
359            offset = 0;
360        }
361        if (offset > this.buffer.getLength()) {
362            offset = this.buffer.getLength();
363        }
364        this.offset = offset;
365        if (log.isDebugEnabled()) {
366            log.debug("Set byte offset of Source {}to {}", this.getSystemName(), offset);
367        }
368    }
369
370    @Override
371    public long getOffset() {
372        return this.offset;
373    }
374
375    @Override
376    public void setMaximumDistance(float maximumDistance) {
377        if (maximumDistance < 0.0f) {
378            maximumDistance = 0.0f;
379        }
380        this.maximumDistance = maximumDistance;
381        if (log.isDebugEnabled()) {
382            log.debug("Set maximum distance of Source {} to {}", this.getSystemName(), maximumDistance);
383        }
384    }
385
386    @Override
387    public float getMaximumDistance() {
388        return this.maximumDistance;
389    }
390
391    @Override
392    public void setRollOffFactor(float rollOffFactor) {
393        this.rollOffFactor = rollOffFactor;
394        if (log.isDebugEnabled()) {
395            log.debug("Set roll-off factor of Source {} to {}", this.getSystemName(), rollOffFactor);
396        }
397    }
398
399    @Override
400    public float getRollOffFactor() {
401        return this.rollOffFactor;
402    }
403
404    @Override
405    public void setLooped(boolean loop) {
406        if (loop) {
407            this.minLoops = LOOP_CONTINUOUS;
408            this.maxLoops = LOOP_CONTINUOUS;
409        } else {
410            this.minLoops = LOOP_NONE;
411            this.maxLoops = LOOP_NONE;
412        }
413        calculateLoops();
414    }
415
416    @Override
417    public boolean isLooped() {
418        return (this.minLoops != LOOP_NONE || this.maxLoops != LOOP_NONE);
419    }
420
421    @Override
422    public void setMinLoops(int loops) {
423        if (this.maxLoops < loops) {
424            this.maxLoops = loops;
425        }
426        this.minLoops = loops;
427        calculateLoops();
428    }
429
430    @Override
431    public int getMinLoops() {
432        return this.minLoops;
433    }
434
435    @Override
436    public void setMaxLoops(int loops) {
437        if (this.minLoops > loops) {
438            this.minLoops = loops;
439        }
440        this.maxLoops = loops;
441        calculateLoops();
442    }
443
444    /**
445     * Calculate the number of times to loop playback of this sound.
446     */
447    protected void calculateLoops() {
448        if (this.minLoops != this.maxLoops) {
449            this.numLoops = this.minLoops + ThreadLocalRandom.current().nextInt(this.maxLoops - this.minLoops);
450        } else {
451            this.numLoops = this.minLoops;
452        }
453    }
454
455    @Override
456    public int getMaxLoops() {
457        return this.maxLoops;
458    }
459
460    @Override
461    public int getNumLoops() {
462        // Call the calculate method each time so as to ensure
463        // randomness when min and max are not equal
464        calculateLoops();
465        return this.numLoops;
466    }
467
468    @Override
469    public int getLastNumLoops() {
470        return this.numLoops;
471    }
472
473//    public void setMinLoopDelay(int loopDelay) {
474//        if (this.maxLoopDelay < loopDelay) {
475//            this.maxLoopDelay = loopDelay;
476//        }
477//        this.minLoopDelay = loopDelay;
478//        calculateLoopDelay();
479//    }
480//
481//    public int getMinLoopDelay() {
482//        return this.minLoopDelay;
483//    }
484//
485//    public void setMaxLoopDelay(int loopDelay) {
486//        if (this.minLoopDelay > loopDelay) {
487//            this.minLoopDelay = loopDelay;
488//        }
489//        this.maxLoopDelay = loopDelay;
490//        calculateLoopDelay();
491//    }
492//
493//    public int getMaxLoopDelay() {
494//        return this.maxLoopDelay;
495//    }
496//
497//    public int getLoopDelay() {
498//        // Call the calculate method each time so as to ensure
499//        // randomness when min and max are not equal
500//        calculateLoopDelay();
501//        return this.loopDelay;
502//    }
503//
504//    /**
505//     * Method to calculate the delay between subsequent loops of this source
506//     */
507//    protected void calculateLoopDelay() {
508//        if (this.minLoopDelay != this.maxLoopDelay) {
509//            Random r = new Random();
510//            this.loopDelay = this.minLoopDelay + r.nextInt(this.maxLoopDelay-this.minLoopDelay);
511//        } else {
512//            this.loopDelay = this.minLoopDelay;
513//        }
514//    }
515    @Override
516    public void setFadeIn(int fadeInTime) {
517        this.fadeInTime = fadeInTime;
518    }
519
520    @Override
521    public int getFadeIn() {
522        return this.fadeInTime;
523    }
524
525    @Override
526    public void setFadeOut(int fadeOutTime) {
527        this.fadeOutTime = fadeOutTime;
528    }
529
530    @Override
531    public int getFadeOut() {
532        return this.fadeOutTime;
533    }
534
535    /**
536     * Used to return the current calculated fade gain for this AudioSource.
537     *
538     * @return current fade gain
539     */
540    protected float getFadeGain() {
541        return this.fadeGain;
542    }
543
544    /**
545     * Calculate the fade gains.
546     */
547    protected void calculateFades() {
548
549        // Calculate how long it's been since we lasted checked fade gains
550        long currentTime = System.currentTimeMillis();
551        long timePassed = currentTime - this.timeOfLastFadeCheck;
552        this.timeOfLastFadeCheck = currentTime;
553
554        switch (this.fading) {
555            case Audio.FADE_NONE:
556                // Reset fade gain
557                this.fadeGain = 1.0f;
558                break;
559            case Audio.FADE_OUT:
560                // Calculate fade-out gain
561                this.fadeGain -= roundDecimal(timePassed) / (this.getFadeOut());
562
563                // Ensure that fade-out gain is not less than 0.0f
564                if (this.fadeGain < 0.0f) {
565                    this.fadeGain = 0.0f;
566
567                    // If so, we're done fading
568                    this.fading = Audio.FADE_NONE;
569                }
570                if (log.isDebugEnabled()) {
571                    log.debug("Set fade out gain of AudioSource {} to {}", this.getSystemName(), this.fadeGain);
572                }
573                break;
574            case Audio.FADE_IN:
575                // Calculate fade-in gain
576                this.fadeGain += roundDecimal(timePassed) / (this.getFadeIn());
577
578                // Ensure that fade-in gain is not greater than 1.0f
579                if (this.fadeGain >= 1.0f) {
580                    this.fadeGain = 1.0f;
581
582                    // If so, we're done fading
583                    this.fading = Audio.FADE_NONE;
584                }
585                if (log.isDebugEnabled()) {
586                    log.debug("Set fade in gain of AudioSource {} to {}", this.getSystemName(), this.fadeGain);
587                }
588                break;
589            default:
590                throw new IllegalArgumentException();
591        }
592    }
593
594    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
595    // types to implement this (yet).  So default to failing.
596    public boolean queueAudioBuffers(Queue<AudioBuffer> audioBuffers) {
597        log.debug("Abstract queueAudioBuffers() called.");
598        return false;
599    }
600
601    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
602    // types to implement this (yet).  So default to failing.
603    public boolean queueAudioBuffer(AudioBuffer audioBuffer) {
604        return false;
605    }
606
607    public boolean unqueueAudioBuffers() {
608        return false;
609    }
610
611    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
612    // types to implement this (yet).  So default to failing.
613    @Override
614    public int numQueuedBuffers() {
615        return 0;
616    }
617
618    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
619    // types to implement this (yet).  So default to failing.
620    @Override
621    public int numProcessedBuffers() {
622        return 0;
623    }
624
625    /**
626     * Binds this AudioSource with the specified AudioBuffer.
627     * <p>
628     * Applies only to sub-types:
629     * <ul>
630     * <li>Source
631     * </ul>
632     *
633     * @param buffer The AudioBuffer to bind to this AudioSource
634     * @return true if successful
635     */
636    abstract boolean bindAudioBuffer(AudioBuffer buffer);
637
638    /**
639     * Method to define if this AudioSource has been bound to an AudioBuffer.
640     *
641     * @param bound True if bound to an AudioBufferr
642     */
643    protected void setBound(boolean bound) {
644        this.bound = bound;
645    }
646
647    protected void setQueued(boolean queued) {
648        this.queued = queued;
649    }
650
651    @Override
652    public boolean isBound() {
653        return this.bound;
654    }
655
656    @Override
657    public boolean isQueued() {
658        return this.queued;
659    }
660
661    @Override
662    public void stateChanged(int oldState) {
663        // Get the current state
664        int i = this.getState();
665
666        // Check if the current state has changed to playing
667        if (i != oldState && i == STATE_PLAYING) {
668            // We've changed to playing so start the move thread
669            this.timeOfLastPositionCheck = System.currentTimeMillis();
670            AudioSourceMoveThread asmt = new AudioSourceMoveThread(this);
671            asmt.start();
672        }
673
674//        // Check if the current state has changed to stopped
675//        if (i!=oldState && i==STATE_STOPPED) {
676//            // We've changed to stopped so determine if we need to start the
677//            // loop delay thread
678//            if (isLooped() && getMinLoops()!=LOOP_CONTINUOUS) {
679//                // Yes, we need to
680//                if (asdt!=null) {
681//                    asdt.cleanup();
682//                    asdt = null;
683//                }
684//                asdt = new AudioSourceDelayThread(this);
685//                asdt.start();
686//            }
687//        }
688    }
689
690    @Override
691    public void play() {
692        this.fading = Audio.FADE_NONE;
693//        if (asdt!=null) {
694//            asdt.interrupt();
695//        }
696        if (this.getState() != STATE_PLAYING) {
697            this.setState(STATE_PLAYING);
698            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PLAY));
699            activeAudioFactory.getCommandThread().interrupt();
700        }
701    }
702
703    /**
704     * Play the clip from the beginning. If looped, start looping.
705     */
706    abstract protected void doPlay();
707
708    @Override
709    public void stop() {
710        stop(true);
711    }
712
713    private void stop(boolean interruptThread) {
714        this.fading = Audio.FADE_NONE;
715        this.setState(STATE_STOPPED);
716        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_STOP));
717        if (interruptThread) {
718            activeAudioFactory.getCommandThread().interrupt();
719        }
720    }
721
722    /**
723     * Stop playing the clip and rewind to the beginning.
724     */
725    abstract protected void doStop();
726
727    @Override
728    public void togglePlay() {
729        this.fading = Audio.FADE_NONE;
730        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PLAY_TOGGLE));
731        activeAudioFactory.getCommandThread().interrupt();
732    }
733
734    /**
735     * Toggle the current playing status. Will always start at/return to the
736     * beginning of the sample.
737     */
738    protected void doTogglePlay() {
739        if (this.getState() == STATE_PLAYING) {
740            stop();
741        } else {
742            play();
743        }
744    }
745
746    @Override
747    public void pause() {
748        this.fading = Audio.FADE_NONE;
749        this.setState(STATE_STOPPED);
750        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PAUSE));
751        activeAudioFactory.getCommandThread().interrupt();
752    }
753
754    /**
755     * Stop playing the clip but retain the current position.
756     */
757    abstract protected void doPause();
758
759    @Override
760    public void resume() {
761        this.fading = Audio.FADE_NONE;
762        this.setState(STATE_PLAYING);
763        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_RESUME));
764        activeAudioFactory.getCommandThread().interrupt();
765    }
766
767    /**
768     * Play the clip from the current position.
769     */
770    abstract protected void doResume();
771
772    @Override
773    public void togglePause() {
774        this.fading = Audio.FADE_NONE;
775        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PAUSE_TOGGLE));
776        activeAudioFactory.getCommandThread().interrupt();
777    }
778
779    /**
780     * Toggle the current playing status. Will retain the playback position of
781     * the sample.
782     */
783    protected void doTogglePause() {
784        if (this.getState() == STATE_PLAYING) {
785            pause();
786        } else {
787            resume();
788        }
789    }
790
791    @Override
792    public void rewind() {
793        this.fading = Audio.FADE_NONE;
794        this.setState(STATE_STOPPED);
795        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_REWIND));
796        activeAudioFactory.getCommandThread().interrupt();
797    }
798
799    /**
800     * Rewind clip to the beginning.
801     */
802    abstract protected void doRewind();
803
804    @Override
805    public void fadeIn() {
806        if (this.getState() != STATE_PLAYING && this.fading != Audio.FADE_IN) {
807            this.fading = Audio.FADE_IN;
808            this.fadeGain = 0.0f;
809            this.timeOfLastFadeCheck = System.currentTimeMillis();
810            this.setState(STATE_PLAYING);
811            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_FADE_IN));
812            activeAudioFactory.getCommandThread().interrupt();
813        }
814    }
815
816    /**
817     * Fade in then play this AudioSource.
818     */
819    abstract protected void doFadeIn();
820
821    @Override
822    public void fadeOut() {
823        if (this.getState() == STATE_PLAYING && this.fading != Audio.FADE_OUT) {
824            this.fading = Audio.FADE_OUT;
825            this.fadeGain = 1.0f;
826            this.timeOfLastFadeCheck = System.currentTimeMillis();
827            this.setState(STATE_PLAYING);
828            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_FADE_OUT));
829            activeAudioFactory.getCommandThread().interrupt();
830        }
831    }
832
833    /**
834     * Fade out then stop this AudioSource.
835     */
836    abstract protected void doFadeOut();
837
838    /**
839     * Get the current fading status.
840     *
841     * @return fading status
842     */
843    protected int getFading() {
844        return this.fading;
845    }
846
847    @Override
848    @Nonnull
849    public String getDebugString() {
850        return "Pos: " + this.getPosition().toString()
851                + ", bound to: " + this.getAssignedBufferName()
852                + ", loops: "
853                + ((this.getMinLoops() == LOOP_CONTINUOUS) ? "infinite"
854                        : ((!this.isLooped()) ? "none"
855                                : "(min=" + this.getMinLoops() + " max=" + this.getMaxLoops() + ")"));
856    }
857
858    private static final Logger log = LoggerFactory.getLogger(AbstractAudioSource.class);
859
860    /**
861     * An internal class used to create a new thread to monitor and maintain
862     * fade in and fade out levels.
863     * <p>
864     * Will exist only as long as this source is in the process of fading in or
865     * out.
866     */
867    protected static class AudioSourceFadeThread extends AbstractAudioThread {
868
869        /**
870         * Reference to the AudioSource object being monitored.
871         */
872        private AbstractAudioSource audioSource;
873
874        /**
875         * Internal variable to hold the fade direction.
876         */
877        private final int fadeDirection;
878
879        /**
880         * Constructor that takes handle to looping AudioSource to monitor.
881         *
882         * @param audioSource looping AudioSource to monitor
883         */
884        AudioSourceFadeThread(AbstractAudioSource audioSource) {
885            super();
886            this.setName("fadesrc-" + super.getName());
887            this.audioSource = audioSource;
888            this.fadeDirection = audioSource.getFading();
889            if (log.isDebugEnabled()) {
890                log.debug("Created AudioSourceFadeThread for AudioSource {}", audioSource.getSystemName());
891            }
892        }
893
894        /**
895         * Main processing loop.
896         */
897        @Override
898        public void run() {
899
900            while (!dying()) {
901
902                // Recalculate the fade levels
903                audioSource.calculateFades();
904
905                // Recalculate the gain levels
906                audioSource.calculateGain();
907
908                // Check if we've done fading
909                if (audioSource.getFading() == Audio.FADE_NONE) {
910                    die();
911                }
912
913                // sleep for a while so as not to overload CPU
914                snooze(20);
915            }
916
917            // Reset fades
918            audioSource.calculateFades();
919
920            // Check if we were fading out and, if so, stop.
921            // Otherwise reset gain
922            if (this.fadeDirection == Audio.FADE_OUT) {
923                audioSource.doStop();
924            } else {
925                audioSource.calculateGain();
926            }
927
928            // Finish up
929            if (log.isDebugEnabled()) {
930                log.debug("Clean up thread {}", this.getName());
931            }
932            cleanup();
933        }
934
935        /**
936         * Shut down this thread and clear references to created objects.
937         */
938        @Override
939        protected void cleanup() {
940            // Thread is to shutdown
941            die();
942
943            // Clear references to objects
944            this.audioSource = null;
945
946            // Finalise cleanup in super-class
947            super.cleanup();
948        }
949    }
950
951    /**
952     * An internal class used to create a new thread to monitor and maintain
953     * current source position with respect to velocity.
954     */
955    protected static class AudioSourceMoveThread extends AbstractAudioThread {
956
957        /**
958         * Reference to the AudioSource object being monitored.
959         */
960        private AbstractAudioSource audioSource;
961
962        /**
963         * Constructor that takes handle to looping AudioSource to monitor.
964         *
965         * @param audioSource looping AudioSource to monitor
966         */
967        AudioSourceMoveThread(AbstractAudioSource audioSource) {
968            super();
969            this.setName("movesrc-" + super.getName());
970            this.audioSource = audioSource;
971            if (log.isDebugEnabled()) {
972                log.debug("Created AudioSourceMoveThread for AudioSource {}", audioSource.getSystemName());
973            }
974        }
975
976        /**
977         * Main processing loop.
978         */
979        @Override
980        public void run() {
981
982            while (!dying()) {
983
984                // Recalculate the position
985                audioSource.calculateCurrentPosition();
986
987                // Check state and die if not playing
988                if (audioSource.getState() != STATE_PLAYING) {
989                    die();
990                }
991
992                // sleep for a while so as not to overload CPU
993                snooze(100);
994            }
995
996//            // Reset the current position
997//            audioSource.resetCurrentPosition();
998            // Finish up
999            if (log.isDebugEnabled()) {
1000                log.debug("Clean up thread {}", this.getName());
1001            }
1002            cleanup();
1003        }
1004
1005        /**
1006         * Shuts this thread down and clears references to created objects.
1007         */
1008        @Override
1009        protected void cleanup() {
1010            // Thread is to shutdown
1011            die();
1012
1013            // Clear references to objects
1014            this.audioSource = null;
1015
1016            // Finalise cleanup in super-class
1017            super.cleanup();
1018        }
1019    }
1020
1021//    /**
1022//     * An internal class used to create a new thread to delay subsequent
1023//     * playbacks of a non-continuous looped source.
1024//     */
1025//    private class AudioSourceDelayThread extends Thread {
1026//
1027//        /**
1028//         * Reference to the AudioSource object being monitored
1029//         */
1030//        private AbstractAudioSource audioSource;
1031//
1032//        /**
1033//         * Constructor that takes handle to looping AudioSource to monitor
1034//         *
1035//         * @param audioSource looping AudioSource to monitor
1036//         */
1037//        AudioSourceDelayThread(AbstractAudioSource audioSource) {
1038//            super();
1039//            this.setName("delaysrc-"+super.getName());
1040//            this.audioSource = audioSource;
1041//            if (log.isDebugEnabled()) log.debug("Created AudioSourceDelayThread for AudioSource " + audioSource.getSystemName());
1042//        }
1043//
1044//        /**
1045//         * Main processing loop
1046//         */
1047//        @Override
1048//        public void run() {
1049//
1050//            // Sleep for the required period of time
1051//            try {
1052//                Thread.sleep(audioSource.getLoopDelay());
1053//            } catch (InterruptedException ex) {}
1054//
1055//            // Restart playing this AudioSource
1056//            this.audioSource.play();
1057//
1058//            // Finish up
1059//            if (log.isDebugEnabled()) log.debug("Clean up thread " + this.getName());
1060//            cleanup();
1061//        }
1062//
1063//        /**
1064//         * Shuts this thread down and clears references to created objects
1065//         */
1066//        protected void cleanup() {
1067//            // Clear references to objects
1068//            this.audioSource = null;
1069//        }
1070//    }
1071
1072}