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

Gabriel Ivăncescu gabrielopcode at gmail.com
Fri Feb 18 12:37:59 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,
with the same GUID to preserve compatibility with existing setups.

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>
---

Minor issue when allocating, it multiplied by sizeof(WCHAR) again...

 dlls/winepulse.drv/mmdevdrv.c | 205 ++++++++++++++++++++++++++++------
 dlls/winepulse.drv/pulse.c    | 140 +++++++++++++++++++++--
 dlls/winepulse.drv/unixlib.h  |  20 ++++
 3 files changed, 321 insertions(+), 44 deletions(-)

diff --git a/dlls/winepulse.drv/mmdevdrv.c b/dlls/winepulse.drv/mmdevdrv.c
index e3d5ea9..8ef3575 100644
--- a/dlls/winepulse.drv/mmdevdrv.c
+++ b/dlls/winepulse.drv/mmdevdrv.c
@@ -69,6 +69,8 @@ static GUID pulse_render_guid =
 static GUID pulse_capture_guid =
 { 0x25da76d0, 0x033c, 0x4235, { 0x90, 0x02, 0x19, 0xf4, 0x88, 0x94, 0xac, 0x6f } };
 
+static const WCHAR *drv_key_devicesW = L"Software\\Wine\\Drivers\\winepulse.drv\\devices";
+
 static CRITICAL_SECTION session_cs;
 static CRITICAL_SECTION_DEBUG session_cs_debug = {
     0, 0, &session_cs,
@@ -145,9 +147,9 @@ struct ACImpl {
 
     AudioSession *session;
     AudioSessionWrapper *session_wrapper;
-};
 
-static const WCHAR defaultW[] = L"PulseAudio";
+    char pulse_name[0];
+};
 
 static const IAudioClient3Vtbl AudioClient3_Vtbl;
 static const IAudioRenderClientVtbl AudioRenderClient_Vtbl;
@@ -267,39 +269,108 @@ static void set_stream_volumes(ACImpl *This)
     pulse_call(set_volumes, &params);
 }
 
-HRESULT WINAPI AUDDRV_GetEndpointIDs(EDataFlow flow, const WCHAR ***ids, GUID **keys,
-        UINT *num, UINT *def_index)
+static void get_device_guid(HKEY drv_key, EDataFlow flow, const char *pulse_name, GUID *guid)
 {
-    WCHAR *id;
+    WCHAR key_name[MAX_PULSE_NAME_LEN + 2];
+    DWORD type, size = sizeof(*guid);
+    LSTATUS status;
+    HKEY dev_key;
 
-    TRACE("%d %p %p %p\n", flow, ids, num, def_index);
+    if (!pulse_name[0]) {
+        *guid = (flow == eRender) ? pulse_render_guid : pulse_capture_guid;
+        return;
+    }
 
-    *num = 1;
-    *def_index = 0;
+    if (!drv_key) {
+        CoCreateGuid(guid);
+        return;
+    }
 
-    *ids = HeapAlloc(GetProcessHeap(), 0, sizeof(**ids));
-    *keys = NULL;
-    if (!*ids)
-        return E_OUTOFMEMORY;
+    key_name[0] = (flow == eRender) ? '0' : '1';
+    key_name[1] = ',';
+    MultiByteToWideChar(CP_UNIXCP, 0, pulse_name, -1, key_name + 2, ARRAY_SIZE(key_name) - 2);
 
-    (*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;
+    status = RegCreateKeyExW(drv_key, key_name, 0, NULL, 0, KEY_READ | KEY_WRITE | KEY_WOW64_64KEY,
+                             NULL, &dev_key, NULL);
+    if (status != ERROR_SUCCESS) {
+        ERR("Failed to open registry key for device %s: %u\n", pulse_name, status);
+        CoCreateGuid(guid);
+        return;
     }
-    memcpy(id, defaultW, sizeof(defaultW));
 
-    if (flow == eRender)
-        (*keys)[0] = pulse_render_guid;
-    else
-        (*keys)[0] = pulse_capture_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: %u\n", pulse_name, status);
+    }
+    RegCloseKey(dev_key);
+}
 
