Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

audio device event not captured #3057

Open
yannrouillard opened this issue Dec 29, 2021 · 14 comments
Open

audio device event not captured #3057

yannrouillard opened this issue Dec 29, 2021 · 14 comments

Comments

@yannrouillard
Copy link

yannrouillard commented Dec 29, 2021

Similar issue posted on https://groups.google.com/g/hammerspoon/c/heoYjCDatNk

I am currently unable to get the callback being fired for any audio event with hammerspoon 0.9.93.

I run the following code:

for _, input_device in ipairs(hs.audiodevice.allInputDevices()) do
    input_device:watcherCallback(function(uuid, event_name, scope, element) print(uuid, event_name, scope, element) end)
    input_device:watcherStart()
end

but it never print anything whatever I do (changing input volume, enabling mic, muting...)
I did the same on the output devices without more success.

I authorized hammerspoon to access the microphone using that command to have the permission request:

hs.microphoneState(true)

but it didn't change anything.

I successfully tested than the inUse() method properly returns the current state of the microphone to ensure the library still get the input device properties.

I also general watcher about device change event does work.
i.e., this code:

hs.audiodevice.watcher.setCallback(function(event) print(event) end)  
hs.audiodevice.watcher.start()

properly output events when I plug/unplug my headset for instance.

But I didn't find any clue about why the individual audiodevice event watcher doesn't work me.

BTW I am on a MacBook Air M1 using Mac OS Big Sur.

@yannrouillard
Copy link
Author

BTW I successfully tested that I can capture event using objective C code.

The following code does print the UUID of the microphone device when it is enabled or not in another app:

#import "math.h"
#import <AudioToolbox/AudioToolbox.h>
#import <Carbon/Carbon.h>
#import <Cocoa/Cocoa.h>
#import <CoreAudio/CoreAudio.h>
#import <Foundation/Foundation.h>

static const AudioObjectPropertySelector watchSelectors[] = {
    kAudioDevicePropertyMute,
    kAudioDevicePropertyJackIsConnected,
    kAudioDevicePropertyDeviceHasChanged,
    kAudioDevicePropertyStereoPan,
    kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
    kAudioDevicePropertyDeviceIsRunningSomewhere};

OSStatus audiodevice_callback(AudioDeviceID deviceID, UInt32 numAddresses,
                              const AudioObjectPropertyAddress addressList[],
                              void *clientData) {
  // Get the UID of the device, to pass into the callback
  NSString *deviceUIDNS = nil;

  AudioObjectPropertyAddress propertyAddress = {
      kAudioDevicePropertyDeviceUID, kAudioObjectPropertyScopeGlobal,
      kAudioObjectPropertyElementMaster};
  CFStringRef deviceUID;
  UInt32 propertySize = sizeof(CFStringRef);

  OSStatus result;

  result = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, NULL,
                                      &propertySize, &deviceUID);
  deviceUIDNS = (__bridge NSString *)deviceUID;

  NSLog(@"Found UID: %@", deviceUIDNS);
  return 0;
}

int main(int argc, char **argv) {
  NSLog(@"Testing");

  AudioObjectPropertyAddress propertyAddressA = {
      kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeWildcard,
      kAudioObjectPropertyElementWildcard};
  AudioDeviceID *deviceList = NULL;
  UInt32 deviceListPropertySize = 0;
  UInt32 numDevices = 0;
  UInt32 tableIndex = 1;
  UInt32 i, j;

  AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddressA, 0,
                                 NULL, &deviceListPropertySize);

  numDevices = deviceListPropertySize / sizeof(AudioDeviceID);
  deviceList = (AudioDeviceID *)calloc(numDevices, sizeof(AudioDeviceID));

  AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddressA, 0,
                             NULL, &deviceListPropertySize, deviceList);

  AudioObjectPropertyAddress propertyAddressB = {
      0, kAudioObjectPropertyScopeWildcard,
      kAudioObjectPropertyElementWildcard};

  const int numSelectors = sizeof(watchSelectors) / sizeof(watchSelectors[0]);

  for (int i = 0; i < numDevices; i++) {
    for (int j = 0; j < numSelectors; j++) {
      propertyAddressB.mSelector = watchSelectors[j];
      AudioObjectAddPropertyListener(deviceList[i], &propertyAddressB,
                                     audiodevice_callback, (void *)nil);
    }
  }

  [NSThread sleepForTimeInterval:100.0f];
}

