ゆとりーなの日記

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

第8回〜oggファイルの読み込みとストリーミング再生〜

前回WAVEファイルを鳴らせるようになりましたが、WAVEファイルをBGMとして使う場合サイズが大きくて残念なことになります。そこで圧縮された音声形式で且つライセンス的にも同人ゲームで使いやすそうなoggをBGMとして使いたいと思うのは自然な流れです。そこで今回はoggをXAudio2を使ってストリーミング再生することにします。
oggファイルの読み込み等は、専用のoggvorbisSDKを使います。適宜ビルドしておいてください。ヘッダのパスを通すことと、できたライブラリをリンクするのも忘れずにやっておきます。
XAudio2で音声を再生するのには波形データを保存した配列が必要でした。まずはoggファイルから波形データを取りだすための下準備をするクラスを作ります。

// streamingbuffer.h
#pragma once
#include "deleter.h"

struct StreamingBuffer {
    explicit StreamingBuffer(const std::string &file_name);
    std::unique_ptr<OggVorbis_File> ovf;
    WAVEFORMATEX format;
};

// streamingbuffer.cc
#include "streamingbuffer.h"

namespace {
OggVorbis_File *createOggVorbis(const std::string &file_name) {
    OggVorbis_File *ovf(new OggVorbis_File());
    FILE *f;
    if (fopen_s(&f, file_name.c_str(), "rb")) {
        throw std::runtime_error("oggファイルを開くのに失敗");
    }
    if (ov_open(f, ovf, nullptr, 0)) {
        fclose(f);
        throw std::runtime_error("oggvorbisを開くのに失敗");
    }
    return ovf;
}
}

StreamingBuffer::StreamingBuffer(const std::string &file_name) : ovf(createOggVorbis(file_name)), format() {
    const vorbis_info *info(ov_info(ovf.get(), -1));
    if (!info) {
	throw std::runtime_error("oggフォーマットの取得に失敗");
    }
    ZeroMemory(&format, sizeof(format));
    // oggファイルから得た情報等を使ってWAVEFORMATEXを初期化
    format.wFormatTag = WAVE_FORMAT_PCM;
    format.nChannels = static_cast<WORD>(info->channels);
    format.nSamplesPerSec = info->rate;
    format.wBitsPerSample = 16;
    format.nBlockAlign = static_cast<WORD>(info->channels * 2);
    format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
}

今回は配列は直接管理せず、oggから配列に波形データを読みだしてくれるOggVorbis_Fileを持つことにします。コンストラクタではOggVorbis_Fileの初期化とoggファイルから得た情報等を使ってWAVEFORMATEXを初期化初期化します。OggVorbis_Fileもむりやりスマポにしました。デリータは次のようになります。

// deleter.cc
#include <vorbis/vorbisfile.h>

struct OggVorbisDeleter {
    void operator ()(OggVorbis_File *ptr) {
        assert(ptr);
        ov_clear(ptr);
        delete ptr;
    }
};

OggVorbis_Fileの後始末にはov_clear関数を呼んでやればOKです。生ポインタ扱いするのでdeleteも呼んであげます。
そしてスマポを円滑に初期化するためにcreateOggVorbis関数を作ります。本来であれば、ov_fopen関数で初期化できるのですが、何故か引数がchar*で腹立たしいので、これと同様の動作をする関数を自作しました。また、スマポにするためOggVorbis_Fileはnewしたものを使います。
同名ファイルでやはり共用したいので、こいつもboost.flyweightを使って扱います。このへんはWAVEファイルの時と同じです。
oggファイルの再生には前回のSoundPlayerクラスを使うことにします。oggファイルのストリーミング再生を行う実装として渡すOggStreamingImplクラスを作ります。SoundPlayerクラスはSoundImplクラスを実装として受け取る様にしているので、OggStreamingImplはこいつの派生クラスにしてやります。この際SoundImplは実装のインターフェイスクラスとして、前回作ったWAVE音声再生実装クラスはWaveSoundImplにしてSoundImplクラスを継承したものとして作り直すことにします。

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

class SoundImpl {
public:
    SoundImpl();
protected:
    void setVoice(IXAudio2SourceVoice *voice);
    void submit(const XAUDIO2_BUFFER &buffer);
private:
    friend class SoundPlayer;
    virtual void init() = 0;
    std::unique_ptr<IXAudio2SourceVoice, VoiceDeleter> voice_;
};

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

SoundImpl::SoundImpl() : voice_() {
}

void SoundImpl::setVoice(IXAudio2SourceVoice *voice) {
    voice_.reset(voice);
}

