Hello all,
Expanding more on this topic:
Priority Settings
I’ve found managing the ‘Priority’ settings of the audio source is crucial. Without setting these appropriately, sounds will fail to play at all or important sounds will be stolen by sounds that are outside of audible distance.
Priority is a range from 0-255 where 0 is highest priority, and 255 is lowest priority.
Controlling this priority is most important when managing large numbers of sound sources.
Setting this in the prefabs alone didn’t really yield great results for me, so now i’m using distance from the listener to dynamically set the priority. It made a big difference in performance and audio stability in my case.
Dynamic Solution:
In the CullAudioSources method, the Priority of each objects Audio Source component is dynamically calculated based on the objects distance from the Player.
The distance of the audio source to the player is mapped onto the Priority level and is broken up into integer steps of 10, clamping the range from 10 - 255. Where the closest objects are 10 (High Priority) and furthest away are 255 and all divisions of 10 in-between.
0-10 is reserved for the most critical sounds that shouldn’t be stolen by sounds further away. For instance, the Player’s sounds are priority 0 (most important). This is set manually in the prefab.
Pops and Clicks
Still having some issues with pops and clicks. I don’t believe these clicks/pops are coming from some active instruments, as the instruments don’t produce these sounds in testing the Cabbage instrument. Instead I think it may be instruments being started/stopped abruptly as they enter/exit the audible range.
Edit: On further reflection, I actually haven’t tested a build for a while, and I know that the Unity Editor is extremely power hungry when testing in game-mode and this could be causing some audio glitches, i’ll see how it is when it’s built.
Volume Rolloff
I attempted to mitigate clicks and pops by implementing a quick volume lerp before we call SetCull(true/false). I think this helped reduce clicks and pops, but some still remain.
Here’s the updated AudioCullingManager script:
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
public class AudioCullingManager : MonoBehaviour
{
[Header("Distance Culling Settings")]
[SerializeField] private float checkInterval = 2f;
[SerializeField] private float minDistance = 1f;
[SerializeField] private float maxDistance = 350f;
[SerializeField] private float volumeLerpDuration = 1f;
[Header("Priority Settings")]
public int maxPriority = 255;
public int minPriority = 0;
public int priorityStepSize = 10;
[Header("Volume Settings")]
public float maxVolume = 0.5f;
public float minVolume = 0;
[Header("Audio Emitter Lists")]
[SerializeField] private List<GameObject> audioEmitterObjs = new List<GameObject>();
[SerializeField] private List<GameObject> activeAudioEmitters = new List<GameObject>();
[SerializeField] private List<GameObject> inactiveAudioEmitters = new List<GameObject>();
private List<GameObject> toRemove = new List<GameObject>();
private Dictionary<CsoundUnity, bool> csoundUnityStates = new Dictionary<CsoundUnity, bool>();
private Dictionary<CsoundUnity, AudioSource> csoundAudioPairs = new Dictionary<CsoundUnity, AudioSource>();
private Dictionary<GameObject, CsoundUnity[]> csoundComponentDict = new Dictionary<GameObject, CsoundUnity[]>();
private Dictionary<GameObject, AudioSource[]> audioSourcesDict = new Dictionary<GameObject, AudioSource[]>();
[Header("Gizmos")]
public bool showGizmos = true; // Show active/inactive audio emitters.
[Header("References")]
public CabbageAudioManager cabbageAudioManager;
public Player player;
private void Awake()
{
cabbageAudioManager = GetComponent<CabbageAudioManager>();
player = FindObjectOfType<Player>();
}
public void InitAudioCulling()
{
audioEmitterObjs.Clear();
activeAudioEmitters.Clear();
inactiveAudioEmitters.Clear();
toRemove.Clear();
foreach (GameObject mapObj in MapObjGen.WorldManagerInstance.mapObjectList)
{
if (mapObj != null && (mapObj.GetComponentInChildren<AudioSource>() != null || mapObj.GetComponentInChildren<CsoundUnity>() != null))
{
AddCullObject(mapObj);
}
}
if (player != null && cabbageAudioManager != null)
{
CullAudioSources();
}
StartCoroutine(CheckAudioEmitterDistance());
}
private IEnumerator CheckAudioEmitterDistance()
{
while (true)
{
if (player != null && cabbageAudioManager != null)
{
CullAudioSources();
}
yield return new WaitForSeconds(checkInterval);
}
}
public bool InRange(GameObject audioEmitterObj)
{
return activeAudioEmitters.Contains(audioEmitterObj);
}
private void CullAudioSources()
{
Vector3 playerPosition = player.transform.position;
activeAudioEmitters.Clear();
inactiveAudioEmitters.Clear();
List<(GameObject obj, float distance)> emitterDistances = new List<(GameObject, float)>();
foreach (var audioEmitterObj in audioEmitterObjs)
{
if (audioEmitterObj == null)
{
toRemove.Add(audioEmitterObj);
continue;
}
float distance = Vector3.Distance(audioEmitterObj.transform.position, playerPosition);
emitterDistances.Add((audioEmitterObj, distance));
}
var sortedEmitters = emitterDistances.OrderBy(e => e.distance).ToList();
for (int i = 0; i < sortedEmitters.Count; i++)
{
var (audioEmitterObj, distance) = sortedEmitters[i];
float logDistance = Mathf.Log10(distance + 1);
if (!csoundComponentDict.TryGetValue(audioEmitterObj, out var csoundUnityComponents) ||
!audioSourcesDict.TryGetValue(audioEmitterObj, out var audioSources))
{
continue;
}
bool shouldEnableEmitter = false;
int componentLength = Mathf.Min(csoundUnityComponents.Length, audioSources.Length);
for (int j = 0; j < componentLength; j++)
{
CsoundUnity csoundUnity = csoundUnityComponents[j];
AudioSource audioSource = audioSources[j];
if (audioSource.minDistance != minDistance || audioSource.maxDistance != maxDistance)
{
audioSource.minDistance = minDistance;
audioSource.maxDistance = maxDistance;
}
csoundAudioPairs[csoundUnity] = audioSource;
float logMaxDistance = Mathf.Log10(audioSource.maxDistance + 1);
bool shouldEnable = logDistance <= logMaxDistance;
if (!csoundUnityStates.TryGetValue(csoundUnity, out bool currentState) || currentState != shouldEnable)
{
float targetVolume;
if (shouldEnable)
{
targetVolume = maxVolume;
}
else
{
targetVolume = minVolume;
}
StartCoroutine(LerpVolume(audioSource, targetVolume, shouldEnable, csoundUnity));
csoundUnityStates[csoundUnity] = shouldEnable;
}
if (shouldEnable)
{
shouldEnableEmitter = true;
}
// Set priority
int numberOfSteps = (maxPriority - priorityStepSize) / priorityStepSize;
float stepSize = maxDistance / numberOfSteps;
int basePriority = ((int)(distance / stepSize) * priorityStepSize) + priorityStepSize;
int priorityStep = Mathf.Clamp(basePriority, priorityStepSize, maxPriority);
audioSource.priority = priorityStep;
}
if (shouldEnableEmitter)
{
if (!activeAudioEmitters.Contains(audioEmitterObj))
{
activeAudioEmitters.Add(audioEmitterObj);
}
}
else
{
if (!inactiveAudioEmitters.Contains(audioEmitterObj))
{
inactiveAudioEmitters.Add(audioEmitterObj);
}
}
}
foreach (var obj in toRemove)
{
RemoveCullObject(obj);
}
toRemove.Clear();
}
public void AddCullObject(GameObject audioEmitterObj)
{
if (audioEmitterObj != null && !audioEmitterObjs.Contains(audioEmitterObj))
{
audioEmitterObjs.Add(audioEmitterObj);
var csoundUnityComponents = audioEmitterObj.GetComponentsInChildren<CsoundUnity>();
var audioSources = audioEmitterObj.GetComponentsInChildren<AudioSource>();
if (csoundUnityComponents.Length > 0 || audioSources.Length > 0)
{
csoundComponentDict[audioEmitterObj] = csoundUnityComponents;
audioSourcesDict[audioEmitterObj] = audioSources;
}
}
}
public void RemoveCullObject(GameObject audioEmitterObj)
{
if (audioEmitterObj != null && audioEmitterObjs.Contains(audioEmitterObj))
{
audioEmitterObjs.Remove(audioEmitterObj);
csoundComponentDict.Remove(audioEmitterObj);
audioSourcesDict.Remove(audioEmitterObj);
}
}
private IEnumerator LerpVolume(AudioSource audioSource, float targetVolume, bool shouldEnable, CsoundUnity csoundUnity)
{
float startVolume = audioSource.volume;
float elapsedTime = 0f;
while (elapsedTime < volumeLerpDuration)
{
audioSource.volume = Mathf.Lerp(startVolume, targetVolume, elapsedTime / volumeLerpDuration);
elapsedTime += Time.deltaTime;
yield return null;
}
audioSource.volume = targetVolume;
csoundUnity.SetCull(!shouldEnable);
}
void OnDrawGizmos()
{
if (!showGizmos) return;
foreach (GameObject emitter in activeAudioEmitters)
{
if (emitter == null) continue;
Gizmos.color = new Color(0, 1, 0, 0.5f);
Gizmos.DrawSphere(emitter.transform.position, 15f);
}
foreach (GameObject emitter in inactiveAudioEmitters)
{
if (emitter == null) continue;
Gizmos.color = new Color(1, 0, 0, 0.5f);
Gizmos.DrawSphere(emitter.transform.position, 15f);
}
}
}
Thats all to report for now… i’ll carry on digging this topic
Thanks all!