ゆとりーなの日記

日記的な事を書いて行くと思はれる

第7回〜WAVEファイルの読み込みと再生〜

折角XAudio2を初期化しても音声をならせなきゃ意味がありません。さっそくWAVEファイルを読み込んで再生する作業に入ります。
XAudio2で音声を再生する場合、波形データを保存した配列が必要になります。そこでまずはstd::vectorにWAVEデータから波形データを保存する構造体を作ります。また、波形データの情報として、WAVEFORMATEX構造体も必要なのでこれもついでに管理します。

// soundbuffer.h
#pragma once

struct SoundBuffer {
    explicit SoundBuffer(const std::string &file_name);
    std::vector<char> buffer;
    WAVEFORMATEX format;
};

// soundbuffer.cc
#include "soundbuffer.h"

SoundBuffer::SoundBuffer(const std::string &file_name) : buffer(), format() {
    ZeroMemory(&format, sizeof(format));
    std::ifstream fin(file_name, std::ios::in | std::ios::binary);
    if (!fin) {
        throw std::runtime_error("ファイルを開くのに失敗");
    }
    const char riff[4] = {'R', 'I', 'F', 'F'};
    char read_data[4];
    // ヘッダの頭から4バイトを読み込んで"RIFE"かどうかを調べる
    fin.read(read_data, sizeof(read_data));
    if (std::memcmp(riff, read_data, sizeof(riff))) {
        throw std::runtime_error("RIFEチャンクが存在しない");
    } 
    // 4バイト読み飛ばし
    fin.ignore(4);
    const char wave[4] = {'W', 'A', 'V', 'E'};
    // 続いての4バイトを読み込んで"WAVE"かどうかを調べる
    fin.read(read_data, sizeof(read_data));
    if (std::memcmp(wave, read_data, sizeof(wave))) {
	throw std::runtime_error("WAVEチャンクが存在しない");
    } 
    // 続いての4バイトを読み込んで"fmt "かどうかを調べる
    const char fmt[4] = {'f', 'm', 't', ' '};
    fin.read(read_data, sizeof(read_data));
    if (std::memcmp(fmt, read_data, sizeof(fmt))) {
	throw std::runtime_error("fmt チャンクが存在しない");
    } 
    std::uint32_t format_size;
    // フォーマットサイズ読み込み
    fin.read(reinterpret_cast<char *>(&format_size), sizeof(format_size));
    std::uint16_t id;
    // フォーマットタグIDの読み込み
    fin.read(reinterpret_cast<char *>(&id), sizeof(id));
    // IDに対応するフォーマットにする
    switch (id) {
    case 1:
	format.wFormatTag = WAVE_FORMAT_PCM;
	break;
    case 2:
	format.wFormatTag = WAVE_FORMAT_ADPCM;
	break;
    default:
	throw std::runtime_error("対応していないフォーマット");
    }
    // チャンネル数を読み込む
    fin.read(reinterpret_cast<char *>(&format.nChannels), sizeof(format.nChannels));
    // サンプルレートを読み込む
    fin.read(reinterpret_cast<char *>(&format.nSamplesPerSec), sizeof(format.nSamplesPerSec));
    // データ速度を読み込む
    fin.read(reinterpret_cast<char *>(&format.nAvgBytesPerSec), sizeof(format.nAvgBytesPerSec));
    // ブロックサイズを読み込む
    fin.read(reinterpret_cast<char *>(&format.nBlockAlign), sizeof(format.nBlockAlign));
    // サンプル当たりのビット数を読み込む
    fin.read(reinterpret_cast<char *>(&format.wBitsPerSample), sizeof(format.wBitsPerSample));
    // 拡張ヘッダ情報を読み飛ばす
    fin.ignore(format_size - 16);
    const char data[4] = {'d', 'a', 't', 'a'};
    // 続いての4バイトを読み込んで"data"かどうかを調べる
    fin.read(read_data, sizeof(read_data));
    if (std::memcmp(data, read_data, sizeof(data))) {
	throw std::runtime_error("dataチャンクが存在しない");
    } 
    std::uint32_t size;
    // 波形データのサイズを読み込む
    fin.read(reinterpret_cast<char *>(&size), sizeof(size));
    buffer.resize(size);
    // 波形データ読み込み
    fin.read(&buffer.front(), size);
}

