この記事では下記2つの情報を参考にしています。
- http://marupeke296.com/OGG_main.html
- http://gameworkslab.jp/category/program/%e3%82%b5%e3%82%a6%e3%83%b3%e3%83%89/
C++でOggの再生、また、XAudio2の情報は今でも多いとは言えません。
上記URL1では、メモリからOggファイルを
再生するための方法が記述されている数少ない資料なのですが、
AudioがDirectSoundを利用したものになっています。
これをXAudio2で再生しようとした場合に
変更できなくて挫折してしまう人が多そうな気がしました。
そのため、今回は上記サイト情報を元にXAudio2で
同様にOggファイルをメモリから読み込んだ上で
再生する処理を記述したいと思います。
理解のためにコード量、ファイル数をさらに削っています。
Oggライブラリの準備や、XAudioの下準備や理解は上記URL等を参考にしてください。
まず、PCMDecoder.hを用意します。 この記事では処理理解重視のために、
ファイル数を減らすため、ヘッダファイルのみに記述していますが、
実装の際はcppと分けて記述する等はお任せします。
#pragma once
#include <Windows.h>
class PCMDecoder
{
public :
PCMDecoder() {clear();}
virtual ~PCMDecoder(){}
virtual bool getSegment(char* buffer, unsigned int size, unsigned int* writeSize, bool * isEnd) = 0;
virtual void setHead() = 0;
virtual bool setSound(const char* filename) = 0;
virtual PCMDecoder* clone() = 0;
bool isLoop() { return m_isLoop; }
bool isReady() { return m_isReady; }
void SetFileName (const char* pFileName) { strcpy(m_filePath, pFileName);}
void SetLoop (bool bLoop) { m_isLoop = bLoop; }
void SetReady (bool isReady) { m_isReady = isReady;}
void SetChannelNum (unsigned int channelNum) { m_channelNum = channelNum;}
void SetSamplingRate (unsigned int samplingRate) { m_SamplingRate = samplingRate;}
void SetBitRate (unsigned int bitrate) { m_bitRate = bitrate;}
const char* getFileName(){ return m_filePath; }
virtual void clear()
{
m_isLoop = false;
m_isReady = false;
m_channelNum = 0;
m_SamplingRate = 0;
m_bitRate = 0;
memset(m_filePath, 0, 256);
}
bool getWaveFormatEx(WAVEFORMATEX& waveFormatEx)
{
if (!isReady()){ return false;}
waveFormatEx.wFormatTag = WAVE_FORMAT_PCM;
waveFormatEx.nChannels = m_channelNum;
waveFormatEx.nSamplesPerSec = m_SamplingRate;
waveFormatEx.wBitsPerSample = m_bitRate;
waveFormatEx.nBlockAlign = m_channelNum * m_bitRate / 8;
waveFormatEx.nAvgBytesPerSec = waveFormatEx.nSamplesPerSec * waveFormatEx.nBlockAlign;
waveFormatEx.cbSize = 0;
return true;
}
private:
bool m_isLoop;
bool m_isReady;
char m_filePath[256];
unsigned int m_channelNum;
unsigned int m_SamplingRate;
unsigned int m_bitRate;
};
書けたらこれを継承した、OggDecoder クラスを作ります。 メモリから読み込むのは資料準拠で別クラスで継承するようにします。
#pragma once
#include "PCMDecoder.h"
#include "vorbis/vorbisfile.h"
#include <assert.h>
class OggDecoder : public PCMDecoder
{
public:
OggDecoder(const char* pFileName)
{
memset(&m_ovf, 0, sizeof(m_ovf));
setSound(pFileName);
}
OggDecoder() { memset(&m_ovf, 0, sizeof(m_ovf)); }
virtual ~OggDecoder(){ clear(); }
virtual void clear()
{
if (!isReady())
{
ov_clear(&m_ovf);
}
memset(&m_ovf, 0, sizeof(m_ovf));
PCMDecoder::clear();
}
virtual void setHead()
{
if (!isReady()) return;
ov_time_seek(&m_ovf, 0.0);
}
virtual bool setSound(const char* filename)
{
clear();
if (ov_fopen((char*)filename, &m_ovf) != 0)
return false;
SetFileName(filename);
vorbis_info* info = ov_info(&m_ovf, -1);
SetChannelNum(info->channels);
SetBitRate(16);
SetSamplingRate(info->rate);
SetReady(true);
return true;
}
virtual PCMDecoder* clone(){ return isReady() ? new OggDecoder(getFileName()) : 0;}
virtual bool getSegment(char* buffer, unsigned int size, unsigned int * writeSize, bool * isEnd)
{
if (!isReady()) return false;
if(buffer == 0)
{
if (isEnd) *isEnd = true;
if (writeSize) * writeSize = 0;
return false;
}
if (isEnd) *isEnd = false;
memset(buffer, 0, size);
const unsigned int requestSize_base = 4096 ; // 読み込み単位
unsigned int requestSize = requestSize_base;
int bitstream = 0;
int readSize = 0;
unsigned int comSize = 0;
bool isAdjust = false;
if (size < requestSize)
{
requestSize = size;
isAdjust = true;
}
while (1)
{
readSize = ov_read(&m_ovf, (char*)(buffer + comSize), requestSize, 0, 2, 1, &bitstream);
if (readSize == 0)
{
if (isLoop() == true)
{
ov_time_seek(&m_ovf, 0.0);
}
else
{
if (isEnd) *isEnd = true;
if(writeSize) *writeSize = comSize;
return true;
}
}
comSize += readSize;
assert(comSize <= size); // バッファオーバー
if (comSize >= size)
{
if (writeSize) *writeSize = comSize;
return true;
}
if (size - comSize < requestSize_base)
{
isAdjust = true;
requestSize = size - comSize;
}
}
if (writeSize) *writeSize = 0;
return false;
}
protected:
OggVorbis_File m_ovf;
};
以下はメモリから読み込む場合のクラスです
#pragma once
#include "OggDecoder.h"
class OggDecoderInMemory : public OggDecoder
{
public:
OggDecoderInMemory():m_size(0), m_curPos(0){}
OggDecoderInMemory(const char* filepath) {setSound(filepath);}
virtual ~OggDecoderInMemory() { clear(); }
virtual void clear()
{
if (m_buffer)
{
delete[] m_buffer;
m_buffer = 0;
m_size = 0;
m_curPos = 0;
}
OggDecoder::clear();
}
static OggDecoderInMemory* getMyPtr(void* stream)
{
OggDecoderInMemory* p = 0;
memcpy(&p, stream, sizeof(OggDecoderInMemory*));
return p;
}
static size_t read(void* buffer, size_t size, size_t maxCount, void* stream)
{
if (buffer == 0) return 0;
OggDecoderInMemory* p = (OggDecoderInMemory*)stream;
int resSize = p->m_size - p->m_curPos;
size_t count = resSize / size;
if (count > maxCount)
{
count = maxCount;
}
memcpy(buffer, p->m_buffer + p->m_curPos, size * count);
p->m_curPos += size * count;
return count;
}
static int seek(void* buffer, ogg_int64_t offset, int flag)
{
OggDecoderInMemory* p = (OggDecoderInMemory*)buffer;
switch (flag)
{
case SEEK_CUR: p->m_curPos += offset; break;
case SEEK_END: p->m_curPos = p->m_size + offset; break;
case SEEK_SET: p->m_curPos = offset; break;
default: return -1;
}
if (p->m_curPos > p->m_size)
{
p->m_curPos = p->m_size;
return -1;
}
else if(p->m_curPos < 0)
{
p->m_curPos = 0;
return -1;
}
return 0;
}
static int close(void* buffer) { return 0; }
static long tell(void* buffer) { return ((OggDecoderInMemory*)buffer)->m_curPos; }
virtual bool setSound(const char* fileName)
{
clear();
FILE* pFile = fopen(fileName, "rb");
if (pFile == 0)
{
return false;
}
fseek(pFile, 0, SEEK_END);
m_size = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
m_buffer = new char[m_size + sizeof(OggDecoderInMemory*)];
OggDecoderInMemory* p = this;
memcpy(m_buffer, &p, sizeof(OggDecoderInMemory*));
size_t readSize = fread(m_buffer + sizeof(OggDecoderInMemory*), m_size, 1, pFile);
if (readSize != 1)
{
clear();
return false;
}
ov_callbacks callbacks =
{
&OggDecoderInMemory::read,
&OggDecoderInMemory::seek,
&OggDecoderInMemory::close,
&OggDecoderInMemory::tell
};
if (ov_open_callbacks(this, &m_ovf, 0, 0, callbacks) != 0)
{
clear();
return false;
}
SetFileName(fileName);
vorbis_info *info = ov_info(&m_ovf, -1);
SetChannelNum(info->channels);
SetBitRate(16);
SetSamplingRate(info->rate);
SetReady(true);
return true;
}
PCMDecoder* clone()
{
if (!isReady()) return 0;
OggDecoderInMemory* cloneobj = new OggDecoderInMemory();
*cloneobj = *this;
cloneobj->m_curPos = 0;
ov_callbacks callbacks =
{
&OggDecoderInMemory::read,
&OggDecoderInMemory::seek,
&OggDecoderInMemory::close,
&OggDecoderInMemory::tell
};
if (ov_open_callbacks(cloneobj, &cloneobj->m_ovf, 0, 0, callbacks) != 0)
{
cloneobj->clear();
return false;
}
cloneobj->SetFileName(getFileName());
cloneobj->SetReady(true);
return cloneobj;
}
private:
char* m_buffer;
int m_size;
long m_curPos;
};
これらをXAudio2で再生するPCMPlayerクラスを作ります。
#pragma once
#include "PCMDecoder.h"
#include <process.h>
#include <xaudio2.h>
class PCMPlayer
{
public:
enum STATE
{
STATE_NONE,
STATE_PLAY,
STATE_PAUSE,
STATE_STOP
};
PCMPlayer() { clear(); }
PCMPlayer( PCMDecoder* pDecoder, IXAudio2* pXaudio = nullptr):m_pPCMDecoder(pDecoder),m_pXaudio(pXaudio)
{
clear();
setDecoder(pDecoder);
}
~PCMPlayer() { TerminateThread();}
void TerminateThread()
{
m_isTerminate = true;
bool end = false;
while (!end && m_threadHandle != 0)
{
DWORD flag = WaitForSingleObject((HANDLE)(__int64)m_threadHandle, 100);
switch(flag)
{
case WAIT_FAILED: end = true; break;
case WAIT_OBJECT_0: end = true; break;
case WAIT_TIMEOUT: break;
default: break;
}
}
m_isTerminate = false;
m_threadHandle = 0;
}
void clear()
{
TerminateThread();
memset(&m_waveFormat, 0, sizeof(m_waveFormat));
m_isReady = false;
m_isLoop = true;
m_state = STATE_NONE;
if (m_pSourceVoice)
{
m_pSourceVoice->Stop();
m_pSourceVoice->DestroyVoice();
}
if (m_SoundBuffer[0] != nullptr)
{
delete (m_SoundBuffer[0]);
m_SoundBuffer[0] = nullptr;
}
if (m_SoundBuffer[1] != nullptr)
{
delete (m_SoundBuffer[1]);
m_SoundBuffer[1] = nullptr;
}
}
bool initializeBuffer()
{
if (m_pPCMDecoder == 0 || m_pPCMDecoder == nullptr) return false;
m_pPCMDecoder->setHead();
return true;
}
static void __cdecl streamThread(void* playerPtr)
{
PCMPlayer* player = (PCMPlayer*)playerPtr;
while (player->m_isTerminate == false)
{
switch (player->getState())
{
case STATE_PLAY:
XAUDIO2_VOICE_STATE state;
player->m_pSourceVoice->GetState(&state);
if (state.BuffersQueued <= 1)//再生キューに常に2つのバッファを溜めておく
{
if (player->m_is_end && player->m_isLoop)
{
player->m_pPCMDecoder->setHead();
}
player->add_next_buffer();
}
break;
case STATE_STOP:break;
case STATE_PAUSE:break;
default:break;
}
Sleep(100);
}
}
HWND GetConsoleHwnd(void)
{
TCHAR pszWindowTitle[1024];
GetConsoleTitle(pszWindowTitle, 1024);
return FindWindow(NULL, pszWindowTitle);
}
bool setDecoder(PCMDecoder* pCMDecoder)
{
if ( m_pPCMDecoder == 0 || m_pPCMDecoder == nullptr)
{
m_isReady = false;
return false;
}
m_state = STATE_STOP;
if (!pCMDecoder->getWaveFormatEx(m_waveFormat))
{
return false;
}
m_pPCMDecoder = pCMDecoder;
if (FAILED(m_pXaudio->CreateSourceVoice(&m_pSourceVoice, &m_waveFormat)))
{
return false;
}
int dataSize = m_waveFormat.nAvgBytesPerSec * m_playTime;
m_SoundBuffer[0] = (char*)malloc(dataSize);
m_SoundBuffer[1] = (char*)malloc(dataSize);
PCMPlayer* player = (PCMPlayer*)this;
player->m_pPCMDecoder->getSegment(m_SoundBuffer[m_cursor], dataSize , &m_writeSize, &m_is_end);
XAUDIO2_BUFFER buf{};
buf.pAudioData = (BYTE*)m_SoundBuffer[0];
buf.Flags = XAUDIO2_END_OF_STREAM;
buf.AudioBytes = dataSize;
m_cursor = m_cursor == 0 ? 1 : 0;
if (!initializeBuffer()) return false;
if (m_threadHandle == 0)
{
m_threadHandle = (unsigned int)_beginthread(PCMPlayer::streamThread, 0, (void*)this);
}
m_isReady = true;
return true;
}
void setDevice(IXAudio2* pDevice) { m_pXaudio = pDevice; }
STATE getState() { return m_state; }
void add_next_buffer(void)
{
int dataSize = m_waveFormat.nAvgBytesPerSec * m_playTime;
m_pPCMDecoder->getSegment(m_SoundBuffer[m_cursor], dataSize, &m_writeSize, &m_is_end);
XAUDIO2_BUFFER buf{};
buf.pAudioData = (BYTE*)m_SoundBuffer[m_cursor];
buf.Flags = XAUDIO2_END_OF_STREAM;
buf.AudioBytes = m_writeSize;
m_pSourceVoice->SubmitSourceBuffer(&buf);
m_cursor = m_cursor == 0 ? 1 : 0;
}
bool isReady() { return m_isReady; }
bool isPlaying() { return m_state == STATE_PLAY; }
bool play(bool isLoop)
{
if (!m_isReady) return false;
m_isLoop = isLoop;
m_pPCMDecoder->SetLoop(isLoop);
m_pSourceVoice->Start();
m_state = STATE_PLAY;
return true;
}
void pause()
{
if (m_state != STATE_PLAY)
{
play(m_isLoop);
return;
}
m_pSourceVoice->Stop();
m_state = STATE_PAUSE;
}
void stop()
{
if (!isReady()) return;
if (m_state == STATE_STOP) return;
m_state = STATE_STOP;
clear();
setDecoder(m_pPCMDecoder);
}
void setVolume(float volume)
{
if (!isReady()) return;
m_pSourceVoice->SetVolume(volume);
}
private:
PCMDecoder* m_pPCMDecoder; //デコーダ情報(ogg,waveファイル等を想定)
WAVEFORMATEX m_waveFormat; //WAVEFORMATEX構造体
unsigned int m_threadHandle = 0; //ストリーム再生スレッドハンドル
bool m_isTerminate = false; //スレッド停止
bool m_isReady; //準備できた?
bool m_isLoop; //ループする?
STATE m_state; //再生状態
unsigned int m_writeSize = 0; //実際にバッファに書き込まれた量
unsigned int m_cursor = 0; //サウンドダブルバッファのどちらを見てるか
bool m_is_end = false; //ストリームの終了タイミング
const unsigned int m_playTime = 1; //1バッファ毎に何秒分データ保持するか
char* m_SoundBuffer[2]; //サウンドダブルバッファ用
IXAudio2* m_pXaudio; //XAudio2のポインタ
IXAudio2SourceVoice* m_pSourceVoice = nullptr; //音源
};
準備は終わりです。以下はメインプログラムで利用する例です。
#define _CRT_SECURE_NO_WARNINGS
#pragma comment ( lib, "libogg_static.lib" )
#pragma comment ( lib, "libvorbis_static.lib" )
#pragma comment ( lib, "libvorbisfile_static.lib" )
#include "OggDecoder.h"
#include "OggDecodeInMemory.h"
#include "PCMPlayerh.h"
IXAudio2*g_pXaudio = nullptr;
IXAudio2MasteringVoice* g_pMasteringVoice = nullptr;
bool CreateAudio()
{
if (g_pXaudio != nullptr || g_pMasteringVoice != nullptr) { return false;}
if (FAILED(CoInitializeEx(0, COINIT_MULTITHREADED))) { return false;}
if (FAILED(XAudio2Create(&g_pXaudio))) { return false;}
if (FAILED(g_pXaudio->CreateMasteringVoice(&g_pMasteringVoice))){ return false;}
return true;
}
void DestroyAudio()
{
if (g_pMasteringVoice){
g_pMasteringVoice->DestroyVoice();
g_pMasteringVoice = nullptr;
}
if (g_pXaudio){
g_pXaudio->Release();
g_pXaudio = nullptr;
}
CoUninitialize();
}
void StopProc()
{
while (1){ Sleep(200); if (GetAsyncKeyState(VK_ESCAPE)) { break; }}
}
int main(int argc, char* argv[])
{
CreateAudio();
OggDecoderInMemory* pDecoder = new OggDecoderInMemory("Test.ogg"); //メモリからロードしたい
//auto* pDecoder = new OggDecoder("Test.ogg"); //ファイルから普通にロードしたいときはこちら
PCMPlayer* player = new PCMPlayer(pDecoder, g_pXaudio);
bool isLoop = true;
player->setDevice(g_pXaudio);
player->setDecoder(pDecoder);
if (player->play(isLoop) == false)
{
printf("failed \n");
}
StopProc(); //ESCAPEキーがあるまで待つ
player->stop();
printf("\n停止しました\n");
DestroyAudio();
delete pDecoder;
delete player;
return 0;
}
Test.ogg ファイルは各自でご用意をお願いします
リポジトリ
https://github.com/bregade/WhiteSkelton/tree/master/XAudio2OggStreaming
ストリーミング再生と思っていたら、DirectSoundから移した仮定で、
ファイルバッファの最後のサイズまで与えていた状態だったため、文言を修正しました。