-    return S_OK;
+HRESULT WINAPI AUDDRV_GetEndpointIDs(EDataFlow flow, WCHAR ***ids_out, GUID **keys,
+        UINT *num, UINT *def_index)
+{
+    struct get_endpoint_ids_params params;
+    GUID *guids = NULL;
+    WCHAR **ids = NULL;
+    unsigned int i = 0;
+    LSTATUS status;
+    HKEY drv_key;
+
+    TRACE("%d %p %p %p\n", flow, ids_out, num, def_index);
+
+    params.flow = flow;
+    params.size = MAX_PULSE_NAME_LEN * 4;
+    params.endpoints = NULL;
+    do {
+        HeapFree(GetProcessHeap(), 0, params.endpoints);
+        params.endpoints = HeapAlloc(GetProcessHeap(), 0, params.size);
+        pulse_call(get_endpoint_ids, &params);
+    } while(params.result == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER));
+
+    if (FAILED(params.result))
+        goto end;
+
+    ids = HeapAlloc(GetProcessHeap(), 0, params.num * sizeof(*ids));
+    guids = HeapAlloc(GetProcessHeap(), 0, params.num * sizeof(*guids));
+    if (!ids || !guids) {
+        params.result = E_OUTOFMEMORY;
+        goto end;
+    }
+
+    status = RegCreateKeyExW(HKEY_CURRENT_USER, drv_key_devicesW, 0, NULL, 0,
+                             KEY_WRITE | KEY_WOW64_64KEY, NULL, &drv_key, NULL);
+    if (status != ERROR_SUCCESS) {
+        ERR("Failed to open devices registry key: %u\n", status);
+        drv_key = NULL;
+    }
+
+    for (i = 0; i < params.num; i++) {
+        unsigned int size = (wcslen(params.endpoints[i].name) + 1) * sizeof(WCHAR);
+        if (!(ids[i] = HeapAlloc(GetProcessHeap(), 0, size))) {
+            params.result = E_OUTOFMEMORY;
+            break;
+        }
+        memcpy(ids[i], params.endpoints[i].name, size);
+        get_device_guid(drv_key, flow, params.endpoints[i].pulse_name, &guids[i]);
+    }
+    if (drv_key)
+        RegCloseKey(drv_key);
+
+end:
+    HeapFree(GetProcessHeap(), 0, params.endpoints);
+    if (FAILED(params.result)) {
+        HeapFree(GetProcessHeap(), 0, guids);
+        while (i--) HeapFree(GetProcessHeap(), 0, ids[i]);
+        HeapFree(GetProcessHeap(), 0, ids);
+    } else {
+        *ids_out = ids;
+        *keys = guids;
+        *num = params.num;
+        *def_index = params.default_idx;
+    }
+    return params.result;
 }
 
 int WINAPI AUDDRV_GetPriority(void)
@@ -314,23 +385,87 @@ int WINAPI AUDDRV_GetPriority(void)
     return SUCCEEDED(params.result) ? Priority_Preferred : Priority_Unavailable;
 }
 
+static BOOL get_pulse_name_by_guid(const GUID *guid, char pulse_name[MAX_PULSE_NAME_LEN], EDataFlow *flow)
+{
+    WCHAR key_name[MAX_PULSE_NAME_LEN + 2];
+    DWORD key_name_size;
+    DWORD index = 0;
+    HKEY key;
+
+    /* Return empty string for default PulseAudio device */
+    pulse_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_CURRENT_USER, drv_key_devicesW, 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, pulse_name, MAX_PULSE_NAME_LEN, 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;
+    char pulse_name[MAX_PULSE_NAME_LEN];
     EDataFlow dataflow;
+    unsigned len;
     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;
+
+    if (!get_pulse_name_by_guid(guid, pulse_name, &dataflow))
+        return AUDCLNT_E_DEVICE_INVALIDATED;
 
     *out = NULL;
 
-    This = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*This));
+    len = strlen(pulse_name) + 1;
+    This = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, FIELD_OFFSET(ACImpl, pulse_name[len]));
     if (!This)
         return E_OUTOFMEMORY;
 
