Audio Recording in watchOS Tutorial

Learn how to record audio right in your own watchOS apps! By Soheil Azarpour.

Leave a rating/review
Save for later
Share

In watchOS 2, Apple introduced a new API to play and record multimedia files on the Apple Watch. In watchOS 4, Apple greatly improved the multimedia API and created great opportunities to build innovative apps and enhance the user experience.

In this tutorial, you’ll learn about watchOS 4’s audio recording and playback APIs and how to use them in your apps. You’ll add audio recording to a memo app so that users can record and review their thoughts and experiences right from their wrists. Let’s get started!

Getting Started

Download the starter project for the tutorial here.

The starter project you’ll use in this tutorial is called TurboMemo. Open TurboMemo.xcodeproj in Xcode and make sure the TurboMemo scheme for iPhone is selected. Build and run in the iPhone simulator, and you’ll see the following screen:

Users can record audio diaries by simply tapping the plus (+) button. The app sorts the entries by date, and users can play back an entry by tapping it.

Try adding some entries to create some initial data.

Now, stop the app and change the scheme to TurboMemoWatch. Build and run in the Watch Simulator, and you’ll see the following screen:

The Watch app syncs with the iPhone app to display the same entries, but it doesn’t do anything else yet. You’re about to change that.

Note: TurboMemo uses Watch Connectivity, which is covered in depth in Chapter 16 and Chapter 19.

Audio Playback

There are two ways you can play an audio file in watchOS. You can either use the built-in media player, or build your own. You’ll start with the built-in media player as it’s simpler and more straightforward. In the next section, you’ll build your own media player.

The easiest way to play a media file is to present the built-in media player controller using the presentMediaPlayerController(with:options:completion:) method of WKInterfaceController. All you have to do is to pass in a file URL that corresponds to the index of the row selected by the user in WKInterfaceTable.

Open TurboMemoWatchExtension/InterfaceController.swift, find the implementation of table(_:, didSelectRowAt:) and update it as follows:

// 1
let memo = memos[rowIndex]
// 2
presentMediaPlayerController(
  with: memo.url,
  options: nil,
  completion: {_,_,_ in })

Going through this step-by-step:

  1. You get the selected memo by passing the selected row index to the array of memos.
  2. You present a media player controller by calling presentMediaPlayerController(with:options:completion:) and passing in the URL of the selected memo. You can optionally pass in a dictionary of playback options. Since you don’t want any particular customization at this point, you pass nil. In the completion block, you can check playback results based on your specific needs. Because the API requires a non-nil completion block, you simply provide an empty block.

That’s it! Build and run the app. Tap on a row in the table and you can now listen to the memos!

Note: To learn more about playback options and playing video files, check out Chapter 21: Handoff Video Playback.

Building an Audio Player

The media player controller in watchOS is great for playing short media files but it comes with limitations: As soon as the user dismisses the player, playback stops. This can be a problem if the user is listening to a long audio memo, and you want to continue playing the file even when the user closes the media player.

The built-in media interface can’t be customized either. So if you want more control over the playback and appearance of the media player, you need to build your own.

You’ll use WKAudioFilePlayer to play long audio files. WKAudioFilePlayer gives you more control over playback and the rate of playback. However, you’re responsible for providing an interface and building your own UI.

Note: Apps can play audio content using WKAudioFilePlayer only through a connected Bluetooth headphone or speaker on a real device. You won’t be able to hear the audio using WKAudioFilePlayer either in watchOS simulator or via Apple Watch speaker. Therefore, to follow along with this section, you’ll need an Apple Watch that’s paired with Bluetooth headphones.

The starter project includes AudioPlayerInterfaceController. You’ll use AudioPlayerInterfaceController as a basis for your custom audio player. But before you go there, while you’re still in InterfaceController, you can rewire the code to call the AudioPlayerInterfaceController instead.

Once again, find the implementation of table(_:didSelectRowAtIndex:) in InterfaceController.swift, and update it as follows:

override func table(
  _ table: WKInterfaceTable,
  didSelectRowAt rowIndex: Int) {

    let memo = memos[rowIndex]
    presentController(
      withName: "AudioPlayerInterfaceController",
      context: memo)
}

Make sure you place the existing code entirely. Here, instead of using the built-in media player, you call your soon-to-be-made custom media player. If you build and run at this point, and select a memo entry from the table, you’ll see the new media player that does … nothing! Time to fix that.

Open AudioPlayerInterfaceController scene in TurboMemoWatch/Interface.storyboard. AudioPlayerInterfaceController provides a basic UI for audio playback.

This has:

  • titleLabel which is blank by default
  • playButton that’s hooked up to playButtonTapped().
  • a static label that says Time lapsed:.
  • interfaceTimer that is set to 0 by default.

Now, open AudioPlayerInterfaceController.swift and add the following properties at the beginning of AudioPlayerInterfaceController:

// 1
private var player: WKAudioFilePlayer!
// 2
private var asset: WKAudioFileAsset!
// 3
private var statusObserver: NSKeyValueObservation?
// 4
private var timer: Timer!

