目次 - SDL_AudioStreamを使う

SDL_AudioStreamを使う

最初期からSDL 2.0.6まで, SDLによる音声の変換は1つの方法しかなかった: SDL_AudioCVT構造体の使用である.

それは様々な使い方のある便利なAPIだが, いくつかの問題があった:

アプリケーションのオーディオコールバックと, 対象の環境のAPIで行われる, 不定なサイズ, 形式, 時間でのデータの生成, 消費の橋渡しのために, しばらくの間SDLが内部で使っていたよりよいAPIがあった. このAPIはデータの即時の変換と再サンプリングだけでなく, 片方で生成されたデータを別の方が消費するまでバッファすることもできる.

SDL 2.0.7では, この内部APIが整理されアプリケーションで使えるようになった. それがSDL_AudioStreamである.

混乱を避けるために: これは補助的なAPIで, 使わなくても音声の再生と録音は行える. SDLはあなたのコールバックと対象の環境の間のデータ変換をこのAPIを使って暗黙の内に変換するだろう. しかし, それを意識する必要はない. もしあなたがコールバックを使わずに必要なときにSDLに音声データを渡したいならば, それも可能だが, それは別のAPI(SDL_QueueAudio()とその仲間)を使うことになる.

SDL_AudioStreamをすぐに使えるのは次のような場合である:

SDL_AudioStreamの使い方はとても単純だ. まず, それを生成する. 例えば, Sint16, モノラル, 22050Hzのデータとして生成し, Float32, ステレオ, 48000Hzとして消費したいとすると

// Sint16/mono/22050Hzのデータを入れると, Float32/stereo/48000Hzのデータとなって戻ってくる
SDL_AudioStream *stream = SDL_NewAudioStream(AUDIO_S16, 1, 22050, AUDIO_F32, 2, 48000);
if (stream == NULL) {
    printf("うおっ, ストリームの生成に失敗した: %s\n", SDL_GetError());
} else {
    // ストリームを使う準備ができた!
}

後はストリームデータを与えるだけだ!

Sint16 samples[1024];
int num_samples = read_more_samples_from_disk(samples); // 何でもよい
// 与えるデータの(サンプル数ではなく)バイト数を指定する!
int rc = SDL_AudioStreamPut(stream, samples, num_samples * sizeof (Sint16));
if (rc == -1) {
    printf("うおっ, ストリームへのサンプリングデータの書き込みに失敗した: %s\n", SDL_GetError());
    return;
}

// おおっと, 最後に1サンプル追加するのを忘れていた...!
//   あなたは一度にいくらでも書き込むことができる.
//   SDLは必要ならばサイズを増やしながら適切にバッファする.
Sint16 onesample = 22;
SDL_AudioStreamPut(stream, &onesample, sizeof (Sint16));

ストリームにデータを追加すると, SDLはそれも変換し再サンプリングする. あなたはどれだけのデータが変換され使えるようになったか尋ねることができる:

int avail = SDL_AudioStreamAvailable(stream);  // これはサンプル数ではなくバイト数である!
if (avail < 100) {
    printf("あと%dバイトのデータを待っている!\n", 100 - avail);
}

そして十分なデータが得られたら, データを要求した形式で読み出すことができる:

float converted[100];
// これはサンプル数ではなくバイト数である!
int gotten = SDL_AudioStreamGet(stream, converted, sizeof (converted));
if (gotten == -1) {
    printf("うおっ, 変換されたデータの読み込みに失敗した: %s\n", SDL_GetError());
}
write_more_samples_to_disk(converted, gotten); /* 何でもよい */

もちろん読み出しは一度でなくてもよい. 入出力の両ストリームはバッファされており, 少ない量を読み出すことができる:

int gotten;
do {
    float converted[100];
    // これはサンプル数ではなくバイト数である!
    gotten = SDL_AudioStreamGet(stream, converted, sizeof (converted));
    if (gotten == -1) {
        printf("うおっ, 変換されたデータの読み込みに失敗した: %s\n", SDL_GetError());
    } else {
        // gottenの値はSDL_AudioStreamGetへの要求より少ないかもしれない!
        write_more_samples_to_disk(converted, gotten); /* 何でもよい */
    }
} while (gotten > 0);

効率化のヒント: バッファの確保, 変換, 再サンプリングはストリームへの書き込み時に発生する. ストリームからの読み込み時には, 少しの管理上の操作と数回のmemcpy()の呼び出しのみを行っている. そのことを考慮すること.

このインターフェースの落とし穴: 予想した値より少ない(0バイトもありうる!)可能性に注意すること. 再サンプリングのとき, SDLはチャンクに送られたデータはスムーズに再サンプリングするためにバッファにパッディングを確保している. 未来を予測しようとするのではなく, スムーズな音声を維持するために最初にストリームに送られた小さなデータを保持し, その後にさらにデータが来てから保持した小さなデータをストリームに戻してから変換を始めている.

このことに対応する2つの方法: ストリームをずっと使うならば, 特別なことは何もしないほうがよい. データが発生するたびに書き込み続け, 利用可能になれば読み込む. これでうまく行くはずだ.

ストリームへのデータの送信を終えるときは, 単にそれをSDLに伝えれば, 内部のバッファに保存された全データを変換しSDL_AudioStreamGet()で読み込めるようにできる.

    SDL_AudioStreamFlush(stream);

次のことに注意すること. ストリームを掃きだした後にデータを書き込むこともできる. しかし, おそらく末尾のパッディングされた部分が無音になり音声の出力が途切れるだろう. 本当に掃きだしたいのは, ストリームを終えて最後の数サンプルを取り出す場合のみのはずだ.

もし, どんな理由であれ, ストリームのデータを読み込まずに破棄したい場合は次のようにする:

    SDL_AudioStreamClear(stream);

これはストリームに書き込まれたデータを読み込まずに破棄して内部の状態をリセットする. (よって, 再サンプリングはあなたが以前にストリームに書き込んだデータではなく, 新しいデータに対して行われる.) これは別のデータ元に対してストリームを再利用する場合や, 現在のデータ元を使わないと決めたとき(例えばVoIPアプリケーションで攻撃的なユーザをミュートする場合)に便利である.

ストリームを使い終えたならば, それを破棄できる:

     SDL_FreeAudioStream(stream);

これは内部状態とバッファを解放する. 解放する前にバッファの内容を吸い出す必要はない. 呼び出しの後, SDL_AudioStreamポインタは使用できなくなる.

以上だ!

SDL Wikiへのリンク

Using SDL_AudioStream - SDL Wiki