@von
Copy link
Contributor

von commented Oct 19, 2022

I'm also seeing the same issue with the hs.audiodevice.watcherCallback seemingly never being called with 0.9.97 (6267).

I suspect related, I'm seeing some strangeness with the state of the watcher apparently not being preserved, e.g.:

> a = hs.audiodevice.allInputDevices()
> a[1]:uid()
BuiltInMicrophoneDevice
> a[1]:watcherIsRunning()
false
> a[1]:watcherCallback(c)
hs.audiodevice: MacBook Pro Microphone (0x6000006ccb98)
> a[1]:watcherStart()
hs.audiodevice: MacBook Pro Microphone (0x6000006ccb98)
> a[1]:watcherIsRunning()
true

So, all that seems to work well, but if we now re-obtain the audiodevice, the watcher isn't running (the following run immediately after the above):

> a = hs.audiodevice.allInputDevices()
> a[1]:uid()
BuiltInMicrophoneDevice
> a[1]:watcherIsRunning()
false
> a[1]:watcherStart()
2022-10-19 11:32:22: 11:32:22 ERROR:   LuaSkin: You must call hs.audiodevice:setCallback() before hs.audiodevice:start()
nil

von added a commit to von/WatcherWatcher.spoon that referenced this issue Oct 28, 2022
@von
Copy link
Contributor

von commented Oct 23, 2023

At some point between 0.9.97 and 0.9.100 (6815), hs.audiodevice.watcherCallback apparently started working again.

I still see the same strangeness with the watcher state I describe in my comment above.

@Rhys-T
Copy link

Rhys-T commented Nov 9, 2023

It looks like what's happening is that every time you call allInputDevices, defaultOutputDevice, or anything else that returns hs.audiodevice objects, it gives you new objects referring to the same devices.1 However, the watcher state and callback are stored per object, not per device. That means that the watcherIsRunning result you get after re-obtaining the device is actually correct, because the object you got that time really isn't running its watcher. It also means that you'll need to save the object that you used to set up the watcher somewhere, or the watcher will stop when the object gets garbage-collected - just like other watchers/observers/event taps/timers/etc., except that the 'watcher object' in this case is the device object itself.

I don't think this behavior is a bug per se, but it's not documented as far as I can tell, and it's definitely not very intuitive. The reason I thought to check for this is:

  1. Problems of the form "Hammerspoon stops calling my callback for X after a while, sometimes" are typically the result of X getting garbage-collected. That's why I force the garbage collector to run at the very end of my config.
  2. I had already seen that some other Hammerspoon modules (hs.application2, hs.axuielement3, etc.) worked similarly, returning new Lua objects each time even if they represented the same 'things' outside HS. (But in those cases, the watchers are usually separate objects.)

Footnotes

  1. Two hs.audiodevice objects for the same device compare equal using == (because they have an __eq metamethod that handles that), but they aren't rawequal to each other, and act as different keys in tables.

  2. hs.application constructors returning duplicate objects #2720

  3. Hash/identifier for userdata objects for hs.axuielement to allow usage as table keys? #3532

@von
Copy link
Contributor

von commented Nov 10, 2023

I don't think this behavior is a bug per se, but it's not documented as far as I can tell, and it's definitely not very intuitive.

Agreed.

At this point, I believe the original problem for this issue has been fixed and it could be closed. [EDITED: Was confused and thought I originally opened this issue.]

I propose I open another issue around the documentation of this state behavior so it's not lost. I'm not set up to generate a PR for the documentation right now, or I'd do that.

@von
Copy link
Contributor

von commented Nov 12, 2023

At some point between 0.9.97 and 0.9.100 (6815), hs.audiodevice.watcherCallback apparently started working again.

I was mistaken when I said this. I forgotten I had a work around that used polling instead of the callbacks which fooled me into thinking the callbacks were working, so this issue is not fixed I believe.

@von
Copy link
Contributor

von commented Nov 13, 2023