@@ -342,6 +477,7 @@ HRESULT WINAPI AUDDRV_GetAudioEndpoint(GUID *guid, IMMDevice *dev, IAudioClient
     This->IAudioStreamVolume_iface.lpVtbl = &AudioStreamVolume_Vtbl;
     This->dataflow = dataflow;
     This->parent = dev;
+    memcpy(This->pulse_name, pulse_name, len);
 
     hr = CoCreateFreeThreadedMarshaler((IUnknown*)&This->IAudioClient3_iface, &This->marshal);
     if (FAILED(hr)) {
@@ -609,6 +745,7 @@ static HRESULT WINAPI AudioClient_Initialize(IAudioClient3 *iface,
     }
 
     params.name = name = get_application_name();
+    params.pulse_name  = This->pulse_name;
     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..311c9bb 100644
--- a/dlls/winepulse.drv/pulse.c
+++ b/dlls/winepulse.drv/pulse.c
@@ -81,6 +81,12 @@ typedef struct _ACPacket
     UINT32 discont;
 } ACPacket;
 
+typedef struct _PhysDevice {
+    struct list entry;
+    WCHAR *name;
+    char pulse_name[0];
+} PhysDevice;
+
 static pa_context *pulse_ctx;
 static pa_mainloop *pulse_ml;
 
@@ -89,6 +95,8 @@ 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 const REFERENCE_TIME MinimumPeriod = 30000;
 static const REFERENCE_TIME DefaultPeriod = 100000;
@@ -128,6 +136,20 @@ static void dump_attr(const pa_buffer_attr *attr)
     TRACE("prebuf: %u\n", attr->prebuf);
 }
 
+static void free_phys_device_lists(void)
+{
+    static struct list *const lists[] = { &g_phys_speakers, &g_phys_sources, NULL };
+    struct list *const *list = lists;
+    PhysDevice *dev, *dev_next;
+
+    do {
+        LIST_FOR_EACH_ENTRY_SAFE(dev, dev_next, *list, PhysDevice, entry) {
+            free(dev->name);
+            free(dev);
+        }
+    } while (*(++list));
+}
+
 /* copied from kernelbase */
 static int muldiv(int a, int b, int c)
 {
@@ -190,6 +212,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);
@@ -215,6 +238,44 @@ static NTSTATUS pulse_main_loop(void *args)
     return STATUS_SUCCESS;
 }
 
