Search code examples
audiosprite-kitwatchos-3

How can I play a custom sound in WatchOS 3 that will playback on the watch speakers


I've read that we can now play custom sounds on the apple watch in watchos 3.

According to the announcement from Apple so apparently there is but I don't have an example to test it out: 3D spatial audio implemented using SCNAudioSource or SCNAudioPlayer. Instead, use playAudioSource:waitForCompletion: or the WatchKit sound or haptic APIs. Found here: https://developer.apple.com/library/prerelease/content/releasenotes/General/WhatsNewInwatchOS/Articles/watchOS3.html

Can someone place a simple example of this. I'm not using SceneKit in my app as I don't need it but if that's the only way to play a custom sound then I'd like to know the minimum code required to accomplish this. Preferably in Objective c but I'll take it in whatever shape. I'm ok using SpriteKit if that's easier also.

Here's what I have so far but it doesn't work:

SCNNode * audioNode = [[SCNNode alloc] init];

SCNAudioSource * audioSource = [SCNAudioSource audioSourceNamed:@"mysound.mp3"];
SCNAudioPlayer * audioPlayer = [SCNAudioPlayer audioPlayerWithSource:audioSource];  
[audioNode addAudioPlayer:audioPlayer];

SCNAction * play = [SCNAction playAudioSource:audioSource waitForCompletion:YES];
[audioNode runAction:play];

Solution

  • This is Objective-c but can be translated into Swift

    I ended up using AVAudioEngine and AVAudioPlayerNode to play audio on the Apple watch.

    The gist of how to do this is as follows:

    I call the following inside the init method of my AudioPlayer (it's an NSObject subclass to encapsulate the functionality)

    _audioPlayer = [[AVAudioPlayerNode alloc] init];
    _audioEngine = [[AVAudioEngine alloc] init];
    [_audioEngine attachNode:_audioPlayer];
    
    AVAudioFormat *stereoFormat = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100 channels:2];
    [_audioEngine connect:_audioPlayer to:_audioEngine.mainMixerNode format:stereoFormat];
    
    if (!_audioEngine.isRunning) {
        NSError* error;
        [_audioEngine startAndReturnError:&error];
    }
    

    I have a cache setup so I don't recreate the AVAudioFile assets every time I want to play a sound but you don't need to.

    So next create an AVAudioFile object:

    NSError *error;
    NSBundle* appBundle = [NSBundle mainBundle];
    NSURL *url = [NSURL fileURLWithPath:[appBundle pathForResource:key ofType:@"aifc"]];
    AVAudioFile *asset = [[AVAudioFile alloc] initForReading:url &error];
    

    Then play that file:

    [_audioPlayer scheduleFile:asset atTime:nil completionHandler:nil];
    [_audioPlayer play];
    

    UPDATE: If the app goes to sleep or is put to the background there is a chance the audio will stop playing/fade out. By activating an Audio Session this will be prevented.

        NSError *error;
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
        if (error) {
          NSLog(@"AVAudioSession setCategory ERROR: %@", error.localizedDescription);
        }
        [[AVAudioSession sharedInstance] setActive:YES error:&error];
        if (error) {
          NSLog(@"AVAudioSession setActive ERROR: %@", error.localizedDescription);
        }
    

    I didn't go over handling any errors but this should work. Don't forget to #import <AVFoundation/AVFoundation.h> at the top of your implementation file.