AVSpeechSynthesizerをバックグラウンド再生ありで使う

(2019/08/09更新。バックグラウンド移行時に音が止まっているならAudioSessionを無効化する方法に書き換えました。)

以前[iOS]電話がかかってきたとき、ヘッドホンジャックが抜かれたときの対応 – nackpan Blogという記事を書きました。

そこでのkkさんのコメントをきっかけにAVSpeechSynthesizerの使い方について調べました。

AVSpeechSynthesizerを用いてテキストを読み上げるアプリ。フォアグラウンドで再生している場合は問題ありません。
バックグラウンド再生を絡めた場合にトラブル。
AVSpeechSynthesizerでspeak中にpauseしたあとアプリを中断、再びアプリを開くと勝手に音が鳴るなど、アプリ再開時の挙動が妙でした。

ログを見るとアプリ再開時にinterruptionの通知が届いていたので、interruptionNotificationについて調査。

InterruptionNotificationについて

Interruptionの通知は、電話がかかってきた時などでAudioSessionに割り込みが発生した時に届くものと考えていました。
今回、アプリの再開時に、interruptionの通知が届きました。

Appleのドキュメントを見直しました。

interruptionNotification – AVAudioSession | Apple Developer Documentation

Noteとして以下の記述があります。

Starting in iOS 10, the system will deactivate the audio session of most apps in response to the app process being suspended. When the app starts running again, it will receive an interruption notification that its audio session has been deactivated by the system. This notification is necessarily delayed in time because it can only be delivered once the app is running again. If your app’s audio session was suspended for this reason, the userInfo dictionary will contain the AVAudioSessionInterruptionWasSuspendedKey key with a value of true.

If your audio session is configured to be non-mixable (the default behavior for the playback, playAndRecord, soloAmbient, and multiRoute categories), it’s recommended that you deactivate your audio session if you’re not actively using audio when you go into the background. Doing so will avoid having your audio session deactivated by the system (and receiving this somewhat confusing notification).

interruptionNotification – AVAudioSession | Apple Developer Documentation

 * iOS 10以降では、システムは、アプリプロセスが中断されると、ほとんどのアプリの AudioSessionを無効にする。
*  アプリの再開時に、AudioSessionがシステムによって無効にされたことを示す割り込み(interruption)通知を受け取る。
* この通知でAVAudioSessionInterruptionWasSuspendedKeyの値はtrue
* AudioSessionがミックス不可(playback、playAndRecord、soloAmbient、multiRouteのデフォルトの動作)設定なら、バックグラウンド移行時に、アクティブに使用していないAudioSessionを無効にすることをお勧め
 * そうすることで、この通知を受け取らなくて済む

とあるように、システムによってAudioSessionが無効にされたことを示すinterruptionがアプリの再開時に届く、とのことでした。

InterruptionNotificationの実験

バックグラウンド再生ありのAVSpeechSynthesizerを使ったアプリで、interruptionの通知について実験しました。
当初、実験した際(07/29)には、iOS 12.4で
* AVSpeechSynthesizerでspeak中に、pause。電源ボタンを押してアプリを中断。すると音声が勝手に鳴ってしまう。電源ボタンを押した直後にinterruptionの通知が届きました。(willResignActiveの前に発生)
という症状が発生しました。
しかし、その後あらためて実験すると、まったくこの症状は出ず、interruptionNotification – AVAudioSession のドキュメントにあるように再開後にinterruptionの通知が届きました。

対処法

アプリ再開時に音が勝手になるなど挙動が変になる問題は、システムによるAudioSessionの無効化が関係しているのだろう。AVSpeechSynthesizerでは再開時に上手いことやってくれない。Noteにあるように、明示的にAudioSessionを無効化すれば、通知を受け取らずに済むし挙動も適切なものになると考えました。

interruptionNotificationのNoteでは、アプリのAudioSessionが無効にされたことを示す通知がアプリ再開後に届くとあり、それを避けるには、バックグラウンド移行時にAudioSessionを無効化するとよい、とありました。


当初の実験(07/29)では、iOS 12.4での電源ボタンによるアプリの中断で、電源ボタンを押した直後に通知が届き、バックグラウンド移行時にAudioSessionを無効化する処理を書いても、音声が短いながらも勝手に鳴ってしまう症状が出ました。そのため、AVSpeechSynthesizerでの音声再生が終了したら、その段階でAudioSessionを無効化にするということで対処することにしていました。これにはAVSpeechSynthesizerDelegateを使いAVSpeechSynthesizerのpause, stop, finishの完了を知り、そこでAudioSessionを無効化にしていました。

しかし、その後の実験では「電源ボタンによるアプリの中断で、電源ボタンを押した直後に通知が届き、音声が短いながらも勝手に鳴ってしまう」症状は再現しませんでした。


そこで、interruptionNotificationのNoteが勧める方法に従いました。
バックグラウンド移行時に音が止まっている場合、AudioSessionを無効化。
speakのさいにAudioSessionを有効化しました。

バックグラウンド移行を検知する方法はこちらの記事が参考になります。
NotificationCenterを用いたライフサイクルイベントの検知

実装

今回はViewController.swiftにすべて記述することとしました。
メッセージラベル、Speakボタン、Pauseボタン、Stopボタンを貼り付けます。

アプリ画面

メッセージラベルとViewControllerをOutletで結びます。
@IBOutlet weak var messageLabel: UILabel!
SpeakボタンとViewControllerをActionで結びます。
@IBAction func speakBtnTapped(_ sender: Any) 
PauseボタンとViewControllerをActionで結びます。
@IBAction func pauseBtnTapped(_ sender: Any)
StopボタンとViewControllerをActionで結びます。
@IBAction func stopBtnTapped(_ sender: Any)

バックグラウンド再生用。
Capabilities > Background Modes をON > Audio, AirPlay, and Picture in Pictureにチェック

ViewController.swift

import UIKit
import AVFoundation
import AVKit
import MediaPlayer

class ViewController: UIViewController, AVSpeechSynthesizerDelegate {

    @IBOutlet weak var messageLabel: UILabel!
    var syntherizer = AVSpeechSynthesizer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // AudioSessionカテゴリをbackground再生ができるものに設定
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(AVAudioSession.Category.playback)
        } catch {
            print("Setting category to AVAudioSessionCategoryPlayback failed.")
        }

        addAudioSessionObservers()
        addRemoteCommandEvent()
        addLifeCycleObserver()
    }
    
    // 電話による割り込みと、オーディオルートの変化を監視します
    func addAudioSessionObservers() {
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
        center.addObserver(self, selector: #selector(audioSessionRouteChanged(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
    }
    
    /// Interruption : 電話などによる割り込み
    @objc func handleInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
            let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                return
        }
        
        if type == .began {
            // interruptionが開始した時(電話がかかってきたなど)
            if let wasSuspendedKeyValue = userInfo[AVAudioSessionInterruptionWasSuspendedKey] as? NSNumber {
                let wasSuspendedKey = wasSuspendedKeyValue.boolValue
                if wasSuspendedKey {
                    // suspeended key : true
                } else {
                    // suspended key : false
                }
            } else {
                // suspended key : nil
            }
        }
        else if type == .ended {
            // interruptionが終了した時の処理
            if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption Ended - playback should resume
                } else {
                    // Interruption Ended - playback should NOT resume
                }
            }
        }
    }
    
    /// Audio Session Route Change : ルートが変化した(ヘッドフォンが抜き差しされた)
    @objc func audioSessionRouteChanged(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
            let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
                return
        }
        
        DispatchQueue.main.async {
            self.messageLabel.text = self.routeChangeReasonDescription(reason: reason)
        }

        switch reason {
        case .newDeviceAvailable:
            let session = AVAudioSession.sharedInstance()
            for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                // ヘッドフォンがつながった
                
                break
            }
        case .oldDeviceUnavailable:
            if let previousRoute =
                userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
                for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                    // ヘッドフォンが外れた
                    
                    // 音声をpauseしています
                    pause()
                    break
                }
            }
        default: ()
        }
        
    }
    
    public func speak() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(true)
        } catch {
            
        }
        let voice = AVSpeechSynthesisVoice(language: "ja-JP")
        let utterance = AVSpeechUtterance(string: "おはようございます。今日もいい天気ですね。音声合成で読み上げています。文章をうまくよめていますか?")
        utterance.voice = voice
        utterance.preUtteranceDelay = 5
        
        if syntherizer.isPaused {
            syntherizer.continueSpeaking()
        } else {
            syntherizer.speak(utterance)
        }
    }
    
    public func pause() {
        syntherizer.pauseSpeaking(at: .immediate)
    }
    
    /// Speakボタンが押された
    @IBAction func speakBtnTapped(_ sender: Any) {
        // 音声出力を行う
        speak()
    }
    
    /// pauseボタンが押された
    @IBAction func pauseBtnTapped(_ sender: Any) {
        pause()
    }
    
    @IBAction func stopBtnTapped(_ sender: Any) {
        syntherizer.stopSpeaking(at: .immediate)
    }
    
    /// 監視する必要がなくなった段階で、Observerを取り外します
    private func removeAudioSessionObservers() {
        let center = NotificationCenter.default
        
        // AVAudio Session
        center.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
        center.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)
        
    }
    
    /// route変更の理由表示用
    private func routeChangeReasonDescription(reason: AVAudioSession.RouteChangeReason) -> String {
        switch reason {
        case .unknown:
            return "unknown"
        case .newDeviceAvailable:
            return "newDeviceAvailable"
        case .oldDeviceUnavailable:
            return "oldDeviceUnailable"
        case .categoryChange:
            return "categoryChange"
        case .override:
            return "override"
        case .wakeFromSleep:
            return "wakeFromSleep"
        case .noSuitableRouteForCategory:
            return "noSuitableRouteForCategory"
        case .routeConfigurationChange:
            return "routeConfigurationChange"
        default:
            return "default"
        }
    }
    
    // MARK: Remote Command Event
    func addRemoteCommandEvent() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(type(of: self).remoteTogglePlayPause(_:)))
        commandCenter.playCommand.addTarget(self, action: #selector(type(of: self).remotePlay(_:)))
        commandCenter.pauseCommand.addTarget(self, action: #selector(type(of: self).remotePause(_:)))
        commandCenter.nextTrackCommand.addTarget(self, action: #selector(type(of: self).remoteNextTrack(_:)))
        commandCenter.previousTrackCommand.addTarget(self, action: #selector(type(of: self).remotePrevTrack(_:)))
    }
    
    @objc func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
        // イヤホンのセンターボタンを押した時の処理
        // 略

    }
    
    @objc func remotePlay(_ event: MPRemoteCommandEvent) {
        // プレイボタンが押された時の処理
        speak()

    }
    
    @objc func remotePause(_ event: MPRemoteCommandEvent) {
        // ポーズボタンが押された時の処理
        pause()
        
    }
    
    @objc func remoteNextTrack(_ event: MPRemoteCommandEvent) {
        // 「次へ」ボタンが押された時の処理
        // (略)
    }
    
    @objc func remotePrevTrack(_ event: MPRemoteCommandEvent) {
        // 「前へ」ボタンが押された時の処理
        // (略)
        
    }
    
    // MARK: - Life Cycle
    func addLifeCycleObserver() {
        let center = NotificationCenter.default
        // 今回はbackground移行を検知
        center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    
    @objc func didEnterBackground(_ notification: Notification) {
        // 音が止まっていたらAudioSessionを無効化
        if !syntherizer.isSpeaking || syntherizer.isPaused {
            deactivateAudioSession()
        }

        let app = UIApplication.shared
        app.beginBackgroundTask(expirationHandler: {
            // このblockはバックグラウンド処理に入って、所定時間後(180秒程度後)に実行される
            // (バックグラウンド再生が継続している間は実行されない)
            // ここでAudioSessionを無効化しておく
            // (この処理がないとバックグラウンド再生中にpauseしたあと、3分すぎにアプリを再開すると
            // いきなり音が鳴る症状が出てしまう)
            self.deactivateAudioSession()
            
        })
    }
    
    func deactivateAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(false)
        } catch {
            
        }
    }
}

