DirectX の構成要素

XAudio2 による Windows 8 のサウンド生成

Charles Petzold

コード サンプルをダウンロードする

Charles PetzoldWindows 8 向けの Windows ストア アプリでは、MediaElement クラスを使って MP3 や WMA のサウンド ファイルを簡単に再生できます。このクラスに、サウンド ファイルの URL またはストリームを指定するだけです。Windows ストア アプリからは、ビデオやオーディオを外部デバイスにストリーミングするための Play To API にアクセスすることもできます。

しかし、もっと高度なオーディオ処理が必要な場合はどうすればよいでしょう。たとえば、ハードウェアにオーディオ ファイルを読み込む際にコンテンツを加工したり、サウンドを動的に生成したりするような場合です。

Windows ストア アプリでは、DirectX を使ってこのような高度な処理を実行できます。Windows 8 は、サウンドの生成と処理用に 2 つの DirectX コンポーネント (Core Audio と XAudio2) をサポートします。大掛かりな構想の中で、2 つともどちらかといえば低レベルのインターフェイスです。ただし、Core Audio は XAudio2 よりさらに低レベルで、オーディオ ハードウェアと密接な結び付きを必要とするアプリケーションを対象として作成されています。

XAudio2 は DirectSound と Xbox XAudio ライブラリに代わるもので、Windows ストア アプリにサウンドによる興味深い仕掛けが必要になったときのおそらく最善の選択肢です。XAudio2 は主にゲームを対象としていますが、それだけにとどまらず、音楽制作やビジネス ユーザーを楽しませる愉快なサウンドなど、より本格的な用途にも使用できます。

XAudio2 バージョン 2.8 は Windows 8 の組み込みコンポーネントです。XAudio2 プログラミング インターフェイスは、DirectX の他のプログラミング インターフェイスと同様、COM を基盤にしています。Windows 8 でサポートされているプログラミング言語であれば、理論上 XAudio2 へのアクセスは可能ですが、XAudio2 を最も自然かつ容易に扱える言語は C++ です。サウンドの操作には、多くの場合、高いパフォーマンスのコードが求められるため、その点でも C++ は優れた選択肢です。

初めての XAudio2 プログラム

ではまず XAudio2 を使って、ボタンをクリックするとシンプルな 5 秒間のサウンドを再生するプログラムを記述してみましょう。Windows 8 と DirectX のプログラミングが初めての方がいるかもしれないので、少しゆっくり進めます。

Windows ストア アプリの作成に適したバージョンの Visual Studio がインストールされているものとします。[新しいプロジェクト] ダイアログ ボックスで、左側の [Visual C++] と [Windows ストア] をクリックし、使用可能なテンプレートのリストの中から [新しいアプリケーション (XAML)] をクリックします。今回はプロジェクトに「SimpleAudio」という名前を付けます。この記事のダウンロード コードに、この名前のプロジェクトがあります。

XAudio2 を使用する実行可能ファイルをビルドする際に、プログラムと xaudio2.lib インポート ライブラリとをリンクする必要があります。[プロジェクト] メニューの 1 番下のメニュー項目をクリックするか、ソリューション エクスプローラーのプロジェクト名を右クリックしてから [プロパティ] をクリックすることで、[(プロジェクト名) プロパティ ページ] ダイアログ ボックスを開きます。左側の列で、[構成プロパティ] をクリックしてから、[リンカー]、[入力] の順にクリックします。[追加の依存ファイル] ( 1 番上の項目)、小さい矢印の順にクリックします。[編集] をクリックし、テキスト ボックスに「xaudio2.lib」と入力します。

xaudio2.h ヘッダー ファイルを参照するため、pch.h ファイルのプリコンパイル済みヘッダーのリストに下記のステートメントを追加します。

#include <xaudio2.h>

MainPage.xaml ファイルには、XAudio2 API の操作中にプログラムで発生する可能性のあるエラーを表示するための TextBlock と、サウンド再生用の Button を追加しました。この追加を図 1 に示します。

図 1 SimpleAudio の MainPage.xaml ファイル

