2019-06-19
CoreAudio AudioObjectRemovePropertyListener not working in Swift
stackoverflow
Question

I am working with CoreAudio in swift and needed to find when the user changes the system volume.

I can get the volume correctly and even add a property listener to find when the user changes the volume. But I need to stop listening at some point (if the user changes the default output device) but I am not able to remove the property listener.

I have created a basic test for playground and made the same test in a command line obj-c project. The test works fine in obj-c but it does not work in swift

The code just adds the listener and then removes it, so changing the volume after running the code should print nothing, but in swift it keeps printing

the swift code:

import CoreAudio

//first get default output device
var outputDeviceAOPA:AudioObjectPropertyAddress = AudioObjectPropertyAddress(
    mSelector: kAudioHardwarePropertyDefaultOutputDevice,
    mScope: kAudioObjectPropertyScopeGlobal,
    mElement: kAudioObjectPropertyElementMaster)

var outputDeviceID = kAudioObjectUnknown
var propertySize = UInt32(sizeof(AudioDeviceID))
AudioObjectGetPropertyData(UInt32(kAudioObjectSystemObject), &outputDeviceAOPA,
    0, nil, &propertySize, &outputDeviceID)

// get volume from device
var volumeAOPA:AudioObjectPropertyAddress = AudioObjectPropertyAddress(
    mSelector: kAudioDevicePropertyVolumeScalar,
    mScope: kAudioObjectPropertyScopeOutput,
    mElement: kAudioObjectPropertyElementMaster
)
var volume:Float32 = 0.5
var volSize = UInt32(sizeof(Float32))

AudioObjectGetPropertyData(outputDeviceID, &volumeAOPA, 0, nil, &volSize, &volume)
print(volume)

var queue = dispatch_queue_create("testqueue", nil)
var listener:AudioObjectPropertyListenerBlock = {
    _, _ in
    AudioObjectGetPropertyData(outputDeviceID, &volumeAOPA, 0, nil, &volSize, &volume)
    print(volume)
}

AudioObjectAddPropertyListenerBlock(outputDeviceID, &volumeAOPA, queue, listener)
AudioObjectRemovePropertyListenerBlock(outputDeviceID, &volumeAOPA, queue, listener)

while true{
    //keep playground running
}

And this is the objective-c code:

//objective-c code working
//  main.m
//  objccatest

#import <Foundation/Foundation.h>
#import <CoreAudio/CoreAudio.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        //first get default output device

        AudioObjectPropertyAddress outputDeviceAOPA;
        outputDeviceAOPA.mSelector= kAudioHardwarePropertyDefaultOutputDevice;

        outputDeviceAOPA.mScope= kAudioObjectPropertyScopeGlobal;
        outputDeviceAOPA.mElement= kAudioObjectPropertyElementMaster;

        AudioObjectID outputDeviceID = kAudioObjectUnknown;
        UInt32 propertySize = sizeof(AudioDeviceID);

        AudioObjectGetPropertyData(kAudioObjectSystemObject, &outputDeviceAOPA,
                                   0, nil, &propertySize, &outputDeviceID);

        // get volume from device
        AudioObjectPropertyAddress volumeAOPA;
        volumeAOPA.mSelector= kAudioDevicePropertyVolumeScalar;
        volumeAOPA.mScope= kAudioObjectPropertyScopeOutput;
        volumeAOPA.mElement= kAudioObjectPropertyElementMaster;

        Float32 volume = 0.5;
        UInt32 volSize = sizeof(Float32);

        AudioObjectGetPropertyData(outputDeviceID, &volumeAOPA, 0, nil, &volSize, &volume);
        NSLog(@"%f", volume);

        dispatch_queue_t queue = dispatch_queue_create("testqueue", nil);
        AudioObjectPropertyListenerBlock listener = ^(UInt32 a, const AudioObjectPropertyAddress* arst){

            AudioObjectGetPropertyData(outputDeviceID, &volumeAOPA, 0, nil, &volSize, &volume);
            NSLog(@"%f", volume);

        };

        AudioObjectAddPropertyListenerBlock(outputDeviceID, &volumeAOPA, queue, listener);
        AudioObjectRemovePropertyListenerBlock(outputDeviceID, &volumeAOPA, queue, listener);

        while (true){
            //keep app running
        }

    }
    return 0;
}

I think this is a bug in the Core Audio API, but maybe there is a workaround or obj-c blocks work different to the swift closures.

Answer
1

Yeah, it could be a bug actually because the listener block can not be removed by AudioObjectRemovePropertyListenerBlock. However, I find that to register an AudioObjectPropertyListenerProc with an AudioObject can be a workaround with Swift.

//var queue = dispatch_queue_create("testqueue", nil)
//var listener:AudioObjectPropertyListenerBlock = {
//    _, _ in
//    AudioObjectGetPropertyData(outputDeviceID, &volumeAOPA, 0, nil, &volSize, &volume)
//    print(volume)
//}
//
//AudioObjectAddPropertyListenerBlock(outputDeviceID, &volumeAOPA, queue, listener)
//AudioObjectRemovePropertyListenerBlock(outputDeviceID, &volumeAOPA, queue, listener)
var data: UInt32 = 0
func listenerProc() -> AudioObjectPropertyListenerProc {
    return { _, _, _, _ in
        AudioObjectGetPropertyData(outputDeviceID, &volumeAOPA, 0, nil, &volSize, &volume)
        print(volume)
        return 0
    }
}
AudioObjectAddPropertyListener(outputDeviceID, &volumeAOPA, listenerProc(), &data)
AudioObjectRemovePropertyListener(outputDeviceID, &volumeAOPA, listenerProc(), &data)
CoreAudio AudioObjectRemovePropertyListener not working in Swift
See more ...