File size: 10,611 Bytes
d82cf6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
from pyglet.libs.win32 import com
from pyglet.libs.win32 import _ole32 as ole32
from pyglet.libs.win32.constants import CLSCTX_INPROC_SERVER
from pyglet.libs.win32.types import *
from pyglet.media.devices import base
from pyglet.util import debug_print

_debug = debug_print('debug_media')

EDataFlow = UINT
# Audio rendering stream. Audio data flows from the application to the audio endpoint device, which renders the stream.
eRender = 0

# Audio capture stream. Audio data flows from the audio endpoint device that captures the stream, to the application.
eCapture = 1

# Audio rendering or capture stream. Audio data can flow either from the application to the audio endpoint device,
# or from the audio endpoint device to the application.
eAll = 2

EDataFlow_enum_count = 3

ERole = UINT
eConsole = 0  # Games, system notification sounds, and voice commands.
eMultimedia = 1  # Music, movies, narration, and live music recording.
eCommunications = 2  # Voice communications (talking to another person).
ERole_enum_count = 3

DEVICE_STATE_ACTIVE = 0x00000001
DEVICE_STATE_DISABLED = 0x00000002
DEVICE_STATE_NOTPRESENT = 0x00000004
DEVICE_STATE_UNPLUGGED = 0x00000008
DEVICE_STATEMASK_ALL = 0x0000000F

STGM_READ = 0
STGM_WRITE = 1
STGM_READWRITE = 2

VT_LPWSTR = 0x001F


class PROPERTYKEY(ctypes.Structure):
    _fields_ = [
        ('fmtid', com.GUID),
        ('pid', DWORD),
    ]

    def __repr__(self):
        return "PROPERTYKEY({}, pid={})".format(self.fmtid, self.pid)


REFPROPERTYKEY = PROPERTYKEY

