[PATCH] mmdevapi: Implement SpatialAudio features

Arkadiusz Hiler ahiler at codeweavers.com
Mon Feb 1 08:56:38 CST 2021


From: Andrew Eikum <aeikum at codeweavers.com>

This makes car radio / ambient noises audible in Cyberpunk 2077.

Signed-off-by: Arkadiusz Hiler <ahiler at codeweavers.com>
---
 dlls/mmdevapi/Makefile.in          |   3 +-
 dlls/mmdevapi/audiovolume.c        |   1 +
 dlls/mmdevapi/devenum.c            |   5 +
 dlls/mmdevapi/main.c               |   1 +
 dlls/mmdevapi/mmdevapi.h           |   1 +
 dlls/mmdevapi/spatialaudio.c       | 980 +++++++++++++++++++++++++++++
 dlls/mmdevapi/tests/Makefile.in    |   3 +-
 dlls/mmdevapi/tests/mmdevenum.c    |   1 +
 dlls/mmdevapi/tests/spatialaudio.c | 441 +++++++++++++
 include/spatialaudioclient.idl     | 121 ++++
 10 files changed, 1555 insertions(+), 2 deletions(-)
 create mode 100644 dlls/mmdevapi/spatialaudio.c
 create mode 100644 dlls/mmdevapi/tests/spatialaudio.c

diff --git a/dlls/mmdevapi/Makefile.in b/dlls/mmdevapi/Makefile.in
index 5f44f7ba53b..903b14335a9 100644
--- a/dlls/mmdevapi/Makefile.in
+++ b/dlls/mmdevapi/Makefile.in
@@ -6,6 +6,7 @@ EXTRADLLFLAGS = -mno-cygwin
 C_SRCS = \
 	audiovolume.c \
 	devenum.c \
-	main.c
+	main.c \
+	spatialaudio.c
 
 IDL_SRCS = mmdevapi_classes.idl
diff --git a/dlls/mmdevapi/audiovolume.c b/dlls/mmdevapi/audiovolume.c
index 9214980120e..6f403cf348a 100644
--- a/dlls/mmdevapi/audiovolume.c
+++ b/dlls/mmdevapi/audiovolume.c
@@ -33,6 +33,7 @@
 #include "audioclient.h"
 #include "endpointvolume.h"
 #include "audiopolicy.h"
+#include "spatialaudioclient.h"
 
 #include "mmdevapi.h"
 
diff --git a/dlls/mmdevapi/devenum.c b/dlls/mmdevapi/devenum.c
index 9e4a29816ce..07b4dca028b 100644
--- a/dlls/mmdevapi/devenum.c
+++ b/dlls/mmdevapi/devenum.c
@@ -35,6 +35,7 @@
 #include "audioclient.h"
 #include "endpointvolume.h"
 #include "audiopolicy.h"
+#include "spatialaudioclient.h"
 
 #include "mmdevapi.h"
 #include "devpkey.h"
@@ -635,6 +636,10 @@ static HRESULT WINAPI MMDevice_Activate(IMMDevice *iface, REFIID riid, DWORD cls
                 IDirectSoundCapture_Release((IDirectSoundCapture*)*ppv);
         }
     }
+    else if (IsEqualIID(riid, &IID_ISpatialAudioClient))
+    {
+        hr = SpatialAudioClient_Create(iface, (ISpatialAudioClient**)ppv);
+    }
     else
         ERR("Invalid/unknown iid %s\n", debugstr_guid(riid));
 
diff --git a/dlls/mmdevapi/main.c b/dlls/mmdevapi/main.c
index 247ebc3b001..eac1da28f18 100644
--- a/dlls/mmdevapi/main.c
+++ b/dlls/mmdevapi/main.c
@@ -37,6 +37,7 @@
 #include "audiopolicy.h"
 #include "devpkey.h"
 #include "winreg.h"
+#include "spatialaudioclient.h"
 
 #include "mmdevapi.h"
 #include "wine/debug.h"
