AndreMiras / pycaw

Python Core Audio Windows Library
MIT License
385 stars 67 forks source link

OSError: exception: access violation writing 0x00000000 #19

Open cberk opened 5 years ago

cberk commented 5 years ago

When attempting to change the master volume repeatedly, sometimes get one of two [errors] as sen below:

( Capture Capture2

cberk commented 5 years ago

i am finding this tricky to catch as the errors don't appear in my console - they just terminate the process. When debugging, the errors appear but only terminate the process after running some additional (arbitrary) lines of code.

lucatatas commented 3 years ago

Were you able to solve this problem? I am having trouble as well...

TurboAnonym commented 3 years ago

Maybe it you changed the volume too fast and COM cant handle it (just a guess...). A delay could help.

cberk commented 3 years ago

Were you able to solve this problem? I am having trouble as well...

I don't think I did, sorry :-(

lucatatas commented 3 years ago

I was able to solve it! It was a pain to find out what did not work, but I figured it out. The problem is deeply hidden in the comtypes package which is imported when you want so set the master volume. Find the init.py of the comtypes package and search for "release". Eventually you will find the delete function of some compointer. I don't exaclty know what is going on, but by default this method should look something like this:

def __del__(self, _debug=logger.debug):
        "Release the COM refcount we own."
        if self:
            # comtypes calls CoUnititialize() when the atexit handlers
            # runs.  CoUninitialize() cleans up the COM objects that
            # are still alive. Python COM pointers may still be
            # present but we can no longer call Release() on them -
            # this may give a protection fault.  So we need the
            # _com_shutting_down flag.
            #
            if not type(self)._com_shutting_down:
                _debug("Release %s", self)
                self.Release()

For me, there seemed to be a problem considering pointers of the class "<class 'comtypes.POINTER(IUnknown)'>", so I replaced this del method by the following:

def __del__(self, _debug=logger.debug):
        "Release the COM refcount we own."
        if str(type(self)) == "<class 'comtypes.POINTER(IUnknown)'>":
            #print("Unkown type")
            pass
        else:
            self.Release()

I simply test if the type of my object is of the type "<class 'comtypes.POINTER(IUnknown)'>" and if that's the case, I simply don't call self.Release(). You could replace this method completely and simply use a pass statement, but this led to huge RAM consumption, so I added the little bit where it only passes on releasing the pointer if it is of the "forbidden" type. This worked brilliantly and because only very few pointers are actually of this type, only a few pointers are not deleted, so the RAM consumption is as low as before. This is especially not a problem, because beforehand, the pointers could not be deleted anyway, hence the error we all had to deal with. I hope this is helpful in any way, it definitely was for me. Feel free to contact me, if you need further help!

mrob95 commented 1 year ago

I was also running into this. As far as I can tell it is a double-free caused by using ctypes cast instead of COM QueryInterface to get a pointer to a new interface.

When you do this, Python has two objects referencing the COM object, but the COM object still only records a reference count of 1 because it doesn't know about the cast. The double free occurs if python deletes one of its objects, calling Release, then COM deletes the object and the memory is reused, then Python tries to delete its second object and call Release again.

This can be reproduced reliably by updating examples/audio_endpoint_volume_example.py to call main in a loop:

 if __name__ == "__main__":
-    main()
+    for i in range(100):
+        main()

Causing:

OSError: exception: access violation writing 0x0000000000000000
Exception ignored in: <function _compointer_base.__del__ at 0x000002BA3B2B98A0>
Traceback (most recent call last):
  File "C:\Users\Mike\Documents\GitHub\pycaw\.venv\Lib\site-packages\comtypes\__init__.py", line 956, in __del__
    self.Release()
  File "C:\Users\Mike\Documents\GitHub\pycaw\.venv\Lib\site-packages\comtypes\__init__.py", line 1211, in Release
    return self.__com_Release()
           ^^^^^^^^^^^^^^^^^^^^
OSError: exception: access violation writing 0x0000000000000000
volume.GetMute(): 0
Traceback (most recent call last):
  File "C:\Users\Mike\Documents\GitHub\pycaw\examples\audio_endpoint_volume_example.py", line 26, in <module>
    main()
  File "C:\Users\Mike\Documents\GitHub\pycaw\examples\audio_endpoint_volume_example.py", line 17, in main
    print("volume.GetMasterVolumeLevel(): %s" % volume.GetMasterVolumeLevel())
                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: exception: access violation writing 0x00007FFB54466CC8

We can trigger the same problem by copying the interface. Anything that results in multiple python objects referencing a COM object with a reference count of 1 is bad news:

def main():
    devices = AudioUtilities.GetSpeakers()
    interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
    interface2 = copy.deepcopy(interface)

if __name__ == "__main__":
    for i in range(100):
        main()

The COM documentation - https://learn.microsoft.com/en-us/windows/win32/com/rules-for-managing-reference-counts - explains pretty well why this causes problems.

The solution is to replace

volume = cast(interface, POINTER(IAudioEndpointVolume))

with

volume = interface.QueryInterface(IAudioEndpointVolume)

I'll make a PR to do this in all the examples.