WWDC Quick Look 💓 By SwiftGGTeam
Enhance your app's audio experience with AirPods

Enhance your app's audio experience with AirPods

元の動画を見る

ハイライト

iOS 17 と macOS 14 では、AirPods の自動切り替え、ステムを押してミュート、空間オーディオの統合コストがほぼゼロになります。メディア App は Now Playing に登録するだけで自動切り替えに参加でき、CallKit App はステムによるミュートを自動的に利用でき、AVPlayer と AudioQueue App では空間オーディオがすぐに使えます。

主要内容

AirPods は世界で最も広く使われているワイヤレスイヤフォンで、毎日何億もの人が音楽、動画、ビデオ会議に使っています。iOS 17 と macOS 14 は、AirPods 体験をさらにシームレスにする 3 つの重要な改善をもたらします。

自動切り替え

macOS 14 から AirPods の自動切り替えに対応します。ユーザーが iPhone で音楽を聴いているときに Mac で会議通知を受け取り、Mac のロックを解除すると、AirPods が自動的に接続され、確認バナーが表示され、iPhone 側の音楽は自動的に一時停止します。(01:33

良い知らせは、すべての App Store App とサンドボックス外の App が追加対応なしで利用できることです。自動切り替えアルゴリズムは、Now Playing の登録状況と入力オーディオのアクティビティに基づいてルーティングを判断します。

ただし、体験を最適化するためのベストプラクティスがいくつかあります。(05:17

  • メディア/ストリーミング App は Now Playing に登録し、システムが適切に優先ルーティングできるようにする
  • 会議/ゲーム App は Now Playing に登録しない
  • 通知音には Audio Services API を使い、通知とメディアコンテンツを区別する
  • 会議 App は通話中だけマイクを開き、終了後すぐに閉じる
  • メディア App はユーザーが選んだデフォルトルートでオーディオを再生する
  • ユーザーが一時停止したあとに無音を再生しない。必要な場合も 2 秒以内に抑える

ステムを押してミュート

iOS 17 と macOS 14 は、AirPods のステムを押してミュート/ミュート解除する操作に対応します。ユーザーが押すと、システムはマイク状態のバナーを表示し、通知音を再生します。(02:26

iOS では、すべての CallKit App が追加コードなしでこの機能を自動的に利用できます。CallKit を使わないコミュニケーション App は、新しい AVAudioApplication API で対応します。(07:03

macOS での動作は少し異なります。App はミュートジェスチャーの通知を受け取ったあと、アップリンク音声を自分でミュートする必要があります。(09:47

空間オーディオ

Apple Music 登録者の 80% 以上が空間オーディオで聴いています。iOS 16 で導入されたパーソナライズされた空間オーディオは、iOS 17 でも対応範囲が広がります。(12:05

macOS 14 は AVPlayer と AVSampleBufferAudioRenderer の空間オーディオに対応します。iOS と tvOS ではさらに AURemoteIO と AudioQueue にも対応します。後者 2 つには API インターフェイスはなく、Now Playing に登録した App で自動的に有効になり、ユーザーはコントロールセンターで設定できます。

詳細

iOS でステムによるミュートに対応する

import AVFAudio
import Combine

class AudioManager: ObservableObject {
    @Published var isMuted = false
    private var cancellables = Set<AnyCancellable>()

    init() {
        let center = NotificationCenter.default
        center.publisher(for: AVAudioApplication.inputMuteStateChangeNotification)
            .sink { [weak self] notification in
                guard let self = self,
                      let userInfo = notification.userInfo,
                      let muteState = userInfo[AVAudioApplication.muteStateKey] as? Bool else {
                    return
                }
                self.isMuted = muteState
            }
            .store(in: &cancellables)
    }

    func toggleMute() {
        let instance = AVAudioApplication.shared
        instance.setInputMuted(!instance.isInputMuted)
    }
}

キーポイント:

  • AVAudioApplication は AVAudioSession ファミリーの新しいメンバーで、App レベルのオーディオ動作を管理する
  • inputMuteStateChangeNotification 通知はミュート状態が変化したときに発火する
  • muteStateKey は通知の userInfo から新しいミュート状態を取り出す
  • setInputMuted() は App 内でミュートを発生させるときに呼び出し、システム状態との同期を保つ
  • isInputMuted は現在のミュート状態を読み取る
  • 通知を購読することは、App がステムによるミュート機能にオプトインすることを意味する

macOS でステムによるミュートに対応する

import AVFAudio
import CoreAudio

class MacAudioManager {
    let instance = AVAudioApplication.shared

    func setupMuteHandler() {
        instance.setInputMuteStateChangeHandler { isMuted in
            // アップリンク音声をミュート
            self.muteUplinkAudio(isMuted)

            // UI 状態を更新
            DispatchQueue.main.async {
                self.updateMuteUI(isMuted)
            }

            // 正常に処理できたかを返す
            return true
        }
    }

    func muteUplinkAudio(_ isMuted: Bool) {
        var propertyAddress = AudioObjectPropertyAddress(
            mSelector: kAudioHardwarePropertyProcessInputMute,
            mScope: kAudioObjectPropertyScopeInput,
            mElement: kAudioObjectPropertyElementMain
        )

        var muteValue: UInt32 = isMuted ? 1 : 0
        let size = UInt32(MemoryLayout.size(ofValue: muteValue))

        AudioObjectSetPropertyData(
            kAudioObjectSystemObject,
            &propertyAddress,
            0,
            nil,
            size,
            &muteValue
        )
    }

    func updateMuteUI(_ isMuted: Bool) {
        // マイク状態インジケータを更新
    }
}

キーポイント:

  • macOS では setInputMuteStateChangeHandler が必須で、システムは通知だけを担当し、ミュートロジックは App が実装する
  • Handler は正常に処理できたかを示す Bool を返し、不適切なミュート操作を拒否できる
  • kAudioHardwarePropertyProcessInputMute は CoreAudio の新しいプロパティで、有効にすると IO を動かしたままプロセスのすべての入力オーディオをミュートする
  • Handler は UI 更新に使わず、UI 更新は inputMuteStateChangeNotification 通知で処理する
  • AudioObjectSetPropertyData はシステムレベルのオーディオオブジェクトを操作するため、エラーコードを正しく処理する必要がある

Now Playing 登録

メディア App が Now Playing に登録することは、自動切り替えと空間オーディオに参加する鍵です。

import MediaPlayer

func setupNowPlaying() {
    var nowPlayingInfo = [String: Any]()
    nowPlayingInfo[MPMediaItemPropertyTitle] = "Song Title"
    nowPlayingInfo[MPMediaItemPropertyArtist] = "Artist Name"
    nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0

    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo

    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.playCommand.addTarget { _ in
        // 再生を処理
        return .success
    }
    commandCenter.pauseCommand.addTarget { _ in
        // 一時停止を処理
        return .success
    }
}

キーポイント:

  • MPNowPlayingInfoCenter は現在再生中のコンテンツをシステムに登録する
  • 登録後、システムはユーザーが選択したデバイスへオーディオを正しくルーティングできる
  • 空間オーディオは、Now Playing に登録した App の AudioQueue と AURemoteIO で自動的に有効になる
  • 会議/ゲーム App は、自動切り替えアルゴリズムを妨げないよう Now Playing に登録しない

オーディオルート変更の監視

import AVFAudio

func setupRouteChangeMonitoring() {
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(handleRouteChange),
        name: AVAudioSession.routeChangeNotification,
        object: nil
    )
}

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

    let session = AVAudioSession.sharedInstance()
    let currentRoute = session.currentRoute

    switch reason {
    case .newDeviceAvailable:
        // AirPods が接続された
        let hasHeadphones = currentRoute.outputs.contains { port in
            port.portType == .headphones || port.portType == .bluetoothA2DP
        }
        if hasHeadphones {
            print("AirPods connected")
        }
    case .oldDeviceUnavailable:
        // AirPods が切断された
        print("AirPods disconnected")
    case .categoryChange:
        // オーディオカテゴリの変更
        break
    default:
        break
    }
}

キーポイント:

  • routeChangeNotification はオーディオルートが変化したときに発火する
  • AVAudioSessionRouteChangeReason 列挙型は変更理由を示す
  • currentRoute.outputs には現在のすべての出力ポートが含まれる
  • .bluetoothA2DP.headphones で AirPods の接続状態を検出できる
  • .oldDeviceUnavailable を正しく処理すると、AirPods 切断時に再生を一時停止できる

重要ポイント

1. スマート会議アシスタント 何を作るか: ビデオ会議 App に AirPods のステムによるミュートを統合し、UI でマイク状態をリアルタイム表示する。 なぜ価値があるか: AVAudioApplication によりミュート対応は数行のコードで済む一方、会議中の操作効率を大きく高められる。 始め方: inputMuteStateChangeNotification を購読し、@Published プロパティで SwiftUI のマイク状態インジケータを駆動し、ツールバーにミュートアイコンを表示する。

2. デバイス間のオーディオ状態同期 何を作るか: ポッドキャストまたは音楽 App で、AirPods が iPhone から Mac に切り替わったとき自動一時停止し、戻ってきたら再生位置を復元する。 なぜ価値があるか: 自動切り替えはシステム機能だが、App レベルの再生状態同期は開発者側で実装する必要がある。 始め方: routeChangeNotification を監視し、.oldDeviceUnavailable で現在の再生位置を保存し、.newDeviceAvailable で同じ AirPods か確認して再生を復元する。

3. 空間オーディオ対応ゲーム 何を作るか: プレイヤーの頭の向きに基づく空間オーディオをゲームに追加し、敵の位置を音で判断できるようにする。 なぜ価値があるか: 空間オーディオは AURemoteIO と AudioQueue App で自動的に有効になるため、マルチチャンネルコンテンツを提供するだけでよい。 始め方: Dolby Atmos など空間オーディオ対応の形式でゲーム効果音をエンコードし、AudioQueue で再生する。ヘッドトラッキングはシステムが自動処理する。

4. AirPods 接続を意識した UI 何を作るか: オーディオ App で AirPods の接続状態を検出し、接続時は空間オーディオのスイッチを表示し、切断時は最適な体験のためイヤフォン接続を促す。 なぜ価値があるか: オーディオデバイスによって体験は大きく異なるため、デバイスタイプに応じて UI を調整するとユーザー満足度を高められる。 始め方: AVAudioSession.sharedInstance().currentRoute.outputs を定期的に確認し、ポートタイプに応じてインターフェイス要素を動的に調整する。

関連セッション

コメント

GitHub Issues · utterances