[iOS][Swift]ミュージックライブラリの音楽の再生、情報の表示(MPMusicPlayerController使用)

MPMusicPlayerControllerを使ったミュージックライブラリアイテムの再生に関してこちらに記事を書きました。
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用) | nackpan Blog
この記事では、ミュージックライブラリから一つの曲を選んで再生・一時停止・停止を行いました。

今回のサンプルでは、MPMusicPlayerControllerを使用して複数のアイテムを順に再生します。
また、再生中の音楽アイテムの情報(アートワーク、アルバム名等)を表示します。
前回のサンプルでは、選曲するとすぐに再生を開始していましたが、今回は選曲するとアイテム情報を表示しますが再生は開始しないこととしました。PLAYボタンを押すと再生開始です。
再生されているアイテムが変更されると、それを検知して変更後のアイテム情報を表示します。
ファイル 2016-04-13 7 24 35
(今回のサンプルのスクリーンショット)

MusicPlayerControllerを使って、ミュージックライブラリのアイテムを再生し、情報を表示する

iPodライブラリアクセス プログラミングガイド(PDF)
iPod Library Access Programming Guide(英語版)
このappleのドキュメントにライブラリにアクセスして再生する方法、必要な曲を選択する方法、現在再生中の音楽の情報を知る方法等、まとめてあります。
(ちなみに、日本語ドキュメント – Apple DeveloperのページにappleのiOS用日本語ドキュメントがまとめてあります。英語版へのリンクもあります)

iOSシミュレータでは動作しないので、実機を用いてください。

Single View Applicationでプロジェクトを作成。
ss 2015-09-16 6.46.53

ViewController.SwiftにMediaPlayerフレームワークをimportします。

import UIKit
import MediaPlayer

class ViewController: UIViewController {

UI配置

ボタンとラベルとイメージビュー(Image View)を配置。
ss 2016-04-13 2.02.06

UIとViewControllerとの接続

ラベルおよびイメージビューとViewController.swiftをOutletを作成して接続。
ss 2016-04-12 21.53.43outletLabels
ss 2016-04-12 21.52.55

ボタンとViewController.swiftをactionを作成して接続。
ss 2016-04-13 2.35.53Action
ss 2015-09-13 9.09.50

プレイヤー準備

プレイヤーを表すpropertyをViewController.swiftに加えます。

class ViewController: UIViewController, MPMediaPickerControllerDelegate {
    
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var artistLabel: UILabel!
    @IBOutlet weak var albumLabel: UILabel!
    @IBOutlet weak var songLabel: UILabel!

    var player = MPMusicPlayerController()
    
    override func viewDidLoad() {

プレイヤーのインスタンスを作成。

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        player = MPMusicPlayerController.applicationMusicPlayer()
        //player = MPMusicPlayerController.systemMusicPlayer()
        
    }

(ここで、applicationMusicPlayerではなく、systemMusicPlayerを用いると、「ミュージック」アプリでの再生状況(再生アイテムや、シャッフル、リピートなどのモード)を反映したものになる)

再生中アイテムの変更通知

今回は、複数の曲を選択して順に再生していきます。
アイテム情報を表示するには、現在再生中のアイテムがなにか分かっていなければなりません。
ミュージックプレーヤー通知という仕組みで、再生中アイテムに変更があった場合には通知を受け取ることができます。
この通知を受けて、プレイヤーが現在再生中のアイテムを取得し情報表示を更新します。

まず、再生中アイテム変更イベントを監視します。

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        player = MPMusicPlayerController.applicationMusicPlayer()
        //player = MPMusicPlayerController.systemMusicPlayer()
        
        // 再生中のItemが変わった時に通知を受け取る
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector: #selector(ViewController.nowPlayingItemChanged(_:)), name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // 通知の有効化
        player.beginGeneratingPlaybackNotifications()
        
    }

再生中のアイテムが変更になったときに、ViewController.nowPlayingItemChanged(_:)を呼び出すように指定しています。

また、ViewControllerのdeinit内に、通知を受け取る必要がなくなった場合の後処理を書いておきます。

