sf::Music クラスを覚えてるかな?
オーディオストリームもそれと似たようなものです。
違いは1つ。オーディオストリームは音声ファイルを再生しません。
音声ファイル以外のデータのストリーム再生をします。
ネットワークを通したストリーミング、プログラム内で生成された音声データ、SFML でサポートしてない音声形式、などなど。
何を隠そう、sf::Music クラスもオーディオストリームクラスの一種です。
この子は "音声サンプルを「ファイル」から取得してストリーム再生をする、オーディオストリームクラス" なのです。
さて、今回は「ストリーミング再生」に関するお話です。
つまり、一度に全部をメモリにロードできないような大きいサイズの音声データを、小さなチャンクに分けてロードしながら再生する、
という処理のお話です。
なので、メモリに1回でロードできるぐらいの小さなサイズの音声を扱う場合は、
オーディオストリームの出番はありません。
そういう音声データなら
sf::SoundBuffer クラスに読み込んで、
通常の
sf::Sound クラスで再生するとよいです。
ではさっそく、オーディオストリームのクラスを作っていきましょう。
まずは 抽象クラス
sf::SoundStream を継承したクラスを用意するのです。
オーバーライドするべき仮想関数は2つ。
onGetData() と onSeek() です。
class MyAudioStream : public sf::SoundStream
{
virtual bool onGetData(Chunk& data);
virtual void onSeek(sf::Time timeOffset);
};
onGetData() はベースクラスが音声サンプルを読み切って、
さらに必要なときに、ベースクラスから毎回呼ばれます。
引数で渡された "data" へ、新たな音声サンプルを入れてあげましょう:
bool MyAudioStream::onGetData(Chunk& data)
{
data.samples = /* 新たな音声サンプルへのポインタを記述 */;
data.sampleCount = /* チャンク内の音声サンプル数を記述 */;
return true;
}
処理がうまくいったならば、true をリターンしてあげましょう。
エラーのとき、またはオーディオデータを最後まで読み込み終わったときには false をリターンしましょう。再生停止、ということです。
onGetData() のリターンの直後、SFML は音声サンプルのコピーを内部に作成します。
なので、必要ないなら元のデータを手元に保存しておかなくても大丈夫です。
onSeek() 関数は、パブリック関数 setPlayingOffset() が呼ばれたときに呼ばれます。
現在の再生ポジションを変更するためです。
引数は新たな再生ポジション(時間)を表してます。サウンドの冒頭からの時間です(現在位置からの差分ではなく)。
オーディオデータの性質によっては、この関数の中身は実装できないことがあります。
その場合は中身を空のままにしておいてください。
そして、クラスの仕様として、再生ポジションの変更はサポートされてないことにしておくのです。
さて、オーディオストリームクラスが大体出来上がってきました。
あと、
sf::SoundStream抽象クラス が知りたがってる情報は2つ。
チャンネル数と、サンプリングレートです。
これらのデータがそろうことで、期待通りの動作をしてくれます。
ベースクラスにこのパラメータを伝えるには、initialize() 関数を使います。
チャンネル数およびサンプリングレートが判明次第、できるだけ早めのタイミングで実行しましょう
(ストリームがロード or 初期化されたタイミングなど)。
// どんなタイミングでこの処理をするかは、キミが作るストリームクラスの目的次第です。
unsigned int channelCount = ...;
unsigned int sampleRate = ...;
initialize ( channelCount, sampleRate );
オーディオストリームは内部的に別スレッドで実行されます。
実際のところ、どこでどんな処理が行われているのかな?
onSeek() は パブリック関数の setPlayingOffset() から直接呼ばれます。
つまり、onSeek() は、オーディオストリームクラスを使っているスレッドと同じスレッドで実行されるということです。
一方、onGetData() は ストリーミングが続いている間、SFML が内部で作るスレッドから、繰り返し呼ばれ続けます。
なので、キミが作るオーディオストリームクラスが、クラスの呼び出し元スレッドからも、内部の再生スレッドからも、
同時にアクセスされる可能性のあるデータを扱っている場合は、
アクセスが衝突しないようにそのデータを保護してあげる必要があります(ミューテックスなどの排他制御で)。
アクセスが衝突すると、思いがけない事件が起きてしまうかもしれません(再生データが壊れるとか、クラッシュするとか)。
スレッドについて、もっとよく知りたいキミは
スレッドのチュートリアルを読んでくださいませ。
さて、これでキミの独自のオーディオストリームクラスの出来上がりです。
さぁ、どうやって使えばいいのかな?
実は、そのへんのお話は
sf::Music のチュートリアルとほとんど同じです。
play()、pause()、stoop()、setPlayingOffset() といった関数で再生処理を制御できます。
ボリュームやピッチの指定もできます。
詳しくは APIドキュメントやオーディオ関連の他のチュートリアルを読んでね。
カスタムオーディオストリームのとってもシンプルなサンプルです。
音声ファイルを読み込んで、サウンドバッファのデータを再生します。
いわゆる車輪の再発明なのですが、あくまでサンプルなので、動作の仕組みに注目するのだ。
音声データの出所に関わらず(ファイル以外でも同じ)、どんなふうにデータがストリーミングされるのかな?
#include <SFML/Audio.hpp>
#include <vector>
// 読み込み済みのサウンドバッファを再生するカスタムオーディオストリームクラス
class MyStream : public sf::SoundStream
{
public:
void load ( const sf::SoundBuffer& buffer )
{
// 音声サンプルをバッファから抽出して、コンテナに乗せる
m_samples.assign (
buffer.getSamples(),
buffer.getSamples() + buffer.getSampleCount()
);
// 現在の再生ポジションをリセット
m_currentSample = 0;
// ベースクラスを初期化
initialize ( buffer.getChannelCount(), buffer.getSampleRate() );
}
private:
virtual bool onGetData (Chunk& data )
{
// 関数の呼び出し1回につきストリーミングするサンプル数
// 適当な値にするよりは何らかの固定値にしておく方が安全です。
const int samplesToStream = 50000;
// 次に再生される音声サンプルへのポインタをセット
data.samples = &m_samples[m_currentSample];
// 終点に到達した?
if (m_currentSample + samplesToStream <= m_samples.size())
{
// また終点じゃない。今回のサンプルをストリーミングして、続行。
data.sampleCount = samplesToStream;
m_currentSample += samplesToStream;
return true;
}
else
{
// 終点に達した。未処理のサンプルをストリーミングして、再生を停止。
data.sampleCount = m_samples.size() - m_currentSample;
m_currentSample = m_samples.size();
return false;
}
}
virtual void onSeek(sf::Time timeOffset)
{
// サンプリングレートとチャンネル数に合わせて、サンプルのインデックスを求める。
m_currentSample = static_cast<std::size_t> (
timeOffset.asSeconds() * getSampleRate() * getChannelCount()
);
}
std::vector<sf::Int16> m_samples;
std::size_t m_currentSample;
};
int main()
{
// サウンドファイルからバッファへ読み込む
sf::SoundBuffer buffer;
buffer.loadFromFile("sound.wav");
// 我らのカスタムオーディオストリームを初期化して再生
MyStream stream;
stream.load(buffer);
stream.play();
// 終点に達するまで演奏を続ける。
while ( stream.getStatus() == MyStream::Playing) {
sf::sleep(sf::seconds(0.1f));
}
return 0;
}