目次 - SDL_AudioStreamを使う

# SDL_AudioStreamを使う

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

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

- 使い方を理解するのが難しい.
- 動的な状態を持つことができない; 構造体を「解放」するAPIはなく, メモリ上に確保することもできず, 様々な変換のために特別なことをすることもできない. 2.0.6では, ただ数ビットの内部状態を保存するだけの使うことができないポインタが構造体を占有している. これも機能を組み込むことを難しくしている.
- 柔軟性に欠ける. アプリケーションが使うフィールドと, SDLが使うフィールドが決められており, 拡張できる余裕がない.
- これが最も問題かもしれない: 特定のチャンクのデータを再サンプリングできない. 一度に全てのデータを与えなければならない. そうしないと音声出力が途切れ飛ばされてしまう.

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

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

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

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

- Ogg Vorbisファイルをデコードしたい. そして再生するために即時に指定の形式に変換したい. しかし, libvorbisは一度に無圧縮の512バイトのデータしか扱うことができない. あなたはそれを一度SDL_AudioStreamにプッシュし, 必要に応じて異なる形式に変換されたデータとして別の側からプルできる.
- VoIPアプリケーションがある. しかし, ネットワークの向こうのオーディオパケットが届くことを保障できない. またはパケットの大きさも分からない. または全く届かなかったときは無音のチャンクと置き換えなければならない...しかし, 再生に備えて1つのまとまった音声データのストリームを生成したい.
- ディスクからできるだけ速く音声データをプルし, 正しく変換できるようにパッディングして, 通過するデータの詳細を気にせずに結果を書き込みたい.
- ある特定の1つの形式のみ扱うミキサーを書きたい. そして入力とは異なる様々な形式で出力したい.
- プログラムで音声を生成している. 再生が終わりそうなとき, さらに追加しながら生成したい.
- 音声の異なる形式への変換には興味がないが, それを使う準備が整うまでキューのようなデータ構造で効率よく保存したい.

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