void SoundImpl::submit(const XAUDIO2_BUFFER &buffer) {
    if (FAILED(voice_->SubmitSourceBuffer(&buffer))) {
        throw std::runtime_error("サウンドバッファのサブミットに失敗しました");
    }
}

仕様上コンストラクタでIXAudio2SourceVoiceを渡せないので、protectedメソッドを介して渡してあげることにします。個人的にはprotectedメンバ変数の使用はあまり好きではないので、IXAudio2SourceVoiceインターフェイスを直接使わなければいけない部分は、SoundImpl::submitメソッドを用意してラップしてあげることにします。
まずは前回のSoundImplをWaveSoundImplとして作り直します。

// wavesoundimpl.h
#pragma once
#include "soundimpl.h"

class WaveSoundImpl : public SoundImpl {
public:
    WaveSoundImpl(const std::string &file_name, boost::intrusive_ptr<IXAudio2> xaudio);
private:
    void init();
    boost::flyweights::flyweight<boost::flyweights::key_value<std::string, SoundBuffer>> buffer_;
};

// wavesoundimpl.cc
#include "wavesoundimpl.h"

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

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

void WaveSoundImpl::init() {
    const XAUDIO2_BUFFER buffer = {
        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
    };
    submit(buffer);
}

単純にSoundImplクラスを継承して、一部処理をSoundImplに投げただけです。これに合わせてSoundDeviceクラスのcreateSoundFromFileメソッドを書き換えます。

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

続いて本題のoggストリーミング再生実装クラスを作ります。

// oggstreamingimpl.h
#pragma once
#include <queue>
#include "soundimpl.h"
#include "streamingbuffer.h"
#include "deleter.h"

class OggStreamingImpl : public SoundImpl, private IXAudio2VoiceCallback {
public:
    OggStreamingImpl(const std::string &file_name, bool looped, uint32_t offset, boost::intrusive_ptr<IXAudio2> xaudio);
private:
    friend class StreamingPlayer;
    void WINAPI OnStreamEnd();
    void WINAPI OnVoiceProcessingPassEnd();
    void WINAPI OnVoiceProcessingPassStart(UINT32);
    void WINAPI OnBufferEnd(void *);
    void WINAPI OnBufferStart(void *);
    void WINAPI OnLoopEnd(void *);
    void WINAPI OnVoiceError(void *, HRESULT);
    void init();
    void loadPcmBuffer();
    bool looped_;
    uint32_t offset_;
    std::queue<std::vector<char>> buffer_queue_;
    boost::flyweights::flyweight<boost::flyweights::key_value<std::string, StreamingBuffer>> ogg_;
};

// oggstreamingimpl.cc
#include "oggstreamingimpl.h"
#include <algorithm>

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

OggStreamingImpl::OggStreamingImpl(const std::string &file_name, bool looped, uint32_t offset, boost::intrusive_ptr<IXAudio2> xaudio) : looped_(looped), offset_(offset), buffer_queue_(), ogg_(file_name) {
    setVoice(createSourceVoice(xaudio, ogg_.get().format, this));
    init();
}

void WINAPI OggStreamingImpl::OnStreamEnd() {
}

void WINAPI OggStreamingImpl::OnVoiceProcessingPassEnd() {
}

void WINAPI OggStreamingImpl::OnVoiceProcessingPassStart(UINT32) {
}

void WINAPI OggStreamingImpl::OnBufferEnd(void *) {
    buffer_queue_.pop();
}

void WINAPI OggStreamingImpl::OnBufferStart(void *) {
    init();
}

void WINAPI OggStreamingImpl::OnLoopEnd(void *) {
}

void WINAPI OggStreamingImpl::OnVoiceError(void *, HRESULT) {
    throw std::runtime_error("ストリーミング再生中にエラーが発生");
}

void OggStreamingImpl::init() {
    loadPcmBuffer();
    const XAUDIO2_BUFFER buffer = {
        XAUDIO2_END_OF_STREAM,
	buffer_queue_.back().size(),
	reinterpret_cast<const BYTE *>(&buffer_queue_.back().front()),
	0,
        0,
        XAUDIO2_NO_LOOP_REGION,
        0,
        0,
        nullptr
    };
    submit(buffer);
}

