How to get the length of an FMOD parameter sound without using the callback functions

Our project has somewhat recently converted to using FMOD as our Audio solution, and after digging into the documentation and trying different methods, we’ve reached a good point where we can do almost everything we want using FMOD, utilizing a combination of EventInstances and StudioEventEmitters to convert from Unity’s base audio system. The hitch we’ve run into recently is getting audio lengths from fmod clips.

We have some functionality that needs to know the duration of an audio clip before calling certain events once it’s completely played. To this point we’ve just been manually inputting those lengths, but we’d like to handle that programmatically.

I have our audio system hooked up to the FMOD callbacks that can be used to get Sound information when an audio is created, played, destroyed, etc, but to get that information the audio needs to be played as far as I understand it; we need to get the length well before the audio is queued up to be played.

The best solution I’ve found is EventDescription.getLength() but I’ve run into so many problems with that. Primarily, it works fine for a bank that is a single track non-parameterized sound. But for any sub sounds that use parameters, it was always returning 0 length.

Additionally, it only seemed to be working with any banks that had a ‘Timeline’ set. I’m less familiar with FMOD studios than our audio team, so forgive me if I’m getting some of the terminology incorrectly.

I’ve spent a lot of time digging deep, and usually the answer comes back to using the callback methods for these solutions, so I just wanted to see if there was an option that we’re missing here.

Hi,

Unfortunately, there’s no easy, generally applicable way to retrieve the duration of an audio asset at runtime without using callback, and it’s flat-out impossible to directly retrieve without playing the sound in question - I’ll be adding this to our internal feature request/improvement tracker.

A workaround would be to enumerate your events and nested events to list the instrument audio clip lengths using a custom Studio script, which you can hook into the Bank building process to export alongside your Banks, and then parse in Unity to apply your desired behavior. I would be happy to create the script for you, but I won’t be able to do so for the next week or so as I’ll be on holidays.

Just to clarify though, which of the following are you looking to retrieve?

  • The duration of a whole event

  • The duration of a single instrument’s audio clip in an event

  • The duration from the start of an event up until the end of a specified instrument’s audio clip

Also, for context, is there any specific reason why the audio clip duration is required, as opposed to checking when an instrument/event has stopped playing?

A script like that would be awesome to check out when you can get around to it. All that functionality would be appreciated, though primarily the duration of a single instrument clip is the direct use case right now.

The reason we want to get the length is that certain other things in our project are trying to be synced in duration with audio clips, and we were trying to avoid a refactor that would game important events dependent on audio playback. It could be a viable solution with some refactoring, but we wanted to explore the options for how to proceed with just knowing the duration of audio clips. Our project wants the ability to for future studios who use the code base to use either FMOD or Unity’s built in audio system and it made more sense to not tie that functionality directly to FMOD’s callback system.

Alright, I’ve created a script that will save your project, build your project’s banks, and then write one of the following to a file in your build directory:

  • Length of all event timelines in the project
  • Length of individual instruments on all event timelines in project
  • Length of all assets in your master assets folder

While it’s not possible to get the length of instruments playing on parameter sheets, as the length property is physical space the instrument takes up on a parameter sheet and not the audio clip length, the file containing all asset lengths should provide you with the length of the clip instead.

To run the script, simply save it to a .js file, place it in your FMOD Studio installation’s “Scripts” folder, and run it from the “Scripts” dropdown menu in Studio. If you run into any issues with the script, or have any questions, feel free to let me know.

studio.menu.addMenuItem({
    name: "Build + Write Lengths",
    subMenuItems: function() {
        return [
            {
                name: "Event Timeline Lengths",
                execute: function() {
                    writeLenToFile("EventTimelineLengths");
                }
            },
            {
                name: "Timeline Instrument Lengths",
                execute: function() {
                    writeLenToFile("TimelineInstrumentLengths");
                }
            },
            {
                name: "Asset Lengths",
                execute: function() {
                    writeLenToFile("AssetLengths");
                }
            }
            
        ];
    }
});

function writeLenToFile(objectType){

    // Save project and build banks before doing anything else
    studio.project.save();
    studio.project.build();

    // Get project path and custom build directory if one is set, and define txt file output path
    var projectPath = studio.project.filePath;
    var projectName = projectPath.substr(projectPath.lastIndexOf("/") + 1, projectPath.length);
    if(typeof studio.project.workspace.builtBanksOutputDirectory == 'undefined'){
        outputPath = projectPath.substr(0, projectPath.lastIndexOf("/") + 1) + "Build/" + objectType + ".txt";
    } else {
        outputPath = studio.project.workspace.builtBanksOutputDirectory + objectType+ ".txt";
    }
    
    // Open txt file at output path, returning early if file cannot be opened
    var textFile = studio.system.getFile(outputPath);
    if (!textFile.open(studio.system.openMode.WriteOnly)) {    
        alert("Failed to open file {0}\n\nCheck the file is not read-only.".format(outputPath));
        console.error("Failed to open file {0}.".format(outputPath));
        return;
    }

    // Write description of info contained in file
    textFile.writeText(objectType + " for '" + projectName +"'\r\n\r\n");
    
    // Do appropriate function for export type
    if (objectType == "EventTimelineLengths"){
        getEventLengths(textFile);
    } else if (objectType == "TimelineInstrumentLengths"){
        getInstrumentLengths(textFile);
    } else if (objectType == "AssetLengths"){
        getAssetLengths(textFile);
    }
    
    textFile.close();
    console.log("File containing " +objectType + " successfully created at: " + outputPath);
}

// Get length of each event timeline by finding length of latest instrument
function getEventLengths(textFile){

    var events = studio.project.model.Event.findInstances();
    events.forEach(function(event) {

      var max = 0;
      if (event.timeline) {
        var instruments = event.timeline.modules;
        instruments.forEach(function(inst) {
          if (inst.entity.indexOf("Sound")!=0) {
            var tmp = inst.start + inst.length;
            if (tmp > max) {
              max = tmp;
            }
          }
        })
      }
    
      if (max > 0){
        textFile.writeText(event.name + " Length = " + max + "\r\n");
      }
      
    })
}

// Get individual lengths of all instruments in all event timelines
function getInstrumentLengths(textFile){
    var events = studio.project.model.Event.findInstances();
    events.forEach(function(event) {
        
        // Iterate over instruments on timeline
        textFile.writeText("Event Name = " + event.name + "\r\n");
        if (event.timeline) {
            event.timeline.modules.forEach(function(inst) {
                if (inst.entity.indexOf("Sound")!=0) {
                    textFile.writeText(
                        "\tInstrument Name = " + inst.name +
                        ", Length = " + inst.length + "\r\n"
                        );
                }
            });
        }

    })
}

// Iterates over entire master asset folder and writes length of each asset to file
function getAssetLengths(textFile){
    studio.project.workspace.masterAssetFolder.assets.forEach(function(asset) {
        if (typeof asset.length != 'undefined'){
            textFile.writeText(
                "Asset Path = " + asset.assetPath + 
                ", Asset Length = " + asset.length + "\r\n"
                );
        }
    })
}

These have been pretty helpful already, I’ve been playing around with the files this script produces, in particular I think we can do what we need to via ‘Length of all assets’ function. It seems to be writing out the lengths in random order (neither alphabetical or ascending/descending based on length), but I can rig up a C# script to read the file and do some sorting on our end. Thanks for all the help, I will reach out again if I run into any problems!

1 Like

No problem!

Ah it occurs to me now that there is no good way to check the name of the asset that will be played in Unity is there (outside of using the aforementioned callback functions).

That is, there’s no way to take an EventEmitter/EventInstance and fetch the name of the asset that will play if X parameter is set to Y, etc.?

Outside of using a callback function, no, there’s no way to grab an asset name from an Event at runtime based on a parameter. However, you can use scripting to export a given instrument’s asset path and length alongside and the region it occupies on a parameter sheet:

studio.menu.addMenuItem({
    name: "Build + Write Parameter Sheet Asset Lengths",
    execute: function() {
        writeLenToFile("ParameterSheetAssets");
    }
});

var tab = "\t";
var tabNum = 0;

function writeLenToFile(objectType){

    // Save project and build banks before doing anything else
    studio.project.save();
    studio.project.build();

    // Get project path and custom build directory if one is set, and define txt file output path
    var projectPath = studio.project.filePath;
    var projectName = projectPath.substr(projectPath.lastIndexOf("/") + 1, projectPath.length);
    if(typeof studio.project.workspace.builtBanksOutputDirectory == 'undefined'){
        outputPath = projectPath.substr(0, projectPath.lastIndexOf("/") + 1) + "Build/" + objectType + ".txt";
    } else {
        outputPath = studio.project.workspace.builtBanksOutputDirectory + objectType+ ".txt";
    }
    
    // Open txt file at output path, returning early if file cannot be opened
    var textFile = studio.system.getFile(outputPath);
    if (!textFile.open(studio.system.openMode.WriteOnly)) {    
        alert("Failed to open file {0}\n\nCheck the file is not read-only.".format(outputPath));
        console.error("Failed to open file {0}.".format(outputPath));
        return;
    }

    // Write description of info contained in file
    textFile.writeText(objectType + " for '" + projectName +"'\r\n\r\n");
    
    // Call function to go through Studio project
    // This is where the if statement in the previous script was
    getAssetsOnParamSheets(textFile);
    
    textFile.close();
    console.log("File containing " + objectType + " successfully created at: " + outputPath);
}

function getAssetsOnParamSheets(textFile){

    // get all events and iterate through them
    var events = studio.project.model.Event.findInstances();
    events.forEach(function(event) {
        
        // if parameter sheet(s) exists, write event name
        if (event.parameters.length > 0){
            textFile.writeText(
                repeatTab(tabNum) + "Event Name = " + event.name + "\r\n"
                );
                
            tabNum++;

            // get parameter sheets and write parameter names
            event.parameters.forEach(function(parameter){
                if (parameter.modules.length > 0){
                    textFile.writeText(
                        repeatTab(tabNum) + "Parameter Name = " + parameter.preset.presetOwner.name + "\r\n"
                        );

                    tabNum++;

                    // write top-level instrument name, and parameter value region it occupies
                    parameter.modules.forEach(function(instrument){
                        var end = instrument.start + instrument.length;
                        textFile.writeText(
                            repeatTab(tabNum) + "Instrument Name = " + instrument.name + "\r\n" +
                            repeatTab(tabNum) + "Parameter Region = "+ instrument.start + "," + end + "\r\n"
                            );
                        
                        // get instrument's audio assets
                        getInstrumentAsset(textFile, instrument);
                        
                    });

                    tabNum--;
                }
            });

            tabNum--;
        }
    });
}

// get instrument asset path, handling recursively handling multiinstruments if needed
function getInstrumentAsset(textFile, instrument){

    // check if instrument is multi instrument, if yes then recurse
    if (instrument.sounds != null){
        instrument.sounds.forEach(function(nestedInstrument){
            getInstrumentAsset(textFile, nestedInstrument);
        });
    } else if (instrument.audioFile != null){
        // if not multi instrument and audio asset exists, write asset path to file
        tabNum++;
        textFile.writeText(
            repeatTab(tabNum) + "Asset Path = " + instrument.audioFile.assetPath + "\r\n" +
            repeatTab(tabNum) + "Asset Length = "+ instrument.audioFile.length + "\r\n"
            );
        tabNum--;
    }
}

// helper func to repeat tab n times for string formatting purposes
function repeatTab(n){
    var str = "";
    var i = 0;
    while (i < n){
        str += tab;
        i++
    }
        
    return str;
}

This should provide the info you’re looking for, though same as before you’ll need to parse the file yourself in Unity. Let me know if you run into any issues with the script.

Thanks for the update, after I had posted and logged off, I was hoping something like that would be possible. I am running into a run time error with the getInstrumentAsset() function, I tried to do some debugging to catch it, but nothing looked out of the ordinary. I’ve tried using that code block as it’s own standalone file in the scripts folder, as well as integrating all 4 of those methods into one file.

The error is the same regardless how I attempt it, I’ll share it here.

TypeError: Result of expression ‘parameter.preset’ [undefined] is not an object

on the line

repeatTab(tabNum) + "Parameter Name = " + parameter.preset.presetOwner.name + “\r\n”

My javascript is rusty, but this is basically saying the objects in event.parameters do not have an object of ‘preset’ defined?

That is indeed what’s happening. This issue is a mistake on my part - action sheets are listed under parameters, but don’t have the same structure as a parameter sheet and lack the preset property. If you edit the if statement just above the erroneous line to be:

// Action sheets will show under parameters, but will only ever have a single module
// As such, check if modules is array to exclude action sheets
if (Array.isArray(parameter.modules) && parameter.modules.length > 0)

then the code should work.

If I could make one last request to the functionality of the above script, would it be possible to also add the legth to these Events that don’t have parameters as well? An example in the following image would be the

Event Name = feature_bell_double_short
Event Name = Volume_level

With that, we’d be able to use this one generated file to get the length of every event in the project.

It is possible; to clarify, which of the following (or some combination of them) do you need?

  • The length of the event’s timeline (specifically, the end of the last instrument on the timeline)
  • The length of each instrument on the timeline
  • The length of the asset(s) in each instrument on the timeline

I believe we need that last one; the length of the sound asset in timeline for a timeline that doesn’t have any parameters. In this example, some of our events have no extra instruments or parameters, like the feature_bell_double_short event in the picture provided, that’s just a simple sound effect.

For those examples, the current loop is not producing an asset length.

Alright, I’ve modified the script to output the same information (instrument name, region of sheet, asset path & length) for the timeline and action sheets as it does parameter sheets. I’ve also fixed the indentation formatting slightly.

studio.menu.addMenuItem({
    name: "Build + Write Event Asset Lengths",
    execute: function() {
        writeLenToFile("EventAssetLengths");
    }
});

var tab = "\t";
var tabNum = 0;

function writeLenToFile(objectType){

    // Save project and build banks before doing anything else
    studio.project.save();
    studio.project.build();

    // Get project path and custom build directory if one is set, and define txt file output path
    var projectPath = studio.project.filePath;
    var projectName = projectPath.substr(projectPath.lastIndexOf("/") + 1, projectPath.length);
    if(typeof studio.project.workspace.builtBanksOutputDirectory == 'undefined'){
        outputPath = projectPath.substr(0, projectPath.lastIndexOf("/") + 1) + "Build/" + objectType + ".txt";
    } else {
        outputPath = studio.project.workspace.builtBanksOutputDirectory + objectType+ ".txt";
    }
    
    // Open txt file at output path, returning early if file cannot be opened
    var textFile = studio.system.getFile(outputPath);
    if (!textFile.open(studio.system.openMode.WriteOnly)) {    
        alert("Failed to open file {0}\n\nCheck the file is not read-only.".format(outputPath));
        console.error("Failed to open file {0}.".format(outputPath));
        return;
    }

    // Write description of info contained in file
    textFile.writeText(objectType + " for '" + projectName +"'\r\n\r\n");
    
    // Call function to go through Studio project
    // This is where the if statement in the previous script was
    IterateEvents(textFile);
    
    textFile.close();
    console.log("File containing " + objectType + " successfully created at: " + outputPath);
}

function IterateEvents(textFile){

    // get all events and iterate through them
    var events = studio.project.model.Event.findInstances();
    events.forEach(function(event) {

        // write event name
        textFile.writeText(
            RepeatTab(tabNum) + "Event Name = " + event.name + "\r\n"
            );

        // if timeline contains instruments, write asset length of instruments
        if (event.timeline.modules.length > 0){
            tabNum++;
            
            textFile.writeText(
                RepeatTab(tabNum) + "Timeline" + "\r\n"
                );

            tabNum++;
            event.timeline.modules.forEach(function(instrument) {
                GetInstrumentInfo(textFile, instrument);
                
            });
            tabNum--;

            tabNum--;
        }        


        // if parameter sheet(s) exists, write asset length of instruments
        if (event.parameters.length > 0){
            tabNum++;

            // get parameter sheets and write parameter names
            event.parameters.forEach(function(parameter){

                // Action sheets will show under parameters, but will only ever have a single module
                // As such, check if modules is array to exclude action sheets
                if (Array.isArray(parameter.modules) && parameter.modules.length > 0){
                    textFile.writeText(
                        RepeatTab(tabNum) + "Parameter Name = " + parameter.preset.presetOwner.name + "\r\n"
                        );

                    tabNum++;

                    parameter.modules.forEach(function(instrument){
                        // get instrument's audio assets
                        GetInstrumentInfo(textFile, instrument);
                        
                    });

                    tabNum--;
                } else if (parameter.modules.sounds && parameter.modules.sounds.length > 0){
                    // else if action sheet contains at least one sound, write action sheet info
                    textFile.writeText(
                        RepeatTab(tabNum) + "Action Sheet\r\n"
                        );
                    tabNum++;
                    GetInstrumentInfo(textFile, parameter.modules);
                    tabNum--;
                }
            });

            tabNum--;
        }
        
    });
}

// get instrument info, handling recursively handling multiinstruments if needed
function GetInstrumentInfo(textFile, instrument){

    // write name and region that instrument occupies on sheet
    var end = instrument.start + instrument.length;
    textFile.writeText(
        RepeatTab(tabNum) + "Instrument Name = " + instrument.name + "\r\n"
        );
    
    tabNum++;
    textFile.writeText(
        RepeatTab(tabNum) + "Region = "+ instrument.start + "," + end + "\r\n"
        );

    // check if instrument is multi instrument, if yes then recurse
    if (instrument.sounds){
        instrument.sounds.forEach(function(nestedInstrument){
            GetInstrumentInfo(textFile, nestedInstrument);
        });
    } else if (instrument.audioFile){
        // if not multi instrument and audio asset exists, write asset path to file
        textFile.writeText(
            RepeatTab(tabNum) + "Asset Path = " + instrument.audioFile.assetPath + "\r\n" +
            RepeatTab(tabNum) + "Asset Length = "+ instrument.audioFile.length + "\r\n"
            );
    }
    tabNum--;
}

// helper func to repeat tab n times for string formatting purposes
function RepeatTab(n){
    var str = "";
    var i = 0;
    while (i < n){
        str += tab;
        i++
    }
        
    return str;
}

As usual, let me know if you run into any issues with the script.

1 Like