WAVEファイルのヘッダを解析して情報をWAVEFORMATEX構造体に保存、波形データをstd::vectorに保存します。ヘッダ的にWAVEファイルのものじゃなさそうと判断したら取り敢えず例外を投げてやることにします。通常のWAVEファイルを読み込む場合は上の様な感じで読み込んでやればいいと思います。
で、例によって同名ファイルの波形データは共有したいのでboost.flyweightを使います。キーは文字列だけでいいので、std::stringを使うことにします。
続いてWAVEを再生するクラスを作ります。インターフェイスはこんな感じにしました。

// soundplayer.h
#pragma once
#include "deleter.h"
#include "soundfwd.h"

class SoundPlayer : boost::noncopyable {
public:
    explicit SoundPlayer(Sound sound);
    void play();
    void stop(bool restart);
    void setVolume(float volume);
    float volume() const;
private:
    Sound sound_;
};

// soundfwd.h
#pragma once

class SoundImpl;

typedef std::shared_ptr<SoundImpl> Sound;

WAVE再生クラスはSoundImplのスマポを実装として受け取り動かすことにします。そこでまずSoundImplクラスを作ります。コピー時の挙動の定義が面倒なので取り敢えずコピー禁止にしておきます。

// soundimpl.h
#pragma once
#include "soundbuffer.h"
#include "deleter.h"

class SoundImpl {
public:
    explicit SoundImpl(const std::string &file_name, boost::intrusive_ptr<IXAudio2> xaudio);
private:
    friend class SoundPlayer;
    void init();
    boost::flyweights::flyweight<boost::flyweights::key_value<std::string, SoundBuffer>> buffer_;
    std::unique_ptr<IXAudio2SourceVoice, VoiceDeleter> voice_;
};

波形データとして、先にあげたboost::flyweights::flyweight>をメンバ変数とします。XAudio2で、実際に音声を鳴らすのを担当するのはIXAudio2SourceVoice*なので、この子のスマポもメンバ変数として持っておきます。こいつらにアクセスしてSoundPlayerクラスは音声を鳴らすのでSoundPlayerクラスをfriend指定しておきます。また、IXAudio2SourceVoiceも独特な後始末を行うので、専用のデリータを用意します。

// deleter.h
struct VoiceDeleter {
    void operator ()(IXAudio2SourceVoice *ptr) {
        assert(ptr);
        ptr->Stop();
        ptr->DestroyVoice();
    }
};

SoundImplクラスの実装です。

#include "soundimpl.h"

namespace {
IXAudio2SourceVoice *createSourceVoice(boost::intrusive_ptr<IXAudio2> xaudio, const WAVEFORMATEX &format) {
    assert(xaudio.get());
    IXAudio2SourceVoice *voice(nullptr);
    // IXAudio2SourceVoice*を得る
    if (FAILED(xaudio->CreateSourceVoice(&voice, &format))) {
        throw std::runtime_error("サウンドバッファの作成に失敗");
    }
    return voice;
}
}

SoundImpl::SoundImpl(const std::string &file_name, boost::intrusive_ptr<IXAudio2> xaudio) : buffer_(file_name), voice_(createSourceVoice(xaudio, buffer_.get().format)) {
    init();
}

void SoundImpl::init() {
    const XAUDIO2_BUFFER submit = {
        XAUDIO2_END_OF_STREAM,
	buffer_.get().buffer.size(),
	reinterpret_cast<const BYTE *>(&buffer_.get().buffer.front()),
        0,
        0,
        XAUDIO2_NO_LOOP_REGION,
        0,
        0,
        nullptr
    };
    // 鳴らす波形データを登録
    if (FAILED(voice_->SubmitSourceBuffer(&submit))) {
        throw std::runtime_error("サウンドバッファのサブミットに失敗しました");
    }
}

