Skip to content

Conversation

SuperKenVery
Copy link

@SuperKenVery SuperKenVery commented Aug 16, 2025

This pr adds support for recording system output audio on macOS.

For usage example, please see src/host/coreaudio/macos/mod.rs test::test_record_output.

To test this, use cargo run --example record_wav --device default-output

Testing requirements:

  • You're on macOS 14.2 or later.
  • The app has permission for recording system audio. Note the "app" means the app that starts cargo run --example, e.g. VSCode or Zed.
  • Don't build with nix. It comes with an old macOS SDK and lacks required symbols.

Closes #876

@SuperKenVery
Copy link
Author

SuperKenVery commented Aug 16, 2025

This pr might seem a little bit messy, but most of the new code should be at src/host/coreaudio/macos/loopback.rs.

I also extracted some code frommod.rs to device.rs, and added some tests in mod.rs.

For Cargo.toml, the main change was adding some features in objc2-xxx and/or add new objc2-xxx dependencies. However, the file was automatically formatted by Zed so it looks like there're lots of changes. But formatting it is anyway a good thing, so I didn't bother reverting that.

@SuperKenVery
Copy link
Author

SuperKenVery commented Aug 16, 2025

It seems that, in the CI, test_record_output test isn't properly finishing. Not sure why, it runes fine on my machine...

Well, CI is on macOS 14.7 while I'm on 15.5. Can anyone on macOS 14 try this?

@wgibbs-rs
Copy link
Member

There are some features not enabled by default. Given the kind of errors produced, I doubt this is a system-specific problem. Make sure you run the proper build commands, run unit tests, and try an example (like "beep"). To properly test this on your system, try using the following commands:

cargo test --all --no-default-features --verbose
cargo test --all --all-features --verbose

What could have changed that impacted wasm? I see the error around this is a time-out issue, so maybe try fixing MacOS and see if that resolves the wasm issue.

@SuperKenVery
Copy link
Author

@wgibbs-rs Thanks for your suggestions!

Both of the following runs flawlessly on my machine:

cargo test --all --no-default-features --verbose
cargo test --all --all-features --verbose

The wasm error is probably a network issue, it didn't even get to the point to run my code. It failed at "install stable".

@roderickvd
Copy link
Member

roderickvd commented Aug 18, 2025

Re-ran the failed tests and they’re OK now. Edit: no you’re right, the macOS test doesn’t finish. I only have an up to date macOS too.

Thanks for your contribution! This seems like a worthwhile feature to have. I am away now and should be able to look more into it in a few days. I’ll start by triggering a Copilot review and see what it comes up with.

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds support for loopback recording (recording system audio output) on macOS by implementing aggregate devices with taps. This enables applications to capture audio that is being played through the system's output devices.

Key changes include:

  • Added loopback recording capability using macOS Core Audio APIs
  • Restructured the device implementation to support both regular and loopback devices
  • Enhanced the record_wav example to demonstrate the new functionality

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/host/coreaudio/macos/mod.rs Restructured module to extract device implementation and add loopback support
src/host/coreaudio/macos/loopback.rs New module implementing loopback device creation using aggregate devices and taps
src/host/coreaudio/macos/device.rs Extracted device implementation with enhanced input stream building for loopback support
examples/record_wav.rs Enhanced example to support recording from default output device with configurable duration
Cargo.toml Added required Core Foundation and Foundation dependencies for loopback functionality

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@roderickvd
Copy link
Member

Any ideas how we could get it to work on macOS 14?

@SuperKenVery
Copy link
Author

SuperKenVery commented Aug 26, 2025

I think it might not be very difficult, the only problem is I don't have a macOS 14 machine.

When I get time, I could probably try to debug this in a VM.

@roderickvd
Copy link
Member

Maybe it's because the CI is asking the (non-existent) user whether microphone access is allowed? Maybe have it time out?

@SuperKenVery
Copy link
Author

the CI is asking the (non-existent) user whether microphone access is allowed

I think that might be the case. But why does recording test pass? It should also ask for microphone access...

@SuperKenVery
Copy link
Author

Ok. I just fired up a macOS 14 VM.

