[Swift]イヤホン(ヘッドホン)が抜けて音声が外に漏れる事故がないように、接続状態をきちんと監視しよう

イヤホン抜けて大音量で音が流れるのって恥ずかしいっすよね・・・///
2023.04.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

CX事業本部の中安です。まいどです。

以前に「シンプルなオーディオプレイヤーを作ってみよう」という記事で タイトル通りオーディオプレイヤーアプリを作ってみました。

今回はその続きとなる内容になります。

ここで作成したオーディオプレイヤーアプリには "ある欠陥" がありました。 それはイヤホンで音楽を聞いていると、そのイヤホンが抜けてしまっても音が鳴り続けてしまうことでした。

たいていのオーディオプレイヤーアプリは、そういうときは音声再生が一時停止して、 周りに聞いている音楽が聞かれるという恥ずかしいことは起きないのですが、 それは自動的に止まるのではなく、制御を入れてやらなくてはいけないのです。

イベントを取る

実件方法はシンプルです。

AVFoundationをインポートし、AVAudioSession.routeChangeNotificationNotificationCenter 経由で監視してやるだけです。

実装例を以下のような感じです。

// ビューコントローラであれば viewDidLoad。その他クラスであれば init() とかに書く
NotificationCenter.default.addObserver(
    self,
    selector: #selector(didAudioSessionRouteChange(_:)),
    name: AVAudioSession.routeChangeNotification,
    object: nil
)

セレクタとして定義したメソッドも実装します。

@objc private func didAudioSessionRouteChange(_ notification: Notification) {
    // 後述します
)

今定義した didAudioSessionRouteChange(_:) は、イヤホンが「刺された時」も「抜かれた時」も同じ箇所が呼ばれます。 そのため、それを判別するためにはイベント通知の中身を見る必要があります。

@objc private func didAudioSessionRouteChange(_ notification: Notification) {
    guard
        let userInfo = notification.userInfo,
        let routeChangeReasonRawValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let routeChangeReason = AVAudioSession.RouteChangeReason(rawValue: routeChangeReasonRawValue)
    else {
        return
    }
    :

上記ソースコードのように RouteChangeReasonという列挙型のオブジェクトを userInfo から取得します。

そして、それをswitch文でケース分けしてやります。

        switch routeChangeReason {
        // イヤホンなどが刺された時
        case .newDeviceAvailable:

        // イヤホンなどが抜かれた時 
        case .oldDeviceUnavailable:

        default:
            break
        }

最終的にはAVAudioSessionPortDescriptionというオブジェクトに詳細な情報が入ってくるのですが、 イヤホンなどが刺された時と抜かれた時で、少しそのオブジェクトの取り方も変わるようなので、そのあたりもご紹介します。

イヤホンが刺された時のイベント

イヤホンが刺されたときは、AVAudioSession.sharedInstance().currentRoute.outputsという配列に情報が入ってきます。 「イヤホンが刺された」と書いていますが、それ以外にも接続する機器があるかもしれませんので、 portTypeというプロパティで接続された機器を判別することもできます。

取得できる機器はこちらを参照ください。

今回は刺されたことが取れればいいので、このようにしました。

guard let output = AVAudioSession.sharedInstance().currentRoute.outputs.first else { return }

print("\(output.portName)が刺された")

イヤホンが抜かれた時のイベント

イヤホンが抜かれた時は、userInfoから AVAudioSessionRouteDescriptionというオブジェクトを抜き出し、 その中から先ほど同様にAVAudioSessionPortDescriptionを取得することができます。

// userInfo は前述で guard let してるものとします
guard
    let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription,
    let output = previousRoute.outputs.first
else {
    return
}

print("\(output.portName)が抜かれた")

printを実行してるタイミングで音声再生を一時停止してやれば、イヤホンが抜けてしまった時に音が周りに聞こえてしまうことがなくなるでしょう。

おわりに

AVFoundationを使用した音声を扱うアプリであれば、接続機器に関するユーザのケアも必要になってくると思いますので、 ぜひご参考になさってください。

それではまたー