[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]電話がかかってきたとき、ヘッドホンジャックが抜かれたときの対応

(2021年10月更新。Xcode 13 + 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: AVAudioSession.sharedInstance())
        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/11/29 更新 iOS 13, Xcode 11, Swift 5.1)

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

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(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remoteTogglePlayPause(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.playCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePlay(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.pauseCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePause(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.nextTrackCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remoteNextTrack(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.previousTrackCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePrevTrack(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })

        
    }
    
    func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
        // イヤホンのセンターボタンを押した時の処理
        print("イヤホンのセンターボタンを押した時の処理")
        // (今回は再生中なら停止、停止中なら再生をおこなっています)
        if let player = audioPlayer {
            if player.isPlaying {
                player.stop()
            } else {
                player.play()
            }
        }
    }
    
    func remotePlay(_ event: MPRemoteCommandEvent) {
        // プレイボタンが押された時の処理
        print("プレイボタンが押された時の処理")
        // (今回は再生をおこなっています)
        if let player = audioPlayer {
            player.play()
        }
    }
    
    func remotePause(_ event: MPRemoteCommandEvent) {
        // ポーズボタンが押された時の処理
        print("ポーズが押された時の処理")
        // (今回は停止をおこなっています)
        if let player = audioPlayer {
            player.stop()
        }
    }
    
    func remoteNextTrack(_ event: MPRemoteCommandEvent) {
        // 「次へ」ボタンが押された時の処理
        // (略)
    }
    
    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

[iOS][Swift]AVAudioPlayerを使う(複数の曲をあつかう)

AVAudioPlayerを使う例として
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(AVAudioPlayer使用) | nackpan Blog
[iOS][Swift]AVAudioPlayerを使う(再生速度の変更、複数のプレイヤー) | nackpan Blog
という記事を書きました。

今回は、AVAudioPlayerで複数のアイテムを扱います。
複数のアイテムを選択したのち、順次再生されるようにしたいと思います。


サンプルプロジェクトを作りました
(プロジェクト一式はGitHubにあります nackpan/AVAudioPlayerDemo3
ss2015-09-23 16 18 32

「選曲」ボタンで、複数の曲を選択し再生を開始します。
「再生・一時停止」ボタンで、再生と一時停止を切り替えます。
「<<」ボタンで前の曲に移ります。
「>>」ボタンで次の曲に移ります。
Message Labelに現在の曲情報が表示されます。

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

プレイヤークラスの作成

前回までのサンプルでは、ViewController.swiftに全部書いていました。
しかし、ViewController.swiftになにもかも書いていくと、どこが何の役割なのかがわかりづらくなってしまいます。
なので、プレイヤークラスを別に作成しました。
今回のサンプルでは、SimplePlayerクラスと名付けました。

複数のアイテムを扱う

プレイヤーが、MPMediaItemの配列をもち、currentIndexで現在のindexを示すこととします。
アイテム末尾到達、「>>」ボタンタップ、「<<」ボタンタップがおこなわれると、currentIndexを変更します。
別のアイテムになるたびにプレイヤーに新アイテムをセットします。

/// プレイヤーにitemをセットして更新
func updatePlayer() {
    let item = mediaItems[currentIndex]
    // MPMediaItemのassetURLからプレイヤーを作成する
    if let url: NSURL = item.assetURL {
        do {
            // itemのassetURLからプレイヤーを作成する
            audioPlayer = try AVAudioPlayer(contentsOfURL: url)
            
            // audioPlayerのdelegate先をselfに設定
            // (アイテム末尾に到達したときに呼ばれるaudioPlayerDidFinishPlaying()を受ける)
            audioPlayer?.delegate = self
            
        } catch  {
            // エラー発生してプレイヤー作成失敗
            audioPlayer = nil
            
            // 「再生中」ではない
            nowPlaying = false
            
            // 戻る
            return
            
        }
        
    } else {
        
        audioPlayer = nil

        // 「再生中」ではない
        nowPlaying = false
    }
}
urlがnilのアイテムもアイテム配列に含めている

今回のサンプルでは、mediaItemsにurlがnilのMPMediaItemも含めています。
その場合のアイテム再生時には、「urlがnilなので再生できない」旨のメッセージを表示して、そこで再生を停止しています。
(実際のアプリでは、事前にチェックしてurlがnilのものを取り除いておく、あるいは再生中にnilのものがあれば飛ばすなどの処理のほうがよさそうですが…)

アイテム末尾到達を知る

AVAudioPlayerDelegateのaudioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) メソッドで知ることができます。

プレイヤーをAVAudioPlayerDelegateプロトコルに準拠させます

class SimplePlayer: NSObject, AVAudioPlayerDelegate {

アイテム末尾に到達した際にaudioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool)が呼ばれるので、そこで必要な処理を行います。
このサンプルでは、
・範囲内であればindexを進めて次の曲の再生に移る
・すべてのアイテムの再生が終わったならそこで再生を終えて、indexを0に戻して最初のアイテム情報を表示する
処理を行っています。

    /// アイテム末尾に到達したときに呼ばれる
    func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
        
        
        // 最後の曲の場合は終了。そうでないなら次の曲へ
        if currentIndex >= mediaItems.count - 1{
            
            // 終了
            
            // indexを0に戻す
            currentIndex = 0
            
            // 新たなitemでプレイヤー作成
            updatePlayer()
            
            // ポーズする
            pause()
            
            
            return
            
        } else {
            
            // 次の曲へ。
            nextItem()
            
            
        }
    }

サンプルプロジェクトのGitHub
nackpan/AVAudioPlayerDemo3

関連

[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用) | nackpan Blog
[iOS][Swift]MPMediaQueryを使って曲を絞り込む | nackpan Blog
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(AVAudioPlayer使用) | nackpan Blog
[iOS][Swift]AVAudioPlayerを使う(再生速度の変更、複数のプレイヤー) | nackpan Blog