aboutsummaryrefslogtreecommitdiff
path: root/engine/src/core/com/jme3/audio/AudioNode.java
blob: bc8b8cd12bba0caaab6aed86061fec8472086fb2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
/*
 * Copyright (c) 2009-2010 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jme3.audio;

import com.jme3.asset.AssetManager;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.util.PlaceholderAssets;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * An <code>AudioNode</code> is used in jME3 for playing audio files.
 * <br/>
 * First, an {@link AudioNode} is loaded from file, and then assigned
 * to an audio node for playback. Once the audio node is attached to the 
 * scene, its location will influence the position it is playing from relative
 * to the {@link Listener}.
 * <br/>
 * An audio node can also play in "headspace", meaning its location
 * or velocity does not influence how it is played. 
 * The "positional" property of an AudioNode can be set via 
 * {@link AudioNode#setPositional(boolean) }.
 * 
 * @author normenhansen
 * @author Kirill Vainer
 */
public class AudioNode extends Node {

    protected boolean loop = false;
    protected float volume = 1;
    protected float pitch = 1;
    protected float timeOffset = 0;
    protected Filter dryFilter;
    protected AudioKey audioKey;
    protected transient AudioData data = null;
    protected transient volatile Status status = Status.Stopped;
    protected transient volatile int channel = -1;
    protected Vector3f velocity = new Vector3f();
    protected boolean reverbEnabled = true;
    protected float maxDistance = 200; // 200 meters
    protected float refDistance = 10; // 10 meters
    protected Filter reverbFilter;
    private boolean directional = false;
    protected Vector3f direction = new Vector3f(0, 0, 1);
    protected float innerAngle = 360;
    protected float outerAngle = 360;
    protected boolean positional = true;

    /**
     * <code>Status</code> indicates the current status of the audio node.
     */
    public enum Status {
        /**
         * The audio node is currently playing. This will be set if
         * {@link AudioNode#play() } is called.
         */
        Playing,
        
        /**
         * The audio node is currently paused.
         */
        Paused,
        
        /**
         * The audio node is currently stopped.
         * This will be set if {@link AudioNode#stop() } is called 
         * or the audio has reached the end of the file.
         */
        Stopped,
    }

    /**
     * Creates a new <code>AudioNode</code> without any audio data set.
     */
    public AudioNode() {
    }

    /**
     * Creates a new <code>AudioNode</code> without any audio data set.
     * 
     * @param audioRenderer The audio renderer to use for playing. Cannot be null.
     *
     * @deprecated AudioRenderer parameter is ignored.
     */
    public AudioNode(AudioRenderer audioRenderer) {
    }

    /**
     * Creates a new <code>AudioNode</code> with the given data and key.
     * 
     * @param audioRenderer The audio renderer to use for playing. Cannot be null.
     * @param audioData The audio data contains the audio track to play.
     * @param audioKey The audio key that was used to load the AudioData
     *
     * @deprecated AudioRenderer parameter is ignored.
     */
    public AudioNode(AudioRenderer audioRenderer, AudioData audioData, AudioKey audioKey) {
        setAudioData(audioData, audioKey);
    }

    /**
     * Creates a new <code>AudioNode</code> with the given data and key.
     * 
     * @param audioData The audio data contains the audio track to play.
     * @param audioKey The audio key that was used to load the AudioData
     */
    public AudioNode(AudioData audioData, AudioKey audioKey) {
        setAudioData(audioData, audioKey);
    }

    /**
     * Creates a new <code>AudioNode</code> with the given audio file.
     * 
     * @param audioRenderer The audio renderer to use for playing. Cannot be null.
     * @param assetManager The asset manager to use to load the audio file
     * @param name The filename of the audio file
     * @param stream If true, the audio will be streamed gradually from disk, 
     *               otherwise, it will be buffered.
     * @param streamCache If stream is also true, then this specifies if
     * the stream cache is used. When enabled, the audio stream will
     * be read entirely but not decoded, allowing features such as 
     * seeking, looping and determining duration.
     *
     * @deprecated AudioRenderer parameter is ignored.
     */
    public AudioNode(AudioRenderer audioRenderer, AssetManager assetManager, String name, boolean stream, boolean streamCache) {
        this.audioKey = new AudioKey(name, stream, streamCache);
        this.data = (AudioData) assetManager.loadAsset(audioKey);
    }

