Audio

From Unify Community Wiki
Revision as of 10:13, 23 April 2009 by Jessy (Talk | contribs)

Jump to: navigation, search

Author: Jessy

Contents

Description

This is a script that replaces most properties of Audio Sources. I feel that its controls behave more intuitively then those of the Audio Source component itself, and it slightly extends functionality as well. I intend for it to be easy to switch from scripting Audio Sources directly, to utilizing this script.

Instructions

If your Game Object has only one Audio Source, then this script will automatically control it, when attached. Otherwise, you can programmatically assign what the script controls via the audioSource property.

The Volume, Min Volume, Max Volume, and Rolloff Factor parameters perform the tasks that the similarly-named parameters of Audio Sources were intended to perform. If scripting these, where you normally would type "audio" in your code, use "Audio" instead.

Rolloff Threshold defines a distance from the Audio Listener within which sound will not get quieter over distance. (Unity normally fixes this distance to 1 world unit.) This can be especially useful for dealing with "extreme" scale characters. (Use large values for giant monsters, small values for insects.)

If Ignore Distance is unchecked, it allows stereo clips to fade in volume over distance (but they do not get positioned in 3D "sound space"). For mono clips, checking this is a more efficient method for achieving the same result as Rolloff Factor = 0.

Performance Mode makes the code execute faster, but the effects of all properties, other than Rolloff Threshold and Ignore Distance, are not automatically updated every frame. You can call the RefreshLoudness(), RefreshMinLoudness(), RefreshMaxLoudness(), RefreshVolume(), and RefreshRolloffExponent() functions, to make changes only when necessary, from external code. It makes sense to leave Performance Mode off when setting up the scene, and then turn it on after tweaking, if you don't need these properties to be extremely dynamic.

JavaScript - Audio.js

<javascript>// If there is only one Audio Source attached to this game object, // and there are no other instances of this script attached, // automatically link this script to the Audio Source. private var audioSource : AudioSource; function Awake () { if (GetComponents(AudioSource).length < 2 && GetComponents(typeof this).length < 2) audioSource = audio; } // automatically links the first attached instance of this script // to an Audio Source, if none were present @script RequireComponent(AudioSource)

var volume : float = 1; var minVolume : float = 0; var maxVolume : float = 1; var rolloffFactor : float = 1; var rolloffThreshold : float = 1; var ignoreDistance = false;

// determines whether the "Refresh" functions will run in Update(), // or must be called manually var performanceMode = false;

private var loudnessExponent = Mathf.Log(Mathf.Sqrt(10), 2); private var loudness : float; private var minLoudness; private var maxLoudness; private var rolloffExponent;

function RefreshLoudness () {loudness = Mathf.Pow(volume, loudnessExponent);} function RefreshMinLoudness () {minLoudness = Mathf.Pow(minVolume, loudnessExponent);} function RefreshMaxLoudness () {maxLoudness = Mathf.Pow(maxVolume, loudnessExponent);} function RefreshVolume () {audioSource.volume = Mathf.Clamp(loudness, minLoudness, maxLoudness);} function RefreshRolloffExponent () {rolloffExponent = Mathf.Log(.5, (1 / rolloffFactor + 1));}

private var audioListener : Transform;

function Start() { // These properties are not useful; // initializing them this way keeps this script as tidy as possible. audioSource.minVolume = 0; audioSource.maxVolume = 1; audioSource.rolloffFactor = 0;

// Unity only allows values between 0 and 1 for volume. minVolume = Mathf.Clamp01(minVolume); maxVolume = Mathf.Clamp01(maxVolume);

// ensure maxVolume is not less than minVolume maxVolume = Mathf.Max(minVolume, maxVolume);

// Negative values don't make sense for these. rolloffFactor = Mathf.Max(rolloffFactor, 0); rolloffThreshold = Mathf.Max(rolloffThreshold, 0);

// Having this in Start() implies that you won't be moving the Audio Listener // to another object during gameplay. var listenerComponent : Component = FindObjectOfType(AudioListener); if (listenerComponent) audioListener = listenerComponent.gameObject.transform;

RefreshLoudness(); RefreshMinLoudness(); RefreshMaxLoudness(); RefreshVolume(); RefreshRolloffExponent(); }

