[PATCH 1/5] winepulse.drv: Expose audio devices to the application.

Gabriel Ivăncescu gabrielopcode at gmail.com
Wed Feb 9 08:10:54 CST 2022


This exposes the actual devices (and virtual sinks/sources) as reported by
PulseAudio to an application, allowing it to select the devices itself and,
for example, record from (or render to) two devices at the same time. The
"PulseAudio" device (which is movable) is still the default, as before.

It keeps a list of the devices on the unixlib side, which then stores the
device type, pulse name and display (friendly) name into the registry,
for persistent settings of an app to identify the device by guid. The
keys are stored under HKLM\Software\Wine\Drivers\winepulse.drv\devices,
with the following format:

  <type>,<pulse device name>

Where <type> is a single character, which is either 0 (for output/sinks)
or 1 (for input/sources), and <pulse device name> is the device name that
PulseAudio uses to identify the device. The "name" value is stored under
this key which is the display name of the device. When enumerating the
devices, the "name" is taken and a GUID is generated if it's missing or
invalid under the "guid" value of the same key; this preserves the GUID
for the same device unless the registry key is cleaned.

Based on a patch by Mark Harmstone <mark at harmstone.com>, with changes by
Sebastian Lackner <sebastian at fds-team.de>.

Signed-off-by: Gabriel Ivăncescu <gabrielopcode at gmail.com>
---

This patchset has lived in wine-staging for a long time, but has been
recently rebased to deal with unixlib separation.

 dlls/winepulse.drv/mmdevdrv.c | 184 ++++++++++++++++++++++++++++------
 dlls/winepulse.drv/pulse.c    | 176 ++++++++++++++++++++++++++++++--
 dlls/winepulse.drv/unixlib.h  |   1 +
 3 files changed, 320 insertions(+), 41 deletions(-)

diff --git a/dlls/winepulse.drv/mmdevdrv.c b/dlls/winepulse.drv/mmdevdrv.c
index 35a66e1..bf14324 100644
--- a/dlls/winepulse.drv/mmdevdrv.c
+++ b/dlls/winepulse.drv/mmdevdrv.c
@@ -134,6 +134,7 @@ struct ACImpl {
     IUnknown *marshal;
     IMMDevice *parent;
     struct list entry;
+    char device[256];
     float *vol;
 
     LONG ref;
@@ -147,8 +148,6 @@ struct ACImpl {
     AudioSessionWrapper *session_wrapper;
 };
 
-static const WCHAR defaultW[] = L"PulseAudio";
-
 static const IAudioClient3Vtbl AudioClient3_Vtbl;
 static const IAudioRenderClientVtbl AudioRenderClient_Vtbl;
 static const IAudioCaptureClientVtbl AudioCaptureClient_Vtbl;
@@ -267,39 +266,96 @@ static void set_stream_volumes(ACImpl *This)
     pulse_call(set_volumes, &params);
 }
 
-HRESULT WINAPI AUDDRV_GetEndpointIDs(EDataFlow flow, const WCHAR ***ids, GUID **keys,
+HRESULT WINAPI AUDDRV_GetEndpointIDs(EDataFlow flow, WCHAR ***ids, GUID **keys,
         UINT *num, UINT *def_index)
 {
-    WCHAR *id;
+    WCHAR flow_char = (flow == eRender) ? '0' : '1';
+    DWORD i, k = 0, count = 0, maxlen, size, type;
+    WCHAR *id, *key_name = NULL;
+    HKEY dev_key, key = NULL;
+    LSTATUS status;
+    GUID guid;
 
     TRACE("%d %p %p %p\n", flow, ids, num, def_index);
 
-    *num = 1;
+    *num = 0;
     *def_index = 0;
-
-    *ids = HeapAlloc(GetProcessHeap(), 0, sizeof(**ids));
+    *ids = NULL;
     *keys = NULL;
-    if (!*ids)
-        return E_OUTOFMEMORY;
 
-    (*ids)[0] = id = HeapAlloc(GetProcessHeap(), 0, sizeof(defaultW));
-    *keys = HeapAlloc(GetProcessHeap(), 0, sizeof(**keys));
-    if (!*keys || !id) {
-        HeapFree(GetProcessHeap(), 0, id);
-        HeapFree(GetProcessHeap(), 0, *keys);
-        HeapFree(GetProcessHeap(), 0, *ids);
-        *ids = NULL;
-        *keys = NULL;
-        return E_OUTOFMEMORY;
+    if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Wine\\Drivers\\winepulse.drv\\devices",
+            0, KEY_READ | KEY_WOW64_64KEY, &key) == ERROR_SUCCESS) {
+        status = RegQueryInfoKeyW(key, NULL, NULL, NULL, &count, &maxlen, NULL, NULL, NULL, NULL, NULL, NULL);
+        if (status != ERROR_SUCCESS || maxlen < 3)
+            count = 0;
     }
-    memcpy(id, defaultW, sizeof(defaultW));
 
-    if (flow == eRender)
-        (*keys)[0] = pulse_render_guid;
-    else
-        (*keys)[0] = pulse_capture_guid;
+    if (count && !(key_name = malloc((maxlen + 1) * sizeof(WCHAR))))
+        goto err;
+
+    *ids = HeapAlloc(GetProcessHeap(), 0, (count + 1) * sizeof(**ids));
+    *keys = HeapAlloc(GetProcessHeap(), 0, (count + 1) * sizeof(**keys));
+    if (!*ids || !*keys)
+        goto err;
+
+    if (!(id = HeapAlloc(GetProcessHeap(), 0, sizeof(L"PulseAudio"))))
+        goto err;
+    wcscpy(id, L"PulseAudio");
+    (*ids)[k] = id;
+    (*keys)[k++] = (flow == eRender) ? pulse_render_guid : pulse_capture_guid;
+
+    for (i = 0; i < count; i++) {
+        DWORD key_name_size = maxlen + 1;
+
+        if (RegEnumKeyExW(key, i, key_name, &key_name_size, NULL, NULL, NULL, NULL) != ERROR_SUCCESS ||
+            key_name_size < 3 || key_name[0] != flow_char ||
+            RegOpenKeyExW(key, key_name, 0, KEY_READ | KEY_WRITE | KEY_WOW64_64KEY, &dev_key) != ERROR_SUCCESS)
+            continue;
+
+        status = RegQueryValueExW(dev_key, L"name", 0, &type, NULL, &size);
+        if (status == ERROR_SUCCESS && type == REG_SZ && size >= sizeof(WCHAR)) {
+            if (!(id = HeapAlloc(GetProcessHeap(), 0, size))) {
+                RegCloseKey(dev_key);
+                goto err;
+            }
+            status = RegQueryValueExW(dev_key, L"name", 0, &type, (BYTE*)id, &size);
+            if (status == ERROR_SUCCESS && type == REG_SZ && size >= sizeof(WCHAR)) {
+                id[size / sizeof(WCHAR) - 1] = 0;
+
+                size = sizeof(guid);
+                status = RegQueryValueExW(dev_key, L"guid", 0, &type, (BYTE*)&guid, &size);
+
+                if (status != ERROR_SUCCESS || type != REG_BINARY || size != sizeof(guid)) {
+                    CoCreateGuid(&guid);
+                    status = RegSetValueExW(dev_key, L"guid", 0, REG_BINARY, (BYTE*)&guid, sizeof(guid));
+                    if (status != ERROR_SUCCESS)
+                        ERR("Failed to store device GUID for %s to registry\n", debugstr_w(key_name + 2));
+                }
+                (*ids)[k] = id;
+                (*keys)[k++] = guid;
+            } else {
+                HeapFree(GetProcessHeap(), 0, id);
+            }
+        }
+        RegCloseKey(dev_key);
+    }
+    *num = k;
+    *ids = HeapReAlloc(GetProcessHeap(), 0, *ids, k * sizeof(**ids));
+    *keys = HeapReAlloc(GetProcessHeap(), 0, *keys, k * sizeof(**keys));
 
+    if (key) RegCloseKey(key);
+    free(key_name);
     return S_OK;
+
+err:
+    if (key) RegCloseKey(key);
+    while (k--) HeapFree(GetProcessHeap(), 0, (*ids)[k]);
+    HeapFree(GetProcessHeap(), 0, *keys);
+    HeapFree(GetProcessHeap(), 0, *ids);
+    free(key_name);
+    *ids = NULL;
+    *keys = NULL;
+    return E_OUTOFMEMORY;
 }
 
 int WINAPI AUDDRV_GetPriority(void)
