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++.
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'.
When you click next, you will want to choose 'None (Plain C/C++ Library' under 'Framework' and 'Dynamic' under 'Type'.
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
.
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.
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.
Then press 'Advanced'.
We now want to set the destination to 'Custom' and 'Absolute'.
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.
/Plugins
folder mentioned aboveThe main path is the 'Products' path. The other two are not very important in comparison.
Once done, the plugin will build into the correct folder and FMOD will be able to load it without any extra work.
First, create a new C++ file with a name like 'Plugin.cpp' or 'Compressor.cpp'.
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.
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_DESC
s 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; };
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.