BoteRock
BoteRock

Reputation: 477

CoreAudio AudioObjectRemovePropertyListener not working in Swift

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.

Upvotes: 4

Views: 3311

Answers (2)

Allen
Allen

Reputation: 1734

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)

Upvotes: 3

Grzegorz Owsiany
Grzegorz Owsiany

Reputation: 331

I had the same problem.

Seems to be an issue with passing Swift closures to Objective-C API. Same closure is block-copied twice, each copy has a different address. Hence the block provided for listener registration is not matched properly with one used during unregistration.

The workaround I've found is to register and unregister listeners using Objective-C helper which will also store block address for unregistration.

Upvotes: 2

Related Questions