@@ -314,26 +370,89 @@ int WINAPI AUDDRV_GetPriority(void)
     return SUCCEEDED(params.result) ? Priority_Preferred : Priority_Unavailable;
 }
 
+static BOOL get_pulse_name_by_guid(const GUID *guid, char name[256], EDataFlow *flow)
+{
+    DWORD key_name_size;
+    WCHAR key_name[258];
+    DWORD index = 0;
+    HKEY key;
+
+    /* Return empty string for default PulseAudio device */
+    name[0] = 0;
+    if (IsEqualGUID(guid, &pulse_render_guid)) {
+        *flow = eRender;
+        return TRUE;
+    } else if (IsEqualGUID(guid, &pulse_capture_guid)) {
+        *flow = eCapture;
+        return TRUE;
+    }
+
+    if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Wine\\Drivers\\winepulse.drv\\devices",
+            0, KEY_READ | KEY_WOW64_64KEY, &key) != ERROR_SUCCESS) {
+        WARN("No devices found in registry\n");
+        return FALSE;
+    }
+
+    for (;;) {
+        DWORD size, type;
+        LSTATUS status;
+        GUID reg_guid;
+        HKEY dev_key;
+
+        key_name_size = ARRAY_SIZE(key_name);
+        if (RegEnumKeyExW(key, index++, key_name, &key_name_size, NULL,
+                NULL, NULL, NULL) != ERROR_SUCCESS)
+            break;
+
+        if (RegOpenKeyExW(key, key_name, 0, KEY_READ | KEY_WOW64_64KEY, &dev_key) != ERROR_SUCCESS) {
+            ERR("Couldn't open key: %s\n", wine_dbgstr_w(key_name));
+            continue;
+        }
+
+        size = sizeof(reg_guid);
+        status = RegQueryValueExW(dev_key, L"guid", 0, &type, (BYTE *)&reg_guid, &size);
+        RegCloseKey(dev_key);
+
+        if (status == ERROR_SUCCESS && type == REG_BINARY && size == sizeof(reg_guid) && IsEqualGUID(&reg_guid, guid)) {
+            RegCloseKey(key);
+
+            TRACE("Found matching device key: %s\n", wine_dbgstr_w(key_name));
+
+            if (key_name[0] == '0')
+                *flow = eRender;
+            else if (key_name[0] == '1')
+                *flow = eCapture;
+            else {
+                WARN("Unknown device type: %c\n", key_name[0]);
+                return FALSE;
+            }
+
+            return WideCharToMultiByte(CP_UNIXCP, 0, key_name + 2, -1, name, 256, NULL, NULL);
+        }
+    }
+
+    RegCloseKey(key);
+    WARN("No matching device in registry for GUID %s\n", debugstr_guid(guid));
+    return FALSE;
+}
+
 HRESULT WINAPI AUDDRV_GetAudioEndpoint(GUID *guid, IMMDevice *dev, IAudioClient **out)
 {
-    ACImpl *This;
+    ACImpl *This = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*This));
     EDataFlow dataflow;
     HRESULT hr;
 
     TRACE("%s %p %p\n", debugstr_guid(guid), dev, out);
-    if (IsEqualGUID(guid, &pulse_render_guid))
-        dataflow = eRender;
-    else if (IsEqualGUID(guid, &pulse_capture_guid))
-        dataflow = eCapture;
-    else
-        return E_UNEXPECTED;
 
     *out = NULL;
-
-    This = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*This));
     if (!This)
         return E_OUTOFMEMORY;
 
+    if (!get_pulse_name_by_guid(guid, This->device, &dataflow)) {
+        HeapFree(GetProcessHeap(), 0, This);
+        return AUDCLNT_E_DEVICE_INVALIDATED;
+    }
+
     This->IAudioClient3_iface.lpVtbl = &AudioClient3_Vtbl;
     This->IAudioRenderClient_iface.lpVtbl = &AudioRenderClient_Vtbl;
     This->IAudioCaptureClient_iface.lpVtbl = &AudioCaptureClient_Vtbl;
@@ -609,6 +728,7 @@ static HRESULT WINAPI AudioClient_Initialize(IAudioClient3 *iface,
     }
 
     params.name = name = get_application_name();