void OggStreamingImpl::loadPcmBuffer() {
    assert(ogg_.get().ovf.get());
    buffer_queue_.push(std::vector<char>());	
    buffer_queue_.back().resize(kBufferSize);
    int request_size(kBufferSize);
    int bit_stream(0);
    long read_size(0);
    uint32_t com_size(0);
    for(;;) {
	read_size = ov_read(ogg_.get().ovf.get(), static_cast<char *>(&buffer_queue_.back().front()) + com_size, request_size, 0, 2, 1, &bit_stream);
        // 曲が終わってた
	if (!read_size) {
            if (looped_) {
                // イントロを抜かした位置にシーク
	        ov_pcm_seek(ogg_.get().ovf.get(), offset_);
            } else {
                // 何も書かずにループを抜ける
                break;
            }
	}
        // 書き込んだサイズを加える
	com_size += read_size;
        // 波形データ配列の次の書き込み位置がバッファサイズを超えてたらループを抜ける
	if (com_size >= buffer_queue_.back().size()) {
	    break;
	}
        // バッファを全部書き込んでなかったら次の書き込み要求サイズをバッファサイズから書き込んだサイズを引いたものにする
	if (buffer_queue_.back().size() - com_size < 4096) {
	    request_size = buffer_queue_.back().size() - com_size;
	}
    }
}

このクラスもSoundImplクラスを継承します。
OggStreamingImpl::loadPcmBufferメソッドでOggVorbis_Fileから配列を取りだす処理を行っています。ov_read関数にOggVorbis_Fileと書き込み先の配列、書き込みたいサイズ等を送ると配列に波形データを書き込んでくれます。書き込みたいサイズに余り大きなサイズをいれても残念なことになるのでおとなしく4096位にしておきましょう。ファイルの終端にいったら0を返すのでこれがきたらループ再生する場合はイントロ部をぬかした部分に読み込み場所をシークします。読み込み場所のシークにはov_pcm_seek関数を使います。ov_read関数は、書き込みたいサイズよりも小さいサイズ分しか書き込んでくれない可能性もあります。ov_read関数の返り値が実は読み込んだサイズを返したりもしているので、これが4096に満たない場合は、差分をもう一回読み込むんで貰うようこれらの処理をループさせます。4096以上書き込んだらループを抜けます。あと、ファイル終端に行っていて、ループ再生をしない場合もループを抜けます。
コンストラクタでは、ループするかの指定の保存、イントロ部の長さの保存、波形データの配列のキュー、同じファイルで共有したStreamingBufferクラスを持ちます。OggStreamingImpl::initメソッドを使って、初回の音声を登録します。
ここで注目なのが、SoundImplクラス意外にもIXAudio2VoiceCallbackクラスを継承している点です。この子がIXAudio2SourceVoice::SubmitSourceBufferで登録した音声を再生し始めた時、し終わった時に、OnBufferStart、OnBufferEndメソッドを自動的に呼び出してくれる等便利なコールバック的な物を実現してくれます。こいつを使ってやればストリーミング再生にマルチスレッドは不要になります。こいつを使うにはIXAudio2::CreateSourceVoiceメソッドで使うIXAudio2VoiceCallbackクラスのインスタンスを登録してやる必要があります。というわけでcreateSourceVoice関数に自身のポインタを渡して登録させています。今回はOnBufferStart、OnBufferEndの他に再生中に重大なエラーが発生した時に呼ばれるOnVoiceErrorメソッドも実装しておきました。ただ例外を投げるだけです。OnBufferStartメソッドが呼ばれた時、次の音声をIXAudio2SourceVoice::SubmitSourceBufferメソッドを使って登録するためにOggStreamingImpl::initメソッドを呼びます。OggStreamingImpl::initメソッドでは、OnBufferEndメソッドが呼ばれるまでは、波形データ配列を破棄してはいけないので、波形データの配列のquaueに波形データの配列をpushして、これに実際の波形を書き込みます。OnBufferEndメソッドが呼ばれたら、今まで再生していた波形データの配列を破棄してもいいので、波形データの配列のquaueをpopします。XAudio2はIXAudio2SourceVoice::SubmitSourceBufferメソッドを使って登録した音声を順番に勝手に再生していってくれるので、ストリーミング再生チックなことが簡単に出来ます。
これでストリーミング再生の基盤が整いました。注意しておくことはIXAudio2VoiceCallbackのメソッドに関しては何もしないメソッドがあっても全てをオーバーライドすることと、呼び出し規約としてWINAPIを付けておくことです。
あとはSoundDeviceクラスに、こいつを作って返すメソッドを追加すれば完成です。

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

class SoundDevice : private boost::noncopyable {
public:
    // インターフェイス
    Sound createStreamingFromFile(const std::string &file_name, bool looped, uint32_t offset);
private:
    // 内部実装関係
};

// sounddevice.cc
Sound SoundDevice::createStreamingFromFile(const std::string &file_name, bool looped, uint32_t offset) {
    assert(xaudio_.get());
    return std::make_shared<OggStreamingImpl>(file_name, looped, offset, xaudio_);
}

TestApplicationクラスに以下の様に組み込めば、oggファイルをストリーミング再生できます。

// 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_->createStreamingFromFile("test.ogg", false, 0)) {
    sound_.play();
}