    /**
     * Creates a new <code>AudioNode</code> with the given audio file.
     * 
     * @param assetManager The asset manager to use to load the audio file
     * @param name The filename of the audio file
     * @param stream If true, the audio will be streamed gradually from disk, 
     *               otherwise, it will be buffered.
     * @param streamCache If stream is also true, then this specifies if
     * the stream cache is used. When enabled, the audio stream will
     * be read entirely but not decoded, allowing features such as 
     * seeking, looping and determining duration.
     */
    public AudioNode(AssetManager assetManager, String name, boolean stream, boolean streamCache) {
        this.audioKey = new AudioKey(name, stream, streamCache);
        this.data = (AudioData) assetManager.loadAsset(audioKey);
    }
    
    /**
     * Creates a new <code>AudioNode</code> with the given audio file.
     * 
     * @param audioRenderer The audio renderer to use for playing. Cannot be null.
     * @param assetManager The asset manager to use to load the audio file
     * @param name The filename of the audio file
     * @param stream If true, the audio will be streamed gradually from disk, 
     *               otherwise, it will be buffered.
     *
     * @deprecated AudioRenderer parameter is ignored.
     */
    public AudioNode(AudioRenderer audioRenderer, AssetManager assetManager, String name, boolean stream) {
        this(audioRenderer, assetManager, name, stream, false);
    }

    /**
     * Creates a new <code>AudioNode</code> with the given audio file.
     * 
     * @param assetManager The asset manager to use to load the audio file
     * @param name The filename of the audio file
     * @param stream If true, the audio will be streamed gradually from disk, 
     *               otherwise, it will be buffered.
     */
    public AudioNode(AssetManager assetManager, String name, boolean stream) {
        this(assetManager, name, stream, false);
    }

    /**
     * Creates a new <code>AudioNode</code> with the given audio file.
     * 
     * @param audioRenderer The audio renderer to use for playing. Cannot be null.
     * @param assetManager The asset manager to use to load the audio file
     * @param name The filename of the audio file
     * 
     * @deprecated AudioRenderer parameter is ignored.
     */
    public AudioNode(AudioRenderer audioRenderer, AssetManager assetManager, String name) {
        this(assetManager, name, false);
    }
    
    /**
     * Creates a new <code>AudioNode</code> with the given audio file.
     * 
     * @param assetManager The asset manager to use to load the audio file
     * @param name The filename of the audio file
     */
    public AudioNode(AssetManager assetManager, String name) {
        this(assetManager, name, false);
    }
    
    protected AudioRenderer getRenderer() {
        AudioRenderer result = AudioContext.getAudioRenderer();
        if( result == null )
            throw new IllegalStateException( "No audio renderer available, make sure call is being performed on render thread." );
        return result;            
    }
    
    /**
     * Start playing the audio.
     */
    public void play(){
        getRenderer().playSource(this);
    }

    /**
     * Start playing an instance of this audio. This method can be used
     * to play the same <code>AudioNode</code> multiple times. Note
     * that changes to the parameters of this AudioNode will not effect the 
     * instances already playing.
     */
    public void playInstance(){
        getRenderer().playSourceInstance(this);
    }
    
    /**
     * Stop playing the audio that was started with {@link AudioNode#play() }.
     */
    public void stop(){
        getRenderer().stopSource(this);
    }
    
    /**
     * Pause the audio that was started with {@link AudioNode#play() }.
     */
    public void pause(){
        getRenderer().pauseSource(this);
    }
    
    /**
     * Do not use.
     */
    public final void setChannel(int channel) {
        if (status != Status.Stopped) {
            throw new IllegalStateException("Can only set source id when stopped");
        }

        this.channel = channel;
    }

    /**
     * Do not use.
     */
    public int getChannel() {
        return channel;
    }

    /**
     * @return The {#link Filter dry filter} that is set.
     * @see AudioNode#setDryFilter(com.jme3.audio.Filter) 
     */
    public Filter getDryFilter() {
        return dryFilter;
    }

