読者です 読者をやめる 読者になる 読者になる

聞きかじりめも

主にC++やメディア処理技術などに関して気付いたことを書いていきます.ここが俺のメモ帳だ!

XAudio2をC++/CLIで使いたい 実装編

前回はXAudio2(というかSlimDX)を使う上での注意事項を散々書いてきたので,いよいよ今回は実装に移ります.C++/CLIの自分用覚書も兼ねてるので当たり前のことも書いています.

あんまり一般的な書き方じゃない気もしますが,とりあえず動くものを作ってみました.実行するとボタンが1つだけあるウインドウが現れて,ボタンを押すと押した瞬間にwave形式の波形データをメモリ上に作り,作ったデータを即再生します.

重要な部分をピックアップ

名前空間を宣言してるのでSlimDX::などは省略されていることに注意してください.

TestForm.h

 using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;

    using namespace SlimDX::XAudio2;
    using namespace SlimDX::Multimedia;

//----------------中略-----------------------

#pragma endregion
    public:
        XAudio2 ^device;
        MasteringVoice ^xMVoice;
        WaveFormat ^format;
        AudioBuffer ^xBuf;
        IO::MemoryStream ^mStream;
        SourceVoice ^xSrcVoice;
    private: 
        System::Void button1_Click(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_Load(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e);
    };
}
#include <Windows.h>

#define PI     3.1415926535f

using namespace RealtimeRendering;
using namespace std;

ヘッダファイルのSystem名前空間のところにしれっとSlimDXが混ざっています.これはXAudio2のハンドルをクラス内で宣言するのにこのあたりに書くのが都合がいいからです.また,ここでハンドルを宣言だけしておくとメンバ関数間で同じハンドルを使えます(つまり初期化と解放と処理が別々の関数で行なえる).ハンドルはpublicにしないとコンパイルエラーになります.最後のusingは別にTestForm.cppに書いてもいいんだけどなんとなく.

TestForm.cpp

 //XAudio2の初期化
    device = gcnew XAudio2();
    xMVoice = gcnew MasteringVoice(device);

フォームをロードしたときにXAudio2オブジェクトを生成します.SlimDXだからHRESULTのtry-catch例外処理で囲わずに作れるのが楽でいいですね.

    // 波形フォーマットの生成
    format = gcnew WaveFormat();
    format->FormatTag = static_cast<WaveFormatTag>(WAVE_FORMAT_PCM);     // フォーマット形式:PCM
    format->Channels = 1;                                                   // チャネル数:モノラル
    format->BitsPerSample = 16;                                             // 16 bit/sample
    format->SamplesPerSecond = 44100;                                       // サンプリングレート
    format->BlockAlignment = format->BitsPerSample / 8 * format->Channels;                // 1ブロックあたりのbyte数
    format->AverageBytesPerSecond = format->SamplesPerSecond * format->BlockAlignment; // 1秒あたりのバッファ数

PCMで波形フォーマットを生成します.簡単なモノラル波形を生成できればいいのでこのようにしました.ただしXAudio2とは異なり,FormatTagに定数を入れるときにSlimDXでは型が違うと怒られるのでcastしてやらなくてはいけません.

 // Sin波生成(double -> short)
    float a = 0.5;                // 振幅(0, 1]
    float freq = 880.0;           // 周波数[Hz]
    float time = 0.1;         // 時間[sec]
    array<short>^ waveData = gcnew array<short>((int)(format->AverageBytesPerSecond * time)); // t秒分のバッファ
    for (int i = 0; i < waveData->Length / 2; i++)
    {
        waveData[i] = (short)(SHRT_MAX * a * Math::Sin(i * PI * freq / (double)format->SamplesPerSecond));
    }
    // short -> byte(PCMフォーマットでメモリ上に波形データを展開)
    array<unsigned char>^ byteArray = gcnew array<unsigned char>(waveData->Length * format->BitsPerSample / 8);  // 16bit音源なので2倍
    Buffer::BlockCopy(waveData, 0, byteArray, 0, byteArray->Length);