    deinit {
        // 再生中アイテム変更に対する監視をはずす
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.removeObserver(self, name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // ミュージックプレーヤー通知の無効化  
        player.endGeneratingPlaybackNotifications()
    }

再生中のアイテムが変更になった際に呼び出されるViewController.nowPlayingItemChanged(_:)の内容を書きます。

    /// 再生中の曲が変更になったときに呼ばれる  
    func nowPlayingItemChanged(notification: NSNotification) {
        
        if let mediaItem = player.nowPlayingItem {
            updateSongInformationUI(mediaItem)
        }
        
    }

MPMusicPlayerControllerのプロパティnowPlayingItemで再生中のアイテムを取得できます。
(今回は使いませんが、アイテムをsetすることもできます)
nowPlayingItemはMPMediaItem型のプロパティです。

MPMediaItem

ミュージックライブラリに対するアクセスでは、MPMediaItemというクラスでアイテム情報を扱います。
MPMediaItem Class Reference
MPMediaItemは、title, artist, artworkなどなど、さまざまな情報をもっています。
アイテム情報を表示する際には、現在再生中のアイテムから情報を取得してラベルやイメージビューに表示します。

    /// 曲情報を表示する
    func updateSongInformationUI(mediaItem: MPMediaItem) {
    
        // 曲情報表示
        // (a ?? b は、a != nil ? a! : b を示す演算子です)  
        // (aがnilの場合にはbとなります)
        artistLabel.text = mediaItem.artist ?? "不明なアーティスト"
        albumLabel.text = mediaItem.albumTitle ?? "不明なアルバム"
        songLabel.text = mediaItem.title ?? "不明な曲"
        
        // アートワーク表示
        if let artwork = mediaItem.artwork {
            let image = artwork.imageWithSize(imageView.bounds.size)
            imageView.image = image
        } else {
            // アートワークがないとき
            // (今回は灰色表示としました)
            imageView.image = nil
            imageView.backgroundColor = UIColor.grayColor()
        }
        
    }

メディアアイテムピッカー

曲を選択するために、メディアアイテムピッカーを用います。
(メディアアイテムピッカーというのは、iOSで用意されているあらかじめ設定済みのViewController。ミュージックライブラリの選択画面と同じようなことが出来る)
ss 2015-09-13 11 41 39

メディアアイテムピッカーでの「選択完了したとき」や「キャンセルされたとき」のイベントを、ViewControllerで受け取れるようにします。
そのために、ViewControllerをメディアアイテムピッカーのデリゲートとして設定します。
「選曲」ボタンを押すと、メディアアイテムピッカーを作成して、デリゲートの設定を行い、ライブラリの曲を選択できるようにします。
「選択完了したとき」「キャンセルされたとき」のメソッドを記述します。

import UIKit
import MediaPlayer

class ViewController: UIViewController, MPMediaPickerControllerDelegate {
    @IBAction func pick(sender: AnyObject) {
        // MPMediaPickerControllerのインスタンスを作成
        let picker = MPMediaPickerController()
        // ピッカーのデリゲートを設定
        picker.delegate = self
        // 複数選択にする。(falseにすると、単数選択になる)
        picker.allowsPickingMultipleItems = true
        // ピッカーを表示する
        presentViewController(picker, animated: true, completion: nil)
        
    }
    
    /// メディアアイテムピッカーでアイテムを選択完了したときに呼び出される
    func mediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        
        // プレイヤーを止める
        player.stop()
        
        // 選択した曲情報がmediaItemCollectionに入っているので、これをplayerにセット。
        player.setQueueWithItemCollection(mediaItemCollection)
        
        // 選択した曲から最初の曲の情報を表示
        if let mediaItem = mediaItemCollection.items.first {
            updateSongInformationUI(mediaItem)
        }
        
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
        
    }
    
    //選択がキャンセルされた場合に呼ばれる
    func mediaPickerDidCancel(mediaPicker: MPMediaPickerController) {
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
    }

選択完了した時に呼び出されるmediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection)で、MPMediaItemCollection型の引数mediaItemCollectionがありますが、これにアイテム情報が入っています。
MPMediaItemCollection Class Reference
MPMediaItemCollectionクラスは、MPMediaItemを集めて管理しているクラスです。
そのプロパティitemsがMPMediaItemの配列になっていますので、今回はそこから先頭のMPMediaItemを取得してそれを元に情報を表示しています。

再生・一時停止・停止

各ボタンのアクションに、「再生」「一時停止」「停止」機能を加えます。

    @IBAction func pushPlay(sender: AnyObject) {
        player.play()
    }

    @IBAction func pushPause(sender: AnyObject) {
        player.pause()
    }

    @IBAction func pushStop(sender: AnyObject) {
        player.stop()
    }

iOSシミュレータでは動作しません。実機を用いてください。 「選曲」ボタンを押すと、メディアアイテムピッカーが表示されるので、曲を選択してください。複数の曲を選択できます。「PLAY」ボタンで音楽の再生。「PAUSE」ボタンで一時停止。「STOP」ボタンで音楽を止めて、再生位置を一番始めに戻します。


ViewController.swift全文

//
//  ViewController.swift
//  MPMusicPlayerControllerDemo2
//
//  Created by KUWAJIMA MITSURU on 2016/04/12.
//  Copyright © 2016年 nackpan. All rights reserved.
//

import UIKit
import MediaPlayer

class ViewController: UIViewController, MPMediaPickerControllerDelegate {
    
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var artistLabel: UILabel!
    @IBOutlet weak var albumLabel: UILabel!
    @IBOutlet weak var songLabel: UILabel!

    var player = MPMusicPlayerController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        player = MPMusicPlayerController.applicationMusicPlayer()
        //player = MPMusicPlayerController.systemMusicPlayer()
        
        // 再生中のItemが変わった時に通知を受け取る
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector: #selector(ViewController.nowPlayingItemChanged(_:)), name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // 通知の有効化
        player.beginGeneratingPlaybackNotifications()
        
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func pick(sender: AnyObject) {
        // MPMediaPickerControllerのインスタンスを作成
        let picker = MPMediaPickerController()
        // ピッカーのデリゲートを設定
        picker.delegate = self
        // 複数選択にする。(falseにすると、単数選択になる)
        picker.allowsPickingMultipleItems = true
        // ピッカーを表示する
        presentViewController(picker, animated: true, completion: nil)
        
    }
    
