kdschlosser / pyWinCoreAudio

Python Windows Core Audio API
GNU General Public License v2.0
34 stars 9 forks source link

IMMDevice.is_default calls default_audio_endpoint with incorrect role argument #6

Open dskrypa opened 1 year ago

dskrypa commented 1 year ago

First off, thank you for your work on this library!

In the develop branch, in IMMDevice.is_default, I noticed that the 2nd argument it uses when calling default_audio_endpoint appears to be incorrect since that method expects the 2nd argument to be an ERole:

    @property
    def is_default(self) -> bool:
        return IMMDeviceEnumerator.default_audio_endpoint(self.data_flow, self.data_flow) == self

It probably ends up providing a correct result for most use cases, but is not 100% accurate. Maybe a is_default_for_role method would be better here, and/or updating is_default to return True if it is the default for any ERole?

Example showing a case where the default communications output endpoint is not identified as being the default by is_default:

>>> for df in (EDataFlow.eRender, EDataFlow.eCapture):
...     for role in (ERole.eConsole, ERole.eMultimedia, ERole.eCommunications):
...         ep_name = IMMDeviceEnumerator.default_audio_endpoint(df, role).name
...         df_str, role_str = str(df), str(role)
...         print(f'{df_str:7s}: {role_str:15s}: {ep_name}')
...
Render : Console        : Digital Audio (S/PDIF) (High Definition Audio Device)
Render : Multimedia     : Digital Audio (S/PDIF) (High Definition Audio Device)
Render : Communications : Speakers (Plantronics Savi 7xx)
Capture: Console        : Transmit (Plantronics Savi 7xx)
Capture: Multimedia     : Transmit (Plantronics Savi 7xx)
Capture: Communications : Transmit (Plantronics Savi 7xx)

>>> for device in devices(False)():
...     for endpoint in device:
...         if endpoint.is_default:
...             df_str = str(endpoint.data_flow)
...             print(f'{df_str:7s}: {endpoint.name}')
...
Capture: Transmit (Plantronics Savi 7xx)
Render : Digital Audio (S/PDIF) (High Definition Audio Device)
kdschlosser commented 1 year ago

the call from that function should read

IMMDeviceEnumerator.default_audio_endpoint(self.data_flow, self.role)

and not

IMMDeviceEnumerator.default_audio_endpoint(self.data_flow, self.data_flow)

so if you can do me a favor and run this code to see if the output ends up being correct we can get it all fixed up.

for df in (EDataFlow.eRender, EDataFlow.eCapture):
    for role in (ERole.eConsole, ERole.eMultimedia, ERole.eCommunications):
        ep = IMMDeviceEnumerator.default_audio_endpoint(df, role)
        print(ep == IMMDeviceEnumerator.default_audio_endpoint(ep.data_flow, ep.role))

I want to make sure that the data_flow and the role as returned from the default device is going to be correct to be used in checking if it is the default or not.

Good catch on this. If the result of running that test is all True then go ahead and fix the code and submit a PR for it and I will get it merged. If you do not want to bother with making a PR let me know and I will get it done.

Otherwise if the library working for what you need it to? is there any functionality that needs to be added to it???

dskrypa commented 1 year ago

I would be happy to submit a PR, but it looks like IMMDevice doesn't have a role attribute/property:

AttributeError: 'POINTER(IMMDevice)' object has no attribute 'role'

I looked at the _methods_ in that class and in IMMEndpoint since that is where the GetDataFlow method that is used by the data_flow property is defined, but didn't see anything that seemed like it would provide the endpoint's role. I'm not overly familiar with the underlying interfaces; I had the Microsoft documentation open yesterday, but haven't dived back in today.

Do you have local changes that haven't been pushed to GitHub, perhaps, that added that?

In terms of library functionality, I'm not entirely sure if what I want to do is actually possible. I have found conflicting suggestions in different threads/answers. My goal is almost exactly the same as what was stated in this thread - I want to toggle the Disable all enhancements setting for a particular render endpoint. Normally, I keep them disabled, but some shows really need the loudness equalization for dialogue to be audible. The setting is buried far too deep in clicky menus...

It sounds like it may be possible to accomplish this by toggling the PKEY_AudioEndpoint_Disable_SysFx property. I did not find an obvious way to do so yet, but I spent most of yesterday reading through different libraries and then getting more familiar with this one. I plan to try again likely either later today or tomorrow.

kdschlosser commented 1 year ago

I will be pushing an update in about a day or so. I am checking into what you are wanting to do with enabling the effects. I have checking to see if they are disabled but nothing to enable them. I have to do some testing to find out if I am able to turn it on the way I would like to.

