Search code examples
iosmacoscombineavaudioengine

Combine Publisher not firing for a KVO property


I'm trying to track an AVAudioPlayerNode's playback state via its Combine Publisher:

import Cocoa
import AVFoundation
import Combine

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    let engine = AVAudioEngine()
    let player = AVAudioPlayerNode()
    var cancellable: AnyCancellable?

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        cancellable = player.publisher(for: \.isPlaying)
            .sink { newValue in
                print("is playing: \(newValue)")
            }

        let url = Bundle.main.url(forResource: "blues", withExtension: "aiff")!
        let file = try! AVAudioFile(forReading: url)

        let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: UInt32(file.length))!
        try! file.read(into: buffer)

        engine.attach(player)
        engine.connect(player, to: engine.mainMixerNode, format: file.processingFormat)

        player.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)

        try! engine.start()
        player.play()
    }
}

The sink gets called just once when it's initialised, and never again. I would expect it to get called when playback starts, but it doesn't... Any ideas why?


Solution

  • AVAudioPlayerNode does not seem to be KVO-compliant for the isPlaying property. Normally the docs mention explicitly when properties are KVO-compliant but the docs for AVAudioPlayerNode don't address it directly

    I ran a simple test using addObserver(_:forKeyPath:options:context:) and confirmed that KVO notifications are not emitted when play() or stop() is called.

    As for why the Combine publisher exists, I assume that NSObject.KeyValueObservingPublisher works for all ObjC @property declarations whether or not the ObjC class internally handles the property in a KVO-compliant way (by sending -willChangeValueForKey: and -didChangeValueForKey:).