SFML には 音を鳴らすための機能として2つのクラスがあります。
sf::Sound と sf::Music です。どっちも大体同じ使い方なんですが、内部の仕組みが違います。
sf::Sound は軽量です。音のデータは sf::SoundBuffer という別のクラスにロードしておいて、それを演奏します。
メモリに収まる程度の小さなサイズの音を鳴らすのに使うとよいです。
たとえばピストルの音とか足音とか。読み込んでから鳴らすまで、タイムラグが発生しない程度の。
sf::Music はストリームです。音のデータ全体をメモリに読み込むわけではなく、
ファイルから、すぐにデータを取り出します。
このクラスは、たとえば、何分も続くような音楽を鳴らすのに使うとよいです。
ファイル全体を読み込んでいたら時間がかかる上にメモリを何百Mバイトも食ってしまうような。
上に書いたように、音のデータは sf::Sound クラスに直接入るのではなくて、
sf::SoundBuffer という別のクラスに入ります。
このクラスで音声のデータがカプセル化されます。
データの形式は基本的には符号付16ビット整数の配列です(いわゆる「オーディオサンプル」)。
1つ1つのサンプルは、その時点での音声信号の振幅です。
要するに、サウンド全体は、1つ1つのサンプルが並んだもの、ということですね。
sf::Sound と sf::SoundBuffer の関係は、
ちょうどグラフィックモジュールの sf::Sprite と sf::Texture と同じです。
なので、スプライトとテクスチャの使い方をすでに知ってるなら、
サウンドとサウンドバッファも同じような考え方で使えます。
サウンドバッファをハードディスク上のファイルから読み込むには loadFromFile関数を使います。
#include <SFML/Audio.hpp>
int main()
{
sf::SoundBuffer buffer;
if (!buffer.loadFromFile("sound.wav"))
return -1;
...
return 0;
}
いつものパターンですが、音声ファイルはメモリからも(loadFromMemory関数)、
ストリームからも(loadFromStream関数)読み込めます。
フォーマットも、大体みなさんがご存知のものは使えます。一覧は APIドキュメントをご覧くださいませ。
オーディオサンプルのリストからバッファを直接作ることもできます。
これで、どんな元データからでもサウンドを作れますね。
std::vector samples = ...;
buffer.loadFromSamples(&samples[0], samples.size(), 2, 44100);
ファイルから読み込むときとは違って、loadFromSamples関数は生のオーディオサンプルを読み込むので、
サウンド全体の情報を得るために引数が多めにあります。
1つ目(第3引数)は、チャンネル番号です。1はモノラル、2はステレオ、など。
2つ目(第4引数)は、サンプリングレートです。1秒間にいくつのサンプルが演奏されるのかを指定します。
こういう情報を指定することで、元のサウンドがどんな状態だったのかを再現できるようになります。
さて、オーディオデータがロードできたので、sf::Sound で演奏してみましょう。
sf::SoundBuffer buffer;
// ……サウンドバッファにオーディオデータを読み込む……
sf::Sound sound;
sound.setBuffer(buffer);
sound.play();
SFML のクールなところは、同じサウンドバッファを別々のサウンドクラス(sf::Sound)にセットできることです。
同時に演奏しても問題なし。
サウンド(音楽も)は別のスレッドで演奏されます。
なので、play関数を呼んだ後、他の処理を実行できます(サウンドクラスのインスタンスやデータが破棄されなければ)。
演奏は、最後まで行くか、意図的に止めるまで続きます。
sf::Musicクラスは sf::Sound とは違って、オーディオデータを前もって読み込むのではなく、ストリームを直接開きます。
なので、初期設定もシンプルです。
sf::Music music;
if (!music.openFromFile("music.ogg"))
return -1; // error
music.play();
関数名に注目。
SFMLのほかの読み込み関数と違って、関数名が openFromFile ですよね。loadFromFrile ではなく。
なぜかというと、実際に音楽データをロードするわけではないから、です。
この関数がしているのは、単にストリームを開いているだけです。音楽が演奏される時点で、実際にロードされます。
そんなわけで、注意点。
元になるオーディオファイルは、演奏が続いている間ずっと存在している必要があります。
sf::Musicクラスの他の読み込み関数(openFromMemory関数、openFromStream)も、同じです。
オーディオデータを読み込んでサウンドや音楽を鳴らせるようになった!
では、それで どんなことができるのか見ていきましょう。
下記の関数で演奏をコントロールできます。
- play関数 : 演奏を開始、または再開
- pause関数 : 演奏を一時停止
- stop関数 : 演奏を停止して、現在位置を0に戻す
- setPlayingOffset関数 : 演奏の現在位置をセット
使い方の例:
// 演奏開始
sound.play();
// 2秒進める
sound.setPlayingOffset(sf::seconds(2));
// 演奏を一時停止
sound.pause();
// 演奏再開
sound.play();
// 演奏を停止して、現在位置を0に戻す
sound.stop();
getStatus関数で サウンドや音楽の現在の状態、を取得できます。
演奏中か、停止しているか、一時停止中か、が分かる、ということです。
そのほか、サウンドと音楽には属性がいくつかあって、いつでも設定を変更できます。
「ピッチ」
周波数に関係する値です。
1より大きいと音が高くなります。1より小さいと、音が低くなります。1だと変化なしです。
ピッチを変更すると、副作用として、演奏の速度が変化します。
sound.setPitch(1.2);
「ボリューム」
……ボリュームです。
値の範囲は 0~100です(0だと無音。100が最大)。デフォルトは100。
なので、初期状態よりも大きい音にすることはできません。
sound.setVolume(50);
「ループ」
サウンドまたは音楽が自動的にループするかどうかの設定です。
ループする設定の場合、最後まで演奏された時点で、最初から再演奏されます。
stop関数を呼び出すまで、何回でも再演奏されます。
ループしない設定の場合、最後まで演奏すると自動的に停止します。
sound.setLoop(true);
属性は他にもあります。が、
特殊な設定なので、別のチュートリアルでお話します。
サウンドバッファを破棄してしまう
サウンドが、まだサウンドバッファを使う可能性があるのに、サウンドバッファをスコープの外に出してしまう(すると破棄される)、
というのはよくあるミスです。
sf::Sound loadSound(std::string filename)
{
sf::SoundBuffer buffer; // ローカル変数なので、関数が終了すると破棄される……
buffer.loadFromFile(filename);
return sf::Sound(buffer);
} // ... here
sf::Sound sound = loadSound("s.wav");
sound.play(); // ERROR: サウンドバッファはすでに存在しない。実行結果はパルプンテ……。
サウンド は サウンドバッファへの「ポインタ」を持ってるだけです。
サウンドバッファそのもののコピー を持っているわけではないのです。
なので、プログラマは、サウンドバッファがいつ生成されて、いつ破棄されるのかを、しっかりと管理して、
サウンドに使われる可能性のある間は、バッファが存在しているようにしないといけません。
サウンドを作りすぎる
もう1つのよくあるエラーは、サウンドを作りすぎることです。
SFML では内部的に制限があります。OS によって異なるんですが、256個以上は作らない方がいいです。
この制限数は sf::Soundと sf::Music の数を合わせた数です。
制限を越えないようにするには、使われていないサウンドを破棄(またはリサイクル)するとよいです。
終了したり使われなくなったサウンドインスタンスをそのままにしておかない。
もちろん、こういう対策は、大量のサウンドや音楽を扱う必要があるときだけです。
ストリーム演奏中に元のデータを消してしまう
sf::Music は演奏が続いている間、元になるデータが必要なのでしたよね?
元になるデータがファイルの場合は、アプリケーションの実行中にファイルを消したり移動したりすることは滅多にないと思いますが、
ファイルではなくメモリやストリームから音楽データを取得している場合は、話がややこしくなります。
// メモリから音楽データを取得(たとえば zipファイルから抽出、とか)
std::vector fileData = ...;
// 演奏
sf::Music music;
music.openFromMemory(&fileData[0], fileData.size());
music.play();
// よーし、もう元のデータは不要だよね?
fileData.clear();
// エラー! 元のデータとのストリーミングは継続中。この後どうなるかはパルプンテ。
sf::Music はコピー不可
最後に、もう1つの失敗例を小耳に挟んでおいてくださいませ。
sf::Musicクラスは
コピー不可です。
なので、次のようなことはできません。
sf::Music music;
sf::Music anotherMusic = music; // ERROR
void doSomething(sf::Music music)
{
...
}
sf::Music music;
doSomething(music); // ERROR (この関数の引数は値ではなく参照で渡すべし)