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:

Hi Giovanni

Just implemented this properly and it works beautifully. I have over 200 sound sources in my scene and they’re culling perfectly well. I had issues with no sound or loud corrupted audio when many audio sources were enabled at once without culling.

Below is the implementation for my specific use case for those interested

- CsoundUnity script -

Added a public bool isCulled to the CsoundUnity class inside the CsoundUnity script:

 public class CsoundUnity : MonoBehaviour
 {
     public bool isCulled;

Added a public method SetCull(bool cullState) to CsoundUnity class

 public void SetCull(bool cullState)
     {
         isCulled = cullState;
     }
 }

Added isCulled as a condition in ProcessBlock:

private void ProcessBlock(float[] samples, int numChannels)
{
    if (compiledOk && initialized && !_quitting && !isCulled)
    {
        for (int i = 0; i < samples.Length; i += numChannels, ksmpsIndex++)
        {
            for (uint channel = 0; channel < numChannels; channel++)
            {
                // necessary to avoid calling csound functions when quitting while reading this block of samples
                // always remember OnAudioFilterRead runs on a different thread
                if (_quitting) return;

                if (isCulled) return;

- Audio Culling Manager script -

using UnityEngine;
using System.Collections.Generic;
using System.Collections;

public class AudioCullingManager : MonoBehaviour
{

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

[SerializeField] private float checkInterval = 5f;
[SerializeField] private float minDistance = 1f;
[SerializeField] private float maxDistance = 350f;

[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 Dictionary<CsoundUnity, bool> csoundUnityStates = new Dictionary<CsoundUnity, bool>();
private Dictionary<CsoundUnity, AudioSource> csoundAudioPairs = new Dictionary<CsoundUnity, AudioSource>();

public MapObjGen mapObjGen;
public CabbageAudioManager cabbageAudioManager;
public Player player;

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

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

public void AddCullObject(GameObject audioEmitterObj)
{
    if (audioEmitterObj != null && !audioEmitterObjs.Contains(audioEmitterObj))
    {
        audioEmitterObjs.Add(audioEmitterObj);
    }
}

public void RemoveCullObject(GameObject audioEmitterObj)
{
    if (audioEmitterObj != null && audioEmitterObjs.Contains(audioEmitterObj))
    {
        audioEmitterObjs.Remove(audioEmitterObj);
    }
}

public bool InRange(GameObject audioEmitterObj)
{
    return activeAudioEmitters.Contains(audioEmitterObj);
}

private void CullAudioSources()
{
    List<GameObject> newActiveEmitters = new List<GameObject>();
    List<GameObject> newInactiveEmitters = new List<GameObject>();

    for (int i = audioEmitterObjs.Count - 1; i >= 0; i--)
    {
        GameObject audioEmitterObj = audioEmitterObjs[i];

        if (audioEmitterObj == null)
        {
            audioEmitterObjs.RemoveAt(i);
            continue;
        }

        float distance = Vector3.Distance(audioEmitterObj.transform.position, player.transform.position);

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

        bool shouldEnableEmitter = false;

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

            if (csoundUnity == null || audioSource == null)
            {
                continue;
            }

            if (audioSource.minDistance != minDistance || audioSource.maxDistance != maxDistance)
            {
                audioSource.minDistance = minDistance;
                audioSource.maxDistance = maxDistance;
            }

            csoundAudioPairs[csoundUnity] = audioSource;

            // Transform the distance to logarithmic scale
            float logDistance = Mathf.Log10(distance + 1);
            float logMaxDistance = Mathf.Log10(audioSource.maxDistance + 1);

            bool shouldEnable = logDistance <= logMaxDistance;

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

            if (shouldEnable)
            {
                shouldEnableEmitter = true;
            }
        }

        if (shouldEnableEmitter)
        {
            newActiveEmitters.Add(audioEmitterObj);
        }
        else
        {
            newInactiveEmitters.Add(audioEmitterObj);
        }
    }

    activeAudioEmitters = newActiveEmitters;
    inactiveAudioEmitters = newInactiveEmitters;
}

// Draw Gizmos in the Scene view to represent active and inactive audio emitters
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);
    }
  }
}

’SetCull’ is called in the method CullAudioSources in AudioCullingManager, where it culls objects that are over a defined distance threshold, it overwrites any existing serialized min/max values to keep it uniform and predictable for all sources, these are defined with the minDistance and maxDistance and is set to 1-350 default in my case.

It’s also using the logarithmic distance so the attenuation more accurately reflects the distance curve in the Audio component, there’s probably a way to optimise this so we’re not calculating distance twice, if Unity is already calculating it elsewhere in the Audio Component for their own culling system. Haven’t investigated that yet.

There is still strain on the system given enough audio sources / activity, particularly when in a dense area where many are active. Results in loud/corrupted audio when pushed, so i’ve found a threshold to work down from - i’ll experiment more with this, perhaps implementing a voice stealing system in AudioCullingManager

My conclusion from this however is that Unity’s audio culling system doesn’t seamlessly integrate with CsoundUnity, so custom solutions must be built, is that fair to say? Personally I enjoy this fact as it allows for flexibility

I’ll keep working on this, thank you for the direction!!

1 Like

This is great. I’ll think about adding something similar to the package itself. Thanks for your contribution.
Yes it’s fair to say that there is no integration with the Unity audio culling system, but is there really one?
I did some googling and found this:
https://docs.unity3d.com/ScriptReference/AudioSource-isVirtual.html
We could use this info to stop processing completely until it is audible again, but I am not sure if then if that value will stay at 0 if nothing is filling the AudioSource samples buffer in OnAudioFilterRead.
I’ll do some tests with the 3D Audio Settings and the rolloff curve to try and understand how culling works.

1 Like

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!

This is a super helpful investigation, thanks for this!
I think it’s important to have such a feature in CsoundUnity (maybe for version 4), so I’ll review and test your code when possible. I’m now working on a paid project so I don’t have much time, but I’ll have a look in the early days of August for sure.
About click and pops: yes it can be caused by the editor processing. Try to keep an eye on the DSP load in the stats, or use the profiler (maybe as a standalone so it doesn’t block the editor too much).
Debugging audio performance is not that easy because analysing brings performance down a lot, so you’ll have to be very smart ;D
I think the clicks can come from this line in the ProcessBlock:

if (isCulled) return;

I think it’s important that you set the samples to 0 at least once (and you have to do it in ProcessBlock because it’s running on the audio thread), like this:

samples[i + channel] = 0.0f;

Let me know if this helps

1 Like

Greetings Giovanni

Very happy to help! Just implemented your idea and it is much smoother now. Definitely improved the clicks and pops. I will put any other clicks and pops that occur down to CPU strain, as i’m noticing them to occur most when something else CPU heavy is happening simultaneously to triggering sounds, such as a dense particle system with collision on each particle, all this combined with the editor too.

Here are the changes I made inside ProcessBlock() in the CsoundUnity script:

if (_quitting) return;

if (mute == true || isCulled)
   samples[i + channel] = 0.0f;
else 
// rest of the code...

I also removed a !isCulled bool from the first if statement:

// from this:
if (compiledOk && initialized && !_quitting && !isCulled)

// to this:
if (compiledOk && initialized && !_quitting)

Full context :

private void ProcessBlock(float[] samples, int numChannels)
{
    if (compiledOk && initialized && !_quitting)
    {
        for (int i = 0; i < samples.Length; i += numChannels, ksmpsIndex++)
        {
            for (uint channel = 0; channel < numChannels; channel++)
            {
                // necessary to avoid calling csound functions when quitting while reading this block of samples
                // always remember OnAudioFilterRead runs on a different thread
                if (_quitting) return;

                if (mute == true || isCulled == true)
                    samples[i + channel] = 0.0f;
                else
                {
                    if ((ksmpsIndex >= GetKsmps()) && (GetKsmps() > 0))
                    {
                        var res = PerformKsmps();
                        performanceFinished = res == 1;
                        ksmpsIndex = 0;

                        foreach (var chanName in availableAudioChannels)
                        {
                            if (!namedAudioChannelTempBufferDict.ContainsKey(chanName)) continue;
                            namedAudioChannelTempBufferDict[chanName] = GetAudioChannel(chanName);
                        }
                    }

                    if (processClipAudio)
                    {
                        SetInputSample((int)ksmpsIndex, (int)channel, samples[i + channel] * (float)csound.Get0dbfs());
                    }

                    //if csound nChnls are more than the current channel, set the last csound channel available on the sample (assumes GetNchnls above 0)
                    var outputSampleChannel = channel < GetNchnls() ? channel : GetNchnls() - 1;
                    var output = (float)GetOutputSample((int)ksmpsIndex, (int)outputSampleChannel) / (float)csound.Get0dbfs();
                    // multiply Csound output by the sample value to maintain spatialization set by Unity. 
                    // don't multiply if reading from a clip: this should maintain the spatialization of the clip anyway
                    samples[i + channel] = processClipAudio ? output : samples[i + channel] * output;

                    if (loudVolumeWarning && (samples[i + channel] > loudWarningThreshold))
                    {
                        samples[i + channel] = 0.0f;
                        Debug.LogWarning("Volume is too high! Clearing output");
                    }
                }
            }

            // update the audioChannels just when this instance is not muted
            if (!mute)
                foreach (var chanName in availableAudioChannels)
                {
                    if (!namedAudioChannelDataDict.ContainsKey(chanName) || !namedAudioChannelTempBufferDict.ContainsKey(chanName)) continue;
                    namedAudioChannelDataDict[chanName][i / numChannels] = namedAudioChannelTempBufferDict[chanName][ksmpsIndex];
                }
        }
    }
}

Thanks again for your help, it’s a pretty solid culling system now thanks to these changes. I’ll carry on

1 Like

Looking great!
Feel free to submit a pull request when you think it’s ready. We can work on that together after it’s submitted.
If possible do it on this branch: feature/user-control-on-ksmps as it is the most recent branch (almost ready for 3.5.0). Or you can still work on the master branch and I’ll merge later.
Or if you’re super brave you can have a look at the release_4_0_0 branch, but I don’t remember its current state.
It has namespaces changes so you could have to add some little things to your existing scripts, like:

using Csound.Unity;

Thanks Giovanni that sounds great, very happy to do that

I couldn’t find the branch feature/user-control-on-ksmps?
I assumed it would be here somewhere: https://github.com/csound/csound/branches/all

Am I missing something?

feature/user-control-on-ksmps from the https://github.com/rorywalsh/CsoundUnity repo

EDIT: I guess this wiki can be helpful https://github.com/rorywalsh/CsoundUnity/wiki/How-to-contribute

1 Like

Gotcha, that makes much more sense now :sweat_smile:

That wiki is very useful indeed thank you