I note that hs.camera does not seem to exhibit the same behavior has hs.audiodevice with regards to objects versus devices. The watchers seem to carry over from one object created to the next. For example:

cameras = hs.camera.allCameras()
2023-11-13 11:08:25: -- Loading extension: camera

cameras[1]:isPropertyWatcherRunning()
false

cameras[1]:setPropertyWatcherCallback(function() end)
hs.camera: (EAB7A68F-EC2B-4487-AADF-D8A91C1CB782:FaceTime HD Camera)

cameras[1]:startPropertyWatcher()
hs.camera: (EAB7A68F-EC2B-4487-AADF-D8A91C1CB782:FaceTime HD Camera)

cameras[1]:isPropertyWatcherRunning()
true

cameras2 = hs.camera.allCameras()

cameras2[1]:isPropertyWatcherRunning()
true

@von
Copy link
Contributor

von commented Nov 13, 2023

@Rhys-T Thank you for the explanation. I have modified my code such that I am storing my hs.audiodevice instances now, which I believe should prevent them from being garbage collected, however I am still not seeing watchers being called.

For clarity, it's not that I'm seeing the watchers called at first and then they stop being called, rather I'm never seeing them called.

If anyone is having any success with hs.audiodevice watchers, I welcome hearing that.

@Rhys-T
Copy link

Rhys-T commented Nov 13, 2023

@von Hmm… it's working on my Hammerspoon (0.9.97 / 6267, Intel), at least with defaultInputDevice() (Internal Microphone) and defaultOutputDevice() (Internal Speakers). I can even create two objects pointing to the same device, give them different callbacks, start their watchers, change the volume from System Preferences (or the keyboard, for the output device), and see both callbacks consistently getting called for each event.

Unfortunately, I'm not familiar enough with macOS's audio APIs to know what to try next, if it's not the garbage collection thing…


Re: hs.camera, while that module also returns a new Lua object/userdata each time, it looks like all the userdatas for a given camera point to the same underlying HSCamera object on the Objective-C side of things, and that's where the property watcher status is tracked. (In hs.audiodevice, the userdata stores the watcher status directly.)

@von
Copy link
Contributor

von commented Nov 14, 2023

Thank you @Rhys-T . Turns out it's my bad and everything is working as it should. Knowing it worked for you caused me to think hard enough about this to realize what I was assuming wrong.

My mistake was a bad assumption about how Zoom works. Looks like it grabs the microphone at the start of a meeting and its muting/unmuting is all internal to the app and it doesn't fire any sort of watcher callback. I was clicking Mute/unmute/etc and not seeing any watcher callbacks and assuming they were not working. If one cares about Zoom mute state, one has to check the Zoom application menu state or a similar workaround.

With the spawning of #3559 to cover the documentation of hd.audiodevice state behavior, I believe I have no more problems with regards to this issue. Since I'm not the original issue author and I don't think they had my issue, I won't speak to it being closed or not.

@Rhys-T
Copy link

Rhys-T commented Nov 14, 2023

@von Glad to hear it's working for you.

If it helps, someone has already written a Spoon for tracking whether or not Zoom is a) connected to a meeting, and b) unmuted: https://github.com/jpf/Zoom.spoon (more usage examples here: https://developer.okta.com/blog/2020/10/22/set-up-a-mute-indicator-light-for-zoom-with-hammerspoon)

@von
Copy link
Contributor

von commented Nov 14, 2023

Thanks @Rhys-T , I am aware of that repo and took my approach from it (you'll see it credited towards the top of my code). It's a good piece of software, my use case (monitoring any microphone or camera access, not just Zoom) is different enough I only borrowed from it instead of re-using it in its entirety.

@esphoenixc
Copy link

Hi, I am building a macOS app and I was successful in capturing the audio input device (internal default macbook microphone) when it's being used. However, as soon as I connect my airpods as my audio device, it does not detect.

I've used kAudioDevicePropertyDeviceIsRunningSomewhere Is this the right issue thread I came to??

@von
Copy link
Contributor

von commented Nov 30, 2023

Hi @esphoenixc it doesn't seem like your issue has anything to do with Hammerspoon in that you're writing a native Mac app. If I'm correct, you're in the wrong place.

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

No branches or pull requests

4 participants