kdschlosser commented 1 year ago

Sorry I am running behind. I recently upgraded to Windows 10 and then to Windows 11 and I have been messing about with the audio components and it not playing nice nice with anything Realtek which is the sound card I have is a USB kind of a sound card. It's funky. I just now managed to locate drivers for it that gave the option you are trying to enable and disable. So now I will be able to locate the property key for it and code up a method to turn it on and off.

On another note. I did add a cool feature. when I push an update you will be able to target a specific audio session and change the endpoint for that session. that will be pretty handy. I also came across a bunch of new interfaces that are in the latest windows SDK so I have to rummage through those and see what they offer.

dskrypa commented 1 year ago

No rush at all! Thank you very much for your help! That sounds cool :)

dskrypa commented 1 year ago

By the way, just to make sure it's clear, when toggling the Disable all enhancements setting via the UI, Windows remembers the last settings that were checked for the specific enhancements that should be enabled. I only ever toggle Disable all enhancements. Assuming toggling it via API would have the same behavior, it should hopefully be relatively straightforward.

kdschlosser commented 1 year ago

OK I added the feature for enabling and disabling all enhancements. you access it using the audio_enhancements_enabled method of an endpoint. you can read the current state and change it as well. The property takes a boolean value

print(endpoint.audio_enhancements_enabled)
endpoint.audio_enhancements_enabled = False
print(endpoint.audio_enhancements_enabled)
endpoint.audio_enhancements_enabled = True
print(endpoint.audio_enhancements_enabled)

The only crappy thing is the sound control panel was not coded to receive property change notifications. So if you have the sound control panel open and change the property using this library you will NOT see the switch move. You have to close the control panel and open it again and the switch will then be in the correct position.

There are 2 places that exist in Windows to change this setting. The first is under System -> Sound and then you click on the endpoint to see the properties. The setting is called "Enhance Audio"

The second place is when you are in the properties page for an endpoint and then you click on the "Advanced" link under "Enhance Audio" doing that will open up the dialog for changing settings to the endpoint. If you select the "Advanced" tab in that dialog the checkbox "Enable Audio Enhancements" is the same setting.

This dialog also does not receive property change notifications either.

new code is in the develop branch.

kdschlosser commented 1 year ago

changing that setting in the library is going to function exactly like it does in the sound settings of Windows

dskrypa commented 1 year ago

Amazing, thank you! I'll test it out tomorrow.

kdschlosser commented 1 year ago

I fixed the example and added better support for some of the properties. so a new commit has been pushed to the develop branch.

kdschlosser commented 1 year ago

I don't know what brand of sound card you have, If you have a realtek card I stumbled upon a huge number of undocumented COM interfaces buried inside of the driver DLL files. They are all for changing settings on the cards. The fun settings like EQ and sound processors and settings for the processors.

It is going to take me a long while to hammer out what the different interfaces do. I just finished writing the COM interface binders in Python. all 6000+ lines worth.

dskrypa commented 1 year ago

I tried toggling the enhancements for a given endpoint, but it resulted in this error:

Traceback (most recent call last):
  Cell In [1], line 3
    ep.audio_enhancements_enabled = False
  File ..\venv\lib\site-packages\pyWinCoreAudio\mmdeviceapi.py:3018 in audio_enhancements_enabled
    self.set_property(PKEY_AudioEndpoint_Disable_SysFx, value, VT_UI4)
  File ..\venv\lib\site-packages\pyWinCoreAudio\mmdeviceapi.py:2885 in set_property
    return pStore.SetValue(key, value, vt)
  File ..\venv\lib\site-packages\pyWinCoreAudio\propsys.py:119 in SetValue
    self.__com_SetValue(key, ctypes.byref(prop_var))
ValueError: NULL COM pointer access

I am running Win 10 Pro, 22H2 (19045.2251), and tried this with Python 3.10.8 in an interactive ipython session. The test was on a device that I had not tried toggling that setting on before though - I just built a new PC, and the new sound card (Sound Blaster Z SE) apparently doesn't have the same options in the Windows Sound Control Panel as my old one (builtin to old motherboard - not sure which vendor/model) did. The above attempt was on the Speakers endpoint for my USB headset (Plantronics Savi 7xx). It was set as the default communications device.

Once I have had time to get settled in, I will definitely take a closer look and try again. Thank you again for your help on this! And I will definitely be playing around with this more later - the new additions sound very interesting.

kdschlosser commented 1 year ago

Try running the script as an administrator.

I will also look and see if there is anything off with it.