コンストラクタで波形データを得るためのファイル名とIXAudio2SourceVoice*を得るために必要なIXAudio2*を受け取ります。例によってスマポをスムーズに初期化するためにIXAudio2SourceVoice*を作って返す関数を作っておきます。IXAudio2SourceVoice*はIXAudio2::CreateSourceVoiceメソッドに、再生する予定の波形データの情報が入ったWAVEFORMATEXを渡すことで得ることが出来ます。
SoundImpl::initメソッドでは、IXAudio2SourceVoice*に再生する波形データを渡します。XAUDIO2_BUFFERに必要な情報を入れてIXAudio2SourceVoice::SubmitSourceBufferメソッドを使って登録します。通常に音声を再生する場合は、上の様な設定でいいと思います。殆どがデフォルトの0で、あとは波形データのサイズ、先頭へのポインタ、ループ再生なしといったことを指定しています。
前回のSoundDeviceクラスにSoundImplのスマポを作って返すメソッドを追加します。

// sounddevice.h
#pragma once
#include "deleter.h"
#include "cominiter.h"
#include "soundfwd.h"

class SoundDevice : private boost::noncopyable {
public:
    Sound createSoundFromFile(const std::string &file_name);
private:
    // 内部実装関係
};

// sounddevice.cc
Sound SoundDevice::createSoundFromFile(const std::string &file_name) {
    assert(xaudio_.get());
    return std::make_shared<SoundImpl>(file_name, xaudio_);
}

単純にSoundImplをmake_sharedしてあげるだけです。
最後にこれらを使ってSoundPlayerクラスの実装します。

// soundplayer.cc
#include "soundplayer.h"
#include "soundimpl.h"

SoundPlayer::SoundPlayer(Sound sound) : sound_(sound) {
}

void SoundPlayer::play() {
    assert(sound_->voice_.get());
    sound_->voice_->Start();
}

void SoundPlayer::stop(bool restart) {
    assert(sound_->voice_.get());
    sound_->voice_->Stop();
    if (restart) {
        sound_->voice_->FlushSourceBuffers();
        sound_->init();
    }
}

void SoundPlayer::setVolume(float volume) {
    assert(sound_->voice_.get());
    sound_->voice_->SetVolume(volume, XAUDIO2_COMMIT_NOW);
}

float SoundPlayer::volume() const {
    assert(sound_->voice_.get());
    float volume(0.f);
    sound_->voice_->GetVolume(&volume);
    return volume;
}

音声の再生にはIXAudio2SourceVoice::Start、停止にはIXAudio2SourceVoice::Stop、音量の設定にはIXAudio2SourceVoice::SetVolume、音量の取得にはIXAudio2SourceVoice::GetVolumeメソッドをそれぞれ呼んでやればいいので、そのように実装します。SoundPlayerメソッドでは、引数をtrueにすると次回の再生は頭から再生するようにしてみました。単にIXAudio2SourceVoice::Stopメソッド後にIXAudio2SourceVoice::Startメソッドを呼んでも続きから再生されるだけなので、頭出しするときは、IXAudio2SourceVoice::FlushSourceBuffersメソッドでボイスをクリアして、再度IXAudio2SourceVoice::SubmitSourceBufferメソッドを使って波形データを登録するようにします。
以上でWAVEファイルを鳴らす準備が整いました。TestApplicationクラスに以下の様に組み込めば、WAVEファイルを鳴らせます。

// testapplication.h
#pragma once
#include "application.h"
#include "soundfwd.h"
#include "soundplayer.h"

class TestApplication : public Application {
public:
    TestApplication(const std::string &caption, int client_width, int client_height, bool windowed);
private:
    SoundPlayer sound_;
};

// testapplication.cc
#include "testapplication.h"
#include "graphicdevice.h"
#include "drawobject.h"
#include "sounddevice.h"

TestApplication::TestApplication(const std::string &caption, int client_width, int client_height, bool windowed) : Application(caption, client_width, client_height, windowed), sound_(sound_device_->createSoundFromFile("test.wav")) {
    sound_.play();
}