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