    /// メディアアイテムピッカーでアイテムを選択完了したときに呼び出される
    func mediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        
        // プレイヤーを止める
        player.stop()
        
        // 選択した曲情報がmediaItemCollectionに入っているので、これをplayerにセット。
        player.setQueueWithItemCollection(mediaItemCollection)
        
        // 選択した曲から最初の曲の情報を表示
        if let mediaItem = mediaItemCollection.items.first {
            updateSongInformationUI(mediaItem)
        }
        
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
        
    }

    
    /// 選択がキャンセルされた場合に呼ばれる
    func mediaPickerDidCancel(mediaPicker: MPMediaPickerController) {
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
    }
    
    /// 曲情報を表示する
    func updateSongInformationUI(mediaItem: MPMediaItem) {
    
        // 曲情報表示
        // (a ?? b は、a != nil ? a! : b を示す演算子です)  
        // (aがnilの場合にはbとなります)
        artistLabel.text = mediaItem.artist ?? "不明なアーティスト"
        albumLabel.text = mediaItem.albumTitle ?? "不明なアルバム"
        songLabel.text = mediaItem.title ?? "不明な曲"
        
        // アートワーク表示
        if let artwork = mediaItem.artwork {
            let image = artwork.imageWithSize(imageView.bounds.size)
            imageView.image = image
        } else {
            // アートワークがないとき
            // (今回は灰色表示としました)
            imageView.image = nil
            imageView.backgroundColor = UIColor.grayColor()
        }
        
    }
    

    
    
    @IBAction func pushPlay(sender: AnyObject) {
        player.play()
    }
    
    @IBAction func pushPause(sender: AnyObject) {
        player.pause()
    }
    
    @IBAction func pushStop(sender: AnyObject) {
        player.stop()
    }
    
    
    /// 再生中の曲が変更になったときに呼ばれる  
    func nowPlayingItemChanged(notification: NSNotification) {
        
        if let mediaItem = player.nowPlayingItem {
            updateSongInformationUI(mediaItem)
        }
        
    }
    
    deinit {
        // 再生中アイテム変更に対する監視をはずす
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.removeObserver(self, name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // ミュージックプレーヤー通知の無効化  
        player.endGeneratingPlaybackNotifications()
    }
    

}

関連

[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用) | nackpan Blog
[iOS][Swift]MPMediaQueryを使って曲を絞り込む | nackpan Blog
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(AVAudioPlayer使用) | nackpan Blog

[Swift]iPhoneを振動させる

iPhoneのバイブレーション機能を用いるには?

システムサウンドにバイブレーションさせるものがあるのでそれを用います。

System Sound Services Reference

• AudioToolboxをimportしたうえで、バイブレーション用サウンドを指定して鳴らすことで、振動させることができます。

import AudioToolbox
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)

バイブレーションのサンプル

Startボタンを押すと3秒間隔で振動し続けるサンプルを作成しました。(Stopボタンを押すと止まります)

Single View Applicationで開発。

Main.storyboardにStartボタンを配置
ss 2015-11-13 17.09.33

StartボタンとViewController.swift間をActionで結びつける

    @IBAction func pushStartOrStopBtn(sender: AnyObject) {

    }

Startボタンが押されると、Timerが起動します。
Timerは3秒間隔で、システムサウンドによるバイブレーションを実行しています。
また、ボタンは押されるごとに、”Start”と”Stop”の表記を切り替えています。
Stopボタンが押されると、Timerを無効にします。

ViewController.swift

//
//  ViewController.swift
//  Vibration Demo
//
//  Created by KUWAJIMA MITSURU on 2015/11/13.
//  Copyright © 2015年 nackpan. All rights reserved.
//

import UIKit
import AudioToolbox

class ViewController: UIViewController {
    
    var timer = NSTimer()
    var nowPlaying = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    @IBAction func pushStartOrStopBtn(sender: AnyObject) {
        let btn = sender as! UIButton
        if nowPlaying {
            pushStopBtn(btn)
        } else {
            pushStartBtn(btn)
            
        }
    }
    
    func pushStartBtn(btn: UIButton) {
        timer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: "vibrate:", userInfo: nil, repeats: true)
        timer.fire()
        
        
        btn.setTitle("Stop", forState: UIControlState.Normal)
        
        nowPlaying = true
        
    }

    
    func vibrate(timer: NSTimer) {
        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
    }
    
    func pushStopBtn(btn: UIButton) {
        
        timer.invalidate()
        
        btn.setTitle("Start", forState: UIControlState.Normal)
        
        nowPlaying = false
        
    }
}

[iOS]マイクへのアクセス許可を求めるダイアログを再度表示する

iOSアプリで、録音機能を使うときには最初にマイクへのアクセス許可を問うダイアログが表示されます。
ss2015-11-01

許可しない・OK どちらかを選ぶことになります。

選んだ後は、そのアプリで録音機能を使ってもダイアログは表示されません。

開発中に、はじめて実行した状態に戻して、「マイクへのアクセス許可ダイアログ」を表示する必要がありました。
アプリをiPhoneから削除して、再度ビルドし直して実行してみましたが、アクセス許可ダイアログは表示されません。
「許可しない・OK」の選んだ方の状態になっています。

困りました。調べてみるとStackOverFlowにそれに関する質問と回答がありました。
ios7 – Resetting iOS 7 microphone access permission – Stack Overflow
こちらの回答によると、
設定 > 一般 > リセット > 位置情報とプライバシーをリセット
で、「マイクへのアクセス許可」情報もリセットできます。

