これは、CoreAudio.frameworkというmacOS向けのオーディオフレームワークについて書かれている本です。
CoreAudio.frameworkとは、ハードウェア抽象レイヤー(HAL)を介して、オーディオに関するハードウェアとのやりとりに利用するためのインターフェースを提供しているフレームワークです。このフレームワークはサービスとしてのCoreAudioの全ての機能を提供するフレームワークではなく、サービスとしてのCoreAudioが利用する低レイヤーな機能を提供するフレームワークであり、AVAudioToolBox.frameworkやAVFAudio.frameworkにある各機能は、CoreAudio.frameworkの各機能を呼び出す形で実装されています。また、このフレームワークはサービスとしてのCoreAudioに共通するデータ型の定義以外はmacOS向けのものとなっており、iOSやtvOS、watchOSでは利用できません。そして、このフレームワークの詳細をAppleのDeveloper Documentationから探そうとしても、ほとんどの項目に説明がありません。本書は、このフレームワークの中からオーディオ出力に必要となる領域に絞り、そのリファレンスとして機能するようになっています。
CoreAudio.frameworkは、接続されているデバイスやそのデバイスが持つストリームやコントロール、クロックなどをその概念毎にひとつのクラスとして扱う形を取っており、AudioObjectというクラスを継承関係の親として表現しています。
これらのクラスのうち、本書で取り扱うオーディオオブジェクトのクラスは以下の4種類です。
・AudioObject
・AudioSystemObject
・AudioDevice
・AudioStream
第1章と第2章で、オーディオオブジェクトの基本的な操作方法やデバイスを通した波形データの出力方法を、サンプルコードを交えながら提示しています。続く第3章、第4章、第5章及び第6章は、先の章で利用した各クラス群それぞれのプロパティーのリファレンスとなっています。これらを活用することにより、CoreAudio.frameworkを利用した、よりローレベルなオーディオプログラミングに取り組むことができるようになるでしょう。なお、本書内にあるサンプルコードはC++言語で記述されています。macOSでの開発はSwiftやObjective-C、Objective-C++を使うことが多いですが、CoreAudio.frameworkはHALを含むローレベルなAPIとなっているため、iOSとの共用部分を除いて、C言語で記述されています。そのため、C言語に近く、メモリー確保や開放などのサンプルには不要なコードを極力なくすため、C++言語を使っています。
本書を執筆するにあたり利用した環境は、以下のようになっています。
・Mac mini (M1, 2020)
・macOS Big Sur 11.6.5 (20G527)
・Xcode Version 13.2.1 (13C100)
本書は以下のような方々を対象読者として想定しています。
・macOSのローレベルオーディオプログラミングに興味がある方
・ゲーム開発等、よりハードウェアに近い領域のオーディオプログラミングが必要な方
・Appleのオンラインドキュメントに何も書かれていないフレームワークに興味がある方
本書に記載されている内容は、情報提供のみを目的としています。したがって、本書の内容をもとにした実装においては、ご自身の責任と判断によって行ってください。本書の内容をもとにした実装の結果について、著者はいかなる責任も負いません。
本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。
オーディオオブジェクト及びそのサブクラスのインスタンスに対して、何かしらの操作をしたい場合、そのインスタンスに紐付けられたAudioObjectID型のハンドルを介して操作します。概念上、全てのクラスはオーディオオブジェクトのサブクラスとなっているため、どのクラスのインスタンスであったとしても、AudioObjectID型の値として扱うことが可能となっています。
AudioObjectID型を介して情報を操作するために、AudioObjectGetPropertyData()という関数が、CoreAudio.frameworkには用意されています。
OSStatus
AudioObjectGetPropertyData(
AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32* ioDataSize,
void* outData);
この関数の戻り値の型はOSStatusですが、CoreAudio.frameworkに定義されている関数の戻り値はほとんどOSStatus型となっており、関数の失敗要因のエラーコードを返します。プロダクトコードではこの戻り値を見た上で適切なエラー処理をする必要がありますが、本書でそれを行うと、例示するコードが煩雑になってしまいます。そのため、他書の例に漏れず、必要な場合以外は戻り値の確認は行いません。
この関数が要求する引数は、それぞれ次のようになっています。
仮引数名 | 概要 |
inObjectID | 対象のオーディオオブジェクト |
inAddress | プロパティーアドレス。対象のプロパティーを指定するための情報 |
inQualifierDataSize | inQualifierDataのバッファサイズ |
inQualifierData | inAddressだけではプロパティーを特定できない場合の追加情報 |
ioDataSize | 入力時はoutDataのバッファサイズで、関数から戻ってきたときは実際に使用されたバッファのサイズ |
outData | 要求するプロパティーの実データ |
第二引数のプロパティーアドレスは、指定したオーディオオブジェクトのどのプロパティーに対して操作したいかを表します。
AudioObjectPropertyAddressは、以下のような要素を持つ構造体として定義されています。
フィールド名 | フィールドの型 | 概要 |
mSelector | AudioObjectPropertySelector | オーディオオブジェクトに関する特定の情報を識別するためのFourCC形式のデータで、プロパティーの一般的な分類を示すセレクター |
mScope | AudioObjectPropertyScope | mSelectorで指定したプロパティーを検索するための範囲となるスコープ |
mElement | AudioPropertyElement | mSelector、mScopeで指定したプロパティーから何番目の要素を取得するかを示すインデックス |
セレクターの型であるAudioObjectPropertySelectorは、その実際の型としてはUInt32で、各クラスが所有しているプロパティーに対応する数値として定義されています。
たとえば、オーディオオブジェクトクラスのセレクターには、その名前を取得するための kAudioObjectPropertyNameや、オーディオオブジェクトが持っているオーディオオブジェクトの一覧を取得するためのkAudioObjectPropertyOwnedObjectsなどがあります。
スコープの型であるAudioObjectPropertyScopeもUInt32の別名で、以下の4種類の識別子が定義されています。全てのプロパティーは少なくともGlobalスコープに属しており、その他はプロパティー毎に属し方が異なります。
Scope名 | 概要 |
kAudioObjectPropertyScopeGlobal | オブジェクト全体を示すスコープ |
kAudioObjectPropertyScopeInput | オブジェクトの入力を示すスコープ |
kAudioObjectPropertyScopeOutput | オブジェクトの出力を示すスコープ |
kAudioObjectPropertyScopePlayThrough | オブジェクトのプレイスルーを示すスコープ |
最後、エレメントを表すAudioPropertyElement型はUInt32の別名として定義されており、実態としては、配列のインデックスのようなものです。複数のエレメントがある場合にその何番目のエレメントのプロパティーを取得したいのかを指定するためにあります。なお、最初の要素を示す0だけは別名がkAudioObjectPropertyElementMainとして定義されており、全てのプロパティーは少なくともこのメインエレメントを有しています。
では、実際にプロパティーのデータを取得してみましょう。ここでは例として、デフォルトの出力デバイスを表すAudioObjectIDを取得してみます。
システムに存在する全てのオーディオオブジェクトのルートとなるオブジェクトは、システムオブジェクトです。このシステムオブジェクトを表すAudioObjectIDとして、kAudioObjectSystemObjectが定義されています。このシステムオブジェクトは、デフォルトの出力デバイスを表すオーディオオブジェクトをプロパティーとして持っているので、システムオブジェクトからデフォルトの出力デバイスのオーディオオブジェクトを取得します。
// (1) ヘッダをインクルード
#include <CoreFoundation/CoreFoundation.h>
#include <CoreAudio/CoreAudio.h>
#include <iostream>
int main()
{
// (2) 取得したいプロパティーのプロパティーアドレスを設定
auto address = AudioObjectPropertyAddress
{
.mSelector = kAudioHardwarePropertyDefaultOutputDevice,
.mScope = kAudioObjectPropertyScopeGlobal,
.mElement = kAudioObjectPropertyElementMain,
};
AudioObjectID id = kAudioObjectUnknown; // (3) 取得するAudioObjectIDの受け皿を用意
UInt32 size = sizeof(id); //(4) 取得するデータサイズを設定
// (5) AudioObjectIDを取得する
auto result = AudioObjectGetPropertyData(
kAudioObjectSystemObject,
&address,
0,
nullptr,
&size,
&id);
if (result == kAudioHardwareNoError)
{
std::cout << "device id: " << id << std::endl;
}
else
{
std::cerr << "error: " << result << std::endl;
}
return result;
}
最初に、(1)で利用するヘッダをインクルードします。CoreAudio.frameworkを利用する場合、インクルードすべきヘッダは以下のふたつです。
・CoreFoundation/CoreFoundation.h
・CoreAudio/CoreAudio.h
ヘッダのインクルードをすることで宣言、定義されている型や関数が利用できるようになったところで、(2)のようにデフォルト出力デバイスを取得するためのプロパティーアドレスを作ります。システムオブジェクトのセレクターのスコープは全てグローバルで、エレメントに関してもデフォルト出力デバイスはひとつしかありませんので、メインエレメントを指定します。
そして、(3)で要求するプロパティーの受け皿としてAudioObjectIDの変数を用意し、(4)でそのサイズもUInt32型の変数として用意します。
(5)実際にAudioObjectGetPropertyData()を呼び出し、成功すると(3)で用意した変数にデフォルト出力デバイスのAudioObjectIDが入っています。
取得対象のプロパティーのデータサイズが不変である場合は、先のコード例のように、直接AudioObjectGetPropertyData()関数を呼び出すこともできますが、プロパティーによってはデータサイズが可変の場合もあります。たとえば、システムに接続されているデバイスの一覧を取得したい場合、データサイズはデバイス数に依存するため、一意には定まりません。そういうとき、まずデータサイズだけを取得してからプロパティーのデータを取得する手法を取る必要があります。このデータサイズを取得する専用の関数として、AudioObjectGetPropertyDataSize()が定義されています。
OSStatus
AudioObjectGetPropertyDataSize(
AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32* outDataSize);
仮引数名 | 概要 |
inObjectID | 対象のオーディオオブジェクト |
inAddress | プロパティーアドレス |
inQualifierDataSize | inQualifierDataのバッファサイズ |
inQualifierData | inAddressだけではプロパティーを特定できない場合の追加情報 |
outDataSize | プロパティーのデータサイズ |
では、実際にシステムに接続されている全てのオーディオデバイスのオーディオオブジェクトを取得してみましょう。
// (1) プロパティーアドレスを設定
auto address = AudioObjectPropertyAddress
{
.mSelector = kAudioHardwarePropertyDevices,
.mScope = kAudioObjectPropertyScopeGlobal,
.mElement = kAudioObjectPropertyElementMain,
};
// (2) データサイズを取得
UInt32 size = 0;
AudioObjectGetPropertyDataSize(
kAudioObjectSystemObject,
&address,
0,
nullptr,
&size);
// (3) オーディオデバイスのリストを格納するメモリーを確保して
auto device_list = std::vector<AudioObjectID>(size / sizeof(AudioObjectID));
// (4) オーディオデバイスのリストを取得する
AudioObjectGetPropertyData(
kAudioObjectSystemObject,
&address,
0,
nullptr,
&size,
&device_list[0]);
for (auto device_id : device_list)
{
std::cout << "device id: " << device_id << std::endl;
}
基本的な流れは先程と同じですが、このプロパティーはデータサイズがシステムの状態によって異なりますので、プロパティーのデータを取得する前に、(2)でデータサイズを確定させています。