波形データはarrayで確保しておきます.最初vectorでやろうとしたけど後でMemoryStreamを生成するときに型が違うと怒られました.本当は波の合成の都合上[-1, 1]の浮動小数点数で波形データを作ってからshortにcastしたかったんだけど,時間が無かったのでそれは次回にしよう.

PCMフォーマットでは8bitが基本ブロックなので,16bitモノラル音源では2byteのshortを1byteのunsigned charに分割しながら入れていかなきゃいけません.そこで大活躍するのがBuffer::BlockCopy()です.これを知る前は自力でビットシフトしながらforでコピーしていました.1行で書けるって素晴らしい…

 // バッファの作成
    xBuf = gcnew AudioBuffer();
    mStream = gcnew IO::MemoryStream(byteArray);
    xBuf->AudioData = mStream;
    xBuf->AudioBytes = (int)mStream->Length;
    xBuf->Flags = BufferFlags::EndOfStream;

AudioDataにどうやってデータを渡すかで苦労したのを憶えています.普通wavファイルから読み込むときはWaveStreamで何の苦も無くできたのですが,自分で波形を作るとなるとMemoryStreamにarrayを渡してそれを渡す,という格好になります.AudioBufferのメンバがどういうものなのか後でちゃんと調べないと.

 // Source Voice の作成
    xSrcVoice = gcnew SourceVoice(device, format);
    xSrcVoice->SubmitSourceBuffer(xBuf);

多分半永久的に音を生成し続けるようなコードになると最初にSourceVoiceオブジェクトを作った方がいいと思います.勘だけど.

 // 再生
    xSrcVoice->Start();
    MessageBox::Show("終了します.");

    // 終了
    xSrcVoice->Stop();

ボタンでStart(),Stop()メソッドだけを呼び出して,他の部分で波形生成・バッファ転送するような仕組みにしたいですが,今のところはこんな感じで.

 delete xBuf;
    delete xSrcVoice;
    delete mStream;
    delete xMVoice;
    delete device;

C++/CLIのハンドルはいつ解放されるかわからない(解放されないかもしれない)ので,明示的に解放します.
ちなみに今回のプログラムでは,こいつを怠ると実行時に

0xC0020001: その文字列結合は無効です。

なんていう見かけないエラーが出る時があります(出ないときもあるから厄介).

以下全ソースコード

RealtimeRendering

TestForm.h

#pragma once

namespace RealtimeRendering {

    using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;

    using namespace SlimDX::XAudio2;
    using namespace SlimDX::Multimedia;