これで、録音をしようとした際に「マイクへのアクセス許可」ダイアログが表示されるようになりました。

ただ、すべてのアプリのプライバシー設定がリセットされるので、他のアプリも再度アクセス許可を出していく必要があります。

[iOS][Swift]録音する

iOSでAVAudioRecorderを使って、音声を録音する方法を紹介します。

AVAudioRecorder Class Reference

今回は
<図>
に示すサンプルを作成しました。
“Record”ボタンを押すと録音開始(それとともにボタンの表記が”Stop”に変わる)
“Stop”ボタンを押すと録音停止
“Play Recording”ボタンを押すと録音した音声の再生開始(それとともにボタンの表記が”Stop Playing”に変わる)
“Stop Playing”ボタンを押すと再生停止
となります。

録音機能の実装

必要なフレームワークをインポート
import AVFoundation

AVAudioRecorderとAVAudioPlayerを使用するのに必要なAVFoudationフレームワークをインポートします

レコーダーとプレイヤー

レコーダーとプレイヤーを保持するために、プロパティとして設定します
レコーダとしてAVAudioRecorder?型のプロパティを設定

    var audioRecorder: AVAudioRecorder?

オーディオセッションの設定

        /// 録音可能カテゴリに設定する
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
        } catch  {
            // エラー処理
            fatalError("カテゴリ設定失敗")
        }

        // sessionのアクティブ化
        do {
            try session.setActive(true)
        } catch {
            // audio session有効化失敗時の処理
            // (ここではエラーとして停止している)
            fatalError("session有効化失敗")
        }

レコーダーの設定

• 録音したファイルの保存先URLを設定します
• 録音時の音質やチャンネル数を設定します
• 準備した保存先URLと録音設定を元にレコーダーを作成します

    func setupAudioRecorder() {

        // 録音用URLを設定
        let dirURL = documentsDirectoryURL()
        let fileName = "recording.caf"
        let recordingsURL = dirURL.URLByAppendingPathComponent(fileName)

        // 録音設定
        let recordSettings: [String: AnyObject] =
        [AVEncoderAudioQualityKey: AVAudioQuality.Min.rawValue,
            AVEncoderBitRateKey: 16,
            AVNumberOfChannelsKey: 2,
            AVSampleRateKey: 44100.0]

        do {
            audioRecorder = try AVAudioRecorder(URL: recordingsURL, settings: recordSettings)
        } catch {
            audioRecorder = nil
        }

    }

    /// DocumentsのURLを取得
    func documentsDirectoryURL() -> NSURL {
        let urls = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask)

        if urls.isEmpty {
            //
            fatalError("URLs for directory are empty.")
        }

        return urls[0]
    }

録音開始・停止

                // 録音開始
                recorder.record()

[iOS]電話がかかってきたとき、ヘッドホンジャックが抜かれたときの対応

(2019年6月更新。Xcode 10 + Swift 5 に書き改めました)

オーディオ系アプリで、電話がかかってきたとき、ヘッドフォンジャックが抜かれたときの対応。

電話がかかってきたとき、ヘッドホンが抜かれたときには、音楽を再生していた場合対処する必要があります。
電話がかかってきたとき、ヘッドホンが抜かれたときを検知して対応するには、Notification Centerを使った通知の仕組みを使います。

NotificationCenter – Foundation | Apple Developer Documentation
Notification centerにイベントを監視(Observe)するよう登録します。イベントからNotirication Centerに通知(Post Notification)があると、Notification Centerは、対応するメソッドを呼び出します。
今回は、割り込み(AVAudioSession.interruptionNotification)とルートチェンジ(AVAudioSession.routeChangeNotification)を監視するように記します。

こちらのドキュメントにそって、実装します。
Responding to Audio Session Route Changes | Apple Developer Documentation
Responding to Audio Session Interruptions | Apple Developer Documentation
このドキュメントによると、AVPlayerの場合は、適切なタイミングでポーズ・再開が行われます。
それ以外のオーディオ再生方法では、適切なポーズ・再開は自動的には行われません。
ですので、ポーズや再開は自前で実装する必要があります。またUIを適切なものに更新することも必要です。
(AVAudioPlayerに関しては、最初にこの記事を書いた2015年においては、ヘッドホンが抜かれたときに自動的に再生が止まった記憶があります。しかし、2019年6月現在、音楽が一旦途切れたあと再生されます。)
(実験したところ、電話などの割り込みの際には、現在のところ音楽はとまります。)

以下は、SpeechSynthesizerで音声を出力して、そのさいヘッドフォンをぬくと再生を止めるという実験です。ヘッドホンを抜き差しすると、メッセージラベルに状況が表示されます。

メッセージラベル、Speakボタン、Pauseボタンを貼り付けます。

メッセージラベルとViewControllerをOutletで結びます。
@IBOutlet weak var messageLabel: UILabel!

SpeakボタンとViewControllerをActionで結びます。
@IBAction func speakBtnTapped(_ sender: Any) 

PauseボタンとViewControllerをActionで結びます。
@IBAction func pauseBtnTapped(_ sender: Any)

ViewController.swift


