5

I'm trying to tap the currently selected output audio device on macOS, so I basically have a pass through listener that can monitor the audio stream currently being output without affecting it.

I want to copy this data to a ring buffer in real time so I can operate on it separately.

The combination of Apple docs and (outdated?) SO answers are confusing as to whether I need to write a hacky kernel extension, can utilise CoreAudio for this, or need to interface with the HAL?

I would like to work in Swift if possible.

Many thanks

(ps. I had been looking at this and this)

Darius
  • 5,180
  • 5
  • 47
  • 62

1 Answers1

5

I don't know about kernel extensions - their use of special "call us" signing certificates or the necessity of turning off SIP discourages casual exploration.

However you can use a combination of CoreAudio and HAL AudioServer plugins to do what you want, and you don't even need to write the plugin yourself, there are several open source versions to choose from.

CoreAudio doesn't give you a way to record from (or "tap") output devices - you can only record from input devices, so the way to get around this is to create a virtual "pass through" device (AudioServerPlugin), not associated with any hardware, that copies output through to input and then set this pass through device as default output and record from its input. I've done this using open source AudioServer Plugins like BackgroundMusic and BlackHole [TODO: add more].

To tap/record from the resulting device you can simply add an AudioDeviceIOProc callback to it or set the device as the kAudioOutputUnitProperty_CurrentDevice of an kAudioUnitSubType_HALOutput AudioUnit

There are two problems with the above virtual pass through device approach:

  1. you can't your hear output anymore, because it's being consumed by the pass through device
  2. changing default output device will switch away from your device and the tap will fall silent.

If 1. is a problem, then a simple is to create a Multi-Output device containing the pass through device and a real output device (see screenshot) & set this as the default output device. Volume controls stop working, but you can still change the real output device's volume in Audio MIDI Setup.app.

For 2. you can add a listener to the default output device and update the multi-output device above when it changes.

You can do most of the above in swift, although for ringbuffer-stowing from the buffer delivery callbacks you'll have to use C or some other language that can respect the realtime audio rules (no locks, no memory allocation, etc). You could maybe try AVAudioEngine to do the tap, but IIRC changing input device is a vale of tears.

Audio MIDI Setup app showing multi-output device set as default output, containing pass thru device (Background Music) and normal output device (Mac-mini speakers), Mac-mini speakers set as master device

Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159
  • this is a really comprehensive overview, thank you. What about this: 1. on program startup I detect the current default output device. 2. programmatically change the current default output device to a virtual AU that is just a proxy to the original default output device. 3. bind a listener to the output device change event and change what the proxy delegates to 4. always force the output device to be the proxy whilst program is running (whenever changed). My other alternative is use Blackhole and have the user select a specific application as the audio source, like Spotify or Skype etc? – Darius Dec 14 '20 at 17:43
  • Exactly. Are you sure blackhole can reroute audio from individual applications? That sounds more like RA's loopback. – Rhythmic Fistman Dec 14 '20 at 18:45
  • Yep fair point, I have Loopback installed so conflated the two in my mind! – Darius Dec 14 '20 at 21:14
  • If you have loopback you configure its device to have exactly one source: the application, and you record from the device using the methods in the answer. I didn't mention loopback in the answer. I believe it uses a different method altogether. – Rhythmic Fistman Dec 14 '20 at 21:22
  • yeah the reason why I can't use Loopback in the solution is primarily I want to distribute this app to other users, and can't guarantee they will have Loopback or the knowhow to set it up. I'm still mulling over what you wrote though because there will be a good compromise using the multi output device I think – Darius Dec 14 '20 at 21:28