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