It does ask for microphone permission. But after granting the permission, it does ran without problems. So, not sure what happened on CI 🤔

@roderickvd
Copy link
Member

I'm no expert but maybe the problem is that loopback requires different permissions: screen capture, instead of microphone for the recording test.

I'd be OK with skipping such tests conditionally on the CI:

  #[test]
  fn test_foo() {
      if std::env::var("CI").is_ok() {
          println!("Skipping test_foo in CI environment due to permissions");
          return;
      }
      // ...
  }

@simlay
Copy link
Member

simlay commented Aug 31, 2025

Huh. I'm getting some odd behavior with the example. I've spent probably 20+ minutes trying to narrow it down but I'm more confused than I was initially. I'm on macOS 15.6.1, I've swapped between xcode 26 beta 6 and xcode 16.4 via xcode-select --switch and back. At first I thought it wasn't working as expected because my sample audio was a shakira youtube video in firefox (not trying to copy, just lazy).

I had it working in a non famous youtube music video and when I play local media. I'm not a huge audio person but the audio quality sounded like "It was under water". I wanted to provide the input recorded audio and the output from the loopback but somehow in the last 15 minutes something changed and now I don't hear anything when I play recorded.wav. I wish I had something more specific to be helpful.

@SuperKenVery
Copy link
Author

@simlay Thanks for reporting!

About the youtube video, I think it might be related to DRM, but I'm not sure. If you could provide its URL I may find out something.

About the sound quality, I'm not sure what's happening. Something that comes to my mind:

  1. According to this, macOS may have bugs handling outputs with a lot of channels:
        // Create a tap description for all processes without a specific output.
        // Note: I believe there is a bug in the CoreAudio Tap API. If the
        // default output device has 2 output channels this works as expected.
        // But if you have a device with 4 output channels then the volume of
        // the resulting buffer will be halved. You can extrapolate this to any
        // number of channels.

        // This bug is also present in ScreenCaptureKit if you use it on macOS
        // 14.2 or later. The bug is not present in macOS 14.1 or earlier.

        // A workaround for this issue could be to increase the volume of the
        // audio in the output by the number of channels. Or simply use the
        // other API to tap a specific output.
  1. Make sure you are recording from "default-output" device, and not from your microphone.

About playing the wav file, well, that's really strange! Hope you would figure it out~

@SuperKenVery
Copy link
Author

@roderickvd

loopback requires different permissions: screen capture

In my case (macOS 15.6.1) there are two sections in the screen capture permissions, "Screen capture and system audio" and "system audio only" (I translated from Chinese, might not be word accurate). It should require the "system audio only" permission.

However on macOS 14, the OS only asks for the microphone permission when running the tests.

I'm personally OK with skipping it on CI, but I'm curious why the microphone tests pass on CI? I didn't see any permission granting thing on the workflow file...

@roderickvd
Copy link
Member

I don’t know and can only guess that it has something to do with these permissions. Fixing the CI without skipping the test would be great but if we can’t win the fight, then let’s spend our energy elsewhere.

Because of permissions. Fuck apple.
@SuperKenVery
Copy link
Author

then let’s spend our energy elsewhere

Agreed. I've now disabled the check on CI and all checks have passed 😄

Copy link
Member

@roderickvd roderickvd left a comment

Choose a reason for hiding this comment

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

Thanks for getting the CI to pass. Couple of review comments.

};
coreaudio::Error::from_os_status(status)?;
let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _;
let ranges: &'static [AudioValueRange] = unsafe { slice::from_raw_parts(ranges, n_ranges) };
Copy link
Member

Choose a reason for hiding this comment

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

Should this be &'static or just &?

Copy link
Author

Choose a reason for hiding this comment

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

This code isn't really written by me... I just moved it to another file.

But looking at the code, it seems it should be & as it's a locally-declared Vec. But maybe it didn't return this pointer so it didn't cause any problem.

Copy link
Member

Choose a reason for hiding this comment

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

Would you check?

Copy link
Author

Choose a reason for hiding this comment

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

Yes sure. Will do that will I get back to my computer~

By not checking the return status
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suppport ScreenCaptureKit loopback
4 participants