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

Enhance your app's audio experience with AirPods

观看原视频

Highlight

iOS 17 和 macOS 14 让 AirPods 自动切换、按压静音和空间音频的集成对开发者几乎零成本:媒体 App 注册 Now Playing 即可参与自动切换,CallKit App 自动获得按压静音,空间音频对 AVPlayer 和 AudioQueue App 开箱即用。

核心内容

AirPods 是全球最受欢迎的无线耳机,每天有数亿人用它听音乐、看视频、开视频会议。iOS 17 和 macOS 14 带来了三项关键改进,让 AirPods 体验更无缝。

自动切换

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

空间音频

超过 80% 的 Apple Music 订阅者使用空间音频收听。iOS 16 引入的个性化空间音频在 iOS 17 中继续扩展支持。(12:05

macOS 14 支持 AVPlayer 和 AVSampleBufferAudioRenderer 的空间音频。iOS 和 tvOS 额外支持 AURemoteIO 和 AudioQueue。后两者没有 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 家族的新成员,管理应用级音频行为
  • 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 向系统注册当前播放内容
  • 注册后系统可以正确路由音频到用户选择的设备
  • 空间音频对 AudioQueue 和 AURemoteIO 自动为注册 Now Playing 的 App 启用
  • 会议/游戏 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,根据端口类型动态调整界面元素。

关联 Session

评论

GitHub Issues · utterances