Active Filters: AVFoundation

Playing audio is an important part of many apps. One common trick is to fade in the volume of audio playback so we don't surprise or startle the user. This year, Apple has made this much simpler to implement using AVAudioPlayer. Let's take a look.

First we'll set up a standard AVAudioPlayer, and begin playing it at a volume of 0:

guard let asset = NSDataAsset(name: "alarm") else { print("Error Loading Audio."); return }

let player: AVAudioPlayer

do {
  player = try AVAudioPlayer(data: asset.data)
} catch { print("Error Playing."); return }

player.volume = 0
player.numberOfLoops = -1

player.play()

At this point the audio is playing but we can't hear it.

Before we check out the new feature, let's review the "old" way we might do this.

Before iOS 10, macOS 10.12, and tvOS 10, fading this audio in was, well, let's just call it "verbose":

func fadeInPlayer() {
  if player.volume <= 1 - fadeVolumeStep {
    player.volume += fadeVolumeStep
    dispatchAfterDelayHelper(time: fadeVolumeStepTime) { fadeInPlayer() }
  } else {
    player.volume = 1
  }
}

fadeInPlayer()

Recursive functions, GCD delays, manually managing state. Yuck.

Thankfully, there's now a better way.

Here's all it takes:

player.setVolume(1, fadeDuration: 1.5)

This single line of code will fade in the audio from our initial volume of 0 up to 1 over a period of 1.5 seconds.

🙌

Neat!

Ever since 1983 when Matthew Broderick's IMSAI 8080 began speaking out loud, we've dreamed of computers that can have conversations with us.

In iOS 9, Apple added the ability to synthesize speech using the high-quality 'Alex' voice. Sadly it's only available on US devices for now, but that's sure to change. Let's try it out:

guard let voice = AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex) else { return }

let synth = AVSpeechSynthesizer()
synth.delegate = self

let utter = AVSpeechUtterance(string: "Would you like to play a game?")
utter.voice = voice

synth.speakUtterance(utter)

We start by making sure 'Alex' is available, then we make a new synthesizer. Next, we create an AVSpeechUtterance, and set it's voice. Then, we simply tell the synthesizer to speak! Very cool.

Even cooler, we can implement one of the optional functions of AVSpeechSynthesizerDelegate to get live progress callbacks as each word is spoken. Neat!

func speechSynthesizer(synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
  print(characterRange)
}

Today we'll take a look at how to record audio from the microphone on a user's device. Let's get started.

The first thing we'll need is an Audio Session. This will be a singleton (Bite #4), and we'll also create a property to hold our recorder:

import AVFoundation

class RecordViewController : UIViewController {
  let session = AVAudioSession.sharedInstance()
  var recorder: AVAudioRecorder?
}

Next, we'll create a function to start recording:

func beginRecording() {
  session.requestRecordPermission { granted in
    guard granted else { return }

    do {
      try self.session.setCategory(AVAudioSessionCategoryPlayAndRecord)
      try self.session.setActive(true)

      let recordingFileName = "recording.caf"

      guard let recordingURL = documentsDirectoryURL()?
        .URLByAppendingPathComponent(recordingFileName) else { return }

      let settings: [String : AnyObject] = [
        AVEncoderAudioQualityKey: AVAudioQuality.High.rawValue,
        AVSampleRateKey: 12000.0,
        AVNumberOfChannelsKey: 1,
        AVFormatIDKey: Int(kAudioFormatMPEG4AAC)
      ]

      try self.recorder = AVAudioRecorder(
        URL: recordingURL, 
        settings: settings
      )
    } catch { }
  }
}

We'll first need to request permission from the user to record audio. Once granted, we'll try to set our Audio Session's category to PlayAndRecord, the category Apple suggests for apps that simultaneously record and playback audio.

We'll create a place for our recording to live, then assemble the settings dictionary for our recorder. We instantiate and store our AVAudioRecorder object, then tell it to start recording. Later, we'll call .stop() on it to stop recording. We can also optionally wire up a delegate to get completion callbacks.

