Getting Programmer Instrument clip info locally

Hello,

I’m using the programmer instrument approach to voiceover illustrated in the docs, within the context of a finite state machine. This has worked well for basic playback, but I need access to more information about the sounds played to complete our approach and the docs seem to not elaborate further on programmer instrument usage.

Specifically: for our dialogue display/playback behavior, I need instance-level access to:

  • Whether or not the string provided references a sound file to be played. If not, I use a default wait time or wait for input before proceeding.
  • If audio exists, the duration of the audio. If there is a voice line, I wait for the duration of the line then automatically proceed to the next.

Everything I can find on working with programmer instruments uses static functions for callbacks - is there a way to do this on an instance level, or am I misunderstanding the intended workflow?

Thanks!

In this case, I will assume you’re using audio tables - if this isn’t the case, please let me know.

If, in your callback, you’re using Studio.System.getSoundInfo() to retrieve an entry from an audio table, you can simply check the returned result - if the sound doesn’t exist, getSoundInfo() should return FMOD.RESULT.ERR_EVENT_NOTFOUND:

// Attempt to get audio asset and its sound info with key
FMOD.Studio.SOUND_INFO soundInfo;
var keyResult = FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(key, out soundInfo);
// Check result to see whether asset exists
// Checking against != FMOD.RESULT.OK also works as a catch-all
if (keyResult == FMOD.RESULT.ERR_EVENT_NOTFOUND)
{
    Debug.Log("Key '" + key + "' doesn't exist");
    break;
}

If the audio file does exist, you can create your Sound, and then do the following to check its length:

// Using asset audio table/FSB, so we need to get the subsound to get length
programmerSound.getSubSound(soundInfo.subsoundindex, out FMOD.Sound subsound);
// Get length of sound in milliseconds
subsound.getLength(out uint length, FMOD.TIMEUNIT.MS);
Debug.Log("Sound length = " + length);

Callbacks and user data are handled per-instance, even when the functions are static - each event instance that has a callback set with Studio.EventInstance.setCallback() will use the specific callback set, even if there are multiple instances of the event. Using Studio.EventDescription.setCallback() will set a functions as a callback for all newly created instances of a given event.

We recommend that the functions used are static to ensure that your code functions properly on all AOT platforms. Please see the section on Callbacks in our Unity docs for more information. Are you running into any specific issues due to the static nature of the functions?

Thanks for the reply - right, the static nature of the callback is what I’m having trouble wrapping my head around. I have an instance of my script reader class controlling a specific flow of dialogue, and its state is reliant upon the information I’m trying to get (clip [non]existence, clip length if extant) for the waiting/playback logic I described. Effectively my problem is relaying the retrieved data in the callback to my script reader instance.

What I’m hoping to achieve:

  • Attempt create programmer sound
    → If key doesn’t exist, set flag on ScriptReader instance to false.
    → Else, retrieve sound length from programmer instrument, and store in local variable on ScriptReader instance.
  • Additionally, subscribe ScriptReader instance to receive event when when programmer instrument has finished or stopped playback if possible.

So far, it still seems like all of the above info is only available in a static context. Am I misunderstanding something?

Apologies for it being unclear - this the intended purpose of “user data” set on your event with Studio::EventInstance::setUserData. For the programmer sound scripting example, it’s used to solely to pass a pointer to a key in, but since you’re passing a pointer, you can set the data being pointed to to effectively pass data between the callback and class instance, which is what it’s used for in the timeline callback scripting example. The scope of the data you set as user data can basically be anything you want - while it’s best to keep it simple, you could pass a handle to your entire ScriptReader class instance if you wanted to.

In your case, you’ll likely want to do the following:

  • Create a class to hold the info you need to pass between the static callback and your non-static class instance:
class ScriptReaderInfo
{
    public string key;
    public bool keyExists;
    public float length;
}

private ScriptReaderInfo scriptReaderInfo;
  • Initialize an instance of the ScriptReaderInfo class and set a pointer to it as your event instance’s user data:
// After creating your event instance...
// Initialize class
scriptReaderInfo = new ScriptReaderInfo();
scriptReaderInfo.key = key;
scriptReaderInfo.keyExists = false;
scriptReaderInfo.length = -1;

