Unity Dynamic Audio Environment

Link To Web Build Link to heading

Programs Used Link to heading

  • FMod Studio
  • Unity
  • Rider

Event Statement (185 words): Link to heading

*Slight Spoilers for 007 Firstlight Ahead.*

The Idea behind this was from 007 firstlight, where you explore “an evil corporate entity”, taking from this I thought what if this is a non-descript base of an evil entity so I thought it would be interesting to do like scifi stuff. I split it into 4 key areas, The outer Server farm area, the inner hall/console area, the garden, and the generator.

The server farm area is filled with these monolithic metallic pillars that represent servers. And there’s a large cooling fan in the back (which occludes across to the player).

The Inner Hall/console area is meant to represent those old school mainframes with all the beeps and boops and the control section of the facility.

The garden is like a small break room I suppose so it’s really quiet, as I’m assuming this is some sort of stealth mission.

The generator is the big thing in the middle of the atrium, and it winds up as the player gets closer to it.

The overall theme is meant to be kind of retro and scifi.

Recording Notes: Link to heading


As shown in the picture above, the location and type of sound are recorded in the filename. As before.

*[ACRONYM]*LOCATION_SoundEffect.WAV (Time in metadata)

Sound Capture Methodology: Link to heading


As shown in the image above, this is how I generally get sound from different sources. I would focus on them with the mic (whilst listening through earphones to isolate sounds on the fly, making it much easier to pick up issues), then record for a set amount of time.

As spatialisation was now being handled virtually I decided to switch the microphone to record L+R mono, this effectively combined the left and right signals and gave me a mono output which removed the stereoscopic aspect of the audio. (to add back later)

Footstep Audios: Link to heading


By holding the mic the way I did I was able to adequately focus it on the specific sounds of steps I took. Again monitoring this with my earphones was very important so that I can personally hear the sound. I also recorded these very late at night so that I could minimise extraneous noise.

Technical Discussions: Link to heading

Stem Processing in Reaper: Link to heading

  • Wildcards
    • I used wildcards to dynamically name stems when exporting from Reaper.
    • I used the submixing functionality to achieve this.
  • Region Management
    • I used the region management tool to allocate regions to the render matrix for export.
    • I also found a macro ie Action that let you set regions for selected tracks which I bound to alt+shit+R
  • Spectrum analysis
    • I used spectrum analysis to isolate sounds
  • Band Pass
    • Using the ReaEQ FX I did a band pass to directly isolate my desired sounds.

Helm Synths: Link to heading

  • For helm I mostly do trial and error and save sounds I created that I liked, this process usually starts with one of the CC-BY synths and I heavily modify them to make them my own.

FMOD: Link to heading

  • AHDSR
    • Used to make the sounds dynamically phase in and out.
  • Groups
    • Control Snapshots of reverb
    • sidechain ambience to important sounds with a compressor.
  • Silent Instruments
    • Random Pauses in ambient stings.
    • Found on FMOD forums.

Auditory Occlusion: Link to heading

There is an object in the scene that implements auditory occlusion and that is the object that represents the fan in the back of the server room / initial room.

The idea behind this is that when the player is walking by the objects behind or around the monoliths are able to occlude the source audio of the fan in the back such that it would implement a shifting low pass when there is more than 1 object between itself and the player.

To apply this I created a custom script that I will explain below.

Variables: Link to heading

    private  GameObject player;
    private Vector3 playerPosition;
    
    
    [SerializeField] 
    private int occlusionValue;

    public bool debug;
    public bool bIsParamDiscrete = true;
    public StudioEventEmitter soundEmitter;
    public string occlusionParameterName = "Occlusion";
    public int maxOcclusionAmount;
    public int distanceCutoff = 50;

Above are variables that the script takes, below is a description:

Variable Description
player A reference to the player object
Player position A reference to the player position
occlusionValue An integer that tracks the layers of occlusion
debug A boolean value that determines if the object is showing its debug print in the console.
bIsParamDiscrete Tells the script whether the Fmod parameter is continuous or discrete.
soundEmitter A reference to the sound emitter that is being altered
occlusionParameterName String name of the parameter that represents occlusion in FMOD event
maxOcclusionAmount How many objects to count until we reach a max amount. Represents the max of the FMOD parameter.
distanceCutoff Distance at which the object gives up occluding for the player. Should be set to the spatialised extent of the FMod event. (I did 25+5 for consistency)

On Creation: Link to heading

// Start is called before the first frame update
void Start()
{
   player =  GameObject.FindGameObjectWithTag("MainCamera");

   if (maxOcclusionAmount <= 0)
   {
       maxOcclusionAmount = 3;
   }
   
   if (distanceCutoff <= 1)
   {
       distanceCutoff = 10;
   }

   occlusionValue = 0;
}

When the object is created, it finds and stores the main camera that is part of the FirstPersonCharacter prefab provided in the project, this is stored in the “player” variable.

The if statements ensure that “maxOcclusionAmount” and “distanceCutOff” are set at a reasonable level.
The last statement sets the occlusion value to 0 in case it was altered in some way.

On Update (Every frame): Link to heading

