Matt Matt - 6 months ago 21
Swift Question

Convert NSSound to AVAudioPlayer

I have some

NSSound
objects that I'd like to convert to
AVAudioPlayer
instances. I have file paths (
NSURL
s) associated with the
NSSound
objects, but the original file may not exist. Here is what I have so far:

class SoundObj: NSObject {
var path: NSURL?
var sound: NSSound?
var player: AVAudioPlayer
}

let aSound = SoundObj()
aSound.path = NSURL(fileURLWithPath: "file:///path/to/sound.m4a")
aSound.sound = NSSound(contentsOfURL: aSound.path)!

do {
try aSound.player = AVAudioPlayer(contentsOfURL: aSound.path)
} catch {
// perhaps use AVAudioPlayer(data: ...)?
}


How do I convert an
NSSound
object to an
AVAudioPlayer
instance?

JAL JAL
Answer

So I didn't see a public interface to get the URL from an NSSound object, so I went digging through the private headers to see what I could find. Turns out there are private instance method url and _url which return the URL of an NSSound. Presumably these are getters for an NSURL ivar or property.

With Objective-C this would be easy: we would just add the methods to a new interface or extension. With pure Swift things are a little trickier, and we need to expose the accessor via an Objective-C protocol:

@objc protocol NSSoundPrivate {
    var url: NSURL? { get }
}

Since url is an instance method, you may get better results with func url() -> NSURL? instead of using a variable. Your milage may vary: using a var to emulate the behavior of a read-only property seemed to work for me.

I wrote a new convenience initializer in an extension on AVAudioPlayer:

extension AVAudioPlayer {
    convenience init?(sound: NSSound) throws {    
        let privateSound = unsafeBitCast(sound, NSSoundPrivate.self)    
        guard let url = privateSound.url else { return nil }
        do {
            try self.init(contentsOfURL: url)
        } catch {
            throw error
        }
    }
}

Usage:

let url = NSURL(...)    
if let sound = NSSound(contentsOfURL: url, byReference: true) {
    do {
        let player = try AVAudioPlayer(sound: sound)
        player?.play()
    } catch {
        print(error)
    }        
}

After attempting to find anything related to NSData in the ivars, instance methods, and properties of an NSSound, I have come to the conclusion that the data portion of whatever you use to initialize an NSSound is obfuscated somewhere in the implementation of the class, and is not available like the URL is.

Comments