Taking this line-by-line:

  1. player is an instance of WKAudioFilePlayer. You’ll use it to play back an audio file.
  2. asset is a representation of the voice memo. You’ll use this to create a new WKAudioFilePlayerItem to play the audio file.
  3. statusObserver is your key-value observer for the player’s status. You’ll need to observer the status of the player and start playing only if the audio file is ready to play.
  4. timer that you use to update the UI. You kick off the timer at the same time you start playing. You do this because currently there’s no other way to know when you’re finished playing the audio file. You’ll have to maintain your own timer with the same duration as your audio file.

You’ll see all these in action in a moment.

Now, add the implementation of awakeWithContext(_:) to AudioPlayerInterfaceController as follows:

override func awake(withContext context: Any?) {
  super.awake(withContext: context)
  // 1
  let memo = context as! VoiceMemo
  // 2
  asset = WKAudioFileAsset(url: memo.url)
  // 3
  titleLabel.setText(memo.filename)
  // 4
  playButton.setEnabled(false)
}

Again, taking this line-by-line:

  1. After calling super as ordinary, you know for sure the context that’s being passed to the controller is a VoiceMemo. This is Design by Contract!
  2. Create a WKAudioFileAsset object with the voice memo and store it in asset. You’ll reuse the asset to replay the same memo when user taps on the play button.
  3. Set the titleLabel with the filename of the memo.
  4. Disable the playButton until the file is ready to be played.

You prepared the interface to playback an audio file, but you haven’t done anything to actually play it. You’ll kick off the playback in didAppear() so that playback starts when the interface is fully presented to the user.

Speaking of didAppear(), add the following to AudioPlayerInterfaceController:

override func didAppear() {
  super.didAppear()
  prepareToPlay()
}

Here, you simply call a convenient method, prepareToPlay(). So let’s add that next:

private func prepareToPlay() {
  // 1
  let playerItem = WKAudioFilePlayerItem(asset: asset)
  // 2
  player = WKAudioFilePlayer(playerItem: playerItem)
  // 3
  statusObserver = player.observe(
    \.status,
    changeHandler: { [weak self] (player, change) in
      // 4
      guard
        player.status == .readyToPlay,
        let duration = self?.asset.duration
        else { return }
      // 5
      let date = Date(timeIntervalSinceNow: duration)
      self?.interfaceTimer.setDate(date)
      // 6
      self?.playButton.setEnabled(false)
      // 7
      player.play()
      self?.interfaceTimer.start()
      // 8
      self?.timer = Timer.scheduledTimer(
        withTimeInterval: duration, 
        repeats: false, block: { _ in
        
        self?.playButton.setEnabled(true)
      })
  })
}

There’s a lot going on here:

  1. Create a WKAudioFilePlayerItem object from the asset you set earlier in awake(withContext:). You have to do this each time you want to play a media file, since WKAudioFilePlayerItem can’t be reused.
  2. Initialize the player with the WKAudioFilePlayerItem you just created. You’ll have to do this even if you’re playing the same file again.
  3. The player may not be ready to play the audio file immediately. You need to observe the status of the WKAudioFilePlayer object, and whenever it’s set to .readyToPlay, you can start the playback. You use the new Swift 4 key-value observation (KVO) API to listen to changes in player.status.
  4. In the observer block, you check for the player’s status and if it’s .readyToPlay, you safely unwrap duration of the asset and continue. Otherwise, you simply ignore the change notification.
  5. Once the item is ready to play, you create a Date object with the duration of the memo, and update interfaceTimer to show the lapsed time.
  6. Disable the playButton while you’re playing the file.
  7. Start playing by calling player.play(), and at the same time, start the countdown in the interface.
  8. Kick off an internal timer to re-enable the playButton after the playback is finished so the user can start it again if they wish.

That was a big chunk of code, but as you see, it’s mostly about maintaining the state of the WKAudioFilePlayer and keeping the interface in sync.

Note: Unfortunately, at the time of writing this tutorial, currentTime of WKAudioFilePlayerItem is not KVO-complaint so you can’t add an observer. Ideally, you would want to observe currentTime instead of maintaining a separate timer on your own.

Before you build and run, there’s one more thing to add!

When the timer is up and playButton is enabled, the user should be able to tap on Play to restart playing the same file. To implement this, find the implementation of playButtonTapped() in AudioPlayerInterfaceController.swift and update it as follows:

@IBAction func playButtonTapped() {
  prepareToPlay()
}

It’s that simple! Merely call the convenient method, prepareToPlay(), to restart the playback.

Next, build and run, and select a voice memo from the list. The app will present your custom interface. The interface will automatically start playing the audio file, and once it’s stopped, the Play button will be re-enabled and you can play it again.

If you have more than one item to play, such as in a playlist, you’ll want to use WKAudioFileQueuePlayer instead of WKAudioFilePlayer and queue your items. The system will play queued items back-to-back and provide a seamless transition between files.