// Pin the class in memory and pass a pointer through the user data
GCHandle structHandle = GCHandle.Alloc(scriptReaderInfo);
dialogueInstance.setUserData(GCHandle.ToIntPtr(structHandle));
  • Instead of retrieving just the key in the callback, get the whole class:
// Start of callback...

FMOD.Studio.EventInstance instance = new FMOD.Studio.EventInstance(instancePtr);

// Retrieve the user data
IntPtr structPtr;
instance.getUserData(out structPtr);

// Get the string object
GCHandle structHandle = GCHandle.FromIntPtr(structPtr);
ScriptReaderInfo scriptReaderInfo = (ScriptReaderInfo)structHandle.Target;

You can then directly set scriptReaderInfo members inside the callback and the data will be accessible outside of the callback, and vice-versa.


This is possible in a few ways. One possibility is, on recieving a specific callback type, setting a bool in the aforementioned ScriptReaderInfo class and checking it from Update(). A more direct way would be to pass a delegate as a part of your class:

public delegate void EventStoppedDelegate();

class ScriptReaderInfo
{
    public string key;
    public bool keyExists;
    public float length;
    // delegate to be called when event is stopped
    public EventStoppedDelegate onEventStopped;
}

void EventStopped()
{
    Debug.Log("Event stopped!");
}
scriptReaderInfo = new ScriptReaderInfo();
scriptReaderInfo.key = key;
scriptReaderInfo.keyExists = false;
scriptReaderInfo.length = -1;
scriptReaderInfo.onEventStopped = new EventStoppedDelegate(EventStopped);

Then, in your callback, you can check for a specific callback type, and call the delegate:

case FMOD.Studio.EVENT_CALLBACK_TYPE.STOPPED:
    {
        scriptReaderInfo.onEventStopped();
        break;
    }

It’s worth noting that, if you are calling a delegate from the callback, you’ll want to keep it simple since it’ll be called from the FMOD Studio thread, as has the potential to slow/block.

This is a bit of a wall of text, so please let me know if you have any questions.

I see - thanks for the in-depth explanation! Only issue now: I’m getting a 0 result returned from getLength(), is there something on the Studio side I need to do to ensure the value returned matches the sound length?

It’s likely that you’re getting the length of the parent sound, instead of the subsound. As previously mentioned, since you’re using audio tables, you’ll need to retrieve the subsound matching the subsoundindex member of the sound info struct retrieved with FMODUnity.RuntimeManager.StudioSystem.getSoundInfo().

If you are retrieving the correct subsound, could I get you to post the snippet of code where you create the sound and attempt to get its length?

Here’s the snippet of the code within the callback responsible for creating the sound/gathering the information.

FMOD.Sound dialogueSound;
var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(dialogueSoundInfo.name_or_data, soundMode | dialogueSoundInfo.mode, ref dialogueSoundInfo.exinfo, out dialogueSound);
  
if (soundResult == FMOD.RESULT.OK)
{
    info.keyExists = true;
    
    parameter.sound = dialogueSound.handle;
    parameter.subsoundIndex = dialogueSoundInfo.subsoundindex;
    
    
    Marshal.StructureToPtr(parameter, parameterPtr, false);
    FMOD.Sound subsound;
    dialogueSound.getSubSound(dialogueSoundInfo.subsoundindex, out subsound);
    uint msLength;
    subsound.getLength(out msLength, FMOD.TIMEUNIT.MS);
    float floatLength = Convert.ToSingle(msLength) * .001f;
    info.length = floatLength;
    Debug.Log("Floatlength: " + floatLength);//outputs 0
    Debug.Log("Intlength: " + msLength);//outputs 0
}

Bumping this - any insight on why subsound lengths would always return 0?

Thanks for posting the code snippet.

Unfortunately, your code works for me without any issues when using audio tables. I tested on FMOD for Unity 2.02.20, trying various advanced loading modes for the audio table in Studio, and was able to retrieve the length of all of them. Just in case it works, I would recommend trying to get the length of dialogueSound instead, but getting the subsound length should be what works.

What version of FMOD for Unity are you using? Can I get you to upload the full code for your ScriptReader class, as well as the relevant banks, to your FMOD user profile so I can give them a test on my end?