function Update () { if (audioSource && audioListener) { if (performanceMode == false) { RefreshLoudness(); RefreshMinLoudness(); RefreshMaxLoudness(); if (ignoreDistance == true) RefreshVolume(); else RefreshRolloffExponent(); } if (ignoreDistance == false) { var distanceToListener = Vector3.Distance(audioListener.position, transform.position); var rolloffBase = distanceToListener / rolloffThreshold; var positionalVolume = Mathf.Pow(rolloffBase, rolloffExponent) * loudness; audioSource.volume = Mathf.Clamp(positionalVolume, minLoudness, maxLoudness); } } }</javascript>


Why I find it necessary to use this

Tweaking the controls for Audio Sources in Unity is not as predictable as it could be. Here are the rogue properties of Audio Sources, with their descriptions as provided by the Unity Reference Manual, and descriptions as they have actually behaved in my tests.

(To be clear, volume is a multiplier for audio samples, which directly correlate with how far your speaker drivers (tweeters, woofers, etc.) are displaced from their resting position during playback. volume = 0 means that your speakers won't be displaced at all. Simplifying a bit, volume = 1 means that the audio will play back as loud as it was recorded, and fractional values will play the audio back as if it were recorded at a lower level. As samples get played back over time, waveforms are recreated, so saying that volume is a multiplier for amplitude is accurate enough that I will be using the term "amplitude" to mean 'distance from zero', later on this page.)


Volume: How loud the sound is at a distance of 1 world unit (1 meter) from the Audio Listener.

This is only true in the Audio Listener's YZ plane. A sound traveling along the Audio Listener's X is 3 dB louder than it should be, according to this description, and it only plays through one speaker, at least in my two-speaker environment. Obviously, this is due to the 3D positional audio algorithm, but the description of the property is not precise, and the algorithm is highly unrealistic along the Audio Listener's X axis. (Fortunately, it gets better if more axes are involved.)

To avoid distortion when within 1 world unit of the Audio Listener, along the X axis, it seems that all mono clips imported into Unity are reduced in level by 3 dB. The Unity 2.5 Audio Importer's "Audio Channels" box also assures that no clips that are converted from stereo to mono have any peaks above -3 dB. Unfortunately, although this is apparently not the case for everyone, on my system, Unity actually clips all samples above -3 dB to exactly -3 dB, which causes distortion for clips with loud peaks. (An appropriate solution would be to multiply all samples by 10^(-3/20), if the 3 dB boost along the Audio Listener's X axis can not be avoided.)

Also, while AudioSource.volume can accept values between 0 and infinity, values above 1 do the same thing as 1. That is to say, an Audio Clip will never play back louder than it was recorded.

A further problem with the volume control is that it uses a "linear taper", which does not correspond with human hearing. For example, if volume is .5, instead of sounding half as loud as 1, it sounds about 2/3 as loud. If volume is .1, instead of sounding 1/10 as loud as 1, it sounds about 1/4 as loud.


Min Volume: The minimum value of the sound. No matter how far away you get, the sound will not get softer than this value.

Max Volume: How loud the sound gets at the loudest. No matter how close you get, the sound will never get louder than this value.

These variables actually have nothing to do with distance. Instead, they are just clamps for AudioSource.volume. The following odd behavior ensues, if Min Volume is greater than Max Volume:


Unity iPhone:

If Volume is less than Min Volume, then Volume behaves as if it were set to Min Volume. If Volume is greater than or equal to Min Volume, then Volume behaves as if it were set to Max Volume.


Unity 2.5:

If Volume is less than Min Volume, and Volume is less than 1, then Volume behaves as if it were set to Min Volume. If Volume is greater than or equal to Min Volume, or greater than or equal to 1, then Volume behaves as if it were set to Max Volume.


Like Volume, although Min Volume and Max Volume can accept values between 0 and infinity, values above 1 do the same thing as 1.


Rolloff Factor: How fast the sound fades. The higher the value, the closer the Listener has to be before hearing the sound.

"How fast the sound fades" is a good description, but the "before hearing the sound" part is inaccurate. A better way to put what Unity is going for is: "The sound will be as loud as if were 'Rolloff Factor' times as far away as it actually is, from the Audio Listener". However, that is not really what happens. The algorithm gets a little wacky, in an effort to balance this idea with the intention of a sound being as loud as AudioSource.volume at a distance of 1 world unit.

