ERR_STUDIO_NOT_LOADED - can't enable metering

I’m trying to enable metering, but I keep getting this error. Here’s my code.

        protected void Start() => StartCoroutine(EnableMeteringRoutine());
        private IEnumerator EnableMeteringRoutine()
        {
            yield return new WaitUntil(() => FMODUnity.RuntimeManager.IsInitialized);
            FMOD.Studio.Bus bus = FMODUnity.RuntimeManager.GetBus("bus:/Voice");
            FMOD.RESULT result = bus.lockChannelGroup(); // Forces bus to be created
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError($"Failed to create bus to enable metering {result}");
                yield break;
            }
            FMOD.ChannelGroup channelGroup;
            result = bus.getChannelGroup(out channelGroup);
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError($"Failed to fetch channel group to enable metering for voice {result}");
                bus.unlockChannelGroup();
                yield break;
            }
            FMOD.DSP dsp;
            if (channelGroup.getDSP(0, out dsp) != FMOD.RESULT.OK)
            {
                Debug.LogError($"Failed to fetch dsp to enable metering for voice {result}");
                bus.unlockChannelGroup();
                yield break;
            }
            dsp.setMeteringEnabled(false, true);
            bus.unlockChannelGroup();
        }

The code works if I wait 5 frames, but not before. It’s non-deterministic though. So it’d be nice to know when I’m allowed to do this.

I’ve also tried to do this to EventInstances instead of a bus and that doesn’t work either. Same error message.

My goal is to be able to fetch the output levels for a voice channel group so that the volume can control whether or not an avatar in our game plays the “speaking” animation. I’m having a lot of trouble working with the API.

While I’m at it, is this how I calculate decibels?

                    FMOD.DSP_METERING_INFO outputMetering;
                    mixerHead.getMeteringInfo(IntPtr.Zero, out outputMetering);
                    float rms = 0;
                    for (int i = 0; i < outputMetering.numchannels; i++)
                    {
                        rms += outputMetering.rmslevel[i] * outputMetering.rmslevel[i];
                    }
                    rms = Mathf.Sqrt(rms / (float)outputMetering.numchannels);

                    float db = rms > 0 ? 20.0f * Mathf.Log10(rms * Mathf.Sqrt(2.0f)) : -80.0f;
                    if (db > 10.0f) db = 10.0f;

Seems like it goes less than -90.

Thanks for getting in touch. As mentioned in the Bus.lockChannelGroup() API reference:

The channel group may not be available immediately after calling this function. When Studio has been initialized in asynchronous mode, the channel group will not be created until the command has been executed in the async thread. When Studio has been initialized with FMOD_STUDIO_INIT_SYNCHRONOUS_UPDATE, the channel group will be created in the next Studio::System::update call.

You can call Studio::System::flushCommands to ensure the channel group has been created. Alternatively you can keep trying to obtain the channel group via Studio:: Bus::getChannelGroup until it is ready.

Perhaps something like the following would work?

            yield return new WaitUntil(() => bus.getChannelGroup(out channelGroup) == FMOD.RESULT.OK);

Thanks so much for pointing that out! I was able to make it work without the random frame waiting using this code:

        protected void Start() => StartCoroutine(EnableMeteringRoutine());
        private IEnumerator EnableMeteringRoutine()
        {
            // Wait for Initialization
            yield return new WaitUntil(() => FMODUnity.RuntimeManager.IsInitialized);
            FMOD.Studio.Bus bus = FMODUnity.RuntimeManager.GetBus("bus:/Voice");
            FMOD.RESULT result = bus.lockChannelGroup(); // Forces bus to be created
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError($"Failed to create bus to enable metering {result}");
                yield break;
            }
            // Channel group won't be created when FMOD is in async mode until  
            // the command has been executed in the async thread.  So we just keep
            // trying to fetch the group until it exists.  We only try 1000 times though.
            int safetyValve = 1000;
            FMOD.ChannelGroup channelGroup;
            channelGroup.handle = IntPtr.Zero;
            yield return new WaitUntil(() =>
            {
                safetyValve--;
                if (--safetyValve <= 0 || (result = bus.getChannelGroup(out channelGroup)) == FMOD.RESULT.OK) return true;
                return false;
            });
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError($"Failed to fetch channel group to enable metering for voice {result}");
                bus.unlockChannelGroup();
                yield break;
            }
            FMOD.DSP dsp;
            if (channelGroup.getDSP(0, out dsp) != FMOD.RESULT.OK)
            {
                Debug.LogError($"Failed to fetch dsp to enable metering for voice {result}");
                bus.unlockChannelGroup();
                yield break;
            }
            dsp.setMeteringEnabled(false, true);
            bus.unlockChannelGroup();
        }

HOWEVER, it seems that if I do this when the game starts, it’s immediately forgotten. Metering only seems to stay enabled if I do it for every event that plays (through that same channel). Is this setting transient? Is there a way to make it stick so I don’t have to call setMeteringEnabled() every time I play audio? Or is that the expected usage?

As described in the Studio API Guide:

By default, when an event instance is created, the system ensures that every bus on its signal path has a corresponding channel group. When an event instance is destroyed, the system destroys any channel groups which are no longer required.

The bus’s channel group is probably getting destroyed when the event instance gets destroyed because you call bus.unlockChannelGroup() at the end of EnableMeteringRoutine(). To prevent this, avoid calling bus.unlockChannelGroup() until you no longer need the metering information - perhaps in OnDisable()?

Ahh OK so it’s actually idiomatic to enable the metering whenever you play a sound using that channel group. Alright, I was just worried that I was doing a bunch of idempotent no-ops, but it sounds like that’s not the case?

Is it better to repeatedly enable metering, or keep the channel group locked until the game stops? This is the dialogue channel group so it’s not really playing all the time. What’s the best practice here?

Generally speaking, best practice is to release any resources you’re not using. In the case of dialogue that only plays occassionally I think it would be best to release the group when it’s no longer required and lock it next time you want to get metering info.

Makes sense to me, thank you for explaining everything!