🐕

(Swift)macOSに接続されているオーディオ入出力デバイスの物理的なフォーマットを取得する

2021/12/31に公開

はじめに

以下の環境で動作確認しました。

  • macOS 12.1 Monterey
  • Swift 5.5.1

物理的なフォーマットとは

macOSの内部ではハードウェアから受け取った音声信号を32 bit Floatに変換してから処理を行います。処理した結果は再び変換されてハードウェアに渡されます。このとき使用されるビット深度と数値型の組み合わせをフォーマットとよびます。

さて、音声の入出力を行うハードウェアにはデジタル信号とアナログ信号を相互変換する回路が搭載されています。入出力される信号のビット深度と数値型の組み合わせとしては16 bit Integerが一般的です。

このようなハードウェアの仕様もフォーマットとよばれます。フォーマットという擁護派曖昧なため、ハードウェアの仕様を意味する場合は物理的なフォーマットとよびます。

実装例

例として、デフォルトの出力デバイスの物理的なフォーマットを取得するプログラムを示します。

なお、kAudioHardwarePropertyDefaultOutputDevicekAudioDevicePropertyScopeOutputOutputの部分をInputに書き換えると入力デバイスの取得ができます。

import AudioToolbox

func getAudioFormatFlag(_ value: AudioFormatFlags) -> String {
  var flags = [String]()

  if (value & kAudioFormatFlagIsFloat) == kAudioFormatFlagIsFloat {
    flags.append("Float")
  }
  if (value & kAudioFormatFlagIsBigEndian) == kAudioFormatFlagIsBigEndian {
    flags.append("Big")
  }
  if (value & kAudioFormatFlagIsSignedInteger) == kAudioFormatFlagIsSignedInteger {
    flags.append("Integer")
  }
  if (value & kAudioFormatFlagIsAlignedHigh) == kAudioFormatFlagIsAlignedHigh {
    flags.append("Align-High")
  }
  if (value & kAudioFormatFlagIsNonInterleaved) == kAudioFormatFlagIsNonInterleaved {
    flags.append("Non-Interleaved")
  }
  if (value & kAudioFormatFlagIsNonMixable) == kAudioFormatFlagIsNonMixable {
    flags.append("Non-Mixable")
  }
  if (value & kAudioFormatFlagsAreAllClear) == kAudioFormatFlagsAreAllClear {
    flags.append("All-Clear")
  }

  let v = flags.joined(separator: " ")

  return v == "" ? "Undefined" : v
}

func getAudioFormat(_ value: AudioFormatID) -> String {
  switch value {
  case kAudioFormatLinearPCM:
    return "PCM"
  case kAudioFormatAC3:
    return "AC3"
  case kAudioFormat60958AC3:
    return "AC3"
  case kAudioFormatAppleIMA4:
    return "IMA4"
  case kAudioFormatMPEG4AAC:
    return "AAC"
  case kAudioFormatMPEG4CELP:
    return "CELP"
  case kAudioFormatMPEG4HVXC:
    return "HVXC"
  case kAudioFormatMPEG4TwinVQ:
    return "TwinVQ"
  case kAudioFormatMACE3:
    return "MACE3"
  case kAudioFormatMACE6:
    return "MACE6"
  case kAudioFormatULaw:
    return "ULaw"
  case kAudioFormatALaw:
    return "ALaw"
  case kAudioFormatQDesign:
    return "QDesign"
  case kAudioFormatQDesign2:
    return "QDesign2"
  case kAudioFormatQUALCOMM:
    return "QUALCOMM"
  case kAudioFormatMPEGLayer1:
    return "MPEG1"
  case kAudioFormatMPEGLayer2:
    return "MPEG2"
  case kAudioFormatMPEGLayer3:
    return "MPEG3"
  case kAudioFormatTimeCode:
    return "TimeCode"
  case kAudioFormatMIDIStream:
    return "MIDIStream"
  case kAudioFormatParameterValueStream:
    return "ParameterValueStream"
  case kAudioFormatAppleLossless:
    return "AppleLossless"
  case kAudioFormatMPEG4AAC_HE:
    return "AAC-HE"
  case kAudioFormatMPEG4AAC_LD:
    return "AAC-LD"
  case kAudioFormatMPEG4AAC_ELD:
    return "AAC-ELD"
  case kAudioFormatMPEG4AAC_ELD_SBR:
    return "AAC-ELD-SBR"
  case kAudioFormatMPEG4AAC_HE_V2:
    return "AAC-HE-V2"
  case kAudioFormatMPEG4AAC_Spatial:
    return "AAC-Spatial"
  case kAudioFormatAMR:
    return "AMR"
  case kAudioFormatAudible:
    return "Audible"
  case kAudioFormatiLBC:
    return "iLBC"
  case kAudioFormatDVIIntelIMA:
    return "DVIIntelIMA"
  case kAudioFormatMicrosoftGSM:
    return "MicrosoftGSM"
  case kAudioFormatAES3:
    return "AES3"
  case kAudioFormatAMR_WB:
    return "AMR-WB"
  case kAudioFormatEnhancedAC3:
    return "EnhancedAC3"
  case kAudioFormatMPEG4AAC_ELD_V2:
    return "AAC-ELD-V2"
  case kAudioFormatFLAC:
    return "FLAC"
  case kAudioFormatMPEGD_USAC:
    return "USAC"
  case kAudioFormatOpus:
    return "Opus"
  default:
    return "Undefined"
  }
}

