Sound design is a vital part of a game. Other than using audio files such as .mp3 and .wav files as audio sources, we can also generate audio at runtime and play it. Generating audio in runtime adds more dynamic sound to the game by playing different sounds based on the state of the game.
To keep this tutorial simple, we will generate white noise and pure sine tone at runtime using code.
Audio Fundamentals
To generate audio, first, we need to know how sound is produced physically. Sound is the result of the wave produced by the vibration of air. A speaker consists of a diaphragm that vibrates to produce sound.
Different types of sounds are produced by a vibrating diaphragm with varying frequencies with different strengths. So, if the vibration of the audio source can be controlled, the generation of sound can be controlled.
Sample Rates
In digital systems, everything is discrete. Although the vibration of a speaker is perceived as continuous movement, it is actually divided into much smaller discrete subdivisions.
These smaller subdivisions are known as a sample, and the total divisions in a second are known as the sample rate. Humans cannot hear sound frequencies greater than 20,000 Hz, so to sample audio, we need a minimum of 40,000 samples a second to play all possible ranges of sound.
Typical audio systems have a sample rate of 44.1 kHz or 48 kHz. The sample rate determines the data that should be passed per second. The strength of vibration is passed for each sample.
Channels
Also, we need to consider channels while generating audio. When a sound is played, more than one sample of data may be played simultaneously for each audio source.
For example, a stereo system has a dual-channel for left and right speakers. This means there are separate audio data for the left and right speakers. For a 5.1 surround sound system, there are 6 channels. So we must be careful about the number of channels we are dealing with before generating audio.
Controlling Audio in Unity
To control audio in Unity, there is an event function called OnAudioFilterRead
. This is available for MonoBehaviour
classes. It consists of a float array, and an integer passed to it as a parameter. The parameter float array consists of data for each sample for each channel.
Let's call this array data
. The int
parameter consists of the number of channels. Let's call this channels
.
The data order in data
is as follows,
Let,
C
is Channel and is indexed as C0,C1,C2,...
for different channels,
S
is Sample and is indexed as S0,S1,S2,...
for sample at different sample index. Then,data
is in the order of [S0C0,S0C1,S0C2,...S0Cn, S1C0,S1C1,....S1Cn, SnC0,....,SnCn, ...]
Generally, the size of data
is 1024 audio samples. It means that for 2 channel audio data, it will contain 2048 floats inside it. To manipulate audio, we need to change the values of those samples.
One thing to note is that this method runs on a separate thread from the main thread, so calling Unity functions from inside of the function is not allowed.
Generating White Noise
White noise is static noise screen noise. This is similar to the static noise we hear on the radio or television. To generate white noise, we just need to set a random value to each sample in data
array.
Attaching this script to any GameObject
in a scene produces white noise. Just be sure that scene contains an audioListener
.
Generating Sine Tone
Sine tone is the purest form of sound consisting of only one frequency. It is generated by vibrating in simple harmonic motion with a certain frequency
. Let's create a float variable frequency
to control the frequency of sound, time
to keep track of played samples, and sampleRate
to store the sample rate of the playback system. We will use Mathf.Sin
to generate a sin wave. Let's create a method for it.
The expression 2*Mathf.PI*(index /(float) sampleRate) * frequency
can be broken down as
(index/(float) sampleRate)
will give the total seconds passed for given index
. index
is the total number of samples. It means the value of this expression will increase by 1 every 1 second.
Now, multiplying it with frequency
(index /(float) sampleRate) * frequency
scales up time so that value of this expression will be increased by frequency
every second.
Finally, multiplying by 2*Mathf.PI
will convert the expression to radian so that the correct value can be obtained when passed to the Sin
method.
This will return a value in the range of (-1,1).
Now, use OnAudioFilterRead
method to fill data from the sine wave for each sample. We are not provided with an index of sample in time-space, so we need to manually track it using the time
variable. I am setting sampleRate
as 48000 in my project. It can differ from your project.
Also, since time is incremented around 40k times per second, its value can get too large and can result in float overflow. So, to control it, we can reset it every 5 seconds.
Attaching it to a GameObject
will play the sine tone of a given frequency
. Since audio is being generated in runtime, we can change the frequency
from the editor and hear the change in pitch.
Conclusion
Here we have it, a white noise and sine tone. This concept can be further expanded to make a more dynamic audio system in your game.
Some examples are playing engine sound based on the vehicle's speed, adding dynamic ear candy in the progression game according to the player's stage, and adding subtle variation in sound to avoid it being monotonous and boring. So, explore more and create interesting sounds.
Thanks for reading, more blogs comming soon.