The update function is what the object does every frame of the game. I will explain what it does step by step.

  1. It checks whether the player actually exists [ie no win condition or other memory issues]
// Step 1: player validity
if (player.Equals(null))
{
  return;
}
  1. Gets and sets the distance and direction to the player [ variables named distance and direction]
// Step 2: player references
Vector3 direction = player.transform.position - transform.position;

float distance = direction.magnitude;
direction = direction.normalized;
  1. if the distance to the player is larger than “distanceCutOff”: Doesn’t do anything and sets the occlusion to 0.
    1. Assumes that FMOD is not even playing the sound.
// Step 3: cutoff
if (distance >= distanceCutoff)
{
    occlusionValue = 0;
    return;
}
  1. Resets the occlusion level and debug string.
    1. The variable “collided” stores the name of every object that is collided see Step 8*.*
// Step 4: reset occlusion 
occlusionValue = 0;
var collided = "";
  1. Sets the rotation of the object to the rotation towards the player.
// Step 5: visualise the rotation
transform.rotation = Quaternion.LookRotation(direction);
  1. Basically beams a laser (raycast) that checks how many objects are between the player and the object
// Step 6: ray cast
// ReSharper disable once Unity.PreferNonAllocApi
var hits = Physics.RaycastAll(transform.position, direction, distance); 
Debug.DrawRay(transform.position, direction * distance, Color.red);
  1. Removes “ground” and “player” from final occlusion value.
// Step 7: parse hits
foreach (var hit in hits)
{
    collided += hit.collider.gameObject.name + ", ";
    
    if (hit.collider.CompareTag("Player"))
    {
        occlusionValue -= 1;
    }
    if (hit.collider.gameObject.name.Equals("ground"))
    {
        occlusionValue -= 1;
    }
}
  1. Updates the level of occlusion and makes sure its within the range of [0, maxOcclusionAmount]
    1. Debug is used to show the occlusion value and what it collided with
// Step 8: update occlusion value
occlusionValue = hits.Length + occlusionValue;
occlusionValue = Math.Clamp(occlusionValue,0,maxOcclusionAmount);

// Debug to check the collisions.
if (debug)
{
    print(collided + occlusionValue); 
}
  1. Checks that the Sound Emitter actually exists and the name of the FMOD param is given. Does nothing otherwise.
// step 9: update occlusion value of emitter
if (soundEmitter.Equals(null) || occlusionParameterName.Equals(null))
{
    return;
}
  1. Uses FMOD Plugin to get the current value of the FMOD Parameter
// Step 10: gets the current value of the parameter
soundEmitter.EventInstance.getParameterByName(occlusionParameterName,out var currentValue);

if (Math.Abs(occlusionValue - Math.Round(currentValue)) == 0)
{
    return;
}
  1. If the parameter is discrete, it sets the value directly, then stops all executions for that frame.
// Step 11: Update FMOD Discretely
if (bIsParamDiscrete)
{
    soundEmitter.EventInstance.setParameterByName(occlusionParameterName, occlusionValue);
    return;
}
  1. Process the parameter as a continuous one.
    1. First we generate the interpolated occlusion value by using the lerp function.
    2. We check if its 1000th’s away from the original value ( this was the most reliable in my testing)
    3. If it’s within the tolerance we snap to the real value, otherwise we keep interpolating it.
    4. A debug section below prints all 3 values
// Step 12: interpret value Continuously.
var newOcclusionValue = Mathf.Lerp(currentValue, occlusionValue, Time.deltaTime * 0.5f);

if (Math.Abs(newOcclusionValue - currentValue) <= 0.001)
{
    soundEmitter.EventInstance.setParameterByName(occlusionParameterName, occlusionValue);
}
else
{
    soundEmitter.EventInstance.setParameterByName(occlusionParameterName,newOcclusionValue);
}

if (!debug)
{
    return;
}

float occlusionInFmod;
soundEmitter.EventInstance.getParameterByName(occlusionParameterName, out occlusionInFmod);
print("Target Oc: "+ occlusionValue +
      ", New Oc Val: " + newOcclusionValue + 
      ", FMOD Oc Val: " + occlusionInFmod );

Reflection: Link to heading

What Sounds Did I Record? Link to heading

For this assignment I largely captured the footstep sounds which can be seen in reapers. I used Helm to generate a majority of the sounds as I was going for a retro-scifi aesthetic. I also recorded some fan sounds to add to the servers and the console.

How was the captured audio used? Link to heading

Primarily for footsteps and the technology, the rest was mostly manipulated through FMOD’s parameters, groups and snapshots.

What am I most proud of in this scene? Link to heading

Most definitely the occlusion method that I created, I think it’s quite rudimentary but it does what it needs to do for the scope of the project. Moreover, it was fun to learn about the FMOD unity library and how it functioned under the hood. I gave the script CC BY-SA which means literally anybody can use it, because It was just so fun making it.

What would I improve? Link to heading

For some unknown reason the web version does not replicate the garden area as it does in the unity project itself.

If I had more time I would have looked at more of the non-digetic sounds and tried to create a little sound track if I could to really enthrall the player into the soundscape.