<Page x:Class="SimpleAudio.MainPage" ... >
  <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <TextBlock Name="errorText"
               FontSize="24"
               TextWrapping="Wrap"
               HorizontalAlignment="Center"
               VerticalAlignment="Center" />
    <Button Name="submitButton"
            Content="Submit Audio Button"
            Visibility="Collapsed"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Click="OnSubmitButtonClick" />
  </Grid>
</Page>

MainPage.xaml.h ヘッダー ファイルの主要部分を図 2 に示します。今回 OnNavigatedTo メソッドは使用しないので、その宣言は削除しています。ここでは、プログラムで XAudio2 を使用するために必要な 4 つのフィールドと、ボタンの Click ハンドラーを宣言しています。

図 2 SimpleAudio の MainPage.xaml.h ヘッダー ファイル

namespace SimpleAudio
{
  public ref class MainPage sealed
  {
  private:
    Microsoft::WRL::ComPtr<IXAudio2> pXAudio2;
    IXAudio2MasteringVoice * pMasteringVoice;
    IXAudio2SourceVoice * pSourceVoice;
    byte soundData[2 * 5 * 44100];
  public:
    MainPage();
  private:
    void OnSubmitButtonClick(Platform::Object^ sender,
      Windows::UI::Xaml::RoutedEventArgs^ args);
  };
}

XAudio2 を使用するすべてのプログラムは、IXAudio2 インターフェイスを実装するオブジェクトを作成する必要があります (オブジェクトは簡単に作成できることをこの後示します)。IXAudio2 は COM の著名な IUnknown クラスから派生しているため、本質的に参照回数がカウントされます。そのため、IXAudio2 独自のリソースがプログラムから参照されなくなった時点で自動的に削除されます。Microsoft::WRL 名前空間の ComPtr (COM ポインター) クラスは、COM オブジェクトへのポインターを、参照回数を追跡する "スマート ポインター" に変換します。Windows ストア アプリで COM オブジェクトを操作する場合は、ComPtr クラスを利用する方法をお勧めします。

簡単な XAudio2 プログラムでなければ、IXAudio2MasteringVoice インターフェイスと IXaudio2SourceVoice インターフェイスを実装するオブジェクトへのポインターも必要になります。XAudio2 の用語では、"ボイス" はオーディオ データの生成や変更を行う 1 つのオブジェクトです。マスター ボイスとは、概念的にはサウンド ミキサーで、個別のボイスをすべてまとめ、それをサウンド生成ハードウェア向けに準備します。このようなボイスは 1 つだけ使用しますが、個別のサウンドを生成するソース ボイスは数多く使用する場合があります (ソース ボイスにフィルター処理やエフェクト処理を適用する方法もあります)。

IXAudio2MasterVoice ポインターと IXAudio2SourceVoice pointers ポインターには参照カウンターがなく、その有効期間は IXAudio2 オブジェクトによって管理されます。

5 秒間のサウンド データ用に次のように大きな配列も含めています。

byte soundData[5 * 2 * 44100];

実際のプログラムでは、このように大きなサイズの配列は実行時に割り当て、必要なくなったらメモリから削除するところですが、このような方法をとった理由はすぐにわかります。

配列のサイズはどのように計算するのでしょう。XAudio2 は圧縮オーディオをサポートしますが、サウンドを生成するプログラムの大半はパルス符号変調 (PCM) 方式を使用しています。PCM の音波は、固定サンプリング レートの固定サイズの値で表されます。音楽 CD の場合、サンプリング レートは 1 秒あたり 44,100 回です。ステレオ方式では 1 サンプルあたり 2 バイトを使用します。その結果、1 秒のオーディオ データは合計 176,400 バイトになります (アプリケーションにサウンドを埋め込むときは、圧縮することをお勧めします。XAudio2 は ADPCM をサポートします。また Media Foundation エンジンでは WMA と MP3 もサポートします)。

今回のプログラムでも、サンプリング レートを 44,100 回とし、1 サンプルあたり 2 バイトの容量を使用するという計算を採用しました。したがって、C++ では、各サンプルを short にします。今回はモノラル サウンドを使うため、1 秒間のオーディオには 88,200 バイト必要です。配列のメモリの割り当ては、5 秒間なのでその 5 倍です。

オブジェクトの作成

MainPage.xaml.cpp ファイルの多くの部分を図 3 に示しています。XAudio2 の初期化はすべて MainPage コンストラクターで行っています。まず XAudio2Create を呼び出して、IXAudio2 インターフェイスを実装するオブジェクトへのポインターを取得します。これが XAudio2 を使用する最初の手順です。一部の COM インターフェイスとは異なり、CoCreateInstance を呼び出す必要はありません。

図 3 MainPage.xaml.cpp

MainPage::MainPage()
{
  InitializeComponent();
  // Create an IXAudio2 object
  HRESULT hr = XAudio2Create(&pXAudio2);
  if (FAILED(hr))
  {
    errorText->Text = "XAudio2Create failure: " + hr.ToString();
    return;
  }
  // Create a mastering voice
  hr = pXAudio2->CreateMasteringVoice(&pMasteringVoice);
  if (FAILED(hr))
  {
    errorText->Text = "CreateMasteringVoice failure: " + hr.ToString();
    return;
  }
  // Create a source voice
  WAVEFORMATEX waveformat;
  waveformat.wFormatTag = WAVE_FORMAT_PCM;
  waveformat.nChannels = 1;
  waveformat.nSamplesPerSec = 44100;
  waveformat.nAvgBytesPerSec = 44100 * 2;
  waveformat.nBlockAlign = 2;
  waveformat.wBitsPerSample = 16;
  waveformat.cbSize = 0;
  hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &waveformat);
  if (FAILED(hr))
  {
    errorText->Text = "CreateSourceVoice failure: " + hr.ToString();
    return;
  }
  // Start the source voice
  hr = pSourceVoice->Start();
  if (FAILED(hr))
  {
    errorText->Text = "Start failure: " + hr.ToString();
    return;
  }
  // Fill the array with sound data
  for (int index = 0, second = 0; second < 5; second++)
  {
    for (int cycle = 0; cycle < 441; cycle++)
    {
      for (int sample = 0; sample < 100; sample++)
      {
        short value = sample < 50 ? 32767 : -32768;
        soundData[index++] = value & 0xFF;
        soundData[index++] = (value >> 8) & 0xFF;
      }
    }
  }
  // Make the button visible
  submitButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
}
void MainPage::OnSubmitButtonClick(Object^ sender, 
  RoutedEventArgs^ args)
{
  // Create a button to reference the byte array
  XAUDIO2_BUFFER buffer = { 0 };
  buffer.AudioBytes = 2 * 5 * 44100;
  buffer.pAudioData = soundData;
  buffer.Flags = XAUDIO2_END_OF_STREAM;
  buffer.PlayBegin = 0;
  buffer.PlayLength = 5 * 44100;
  // Submit the buffer
  HRESULT hr = pSourceVoice->SubmitSourceBuffer(&buffer);
  if (FAILED(hr))
  {
    errorText->Text = "SubmitSourceBuffer failure: " + hr.ToString();
    submitButton->Visibility = 
      Windows::UI::Xaml::Visibility::Collapsed;
    return;
  }
}

IXAudio2 オブジェクトを作成したら、CreateMasteringVoice メソッドと CreateSourceVoice メソッドにより、ヘッダー ファイルでフィールドとして定義した別の 2 つのインターフェイスへのポインターを取得します。

CreateSourceVoice の呼び出しには WAVEFORMATEX 構造体が必要です。これは Win32 API でオーディオを扱ったことのある開発者にはおなじみの構造体です。この構造体では、今回の特定のボイスに使用するオーディオ データの性質を定義します (異なるソース ボイスには異なる形式を使用できます)。PCM の場合、関係するのはサンプリング レート (この例では 44,100)、各サンプルのサイズ (2 バイトまたは 16 ビット)、チャンネル数 (ここでは 1) の 3 つの数値です。他のフィールドは以下のようにします。nBlockAlign フィールドは nChannels*wBitsPerSample/8、nAvgBytesPerSec フィールドは nSamplesPerSec*nBlockAlign にします。この例では、フィールドの値をすべて明確な数値で示しています。

IXAudio2SourceVoice オブジェクトを取得したら、オブジェクトの Start メソッドを呼び出すことができます。この時点で XAudio2 は、概念的にサウンドを再生していますが、ここでは実際に再生するオーディオ データを指定していません。Stop メソッドも使用でき、実際のプログラムではこの 2 つのメソッドを使ってサウンドの再生と停止を行います。

上記の 4 つの呼び出しは、コードに示すほど単純ではありません。どの呼び出しにも追加の引数がありますが、どの引数にも有効な既定値が定義されているため、ここではその既定値を単純にそのまま利用することにしています。

DirectX のほぼすべての関数やメソッドの呼び出しでは、成功か失敗かを示す HRESULT 値が返されます。このようなエラーの対処にはさまざまな方法がありますが、今回は単に XAML ファイルで定義した TextBlock を使ってエラー コードを表示し、それ以上処理を行わないことにしました。

PCM オーディオ データ

コンストラクターは soundData 配列にオーディオ データを設定するところで終わっていますが、実際にはボタンが押されるまでその配列は IXAudio2SourceVoice に渡されません。

サウンドとは振動で、人間は空気中ではおおよそ 20 Hz (サイクル数/秒) から 20,000 Hz の振動を聞き取ることができます。ピアノのちょうど中央にあるドの音は約 261.6 Hz です。

44,100 Hz のサンプリング レートと 16 ビット (2 バイト) のサンプル数を使用し、ピアノの 1 番高い音の周波数をちょっと超える 4,410 Hz の波形のオーディオ データを生成するとします。このような波形の各サイクルには、16 ビットの符号付き数値のサンプルが 10 個必要なので、このような 10 個の値のサンプルが、サウンド 1 秒あたり 4,410 回繰り返されることになります。

周波数 441 Hz の波形では、100 個のサンプルがレンダリングされます。この周波数 441 Hz は、ピアノをチューニングする際の基準となる 440 Hz (ピアノでは中央のドの上のラの音) に非常に近い周波数です。周波数 441 Hz の波形では、このサイクルがサウンド 1 秒あたり 441 回繰り返されることになります。

PCM ではサンプリング レートが一定になるため、低周波数のサウンドの方が高周波数のサウンドに比べて高い解像度でサンプリングおよびレンダリングされるように思えます。この考えで問題ないでしょうか。わずか 10 個のサンプルをレンダリングする 4,410 Hz の波形の方が、441 Hz の波形よりも音のひずみが大きくなるのでしょうか。

PCM では、サンプリング レートの半分を超える周波数 (ベル研究所の技術者ハリー・ナイキストにちなんで命名された、いわゆるナイキスト周波数) で量子化のひずみが起こることがわかります。44,100 Hz のサンプリング レートが CD オーディオに採用された理由の 1 つは、ナイキスト周波数が 22,050 Hz で、人間の聴覚の限界が約 20,000 Hz だったためです。つまり、44,100 Hz のサンプリング レートでは、量子化のひずみが人間には知覚されません。

SimpleAudio プログラムでは、アルゴリズム的にシンプルな波形 (周波数 441 Hz の矩形波) が生成されます。1 サイクルあたり 100 個のサンプルがあります。各サイクルでは、最初の 50 個のサンプルが正の最大値 (short 型の整数の場合は 32,767)、次の 50 個のサンプルが負の最大値 (-32,768) です。これら short 値を byte 配列に格納するときは、下位バイトから先に格納する必要があります。

soundData[index + 0] = value & 0xFF;
soundData[index + 1] = (value >> 8) & 0xFF;

この時点でもまだ実際には再生されません。再生はボタンの Click ハンドラーで行います。XAUDIO2_BUFFER 構造体を使用して、サンプル回数として指定されたバイト数と再生時間を持つバイト配列を参照します。このバッファーを IXAudio2SourceVoice オブジェクトの SubmitSourceBuffer メソッドに渡します。Start メソッドが (今回の例のように) 既に呼び出されていていれば、すぐにサウンドの再生が始まります。

言うまでもありませんが、サウンドは非同期に再生されます。SubmitSourceBuffer 呼び出しからはすぐに復帰し、サウンド ハードウェアにデータを送り込む実際の処理は別のスレッドで行われます。SubmitSourceBuffer に渡した XAUDIO2_BUFFER は、呼び出しの終了後 (このプログラムでは Click ハンドラーが終了し、ローカル変数がスコープ外になったとき) に破棄することができます。ただし、実際のバイト配列はアクセス可能なメモリ内に残っている必要があります。実際、プログラムではサウンドの再生中にこのようなバイト列を操作できます。ただし、プログラムでサウンド データを動的に生成するという、もっと優れた手法 (コールバック メソッドが必要) があります。

サウンドが終了するタイミングを判断するコールバックを使用しないのであれば、プログラムの実行中は soundData 配列を保持する必要があります。

ボタンを複数回クリックすることができます。各呼び出しでは、前のバッファーの終了時に再生される別のバッファーが効果的にキューに登録されます。プログラムがバックグラウンドに移動すると、サウンドはミュート状態になりますが、再生はミュート状態で続行されます。つまり、ボタンをクリック後、プログラムを少なくとも 5 秒間バックグラウンドに移動すると、プログラムをフォアグラウンドに戻しても何も再生されません。

サウンドの特性

日常生活で耳にするサウンドは、いくつもの異なる音源から同時に発せられるため、かなり複雑です。しかし、場合によっては、特に音楽サウンドを扱うときは、わずかな特性だけで個別の音色を定義できます。

  • 振幅: 聴覚には音量として知覚されます。
  • 周波数: 音程として知覚されます。
  • 空間: 複数のスピーカーでオーディオを再生するイメージです。
  • 音質: サウンドでは倍音の混合に関連します。たとえば、トランペットとピアノの聞こえ方の違いを表現します。

SoundCharacteristics プロジェクトでは、これら 4 つの特性の例を個別に示します。SimpleAudio プロジェクトと同様にサンプル レート 44,100、サンプル回数 16 ビットを使いますが、ステレオ サウンドを生成します。2 チャンネルのサウンドの場合、データをインターリーブする必要があります。つまり、左側のチャンネルには最初の 16 ビット サンプル、右側のチャンネルには次の 16 ビット サンプルを配置します。

SoundCharacteristics の MainPage.xaml.h ヘッダー ファイルでは次の定数を定義します。

static const int sampleRate = 44100;
static const int seconds = 5;
static const int channels = 2;
static const int samples = seconds * sampleRate;

サウンド データ用に 4 つの配列も定義しますが、byte 型ではなく short 型にします。

short volumeSoundData[samples * channels];
short pitchSoundData[samples * channels];
short spaceSoundData[samples * channels];
short timbreSoundData[samples * channels];

short 配列を使うと、16 ビット波形の値を半分ずつに分割する必要がないため、初期化が容易になります。サウンド データを送信するときに、簡単なキャストを使用して、XAUDIO2_BUFFER で配列を参照できます。このプログラムではステレオを使用しているため、これらの配列は SimpleAudio の配列の 2 倍のバイト数になります。

これらの 4 つの配列はすべて、MainPage コンストラクターで初期化します。音量のデモの場合、441 Hz の矩形波を使用することは変わりませんが、音量がゼロから始まり、最初の 2 秒間は徐々に上がり、その後最後の 2 秒間で緩やかに下がります。図 4 に volumeSoundData を初期化するコードを示します。

図 4 SoundCharacteristics の音量を変えるサウンド データ

for (int index = 0, sample = 0; sample < samples; sample++)
{
  double t = 1;
  if (sample < 2 * samples / 5)
    t = sample / (2.0 * samples / 5);
  else if (sample > 3 * samples / 5)
    t = (samples - sample) / (2.0 * samples / 5);
  double amplitude = pow(2, 15 * t) - 1;
  short waveform = sample % 100 < 50 ? 1 : -1;
  short value = short(amplitude * waveform);
  volumeSoundData[index++] = value;
  volumeSoundData[index++] = value;
}

音量に対する人間の聞こえ方は対数表現になります。毎回波形の振幅を 2 倍にすることことは、音量を 6 dB 上げていくことに相当します (オーディオ CD に使われている 16 ビットの振幅はダイナミック レンジ 96 デシベルです)。図 4 で示した音量を変化させるコードでは、0 から 1 に直線的に増加し、その後 0 に戻るように減少していく値 t を最初に計算しています。amplitude 変数は、pow 関数を使って計算し、値を 0 ~ 32,767 の範囲に収めます。この値を、1 または -1 の値を持つ矩形波に乗算し、結果を配列に 2 回追加します。1 回目は左のチャンネル、2 回目は右のチャンネル用です。

周波数に対する人間の聞こえ方も対数表現になります。世界の音楽の多くは、音程がオクターブ間隔で編成され、1 オクターブ上がると周波数が 2 倍になります。「虹の彼方に (Somewhere over the Rainbow)」のコーラスの最初の 2 音のバス パートとソプラノ パートの音程の差はちょうど 1 オクターブになります。図 5に示すコードでは、(音量の例と同様に) 0 から 1 に向かい、また 0 に戻る t の値に基づいて、220 ~ 880 の 2 オクターブの範囲で音程を変えています。

図 5 SoundCharacteristics の周波数を変えるサウンド データ

double angle = 0;
for (int index = 0, sample = 0; sample < samples; sample++)
{
  double t = 1;
  if (sample < 2 * samples / 5)
    t = sample / (2.0 * samples / 5);
  else if (sample > 3 * samples / 5)
    t = (samples - sample) / (2.0 * samples / 5);
  double frequency = 220 * pow(2, 2 * t);
  double angleIncrement = 360 * frequency / waveformat.nSamplesPerSec;
  angle += angleIncrement;
  while (angle > 360)
    angle -= 360;
  short value = angle < 180 ? 32767 : -32767;
  pitchSoundData[index++] = value;
  pitchSoundData[index++] = value;
}

以前の例で、周波数を 441 Hz にしたのは、サンプリング レート 44,100 を割り切れるためです。一般にサンプリング レートは周波数の整数倍にはならないため、1 サイクルあたりのサンプル数は整数になりません。このプログラムでは浮動小数点変数の angleIncrement を管理しています。この変数は周波数に比例して、0 ~ 360 度の範囲で角度を上げるのに使用しています。この角度の値は、その後波形を構築するのに使用します。

空間のデモでは、サウンドのチャンネルを中央から左、次に右、その後中央へと移動します。音質のデモでは、波形を 441 Hz の正弦曲線から始めます。正弦曲線は、振動の最も基本的な種類の数学表現で、変位に反比例する力を求める微分方程式の解です。その他の周期波形はすべて、調波を含みます。調波も正弦波ですが、周波数は基本周波数の整数倍になります。音質のデモでは、サウンドの調和性を増加しながら、波形を正弦波から三角波、矩形波、鋸歯状波へと滑らかに変えています。

大きな構想に向けて

今回は 1 つの IXAudio2SourceVoice オブジェクトのサウンド データを生成して、音量、音程、空間、音質を制御する方法のデモを行っただけですが、このオブジェクト自体には音量、空間、および周波数も変更できるメソッドがあります (3D 空間機能もサポートされます)。個々の音色のブロックを 1 つのソース ボイスに組み合わせる複合サウンド データを生成することも可能ですが、複数の IXAudio2SourceVoice オブジェクトを作成して、同じマスター ボイスでそれらをまとめて再生することも可能です。

また、XAudio2 では、フィルターや、反響やエコーなどのエフェクトを定義できる IXAudioSubmixVoice インターフェイスを定義します。フィルターには既存の音色の音質を動的に変化される機能があり、魅力的で実用的な音楽サウンドを作るのにとても役立ちます。

おそらく、今回の 2 つのプログラムで示した内容を根本的に強化するには、XAudio2 のコールバック関数を利用する必要があります。今回の 2 つのプログラムで行ったように、大容量のサウンド データの初期化と割り当てを行うのではなく、プログラムにとっては再生の直前にサウンド データを動的に生成する方が適切です。

Charles Petzold は MSDN マガジンの記事を長期にわたって担当しており、Windows 8 向けのアプリケーション開発についての書籍『Programming Windows, 6th edition』 (O’Reilly Media、2012 年) の著者でもあります。彼の Web サイトは charlespetzold.com、英語 です。

この記事のレビューに協力してくれた技術スタッフの Scott Selfon に心より感謝いたします。