PKEY_Device_FriendlyName = PROPERTYKEY(
    com.GUID(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14)
PKEY_Device_DeviceDesc = PROPERTYKEY(
    com.GUID(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 2)
PKEY_DeviceInterface_FriendlyName = PROPERTYKEY(
    com.GUID(0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22), 2)


class IPropertyStore(com.pIUnknown):
    _methods_ = [
        ('GetCount',
         com.STDMETHOD(POINTER(DWORD))),
        ('GetAt',
         com.STDMETHOD(DWORD, POINTER(PROPERTYKEY))),
        ('GetValue',
         com.STDMETHOD(REFPROPERTYKEY, POINTER(PROPVARIANT))),
        ('SetValue',
         com.STDMETHOD()),
        ('Commit',
         com.STDMETHOD()),
    ]


CLSID_MMDeviceEnumerator = com.GUID(0xbcde0395, 0xe52f, 0x467c, 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e)
IID_IMMDeviceEnumerator = com.GUID(0xa95664d2, 0x9614, 0x4f35, 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6)


class IMMNotificationClient(com.IUnknown):
    _methods_ = [
        ('OnDeviceStateChanged',
         com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, DWORD)),
        ('OnDeviceAdded',
         com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
        ('OnDeviceRemoved',
         com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
        ('OnDefaultDeviceChanged',
         com.METHOD(ctypes.c_void_p, ctypes.c_void_p, EDataFlow, ERole, LPCWSTR)),
        ('OnPropertyValueChanged',
         com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, PROPERTYKEY)),
    ]


class AudioNotificationCB(com.COMObject):
    _interfaces_ = [IMMNotificationClient]

    def __init__(self, audio_devices):
        super().__init__()
        self.audio_devices = audio_devices
        self._lost = False

    def OnDeviceStateChanged(self, this, pwstrDeviceId, dwNewState):
        device = self.audio_devices.get_cached_device(pwstrDeviceId)

        old_state = device.state
        assert _debug(
            "Audio device {} changed state. From state: {} to state: {}".format(device.name, old_state, dwNewState))

        device.state = dwNewState
        self.audio_devices.dispatch_event('on_device_state_changed', device, old_state, dwNewState)

    def OnDeviceAdded(self, this, pwstrDeviceId):
        assert _debug("Audio device was added {}".format(pwstrDeviceId))
        self.audio_devices.dispatch_event('on_device_added', pwstrDeviceId)

    def OnDeviceRemoved(self, this, pwstrDeviceId):
        assert _debug("Audio device was removed {}".format(pwstrDeviceId))
        self.audio_devices.dispatch_event('on_device_removed', pwstrDeviceId)

    def OnDefaultDeviceChanged(self, this, flow, role, pwstrDeviceId):
        # Only support eConsole role right now
        if role == 0:
            if pwstrDeviceId is None:
                device = None
            else:
                device = self.audio_devices.get_cached_device(pwstrDeviceId)

            self.audio_devices.dispatch_event('on_default_changed', device)

    def OnPropertyValueChanged(self, this, pwstrDeviceId, key):
        pass


class IMMDevice(com.pIUnknown):
    _methods_ = [
        ('Activate',
         com.STDMETHOD(com.REFIID, DWORD, POINTER(PROPVARIANT))),
        ('OpenPropertyStore',
         com.STDMETHOD(UINT, POINTER(IPropertyStore))),
        ('GetId',
         com.STDMETHOD(POINTER(LPWSTR))),
        ('GetState',
         com.STDMETHOD(POINTER(DWORD))),
    ]


class IMMDeviceCollection(com.pIUnknown):
    _methods_ = [
        ('GetCount',
         com.STDMETHOD(POINTER(UINT))),
        ('Item',
         com.STDMETHOD(UINT, POINTER(IMMDevice))),
    ]


class IMMDeviceEnumerator(com.pIUnknown):
    _methods_ = [
        ('EnumAudioEndpoints',
         com.STDMETHOD(EDataFlow, DWORD, c_void_p)),
        ('GetDefaultAudioEndpoint',
         com.STDMETHOD(EDataFlow, ERole, ctypes.POINTER(IMMDevice))),
        ('GetDevice',
         com.STDMETHOD(LPCWSTR, POINTER(IMMDevice))),
        ('RegisterEndpointNotificationCallback',
         com.STDMETHOD(POINTER(IMMNotificationClient))),
        ('UnregisterEndpointNotificationCallback',
         com.STDMETHOD()),
    ]


class Win32AudioDevice(base.AudioDevice):
    _platform_state = {
        DEVICE_STATE_ACTIVE: base.DeviceState.ACTIVE,
        DEVICE_STATE_DISABLED: base.DeviceState.DISABLED,
        DEVICE_STATE_NOTPRESENT: base.DeviceState.MISSING,
        DEVICE_STATE_UNPLUGGED: base.DeviceState.UNPLUGGED
    }

    _platform_flow = {
        eRender: base.DeviceFlow.OUTPUT,
        eCapture: base.DeviceFlow.INPUT,
        eAll: base.DeviceFlow.INPUT_OUTPUT
    }


class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
    def __init__(self):
        self._device_enum = IMMDeviceEnumerator()
        ole32.CoCreateInstance(CLSID_MMDeviceEnumerator, None, CLSCTX_INPROC_SERVER, IID_IMMDeviceEnumerator,
                               byref(self._device_enum))

        # Keep all devices cached, and the callback can keep them updated.
        self.devices = self._query_all_devices()

        super().__init__()

        self._callback = AudioNotificationCB(self)
        self._device_enum.RegisterEndpointNotificationCallback(self._callback)

    def get_default_output(self):
        """Attempts to retrieve a default audio output for the system. Returns None if no available devices found."""
        try:
            device = IMMDevice()
            self._device_enum.GetDefaultAudioEndpoint(eRender, eConsole, byref(device))
            dev_id, name, desc, dev_state = self.get_device_info(device)
            device.Release()

            pid = self.get_cached_device(dev_id)
            pid.state = dev_state
            return pid
        except OSError:
            assert _debug("No default audio output was found.")
            return None

    def get_default_input(self):
        """Attempts to retrieve a default audio input for the system. Returns None if no available devices found."""
        try:
            device = IMMDevice()
            self._device_enum.GetDefaultAudioEndpoint(eCapture, eConsole, byref(device))
            dev_id, name, desc, dev_state = self.get_device_info(device)
            device.Release()

            pid = self.get_cached_device(dev_id)
            pid.state = dev_state
            return pid
        except OSError:
            assert _debug("No default input output was found.")
            return None

    def get_cached_device(self, dev_id):
        """Gets the cached devices, so we can reduce calls to COM and tell current state vs new states."""
        for device in self.devices:
            if device.id == dev_id:
                return device

        raise Exception("Attempted to get a device that does not exist.")

        # return None

    def get_output_devices(self, state=DEVICE_STATE_ACTIVE):
        return [device for device in self.devices if device.state == state and device.flow == eRender]

    def get_input_devices(self, state=DEVICE_STATE_ACTIVE):
        return [device for device in self.devices if device.state == state and device.flow == eCapture]

    def get_all_devices(self):
        return self.devices

    def _query_all_devices(self):
        return self.get_devices(flow=eRender, state=DEVICE_STATEMASK_ALL) + self.get_devices(flow=eCapture,
                                                                                             state=DEVICE_STATEMASK_ALL)

    def get_device_info(self, device):
        """Return the ID, Name, and Description of the Audio Device."""
        store = IPropertyStore()
        device.OpenPropertyStore(STGM_READ, byref(store))

        dev_id = LPWSTR()
        device.GetId(byref(dev_id))

        name = self.get_pkey_value(store, PKEY_Device_FriendlyName)
        description = self.get_pkey_value(store, PKEY_Device_DeviceDesc)

        state = DWORD()
        device.GetState(byref(state))

        store.Release()

        return dev_id.value, name, description, state.value

    def get_devices(self, flow=eRender, state=DEVICE_STATE_ACTIVE):
        """Get's all of the specified devices (by default, all output and active)."""
        collection = IMMDeviceCollection()
        self._device_enum.EnumAudioEndpoints(flow, state, byref(collection))

        count = UINT()
        collection.GetCount(byref(count))

        devices = []
        for i in range(count.value):
            dev_itf = IMMDevice()
            collection.Item(i, byref(dev_itf))

            dev_id, name, desc, dev_state = self.get_device_info(dev_itf)
            device = Win32AudioDevice(dev_id, name, desc, flow, dev_state)
            dev_itf.Release()
            devices.append(device)

        collection.Release()

        return devices

    @staticmethod
    def get_pkey_value(store, pkey):
        try:
            propvar = PROPVARIANT()
            store.GetValue(pkey, byref(propvar))
            value = propvar.pwszVal
            ole32.PropVariantClear(propvar)
        except Exception as err:
            value = "Unknown"

        return value