Finally, we can play back our file using AVAudioPlayer:

let audioPlayer = try AVAudioPlayer(contentsOfURL: recordingURL)
audioPlayer.prepareToPlay()
audioPlayer.play()

In Bite #101 we started working on a custom camera view controller.

Today we'll complete it by adding a way for users to capture a photo and do something with it. We'll start by making it easy to use. We'll make the whole screen a capture button by adding a tap gesture recognizer to our view:

let tapGR = UITapGestureRecognizer(target: self, action: "capturePhoto:")
tapGR.numberOfTapsRequired = 1
view.addGestureRecognizer(tapGR)

Next, we want to ask our output to capture a still image. Before we can, we'll need an AVCaptureConnection.

Connections were already implicitly created for us by our session. They represent the conceptual pipes that move data between inputs and outputs.

We grab a connection and use it to ask our output to capture a still image, asynchronously:

func capturePhoto(tapGR: UITapGestureRecognizer) {
  guard let connection = output.connectionWithMediaType(AVMediaTypeVideo) else { return }
  connection.videoOrientation = .Portrait

  output.captureStillImageAsynchronouslyFromConnection(connection) { (sampleBuffer, error) in
    guard sampleBuffer != nil && error == nil else { return }

    let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(sampleBuffer)
    guard let image = UIImage(data: imageData) else { return }

    self.presentActivityVCForImage(image)
  }
}

In the closure, we'll do a safety check then convert the CMSampleBuffer we've been given into an NSData then a UIImage.

Lastly, we'll use UIActivityViewController (covered in Bite #71) to allow the user to do something with their new photo.

Download the project we built in Bites #101 & #102 at j.mp/bite102

We looked at allowing our users to capture photos/videos using UIImagePickerController in Bite #83. Now we'll take things to the next level by starting to create our own custom camera view controller. Today we'll get all the plumbing wired up and get the preview on the screen. Let's get started.

func setupSession() {
  session = AVCaptureSession()
  session.sessionPreset = AVCaptureSessionPresetPhoto

  let camera = AVCaptureDevice
    .defaultDeviceWithMediaType(AVMediaTypeVideo)

  do { input = try AVCaptureDeviceInput(device: camera) } catch { return }

  output = AVCaptureStillImageOutput()
  output.outputSettings = [ AVVideoCodecKey: AVVideoCodecJPEG ]

  guard session.canAddInput(input) && session.canAddOutput(output) else { return }

  session.addInput(input)
  session.addOutput(output)

  previewLayer = AVCaptureVideoPreviewLayer(session: session)
  previewLayer!.videoGravity = AVLayerVideoGravityResizeAspect
  previewLayer!.frame = view.bounds
  previewLayer!.connection?.videoOrientation = .Portrait

  view.layer.addSublayer(previewLayer!)

  session.startRunning()
}

We'll start with the "single view" template in Xcode. There are a number of different objects we'll need to setup and glue together, so we'll go into our view controller and add a function called setupSession. We'll call this in viewWillAppear(animated:).

First we'll instantiate an AVCaptureSession. It's sort of the central hub of all this. We can configure it with a number of different presets. We'll use a preset for taking high quality still photos. Now, our session needs some inputs and outputs.

We'll use defaulDeviceWithMediaType and pass in Video to get the default hardware device for capturing and recording images on the user's device (usually the the back camera). Then we'll try to create an AVCaptureDeviceInput from the device. Next up, an output.

Capture sessions can return us data in all sorts of interesting ways: Still images, videos, raw pixel data, and more. Here we'll set up an AVCaptureStillImageOutput and ask it for JPEG photos. We'll do one more safety check then add both our input and output to our session.

Finally, let's display our camera so the user can see what they're photographing.

We'll pass our session into a new AVCapturePreviewLayer and add it to our view. Then we just need to start the session. If we run the app we'll see it's starting to look like a camera, neat!

Tomorrow, we'll finish up by adding the ability to actually capture some photos.

Update: Part 2 is right here