/*
* Copyright (c) 2009-2012 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 a scene Node which can play audio assets.
*
* An AudioNode is either positional or ambient, with positional being the
* default. Once a positional node is attached to the scene, its location and
* velocity relative to the {@link Listener} affect how it sounds when played.
* Positional nodes can only play monoaural (single-channel) assets, not stereo
* ones.
*
* An ambient AudioNode plays in "headspace", meaning that the node's location
* and velocity do not affect how it sounds when played. Ambient audio nodes can
* play stereo assets.
*
* The "positional" property of an AudioNode can be set via
* {@link AudioNode#setPositional(boolean) }.
*
* @author normenhansen
* @author Kirill Vainer
*/
public class AudioNode extends Node implements AudioSource {
//Version #1 : AudioKey is now stored into "audio_key" instead of "key"
public static final int SAVABLE_VERSION = 1;
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 AudioSource.Status status = AudioSource.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.
* @deprecated - use AudioSource.Status instead
*/
@Deprecated
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> 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 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 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(){
if (positional && data.getChannels() > 1) {
throw new IllegalStateException("Only mono audio is supported for positional audio nodes");
}
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(){
if (positional && data.getChannels() > 1) {
throw new IllegalStateException("Only mono audio is supported for positional audio nodes");
}
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 != AudioSource.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 AudioSource.Status getStatus() {
return status;
}
/**
* Do not use.
*/
public final void setStatus(AudioSource.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 the sound sample when to start playing.
*/
public float getTimeOffset() {
return timeOffset;
}
/**
* Set the time offset in the sound sample when to 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) {
((AudioStream) data).setTime(timeOffset);
}else if(status == AudioSource.Status.Playing){
stop();
play();
}
}
public Vector3f getPosition() {
return getWorldTranslation();
}
/**
* @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().loadAsset(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 + "]";
}
}