func main() {
  var address = AudioObjectPropertyAddress(
    mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDefaultOutputDevice),
    mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal),
    mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMain)
  )

  var size = UInt32(MemoryLayout<AudioDeviceID>.size)
  var deviceID = AudioDeviceID()

  AudioObjectGetPropertyData(
    AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &deviceID)

  var deviceNameRef: CFString? = nil

  address.mSelector = kAudioObjectPropertyName
  size = UInt32(MemoryLayout<CFString?>.size)

  AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &deviceNameRef)

  guard let deviceName = deviceNameRef else {
    fatalError("failed to obtain output device name")
  }

  var description = AudioStreamBasicDescription()

  address.mSelector = kAudioStreamPropertyPhysicalFormat
  address.mScope = kAudioDevicePropertyScopeOutput

  size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)

  AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &description)

  let physicalFormat =
    "\(getAudioFormatFlag(description.mFormatFlags)) \(getAudioFormat(description.mFormatID))"

  print("Name: \(deviceName)")
  print("Channels: \(description.mChannelsPerFrame)")
  print("Bit Depth: \(description.mBitsPerChannel) bit \(physicalFormat)")
  print("Sample Rate: \(Int(description.mSampleRate)) Hz")
}

main()

試運転

上記のプログラムをmain.swiftとして保存してください。実行すると、オーディオMIDI設定に表示されている値と同じものが出力されます。

$ swift main.swift
Name: MacBook Pro Speakers
Channels: 2
Bit Depth: 32 bit Float PCM
Sample Rate: 96000 Hz

話はそれますが、つい最近、MACKIEのオーディオインターフェース搭載USBマイクを購入しました。機種名はEM-USBです。せっかくなので、それを接続したときの結果を示します。

$ swift main.swift
Name: EM-USB Microphone
Channels: 2
Bit Depth: 16 bit Integer PCM
Sample Rate: 48000 Hz

ハードウェアの仕様どおりの値が出力されています。

参考資料

残念ながら、Swiftのドキュメントは全く整備されていません。Apple Developerのサイトには定数が列挙されているだけです。それぞれの定数が何を意味しているのか一歳説明がありません。それどころか、記事を投稿した時点ではページのタイトルすら設定されていない状況です。

どのような定数が定義されているのか確認するにはObj-Cのヘッダーファイルを確認するのが最も確実です。通常は以下のディレクトリに格納されています。

  • /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/CoreAudio.framework/Versions/A/Headers
  • /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/CoreAudioTypes.framework/Versions/A/Headers

Discussion