    /// <summary>
    /// TestForm の概要
    /// </summary>
    public ref class TestForm : public System::Windows::Forms::Form
    {
    public:
        TestForm(void)
        {
            InitializeComponent();
            //
            //TODO: ここにコンストラクター コードを追加します
            //
        }

    protected:
        /// <summary>
        /// 使用中のリソースをすべてクリーンアップします。
        /// </summary>
        ~TestForm()
        {
            if (components)
            {
                delete components;
            }
        }
    private: System::Windows::Forms::Button^  button1;
    protected:

    private:
        /// <summary>
        /// 必要なデザイナー変数です。
        /// </summary>
        System::ComponentModel::Container ^components;

#pragma region Windows Form Designer generated code
        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        void InitializeComponent(void)
        {
            this->button1 = (gcnew System::Windows::Forms::Button());
            this->SuspendLayout();
            // 
            // button1
            // 
            this->button1->Location = System::Drawing::Point(98, 121);
            this->button1->Name = L"button1";
            this->button1->Size = System::Drawing::Size(75, 23);
            this->button1->TabIndex = 0;
            this->button1->Text = L"button1";
            this->button1->UseVisualStyleBackColor = true;
            this->button1->Click += gcnew System::EventHandler(this, &TestForm::button1_Click);
            // 
            // TestForm
            // 
            this->AutoScaleDimensions = System::Drawing::SizeF(6, 12);
            this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font;
            this->ClientSize = System::Drawing::Size(284, 261);
            this->Controls->Add(this->button1);
            this->Name = L"TestForm";
            this->Text = L"TestForm";
            this->FormClosing += gcnew System::Windows::Forms::FormClosingEventHandler(this, &TestForm::TestForm_FormClosing);
            this->Load += gcnew System::EventHandler(this, &TestForm::TestForm_Load);
            this->ResumeLayout(false);

        }
#pragma endregion
    public:
        XAudio2 ^device;
        MasteringVoice ^xMVoice;
        WaveFormat ^format;
        AudioBuffer ^xBuf;
        IO::MemoryStream ^mStream;
        SourceVoice ^xSrcVoice;
    private: 
        System::Void button1_Click(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_Load(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e);
    };
}
#include <Windows.h>

#define PI     3.1415926535f

using namespace RealtimeRendering;
using namespace std;

TestForm.cpp

#include "TestForm.h"


//----------------------------------------
// XAudio2の初期化処理
//----------------------------------------
System::Void TestForm::TestForm_Load(System::Object^  sender, System::EventArgs^  e)
{
    //XAudio2の初期化
    device = gcnew XAudio2();
    xMVoice = gcnew MasteringVoice(device);

    // 波形フォーマットの生成
    format = gcnew WaveFormat();
    format->FormatTag = static_cast<WaveFormatTag>(WAVE_FORMAT_PCM);     // フォーマット形式:PCM
    format->Channels = 1;                                                   // チャネル数:モノラル
    format->BitsPerSample = 16;                                             // 16 bit/sample
    format->SamplesPerSecond = 44100;                                       // サンプリングレート
    format->BlockAlignment = format->BitsPerSample / 8 * format->Channels;                // 1ブロックあたりのbyte数
    format->AverageBytesPerSecond = format->SamplesPerSecond * format->BlockAlignment; // 1秒あたりのバッファ数

}
//----------------------------------------
// メモリ解放処理
//----------------------------------------
System::Void TestForm::TestForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e)
{
    delete xBuf;
    delete xSrcVoice;
    delete mStream;
    delete xMVoice;
    delete device;
}
//----------------------------------------
// Sin波生成直後に再生
//----------------------------------------
System::Void TestForm::button1_Click(System::Object^  sender, System::EventArgs^  e)
{
    // Sin波生成(double -> short)
    float a = 0.5;                // 振幅(0, 1]
    float freq = 880.0;           // 周波数[Hz]
    float time = 0.1;         // 時間[sec]
    array<short>^ waveData = gcnew array<short>((int)(format->AverageBytesPerSecond * time)); // t秒分のバッファ
    for (int i = 0; i < waveData->Length / 2; i++)
    {
        waveData[i] = (short)(SHRT_MAX * a * Math::Sin(i * PI * freq / (double)format->SamplesPerSecond));
    }
    // short -> byte(PCMフォーマットでメモリ上に波形データを展開)
    array<unsigned char>^ byteArray = gcnew array<unsigned char>(waveData->Length * format->BitsPerSample / 8);  // 16bit音源なので2倍
    Buffer::BlockCopy(waveData, 0, byteArray, 0, byteArray->Length);

    // バッファの作成
    xBuf = gcnew AudioBuffer();
    mStream = gcnew IO::MemoryStream(byteArray);
    xBuf->AudioData = mStream;
    xBuf->AudioBytes = (int)mStream->Length;
    xBuf->Flags = BufferFlags::EndOfStream;

    // Source Voice の作成
    xSrcVoice = gcnew SourceVoice(device, format);
    xSrcVoice->SubmitSourceBuffer(xBuf);

    // 再生
    xSrcVoice->Start();
    MessageBox::Show("終了します.");

    // 終了
    xSrcVoice->Stop();
}

main.cpp

//-------------------------------------------------------
//     XAudio2によるリアルタイムサウンド合成
//-------------------------------------------------------

#include "TestForm.h"

using namespace RealtimeRendering;

[STAThreadAttribute]
int main(array<System::String ^> ^args)
{
    Application::EnableVisualStyles();
    Application::SetCompatibleTextRenderingDefault(false);
    Application::Run(gcnew TestForm());
    return 0;
}