Skip to content

add support for loopback recording on macOS 14.2+#196

Open
TnTora wants to merge 1 commit into
bastibe:masterfrom
TnTora:master
Open

add support for loopback recording on macOS 14.2+#196
TnTora wants to merge 1 commit into
bastibe:masterfrom
TnTora:master

Conversation

@TnTora

@TnTora TnTora commented Jan 14, 2026

Copy link
Copy Markdown
Contributor

This is my first attempt at integrating loopback recording on Mac as mentioned in #194.

For now, I placed the code necessary to generate the loopback device under the helper class _CoreAudio. Let me know if you would prefer it having its own helper class.

Based on the requirements for this sample project from Apple, I decided to check for loopback support using platform.mac_ver and setting 14.2 as a minimum requirement.

With the current implementation, once the user calls all_microphones(include_loopback=True) for the first time, a loopback device is created. Its id is used as a key in the dictionary _CoreAudio.loopback_devices while the id of the tap object is stored as its value. Whether or not a device is loopback is determined by its id being present in the dictionary.

This id values can be used to destroy the device. I am uncertain whether or not there is a need to actively destroy every device. I set them up to be private which means they are linked to, and only accessible from, the process which created them and from my observations they seem to disappear once the process is stopped.

If I can manage to make process inclusion and exclusion work under cffi, I was thinking the create_loopback_device function could be exposed to the user to give better control over the tap options. If you think it would be outside the scope of this library let me know.

@bastibe

bastibe commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Sorry for sleeping on this for several months.

This looks very promising. However, it only adds a global loopback, and the loopback device does not appear to be destroyed properly. At least, on my machine, it only works the first time. If I run python again, it fails with

>>> import soundcard
>>> mics = soundcard.all_microphones(include_loopback=True)
'AudioHardwareCreateAggregateDevice' Error Status: 1852797029

Comment thread soundcard/coreaudio.py
_objc.objc_msgSend(NSAutoreleasePool, _objc.sel_registerName(b"release"))

if status == 0:
cls.loopback_devices[aggr_id[0]] = tap_id[0]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only ever assigns one device

Comment thread soundcard/coreaudio.py
_ca.AudioHardwareDestroyProcessTap(tap_id[0])

@classmethod
def destroy_loopback_device(cls, aggr_id):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is never called, right?

@cclauss

cclauss commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Please rebase now that we have GitHub Actions tests.

% git grep oundflower # These lines should also be updated so these tests can pass on macOS.

test_soundcard.py:        # soundflower is EoL and does not support modern macOS or Apple Silicon
test_soundcard.py:        return soundcard.get_speaker('Soundflower64')
test_soundcard.py:        # soundflower is EoL and does not support modern macOS or Apple Silicon
test_soundcard.py:        return soundcard.get_microphone('Soundflower64')

Comment thread soundcard/coreaudio.py
Comment on lines +24 to +34
mac_ver = platform.mac_ver()[0]
loopback_support = True

try:
for mac_v, min_v in zip(mac_ver.split("."), (14, 2), strict=False):
if int(mac_v) == min_v:
continue
loopback_support = int(mac_v) > min_v
break
except ValueError:
loopback_support = False

@cclauss cclauss Jun 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mac_ver = platform.mac_ver()[0]
loopback_support = True
try:
for mac_v, min_v in zip(mac_ver.split("."), (14, 2), strict=False):
if int(mac_v) == min_v:
continue
loopback_support = int(mac_v) > min_v
break
except ValueError:
loopback_support = False
mac_ver = tuple(int(x) for x in platform.mac_ver()[0].split(".")[:2])
loopback_support = mac_ver >= (14, 2)
>>> import platform
>>> mac_ver = tuple(int(x) for x in platform.mac_ver()[0].split(".")[:2])
>>> mac_ver
(26, 5)
>>> loopback_support = mac_ver >= (14, 2)
>>> loopback_support
True

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants