bastibe / SoundCard

A Pure-Python Real-Time Audio Library
https://soundcard.readthedocs.io
BSD 3-Clause "New" or "Revised" License
680 stars 69 forks source link

Record loopback device #112

Closed josephernest closed 3 years ago

josephernest commented 3 years ago

Congrats @bastibe for this great project!

I've used in a recent project, and I am now thinking about re-writing my entire code in pure C++.

Just out curiosity, because I think you have spent some time to write mediafoundation.py, and you probably know the use of MediaFoundation for loopback device well, would you have a small C++ sample code that does just this:

import soundcard as sc
lb = sc.all_microphones(include_loopback=True)[0]
with lb.recorder(samplerate=44100) as mic:
    for i in range(1000):
        data = mic.record(numframes=None)          # just record the loopback device, and get chunks as binary / RAW bytes
        print(data)

Thanks in advance!

bastibe commented 3 years ago

I do not have a small C++ sample code. However, mediafoundation.py is calling C functions using CFFI, which translates C function calls to Python function calls in a very straight-forward manner.

For example, have a look at your search for loopback microphones using sc.all_microphones(include_loopback=True):

# stripped out anything not relevant for this example:
def all_microphones(include_loopback=False):
    with _DeviceEnumerator() as enum:
        return [_Microphone(dev, isloopback=True) for dev in enum.all_devices('speaker')]

So further detail must be in _DeviceEnumerator:

class _DeviceEnumerator:
    """Wrapper class for an IMMDeviceEnumerator**.
    Provides methods for retrieving _Devices and pointers to the
    underlying IMMDevices.
    """

    def __init__(self):
        self._ptr = _ffi.new('IMMDeviceEnumerator **')
        IID_MMDeviceEnumerator = _guidof("{BCDE0395-E52F-467C-8E3D-C4579291692E}")
        IID_IMMDeviceEnumerator = _guidof("{A95664D2-9614-4F35-A746-DE8DB63617E6}")
        # see shared/WTypesbase.h and um/combaseapi.h:
        CLSCTX_ALL = 23
        hr = _ole32.CoCreateInstance(IID_MMDeviceEnumerator, _ffi.NULL, CLSCTX_ALL,
                                  IID_IMMDeviceEnumerator, _ffi.cast("void **", self._ptr))
        _com.check_error(hr)

    def all_devices(self, kind):
        """Yields all sound cards of a given kind.
        Kind may be 'speaker' or 'microphone'.
        Sound cards are returned as _Device objects.
        """

        DEVICE_STATE_ACTIVE = 0x1
        data_flow = 1 # record
        ppDevices = _ffi.new('IMMDeviceCollection **')
        hr = self._ptr[0][0].lpVtbl.EnumAudioEndpoints(self._ptr[0], data_flow, DEVICE_STATE_ACTIVE, ppDevices);
        _com.check_error(hr)

        for ppDevice in _DeviceCollection(ppDevices):
            device = _Device(self._device_id(ppDevice))
            _com.release(ppDevice)
            yield device

Any call to _com and _ole32 is a straight-up method call in C++. The only "weird" thing here is that we use _ffi.new instead of new and (regrettably) self._ptr[0][0].lpVtbl.EnumAudioEndpoints(self._ptr[0], ...) instead of self._ptr.EnumAudioEndpoints. The latter is because we are calling a C++ method from C, which does not support classes.

Have a look at mediafoundation.py.h for a copied-together summary of all the C++ header files used in SoundCard/mediafoundation.

josephernest commented 3 years ago

Thanks for your answer, I will study this!