銀河鉄道

Swift|Headphone Unplug Detectionと自動停止のしくみ|イヤホン抜け検知

サムネイル
[Swift]Headphone unplugStay quiet.
急な音漏れを
防ごう

イヤホンをはずしたことが、どうしてわかるの?

iPhoneは、イヤホンが抜けた瞬間をOSが検知してアプリに知らせてくれる。
その合図を受けて止める仕組みが、今回のテーマ。

iOSが「イヤホン抜けた!」をどうやって知らせるか

iPhoneでは、イヤホンが抜けたりBluetooth接続が切れたりすると、
iOSが自動でオーディオ経路の変化を検知してアプリに通知を送る

この仕組みを使えば

自分のアプリで「スピーカーから急に音が鳴る事故」を防げる

イヤホン抜けの通知を受け取るコード

import AVFoundation

final class AudioSessionManager {
    private let nc = NotificationCenter.default
    var onHeadphoneUnplugged: (() -> Void)?

    func activate() throws {
        let s = AVAudioSession.sharedInstance()
        try s.setCategory(.playback, options: [.mixWithOthers])
        try s.setActive(true)
        observeRouteChange()
    }

    private func observeRouteChange() {
        nc.addObserver(forName: AVAudioSession.routeChangeNotification,
                       object: nil, queue: .main) { [weak self] note in
            guard let self,
                  let info = note.userInfo,
                  let reasonRaw = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
                  let reason = AVAudioSession.RouteChangeReason(rawValue: reasonRaw)
            else { return }

            // 旧ルート(抜けたデバイスを知る)
            let prev = info[AVAudioSessionRouteChangePreviousRouteKey]
                        as? AVAudioSessionRouteDescription

            if reason == .oldDeviceUnavailable,
               let outputs = prev?.outputs {
                let wasHeadphones = outputs.contains { out in
                    out.portType == .headphones ||
                    out.portType == .bluetoothA2DP ||
                    out.portType == .bluetoothHFP ||
                    out.portType == .bluetoothLE
                }
                if wasHeadphones {
                    onHeadphoneUnplugged?() // ここで停止/フェードダウンなどを行う
                }
            }
        }
    }
}

呼び出し側の処理(フェード停止の例)

// LocalAudioEngine.swift 側
engineSession.onHeadphoneUnplugged = { [weak self] in
    self?.fade(to: 0.0, duration: 0.4) { [weak self] in
        self?.stop()
        // 必要ならUIへ「イヤホンが外れたため停止しました」を通知
    }
}

このコードは絶対に必要

このコード(AVAudioSession.routeChangeNotification の購読)を書かない限り、アプリは何も気づかない


iOSが「イヤホン抜けた〜!」って通知を投げてはくれるけど、アプリが受け取る“仕組み”を組み込んでおかなければ無意味になる

レイヤー役割やってくれる?
iOS(システム)イヤホン抜けを検知して通知を発行する自動でやってくれる
アプリ通知を「購読」して中身を見て、止める処理を書く自分で書かないと動かない

受け取らなければ無意味

だから注意点

  1. NotificationCenter の購読登録(addObserver) がないと、通知をキャッチできない
  2. setActive(true)AVAudioSession を有効化してないと、そもそも通知が来ないこともある
  3. フェード処理や停止処理 もアプリ側の実装に任されてる(自動停止はしてくれない)

一言で言うと

iOSは「知らせるだけ」。
止める・フェードするのはアプリの責任。

もういちど、流れを整理

仕組みの流れ

  1. 物理的な変化が起こる
    イヤホンが抜ける/Bluetoothが切れる。
  2. Core Audioが経路変更を検知
    出力ルートから「ヘッドフォン」が消える。
  3. AVAudioSessionが通知を発行
    AVAudioSession.routeChangeNotificationNotificationCenter 経由で送られる。
  4. アプリが通知を受け取る
    userInfo から「理由(Reason)」と「直前のルート(PreviousRoute)」を確認し、
    「イヤホンが使われていたなら停止やフェードを実行」。

つまり、アプリが“定期的にチェックする必要はない”。
OSが変化を検知してイベントを投げてくれる仕組み。

理由コード(抜粋)

Reason意味
.oldDeviceUnavailable前の出力が消えた(イヤホンが抜けた/BT切断)
.newDeviceAvailable新しい出力が追加された(BT接続など)
.categoryChangeセッションカテゴリ変更
.routeConfigurationChange経路構成の変更(複合的)

実装のコツ

  • queue: .main でUI操作やフェードを安全に処理
  • 多重発火を防ぐフラグを持つ(連続通知が来る機種がある)
  • AirPlayは portType == .airPlay なので別扱い
  • 実運用では AVAudioSession.sharedInstance().setActive(true) を最初に呼ぶと安定

1行まとめ

イヤホンが抜けたら、OSが自動で「経路変わったで」と知らせる。アプリはそれを受けて止めるだけ。

Vocabulary

EnglishJapanese
route change notification経路変化通知
NotificationCenter通知センター
userInfo通知情報ディクショナリ
previous route直前のオーディオ経路
oldDeviceUnavailable旧デバイス消失
AirPlayエアプレイ
observerオブザーバ(購読者)

Silence is golden—especially when your app knows when to stay quiet.


著者

author
月うさぎ

編集後記:
この記事の内容がベストではないかもしれません。