terrafx / terrafx.interop.windows

Interop bindings for Windows.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.