11

I have some NSSound objects that I'd like to convert to AVAudioPlayer instances. I have file paths (NSURLs) 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
  • 41,701
  • 23
  • 172
  • 300
Matt
  • 2,576
  • 6
  • 34
  • 52
  • If the audio file doesn't exist won't this line crash? `aSound.sound = NSSound(contentsOfURL: aSound.path)!` – brimstone Apr 30 '16 at 03:27
  • Yes, but for this case `.sound` is never null. – Matt Apr 30 '16 at 03:28
  • Honestly, I don't think you can get sound data from a NSSound. – brimstone Apr 30 '16 at 03:40
  • @brimstone is there a way to verify that? – Matt Apr 30 '16 at 05:06
  • I'm not quite sure what you're asking. Would you like a custom `AVAudioPlayer` initializer that takes in an `NSSound` and returns a `AVAudioPlayer`? I'm thinking `AVAsset` might be better for your needs. – JAL May 02 '16 at 19:37
  • @JAL yes, that would work. – Matt May 02 '16 at 23:05
  • Hey Matt did my answer help you? Feel free to follow up if you have any other questions. – JAL May 05 '16 at 15:50
  • @JAL yes it did. Is there any way you know of to get NSData from an NSSound? That's the only other thing that I would be looking for. – Matt May 06 '16 at 00:05
  • I unfortunately do not see anything about data in the private header. You might have some luck by adding `var data: NSData? { get }` ( or `_data`) to the protocol and trying to access it that way, but I doubt that will work. I will investigate further when I get back to my Mac tomorrow morning. – JAL May 06 '16 at 00:20
  • I'm sorry to say that I could not find any way to access any `NSData`objects from an `NSSound`. I have updated my answer to reflect this. The data representation must be hidden somewhere in the implementation of the class. I did my best to try and win the bounty :) Let me know if you chose to award it or have any other questions. – JAL May 06 '16 at 12:57
  • @JAL Do you think it would be possible to write the `NSSound` object to a temporary file, and then use `NSData(contentsOfFile: String)` to get the data to use to initialize the `AVAudioPlayer` object? – Matt May 29 '16 at 02:49
  • @Matt sure, `NSSound` conforms to `NSCoding` so you could write the data to a file using `NSKeyedArchiver`. – JAL May 31 '16 at 13:01

1 Answers1

3

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.

JAL
  • 41,701
  • 23
  • 172
  • 300