Sound has a friendly property: its measured amplitude decreases directly with distance from its source. (Take a measurement of amplitude, move n times farther away from the sound source than you already are, and a new measurement will yield 1/n times the original amplitude. This isn't true indoors, due to reflection and absorption of sound waves, but this ideal case is pretty accurate in open fields. Getting realistic indoor sound is not really within the capabilities of Unity's current audio engine.) So, using the realistic (outdoor) model, we could use the equation 'amplitude = volume/(rolloffFactor * distance)', and accurately have the sound play back as if it were 'Rolloff Factor' times as far away than it is. The problem with this model, is that instead of the sound's amplitude being 'volume * loudness of clip' at a distance of 1 world unit, it is 'volume * loudness of clip' at a distance of '1/rolloffFactor' world units. This codependence of properties is not particularly desirable, but unavoidable if adhering to this model.

In an effort to combine these two competing ideals, Unity uses the distance model of amplitude = volume / (1.0 + rolloffFactor * (distance – 1.0)). For mono audio clips, if rolloffFactor = 1, then the equation is physically accurate, and if rolloffFactor = 0, then there is some degree of positioning, despite the overall sound level not varying if distance from the Listener is altered. (See the description of what happens along the Listener's X axis, under 'Volume', above.) However, no other rolloffFactor values make sense, outside of a mathematical context.

For example, if rolloffFactor = 2, at a distance of two world units, you would expect the sound to be as loud as it would be at 4 world units, if rolloffFactor were 1. However, instead of the amplitude being multiplied by 1/4, as desired, the multiplicative factor would be 1/3. Tweaking rolloffFactor will probably be less intuitive, to anyone, than tweaking a good volume, minVolume, or maxVolume control, so I don't think it's as important to "fix" this property, as it is to fix the others, but I do believe that it can be improved.

Details of my audio control improvement methods

Volume: Although Unity's volume control is a precise multiplier of audio amplitude, the amplitude of a waveform does not correlate directly with human perception of loudness. Loudness perception is complex, but "experimentally it was found that a 10 dB increase in sound level corresponds approximately to a perceived doubling of loudness.". Therefore, it makes sense for a volume control to yield an amplitude that is...


exactly what is input, for a value of 1,

10 decibels below the input, for a value of .5,

20 decibels below the input, for a value of .25,

etc.


Amplitudinal difference, as a multiplicative factor, based on a difference in decibels, can be found with the expression 10^(dB / 20). Inputting the value of 10 dB results in 10^(10/20), which Mathf.Sqrt(10) in the code represents.


Raising this value to a base 2 logarithm gives exactly the behavior we want for a volume control. This expression would be (10^(1/2))^log[2]volume. However, the algorithm will execute faster in the form that I gave in the code, which is volume^log[2](10^(1/2)) (the RefreshLoudness function), due to only having to calculate a logarithm once, at runtime. Thanks to David Huntrods (dawvee on the Unity forums), for proof that a^log(b) = b^log(a):

b = a^log[a](b).

Therefore, a^log(b) = a^log(a^log[a](b))

The exponent inside the log can be brought outside to give us:

a^loga(b)*log10(a)

And since x^a*b = (x^a)^b, we can do this:

(a^loga(b))^log10(a)

and finally, substitute the part in parentheses with the identity of b again to give us this:

b^log10(a)

and we have equality!

  • AudioSource.volume only responds to values of 1 or less. There is the potential to easily induce digital distortion if non-clipped waveforms can be amplified, so apparently the choice was made to disallow this. I think this may have been done before it easily allows for 3D positional audio not to distort within a distance of less than one world meter, using the current distance model of volume = 1.0 / (1.0 + rolloff * (distance – 1.0)). Boosting the level of audio clips can only be done outside Unity at the moment. My conversion script will continue to function properly, above values of 1, if the restriction is ever dropped.
  • Values of less than zero, for either this loudness script, or audio.volume, both equate to multiplying the audio amplitude by zero. It is possible, that if Unity ever allows us the ability to invert the phase of a signal, that negative values could be useful, but this script would not account for that as is. I will update it if necessary, but hopefully, by the time Unity allows for phase inversion, this script will be obsolete.  ;-)
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox