XT-Audio事始め


Something went wrong

はじめに

Audio I/O のプログラムは枯れたライブラリが揃っている反面、Windows Vistaで導入されたWASAPI以降はリアルタイム向けの新しいAPIは登場せず、今もPortAudioをはじめとしたC言語スタイルなどの古い設計のライブラリが目立ちます。

そうした中、C++11を利用したXT-Audioはアプリケーションをモダンな設計にするためのよいきっかけになりそうでしたが、ライセンスがLGPLだったため、しばらく様子見をしていました。
https://sjoerdvankreel.github.io/xt-audio/

それが、v1.8からMITに変わり、ライセンスとしては導入しやすくなりました。XT-Audioを使っている記事はQiitaなどでも見つからなかったため、WindowsプラットフォームでのXT-Audioの使い勝手を紹介したいと思います。

WindowsのオーディオAPIとAudio Engine(旧Kernel Mode Audio Mixer)

XT-Audioを触る前に、Audio I/O ライブラリ全般の利用にあたって言えることですが、各ライブラリが使用するオーディオAPIを把握しておくとより理解が深まります。

まず、Windowsで最も古いオーディオAPIは、Windows 3.1で登場したMME(MultiMedia eXtensions)です。しかし、このMMEは動作が遅いため、後にゲームでの利用を目的としたDirect Sound(Directx8以降はDirectX Audio)が登場しました。今ならXAudio2が使われていると思いますが、私が学生時代の頃はこのDirect Soundのお世話になったものです。

しかし、Direct Soundであってもレイテンシーは発生します。その理由となるのが、Windowsのカーネルミキサー(Kernel Mode Audio Mixer)を通す必要があるためです。また、このカーネルミキサーを通すことによって、音質の劣化も発生します。これは同じカーネルミキサーを通しているMMEも同様です。Windowsでは様々なサンプリングレートの発生音をサンプルレートコンバージョンによって統一する必要がありますので、一般的なWindowsの使い方としてはなくてはならないものの、音のレイテンシーや品質への影響はどうしても避けられません。

そこで、ドイツのSteinbergというメーカーがカーネルミキサーを通さないASIO(Audio Stream Input Output)というオーディオデバイス用のAPIの規格を作りました。Steinbergはヤマハの子会社で、Cubaseのソフトも開発も手掛けています。このASIOですが、SDKも公開されており、他のメーカーを含めた様々なオーディオインターフェースを低遅延で利用することができます。カーネルミキサーを通さないので、音質の劣化を避けることができます。しかし、良いことばかりではなく、ハードウェアがASIOの規格に対応していないと利用することができません。
https://new.steinberg.net/developers/

Windows Vistaになって、新しいオーディオAPIであるWASAPI(Windows Audio Session API)が登場しました。このWASAPIですが、2つのモードがあり、従来のカーネルミキサー(VistaになってAudio Engineに変更)を通す共有モードとASIOのようなAudio Engineを通さない排他モードが利用できます。この排他モードを利用することで、ASIOのようなハードウェア側の対応なしに低遅延と音質の劣化が避けられるようになります。なお、従来のMMEやDirect Soundについては、WASAPIの共有モードで動作させることで、互換性が維持されています。

商用のオーディオライブラリを利用せずにゲームを開発する場合は、XAudio2も選択肢の1つですが、このAPIもAudio Engineを経由します。XAudio2には排他モードはなく、Audio I/O ライブラリにXAudio2が採用されている事例を見たこともありませんので、今回の選定ではXAudio2は除外します。
https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/windows-audio-architecture

結論としては、低遅延と音源の品質を維持したい場合は、ASIOもしくは排他モードのWASAPIを使用することになります。この2つの遅延の違いについては、ほとんど性能差はないと捉えて問題ないかと思います。ただ、規格はASIOのほうが古く(枯れている)、多くのDAWなどでの実績もあります。しかし、メーカーのオーディオドライバーによっては、ASIO SDK内でクラッシュしたり、不正なシステム時間が返ってくるなどの問題もあり、必ずしもASIOがよいというわけでもありません。

知名度の高いAudio I/O ライブラリ

ここでXT-Audio以外にどのようなライブラリがあるかを簡単に紹介します。先ほどのオーディオAPIの説明から、Audio I/O ライブラリはASIOもしくはWASAPIをサポートしているものが望ましいということが分かるかと思います。ただ、基本的にはWASAPIはサポートされているものがほとんどですので、ASIOが対応しているかどうかの違いで見ていくのがよいかと思います。この中で、設計としてはlibsoundioがよい評判を聞きますが、残念ながらASIOには対応していません。XT-Audioのライブラリを試してみようと思った動機は、ASIO対応とモダンな設計を両立したライブラリを探していたのが発端になります。

PortAudio
ASIOに対応しているAudio I/O ライブラリは「PortAudio」が有名です。Windows/Mac/Linuxとクロスプラットフォームで動作します。C言語スタイルであり、設計も古いものですが、枯れたライブラリとしての安心感はあります。C言語ということで、Unityへの組み込みもしやすいほうかと思います。公式サイトでは2016年のv19.6.0で止まっていましたが、Githubの更新は続いており、v19.7.0がちょうど今月(April 6, 2021)リリースされました。今も更新が続いているライブラリであることも、利点の1つかと思います。
http://www.portaudio.com/

RtAudio
PortAudioとよく比較されるのが「RtAudio」です。このライブラリもASIOをサポートしており、PortAudioと同じプラットフォームで動作します。また、PortAudioと異なるのがC++で実装されている点です。ただ、更新頻度は低いようです。最新版は「17 April 2019」でしたが、公式サイトからはダウンロードできず。。。ただ、Githubからはダウンロードは可能です。また、PortAudioよりはコードはシンプルに記述できます。
https://www.music.mcgill.ca/~gary/rtaudio/

libsoundio
ここ数年で見かけるようになったと感じるライブラリで、こちらもクロスプラットフォーム対応ですが、LinuxではPulseAudioなどもサポートしています。ただし、ASIOはサポートされていません。
http://libsound.io/

libsoundioとPortAudioやRtAudioとの比較は、libsoundioの公式サイトで利点をそれぞれ上げています。
https://github.com/andrewrk/libsoundio/wiki/libsoundio-vs-PortAudio
https://github.com/andrewrk/libsoundio/wiki/libsoundio-vs-RtAudio

XT-Audioの導入手順(Windows)

XT-AudioはGihubに公開されていますので、ここからダウンロードします。今回使用するバージョンは「v1.9」です。
https://github.com/sjoerdvankreel/xt-audio

次にビルドで使用するツールは下記のとおりです。

  • Visual Studio 2019
  • Java SDK 11
  • .NET SDK 4.8
  • .NET Core SDK 3.1
  • CMake 3.15
  • Maven 3.3.3
  • Doxygen 1.8.10
  • Sandcastle (SHFB) 2020.3.6.0

※ コードにDoxygen形式のコメントがないため、現状ではほとんど参考にできません。

次に、ASIOを利用するにあたって、「ASIO SDK」と「AsmJit」が必要になるので、それぞれ事前にダウンロードしておきます。

ASIO SDK

ASIO SDKは以下のサイトから入手できます。
https://new.steinberg.net/developers/

XT-Audioではソースだけあれば問題ありませんが、もし他の目的で自前でビルドしたい場合は、以下のワークスペースファイル(dsw)をVisual Studioで開きます。dswの拡張子はもはや懐かしいくらいですね。

asio/asio.dsw

なお、Visual Studio 2019だとデバッグ版のビルドでエラーになるので、プロジェクトのプロパティを下図のように修正します。他には、デフォルトだと「x86」しかソリューション構成がないため、「x64」が必要な場合は追加しておく必要があります。

AsmJit

AsmJitはマシンコードを生成するライブラリです。以下のサイトからダウンロードできます。AsmJitはzlibライセンスです。
https://github.com/asmjit/asmjit

CMakeLists.txtが同梱されていますので、自前でビルドしたい場合は、CMakeを使ってプロジェクトを生成・ビルドを行います。

XT-Audioのビルド

まず、以下のバッチを実行します。[ENABLE_DSOUND]などの定義を有効にする場合は「1」,無効にする場合は「0」を設定します。今回はWASAPIとASIOだけ使用したかったので、[ENABLE_DSOUND]は「0」にしています。パス指定には円マークではスラッシュを使用します。また、msbuildを使用しますので「Developer Command Prompt for VS 2019」からバッチを実行します。

build/build.bat [ENABLE_DSOUND] [ENABLE_WASAPI] [ENABLE_ASIO] [asiosdkへのパス] [/asmjit/srcへのパス]
[xt-audioへのパス]\dist\cpp\sample\x64\Release\xt-sample.exe

コードについて

これで実行できるようになりましたので、次にサンプルの中身を見ていきたいと思います。

Simple.cpp

デバイスを列挙するサンプルです。まず、各サンプルでも最初に実行するのはInit関数です。DoxygenにAPIの説明がないので、いきなり困りましたが、第1引数のidはnullptrもしくはからの文字列の場合は"XT-Audio"が設定されます。第2引数のvoid*はHWNDを指定するもので、nullptrの場合はライブラリ内でCreateWindowでウィンドウを生成します。

  std::unique_ptr<Xt::Platform> platform = Xt::Audio::Init("", nullptr);

次にInit()からGetSystems()を呼び出すことで有効なオーディオAPIをSystemとして受け取れます。今回はビルド時点でWASAPIとASIOを有効にしていましたので、この2つがSystemとして返されます。

  platform->GetSystems()

先ほど取得したSystemをPlatform::GetService()に渡すことで、デバイス情報を管理するServiceが取得できます。

    std::unique_ptr<Xt::Service> service = platform->GetService(system);

Serviceには、以下のコードで有効なデバイスを全て列挙くすることができます。WASAPIの場合は、Shared/Exclusive/Loopbackがそれぞれ別のデバイスとして取得できます。

    std::unique_ptr<Xt::DeviceList> list = service->OpenDeviceList(Xt::EnumFlagsAll);
    for(int32_t d = 0; d < list->GetCount(); d++)
    {
      std::string id = list->GetId(d);
      std::cout << system << ": " << list->GetName(id) << "\n";
    }

PrintDetailed.cpp

詳細情報を出力するサンプルです。まず、GetVersion()でSDKのバージョンが取得できます。

次にセットアップで指定可能な3種類のタイプ(ProAudio, SystemAudio, ConsumerAudio)の状態を確認します。WindowsだとProAudioはASIO、SystemAudioはWASAPI、ConsumerAudioはDirectSoundに該当します。

    Xt::Version version = Xt::Audio::GetVersion();
    std::cout << "Version: " << version.major << "." << version.minor << "\n";    
    Xt::System pro = platform->SetupToSystem(Xt::Setup::ProAudio);
    std::cout << "Pro Audio: " << pro << " (" << (platform->GetService(pro) != nullptr) << ")\n";
    Xt::System system = platform->SetupToSystem(Xt::Setup::SystemAudio);
    std::cout << "System Audio: " << system << " (" << (platform->GetService(system) != nullptr) << ")\n";
    Xt::System consumer = platform->SetupToSystem(Xt::Setup::ConsumerAudio);
    std::cout << "Consumer Audio: " << consumer << " (" << (platform->GetService(consumer) != nullptr) << ")\n";

このあとはサービスと各デバイスの情報を取得していますが、取得できる情報は以下のようなものがあります(全てではありません)。

  • サービスで得られる情報
    • オーディオAPI
    • APIの能力
    • 入力用デバイス数
    • 出力用デバイス数
    • デフォルトの入出力デバイス
  • デバイスで得られる情報
    • デバイス名
    • 性能(入力用 or 出力用、WASAPIの排他モードのように直接ハードウェアか、Loopbackか、など)
    • 入力チャンネル数
    • 出力チャンネル数
    • 現在のサンプリング周波数、サンプリングフォーマット

RenderSimple.cpp

オーディオのRendering(出力)用のサンプルです。まず、最初に service を取得しています。Xt::Setup::ConsumerAudioはDirectSoundに該当するため、例えばDirectSoundを無効にしているとserviceの中身が空となって返ってきます。ビルド時に有効にしたオーディオAPIのみ利用できますので、ご注意ください。

  Xt::System system = platform->SetupToSystem(Xt::Setup::ConsumerAudio);
  std::unique_ptr<Xt::Service> service = platform->GetService(system);
  if (!service) return 0;

次にオーディオのフォーマットの指定を行います。最初は Xt::Device::SupportsFormat() で失敗していましたが、WASAPIの場合はライブラリの内部で IAudioClient::IsFormatSupported() を呼び出しており、このチェックでチャンネル数とサンプリングレートが適切でなかったのが理由でした。余談ですが、この時の IAudioClient::IsFormatSupported() の戻り値はS_FALSEあり、この結果の場合は「Succeeded with a closest match to the specified format.」と失敗したというわけではないので、これがXT-Audioの作成者が意図したとおりの実装かは判断が難しいところです。

  static Xt::Channels const Channels(0, 0, 2, 0);
  static Xt::Mix const Mix(48000, Xt::Sample::Float32);
  static Xt::Format const Format(Mix, Channels);

  std::optional<std::string> id = service->GetDefaultDeviceId(true);
  if(!id.has_value()) return 0;
  std::unique_ptr<Xt::Device> device = service->OpenDevice(id.value());
  if(!device->SupportsFormat(Format)) return 0;

正しいオーディオのフォーマットが保証された後は、Xt::Device::OpenStream から Xt::Stream をオープンして、ストームの開始と停止を行います。OnBuffer はレンダリングするバッファを設定するためのコールバック関数です。なお、Xt::StreamParamsですが、第1引数のインターリーブのフラグ設定、第3引数のonXRunは登録してもとくに呼び出されず、第4引数のonRunningのストリームの開始と終了時にコールバックが呼ばれる挙動をしています。WindowsのオーディオAPIはインターリーブのみですが、大抵のアプリケーションはストリームごとにデータを加工すると思いますので、個人的にはノンインターリーブのほうがプログラムは見やすくなるかと思います。

  double bufferSize = device->GetBufferSize(Format).current;
  Xt::StreamParams streamParams(true, OnBuffer, nullptr, nullptr);
  Xt::DeviceStreamParams deviceParams(streamParams, Format, bufferSize);
  std::unique_ptr<Xt::Stream> stream = device->OpenStream(deviceParams, nullptr);
  stream->Start();
  std::this_thread::sleep_for(std::chrono::seconds(2));
  stream->Stop();

Conclusion

結論としては、XT-Audioはしばらく保留かなと感じました。まだ、ライブラリとしては成熟しておらず、業務として運用していくには厳しい完成度だと思います。特にエラー内容が取得できないのは、ツールに組み込んでいく上で致命的です。とはいえ、Audio I/O ライブラリは変化の乏しい分野であり、.Netにも対応しているのは素晴らしいことですので、今後の更新もぜひ期待したいです。

  • Pros
    • .Net/Javaに対応
    • ASIOに対応
    • シンプルに実装が可能
    • C++11(設定はC++17)でのモダンな実装
  • Cons
    • Macに対応していない
    • API のドキュメントがない
    • API 失敗時のエラーが取得できない。Debugビルドでのエラーログの出力もない。
    • エラーのコールバックがメッセージでしか取得できず、エラーのハンドリングもあまり行われていない
    • 内部でCreateWindowを呼び出すなど、意図しない振る舞いを行うことがある
    • 開発にVisual Studio 2019 が必要
    • unique_ptrで管理しているため、安全な反面、実装が面倒なケースが出てくる