+    params.device   = This->device;
     params.dataflow = This->dataflow;
     params.mode     = mode;
     params.flags    = flags;
diff --git a/dlls/winepulse.drv/pulse.c b/dlls/winepulse.drv/pulse.c
index 3e65936..8934387 100644
--- a/dlls/winepulse.drv/pulse.c
+++ b/dlls/winepulse.drv/pulse.c
@@ -81,6 +81,11 @@ typedef struct _ACPacket
     UINT32 discont;
 } ACPacket;
 
+typedef struct _PhysDevice {
+    struct list entry;
+    char device[0];
+} PhysDevice;
+
 static pa_context *pulse_ctx;
 static pa_mainloop *pulse_ml;
 
@@ -89,6 +94,9 @@ static WAVEFORMATEXTENSIBLE pulse_fmt[2];
 static REFERENCE_TIME pulse_min_period[2], pulse_def_period[2];
 
 static UINT g_phys_speakers_mask = 0;
+static struct list g_phys_speakers = LIST_INIT(g_phys_speakers);
+static struct list g_phys_sources = LIST_INIT(g_phys_sources);
+static HKEY devices_key;
 
 static const REFERENCE_TIME MinimumPeriod = 30000;
 static const REFERENCE_TIME DefaultPeriod = 100000;
@@ -128,6 +136,16 @@ static void dump_attr(const pa_buffer_attr *attr)
     TRACE("prebuf: %u\n", attr->prebuf);
 }
 
+static void free_phys_device_lists(void)
+{
+    PhysDevice *dev, *dev_next;
+
+    LIST_FOR_EACH_ENTRY_SAFE(dev, dev_next, &g_phys_speakers, PhysDevice, entry)
+        free(dev);
+    LIST_FOR_EACH_ENTRY_SAFE(dev, dev_next, &g_phys_sources, PhysDevice, entry)
+        free(dev);
+}
+
 /* copied from kernelbase */
 static int muldiv(int a, int b, int c)
 {
@@ -152,6 +170,61 @@ static int muldiv(int a, int b, int c)
     return ret;
 }
 
