😊

[macOS] AVAudioEngineの音声出力デバイス切り替えを検知する

2021/11/23に公開

結論

macOSでAVAudioSessionのrouteChangeNotificationは使えません。

error: 'AVAudioSession' is unavailable in macOS

代わりにAVAudioEngineConfigurationChangeを使ってください。

はじめに

以下の環境で動作検証しました。

  • Xcode 12.5 (12E262)
  • macOS Big Sur 11.5.2

実装例

以下は440 Hzのサイン波を再生するコードです。再生中に出力デバイスが切り替わっても途切れることなく再生が続きます。

import AVFoundation

class Engine {
  var engine = AVAudioEngine()
  var index = 0

  init() {
  setup()

    NotificationCenter.default.addObserver(
      self, selector: #selector(onConfigurationChange), name: .AVAudioEngineConfigurationChange,
      object: nil)
  }
  @objc func onConfigurationChange(notification: Notification) {
    print("Responding AVAudioEngineConfigurationChange")
    setup()
  }
  func setup() {
    let engine = AVAudioEngine()
    let outputFormat = engine.outputNode.inputFormat(forBus: 0)
    let sourceNode = AVAudioSourceNode(format: outputFormat) {
      _, _, frameCount, audioBufferList -> OSStatus in
      let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList)

      for i in 0..<Int(frameCount) {
        for buffer in ablPointer {
        let phase = Double(self.index % Int(outputFormat.sampleRate))
        let signal = sin(440.0 * phase / outputFormat.sampleRate * 2.0 * Double.pi)
          let frame: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)

          frame[i] = Float(signal)
          // frame[i] = Float.random(in: -1.0...1.0)
        }

        self.index += 1
      }

      return noErr
    }

    engine.attach(sourceNode)
    engine.connect(sourceNode, to: engine.mainMixerNode, format: outputFormat)
    engine.prepare()

    do {
      try engine.start()
    } catch {
      fatalError("Failed to start AVAudioEngine: \(error)")
    }

    self.engine = engine
    print("Running AVAudioEngine: \(outputFormat.sampleRate) Hz / \(outputFormat.channelCount) ch")
  }
}

let e = Engine()

// 音声のレンダリングは別スレッドで行われるため、メインスレッドは待機状態にする。
var wg = DispatchGroup()

print("Press Ctrl-C to quit ...")

wg.enter()
wg.wait()

試運転

上記のコードをmain.swiftとして保存し、次のコマンドを実行してください。

$ swift main.swift

以下はMacBookのスピーカー→USBオーディオインターフェース→MacBookのスピーカーと切り替えた場合の例です。

Running AVAudioEngine: 96000.0 Hz / 2 ch
Press Ctrl-C to quit ...
Responding AVAudioEngineConfigurationChange
Running AVAudioEngine: 44100.0 Hz / 2 ch
Responding AVAudioEngineConfigurationChange
Running AVAudioEngine: 96000.0 Hz / 2 ch

余談

NotificationCenterのaddObserverを実行するタイミングはいつでも構いません。AVAudioEngineのstartが実行される前でも後でも動作します。

ただし、addObserverの実行後にstartを実行すると出力デバイス切り替えのコールバックが呼ばれないことがあります。頻度は10回に1回ほどです。

Apple DeveloperのドキュメントにはaddObserverをいつ実行する必要があるのか記載がありません。addObserverの実行後にstartを実行することは想定していないのかもしれません。

確実に出力デバイス切り替えの通知を受け取るにはstartの実行後にaddObserverを実行するのがおすすめです。

さらに余談になりますが、AVAudioSessionのドキュメントにはAvailabilityとしてmacOS 11.0+の記載があります。Mac CatalystはAVAudioSessionに対応しているため、macOSの記述と間違えたものと思われます。

参考資料

Discussion