I wrote an experimental spatializer plug-in using Steam Audio and got it working on Windows by loading the library dynamically at runtime. I’m a bit rusty/fuzzy on the process so I may not remember everything or get it right, but I’ll offer the insights I did have.
First, I can’t really answer the question about the VC++ redistributable, since I did not need it in my case.
The biggest issue I ran into, is that in order to automatically load and use a dynamically link to a .dll, the .dll either has to exist in a path that Windows will scan for automatically, or in the same path as the executable that is loading it (in the same location as the DAW executable).
I didn’t want to install my plug-in’s needed .dll files to something like C:\Windows\System32, and it’s obviously not feasible to install the plug-in into the DAW’s *.exe folder (let alone all possible DAWs), so the only choice I really had was to load it manually from the plug-in instance itself at runtime from a known location.
In my situation, the Steam Audio library is distributed as a .dll binary, and matching header file. Since I needed to manually load the .dll at runtime, and my solution was to create my own wrapper class around their API. Fortunately, their header file had very consistent syntax, so I was able to write a script that generated most of my wrapper file’s source code for me, otherwise it would have been pretty tedious (my script wrote most of the .h and .cpp to wrap their methods for me, but you can do it by hand).
The 3rd Party API
For example, in their included header file for the Steam Audio API, they might have something like this:
/** Creates a context object. A context must be created before creating any other API objects.
\param settings Pointer to the `IPLContextSettings` struct that specifies context creation parameters.
\param context [out] Handle to the created context object.
\return Status code indicating whether or not the operation succeeded.
*/
typedef IPLerror(IPLCALL* iplContextCreate_api)(IPLContextSettings* settings, IPLContext* context);
Wrapper Class
I created a wrapper class called SteamAudioAPI, in it, I include their original header file with all of their typedefs, structs, enums, etc.. Next for each call in their original API, I create a private declaration of the function being wrapped:
private:
iplContextCreate_api iplContextCreate_func;
And then a public wrapper method on my wrapper class:
public:
IPLerror iplContextCreate(IPLContextSettings* settings, IPLContext* context);
Loading and Binding at Runtime
As I mentioned earlier, I have to manually load and bind phonon.dll at runtime from the JUCE plug-in instance. So my SteamAudioAPI class has methods for this:
/* Gets whether or not the dynamic library has been loaded into memory.*/
bool isLibraryLoaded() { return isLoaded; }
/** Sets the location of the Cymatic Somatic Core library file to load.
* @param path The absolute path to the DLL file to load
*/
void setLibraryPath(juce::String path);
/** Loads the Steam Audio library into memory.
* @param modulePath The file path to the Steam Audio dynamic library.
*/
void load(juce::String modulePath = {});
/* Unloads the Steam Audio library from memory.*/
void unload();
My implementation looks like this:
#include "SteamAudioAPI.h"
#ifdef JUCE_WINDOWS
#include <windows.h>
typedef LPCWSTR OS_STRING;
#else
//typedef std::String OS_STRING;
#endif
namespace CymaticSomatics
{
#ifdef JUCE_WINDOWS
HMODULE steamAudioModule;
LPCWSTR steamAudioModulePath = L"phonon.dll";
#endif
void SteamAudioAPI::setLibraryPath(juce::String path)
{
#ifdef JUCE_WINDOWS
steamAudioModulePath = path.toUTF16();
#endif
}
void SteamAudioAPI::load(juce::String modulePath)
{
isLoaded = false;
#ifdef JUCE_WINDOWS
if (modulePath.isNotEmpty()) steamAudioModulePath = modulePath.toUTF16();
steamAudioModule = LoadLibraryW(steamAudioModulePath);
#endif
if (!steamAudioModule || !bindToAPI()) return;
isLoaded = true;
}
void SteamAudioAPI::unload()
{
if (!isLoaded) return;
#ifdef JUCE_WINDOWS
FreeLibrary(steamAudioModule);
#endif
}
Loading Manually
Specifically, to load a .dll at runtime, use the Windows system call LoadLibraryW()
Unloading Manually
To unload the library manually, use the Windows system call FreeLibrary()
Binding Methods to their Wrappers
OK, so that loads or unloads the .dll at runtime, but how do you actually bind to the methods and call them?
My wrapper class also has a private method bindToAPI() which is called from the load() method:
bool bindToAPI();
It’s implementation looks like this:
bool SteamAudioAPI::bindToAPI()
{
#ifdef JUCE_WINDOWS
iplContextCreate_func = reinterpret_cast<iplContextCreate_api>(GetProcAddress(steamAudioModule, "iplContextCreate"));
if (!iplContextCreate_func) return false;
iplContextRetain_func = reinterpret_cast<iplContextRetain_api>(GetProcAddress(steamAudioModule, "iplContextRetain"));
if (!iplContextRetain_func) return false;
iplContextRelease_func = reinterpret_cast<iplContextRelease_api>(GetProcAddress(steamAudioModule, "iplContextRelease"));
if (!iplContextRelease_func) return false;
/* It's quite long, so I've omitted the rest, but the idea is that if a single
binding fails, the whole binding operation is considered a failure.
*/
#endif
return true;
}
After this method returns true, then all of the publicly exposed methods of the SteamAudioAPI are now bound to the methods in the manually loaded .dll, and just wrap/forward calls to them.
AudioProcessor
In my AudioProcessor class, I have an instance of the SteamAudioAPI class:
private:
// The local Steam Audio API module
SteamAudioAPI steamAudio;
I wrote this as a JUCE module, so I can include it in any JUCE project, and since the module references the original header provided by Steam, all of the basic types are defined so I can work with them directly from the AudioProcessor code.
I manually install the Steam Audio .dll into the same location as my VST3 since I can control that, and have a few utility methods to resolve the path:
// Get the parent directory the processor is executing from
juce::String getPluginDirectoryPath()
{
return File::getSpecialLocation(File::SpecialLocationType::currentExecutableFile).getParentDirectory().getFullPathName();
}
// Gets the Steam Audio dynamic library binary file path
juce::String getSteamAudioPath()
{
return getPluginDirectoryPath() + File::getSeparatorChar() + "phonon.dll";
}
I also have some methods on the AudioProcessor that load/release the API at runtime:
// Creates and initializes the Steam Audio engine
void createSteamAudio();
// Releases the Steam Audio engine
void releaseSteamAudio();
I call createSteamAudio() in the AudioProcessor’s constructor, and call releaseSteamAudio() in the AudioProcessor’s destructor.
Here’s what their implementation looks like:
void CySoSpatializer::createSteamAudio()
{
auto steamAudioFilePath = getSteamAudioPath();
DBG("Loading Steam Audio from: " + steamAudioFilePath);
steamAudio.load(steamAudioFilePath);
if (!steamAudio.isLibraryLoaded())
{
errorLoadingSteamAudio = true;
DBG("Error loading library");
return;
}
else
{
contextSettings.version = STEAMAUDIO_VERSION;
auto error = steamAudio.iplContextCreate(&contextSettings, &context);
hrtfSettings.type = IPL_HRTFTYPE_DEFAULT;
}
}
void CySoSpatializer::releaseSteamAudio()
{
if (!errorLoadingSteamAudio)
{
steamAudio.iplContextRelease(&context);
}
}
That stuff will obviously be specific to the library you are working with, but the main point was to show how I call steamAudio.load() with the path of the .dll, which loads, and binds the API - and the check to make sure it loaded correctly.
Conclusion
I’m not sure ultimately how much this applies to your scenario, but I know I banged my head on this until I figured out a valid solution, so hopefully you can get some insight out of it.