Callbacks from C# without crackling/popping

I’m using FMOD and FMOD Studio from C# without particular issues, but I ended up needing to add a custom DSP callback.

If the callback is written in C#, as expected, I get occasional pops/crackling (from GC stop-the-world pauses) and audio drops out if I pause to break into the debugger, since it suspends all my managed code, as expected.

I solved this on Windows by porting the DSP callback to C++. I can take the DSP callback and pass it to FMOD like this:

// Pin buffer used by DSP callback
hCollectorBuffer = GCHandle.Alloc(_WaveformSamples, GCHandleType.Pinned);
// Get function pointer to DSP callback from C++, in 'callback' local
Collector_Init(hCollectorBuffer.AddrOfPinnedObject(), _WaveformSamples.Length, out var callback);
// Create delegate for the C++ DSP callback to pass to FMOD via marshaling
CallbackDelegate = Marshal.GetDelegateForFunctionPointer<DSP_READ_CALLBACK>(callback);
// Maintain a handle to the delegate so it's not collected
hCallbackDelegate = GCHandle.Alloc(CallbackDelegate);

// Populate DSP description
var nameBytes = new byte[32];
var name = "Collector\0";
Encoding.UTF8.GetBytes(name, 0, name.Length, nameBytes, 0);
var dspDesc = new DSP_DESCRIPTION {
    name = nameBytes,
    // This part sucks. We're round-tripping the C++ DSP callback through a Delegate wrapper.
    read = CallbackDelegate,
    numinputbuffers = 1,
    numoutputbuffers = 0
};
// Create DSP from description
AssertOk(FMod.System.createDSP(ref dspDesc, out FMod.CollectorDsp));

However, when running under Mono (on Steam Deck), there’s a problem: C++ function pointers don’t round-trip through delegates there. A managed wrapper gets passed to FMod, so the audio crackles/pops a lot on Steam Deck. I’ve filed an issue report about it so it may eventually get fixed in mono, but I was curious whether anyone had suggestions for a clean workaround.

Right now the workaround I’ve come up with is to define my own version of DSP_DESCRIPTION that uses IntPtr instead of delegates, and that seems to work. But it means if DSP_DESCRIPTION or createDSP ever change I’ll need to manually update my custom pinvoke, which isn’t great for maintenance:


            [DllImport(FMOD.VERSION.dll)]
            public static extern RESULT FMOD5_System_CreateDSP (IntPtr system, ref DSP_DESCRIPTION_P description, out IntPtr dsp);

            [StructLayout(LayoutKind.Sequential)]
            public struct DSP_DESCRIPTION_P
            {
                public uint                           pluginsdkversion;
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
                public byte[]                         name;
                public uint                           version;
                public int                            numinputbuffers;
                public int                            numoutputbuffers;
                public IntPtr                         create;
                public IntPtr                         release;
                public IntPtr                         reset;
                public IntPtr                         read;
                public IntPtr                         process;
                public IntPtr                         setposition;

                public int                            numparameters;
                public IntPtr                         paramdesc;
                public IntPtr                         setparameterfloat;
                public IntPtr                         setparameterint;
                public IntPtr                         setparameterbool;
                public IntPtr                         setparameterdata;
                public IntPtr                         getparameterfloat;
                public IntPtr                         getparameterint;
                public IntPtr                         getparameterbool;
                public IntPtr                         getparameterdata;
                public IntPtr                         shouldiprocess;
                public IntPtr                         userdata;

                public IntPtr                         sys_register;
                public IntPtr                         sys_deregister;
                public IntPtr                         sys_mix;
            }

It seems like this would probably be an issue for Unity developers as well but I couldn’t find any existing threads discussing this problem. Maybe IL2CPP is immune to this issue and most Unity developers are using that instead of Mono?

I’m not sure about the occasional pops/crackling, this isn’t something we have encountered but I’ll write up a task to investigate this further.

Are you able to try a DllImport instead of getting the callback via a function call? We have tested and confirmed that DllImported functions passed to FMOD as delegates come through as raw function pointers when using Mono.

It looks like I started with this approach first, hoping that it would solve the crackling problems for me. If DllImport works that’s great to know and I will try it out.