import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController {

    @IBOutlet weak var messageLabel: UILabel!
    
    let syntherizer = AVSpeechSynthesizer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        addAudioSessionObservers()
        
    }
    
    // 電話による割り込みと、オーディオルートの変化を監視します
    func addAudioSessionObservers() {
        
        AVAudioSession.sharedInstance()
        
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
        center.addObserver(self, selector: #selector(audioSessionRouteChanged(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
        
    }
    
    /// Interruption : 電話による割り込み
    @objc func handleInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
            let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                return
        }
        
        if type == .began {
            // interruptionが開始した時(電話がかかってきたなど)
        }
        else if type == .ended {
            // interruptionが終了した時の処理
            if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption Ended - playback should resume
                } else {
                    // Interruption Ended - playback should NOT resume
                }
            }
        }
    }
    
    /// Audio Session Route Change : ルートが変化した(ヘッドフォンが抜き差しされた)
    @objc func audioSessionRouteChanged(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
            let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
                return
        }
        
        DispatchQueue.main.async {
            self.messageLabel.text = self.routeChangeReasonDescription(reason: reason)
        }

        switch reason {
        case .newDeviceAvailable:
            let session = AVAudioSession.sharedInstance()
            for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                // ヘッドフォンがつながった
                
                break
            }
        case .oldDeviceUnavailable:
            if let previousRoute =
                userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
                for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                    // ヘッドフォンが外れた
                    
                    // このサンプルでは音声をpauseしています
                    syntherizer.pauseSpeaking(at: .immediate)
                    
                    break
                }
            }
        default: ()
        }
        
    }
    
    private func speak() {
        let voice = AVSpeechSynthesisVoice(language: "ja-JP")
        let utterance = AVSpeechUtterance(string: "おはようございます。今日もいい天気ですね。音声合成で読み上げています。文章をうまくよめていますか?")
        utterance.voice = voice
        //utterance.preUtteranceDelay = 5
        if syntherizer.isPaused {
            syntherizer.continueSpeaking()
        } else {
            syntherizer.speak(utterance)
        }
    }
    

    /// Speakボタンが押された
    @IBAction func speakBtnTapped(_ sender: Any) {
        // 音声出力を行う
        speak()
    }
    
    /// pauseボタンが押された
    @IBAction func pauseBtnTapped(_ sender: Any) {
        // ポーズする
        syntherizer.pauseSpeaking(at: .immediate)
    }
    
    /// 監視する必要がなくなった段階で、Observerを取り外します
    private func removeAudioSessionObservers() {
        let center = NotificationCenter.default
        
        // AVAudio Session
        center.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
        center.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)
        
    }
    
    /// route変更の理由表示用
    private func routeChangeReasonDescription(reason: AVAudioSession.RouteChangeReason) -> String {
        switch reason {
        case .unknown:
            return "unknown"
        case .newDeviceAvailable:
            return "newDeviceAvailable"
        case .oldDeviceUnavailable:
            return "oldDeviceUnailable"
        case .categoryChange:
            return "categoryChange"
        case .override:
            return "override"
        case .wakeFromSleep:
            return "wakeFromSleep"
        case .noSuitableRouteForCategory:
            return "noSuitableRouteForCategory"
        case .routeConfigurationChange:
            return "routeConfigurationChange"
        default:
            return "default"
        }
    }
    
}

[iOS][Swift]リモートコマンドイベントに対応する

(2019/02/17 更新 Xcode 10.1 + Swift 4.2)

たいていのオーディオ系アプリでは、イヤホンやロック画面、コントロールセンターからの再生や停止ができます。

iPhoneのイヤホンではセンターボタンの操作でオーディオの操作ができます。
ss2014-10-30-01
再生・一時停止:センターボタンを1回押す
次の区間へ:センターボタンをすばやく2回押す
前の区間へ:センターボタンをすばやく3回押す

といった操作ができます。

コントロールセンターでは、赤丸で囲った部分でオーディオアプリの再生制御ができます。
ss2015-09-25-01

iPhone用イヤホン操作などのイベントはリモートコマンドイベントとよばれています。
このリモートコマンドイベントを受け取るために、必要な実装を紹介します。

Appleのドキュメントを見てみます。
Handling External Player Events Notifications | Apple Developer Documentation
Remote Command Center Events | Apple Developer Documentation

以前の版(2013年)では、remoteControlReceivedWithEvent:を使った方法が紹介されていましたが、
2015年版では、MPRemoteCommandオブジェクトを使って、アクションハンドラを登録する方法が記されています。

Handling External Player Events Notificationsの記事に
“Your app must be the Now Playing app. An app does not receive remote control events until it begins playing audio. “
(アプリが再生中でなければなりません。アプリが、音声の再生を開始するまでリモートコントロールイベントを受け取りません)
とあります。アプリがオーディオ再生開始以降でないと、リモートコントロールイベント(リモートコマンドイベント)を受け取らないのでご注意ください。

* オーディオを使用せずにイヤホンからの操作でページをめくるアプリなどは製作できません

* オーディオを使用しているアプリでも、オーディオ再生が開始されたあとでないとイヤホンからの操作はできません

オーディオ再生に関しては以前の記事をご参照ください。
参考:[iOS][Swift]AVAudioPlayerを使う(リソースファイルを使う) – nackpan Blog

MPRemoteCommandにアクションハンドラを登録してリモートコマンドイベントを制御

MPRemoteCommandCenter Class Reference
MPRemoteCommand Class Reference

MPRemoteCommandCenterがもつリモートコマンドにアクションハンドラを登録します。
リモートコマンドには、
・togglePlayPauseCommand(イヤホンのセンターボタンを押した)
・playCommand(コントロールセンターのプレイボタンを押した)
・pauseCommand(コントロールセンターのポーズボタンを押した)
などがありますので、それぞれのコマンドにたいしてどのようなアクションを起こすかを記述します。

起動するといきなりサウンドが流れ、イヤホンやコントロールセンターから再生・停止ができるサンプルを作成しました。
Single View Appで作成。
File > Add Files to “プロジェクト” から、sound.mp3と名前をつけたオーディオファイルを追加。

ViewController.swift