+/* wrapper for NtCreateKey that creates the key recursively if necessary */
+static HKEY reg_create_key(HKEY root, const WCHAR *name, ULONG name_len)
+{
+    UNICODE_STRING nameW = { name_len, name_len, (WCHAR *)name };
+    OBJECT_ATTRIBUTES attr;
+    NTSTATUS status;
+    HANDLE ret = 0;
+
+    attr.Length = sizeof(attr);
+    attr.RootDirectory = root;
+    attr.ObjectName = &nameW;
+    attr.Attributes = 0;
+    attr.SecurityDescriptor = NULL;
+    attr.SecurityQualityOfService = NULL;
+
+    status = NtCreateKey(&ret, KEY_QUERY_VALUE | KEY_WRITE | KEY_WOW64_64KEY, &attr, 0, NULL, 0, NULL);
+    if (status == STATUS_OBJECT_NAME_NOT_FOUND) {
+        DWORD pos = 0, i = 0, len = name_len / sizeof(WCHAR);
+
+        /* don't try to create registry root */
+        if (!root) i += 10;
+
+        while (i < len && name[i] != '\\') i++;
+        if (i == len) return 0;
+        for (;;) {
+            nameW.Buffer = (WCHAR *)name + pos;
+            nameW.Length = (i - pos) * sizeof(WCHAR);
+            status = NtCreateKey(&ret, KEY_QUERY_VALUE | KEY_WRITE | KEY_WOW64_64KEY, &attr, 0, NULL, 0, NULL);
+
+            if (attr.RootDirectory != root) NtClose(attr.RootDirectory);
+            if (!NT_SUCCESS(status)) return 0;
+            if (i == len) break;
+            attr.RootDirectory = ret;
+            while (i < len && name[i] == '\\') i++;
+            pos = i;
+            while (i < len && name[i] != '\\') i++;
+        }
+    }
+    return ret;
+}
+
+static HKEY open_devices_key(void)
+{
+    static const WCHAR drv_key_devicesW[] = {
+        '\\','R','e','g','i','s','t','r','y','\\','M','a','c','h','i','n','e','\\',
+        'S','o','f','t','w','a','r','e','\\','W','i','n','e','\\','D','r','i','v','e','r','s','\\',
+        'w','i','n','e','p','u','l','s','e','.','d','r','v','\\','d','e','v','i','c','e','s'
+    };
+    HANDLE ret;
+
+    if (!(ret = reg_create_key(NULL, drv_key_devicesW, sizeof(drv_key_devicesW))))
+        ERR("Failed to open devices registry key\n");
+    return ret;
+}
+
 /* Following pulseaudio design here, mainloop has the lock taken whenever
  * it is handling something for pulse, and the lock is required whenever
  * doing any pa_* call that can affect the state in any way
@@ -190,6 +263,7 @@ static NTSTATUS pulse_process_attach(void *args)
 
 static NTSTATUS pulse_process_detach(void *args)
 {
+    free_phys_device_lists();
     if (pulse_ctx)
     {
         pa_context_disconnect(pulse_ctx);
@@ -357,12 +431,73 @@ static DWORD pulse_channel_map_to_channel_mask(const pa_channel_map *map)
     return mask;
 }
 
-/* For default PulseAudio render device, OR together all of the
- * PKEY_AudioEndpoint_PhysicalSpeakers values of the sinks. */
+static void store_device_info(EDataFlow flow, const char *device, const char *name)
+{
+    static const WCHAR nameW[] = { 'n','a','m','e' };
+    UNICODE_STRING name_str = { sizeof(nameW), sizeof(nameW), (WCHAR*)nameW };
+    UINT name_len = strlen(name);
+    WCHAR key_name[258], *wname;
+    DWORD len, key_len;
+    HKEY key;
+
+    if (!devices_key || !(wname = malloc((name_len + 1) * sizeof(WCHAR))))
+        return;
+
+    key_name[0] = (flow == eCapture) ? '1' : '0';
+    key_name[1] = ',';
+
+    key_len = ntdll_umbstowcs(device, strlen(device), key_name + 2, ARRAY_SIZE(key_name) - 2);
+    if (!key_len || key_len >= ARRAY_SIZE(key_name) - 2)
+        goto done;
+    key_len += 2;
+
+    if (!(len = ntdll_umbstowcs(name, name_len, wname, name_len)))
+        goto done;
+    wname[len] = 0;
+
+    if (!(key = reg_create_key(devices_key, key_name, key_len * sizeof(WCHAR)))) {
+        ERR("Failed to open registry key for device %s\n", device);
+        goto done;
+    }
+
+    if (NtSetValueKey(key, &name_str, 0, REG_SZ, wname, (len + 1) * sizeof(WCHAR)))
+        ERR("Failed to store name for %s to registry\n", device);
+    NtClose(key);
+
+done:
+    free(wname);
+}
+
+static void pulse_add_device(struct list *list, const char *device)
+{
+    DWORD len = strlen(device);
+    PhysDevice *dev = malloc(FIELD_OFFSET(PhysDevice, device[len + 1]));
+
+    if (!dev)
+        return;
+    memcpy(dev->device, device, len + 1);
+
+    list_add_tail(list, &dev->entry);
+}
+
 static void pulse_phys_speakers_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata)
 {
-    if (i)
+    if (i && i->name && i->name[0]) {
+        /* For default PulseAudio render device, OR together all of the
+         * PKEY_AudioEndpoint_PhysicalSpeakers values of the sinks. */
         g_phys_speakers_mask |= pulse_channel_map_to_channel_mask(&i->channel_map);
+
+        store_device_info(eRender, i->name, i->description);
+        pulse_add_device(&g_phys_speakers, i->name);
+    }
+}
+
+static void pulse_phys_sources_cb(pa_context *c, const pa_source_info *i, int eol, void *userdata)
+{
+    if (i && i->name && i->name[0]) {
+        store_device_info(eCapture, i->name, i->description);
+        pulse_add_device(&g_phys_sources, i->name);
+    }
 }
 
 /* For most hardware on Windows, users must choose a configuration with an even
@@ -579,7 +714,15 @@ static NTSTATUS pulse_test_connect(void *args)
     pulse_probe_settings(1, &pulse_fmt[0]);
     pulse_probe_settings(0, &pulse_fmt[1]);
 
+    free_phys_device_lists();
+    list_init(&g_phys_speakers);
+    list_init(&g_phys_sources);
     g_phys_speakers_mask = 0;
+
+    devices_key = open_devices_key();
+    pulse_add_device(&g_phys_speakers, "");
+    pulse_add_device(&g_phys_sources, "");
+
     o = pa_context_get_sink_info_list(pulse_ctx, &pulse_phys_speakers_cb, NULL);
     if (o) {
         while (pa_mainloop_iterate(pulse_ml, 1, &ret) >= 0 &&
@@ -588,6 +731,15 @@ static NTSTATUS pulse_test_connect(void *args)
         pa_operation_unref(o);
     }
 
+    o = pa_context_get_source_info_list(pulse_ctx, &pulse_phys_sources_cb, NULL);
+    if (o) {
+        while (pa_mainloop_iterate(pulse_ml, 1, &ret) >= 0 &&
+                pa_operation_get_state(o) == PA_OPERATION_RUNNING)
+        {}
+        pa_operation_unref(o);
+    }
+    NtClose(devices_key);
+
     pa_context_unref(pulse_ctx);
     pulse_ctx = NULL;
     pa_mainloop_free(pulse_ml);
@@ -771,8 +923,9 @@ static HRESULT pulse_spec_from_waveformat(struct pulse_stream *stream, const WAV
     return S_OK;
 }
 
-static HRESULT pulse_stream_connect(struct pulse_stream *stream, UINT32 period_bytes)
+static HRESULT pulse_stream_connect(struct pulse_stream *stream, const char *device, UINT32 period_bytes)
 {
+    pa_stream_flags_t flags = PA_STREAM_START_CORKED | PA_STREAM_START_UNMUTED | PA_STREAM_ADJUST_LATENCY;
     int ret;
     char buffer[64];
     static LONG number;
@@ -797,12 +950,17 @@ static HRESULT pulse_stream_connect(struct pulse_stream *stream, UINT32 period_b
     attr.maxlength = stream->bufsize_frames * pa_frame_size(&stream->ss);
     attr.prebuf = pa_frame_size(&stream->ss);
     dump_attr(&attr);
+
+    /* If device name is given, use exactly the specified device */
+    if (device[0])
+        flags |= PA_STREAM_DONT_MOVE;
+    else
+        device = NULL;  /* use default */
+
     if (stream->dataflow == eRender)