    /**
     * Set the dry filter to use for this audio node.
     * 
     * When {@link AudioNode#setReverbEnabled(boolean) reverb} is used, 
     * the dry filter will only influence the "dry" portion of the audio, 
     * e.g. not the reverberated parts of the AudioNode playing.
     * 
     * See the relevent documentation for the {@link Filter} to determine
     * the effect.
     * 
     * @param dryFilter The filter to set, or null to disable dry filter.
     */
    public void setDryFilter(Filter dryFilter) {
        this.dryFilter = dryFilter;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.DryFilter);
    }

    /**
     * Set the audio data to use for the audio. Note that this method
     * can only be called once, if for example the audio node was initialized
     * without an {@link AudioData}.
     * 
     * @param audioData The audio data contains the audio track to play.
     * @param audioKey The audio key that was used to load the AudioData
     */
    public void setAudioData(AudioData audioData, AudioKey audioKey) {
        if (data != null) {
            throw new IllegalStateException("Cannot change data once its set");
        }

        data = audioData;
        this.audioKey = audioKey;
    }

    /**
     * @return The {@link AudioData} set previously with 
     * {@link AudioNode#setAudioData(com.jme3.audio.AudioData, com.jme3.audio.AudioKey) }
     * or any of the constructors that initialize the audio data.
     */
    public AudioData getAudioData() {
        return data;
    }

    /**
     * @return The {@link Status} of the audio node. 
     * The status will be changed when either the {@link AudioNode#play() }
     * or {@link AudioNode#stop() } methods are called.
     */
    public Status getStatus() {
        return status;
    }

    /**
     * Do not use.
     */
    public final void setStatus(Status status) {
        this.status = status;
    }

    /**
     * @return True if the audio will keep looping after it is done playing,
     * otherwise, false.
     * @see AudioNode#setLooping(boolean)
     */
    public boolean isLooping() {
        return loop;
    }

    /**
     * Set the looping mode for the audio node. The default is false.
     * 
     * @param loop True if the audio should keep looping after it is done playing.
     */
    public void setLooping(boolean loop) {
        this.loop = loop;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.Looping);
    }

    /**
     * @return The pitch of the audio, also the speed of playback.
     * 
     * @see AudioNode#setPitch(float) 
     */
    public float getPitch() {
        return pitch;
    }

    /**
     * Set the pitch of the audio, also the speed of playback.
     * The value must be between 0.5 and 2.0.
     * 
     * @param pitch The pitch to set.
     * @throws IllegalArgumentException If pitch is not between 0.5 and 2.0.
     */
    public void setPitch(float pitch) {
        if (pitch < 0.5f || pitch > 2.0f) {
            throw new IllegalArgumentException("Pitch must be between 0.5 and 2.0");
        }

        this.pitch = pitch;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.Pitch);
    }

    /**
     * @return The volume of this audio node.
     * 
     * @see AudioNode#setVolume(float)
     */
    public float getVolume() {
        return volume;
    }

    /**
     * Set the volume of this audio node.
     * 
     * The volume is specified as gain. 1.0 is the default.
     * 
     * @param volume The volume to set.
     * @throws IllegalArgumentException If volume is negative
     */
    public void setVolume(float volume) {
        if (volume < 0f) {
            throw new IllegalArgumentException("Volume cannot be negative");
        }

        this.volume = volume;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.Volume);
    }

    /**
     * @return The time offset in seconds when the sound will start playing.
     */
    public float getTimeOffset() {
        return timeOffset;
    }

    /**
     * Set the time offset in seconds when the sound will start playing.
     * 
     * @param timeOffset The time offset
     * @throws IllegalArgumentException If timeOffset is negative
     */
    public void setTimeOffset(float timeOffset) {
        if (timeOffset < 0f) {
            throw new IllegalArgumentException("Time offset cannot be negative");
        }

        this.timeOffset = timeOffset;
        if (data instanceof AudioStream) {
            System.out.println("request setTime");
            ((AudioStream) data).setTime(timeOffset);
        }else if(status == Status.Playing){
            stop();
            play();
        }
    }

    /**
     * @return The velocity of the audio node.
     * 
     * @see AudioNode#setVelocity(com.jme3.math.Vector3f)
     */
    public Vector3f getVelocity() {
        return velocity;
    }

    /**
     * Set the velocity of the audio node. The velocity is expected
     * to be in meters. Does nothing if the audio node is not positional.
     * 
     * @param velocity The velocity to set.
     * @see AudioNode#setPositional(boolean)
     */
    public void setVelocity(Vector3f velocity) {
        this.velocity.set(velocity);
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.Velocity);
    }

    /**
     * @return True if reverb is enabled, otherwise false.
     * 
     * @see AudioNode#setReverbEnabled(boolean)
     */
    public boolean isReverbEnabled() {
        return reverbEnabled;
    }

    /**
     * Set to true to enable reverberation effects for this audio node.
     * Does nothing if the audio node is not positional.
     * <br/>
     * When enabled, the audio environment set with 
     * {@link AudioRenderer#setEnvironment(com.jme3.audio.Environment) }
     * will apply a reverb effect to the audio playing from this audio node.
     * 
     * @param reverbEnabled True to enable reverb.
     */
    public void setReverbEnabled(boolean reverbEnabled) {
        this.reverbEnabled = reverbEnabled;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.ReverbEnabled);
    }

    /**
     * @return Filter for the reverberations of this audio node.
     * 
     * @see AudioNode#setReverbFilter(com.jme3.audio.Filter) 
     */
    public Filter getReverbFilter() {
        return reverbFilter;
    }

    /**
     * Set the reverb filter for this audio node.
     * <br/>
     * The reverb filter will influence the reverberations
     * of the audio node playing. This only has an effect if
     * reverb is enabled.
     * 
     * @param reverbFilter The reverb filter to set.
     * @see AudioNode#setDryFilter(com.jme3.audio.Filter)
     */
    public void setReverbFilter(Filter reverbFilter) {
        this.reverbFilter = reverbFilter;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.ReverbFilter);
    }

    /**
     * @return Max distance for this audio node.
     * 
     * @see AudioNode#setMaxDistance(float)
     */
    public float getMaxDistance() {
        return maxDistance;
    }

    /**
     * Set the maximum distance for the attenuation of the audio node.
     * Does nothing if the audio node is not positional.
     * <br/>
     * The maximum distance is the distance beyond which the audio
     * node will no longer be attenuated.  Normal attenuation is logarithmic
     * from refDistance (it reduces by half when the distance doubles).
     * Max distance sets where this fall-off stops and the sound will never
     * get any quieter than at that distance.  If you want a sound to fall-off
     * very quickly then set ref distance very short and leave this distance
     * very long.
     * 
     * @param maxDistance The maximum playing distance.
     * @throws IllegalArgumentException If maxDistance is negative
     */
    public void setMaxDistance(float maxDistance) {
        if (maxDistance < 0) {
            throw new IllegalArgumentException("Max distance cannot be negative");
        }

        this.maxDistance = maxDistance;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.MaxDistance);
    }

    /**
     * @return The reference playing distance for the audio node.
     * 
     * @see AudioNode#setRefDistance(float) 
     */
    public float getRefDistance() {
        return refDistance;
    }

    /**
     * Set the reference playing distance for the audio node.
     * Does nothing if the audio node is not positional.
     * <br/>
     * The reference playing distance is the distance at which the
     * audio node will be exactly half of its volume.
     * 
     * @param refDistance The reference playing distance.
     * @throws  IllegalArgumentException If refDistance is negative
     */
    public void setRefDistance(float refDistance) {
        if (refDistance < 0) {
            throw new IllegalArgumentException("Reference distance cannot be negative");
        }

        this.refDistance = refDistance;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.RefDistance);
    }

    /**
     * @return True if the audio node is directional
     * 
     * @see AudioNode#setDirectional(boolean) 
     */
    public boolean isDirectional() {
        return directional;
    }

    /**
     * Set the audio node to be directional.
     * Does nothing if the audio node is not positional.
     * <br/>
     * After setting directional, you should call 
     * {@link AudioNode#setDirection(com.jme3.math.Vector3f) }
     * to set the audio node's direction.
     * 
     * @param directional If the audio node is directional
     */
    public void setDirectional(boolean directional) {
        this.directional = directional;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.IsDirectional);
    }

    /**
     * @return The direction of this audio node.
     * 
     * @see AudioNode#setDirection(com.jme3.math.Vector3f)
     */
    public Vector3f getDirection() {
        return direction;
    }

    /**
     * Set the direction of this audio node.
     * Does nothing if the audio node is not directional.
     * 
     * @param direction 
     * @see AudioNode#setDirectional(boolean) 
     */
    public void setDirection(Vector3f direction) {
        this.direction = direction;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.Direction);
    }

    /**
     * @return The directional audio node, cone inner angle.
     * 
     * @see AudioNode#setInnerAngle(float) 
     */
    public float getInnerAngle() {
        return innerAngle;
    }

    /**
     * Set the directional audio node cone inner angle.
     * Does nothing if the audio node is not directional.
     * 
     * @param innerAngle The cone inner angle.
     */
    public void setInnerAngle(float innerAngle) {
        this.innerAngle = innerAngle;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.InnerAngle);
    }

    /**
     * @return The directional audio node, cone outer angle.
     * 
     * @see AudioNode#setOuterAngle(float) 
     */
    public float getOuterAngle() {
        return outerAngle;
    }

    /**
     * Set the directional audio node cone outer angle.
     * Does nothing if the audio node is not directional.
     * 
     * @param outerAngle The cone outer angle.
     */
    public void setOuterAngle(float outerAngle) {
        this.outerAngle = outerAngle;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.OuterAngle);
    }

    /**
     * @return True if the audio node is positional.
     * 
     * @see AudioNode#setPositional(boolean) 
     */
    public boolean isPositional() {
        return positional;
    }

    /**
     * Set the audio node as positional.
     * The position, velocity, and distance parameters effect positional
     * audio nodes. Set to false if the audio node should play in "headspace".
     * 
     * @param positional True if the audio node should be positional, otherwise
     * false if it should be headspace.
     */
    public void setPositional(boolean positional) {
        this.positional = positional;
        if (channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.IsPositional);
    }

    @Override
    public void updateGeometricState(){
        boolean updatePos = false;
        if ((refreshFlags & RF_TRANSFORM) != 0){
            updatePos = true;
        }
        
        super.updateGeometricState();

        if (updatePos && channel >= 0)
            getRenderer().updateSourceParam(this, AudioParam.Position);
    }

    @Override
    public AudioNode clone(){
        AudioNode clone = (AudioNode) super.clone();
        
        clone.direction = direction.clone();
        clone.velocity  = velocity.clone();
        
        return clone;
    }
    
    @Override
    public void write(JmeExporter ex) throws IOException {
        super.write(ex);
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(audioKey, "audio_key", null);
        oc.write(loop, "looping", false);
        oc.write(volume, "volume", 1);
        oc.write(pitch, "pitch", 1);
        oc.write(timeOffset, "time_offset", 0);
        oc.write(dryFilter, "dry_filter", null);

        oc.write(velocity, "velocity", null);
        oc.write(reverbEnabled, "reverb_enabled", false);
        oc.write(reverbFilter, "reverb_filter", null);
        oc.write(maxDistance, "max_distance", 20);
        oc.write(refDistance, "ref_distance", 10);

        oc.write(directional, "directional", false);
        oc.write(direction, "direction", null);
        oc.write(innerAngle, "inner_angle", 360);
        oc.write(outerAngle, "outer_angle", 360);
        
        oc.write(positional, "positional", false);
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        super.read(im);
        InputCapsule ic = im.getCapsule(this);
        
        // NOTE: In previous versions of jME3, audioKey was actually
        // written with the name "key". This has been changed
        // to "audio_key" in case Spatial's key will be written as "key".
        if (ic.getSavableVersion(AudioNode.class) == 0){
            audioKey = (AudioKey) ic.readSavable("key", null);
        }else{
            audioKey = (AudioKey) ic.readSavable("audio_key", null);
        }
        
        loop = ic.readBoolean("looping", false);
        volume = ic.readFloat("volume", 1);
        pitch = ic.readFloat("pitch", 1);
        timeOffset = ic.readFloat("time_offset", 0);
        dryFilter = (Filter) ic.readSavable("dry_filter", null);

        velocity = (Vector3f) ic.readSavable("velocity", null);
        reverbEnabled = ic.readBoolean("reverb_enabled", false);
        reverbFilter = (Filter) ic.readSavable("reverb_filter", null);
        maxDistance = ic.readFloat("max_distance", 20);
        refDistance = ic.readFloat("ref_distance", 10);

        directional = ic.readBoolean("directional", false);
        direction = (Vector3f) ic.readSavable("direction", null);
        innerAngle = ic.readFloat("inner_angle", 360);
        outerAngle = ic.readFloat("outer_angle", 360);
        
        positional = ic.readBoolean("positional", false);
        
        if (audioKey != null) {
            try {
                data = im.getAssetManager().loadAudio(audioKey);
            } catch (AssetNotFoundException ex){
                Logger.getLogger(AudioNode.class.getName()).log(Level.FINE, "Cannot locate {0} for audio node {1}", new Object[]{audioKey, key});
                data = PlaceholderAssets.getPlaceholderAudio();
            }
        }
    }

    @Override
    public String toString() {
        String ret = getClass().getSimpleName()
                + "[status=" + status;
        if (volume != 1f) {
            ret += ", vol=" + volume;
        }
        if (pitch != 1f) {
            ret += ", pitch=" + pitch;
        }
        return ret + "]";
    }
}