Add support for IXAudio2VoiceCallback and IXAudio2EngineCallback callbacks
jeremyVignelles opened this issue · comments
Description (optional)
Unless I'm missing something, these structs don't let me plug my own functions : https://github.com/terrafx/terrafx.interop.windows/blob/e8cca83a4a89bc56b7763c65886358fe67ca41e2/sources/Interop/Windows/DirectX/um/xaudio2/IXAudio2VoiceCallback.cs
Rationale
When a buffer is sent to the audio pipeline, it's not played immediately to the audio output but queued, and must be kept in the calling code, until the OnBufferEnd callback is called, where it's safe to remove that buffer.
More info here
Proposed API
var callback = new IXAudio2VoiceCallback {
OnBufferEnd = this.OnBufferEnd
};
var result = this._engine.Get()->CreateSourceVoice(&sourceVoice, &format, pCallback: &callback);
Drawbacks
N/A
Alternatives
Unknown yet.
Other thoughts
Discussions (optional)
TerraFX is designed to be low-level and blittable, basically as close to the underlying C definitions as possible. Due to this, you have to manually define your "COM/Native Callable Wrapper".
There are multiple ways you can do this, but one of the "simpler" ways is the following:
public struct XAudio2VoiceCallback : IXAudio2VoiceCallback.Interface, IDisposable
{
private static readonly IXAudio2VoiceCallback.Vtbl<XAudio2VoiceCallback>* Vtbl = InitVtbl();
private static IXAudio2VoiceCallback.Vtbl<XAudio2VoiceCallback>* InitVtbl()
{
var lpVtbl = (IXAudio2VoiceCallback.Vtbl<XAudio2VoiceCallback>*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(XAudio2VoiceCallback), sizeof(IXAudio2VoiceCallback.Vtbl<XAudio2VoiceCallback>));
lpVtbl->OnVoiceProcessingPassStart = ...;
lpVtbl->OnVoiceProcessingPassEnd = ...;
lpVtbl->OnStreamEnd = ...;
lpVtbl->OnBufferStart = ...;
lpVtbl->OnBufferEnd = &OnBufferEnd;
lpVtbl->OnLoopEnd = ...;
lpVtbl->OnVoiceError = ...;
return lpVtbl;
[UnmanagedCallersOnly]
static void OnBufferEnd(XAudio2VoiceCallback pThis, void* pBufferContext)
{
// Just forward down to the implementation
pThis->OnBufferEnd(pBufferContext);
}
}
public static XAudio2VoiceCallback* Create()
{
// Native needs a pointer and it often needs to persist past the stack frame
return (XAudio2VoiceCallback*)NativeMemory.Alloc((uint)sizeof(XAudio2VoiceCallback));
}
// Field initializers for structs are valid in C# 10
private IXAudio2VoiceCallback.Vtbl<XAudio2VoiceCallback>* lpVtbl = Vtbl;
// Feel free to track other state via fields as appropriate
public void OnBufferEnd(void* pBufferContext)
{
// Actual implementation
}
// Implement rest of interface
public void Dispose()
{
// This is safe under the assumption that all XAudio2VoiceCallback are made via the static Create method
NativeMemory.Free(Unsafe.AsPointer(ref this));
}
}
Thanks for your help, I learned a lot of things.
However, I was unable to use your code because CreateSourceVoice
takes an IXAudioCallback
and not an IXAudioCallback.Interface
. This is what I end up doing:
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
internal static unsafe class XAudio2VoiceCallback
{
private static IXAudio2VoiceCallback* Instance;
private static IXAudio2VoiceCallback.Vtbl<IXAudio2VoiceCallback>* Vtbl;
static XAudio2VoiceCallback()
{
Instance = (IXAudio2VoiceCallback*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(XAudio2VoiceCallback), sizeof(IXAudio2VoiceCallback));
Vtbl = (IXAudio2VoiceCallback.Vtbl<IXAudio2VoiceCallback>*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(XAudio2VoiceCallback), sizeof(IXAudio2VoiceCallback.Vtbl<IXAudio2VoiceCallback>));
Instance->lpVtbl = (void**)Vtbl;
Vtbl->OnVoiceProcessingPassStart = &OnVoiceProcessingPassStart;
Vtbl->OnVoiceProcessingPassEnd = &OnVoiceProcessingPassEnd;
Vtbl->OnStreamEnd = &OnStreamEnd;
Vtbl->OnBufferStart = &OnBufferStart;
Vtbl->OnBufferEnd = &OnBufferEnd;
Vtbl->OnLoopEnd = &OnLoopEnd;
Vtbl->OnVoiceError = &OnVoiceError;
}
public static IXAudio2VoiceCallback* GetCallback()
{
return Instance;
}
[UnmanagedCallersOnly]
private static void OnVoiceProcessingPassStart(IXAudio2VoiceCallback* pThis, uint bytesRequired)
{
}
[UnmanagedCallersOnly]
private static void OnVoiceProcessingPassEnd(IXAudio2VoiceCallback* pThis)
{
}
[UnmanagedCallersOnly]
private static void OnStreamEnd(IXAudio2VoiceCallback* pThis)
{
}
[UnmanagedCallersOnly]
private static void OnBufferStart(IXAudio2VoiceCallback* pThis, void* pBufferContext)
{
}
[UnmanagedCallersOnly]
private static void OnBufferEnd(IXAudio2VoiceCallback* pThis, void* pBufferContext)
{
// Do something with pBufferContext
NativeMemory.Free(pBufferContext);
}
[UnmanagedCallersOnly]
private static void OnLoopEnd(IXAudio2VoiceCallback* pThis, void* pBufferContext)
{
}
[UnmanagedCallersOnly]
private static void OnVoiceError(IXAudio2VoiceCallback* pThis, void* pBufferContext, HRESULT error)
{
}
}
usage:
engine->CreateSourceVoice(sourceVoice, &format, pCallback: XAudio2VoiceCallback.GetCallback())
However, I was unable to use your code because CreateSourceVoice takes an IXAudioCallback and not an IXAudioCallback.Interface. This is what I end up doing:
Right. C# doesn't have struct inheritance and so the IXAudioCallback.Interface
is just a helper to make things like generic code simpler and to help ensure you are matching the correct contract.
The only actual requirement from the C side is that you pass some struct pointer
where the first field is a Vtbl pointer
that matches IXAudioCallback.Vtbl
.
Doing things like (IXAudio2Callback*)XAudio2Callback.Create()
would have worked to get you a valid interface pointer from the struct definition. Your logic works as well, just via a singleton.