-        ret = pa_stream_connect_playback(stream->stream, NULL, &attr,
-        PA_STREAM_START_CORKED|PA_STREAM_START_UNMUTED|PA_STREAM_ADJUST_LATENCY, NULL, NULL);
+        ret = pa_stream_connect_playback(stream->stream, device, &attr, flags, NULL, NULL);
     else
-        ret = pa_stream_connect_record(stream->stream, NULL, &attr,
-        PA_STREAM_START_CORKED|PA_STREAM_START_UNMUTED|PA_STREAM_ADJUST_LATENCY);
+        ret = pa_stream_connect_record(stream->stream, device, &attr, flags);
     if (ret < 0) {
         WARN("Returns %i\n", ret);
         return AUDCLNT_E_ENDPOINT_CREATE_FAILED;
@@ -864,7 +1022,7 @@ static NTSTATUS pulse_create_stream(void *args)
 
     stream->share = params->mode;
     stream->flags = params->flags;
-    hr = pulse_stream_connect(stream, stream->period_bytes);
+    hr = pulse_stream_connect(stream, params->device, stream->period_bytes);
     if (SUCCEEDED(hr)) {
         UINT32 unalign;
         const pa_buffer_attr *attr = pa_stream_get_buffer_attr(stream->stream);
diff --git a/dlls/winepulse.drv/unixlib.h b/dlls/winepulse.drv/unixlib.h
index d28a73c..5445a0f 100644
--- a/dlls/winepulse.drv/unixlib.h
+++ b/dlls/winepulse.drv/unixlib.h
@@ -40,6 +40,7 @@ struct main_loop_params
 struct create_stream_params
 {
     const char *name;
+    const char *device;
     EDataFlow dataflow;
     AUDCLNT_SHAREMODE mode;
     DWORD flags;
-- 
2.34.1




More information about the wine-devel mailing list