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()).alive();
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//    public void setMinLoopDelay(int loopDelay) {
469//        if (this.maxLoopDelay < loopDelay) {
470//            this.maxLoopDelay = loopDelay;
471//        }
472//        this.minLoopDelay = loopDelay;
473//        calculateLoopDelay();
474//    }
475//
476//    public int getMinLoopDelay() {
477//        return this.minLoopDelay;
478//    }
479//
480//    public void setMaxLoopDelay(int loopDelay) {
481//        if (this.minLoopDelay > loopDelay) {
482//            this.minLoopDelay = loopDelay;
483//        }
484//        this.maxLoopDelay = loopDelay;
485//        calculateLoopDelay();
486//    }
487//
488//    public int getMaxLoopDelay() {
489//        return this.maxLoopDelay;
490//    }
491//
492//    public int getLoopDelay() {
493//        // Call the calculate method each time so as to ensure
494//        // randomness when min and max are not equal
495//        calculateLoopDelay();
496//        return this.loopDelay;
497//    }
498//
499//    /**
500//     * Method to calculate the delay between subsequent loops of this source
501//     */
502//    protected void calculateLoopDelay() {
503//        if (this.minLoopDelay != this.maxLoopDelay) {
504//            Random r = new Random();
505//            this.loopDelay = this.minLoopDelay + r.nextInt(this.maxLoopDelay-this.minLoopDelay);
506//        } else {
507//            this.loopDelay = this.minLoopDelay;
508//        }
509//    }
510    @Override
511    public void setFadeIn(int fadeInTime) {
512        this.fadeInTime = fadeInTime;
513    }
514
515    @Override
516    public int getFadeIn() {
517        return this.fadeInTime;
518    }
519
520    @Override
521    public void setFadeOut(int fadeOutTime) {
522        this.fadeOutTime = fadeOutTime;
523    }
524
525    @Override
526    public int getFadeOut() {
527        return this.fadeOutTime;
528    }
529
530    /**
531     * Used to return the current calculated fade gain for this AudioSource.
532     *
533     * @return current fade gain
534     */
535    protected float getFadeGain() {
536        return this.fadeGain;
537    }
538
539    /**
540     * Calculate the fade gains.
541     */
542    protected void calculateFades() {
543
544        // Calculate how long it's been since we lasted checked fade gains
545        long currentTime = System.currentTimeMillis();
546        long timePassed = currentTime - this.timeOfLastFadeCheck;
547        this.timeOfLastFadeCheck = currentTime;
548
549        switch (this.fading) {
550            case Audio.FADE_NONE:
551                // Reset fade gain
552                this.fadeGain = 1.0f;
553                break;
554            case Audio.FADE_OUT:
555                // Calculate fade-out gain
556                this.fadeGain -= roundDecimal(timePassed) / (this.getFadeOut());
557
558                // Ensure that fade-out gain is not less than 0.0f
559                if (this.fadeGain < 0.0f) {
560                    this.fadeGain = 0.0f;
561
562                    // If so, we're done fading
563                    this.fading = Audio.FADE_NONE;
564                }
565                if (log.isDebugEnabled()) {
566                    log.debug("Set fade out gain of AudioSource {} to {}", this.getSystemName(), this.fadeGain);
567                }
568                break;
569            case Audio.FADE_IN:
570                // Calculate fade-in gain
571                this.fadeGain += roundDecimal(timePassed) / (this.getFadeIn());
572
573                // Ensure that fade-in gain is not greater than 1.0f
574                if (this.fadeGain >= 1.0f) {
575                    this.fadeGain = 1.0f;
576
577                    // If so, we're done fading
578                    this.fading = Audio.FADE_NONE;
579                }
580                if (log.isDebugEnabled()) {
581                    log.debug("Set fade in gain of AudioSource {} to {}", this.getSystemName(), this.fadeGain);
582                }
583                break;
584            default:
585                throw new IllegalArgumentException();
586        }
587    }
588
589    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
590    // types to implement this (yet).  So default to failing.
591    public boolean queueAudioBuffers(Queue<AudioBuffer> audioBuffers) {
592        log.debug("Abstract queueAudioBuffers() called.");
593        return false;
594    }
595
596    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
597    // types to implement this (yet).  So default to failing.
598    public boolean queueAudioBuffer(AudioBuffer audioBuffer) {
599        return false;
600    }
601
602    public boolean unqueueAudioBuffers() {
603        return false;
604    }
605
606    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
607    // types to implement this (yet).  So default to failing.
608    @Override
609    public int numQueuedBuffers() {
610        return 0;
611    }
612
613    // Probably aught to be abstract, but I don't want to force the non-JOAL Source
614    // types to implement this (yet).  So default to failing.
615    @Override
616    public int numProcessedBuffers() {
617        return 0;
618    }
619
620    /**
621     * Binds this AudioSource with the specified AudioBuffer.
622     * <p>
623     * Applies only to sub-types:
624     * <ul>
625     * <li>Source
626     * </ul>
627     *
628     * @param buffer The AudioBuffer to bind to this AudioSource
629     * @return true if successful
630     */
631    abstract boolean bindAudioBuffer(AudioBuffer buffer);
632
633    /**
634     * Method to define if this AudioSource has been bound to an AudioBuffer.
635     *
636     * @param bound True if bound to an AudioBufferr
637     */
638    protected void setBound(boolean bound) {
639        this.bound = bound;
640    }
641
642    protected void setQueued(boolean queued) {
643        this.queued = queued;
644    }
645
646    @Override
647    public boolean isBound() {
648        return this.bound;
649    }
650
651    @Override
652    public boolean isQueued() {
653        return this.queued;
654    }
655
656    @Override
657    public void stateChanged(int oldState) {
658        // Get the current state
659        int i = this.getState();
660
661        // Check if the current state has changed to playing
662        if (i != oldState && i == STATE_PLAYING) {
663            // We've changed to playing so start the move thread
664            this.timeOfLastPositionCheck = System.currentTimeMillis();
665            AudioSourceMoveThread asmt = new AudioSourceMoveThread(this);
666            asmt.start();
667        }
668
669//        // Check if the current state has changed to stopped
670//        if (i!=oldState && i==STATE_STOPPED) {
671//            // We've changed to stopped so determine if we need to start the
672//            // loop delay thread
673//            if (isLooped() && getMinLoops()!=LOOP_CONTINUOUS) {
674//                // Yes, we need to
675//                if (asdt!=null) {
676//                    asdt.cleanup();
677//                    asdt = null;
678//                }
679//                asdt = new AudioSourceDelayThread(this);
680//                asdt.start();
681//            }
682//        }
683    }
684
685    @Override
686    public void play() {
687        this.fading = Audio.FADE_NONE;
688//        if (asdt!=null) {
689//            asdt.interrupt();
690//        }
691        if (this.getState() != STATE_PLAYING) {
692            this.setState(STATE_PLAYING);
693            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PLAY));
694            activeAudioFactory.getCommandThread().interrupt();
695        }
696    }
697
698    /**
699     * Play the clip from the beginning. If looped, start looping.
700     */
701    abstract protected void doPlay();
702
703    @Override
704    public void stop() {
705        stop(true);
706    }
707
708    private void stop(boolean interruptThread) {
709        this.fading = Audio.FADE_NONE;
710        this.setState(STATE_STOPPED);
711        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_STOP));
712        if (interruptThread) {
713            activeAudioFactory.getCommandThread().interrupt();
714        }
715    }
716
717    /**
718     * Stop playing the clip and rewind to the beginning.
719     */
720    abstract protected void doStop();
721
722    @Override
723    public void togglePlay() {
724        this.fading = Audio.FADE_NONE;
725        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PLAY_TOGGLE));
726        activeAudioFactory.getCommandThread().interrupt();
727    }
728
729    /**
730     * Toggle the current playing status. Will always start at/return to the
731     * beginning of the sample.
732     */
733    protected void doTogglePlay() {
734        if (this.getState() == STATE_PLAYING) {
735            stop();
736        } else {
737            play();
738        }
739    }
740
741    @Override
742    public void pause() {
743        this.fading = Audio.FADE_NONE;
744        this.setState(STATE_STOPPED);
745        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PAUSE));
746        activeAudioFactory.getCommandThread().interrupt();
747    }
748
749    /**
750     * Stop playing the clip but retain the current position.
751     */
752    abstract protected void doPause();
753
754    @Override
755    public void resume() {
756        this.fading = Audio.FADE_NONE;
757        this.setState(STATE_PLAYING);
758        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_RESUME));
759        activeAudioFactory.getCommandThread().interrupt();
760    }
761
762    /**
763     * Play the clip from the current position.
764     */
765    abstract protected void doResume();
766
767    @Override
768    public void togglePause() {
769        this.fading = Audio.FADE_NONE;
770        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_PAUSE_TOGGLE));
771        activeAudioFactory.getCommandThread().interrupt();
772    }
773
774    /**
775     * Toggle the current playing status. Will retain the playback position of
776     * the sample.
777     */
778    protected void doTogglePause() {
779        if (this.getState() == STATE_PLAYING) {
780            pause();
781        } else {
782            resume();
783        }
784    }
785
786    @Override
787    public void rewind() {
788        this.fading = Audio.FADE_NONE;
789        this.setState(STATE_STOPPED);
790        activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_REWIND));
791        activeAudioFactory.getCommandThread().interrupt();
792    }
793
794    /**
795     * Rewind clip to the beginning.
796     */
797    abstract protected void doRewind();
798
799    @Override
800    public void fadeIn() {
801        if (this.getState() != STATE_PLAYING && this.fading != Audio.FADE_IN) {
802            this.fading = Audio.FADE_IN;
803            this.fadeGain = 0.0f;
804            this.timeOfLastFadeCheck = System.currentTimeMillis();
805            this.setState(STATE_PLAYING);
806            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_FADE_IN));
807            activeAudioFactory.getCommandThread().interrupt();
808        }
809    }
810
811    /**
812     * Fade in then play this AudioSource.
813     */
814    abstract protected void doFadeIn();
815
816    @Override
817    public void fadeOut() {
818        if (this.getState() == STATE_PLAYING && this.fading != Audio.FADE_OUT) {
819            this.fading = Audio.FADE_OUT;
820            this.fadeGain = 1.0f;
821            this.timeOfLastFadeCheck = System.currentTimeMillis();
822            this.setState(STATE_PLAYING);
823            activeAudioFactory.audioCommandQueue(new AudioCommand(this, Audio.CMD_FADE_OUT));
824            activeAudioFactory.getCommandThread().interrupt();
825        }
826    }
827
828    /**
829     * Fade out then stop this AudioSource.
830     */
831    abstract protected void doFadeOut();
832
833    /**
834     * Get the current fading status.
835     *
836     * @return fading status
837     */
838    protected int getFading() {
839        return this.fading;
840    }
841
842    @Override
843    @Nonnull
844    public String getDebugString() {
845        return "Pos: " + this.getPosition().toString()
846                + ", bound to: " + this.getAssignedBufferName()
847                + ", loops: "
848                + ((this.getMinLoops() == LOOP_CONTINUOUS) ? "infinite"
849                        : ((!this.isLooped()) ? "none"
850                                : "(min=" + this.getMinLoops() + " max=" + this.getMaxLoops() + ")"));
851    }
852
853    private static final Logger log = LoggerFactory.getLogger(AbstractAudioSource.class);
854
855    /**
856     * An internal class used to create a new thread to monitor and maintain
857     * fade in and fade out levels.
858     * <p>
859     * Will exist only as long as this source is in the process of fading in or
860     * out.
861     */
862    protected static class AudioSourceFadeThread extends AbstractAudioThread {
863
864        /**
865         * Reference to the AudioSource object being monitored.
866         */
867        private AbstractAudioSource audioSource;
868
869        /**
870         * Internal variable to hold the fade direction.
871         */
872        private final int fadeDirection;
873
874        /**
875         * Constructor that takes handle to looping AudioSource to monitor.
876         *
877         * @param audioSource looping AudioSource to monitor
878         */
879        AudioSourceFadeThread(AbstractAudioSource audioSource) {
880            super();
881            this.setName("fadesrc-" + super.getName());
882            this.audioSource = audioSource;
883            this.fadeDirection = audioSource.getFading();
884            if (log.isDebugEnabled()) {
885                log.debug("Created AudioSourceFadeThread for AudioSource {}", audioSource.getSystemName());
886            }
887        }
888
889        /**
890         * Main processing loop.
891         */
892        @Override
893        public void run() {
894
895            while (!dying()) {
896
897                // Recalculate the fade levels
898                audioSource.calculateFades();
899
900                // Recalculate the gain levels
901                audioSource.calculateGain();
902
903                // Check if we've done fading
904                if (audioSource.getFading() == Audio.FADE_NONE) {
905                    die();
906                }
907
908                // sleep for a while so as not to overload CPU
909                snooze(20);
910            }
911
912            // Reset fades
913            audioSource.calculateFades();
914
915            // Check if we were fading out and, if so, stop.
916            // Otherwise reset gain
917            if (this.fadeDirection == Audio.FADE_OUT) {
918                audioSource.doStop();
919            } else {
920                audioSource.calculateGain();
921            }
922
923            // Finish up
924            if (log.isDebugEnabled()) {
925                log.debug("Clean up thread {}", this.getName());
926            }
927            cleanup();
928        }
929
930        /**
931         * Shut down this thread and clear references to created objects.
932         */
933        @Override
934        protected void cleanup() {
935            // Thread is to shutdown
936            die();
937
938            // Clear references to objects
939            this.audioSource = null;
940
941            // Finalise cleanup in super-class
942            super.cleanup();
943        }
944    }
945
946    /**
947     * An internal class used to create a new thread to monitor and maintain
948     * current source position with respect to velocity.
949     */
950    protected static class AudioSourceMoveThread extends AbstractAudioThread {
951
952        /**
953         * Reference to the AudioSource object being monitored.
954         */
955        private AbstractAudioSource audioSource;
956
957        /**
958         * Constructor that takes handle to looping AudioSource to monitor.
959         *
960         * @param audioSource looping AudioSource to monitor
961         */
962        AudioSourceMoveThread(AbstractAudioSource audioSource) {
963            super();
964            this.setName("movesrc-" + super.getName());
965            this.audioSource = audioSource;
966            if (log.isDebugEnabled()) {
967                log.debug("Created AudioSourceMoveThread for AudioSource {}", audioSource.getSystemName());
968            }
969        }
970
971        /**
972         * Main processing loop.
973         */
974        @Override
975        public void run() {
976
977            while (!dying()) {
978
979                // Recalculate the position
980                audioSource.calculateCurrentPosition();
981
982                // Check state and die if not playing
983                if (audioSource.getState() != STATE_PLAYING) {
984                    die();
985                }
986
987                // sleep for a while so as not to overload CPU
988                snooze(100);
989            }
990
991//            // Reset the current position
992//            audioSource.resetCurrentPosition();
993            // Finish up
994            if (log.isDebugEnabled()) {
995                log.debug("Clean up thread {}", this.getName());
996            }
997            cleanup();
998        }
999
1000        /**
1001         * Shuts this thread down and clears references to created objects.
1002         */
1003        @Override
1004        protected void cleanup() {
1005            // Thread is to shutdown
1006            die();
1007
1008            // Clear references to objects
1009            this.audioSource = null;
1010
1011            // Finalise cleanup in super-class
1012            super.cleanup();
1013        }
1014    }
1015
1016//    /**
1017//     * An internal class used to create a new thread to delay subsequent
1018//     * playbacks of a non-continuous looped source.
1019//     */
1020//    private class AudioSourceDelayThread extends Thread {
1021//
1022//        /**
1023//         * Reference to the AudioSource object being monitored
1024//         */
1025//        private AbstractAudioSource audioSource;
1026//
1027//        /**
1028//         * Constructor that takes handle to looping AudioSource to monitor
1029//         *
1030//         * @param audioSource looping AudioSource to monitor
1031//         */
1032//        AudioSourceDelayThread(AbstractAudioSource audioSource) {
1033//            super();
1034//            this.setName("delaysrc-"+super.getName());
1035//            this.audioSource = audioSource;
1036//            if (log.isDebugEnabled()) log.debug("Created AudioSourceDelayThread for AudioSource " + audioSource.getSystemName());
1037//        }
1038//
1039//        /**
1040//         * Main processing loop
1041//         */
1042//        @Override
1043//        public void run() {
1044//
1045//            // Sleep for the required period of time
1046//            try {
1047//                Thread.sleep(audioSource.getLoopDelay());
1048//            } catch (InterruptedException ex) {}
1049//
1050//            // Restart playing this AudioSource
1051//            this.audioSource.play();
1052//
1053//            // Finish up
1054//            if (log.isDebugEnabled()) log.debug("Clean up thread " + this.getName());
1055//            cleanup();
1056//        }
1057//
1058//        /**
1059//         * Shuts this thread down and clears references to created objects
1060//         */
1061//        protected void cleanup() {
1062//            // Clear references to objects
1063//            this.audioSource = null;
1064//        }
1065//    }
1066
1067}