Cabbage Logo
Back to Cabbage Site

Unity: Dynamically enable/disable AudioSource+CsoundUnity components

Hello everyone,

Using Unity 2022.2.18

Currently implementing Csound in a 3D environment with many sound sources and various cabbage instruments. The CsoundUnity components and their respective AudioSource are contained in the spawned map object/npc prefabs.

In hopes of optimising performance, I’m testing a script called AudioCullingManager that manages all of the map objects in the game world, getting all of their relevant audio components and enabling/disabling them when crossing a distance threshold relative to the player.

AudioCullingManager script:

using UnityEngine;
using System.Collections.Generic;

public class AudioCullingManager : MonoBehaviour

{

public Player player; 

public float cullingDistance = 50f; // Distance threshold for audio culling

[SerializeField] private List<GameObject> audioEmitterObjs = new List<GameObject>(); // Pool of objects with audio components

public MapObjGen mapObjGen;

private void Awake()
{
    mapObjGen = GetComponentInParent<MapObjGen>();
}

public void InitAudioCulling()
{
    foreach (GameObject mapObj in mapObjGen.mapObjectList)
    {
        if (mapObj.GetComponentInChildren<AudioSource>() != null || mapObj.GetComponentInChildren<CsoundUnity>() != null)
        {
            audioEmitterObjs.Add(mapObj);
        }
    }
}

void Update()
{
    if (player != null)
    {
        foreach (GameObject audioEmitterObj in audioEmitterObjs)
        {
            float distance = Vector3.Distance(audioEmitterObj.transform.position, player.transform.position);

            // Get components on each audioEmitterObj
            var audioSources = audioEmitterObj.GetComponentsInChildren<AudioSource>(true);
            var csoundUnityComponents = audioEmitterObj.GetComponentsInChildren<CsoundUnity>(true);

            bool shouldEnable = distance <= cullingDistance;

            // Enable/disable CsoundUnity components
            foreach (var csoundUnity in csoundUnityComponents)
            {
                csoundUnity.enabled = shouldEnable;
            }

            // Enable/disable AudioSource components
            foreach (var audioSource in audioSources)
            {
                audioSource.enabled = shouldEnable;
            }
        }
    }
}

This script does indeed do what is described, but I fear that due to disabling/enabling them, the sound is lost. Is it possible to dynamically turn these components on and off? Or would I need to reinitialise them somehow?

I also make sure that i’m checking the cSoundUnity component is not null when making any parameter changes (that are called even if the sound is off, currently). Heres an example:

CabbageAudioManager script (Manages Cabbage instrument parameters)

public void SetParameter(CsoundUnity cSound, string parameterName, float value)
{
    if (cSound != null && cSound.GetChannel(parameterName) != value)
    {
        cSound.SetChannel(parameterName, value);
    }
    //Debug.Log($"Parameter {parameterName} set to: {value}");
}

I am aware Unity has its own audio culling system in Project Settings > Audio, which provides configurable Real and Virtual voices. I do not believe the audio culling provided by Unity is making much difference, as it appears all voices are active and none are culled when monitoring the Audio Profiler in Game Mode, despite the emitter being far enough away to be culled.

I’ve exhausted my debugging of that issue for now. I saw a big performance boost when dynamically enabling/disabling Audio and Csound components with the AudioCullingManager script. Though i’m yet to find out if the performance is still much better after fixing the no sound issue with the AudioCulling script.

That’s all for now, thanks!!

Quick answer as I’m afk:
I’d simply turn off the running instruments using SendScoreEvent, and keep the Csound instances running. It won’t consume cpu
(you will still have the overhead of writing/reading into/from the AudioSource using OnAudioFilterRead) but they will stay alive.

See https://github.com/rorywalsh/CsoundUnity/blob/master/Documentation~/controlling_csound_from_unity.md#starting--stopping-instruments

But I understand the need of a culling system / a way to turn the computation (it happens inside the CsoundUnity.ProcessBlock function) off completely.
It will be implemented in version 4. I’m very busy atm and I have no time to work on this for now.
Any help is appreciated!

@giovannibedetti Thanks for the reply!!

Very much appreciate your response. I took a look at those resources.
So this is what I ended up doing:

I expanded the AudioCullingManager script to more accurately manage the state of the culling.
Theres certainly room for improvement.

It gets the maxDistance of the AudioSource associated with the playing cSoundUnity component and multiplies it by 3, this gives us the distance threshold for turning the instrument on/off when it’s out of hearing range. The multiplier is indeed a magic number. I plan to refine this value to more accurately reflect the distance threshold, but it works to cull when out of range for now.

AudioCullingManager script:

public class AudioCullingManager : MonoBehaviour 
{

public bool showGizmos = true; // Show active/inactive audio emitters.

public Player player;

[SerializeField] private float checkInterval = 1f;

[SerializeField] private List<GameObject> audioEmitterObjs = new List<GameObject>();
[SerializeField] private List<GameObject> activeAudioEmitters = new List<GameObject>();
[SerializeField] private List<GameObject> inactiveAudioEmitters = new List<GameObject>();

public MapObjGen mapObjGen;
public CabbageAudioManager cabbageAudioManager;

private Dictionary<CsoundUnity, bool> csoundUnityStates = new Dictionary<CsoundUnity, bool>();
private Dictionary<CsoundUnity, AudioSource> csoundAudioPairs = new Dictionary<CsoundUnity, AudioSource>();

private void Awake()
{
    mapObjGen = GetComponentInParent<MapObjGen>();
    cabbageAudioManager = GetComponent<CabbageAudioManager>();
}

public void InitAudioCulling()
{
    foreach (GameObject mapObj in mapObjGen.mapObjectList)
    {
        if (mapObj.GetComponentInChildren<AudioSource>() != null || mapObj.GetComponentInChildren<CsoundUnity>() != null)
        {
            audioEmitterObjs.Add(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);
    }
}

private void CullAudioSources()
{
    activeAudioEmitters.Clear();
    inactiveAudioEmitters.Clear();

    foreach (GameObject audioEmitterObj in audioEmitterObjs)
    {
        float distance = Vector3.Distance(audioEmitterObj.transform.position, player.transform.position);

        CsoundUnity[] csoundUnityComponents = audioEmitterObj.GetComponentsInChildren<CsoundUnity>();
        AudioSource[] audioSources = audioEmitterObj.GetComponentsInChildren<AudioSource>();

        for (int i = 0; i < csoundUnityComponents.Length; i++)
        {
            CsoundUnity csoundUnity = csoundUnityComponents[i];
            AudioSource audioSource = audioSources.Length > i ? audioSources[i] : null;

            if (audioSource != null)
            {
                csoundAudioPairs[csoundUnity] = audioSource;

                bool shouldEnable = distance <= audioSource.maxDistance * 3;

                if (!csoundUnityStates.TryGetValue(csoundUnity, out bool currentState) || currentState != shouldEnable)
                {
                    if (shouldEnable)
                    {
                        cabbageAudioManager.InstrumentON(csoundUnity);
                    }
                    else
                    {
                        cabbageAudioManager.InstrumentOFF(csoundUnity);
                    }
                    csoundUnityStates[csoundUnity] = shouldEnable;
                }

                if (shouldEnable)
                {
                    if (!activeAudioEmitters.Contains(audioEmitterObj))
                    {
                        activeAudioEmitters.Add(audioEmitterObj);
                    }
                }
                else
                {
                    if (!inactiveAudioEmitters.Contains(audioEmitterObj))
                    {
                        inactiveAudioEmitters.Add(audioEmitterObj);
                    }
                }
            }
        }
    }
}

// Draw Gizmos in the Scene view to represent active and inactive audio emitters
void OnDrawGizmos()
{
    if (!showGizmos) return;

    foreach (GameObject emitter in activeAudioEmitters)
    {
        Gizmos.color = new Color(0, 1, 0, 0.5f);
        Gizmos.DrawSphere(emitter.transform.position, 15f); 
    }

    foreach (GameObject emitter in inactiveAudioEmitters)
    {
        Gizmos.color = new Color(1, 0, 0, 0.5f);
        Gizmos.DrawSphere(emitter.transform.position, 15f); 
    }
  }
}

CabbageAudioManager script:

public void InstrumentOFF(CsoundUnity cSound)
{
    if (cSound == null)
    {
        Debug.LogError("CsoundUnity reference is null. Exiting InstrumentOFF method.");
        return;
    }

    string turnOffCommand = "i-1 0 -1";
    cSound.SendScoreEvent(turnOffCommand);
}

public void InstrumentON(CsoundUnity cSound)
{
    if (cSound == null)
    {
        Debug.LogError("CsoundUnity reference is null. Exiting InstrumentON method.");
        return;
    }

    string turnOnCommand = "i1 0 -1";
    cSound.SendScoreEvent(turnOnCommand);
}

This only turns off instrument 1, for now. This works fine for my case atm as most of my instruments run on instrument 1. Some of my other instruments do use specific instrument names though.

It would be cool if I could somehow access an instrument list to turn off all instruments on a CsoundUnity component at once. One idea I had as a hacky workaround to not having this, is that I could create a script that gets the Cabbage instrument names on Awake, by creating/parsing through a txt version of the Cabbage file and looking for the text that follows all instances of ‘instr’ in the file. Store them, somehow pair those to the relevant CsoundUnity instances, and then calling start or stop on all of those instrument names we collected in a for loop we could:
foreach string ‘instrument’ in ‘instrumentNames’ on this cSound component —>

string instrumentName = instrument; 
string turnOffCommand = $"i-{instrumentName} 0 -1"; 
cSound.SendScoreEvent(turnOffCommand);

Haven’t tried this or fleshed this idea out yet. Just some thoughts.

Thanks for the help!!

This is great. I am sorry that for a little change you have to write so much code.

The cleanest way to implement this would be adding a bool like the ‘mute’ bool, called say ‘cull’.
Then if it is false discard computation completely adding it in this line:

I will try to find some time to implement this later today, but no guarantee :wink:

EDIT: Yes of course we will still need to stop all the running instruments, and restore them when it is not culled again. Your solution works great but I’ll think about a more generic solution :thinking: