How To Build Plugins For FMOD


When I first started using FMOD Studio, the low-level code seemed daunting and complicated. But over the years, I gained more programming experience and became comfortable with how FMOD worked; I took some audio programming lessons using JUCE and felt comfortable enough to tackle making plugins for FMOD. However, there weren't any easy to follow guides on the topic, and so I am writing one to help those in a similar position as I.

I will be doing this guide on Mac and using Xcode, but it is still possible to do this on Windows and Linux. The main goal is to create a dynamic library placed in a specific folder so that FMOD can load it and use it.

As well, I will be assuming you have, and know how to use, Xcode and can read C++.

View the finished file

Setting Up Xcode

I'm using 10.2.1 but this will work with practically any version. Any differences between versions shouldn't be a concern as we aren't using fancy features to meet our goals.

Create a new Project. We want to choose the 'Library' option for 'macOS'.

Choosing the Library option in the New Project dialogue window

When you click next, you will want to choose 'None (Plain C/C++ Library' under 'Framework' and 'Dynamic' under 'Type'.

Options for our new project

Adding FMOD To The Project

You will need to download the programmer's API from FMOD's website and add it to a sensible folder. I have an 'FMOD' folder in 'Applications' that houses all API folders and application versions.

The API version you download needs to be the same version as the application version you're building for.

Once downloaded, we need to set the 'Header Search Paths' to point to the FMOD files we just downloaded. Specifically the low-level header files.

This is found in "FMOD Programmers API X.XX.XX/api/lowlevel/inc".

In FMOD 2.00.00 and later, lowlevel is called core.

Add the FMOD header files to Xcode

You can drag and drop the folder to get the full path or copy the path from Finder. The primary importance is the quotation marks, so Xcode doesn't treat the string as multiple directories.

Setting Destination Paths

It's essential that the plugin we make goes into the correct folder for FMOD to read. We can either set this path for the whole system or for the project. Alternatively, FMOD's preferences can be changed to point to a specific folder on the computer -personally, this is what I like to do.

To do this in Xcode, first go to the project settings.

Clicking Project Settings

Then press 'Advanced'.

Settings dialogue window

We now want to set the destination to 'Custom' and 'Absolute'.

Advanced Settings dialogue and choosing the correct options

Choosing 'Relative to Derived Data' and 'Relative to Workspace' will still work but require more work to get the correct path. By choosing 'Absolute' we're able to press the folder icon and point to the exact path we want.

Selecting the correct paths:
  • Products: Point to the /Plugins folder mentioned above
  • Intermediates + Index Datastore: This is best put somewhere secure. Do not put it the Xcode project folder as it can get deleted when performing a 'Clean' and do not put it on the Desktop

The main path is the 'Products' path. The other two are not very important in comparison.

Inputting our destination paths
This folder requires FMOD's preferences to be set. Use default paths for less work

Once done, the plugin will build into the correct folder and FMOD will be able to load it without any extra work.

Writing The Code

First, create a new C++ file with a name like 'Plugin.cpp' or 'Compressor.cpp'.

Creating a new C++ file

When you press next and the option 'Also create a header file' is given, uncheck the toggle as we don't need a header file.

Naming our file

Once we have our file, we can begin creating our plugin. We'll first include the FMOD C++ header, fmod.hpp, and some standard libraries to make our lives easier.

#include <math.h>
#include <stdio.h>
#include <string>

#include "fmod.hpp"

We then declare the most important function in the plugin. This method is what FMOD calls to get information about our plugin.

extern "C"
{
   F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription();
}

It's important to be using the "C" extern so the C++ code correctly loads in FMOD; the macros F_EXPORT and F_CALL make it easy for our code to run on other platforms.

Next, we define the callback methods for our plugin. When the plugin is loaded, FMOD will call these functions and allow us to do whatever we want in our plugin.

FMOD_RESULT F_CALLBACK Plugin_Create                    (FMOD_DSP_STATE *dsp_state);

FMOD_RESULT F_CALLBACK Plugin_Release                   (FMOD_DSP_STATE *dsp_state);

FMOD_RESULT F_CALLBACK Plugin_Process                   (FMOD_DSP_STATE *dsp_state, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op);

FMOD_RESULT F_CALLBACK Plugin_SetBool             (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL value);

FMOD_RESULT F_CALLBACK Plugin_GetBool             (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL *value, char *valuestr);

In should be noted that these aren't the callbacks available; these are just the necessary ones for a very basic plugin.

Next, we'll add our parameters for the plugin. I'll be making a 'Mute' plugin so will just have an on/off toggle.

static FMOD_DSP_PARAMETER_DESC mute;

FMOD_DSP_PARAMETER_DESC* Silence_DSP_Param[1] =
{
    &mute
};

All parameters at this point should be FMOD_DSP_PARAMETER_DESCs as setting them to bools, floats and ints happen later.

Finally, we can define our plugin.

FMOD_DSP_DESCRIPTION Silence_Desc =
{
    FMOD_PLUGIN_SDK_VERSION,    // version
    "Kelly Silence",            // name
    0x00010000,                 // plugin version
    1,                          // no. input buffers
    1,                          // no. output buffers
    Plugin_Create,              // create
    Plugin_Release,             // release
    0,                          // reset
    0,                          // read
    Plugin_Process,             // process
    0,                          // setposition
    1,                          // no. parameter
    Silence_DSP_Param,          // pointer to parameter descriptions
    0,                          // Set float
    0,                          // Set int
    Plugin_SetBool,             // Set bool
    0,                          // Set data
    0,                          // Get float
    0,                          // Get int
    Plugin_GetBool,             // Get bool
    0,                          // Get data
    0,                          // Check states before processing
    0,                          // User data
    0,                          // System register
    0,                          // System deregister
    0                           // Mixer thread execute / after execute
};

This structure allows us to set data like SDK version, name and version but more importantly, enables us to tell FMOD what functions to call.

Now we define our FMODGetDSPDescription() to return our newly made description. We also create our toggle parameter for FMOD and point it to our earlier FMOD_DSP_PARAMETER_DESC.

extern "C"
{

F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription ()
{

    FMOD_DSP_INIT_PARAMDESC_BOOL(mute, "Mute", "", "Whether this plugin lets through audio or not", false, 0);
    return &Silence_Desc;
}

}

At this point, FMOD will be able to load the plugin. But our methods don't do anything.

However, before we can define our methods, we need someway of storing the state of our plugin. In the case of this plugin, we need to know whether to mute the audio or not. To do this, we'll create a class and use this as our plugin's state/memory. We can then set a pointer in the FMOD plugin to this class so all of our callback methods can poll the plugin's state.

class SilenceState
{
public:
    void SetMute(bool value) { m_mute = value; }
    bool GetMute() const { return m_mute; }

private:
    bool m_mute;
};

Define Our Methods

Now we'll begin defining our callback methods.

FMOD_RESULT F_CALLBACK Plugin_Create                    (FMOD_DSP_STATE *dsp_state)
{
    dsp_state->plugindata = (TOmSSilenceState* )FMOD_DSP_ALLOC(dsp_state, sizeof(TOmSSilenceState));
    if (!dsp_state->plugindata)
    {
        return FMOD_ERR_MEMORY;
    }

    return FMOD_OK;
}

FMOD_DSP_STATE::plugindata is a void pointer to be set and used by the user. We call FMOD_DSP_ALLOC to create the class, check whether the object instantiated and return from the method.

Of course, whenever we allocate memory we need to deallocate.

FMOD_RESULT F_CALLBACK Plugin_Release                   (FMOD_DSP_STATE *dsp_state)
{
    TOmSSilenceState* state = (TOmSSilenceState* )dsp_state->plugindata;
    FMOD_DSP_FREE(dsp_state, state);
    return FMOD_OK;
}

Now for the main part of the plugin.

FMOD_RESULT F_CALLBACK Plugin_Process                   (FMOD_DSP_STATE *dsp_state, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op)
{
    switch (op) {
        case FMOD_DSP_PROCESS_QUERY:

            if (outbufferarray && inbufferarray)
            {
                outbufferarray[0].bufferchannelmask[0] = inbufferarray[0].bufferchannelmask[0];
                outbufferarray[0].buffernumchannels[0] = inbufferarray[0].buffernumchannels[0];
                outbufferarray[0].speakermode          = inbufferarray[0].speakermode;
            }

            if (inputsidle)
            {
                return FMOD_ERR_DSP_DONTPROCESS;
            }
            break;

        case FMOD_DSP_PROCESS_PERFORM:

            TOmSSilenceState* state = (TOmSSilenceState* )dsp_state->plugindata;
            unsigned int samples = length * inbufferarray[0].buffernumchannels[0];

            while (samples--)
            {
                if (state->GetMute())
                {
                    *outbufferarray[0].buffers[0]++ = 0;
                }
                else
                {
                    *outbufferarray[0].buffers[0]++ = *inbufferarray[0].buffers[0]++;
                }

            }

            break;
    }

    return FMOD_OK;
}

The method is invoked twice every DSP mix. The first call checks whether to process the plugin and the second does the processing -if we haven't returned FMOD_ERR_DSP_DONTPROCESS or FMOD_ERR_DSP_SILENCE.

For this plugin, we check if any audio is coming through by polling inputsidle. If there is no audio, we return FMOD_ERR_DSP_DONTPROCESS and FMOD doesn't call the method a second time.

If there is input, we need to fill the output buffer. First, we get a pointer to the plugin's state and then get the total length of the buffer.

The while loop then checks the plugin's state and either sets the output buffer to 0 (muted) or equal to the input buffer's signal.

Finally, we need our plugin to change state based on FMOD.

FMOD_RESULT F_CALLBACK Plugin_SetBool             (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL value)
{
    TOmSSilenceState* state = (TOmSSilenceState* )dsp_state->plugindata;
    state->SetMute(value);
    return FMOD_OK;
}

FMOD_RESULT F_CALLBACK Plugin_GetBool             (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL *value, char *valuestr)
{
    TOmSSilenceState* state = (TOmSSilenceState* )dsp_state->plugindata;
    *value = state->GetMute();
    return FMOD_OK;
}

And that's the plugin finished.

You can view this plugin and others on my GitHub. I don't claim to be an expert, and there are still mistakes to fix -even while writing this I was updating the plugin due to errors and inaccuracies- but it should be a good place to start if you're stuck or just interested.