/* * Copyright (c) 2009-2012, 2016 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.audio.AudioData.DataType; 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 com.jme3.util.clone.Cloner; 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 previousWorldTranslation = Vector3f.NAN.clone(); protected Vector3f velocity = new Vector3f(); protected boolean reverbEnabled = false; 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; protected boolean velocityFromTranslation = false; protected float lastTpf; /** * <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 type The type. If <code>{@link com.jme3.audio.AudioData.DataType}.Stream</code>, the audio will be streamed gradually from disk, * otherwise it will be buffered (<code>{@link com.jme3.audio.AudioData.DataType}.Buffer</code>) */ public AudioNode(AssetManager assetManager, String name, DataType type) { this(assetManager, name, type == DataType.Stream, true); } /** * 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. * * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead */ 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. * * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType)} instead */ public AudioNode(AssetManager assetManager, String name, boolean stream) { this(assetManager, name, stream, true); // Always streamCached } /** * 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, DataType.Buffer); } /** * 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 * @deprecated Use {@link AudioNode#AudioNode(com.jme3.asset.AssetManager, java.lang.String, com.jme3.audio.AudioData.DataType) } instead */ public AudioNode(AssetManager assetManager, String name) { this(assetManager, name, DataType.Buffer); } 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; } /** * Get the Type of the underlying AudioData to see if it's streamed or buffered. * This is a shortcut to getAudioData().getType() * <b>Warning</b>: Can return null! * @return The {@link com.jme3.audio.AudioData.DataType} of the audio node. */ public DataType getType() { if (data == null) return null; else return data.getDataType(); } /** * @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(); } } @Override public float getPlaybackTime() { if (channel >= 0) return getRenderer().getSourcePlaybackTime(this); else return 0; } 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); } } public boolean isVelocityFromTranslation() { return velocityFromTranslation; } public void setVelocityFromTranslation(boolean velocityFromTranslation) { this.velocityFromTranslation = velocityFromTranslation; } @Override public void updateLogicalState(float tpf) { super.updateLogicalState(tpf); lastTpf = tpf; } @Override public void updateGeometricState() { super.updateGeometricState(); if (channel < 0) { return; } Vector3f currentWorldTranslation = worldTransform.getTranslation(); if (Float.isNaN(previousWorldTranslation.x) || !previousWorldTranslation.equals(currentWorldTranslation)) { getRenderer().updateSourceParam(this, AudioParam.Position); if (velocityFromTranslation) { velocity.set(currentWorldTranslation).subtractLocal(previousWorldTranslation); velocity.multLocal(1f / lastTpf); getRenderer().updateSourceParam(this, AudioParam.Velocity); } previousWorldTranslation.set(currentWorldTranslation); } } @Override public AudioNode clone(){ AudioNode clone = (AudioNode) super.clone(); clone.direction = direction.clone(); clone.velocity = velocity.clone(); return clone; } /** * Called internally by com.jme3.util.clone.Cloner. Do not call directly. */ @Override public void cloneFields( Cloner cloner, Object original ) { super.cloneFields(cloner, original); this.direction = cloner.clone(direction); this.velocity = cloner.clone(velocity); // Change in behavior: the filters were not cloned before meaning // that two cloned audio nodes would share the same filter instance. // While settings will only be applied when the filter is actually // set, I think it's probably surprising to callers if the values of // a filter change from one AudioNode when a different AudioNode's // filter attributes are updated. // Plus if they disable and re-enable the thing using the filter then // the settings get reapplied and it might be surprising to have them // suddenly be strange. // ...so I'll clone them. -pspeed this.dryFilter = cloner.clone(dryFilter); this.reverbFilter = cloner.clone(reverbFilter); } @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); oc.write(velocityFromTranslation, "velocity_from_translation", 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); velocityFromTranslation = ic.readBoolean("velocity_from_translation", 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 + "]"; } }