import UIKit
import AVFoundation
import AVKit
import MediaPlayer


class ViewController: UIViewController {
    
    var audioPlayer:AVAudioPlayer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        addRemoteCommandEvent()
        
        setupPlayer()
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    
    func setupPlayer() {
        let fileName: String? = "sound"
        let fileExtension:String? = "mp3"
        if let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) {
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: url)
                audioPlayer?.prepareToPlay()
                audioPlayer?.play()
            } catch {
                // プレイヤー作成失敗
                // その場合は、プレイヤーをnilとする
                audioPlayer = nil
            }
            
        } else {
            // urlがnilなので再生できない
            fatalError("Url is nil.")
        }
    }
    
    
    
    // MARK: Remote Command Event
    func addRemoteCommandEvent() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(type(of: self).remoteTogglePlayPause(_:)))
        commandCenter.playCommand.addTarget(self, action: #selector(type(of: self).remotePlay(_:)))
        commandCenter.pauseCommand.addTarget(self, action: #selector(type(of: self).remotePause(_:)))
        commandCenter.nextTrackCommand.addTarget(self, action: #selector(type(of: self).remoteNextTrack(_:)))
        commandCenter.previousTrackCommand.addTarget(self, action: #selector(type(of: self).remotePrevTrack(_:)))
        
    }
    
    @objc func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
        // イヤホンのセンターボタンを押した時の処理
        print("イヤホンのセンターボタンを押した時の処理")
        // (今回は再生中なら停止、停止中なら再生をおこなっています)
        if let player = audioPlayer {
            if player.isPlaying {
                player.stop()
            } else {
                player.play()
            }
        }
    }
    
    @objc func remotePlay(_ event: MPRemoteCommandEvent) {
        // プレイボタンが押された時の処理
        print("プレイボタンが押された時の処理")
        // (今回は再生をおこなっています)
        if let player = audioPlayer {
            player.play()
        }
    }
    
    @objc func remotePause(_ event: MPRemoteCommandEvent) {
        // ポーズボタンが押された時の処理
        print("ポーズが押された時の処理")
        // (今回は停止をおこなっています)
        if let player = audioPlayer {
            player.stop()
        }
    }
    
    @objc func remoteNextTrack(_ event: MPRemoteCommandEvent) {
        // 「次へ」ボタンが押された時の処理
        // (略)
    }
    
    @objc func remotePrevTrack(_ event: MPRemoteCommandEvent) {
        // 「前へ」ボタンが押された時の処理
        // (略)
        
    }
}

AVPlayerViewControllerを使用した動画プレーヤーの場合

オーディオアプリの例では、リモートコマンドへアクションを登録する必要がありました。
AVPlayerViewControllerを使用した動画プレーヤーの場合では、リモートコマンド関連のコードを一切追加せずに基本のプレーヤーを設定した段階で、リモートからの操作は可能です。

画面中央のボタンを押すと動画が再生され、イヤホン・コントロールセンターから一時停止・再生が可能なサンプルを作成します。

AppleDocumentationを参考にします。

Building a Basic Playback App

Appleの記事ではサーバー上の動画を再生していますが、今回は簡便のためにプロジェクトに追加した動画を再生します。

Single View Appで作成。
File > Add Files to “プロジェクト” から、movie01.mp4と名前をつけた動画ファイルを追加。
ボタンをMain.storyboard上に配置し、ActionでViewController.swiftと結びつける。
そのさいのメソッド名をsetupPlayer(_ sender: Any)としています。

Main.storyboard

ViewController.swift

import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func setupPlayer(_ sender: Any) {
        guard let url = Bundle.main.url(forResource: "movie01", withExtension: "mp4") else {
            return
        }
        
        let player = AVPlayer(url: url)
        
        let controller = AVPlayerViewController()
        controller.player = player
        
        present(controller, animated: true) {
            player.play()
        }
    }
}

関連

[iOS]電話がかかってきたとき、ヘッドホンジャックが抜かれたときの対応 – nackpan Blog
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用) – nackpan Blog

[iOS][Swift]バックグラウンドに移行してもオーディオ再生を続ける

オーディオ再生中にバックグラウンドに移行しても再生を続けるには?

Targetsから
Capabilities > Background Modes をONにして、
Audio, AirPlay and Picture in Picture をチェックします
ss 2015-09-24 22.10.27
(この設定によって、Info.plistに”Required Background Modes”キーが書き加えられます)

この設定をした上で、ソースコード上でAVAudioSessionのカテゴリを設定します。
AVAudioSessionのカテゴリとは、消音スイッチが入っているときに音楽再生するか?や、録音可能か?などの設定を定めるものです。
AVAudioSessionプログラミングガイド(日本語・PDF)
バックグラウンド再生の場合には、 AVAudioSessionCategoryPlaybackにします。

        /// バックグラウンドでも再生できるカテゴリに設定する
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(AVAudioSessionCategoryPlayback)
        } catch  {
            // エラー処理
            fatalError("カテゴリ設定失敗")
        }

        // sessionのアクティブ化
        do {
            try session.setActive(true)
        } catch {
            // audio session有効化失敗時の処理
            // (ここではエラーとして停止している)
            fatalError("session有効化失敗")
        }

これで、音楽再生中にバックグラウンドに移行しても再生は続きます。

ただ、これで音楽を流し続けることはできるのですが、イヤホンやコントロールセンターからの操作を受け付けません。
そういった操作(リモートコントロールイベント)を受け付ける手順はこちらの記事をごらんください。
[iOS][Swift]リモートコントロールイベントに対応する | nackpan Blog

参考

Technical Q&A QA1668: Playing media while in the background using AV Foundation on iOS