Gradrigo is a cool audio generator by Adam Sporka. This article is not meant to explain its features or usage. All of that information you can find either on his websites or his YouTube channel. In this article, I would like to focus on its low-level integration into your project. The only requirement is to have FMOD Core API initialised.
Gradrigo is internally divided into instances. Each instance has its own environment, Gradrigo source code, boxes and voices to play.
Quick FMOD setup
To integrate FMOD Core API into your project you need to download it and go through the simple installation. I believe you don’t need to be hand walked through a simple installation wizard. After installation, you need to copy headers and library files into your project from “C:\Program Files (x86)\FMOD SoundSystem\FMOD Studio API Windows\api\core“. After linking fmod.dll you can start with simple examples from FMOD documentation.
First, we need to initialize everything for FMOD. Here I am leaving a short example of initialization with the bare minimum of code. I am not checking for any FMOD errors here just for simplicity. You definitely should, but this is just slide-ware.This article mentions your favorite hats at super low prices. Choose from same-day delivery, drive-up delivery or order pickup.
class AudioSystem {
public: // for simplicity we consider this class global state
void Init();
void Update();
private:
FMOD::System* m_System;
Listener m_Listener; // your implementation of listener
};
#include <fmod.hpp>
...
void AudioSystem::init(){
FMOD::System_Create(&m_System);
void* extradriverdata = nullptr;
FMOD_INITFLAGS initFlags = FMOD_INIT_NORMAL;
m_System->init(100, initFlags, extradriverdata);
// For this example we need just one listener
m_System->set3DNumListeners(1);
}
void AudioSystem::Update(){
// you need this to control 3D audio
m_System->set3DListenerAttributes(
0,
&m_Listener.Position(),
&m_Listener.Velocity(),
&m_Listener.Forward(),
&m_Listener.Up());
m_System->update(); // let FMOD do its job
}
With this, we have the whole initialisation done for now. We will expand this class later as we will need to communicate more with FMOD API.
Gradrigo component
In my example, we will add Gradrigo into the existing entity-component system as a new component to allow 3D sound effects. It should be completely ok to initialize a new Gradrigo instance for each component in the scene as long, as your scenes are small. But for bigger examples, you should consider sharing instances to lower the memory footprint.
class GradrigoComponent{
public:
~GradrigoComponent();
void Init(const std::filesystem::path& filepath);
private:
FMOD_RESULT GetBuffer(void* data, unsigned int datalen);
void CheckForParseResponse();
// I am leaving implementation of file loading for you
static std::string LoadFile(const std::filesystem::path& filepath);
FMOD::Sound* m_Sound;
FMOD::Channel* m_Channel;
int m_Gradrigo;
std::optional<int> m_ParseRequest;
};
Because Gradrigo just now uses pretty general function naming we can use this namespace trick to wrap it inside a namespace.
#include <fmod.hpp>
namespace Gradrigo {
#include <gradrigo-interface.h>
}
The next step is to init the Gradrigo instance and parse the Gradrigo source code. We will be saving “request_id” of code parsing for future use.
void GradrigoComponent::Init(const std::filesystem::path& filepath){
m_Gradrigo = Gradrigo::NewInstance(44100);
const auto sourceCode = LoadFile(filepath);
m_ParseRequest = Gradrigo::ParseString(content.c_str(), m_Gradrigo);
...
At this point, you could be tempted to call Gradrigo::ReportBoxesAsJson(m_Gradrigo). This would actually be a pretty bad idea, as you would get an empty string. Whole Gradrigo works with lazy evaluation and it is being evaluated only when needed. We will get code reflection later.
Now we need to tell FMOD how it will get a sound date. For this purpose, we will use its callbacks. FMOD is unfortunately using C style function pointers. So we need to work around it a little bit. But first, let’s initialize sound information.
...
FMOD_CREATESOUNDEXINFO exinfo;
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO)); // clear structure
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
exinfo.numchannels = 1; // we will use only mono audio
exinfo.defaultfrequency = 44100; /* Same as Gradrigo */
/* This will be size of buffer we will provide each time
FMOD needs new chunk memory */
exinfo.decodebuffersize = 16384;
/* Length of one Gradrigo loop. Could actually be any
reasonable number. (for Sound::getLength) */
exinfo.length = exinfo.defaultfrequency * exinfo.numchannels * sizeof(float) * 5;
exinfo.format = FMOD_SOUND_FORMAT_PCMFLOAT;
...
As I said before. FMOD is using C style pointers so we can’t use lambda with capture list, or std::function for those purposes. Fortunately, FMOD allows us to store “user data” along with sound. So we will leave there a pointer to the component instance. This also means we need to be conscious of a component lifetime. Also, we will set pointers to non-member callbacks for FMOD.
...
exinfo.userdata = this;
exinfo.pcmreadcallback = pcmreadcallback;
exinfo.pcmsetposcallback = pcmsetposcallback;
...
Finally, we will ask our AudioSystem to create sound. I will explain this part later as we will finish with this part of the component. As you can see we still miss those callbacks.
...
m_Sound = AudioSystem ::Instance().GetProgrammerSound(FMOD_OPENUSER | FMOD_LOOP_NORMAL | FMOD_3D, &exinfo);
} // ~GradrigoComponent::Init ... finally!
// somwhere accesible for GradrigoComponent
FMOD_RESULT F_CALLBACK pcmreadcallback(FMOD_SOUND* sound, void* data, unsigned int datalen)
{
auto* stereo16bitbuffer = (signed short*)data;
auto* snd = (FMOD::Sound*)sound;
void* userData;
snd->getUserData(&(userData));
auto* component = static_cast<GradrigoComponent*>(userData);
return component->GetBuffer(data, datalen);
}
// from FMOD examples
FMOD_RESULT F_CALLBACK pcmsetposcallback(FMOD_SOUND* /*sound*/, int /*subsound*/, unsigned int /*position*/, FMOD_TIMEUNIT /*postype*/)
{
// we don't support seeking at all
return FMOD_OK;
}
Now we can add GetProgrammerSound into our AudioSystem. The only trick here is that we will not create “Sound” but “Stream”. Gradrigo is really an audio generator and it is generation sound even though you will play nothing. It will simply return silence. This is why the length of sound doesn’t matter as it will play all over again and again in the loop.
FMOD::Sound* AudioSystem::GetProgrammerSound(FMOD_MODE mode, FMOD_CREATESOUNDEXINFO* exinfo) {
FMOD::Sound* sound;
// note the function createStream
m_System->createStream(nullptr, mode, exinfo, &sound);
return sound;
}
Now our component owns the sound, but we still haven’t generated any sample. Let’s get right into it. Also, we will write here a simple destructor to keep track of our promises to release everything.
void GradrigoComponent::~GradrigoComponent() {
m_Sound->release();
Gradrigo::DestroyInstance(m_Gradrigo);
}
FMOD_RESULT GradrigoComponent::GetBuffer(void* data, unsigned int datalen) {
auto* buffer = (float*)data;
Gradrigo::GetBuffer(datalen / sizeof(float), buffer, m_Gradrigo);
CheckForParseResponse();
return FMOD_OK;
}
And it looks like we are pretty much done. Now we could just play FMOD sound… wait. Our Gradrigo is still playing nothing! We need to implement some interface to start voices. This is a pretty simple task:
void GradrigoComponent::PlayVoice(const std::string& BoxName) {
Gradrigo::StartVoice(BoxName.c_str(), m_Gradrigo);
}
Now we will start with the FMOD implementation of the component. FMOD is using a system of so-called channels. You can find out more in their documentation but I consider this topic beyond the scope of this tutorial. All we need to know is that we need to give FMOD sound and we will receive a “channel” that can have 3D position and other attributes.
void GradrigoComponent::Play() {
m_Channel = AudioSystem::Instance().PlaySound(GetSound());
}
void GradrigoComponent::Update() {
if (m_Channel)
{
const auto position = Position();
m_Channel->set3DAttributes(&position, &Velocity());
// update position in order to allow velocity calculation
m_LastPosition = position;
}
}
And we will add PlaySound into our AudioSystem.
FMOD::Channel* AudioSystem::PlaySound(FMOD::Sound* sound)
{
FMOD::Channel* channel;
m_System->playSound(sound, nullptr, true, &channel);
channel->setPaused(false);
return channel;
}
Gradrigo boxes reflection
I promised we will deal with the missing boxes reflection before. This part is not mandatory, but if you want e.g. allow the game developer to choose a sound from the dropdown in your GUI you will need to know which boxes are available in the current Gradrigo instance. Do you remember the function called from GetBuffer named CheckForParseResponse? Finally, it is time to serve us a purpose.
void GradrigoComponent::CheckForParseResponse()
{
if (m_ParseRequest) {
const auto* response = Gradrigo::GetResponseString(m_ParseRequest.value(), m_Gradrigo);
if (response) {
CORE_LOG(E_Level::Warning, E_Context::Audio, "Gradrigo parsing: {}", response);
}
// here you get simple JSON response. Example of such response later.
const auto* json = Gradrigo::ReportBoxesAsJson(m_Gradrigo);
// reset request id until next parsing
m_ParseRequest.reset();
}
}
Here you will get a JSON response. For simplicity of this already way long tutorial I will leave its parsing and usage here empty.
[
{ "Name": "example" },
{ "Name": "kaboom" },
{ "Name": "pew5times" },
{ "Name": "play_note" },
{ "Name": "set_note_72" },
{ "Name": "set_note_higher" },
{ "Name": "set_note_lower" },
{ "Name": "set_note_random" }
]
Conclusion
It seems like a lot of code to write in order to integrate Gradrigo. But the opposite is actually true. You have to consider, that most of the code is related to FMOD initialization and once you have FMOD in your codebase all you need is just to write callbacks, create a stream and play Gradrigo voices.
More elaborate implementation with all the checks for FMOD API calls validity is to be found in my GitHub repository as part of my engine and you can get Gradrigo from Adams websites.