+static NTSTATUS pulse_get_endpoint_ids(void *args)
+{
+    struct get_endpoint_ids_params *params = args;
+    struct list *list = (params->flow == eRender) ? &g_phys_speakers : &g_phys_sources;
+    struct endpoint *endpoint = params->endpoints;
+    DWORD len, name_len, needed;
+    PhysDevice *dev;
+    char *ptr;
+
+    params->num = list_count(list);
+    needed = params->num * sizeof(*params->endpoints);
+    ptr = (char*)(endpoint + params->num);
+
+    LIST_FOR_EACH_ENTRY(dev, list, PhysDevice, entry) {
+        name_len = lstrlenW(dev->name) + 1;
+        len = strlen(dev->pulse_name) + 1;
+        needed += name_len * sizeof(WCHAR) + ((len + 1) & ~1);
+
+        if (needed <= params->size) {
+            endpoint->name = (WCHAR*)ptr;
+            memcpy(endpoint->name, dev->name, name_len * sizeof(WCHAR));
+            ptr += name_len * sizeof(WCHAR);
+            endpoint->pulse_name = ptr;
+            memcpy(endpoint->pulse_name, dev->pulse_name, len);
+            ptr += (len + 1) & ~1;
+            endpoint++;
+        }
+    }
+    params->default_idx = 0;
+
+    if (needed > params->size) {
+        params->size = needed;
+        params->result = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+    } else
+        params->result = S_OK;
+    return STATUS_SUCCESS;
+}
+
 static void pulse_contextcallback(pa_context *c, void *userdata)
 {
     switch (pa_context_get_state(c)) {
@@ -357,12 +418,49 @@ 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 pulse_add_device(struct list *list, const char *pulse_name, const char *name)
+{
+    DWORD len = strlen(pulse_name), name_len = strlen(name);
+    PhysDevice *dev = malloc(FIELD_OFFSET(PhysDevice, pulse_name[len + 1]));
+    WCHAR *wname;
+
+    if (!dev)
+        return;
+
+    if (!(wname = malloc((name_len + 1) * sizeof(WCHAR)))) {
+        free(dev);
+        return;
+    }
+
+    if (!(name_len = ntdll_umbstowcs(name, name_len, wname, name_len)) ||
+        !(dev->name = realloc(wname, (name_len + 1) * sizeof(WCHAR)))) {
+        free(wname);
+        free(dev);
+        return;
+    }
+    dev->name[name_len] = 0;
+    memcpy(dev->pulse_name, pulse_name, 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)
-        g_phys_speakers_mask |= pulse_channel_map_to_channel_mask(&i->channel_map);
+    if (!i || !i->name || !i->name[0])
+        return;
+
+    /* 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);
+
+    pulse_add_device(&g_phys_speakers, i->name, i->description);
+}
+
+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])
+        return;
+    pulse_add_device(&g_phys_sources, i->name, i->description);
 }
 
 /* For most hardware on Windows, users must choose a configuration with an even
@@ -579,7 +677,14 @@ 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;
+
+    pulse_add_device(&g_phys_speakers, "", "PulseAudio");
+    pulse_add_device(&g_phys_sources, "", "PulseAudio");
+
     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 +693,14 @@ 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);
+    }
+
     pa_context_unref(pulse_ctx);
     pulse_ctx = NULL;
     pa_mainloop_free(pulse_ml);
@@ -771,8 +884,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 *pulse_name, 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 +911,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 specific device was requested, use it exactly */
+    if (pulse_name[0])
+        flags |= PA_STREAM_DONT_MOVE;
+    else
+        pulse_name = 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, pulse_name, &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, pulse_name, &attr, flags);
     if (ret < 0) {
         WARN("Returns %i\n", ret);
         return AUDCLNT_E_ENDPOINT_CREATE_FAILED;
@@ -864,7 +983,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->pulse_name, stream->period_bytes);
     if (SUCCEEDED(hr)) {
         UINT32 unalign;
         const pa_buffer_attr *attr = pa_stream_get_buffer_attr(stream->stream);
@@ -1967,6 +2086,7 @@ const unixlib_entry_t __wine_unix_call_funcs[] =
     pulse_process_attach,
     pulse_process_detach,
     pulse_main_loop,
+    pulse_get_endpoint_ids,
     pulse_create_stream,
     pulse_release_stream,
     pulse_start,
diff --git a/dlls/winepulse.drv/unixlib.h b/dlls/winepulse.drv/unixlib.h
index d28a73c..982704a 100644
--- a/dlls/winepulse.drv/unixlib.h
+++ b/dlls/winepulse.drv/unixlib.h
@@ -19,6 +19,8 @@
 #include "wine/list.h"
 #include "wine/unixlib.h"
 
+#define MAX_PULSE_NAME_LEN 256
+
 struct pulse_stream;
 
 struct pulse_config
@@ -32,14 +34,31 @@ struct pulse_config
     unsigned int speakers_mask;
 };
 
+struct endpoint
+{
+    WCHAR *name;
+    char *pulse_name;
+};
+
 struct main_loop_params
 {
     HANDLE event;
 };
 
+struct get_endpoint_ids_params
+{
+    EDataFlow flow;
+    struct endpoint *endpoints;
+    unsigned int size;
+    HRESULT result;
+    unsigned int num;
+    unsigned int default_idx;
+};
+
 struct create_stream_params
 {
     const char *name;
+    const char *pulse_name;
     EDataFlow dataflow;
     AUDCLNT_SHAREMODE mode;
     DWORD flags;
@@ -191,6 +210,7 @@ enum unix_funcs
     process_attach,
     process_detach,
     main_loop,
+    get_endpoint_ids,
     create_stream,
     release_stream,
     start,
-- 
2.34.1




More information about the wine-devel mailing list