diff --git a/dlls/mmdevapi/mmdevapi.h b/dlls/mmdevapi/mmdevapi.h
index bc9788e95c8..3bcf568cddf 100644
--- a/dlls/mmdevapi/mmdevapi.h
+++ b/dlls/mmdevapi/mmdevapi.h
@@ -71,5 +71,6 @@ typedef struct MMDevice {
 
 extern HRESULT AudioClient_Create(MMDevice *parent, IAudioClient **ppv) DECLSPEC_HIDDEN;
 extern HRESULT AudioEndpointVolume_Create(MMDevice *parent, IAudioEndpointVolumeEx **ppv) DECLSPEC_HIDDEN;
+extern HRESULT SpatialAudioClient_Create(IMMDevice *device, ISpatialAudioClient **out) DECLSPEC_HIDDEN;
 
 extern const WCHAR drv_keyW[] DECLSPEC_HIDDEN;
diff --git a/dlls/mmdevapi/spatialaudio.c b/dlls/mmdevapi/spatialaudio.c
new file mode 100644
index 00000000000..c45acb7fec2
--- /dev/null
+++ b/dlls/mmdevapi/spatialaudio.c
@@ -0,0 +1,980 @@
+/*
+ * Copyright 2020 Andrew Eikum for CodeWeavers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#define COBJMACROS
+#define NONAMELESSUNION
+
+#include <stdarg.h>
+
+#include "windef.h"
+#include "winbase.h"
+#include "winnls.h"
+#include "winreg.h"
+#include "wine/heap.h"
+#include "wine/debug.h"
+#include "wine/list.h"
+
+#include "ole2.h"
+#include "mmdeviceapi.h"
+#include "mmsystem.h"
+#include "audioclient.h"
+#include "endpointvolume.h"
+#include "audiopolicy.h"
+#include "spatialaudioclient.h"
+
+#include "mmdevapi.h"
+
+WINE_DEFAULT_DEBUG_CHANNEL(mmdevapi);
+
+#define MAX_PERIODS 3
+
+static UINT32 AudioObjectType_to_index(AudioObjectType type)
+{
+    UINT32 o = 0;
+    while(type){
+        type >>= 1;
+        ++o;
+    }
+    return o - 2;
+}
+
+typedef struct SpatialAudioImpl SpatialAudioImpl;
+typedef struct SpatialAudioStreamImpl SpatialAudioStreamImpl;
+typedef struct SpatialAudioObjectImpl SpatialAudioObjectImpl;
+
+struct SpatialAudioObjectImpl {
+    ISpatialAudioObject ISpatialAudioObject_iface;
+    LONG ref;
+
+    SpatialAudioStreamImpl *sa_stream;
+    AudioObjectType type;
+    UINT32 static_idx;
+
+    float *buf;
+
+    struct list entry;
+};
+
+struct SpatialAudioStreamImpl {
+    ISpatialAudioObjectRenderStream ISpatialAudioObjectRenderStream_iface;
+    LONG ref;
+    CRITICAL_SECTION lock;
+
+    SpatialAudioImpl *sa_client;
+    SpatialAudioObjectRenderStreamActivationParams params;
+
+    IAudioClient *client;
+    IAudioRenderClient *render;
+
+    UINT32 period_frames, update_frames;
+    WAVEFORMATEXTENSIBLE stream_fmtex;
+
+    float *buf;
+
+    UINT32 static_object_map[17];
+
+    struct list objects;
+};
+
+struct SpatialAudioImpl {
+    ISpatialAudioClient ISpatialAudioClient_iface;
+    IAudioFormatEnumerator IAudioFormatEnumerator_iface;
+    IMMDevice *mmdev;
+    LONG ref;
+    WAVEFORMATEXTENSIBLE object_fmtex;
+};
+
+static inline SpatialAudioObjectImpl *impl_from_ISpatialAudioObject(ISpatialAudioObject *iface)
+{
+    return CONTAINING_RECORD(iface, SpatialAudioObjectImpl, ISpatialAudioObject_iface);
+}
+
+static inline SpatialAudioStreamImpl *impl_from_ISpatialAudioObjectRenderStream(ISpatialAudioObjectRenderStream *iface)
+{
+    return CONTAINING_RECORD(iface, SpatialAudioStreamImpl, ISpatialAudioObjectRenderStream_iface);
+}
+
+static inline SpatialAudioImpl *impl_from_ISpatialAudioClient(ISpatialAudioClient *iface)
+{
+    return CONTAINING_RECORD(iface, SpatialAudioImpl, ISpatialAudioClient_iface);
+}
+
+static inline SpatialAudioImpl *impl_from_IAudioFormatEnumerator(IAudioFormatEnumerator *iface)
+{
+    return CONTAINING_RECORD(iface, SpatialAudioImpl, IAudioFormatEnumerator_iface);
+}
+
+static HRESULT WINAPI SAO_QueryInterface(ISpatialAudioObject *iface,
+        REFIID riid, void **ppv)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+
+    TRACE("(%p)->(%s,%p)\n", This, debugstr_guid(riid), ppv);
+
+    if (!ppv)
+        return E_POINTER;
+
+    *ppv = NULL;
+
+    if (IsEqualIID(riid, &IID_IUnknown) ||
+            IsEqualIID(riid, &IID_ISpatialAudioObjectBase) ||
+            IsEqualIID(riid, &IID_ISpatialAudioObject)) {
+        *ppv = &This->ISpatialAudioObject_iface;
+    }
+    else
+        return E_NOINTERFACE;
+
+    IUnknown_AddRef((IUnknown *)*ppv);
+
+    return S_OK;
+}
+
+static ULONG WINAPI SAO_AddRef(ISpatialAudioObject *iface)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+    ULONG ref = InterlockedIncrement(&This->ref);
+    TRACE("(%p) new ref %u\n", This, ref);
+    return ref;
+}
+
+static ULONG WINAPI SAO_Release(ISpatialAudioObject *iface)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+    ULONG ref = InterlockedDecrement(&This->ref);
+    TRACE("(%p) new ref %u\n", This, ref);
+    if(!ref){
+        EnterCriticalSection(&This->sa_stream->lock);
+        list_remove(&This->entry);
+        LeaveCriticalSection(&This->sa_stream->lock);
+
+        ISpatialAudioObjectRenderStream_Release(&This->sa_stream->ISpatialAudioObjectRenderStream_iface);
+        heap_free(This->buf);
+        heap_free(This);
+    }
+    return ref;
+}
+
+static HRESULT WINAPI SAO_GetBuffer(ISpatialAudioObject *iface,
+        BYTE **buffer, UINT32 *bytes)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+
+    TRACE("(%p)->(%p, %p)\n", This, buffer, bytes);
+
+    EnterCriticalSection(&This->sa_stream->lock);
+
+    if(This->sa_stream->update_frames == ~0){
+        LeaveCriticalSection(&This->sa_stream->lock);
+        return SPTLAUDCLNT_E_OUT_OF_ORDER;
+    }
+
+    *buffer = (BYTE *)This->buf;
+    *bytes = This->sa_stream->update_frames *
+        This->sa_stream->sa_client->object_fmtex.Format.nBlockAlign;
+
+    LeaveCriticalSection(&This->sa_stream->lock);
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAO_SetEndOfStream(ISpatialAudioObject *iface, UINT32 frames)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+    FIXME("(%p)->(%u)\n", This, frames);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAO_IsActive(ISpatialAudioObject *iface, BOOL *active)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+    FIXME("(%p)->(%p)\n", This, active);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAO_GetAudioObjectType(ISpatialAudioObject *iface,
+        AudioObjectType *type)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+
+    TRACE("(%p)->(%p)\n", This, type);
+
+    *type = This->type;
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAO_SetPosition(ISpatialAudioObject *iface, float x,
+        float y, float z)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+    FIXME("(%p)->(%f, %f, %f)\n", This, x, y, z);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAO_SetVolume(ISpatialAudioObject *iface, float vol)
+{
+    SpatialAudioObjectImpl *This = impl_from_ISpatialAudioObject(iface);
+    FIXME("(%p)->(%f)\n", This, vol);
+    return E_NOTIMPL;
+}
+
+static ISpatialAudioObjectVtbl ISpatialAudioObject_vtbl = {
+    SAO_QueryInterface,
+    SAO_AddRef,
+    SAO_Release,
+    SAO_GetBuffer,
+    SAO_SetEndOfStream,
+    SAO_IsActive,
+    SAO_GetAudioObjectType,
+    SAO_SetPosition,
+    SAO_SetVolume,
+};
+
+static HRESULT WINAPI SAORS_QueryInterface(ISpatialAudioObjectRenderStream *iface,
+        REFIID riid, void **ppv)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+
+    TRACE("(%p)->(%s,%p)\n", This, debugstr_guid(riid), ppv);
+
+    if (!ppv)
+        return E_POINTER;
+
+    *ppv = NULL;
+
+    if (IsEqualIID(riid, &IID_IUnknown) ||
+            IsEqualIID(riid, &IID_ISpatialAudioObjectRenderStreamBase) ||
+            IsEqualIID(riid, &IID_ISpatialAudioObjectRenderStream)) {
+        *ppv = &This->ISpatialAudioObjectRenderStream_iface;
+    }
+    else
+        return E_NOINTERFACE;
+
+    IUnknown_AddRef((IUnknown *)*ppv);
+
+    return S_OK;
+}
+
+static ULONG WINAPI SAORS_AddRef(ISpatialAudioObjectRenderStream *iface)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    ULONG ref = InterlockedIncrement(&This->ref);
+    TRACE("(%p) new ref %u\n", This, ref);
+    return ref;
+}
+
+static ULONG WINAPI SAORS_Release(ISpatialAudioObjectRenderStream *iface)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    ULONG ref = InterlockedDecrement(&This->ref);
+    TRACE("(%p) new ref %u\n", This, ref);
+    if(!ref){
+        IAudioClient_Stop(This->client);
+        if(This->update_frames != ~0 && This->update_frames > 0)
+            IAudioRenderClient_ReleaseBuffer(This->render, This->update_frames, 0);
+        IAudioRenderClient_Release(This->render);
+        IAudioClient_Release(This->client);
+        if(This->params.NotifyObject)
+            ISpatialAudioObjectRenderStreamNotify_Release(This->params.NotifyObject);
+        heap_free((void*)This->params.ObjectFormat);
+        CloseHandle(This->params.EventHandle);
+        DeleteCriticalSection(&This->lock);
+        ISpatialAudioClient_Release(&This->sa_client->ISpatialAudioClient_iface);
+        heap_free(This);
+    }
+    return ref;
+}
+
+static HRESULT WINAPI SAORS_GetAvailableDynamicObjectCount(
+        ISpatialAudioObjectRenderStream *iface, UINT32 *count)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    FIXME("(%p)->(%p)\n", This, count);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAORS_GetService(ISpatialAudioObjectRenderStream *iface,
+        REFIID riid, void **service)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    FIXME("(%p)->(%s, %p)\n", This, debugstr_guid(riid), service);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAORS_Start(ISpatialAudioObjectRenderStream *iface)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    HRESULT hr;
+
+    TRACE("(%p)->()\n", This);
+
+    hr = IAudioClient_Start(This->client);
+    if(FAILED(hr)){
+        WARN("IAudioClient::Start failed: %08x\n", hr);
+        return hr;
+    }
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAORS_Stop(ISpatialAudioObjectRenderStream *iface)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    HRESULT hr;
+
+    TRACE("(%p)->()\n", This);
+
+    hr = IAudioClient_Stop(This->client);
+    if(FAILED(hr)){
+        WARN("IAudioClient::Stop failed: %08x\n", hr);
+        return hr;
+    }
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAORS_Reset(ISpatialAudioObjectRenderStream *iface)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    FIXME("(%p)->()\n", This);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAORS_BeginUpdatingAudioObjects(ISpatialAudioObjectRenderStream *iface,
+        UINT32 *dyn_count, UINT32 *frames)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    SpatialAudioObjectImpl *object;
+    UINT32 pad;
+    HRESULT hr;
+
+    TRACE("(%p)->(%p, %p)\n", This, dyn_count, frames);
+
+    EnterCriticalSection(&This->lock);
+
+    if(This->update_frames != ~0){
+        LeaveCriticalSection(&This->lock);
+        return SPTLAUDCLNT_E_OUT_OF_ORDER;
+    }
+
+    hr = IAudioClient_GetCurrentPadding(This->client, &pad);
+    if(FAILED(hr)){
+        WARN("GetCurrentPadding failed: %08x\n", hr);
+        LeaveCriticalSection(&This->lock);
+        return hr;
+    }
+
+    if(pad < This->period_frames * MAX_PERIODS){
+        This->update_frames = This->period_frames * MAX_PERIODS - pad;
+    }else{
+        This->update_frames = 0;
+    }
+
+    if(This->update_frames > 0){
+        hr = IAudioRenderClient_GetBuffer(This->render, This->update_frames, (BYTE **)&This->buf);
+        if(FAILED(hr)){
+            WARN("GetBuffer failed: %08x\n", hr);
+            This->update_frames = ~0;
+            LeaveCriticalSection(&This->lock);
+            return hr;
+        }
+
+        LIST_FOR_EACH_ENTRY(object, &This->objects, SpatialAudioObjectImpl, entry){
+            memset(object->buf, 0, This->update_frames * This->sa_client->object_fmtex.Format.nBlockAlign);
+        }
+    }
+
+    *dyn_count = 0;
+    *frames = This->update_frames;
+
+    LeaveCriticalSection(&This->lock);
+
+    return S_OK;
+}
+
+static void mix_static_object(SpatialAudioStreamImpl *stream, SpatialAudioObjectImpl *object)
+{
+    float *in = object->buf, *out;
+    UINT32 i;
+    if(object->static_idx == ~0 ||
+            stream->static_object_map[object->static_idx] == ~0){
+        WARN("Got unmapped static object?! Not mixing. Type: 0x%x\n", object->type);
+        return;
+    }
+    out = stream->buf + stream->static_object_map[object->static_idx];
+    for(i = 0; i < stream->update_frames; ++i){
+        *out += *in;
+        ++in;
+        out += stream->stream_fmtex.Format.nChannels;
+    }
+}
+
+static HRESULT WINAPI SAORS_EndUpdatingAudioObjects(ISpatialAudioObjectRenderStream *iface)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    SpatialAudioObjectImpl *object;
+    HRESULT hr;
+
+    TRACE("(%p)->()\n", This);
+
+    EnterCriticalSection(&This->lock);
+
+    if(This->update_frames == ~0){
+        LeaveCriticalSection(&This->lock);
+        return SPTLAUDCLNT_E_OUT_OF_ORDER;
+    }
+
+    if(This->update_frames > 0){
+        LIST_FOR_EACH_ENTRY(object, &This->objects, SpatialAudioObjectImpl, entry){
+            if(object->type != AudioObjectType_Dynamic)
+                mix_static_object(This, object);
+            else
+                WARN("Don't know how to mix dynamic object yet. %p\n", object);
+        }
+
+        hr = IAudioRenderClient_ReleaseBuffer(This->render, This->update_frames, 0);
+        if(FAILED(hr))
+            WARN("ReleaseBuffer failed: %08x\n", hr);
+    }
+
+    This->update_frames = ~0;
+
+    LeaveCriticalSection(&This->lock);
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAORS_ActivateSpatialAudioObject(ISpatialAudioObjectRenderStream *iface,
+        AudioObjectType type, ISpatialAudioObject **object)
+{
+    SpatialAudioStreamImpl *This = impl_from_ISpatialAudioObjectRenderStream(iface);
+    SpatialAudioObjectImpl *obj;
+
+    TRACE("(%p)->(0x%x, %p)\n", This, type, object);
+
+    if(type == AudioObjectType_Dynamic)
+        return SPTLAUDCLNT_E_NO_MORE_OBJECTS;
+
+    if(type & ~This->params.StaticObjectTypeMask)
+        return SPTLAUDCLNT_E_STATIC_OBJECT_NOT_AVAILABLE;
+
+    LIST_FOR_EACH_ENTRY(obj, &This->objects, SpatialAudioObjectImpl, entry){
+        if(obj->static_idx == AudioObjectType_to_index(type))
+            return SPTLAUDCLNT_E_OBJECT_ALREADY_ACTIVE;
+    }
+
+    obj = heap_alloc_zero(sizeof(*obj));
+    obj->ISpatialAudioObject_iface.lpVtbl = &ISpatialAudioObject_vtbl;
+    obj->ref = 1;
+    obj->type = type;
+    if(type == AudioObjectType_None){
+        FIXME("AudioObjectType_None not implemented yet!\n");
+        obj->static_idx = ~0;
+    }else{
+        obj->static_idx = AudioObjectType_to_index(type);
+    }
+
+    obj->sa_stream = This;
+    SAORS_AddRef(&This->ISpatialAudioObjectRenderStream_iface);
+
+    obj->buf = heap_alloc_zero(This->period_frames * MAX_PERIODS * This->sa_client->object_fmtex.Format.nBlockAlign);
+
+    EnterCriticalSection(&This->lock);
+
+    list_add_tail(&This->objects, &obj->entry);
+
+    LeaveCriticalSection(&This->lock);
+
+    *object = &obj->ISpatialAudioObject_iface;
+
+    return S_OK;
+}
+
+static ISpatialAudioObjectRenderStreamVtbl ISpatialAudioObjectRenderStream_vtbl = {
+    SAORS_QueryInterface,
+    SAORS_AddRef,
+    SAORS_Release,
+    SAORS_GetAvailableDynamicObjectCount,
+    SAORS_GetService,
+    SAORS_Start,
+    SAORS_Stop,
+    SAORS_Reset,
+    SAORS_BeginUpdatingAudioObjects,
+    SAORS_EndUpdatingAudioObjects,
+    SAORS_ActivateSpatialAudioObject,
+};
+
+static HRESULT WINAPI SAC_QueryInterface(ISpatialAudioClient *iface, REFIID riid, void **ppv)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+
+    TRACE("(%p)->(%s,%p)\n", This, debugstr_guid(riid), ppv);
+
+    if (!ppv)
+        return E_POINTER;
+
+    *ppv = NULL;
+
+    if (IsEqualIID(riid, &IID_IUnknown) ||
+            IsEqualIID(riid, &IID_ISpatialAudioClient)) {
+        *ppv = &This->ISpatialAudioClient_iface;
+    }
+    else
+        return E_NOINTERFACE;
+
+    IUnknown_AddRef((IUnknown *)*ppv);
+
+    return S_OK;
+}
+
+static ULONG WINAPI SAC_AddRef(ISpatialAudioClient *iface)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    ULONG ref = InterlockedIncrement(&This->ref);
+    TRACE("(%p) new ref %u\n", This, ref);
+    return ref;
+}
+
+static ULONG WINAPI SAC_Release(ISpatialAudioClient *iface)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    ULONG ref = InterlockedDecrement(&This->ref);
+    TRACE("(%p) new ref %u\n", This, ref);
+    if (!ref) {
+        IMMDevice_Release(This->mmdev);
+        heap_free(This);
+    }
+    return ref;
+}
+
+static HRESULT WINAPI SAC_GetStaticObjectPosition(ISpatialAudioClient *iface,
+        AudioObjectType type, float *x, float *y, float *z)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    FIXME("(%p)->(0x%x, %p, %p, %p)\n", This, type, x, y, z);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAC_GetNativeStaticObjectTypeMask(ISpatialAudioClient *iface,
+        AudioObjectType *mask)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    FIXME("(%p)->(%p)\n", This, mask);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAC_GetMaxDynamicObjectCount(ISpatialAudioClient *iface,
+        UINT32 *value)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    FIXME("(%p)->(%p)\n", This, value);
+
+    *value = 0;
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAC_GetSupportedAudioObjectFormatEnumerator(
+        ISpatialAudioClient *iface, IAudioFormatEnumerator **enumerator)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+
+    TRACE("(%p)->(%p)\n", This, enumerator);
+
+    *enumerator = &This->IAudioFormatEnumerator_iface;
+    SAC_AddRef(iface);
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAC_GetMaxFrameCount(ISpatialAudioClient *iface,
+        const WAVEFORMATEX *format, UINT32 *count)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+
+    /* FIXME: should get device period from the device */
+    static const REFERENCE_TIME period = 100000;
+
+    TRACE("(%p)->(%p, %p)\n", This, format, count);
+
+    *count = MulDiv(period, format->nSamplesPerSec, 10000000) * MAX_PERIODS;
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAC_IsAudioObjectFormatSupported(ISpatialAudioClient *iface,
+        const WAVEFORMATEX *format)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    FIXME("(%p)->(%p)\n", This, format);
+    return E_NOTIMPL;
+}
+
+static HRESULT WINAPI SAC_IsSpatialAudioStreamAvailable(ISpatialAudioClient *iface,
+        REFIID stream_uuid, const PROPVARIANT *info)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    FIXME("(%p)->(%s, %p)\n", This, debugstr_guid(stream_uuid), info);
+    return E_NOTIMPL;
+}
+
+static WAVEFORMATEX *clone_fmtex(const WAVEFORMATEX *src)
+{
+    WAVEFORMATEX *r = heap_alloc(sizeof(WAVEFORMATEX) + src->cbSize);
+    memcpy(r, src, sizeof(WAVEFORMATEX) + src->cbSize);
+    return r;
+}
+
+static const char *debugstr_fmtex(const WAVEFORMATEX *fmt)
+{
+    static char buf[2048];
+    if(fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE){
+        const WAVEFORMATEXTENSIBLE *fmtex = (const WAVEFORMATEXTENSIBLE *)fmt;
+        snprintf(buf, sizeof(buf), "tag: 0x%x (%s), ch: %u (mask: 0x%x), rate: %u, depth: %u",
+                fmt->wFormatTag, debugstr_guid(&fmtex->SubFormat),
+                fmt->nChannels, fmtex->dwChannelMask, fmt->nSamplesPerSec,
+                fmt->wBitsPerSample);
+    }else{
+        snprintf(buf, sizeof(buf), "tag: 0x%x, ch: %u, rate: %u, depth: %u",
+                fmt->wFormatTag, fmt->nChannels, fmt->nSamplesPerSec,
+                fmt->wBitsPerSample);
+    }
+    return buf;
+}
+
+static void static_mask_to_channels(AudioObjectType static_mask, WORD *count, DWORD *mask, UINT32 *map)
+{
+    UINT32 out_chan = 0, map_idx = 0;
+    *count = 0;
+    *mask = 0;
+#define CONVERT_MASK(f, t) \
+    if(static_mask & f){ \
+        *count += 1; \
+        *mask |= t; \
+        map[map_idx++] = out_chan++; \
+        TRACE("mapping 0x%x to %u\n", f, out_chan - 1); \
+    }else{ \
+        map[map_idx++] = ~0; \
+    }
+    CONVERT_MASK(AudioObjectType_FrontLeft, SPEAKER_FRONT_LEFT);
+    CONVERT_MASK(AudioObjectType_FrontRight, SPEAKER_FRONT_RIGHT);
+    CONVERT_MASK(AudioObjectType_FrontCenter, SPEAKER_FRONT_CENTER);
+    CONVERT_MASK(AudioObjectType_LowFrequency, SPEAKER_LOW_FREQUENCY);
+    CONVERT_MASK(AudioObjectType_SideLeft, SPEAKER_SIDE_LEFT);
+    CONVERT_MASK(AudioObjectType_SideRight, SPEAKER_SIDE_RIGHT);
+    CONVERT_MASK(AudioObjectType_BackLeft, SPEAKER_BACK_LEFT);
+    CONVERT_MASK(AudioObjectType_BackRight, SPEAKER_BACK_RIGHT);
+    CONVERT_MASK(AudioObjectType_TopFrontLeft, SPEAKER_TOP_FRONT_LEFT);
+    CONVERT_MASK(AudioObjectType_TopFrontRight, SPEAKER_TOP_FRONT_RIGHT);
+    CONVERT_MASK(AudioObjectType_TopBackLeft, SPEAKER_TOP_BACK_LEFT);
+    CONVERT_MASK(AudioObjectType_TopBackRight, SPEAKER_TOP_BACK_RIGHT);
+    CONVERT_MASK(AudioObjectType_BackCenter, SPEAKER_BACK_CENTER);
+}
+
+static HRESULT activate_stream(SpatialAudioStreamImpl *stream)
+{
+    WAVEFORMATEXTENSIBLE *object_fmtex = (WAVEFORMATEXTENSIBLE *)stream->params.ObjectFormat;
+    HRESULT hr;
+    REFERENCE_TIME period;
+
+    if(!(object_fmtex->Format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
+                (object_fmtex->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
+                 IsEqualGUID(&object_fmtex->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)))){
+        FIXME("Only float formats are supported for now\n");
+        return E_INVALIDARG;
+    }
+
+    hr = IMMDevice_Activate(stream->sa_client->mmdev, &IID_IAudioClient,
+            CLSCTX_INPROC_SERVER, NULL, (void**)&stream->client);
+    if(FAILED(hr)){
+        WARN("Activate failed: %08x\n", hr);
+        return hr;
+    }
+
+    hr = IAudioClient_GetDevicePeriod(stream->client, &period, NULL);
+    if(FAILED(hr)){
+        WARN("GetDevicePeriod failed: %08x\n", hr);
+        IAudioClient_Release(stream->client);
+        return hr;
+    }
+
+    stream->stream_fmtex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+    static_mask_to_channels(stream->params.StaticObjectTypeMask,
+            &stream->stream_fmtex.Format.nChannels, &stream->stream_fmtex.dwChannelMask,
+            stream->static_object_map);
+    stream->stream_fmtex.Format.nSamplesPerSec = stream->params.ObjectFormat->nSamplesPerSec;
+    stream->stream_fmtex.Format.wBitsPerSample = stream->params.ObjectFormat->wBitsPerSample;
+    stream->stream_fmtex.Format.nBlockAlign = (stream->stream_fmtex.Format.nChannels * stream->stream_fmtex.Format.wBitsPerSample) / 8;
+    stream->stream_fmtex.Format.nAvgBytesPerSec = stream->stream_fmtex.Format.nSamplesPerSec * stream->stream_fmtex.Format.nBlockAlign;
+    stream->stream_fmtex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+    stream->stream_fmtex.Samples.wValidBitsPerSample = stream->stream_fmtex.Format.wBitsPerSample;
+    stream->stream_fmtex.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+
+    hr = IAudioClient_Initialize(stream->client, AUDCLNT_SHAREMODE_SHARED,
+            AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST,
+            period * MAX_PERIODS, 0, &stream->stream_fmtex.Format, NULL);
+    if(FAILED(hr)){
+        WARN("Initialize failed: %08x\n", hr);
+        IAudioClient_Release(stream->client);
+        return hr;
+    }
+
+    /* XXX: OK that this is the user's handle? */
+    hr = IAudioClient_SetEventHandle(stream->client, stream->params.EventHandle);
+    if(FAILED(hr)){
+        WARN("SetEventHandle failed: %08x\n", hr);
+        IAudioClient_Release(stream->client);
+        return hr;
+    }
+
+    hr = IAudioClient_GetService(stream->client, &IID_IAudioRenderClient, (void**)&stream->render);
+    if(FAILED(hr)){
+        WARN("GetService(AudioRenderClient) failed: %08x\n", hr);
+        IAudioClient_Release(stream->client);
+        return hr;
+    }
+
+    stream->period_frames = MulDiv(period, stream->stream_fmtex.Format.nSamplesPerSec, 10000000);
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAC_ActivateSpatialAudioStream(ISpatialAudioClient *iface,
+        const PROPVARIANT *prop, REFIID riid, void **stream)
+{
+    SpatialAudioImpl *This = impl_from_ISpatialAudioClient(iface);
+    SpatialAudioObjectRenderStreamActivationParams *params;
+    HRESULT hr;
+
+    TRACE("(%p)->(%s, %p)\n", This, debugstr_guid(riid), stream);
+
+    if(IsEqualIID(riid, &IID_ISpatialAudioObjectRenderStream)){
+        SpatialAudioStreamImpl *obj;
+
+        if(prop &&
+                (prop->vt != VT_BLOB ||
+                 prop->u.blob.cbSize != sizeof(SpatialAudioObjectRenderStreamActivationParams))){
+            WARN("Got invalid params\n");
+            *stream = NULL;
+            return E_INVALIDARG;
+        }
+
+        params = (SpatialAudioObjectRenderStreamActivationParams*) prop->u.blob.pBlobData;
+
+        if(params->StaticObjectTypeMask & AudioObjectType_Dynamic){
+            *stream = NULL;
+            return E_INVALIDARG;
+        }
+
+        if(!params->ObjectFormat || memcmp(params->ObjectFormat, &This->object_fmtex.Format, sizeof(*params->ObjectFormat) + params->ObjectFormat->cbSize)) {
+            *stream = NULL;
+            return AUDCLNT_E_UNSUPPORTED_FORMAT;
+        }
+
+        obj = heap_alloc_zero(sizeof(SpatialAudioStreamImpl));
+
+        obj->ISpatialAudioObjectRenderStream_iface.lpVtbl = &ISpatialAudioObjectRenderStream_vtbl;
+        obj->ref = 1;
+        memcpy(&obj->params, params, sizeof(obj->params));
+
+        obj->update_frames = ~0;
+
+        InitializeCriticalSection(&obj->lock);
+        list_init(&obj->objects);
+
+        obj->sa_client = This;
+        SAC_AddRef(&This->ISpatialAudioClient_iface);
+
+        obj->params.ObjectFormat = clone_fmtex(obj->params.ObjectFormat);
+
+        if(obj->params.EventHandle != INVALID_HANDLE_VALUE &&
+                obj->params.EventHandle != 0)
+            DuplicateHandle(GetCurrentProcess(), obj->params.EventHandle,
+                    GetCurrentProcess(), &obj->params.EventHandle, 0, FALSE,
+                    DUPLICATE_SAME_ACCESS);
+
+        if(obj->params.NotifyObject)
+            ISpatialAudioObjectRenderStreamNotify_AddRef(obj->params.NotifyObject);
+
+        if(TRACE_ON(mmdevapi)){
+            TRACE("ObjectFormat: {%s}\n", debugstr_fmtex(obj->params.ObjectFormat));
+            TRACE("StaticObjectTypeMask: 0x%x\n", obj->params.StaticObjectTypeMask);
+            TRACE("MinDynamicObjectCount: 0x%x\n", obj->params.MinDynamicObjectCount);
+            TRACE("MaxDynamicObjectCount: 0x%x\n", obj->params.MaxDynamicObjectCount);
+            TRACE("Category: 0x%x\n", obj->params.Category);
+            TRACE("EventHandle: %p\n", obj->params.EventHandle);
+            TRACE("NotifyObject: %p\n", obj->params.NotifyObject);
+        }
+
+        hr = activate_stream(obj);
+        if(FAILED(hr)){
+            if(obj->params.NotifyObject)
+                ISpatialAudioObjectRenderStreamNotify_Release(obj->params.NotifyObject);
+            DeleteCriticalSection(&obj->lock);
+            heap_free((void*)obj->params.ObjectFormat);
+            CloseHandle(obj->params.EventHandle);
+            ISpatialAudioClient_Release(&obj->sa_client->ISpatialAudioClient_iface);
+            heap_free(obj);
+            *stream = NULL;
+            return hr;
+        }
+
+        *stream = &obj->ISpatialAudioObjectRenderStream_iface;
+    }else{
+        FIXME("Unsupported audio stream IID: %s\n", debugstr_guid(riid));
+        *stream = NULL;
+        return E_NOTIMPL;
+    }
+
+    return S_OK;
+}
+
+static ISpatialAudioClientVtbl ISpatialAudioClient_vtbl = {
+    SAC_QueryInterface,
+    SAC_AddRef,
+    SAC_Release,
+    SAC_GetStaticObjectPosition,
+    SAC_GetNativeStaticObjectTypeMask,
+    SAC_GetMaxDynamicObjectCount,
+    SAC_GetSupportedAudioObjectFormatEnumerator,
+    SAC_GetMaxFrameCount,
+    SAC_IsAudioObjectFormatSupported,
+    SAC_IsSpatialAudioStreamAvailable,
+    SAC_ActivateSpatialAudioStream,
+};
+
+static HRESULT WINAPI SAOFE_QueryInterface(IAudioFormatEnumerator *iface,
+        REFIID riid, void **ppvObject)
+{
+    SpatialAudioImpl *This = impl_from_IAudioFormatEnumerator(iface);
+    return SAC_QueryInterface(&This->ISpatialAudioClient_iface, riid, ppvObject);
+}
+
+static ULONG WINAPI SAOFE_AddRef(IAudioFormatEnumerator *iface)
+{
+    SpatialAudioImpl *This = impl_from_IAudioFormatEnumerator(iface);
+    return SAC_AddRef(&This->ISpatialAudioClient_iface);
+}
+
+static ULONG WINAPI SAOFE_Release(IAudioFormatEnumerator *iface)
+{
+    SpatialAudioImpl *This = impl_from_IAudioFormatEnumerator(iface);
+    return SAC_Release(&This->ISpatialAudioClient_iface);
+}
+
+static HRESULT WINAPI SAOFE_GetCount(IAudioFormatEnumerator *iface, UINT32 *count)
+{
+    SpatialAudioImpl *This = impl_from_IAudioFormatEnumerator(iface);
+
+    TRACE("(%p)->(%p)\n", This, count);
+
+    *count = 1;
+
+    return S_OK;
+}
+
+static HRESULT WINAPI SAOFE_GetFormat(IAudioFormatEnumerator *iface,
+        UINT32 index, WAVEFORMATEX **format)
+{
+    SpatialAudioImpl *This = impl_from_IAudioFormatEnumerator(iface);
+
+    TRACE("(%p)->(%u, %p)\n", This, index, format);
+
+    if(index > 0)
+        return E_INVALIDARG;
+
+    *format = &This->object_fmtex.Format;
+
+    return S_OK;
+}
+
+static IAudioFormatEnumeratorVtbl IAudioFormatEnumerator_vtbl = {
+    SAOFE_QueryInterface,
+    SAOFE_AddRef,
+    SAOFE_Release,
+    SAOFE_GetCount,
+    SAOFE_GetFormat,
+};
+
+HRESULT SpatialAudioClient_Create(IMMDevice *mmdev, ISpatialAudioClient **out)
+{
+    SpatialAudioImpl *obj;
+    IAudioClient *aclient;
+    WAVEFORMATEX *closest;
+    HRESULT hr;
+
+    obj = heap_alloc_zero(sizeof(*obj));
+
+    obj->ref = 1;
+    obj->ISpatialAudioClient_iface.lpVtbl = &ISpatialAudioClient_vtbl;
+    obj->IAudioFormatEnumerator_iface.lpVtbl = &IAudioFormatEnumerator_vtbl;
+
+    obj->object_fmtex.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
+    obj->object_fmtex.Format.nChannels = 1;
+    obj->object_fmtex.Format.nSamplesPerSec = 48000;
+    obj->object_fmtex.Format.wBitsPerSample = sizeof(float) * 8;
+    obj->object_fmtex.Format.nBlockAlign = (obj->object_fmtex.Format.nChannels * obj->object_fmtex.Format.wBitsPerSample) / 8;
+    obj->object_fmtex.Format.nAvgBytesPerSec = obj->object_fmtex.Format.nSamplesPerSec * obj->object_fmtex.Format.nBlockAlign;
+    obj->object_fmtex.Format.cbSize = 0;
+
+    hr = IMMDevice_Activate(mmdev, &IID_IAudioClient,
+            CLSCTX_INPROC_SERVER, NULL, (void**)&aclient);
+    if(FAILED(hr)){
+        WARN("Activate failed: %08x\n", hr);
+        heap_free(obj);
+        return hr;
+    }
+
+    hr = IAudioClient_IsFormatSupported(aclient, AUDCLNT_SHAREMODE_SHARED, &obj->object_fmtex.Format, &closest);
+
+    IAudioClient_Release(aclient);
+
+    if(hr == S_FALSE){
+        if(sizeof(WAVEFORMATEX) + closest->cbSize > sizeof(obj->object_fmtex)){
+            ERR("Returned format too large: %s\n", debugstr_fmtex(closest));
+            CoTaskMemFree(closest);
+            heap_free(obj);
+            return AUDCLNT_E_UNSUPPORTED_FORMAT;
+        }else if(!((closest->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
+                    (closest->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
+                     IsEqualGUID(&((WAVEFORMATEXTENSIBLE *)closest)->SubFormat,
+                         &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))) &&
+                    closest->wBitsPerSample == 32)){
+            ERR("Returned format not 32-bit float: %s\n", debugstr_fmtex(closest));
+            CoTaskMemFree(closest);
+            heap_free(obj);
+            return AUDCLNT_E_UNSUPPORTED_FORMAT;
+        }
+        WARN("The audio stack doesn't support 48kHz 32bit float. Using the closest match. Audio may be glitchy. %s\n", debugstr_fmtex(closest));
+        memcpy(&obj->object_fmtex,
+               closest,
+               sizeof(WAVEFORMATEX) + closest->cbSize);
+        CoTaskMemFree(closest);
+    } else if(hr != S_OK){
+        WARN("Checking supported formats failed: %08x\n", hr);
+        heap_free(obj);
+        return hr;
+    }
+
+    obj->mmdev = mmdev;
+    IMMDevice_AddRef(mmdev);
+
+    *out = &obj->ISpatialAudioClient_iface;
+
+    return S_OK;
+}
diff --git a/dlls/mmdevapi/tests/Makefile.in b/dlls/mmdevapi/tests/Makefile.in
index 0f17ea31923..062ad413922 100644
--- a/dlls/mmdevapi/tests/Makefile.in
+++ b/dlls/mmdevapi/tests/Makefile.in
@@ -6,4 +6,5 @@ C_SRCS = \
 	dependency.c \
 	mmdevenum.c \
 	propstore.c \
-	render.c
+	render.c \
+	spatialaudio.c
diff --git a/dlls/mmdevapi/tests/mmdevenum.c b/dlls/mmdevapi/tests/mmdevenum.c
index 350d6474a46..1fd1c7d4aca 100644
--- a/dlls/mmdevapi/tests/mmdevenum.c
+++ b/dlls/mmdevapi/tests/mmdevenum.c
@@ -24,6 +24,7 @@
 #include "endpointvolume.h"
 #include "mmdeviceapi.h"
 #include "audioclient.h"
+#include "spatialaudioclient.h"
 #include "audiopolicy.h"
 #include "dshow.h"
 #include "dsound.h"
diff --git a/dlls/mmdevapi/tests/spatialaudio.c b/dlls/mmdevapi/tests/spatialaudio.c
new file mode 100644
index 00000000000..8e4d4a874ef
--- /dev/null
+++ b/dlls/mmdevapi/tests/spatialaudio.c
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2021 Arkadiusz Hiler for CodeWeavers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#include <math.h>
+#include <stdio.h>
+
+#include "wine/test.h"
+
+#define COBJMACROS
+
+#ifdef STANDALONE
+#include "initguid.h"
+#endif
+
+#include "mmdeviceapi.h"
+#include "spatialaudioclient.h"
+
+static IMMDeviceEnumerator *mme = NULL;
+static IMMDevice *dev = NULL;
+static ISpatialAudioClient *sac = NULL;
+static UINT32 max_dyn_count;
+static HANDLE event;
+static WAVEFORMATEX format;
+
+static void test_formats(void)
+{
+    HRESULT hr;
+    IAudioFormatEnumerator *afe;
+    UINT32 format_count = 0;
+    WAVEFORMATEX *fmt = NULL;
+
+    hr = ISpatialAudioClient_GetSupportedAudioObjectFormatEnumerator(sac, &afe);
+    ok(hr == S_OK, "Getting format enumerator failed: 0x%08x\n", hr);
+
+    hr = IAudioFormatEnumerator_GetCount(afe, &format_count);
+    ok(hr == S_OK, "Getting format count failed: 0x%08x\n", hr);
+    ok(format_count == 1, "Got wrong format count, expected 1 got %u\n", format_count);
+
+    hr = IAudioFormatEnumerator_GetFormat(afe, 0, &fmt);
+    ok(hr == S_OK, "Getting format failed: 0x%08x\n", hr);
+    ok(fmt != NULL, "Expected to get non-NULL format\n");
+
+    ok(fmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT, "Wrong format, expected WAVE_FORMAT_IEEE_FLOAT got %hx\n", fmt->wFormatTag);
+    ok(fmt->nChannels == 1, "Wrong number of channels, expected 1 got %hu\n", fmt->nChannels);
+    ok(fmt->nSamplesPerSec == 48000, "Wrong sample ret, expected 48000 got %u\n", fmt->nSamplesPerSec);
+    ok(fmt->wBitsPerSample == 32, "Wrong bits per sample, expected 32 got %hu\n", fmt->wBitsPerSample);
+    ok(fmt->nBlockAlign == 4, "Wrong block align, expected 4 got %hu\n", fmt->nBlockAlign);
+    ok(fmt->nAvgBytesPerSec == 192000, "Wrong avg bytes per sec, expected 192000 got %u\n", fmt->nAvgBytesPerSec);
+    ok(fmt->cbSize == 0, "Wrong cbSize for simple format, expected 0, got %hu\n", fmt->cbSize);
+
+    memcpy(&format, fmt, sizeof(format));
+
+    IAudioFormatEnumerator_Release(afe);
+}
+
+static void fill_activation_params(SpatialAudioObjectRenderStreamActivationParams *activation_params)
+{
+	activation_params->StaticObjectTypeMask =  \
+		    AudioObjectType_FrontLeft     |
+		    AudioObjectType_FrontRight    |
+		    AudioObjectType_FrontCenter   |
+		    AudioObjectType_LowFrequency  |
+		    AudioObjectType_SideLeft      |
+		    AudioObjectType_SideRight     |
+		    AudioObjectType_BackLeft      |
+		    AudioObjectType_BackRight     |
+		    AudioObjectType_TopFrontLeft  |
+		    AudioObjectType_TopFrontRight |
+		    AudioObjectType_TopBackLeft   |
+		    AudioObjectType_TopBackRight;
+
+	activation_params->MinDynamicObjectCount = 0;
+	activation_params->MaxDynamicObjectCount = 0;
+	activation_params->Category = AudioCategory_GameEffects;
+	activation_params->EventHandle = event;
+	activation_params->NotifyObject = NULL;
+
+        activation_params->ObjectFormat = &format;
+}
+
+typedef struct NotifyObject
+{
+    ISpatialAudioObjectRenderStreamNotify ISpatialAudioObjectRenderStreamNotify_iface;
+    LONG ref;
+} NotifyObject;
+
+static WINAPI HRESULT notifyobj_QueryInterface(
+        ISpatialAudioObjectRenderStreamNotify *This,
+        REFIID riid,
+        void **ppvObject)
+{
+    return S_OK;
+}
+
+static WINAPI ULONG notifyobj_AddRef(
+        ISpatialAudioObjectRenderStreamNotify *This)
+{
+    NotifyObject *obj = CONTAINING_RECORD(This, NotifyObject, ISpatialAudioObjectRenderStreamNotify_iface);
+    ULONG ref = InterlockedIncrement(&obj->ref);
+    return ref;
+}
+
+static WINAPI ULONG notifyobj_Release(
+        ISpatialAudioObjectRenderStreamNotify *This)
+{
+    NotifyObject *obj = CONTAINING_RECORD(This, NotifyObject, ISpatialAudioObjectRenderStreamNotify_iface);
+    ULONG ref = InterlockedDecrement(&obj->ref);
+    return ref;
+}
+
+static WINAPI HRESULT notifyobj_OnAvailableDynamicObjectCountChange(
+        ISpatialAudioObjectRenderStreamNotify *This,
+        ISpatialAudioObjectRenderStreamBase *stream,
+        LONGLONG deadline,
+        UINT32 object_count)
+{
+    ok(FALSE, "Expected to never be notified of dynamic object count change\n");
+    return S_OK;
+}
+
+static const ISpatialAudioObjectRenderStreamNotifyVtbl notifyobjvtbl =
+{
+    notifyobj_QueryInterface,
+    notifyobj_AddRef,
+    notifyobj_Release,
+    notifyobj_OnAvailableDynamicObjectCountChange
+};
+
+static void test_stream_activation(void)
+{
+        HRESULT hr;
+        WAVEFORMATEX wrong_format;
+        ISpatialAudioObjectRenderStream *sas = NULL;
+
+	SpatialAudioObjectRenderStreamActivationParams activation_params;
+	PROPVARIANT activation_params_prop;
+        NotifyObject notify_object;
+
+	PropVariantInit(&activation_params_prop);
+	activation_params_prop.vt = VT_BLOB;
+	activation_params_prop.blob.cbSize = sizeof(activation_params);
+	activation_params_prop.blob.pBlobData = (BYTE*) &activation_params;
+
+        /* correct params */
+        fill_activation_params(&activation_params);
+	hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+        ok(hr == S_OK, "Failed to activate spatial audio stream: 0x%08x\n", hr);
+        ok(ISpatialAudioObjectRenderStream_Release(sas) == 0, "Expected to release the last reference\n");
+
+        /* no event handle */
+        fill_activation_params(&activation_params);
+	activation_params.EventHandle = NULL;
+	hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+        ok(hr == E_INVALIDARG, "Expected lack of no EventHandle to be invalid: 0x%08x\n", hr);
+        ok(sas == NULL, "Expected spatial audio stream to be set to NULL upon failed activation\n");
+
+        /* must use only queried sample rate */
+        fill_activation_params(&activation_params);
+        memcpy(&wrong_format, &format, sizeof(format));
+        activation_params.ObjectFormat = &wrong_format;
+        wrong_format.nSamplesPerSec = 44100;
+        wrong_format.nAvgBytesPerSec = wrong_format.nSamplesPerSec * wrong_format.nBlockAlign;
+	hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+        ok(hr == AUDCLNT_E_UNSUPPORTED_FORMAT, "Expected format to be unsupported: 0x%08x\n", hr);
+        ok(sas == NULL, "Expected spatial audio stream to be set to NULL upon failed activation\n");
+
+        /* dynamic objects are not supported */
+        if (max_dyn_count == 0)
+        {
+            fill_activation_params(&activation_params);
+            activation_params.StaticObjectTypeMask |= AudioObjectType_Dynamic;
+            hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+            ok(hr == E_INVALIDARG, "Expected dynamic objects type be invalid: 0x%08x\n", hr);
+            ok(sas == NULL, "Expected spatial audio stream to be set to NULL upon failed activation\n");
+        }
+
+	activation_params.MinDynamicObjectCount = max_dyn_count + 1;
+	activation_params.MaxDynamicObjectCount = max_dyn_count + 1;
+	hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+        if (max_dyn_count)
+            ok(hr == AUDCLNT_E_UNSUPPORTED_FORMAT, "Expected dynamic object count exceeding max to be unsupported: 0x%08x\n", hr);
+        else
+            ok(hr == E_INVALIDARG, "Expected setting dynamic object count to be invalid: 0x%08x\n", hr);
+
+        /* ISpatialAudioObjectRenderStreamNotify */
+        fill_activation_params(&activation_params);
+        notify_object.ISpatialAudioObjectRenderStreamNotify_iface.lpVtbl = ¬ifyobjvtbl;
+        notify_object.ref = 0;
+        activation_params.NotifyObject = &notify_object.ISpatialAudioObjectRenderStreamNotify_iface;
+	hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+        ok(hr == S_OK, "Failed to activate spatial audio stream: 0x%08x\n", hr);
+        ok(notify_object.ref == 1, "Expected to get increased NotifyObject's ref count\n");
+        ok(ISpatialAudioObjectRenderStream_Release(sas) == 0, "Expected to release the last reference\n");
+        ok(notify_object.ref == 0, "Expected to get lowered NotifyObject's ref count\n");
+}
+
+static void test_audio_object_activation(void)
+{
+        HRESULT hr;
+        BOOL is_active;
+        ISpatialAudioObjectRenderStream *sas = NULL;
+	ISpatialAudioObject *sao1, *sao2;
+
+	SpatialAudioObjectRenderStreamActivationParams activation_params;
+	PROPVARIANT activation_params_prop;
+
+	PropVariantInit(&activation_params_prop);
+	activation_params_prop.vt = VT_BLOB;
+	activation_params_prop.blob.cbSize = sizeof(activation_params);
+	activation_params_prop.blob.pBlobData = (BYTE*) &activation_params;
+
+        fill_activation_params(&activation_params);
+        activation_params.StaticObjectTypeMask &= ~AudioObjectType_FrontRight;
+	hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+        ok(hr == S_OK, "Failed to activate spatial audio stream: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_FrontLeft, &sao1);
+        ok(hr == S_OK, "Failed to activate spatial audio object: 0x%08x\n", hr);
+        hr = ISpatialAudioObject_IsActive(sao1, &is_active);
+        todo_wine ok(hr == S_OK, "Failed to check if spatial audio object is active: 0x%08x\n", hr);
+        todo_wine ok(is_active, "Expected spaital audio object to be active\n");
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_FrontLeft, &sao2);
+        ok(hr == SPTLAUDCLNT_E_OBJECT_ALREADY_ACTIVE, "Expected audio object to be already active: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_FrontRight, &sao2);
+        ok(hr == SPTLAUDCLNT_E_STATIC_OBJECT_NOT_AVAILABLE, "Expected static object to be not available: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_Dynamic, &sao2);
+        ok(hr == SPTLAUDCLNT_E_NO_MORE_OBJECTS, "Expected to not have no more dynamic objects: 0x%08x\n", hr);
+
+        ISpatialAudioObject_Release(sao1);
+        ISpatialAudioObjectRenderStream_Release(sas);
+}
+
+static BOOL is_buffer_zeroed(BYTE *buffer, UINT32 buffer_length)
+{
+    UINT32 i;
+
+    for (i = 0; i < buffer_length; i++)
+    {
+        if (buffer[i] != 0)
+            return FALSE;
+    }
+    return TRUE;
+}
+
+static void test_audio_object_buffers(void)
+{
+        HRESULT hr;
+        ISpatialAudioObjectRenderStream *sas = NULL;
+	ISpatialAudioObject *sao[4];
+        UINT32 dyn_object_count, frame_count, buffer_length;
+        BYTE *buffer;
+        INT i;
+
+	SpatialAudioObjectRenderStreamActivationParams activation_params;
+	PROPVARIANT activation_params_prop;
+
+	PropVariantInit(&activation_params_prop);
+	activation_params_prop.vt = VT_BLOB;
+	activation_params_prop.blob.cbSize = sizeof(activation_params);
+	activation_params_prop.blob.pBlobData = (BYTE*) &activation_params;
+
+        fill_activation_params(&activation_params);
+	hr = ISpatialAudioClient_ActivateSpatialAudioStream(sac, &activation_params_prop, &IID_ISpatialAudioObjectRenderStream, (void**)&sas);
+        ok(hr == S_OK, "Failed to activate spatial audio stream: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_FrontLeft, &sao[0]);
+        ok(hr == S_OK, "Failed to activate spatial audio object: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_Start(sas);
+        ok(hr == S_OK, "Failed to activate spatial audio render stream: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_FrontRight, &sao[1]);
+        ok(hr == S_OK, "Failed to activate spatial audio object: 0x%08x\n", hr);
+
+        hr = WaitForSingleObject(event, 200);
+        ok(hr == WAIT_OBJECT_0, "Expected event to be flagged: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_SideLeft, &sao[2]);
+        ok(hr == S_OK, "Failed to activate spatial audio object: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_BeginUpdatingAudioObjects(sas, &dyn_object_count, &frame_count);
+        ok(hr == S_OK, "Failed to beging updating audio objects: 0x%08x\n", hr);
+        ok(dyn_object_count == 0, "Unexpected dynamic objects\n");
+        ok(frame_count > 0, "Expected to get non-zero frames to update\n");
+
+        hr = ISpatialAudioObjectRenderStream_ActivateSpatialAudioObject(sas, AudioObjectType_SideRight, &sao[3]);
+        ok(hr == S_OK, "Failed to activate spatial audio object: 0x%08x\n", hr);
+
+        for (i = 0; i < ARRAYSIZE(sao); i++)
+        {
+            hr = ISpatialAudioObject_GetBuffer(sao[i], &buffer, &buffer_length);
+            ok(hr == S_OK, "Expected to be able to get buffers for audio object: 0x%08x\n", hr);
+            ok(buffer != NULL, "Expected to get a non-NULL buffer\n");
+            ok(buffer_length == frame_count * format.wBitsPerSample / 8, "Expected buffer length to be sample_size * frame_count = %hu but got %u\n",
+                    frame_count * format.wBitsPerSample / 8, buffer_length);
+            ok(is_buffer_zeroed(buffer, buffer_length), "Expected audio object's buffer to be zeroed\n");
+        }
+
+        hr = ISpatialAudioObjectRenderStream_EndUpdatingAudioObjects(sas);
+        ok(hr == S_OK, "Failed to end updating audio objects: 0x%08x\n", hr);
+
+        hr = WaitForSingleObject(event, 200);
+        ok(hr == WAIT_OBJECT_0, "Expected event to be flagged: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_BeginUpdatingAudioObjects(sas, &dyn_object_count, &frame_count);
+        ok(hr == S_OK, "Failed to beging updating audio objects: 0x%08x\n", hr);
+        ok(dyn_object_count == 0, "Unexpected dynamic objects\n");
+        ok(frame_count > 0, "Expected to get non-zero frames to update\n");
+
+        /* one more iteration but not with every object */
+        for (i = 0; i < ARRAYSIZE(sao) - 1; i++)
+        {
+            hr = ISpatialAudioObject_GetBuffer(sao[i], &buffer, &buffer_length);
+            ok(hr == S_OK, "Expected to be able to get buffers for audio object: 0x%08x\n", hr);
+            ok(buffer != NULL, "Expected to get a non-NULL buffer\n");
+            ok(buffer_length == frame_count * format.wBitsPerSample / 8, "Expected buffer length to be sample_size * frame_count = %hu but got %u\n",
+                    frame_count * format.wBitsPerSample / 8, buffer_length);
+            ok(is_buffer_zeroed(buffer, buffer_length), "Expected audio object's buffer to be zeroed\n");
+        }
+
+        hr = ISpatialAudioObjectRenderStream_EndUpdatingAudioObjects(sas);
+        ok(hr == S_OK, "Failed to end updating audio objects: 0x%08x\n", hr);
+
+        /* ending the stream */
+        hr = ISpatialAudioObject_SetEndOfStream(sao[0], 0);
+        todo_wine ok(hr == SPTLAUDCLNT_E_OUT_OF_ORDER, "Expected that ending the stream at this point won't be allowed: 0x%08x\n", hr);
+
+        hr = WaitForSingleObject(event, 10000);
+        ok(hr == WAIT_OBJECT_0, "Expected event to be flagged: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObject_SetEndOfStream(sao[0], 0);
+        todo_wine ok(hr == SPTLAUDCLNT_E_OUT_OF_ORDER, "Expected that ending the stream at this point won't be allowed: 0x%08x\n", hr);
+
+        hr = ISpatialAudioObjectRenderStream_BeginUpdatingAudioObjects(sas, &dyn_object_count, &frame_count);
+        ok(hr == S_OK, "Failed to beging updating audio objects: 0x%08x\n", hr);
+        ok(dyn_object_count == 0, "Unexpected dynamic objects\n");
+        ok(frame_count > 0, "Expected to get non-zero frames to update\n");
+
+        /* expect the object that was not updated last cycle to be invalidated */
+        hr = ISpatialAudioObject_GetBuffer(sao[ARRAYSIZE(sao) - 1], &buffer, &buffer_length);
+        todo_wine ok(hr == SPTLAUDCLNT_E_RESOURCES_INVALIDATED, "Expected audio object to be invalidated: 0x%08x\n", hr);
+
+        for (i = 0; i < ARRAYSIZE(sao) - 1; i++)
+        {
+            hr = ISpatialAudioObject_GetBuffer(sao[i], &buffer, &buffer_length);
+            ok(hr == S_OK, "Expected to be able to get buffers for audio object: 0x%08x\n", hr);
+
+            hr = ISpatialAudioObject_SetEndOfStream(sao[i], 0);
+            todo_wine ok(hr == S_OK, "Failed to end the stream: 0x%08x\n", hr);
+
+            hr = ISpatialAudioObject_GetBuffer(sao[i], &buffer, &buffer_length);
+            todo_wine ok(hr == SPTLAUDCLNT_E_RESOURCES_INVALIDATED, "Expected audio object to be invalidated: 0x%08x\n", hr);
+        }
+
+        hr = ISpatialAudioObjectRenderStream_EndUpdatingAudioObjects(sas);
+        ok(hr == S_OK, "Failed to end updating audio objects: 0x%08x\n", hr);
+
+        for (i = 0; i < ARRAYSIZE(sao); i++)
+        {
+            ISpatialAudioObject_Release(sao[i]);
+        }
+
+        ISpatialAudioObjectRenderStream_Release(sas);
+}
+
+START_TEST(spatialaudio)
+{
+    HRESULT hr;
+
+    event = CreateEventA(NULL, FALSE, FALSE, "spatial-audio-test-prog-event");
+    ok (event != NULL, "Failed to create event, last error: 0x%08x\n", GetLastError());
+
+    CoInitializeEx(NULL, COINIT_MULTITHREADED);
+    hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &IID_IMMDeviceEnumerator, (void**)&mme);
+    if (FAILED(hr))
+    {
+        skip("mmdevapi not available: 0x%08x\n", hr);
+        goto cleanup;
+    }
+
+    hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(mme, eRender, eMultimedia, &dev);
+    ok(hr == S_OK || hr == E_NOTFOUND, "GetDefaultAudioEndpoint failed: 0x%08x\n", hr);
+    if (hr != S_OK || !dev)
+    {
+        if (hr == E_NOTFOUND)
+            skip("No sound card available\n");
+        else
+            skip("GetDefaultAudioEndpoint returns 0x%08x\n", hr);
+        goto cleanup;
+    }
+
+    hr = IMMDevice_Activate(dev, &IID_ISpatialAudioClient, CLSCTX_INPROC_SERVER, NULL, (void**)&sac);
+    ok(hr == S_OK || hr == E_NOINTERFACE, "ISpatialAudioClient Activation failed: 0x%08x\n", hr);
+    if (hr != S_OK || !dev)
+    {
+        if (hr == E_NOINTERFACE)
+            skip("ISpatialAudioClient interface not found\n");
+        else
+            skip("ISpatialAudioClient Activation returns 0x%08x\n", hr);
+        goto cleanup;
+    }
+
+    hr = ISpatialAudioClient_GetMaxDynamicObjectCount(sac, &max_dyn_count);
+    ok(hr == S_OK, "Failed to get max dynamic object count: 0x%08x\n", hr);
+
+    /* that's the default, after manually enabling Windows Sonic it's possible to have max_dyn_count > 0 */
+    /* ok(max_dyn_count == 0, "expected max dynamic object count to be 0 got %u\n", max_dyn_count); */
+
+    test_formats();
+    test_stream_activation();
+    test_audio_object_activation();
+    test_audio_object_buffers();
+
+    ISpatialAudioClient_Release(sac);
+
+cleanup:
+    if (dev)
+        IMMDevice_Release(dev);
+    if (mme)
+        IMMDeviceEnumerator_Release(mme);
+    CoUninitialize();
+    CloseHandle(event);
+}
diff --git a/include/spatialaudioclient.idl b/include/spatialaudioclient.idl
index 16a1541fd1d..08c84965566 100644
--- a/include/spatialaudioclient.idl
+++ b/include/spatialaudioclient.idl
@@ -43,6 +43,47 @@ typedef [v1_enum] enum AudioObjectType
     AudioObjectType_BackCenter       = 0x00020000,
 } AudioObjectType;
 
+cpp_quote("#define SPTLAUDCLNT_E_DESTROYED                     AUDCLNT_ERR(0x100)")
+cpp_quote("#define SPTLAUDCLNT_E_OUT_OF_ORDER                  AUDCLNT_ERR(0x101)")
+cpp_quote("#define SPTLAUDCLNT_E_RESOURCES_INVALIDATED         AUDCLNT_ERR(0x102)")
+cpp_quote("#define SPTLAUDCLNT_E_NO_MORE_OBJECTS               AUDCLNT_ERR(0x103)")
+cpp_quote("#define SPTLAUDCLNT_E_PROPERTY_NOT_SUPPORTED        AUDCLNT_ERR(0x104)")
+cpp_quote("#define SPTLAUDCLNT_E_ERRORS_IN_OBJECT_CALLS        AUDCLNT_ERR(0x105)")
+cpp_quote("#define SPTLAUDCLNT_E_METADATA_FORMAT_NOT_SUPPORTED AUDCLNT_ERR(0x106)")
+cpp_quote("#define SPTLAUDCLNT_E_STREAM_NOT_AVAILABLE          AUDCLNT_ERR(0x107)")
+cpp_quote("#define SPTLAUDCLNT_E_INVALID_LICENSE               AUDCLNT_ERR(0x108)")
+cpp_quote("#define SPTLAUDCLNT_E_STREAM_NOT_STOPPED            AUDCLNT_ERR(0x10a)")
+cpp_quote("#define SPTLAUDCLNT_E_STATIC_OBJECT_NOT_AVAILABLE   AUDCLNT_ERR(0x10b)")
+cpp_quote("#define SPTLAUDCLNT_E_OBJECT_ALREADY_ACTIVE         AUDCLNT_ERR(0x10c)")
+cpp_quote("#define SPTLAUDCLNT_E_INTERNAL                      AUDCLNT_ERR(0x10d)")
+
+interface ISpatialAudioObjectRenderStreamBase;
+
+[
+    object,
+    uuid(dddf83e6-68d7-4c70-883f-a1836afb4a50),
+    pointer_default(unique),
+    local
+]
+interface ISpatialAudioObjectRenderStreamNotify : IUnknown
+{
+    HRESULT OnAvailableDynamicObjectCountChange(
+        [in] ISpatialAudioObjectRenderStreamBase *stream,
+        [in] LONGLONG deadline,
+        [in] UINT32 object_count);
+}
+
+typedef struct tagSpatialAudioObjectRenderStreamActivationParams
+{
+    const WAVEFORMATEX *ObjectFormat;
+    AudioObjectType StaticObjectTypeMask;
+    UINT32 MinDynamicObjectCount;
+    UINT32 MaxDynamicObjectCount;
+    AUDIO_STREAM_CATEGORY Category;
+    HANDLE EventHandle;
+    ISpatialAudioObjectRenderStreamNotify *NotifyObject;
+} SpatialAudioObjectRenderStreamActivationParams;
+
 [
     object,
     uuid(dcdaa858-895a-4a22-a5eb-67bda506096d),
@@ -98,3 +139,83 @@ interface ISpatialAudioClient : IUnknown
         [in] REFIID riid,
         [out, iid_is(riid)] void **stream);
 }
+
+[
+    object,
+    uuid(cce0b8f2-8d4d-4efb-a8cf-3d6ecf1c30e0),
+    pointer_default(unique),
+    local
+]
+interface ISpatialAudioObjectBase : IUnknown
+{
+    HRESULT GetBuffer(
+        [out] BYTE **buffer,
+        [out] UINT32 *bytes);
+
+    HRESULT SetEndOfStream(
+        [in] UINT32 frames);
+
+    HRESULT IsActive(
+        [out] BOOL *active);
+
+    HRESULT GetAudioObjectType(
+        [out] AudioObjectType *type);
+}
+
+[
+    object,
+    uuid(dde28967-521b-46e5-8f00-bd6f2bc8ab1d),
+    pointer_default(unique),
+    local
+]
+interface ISpatialAudioObject : ISpatialAudioObjectBase
+{
+    HRESULT SetPosition(
+        [in] float x,
+        [in] float y,
+        [in] float z);
+
+    HRESULT SetVolume(
+        [in] float vol);
+}
+
+[
+    object,
+    uuid(feaaf403-c1d8-450d-aa05-e0ccee7502a8),
+    pointer_default(unique),
+    local
+]
+interface ISpatialAudioObjectRenderStreamBase : IUnknown
+{
+    HRESULT GetAvailableDynamicObjectCount(
+        [out] UINT32 *count);
+
+    HRESULT GetService(
+        [in] REFIID riid,
+        [out] void **service);
+
+    HRESULT Start();
+
+    HRESULT Stop();
+
+    HRESULT Reset();
+
+    HRESULT BeginUpdatingAudioObjects(
+        [out] UINT32 *count,
+        [out] UINT32 *frames);
+
+    HRESULT EndUpdatingAudioObjects();
+}
+
+[
+    object,
+    uuid(bab5f473-b423-477b-85f5-b5a332a04153),
+    pointer_default(unique),
+    local
+]
+interface ISpatialAudioObjectRenderStream : ISpatialAudioObjectRenderStreamBase
+{
+    HRESULT ActivateSpatialAudioObject(
+        [in] AudioObjectType type,
+        [out] ISpatialAudioObject **object);
+}
-- 
2.30.0




More information about the wine-devel mailing list