PulseAudio:バッファ

バッファ
ストリームの再生/録音バッファに関する情報は、pa_stream_get_buffer_attr() で取得できます。

バッファの情報は、ストリームの接続時にクライアントが指定することができ、ストリームの作成後に変更することもできます。
バッファの情報を取得
const pa_buffer_attr *pa_stream_get_buffer_attr(pa_stream *s);

ストリームの現在のバッファ情報を取得します。
バッファの情報を変更する
pa_operation *pa_stream_set_buffer_attr(pa_stream *s,
    const pa_buffer_attr *attr, pa_stream_success_cb_t cb, void *userdata);

//コールバック関数
typedef void (*pa_stream_success_cb_t)(pa_stream *s, int success, void *userdata);

ストリームの接続後に、バッファの情報を変更します。

ここで指定された値が、実際にそのまま適用されるとは限らず、サーバーが異なる値を選択する場合もあります。
cb で指定されたコールバックが来た後に、pa_stream_get_buffer_attr() で、実際に設定された値を確認してください。
pa_buffer_attr 構造体
typedef struct pa_buffer_attr {
    uint32_t maxlength;
    uint32_t tlength;
    uint32_t prebuf;
    uint32_t minreq;
    uint32_t fragsize;
} pa_buffer_attr;

maxlengthバッファ全体の最大バイト数。
tlength(再生のみ)再生用のターゲットバイト数。
サーバは、未再生のデータとして、再生バッファ上に少なくとも tlength バイトが存在するよう、クライアントにデータを要求する。

実質的に、このサイズが、バッファの長さであるような扱いになります。
バッファの最大サイズ以内であれば、このサイズを超えるデータを要求する場合もあります。
prebuf(再生のみ)プリバッファのバイト数。
再生バッファ上に、少なくとも prebuf バイトのデータが書き込まれるまで、サーバーは再生を開始しない。
0 の場合は、手動で再生の開始/停止を行います。
minreq(再生のみ)クライアントに、minreq バイト未満のデータ要求は行わない。
つまり、このサイズ以上が書き込みできるようになるまで、書き込み可能であるとはみなさない。
fragsize(録音のみ)サーバーは、fragsize バイトのブロックで、データを送信する。

※バッファ情報を設定する際、各値に (uint32_t)-1 を指定しておくと、自動的に適切な値が選択されます。

なお、ストリームの接続時に、attr 引数に NULL を指定した場合と、構造体の各値に (uint32_t)-1 を指定した場合では、実際に設定された値は異なります。
前者はデフォルトの値で、後者は可能な限り最高の値です。
前回のプログラムでのバッファ情報
前回のプログラムで、バッファ情報を取得した場合は、以下のような値になります。

maxlength: 4194304
tlength: 24000
prebuf: 22082
minreq: 1920
fragsize: 4294967295(-1)

maxlength
maxlength (バッファの最大サイズ) は、4194304 byte です。
前回のサンプルスペックは、16bit, 48000 Hz, 1ch だったので、4194304/(48000*2) = 43.69 秒分の長さになります。

実際に、このサイズ分のバッファが確保されているというわけではなく、最大でこのサイズまでは、データを保持できるということです。

tlength
tlength (ターゲットのサイズ) は、24000 byte です。
これは、前回最初に pa_stream_writable_size() を実行した時の、書き込み可能なサイズと一致します。

その後も、書き込み済みで未再生のデータも含めて、24000 byte の再生データがバッファに存在するよう、サイズが要求されていました。

サイズの取得やコールバック関数で、データの要求を PulseAudio に任せる場合は、基本的にこのサイズが基準となります。
ただし、この値は”少なくとも”という前置きが付いているので、書き込み可能なサイズとして、tlength 以上の値が返ってくる場合もあります。

pa_stream_writable_size() などを使わずに、自分で適切なサイズを選択して書き込むこともできます。
実際は、バッファの最大サイズ以内であれば、このサイズよりも大きいデータを、バッファに書き込むことができます。

prebuf
prebuf (プリバッファのサイズ) は、22082 byte です。

接続直後やバッファアンダーフロー後など、再生が停止している状態で、バッファにこのサイズ以上のデータが書き込まれた時、自動的に再生を開始します。

逆に言うと、このサイズ未満しか書き込まれていない状態では、再生が行われない、ということになります。
少ないデータでも、書き込んだらすぐに再生したいという場合は、pa_stream_trigger() を使います。

minreq
minreq は 1920 byte です。

サーバーは、常にこのサイズ以上のデータを要求し、このサイズ未満しか書き込めない状態であれば、データは要求しません。
これも、データの要求を PulseAudio に任せる場合に有効です。

再生バッファ上で、このサイズ以上が書き込みできるようになるまでは、pa_stream_writable_size() で 0 を返し、また、書き込み可能になったというコールバック関数を実行しません。

小さいサイズで書き込みを繰り返すと、効率が悪いので、ある程度バッファが空いてから、データを要求するということです。
アンダーフローとオーバーフロー
再生用バッファには、バッファアンダーフローと、バッファオーバーフローの状態があります。

  • バッファアンダーフロー
    再生中に、バッファが空になった。
    書き込みよりも、再生の方が追いついてしまった状態。
  • バッファオーバーフロー
    バッファの最大サイズを超えて、データが書き込まれた。

データの要求を PulseAudio に任せている場合は、オーバーフローにならないよう、書き込み可能なサイズを受け取った上で書き込むので、基本的にオーバーフローは起こりません。
しかし、書き込み可能なサイズに構わず、どんどんデータを書き込んでいくと、最終的にオーバーフローになります。

アンダーフローは、再生中、書き込みが追いつかなくなった場合に、常に起こる可能性があります。

アンダーフロー、オーバーフローの状態になった時は、コールバック関数を呼ぶことができます。
コールバック
void pa_stream_set_underflow_callback(pa_stream *p, pa_stream_notify_cb_t cb, void *userdata);
void pa_stream_set_overflow_callback(pa_stream *p, pa_stream_notify_cb_t cb, void *userdata);

//コールバック関数
typedef void (*pa_stream_notify_cb_t)(pa_stream *p, void *userdata);

pa_stream_set_underflow_callback() は、アンダーフローになった時に実行されるコールバック関数をセットします。
pa_stream_set_overflow_callback() は、オーバーフローになった時に実行されるコールバック関数をセットします。
プログラム
オーバーフローのテストを行うプログラムです。

バッファの最大サイズを 0.5 秒分に変更した後、'ドレミファ' で1秒(各1/4秒)のデータを書き込みます。
書き込み可能な状態になるまで待たずに、1秒分のデータを一気に書き込んでいきます。

$ cc -o 13-overflow 13-overflow.c util.c -lpulse

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pulse/pulseaudio.h>
#include "util.h"

#define SAMPRATE  48000
#define BUFMAXLEN SAMPRATE

PulseData *pulse;

//オーバーフロー

static void _cb_overflow(pa_stream *p,void *userdata)
{
    printf("* overflow\n");
}

//バッファのすべてのデータを書き込み

static void _write_data(void *buf,int sendsize)
{
    pa_stream *strm = pulse->stream;
    size_t size;
    void *writebuf;

    pa_threaded_mainloop_lock(pulse->mainloop);

    while(sendsize)
    {
        size = pa_stream_writable_size(strm);
        printf("writable: %d\n", (int)size);

        size = sendsize;
        if(size > BUFMAXLEN) size = BUFMAXLEN;
        
        if(pa_stream_begin_write(strm, &writebuf, &size)) break;

        memcpy(writebuf, buf, size);

        printf("* write: %d\n", (int)size);

        pa_stream_write(strm, writebuf, size, NULL, 0, PA_SEEK_RELATIVE);

        //100 ms 待つ (イベント処理のため)
        pa_threaded_mainloop_unlock(pulse->mainloop);
        pa_msleep(100);
        pa_threaded_mainloop_lock(pulse->mainloop);

        buf += size;
        sendsize -= size;
    }

    pa_threaded_mainloop_unlock(pulse->mainloop);
}

//バッファ情報のセット

static pa_operation *_set_buffer_attr(PulseData *p,void *data)
{
    struct pa_buffer_attr attr;

    attr.maxlength = BUFMAXLEN;   //0.5秒
    attr.tlength = BUFMAXLEN / 4; //1/8秒
    attr.prebuf = (uint32_t)-1;
    attr.minreq = (uint32_t)-1;
    attr.fragsize = (uint32_t)-1;

    return pa_stream_set_buffer_attr(pulse->stream, &attr, pulse_stream_success_callback_signal, p);
}

//バッファ情報表示

static void _put_buffer_attr(void)
{
    const pa_buffer_attr *attr;

    attr = pa_stream_get_buffer_attr(pulse->stream);

    printf("maxlength: %u\n", attr->maxlength);
    printf("tlength: %u\n", attr->tlength);
    printf("prebuf: %u\n", attr->prebuf);
    printf("minreq: %u\n----\n", attr->minreq);
}

int main(void)
{
    uint8_t *buf,*dst;
    int bufsize,i;
    double freq[4] = {261.626, 293.665, 329.628, 349.228};

    pulse = pulse_connect(0);
    if(!pulse) return 1;

    if(pulse_create_stream_play(pulse, PA_SAMPLE_S16LE, SAMPRATE, 1))
        pulse_enderr(pulse);

    pulse_wait_operation(pulse, _set_buffer_attr, NULL);

    _put_buffer_attr();

    //コールバック

    pa_threaded_mainloop_lock(pulse->mainloop);
    pa_stream_set_overflow_callback(pulse->stream, _cb_overflow, NULL);
    pa_threaded_mainloop_unlock(pulse->mainloop);

    //書き込み

    bufsize = 2 * SAMPRATE;

    buf = dst = (uint8_t *)malloc(bufsize);

    for(i = 0; i < 4; i++)
    {
        pulse_write_buf_sawtooth(dst, bufsize / 4, SAMPRATE, freq[i]);
        dst += bufsize / 4;
    }
    
    _write_data(buf, bufsize);

    free(buf);

    //

    pulse_wait_drain(pulse);

    pulse_free(pulse);

    return 0;
}
実行結果
# stream connected
maxlength: 48000
tlength: 12000
prebuf: 10082
minreq: 1920
----
writable: 24000
* write: 48000
writable: 0
* write: 48000
* overflow

'ドレミファ' の計1秒分のデータを書き込んでいますが、実際に再生されるのは、'ドレ' の2音 (0.5秒) だけです。
つまり、最大バッファサイズを超えた、'ミファ' の音は、実際には再生バッファに書き込まれていません。
解説
デフォルトのバッファ状態の場合、バッファの最大サイズが大きいので、ストリームの接続後に、バッファの最大サイズを 0.5 秒分に設定しています。
tlength は 1/8 秒分です。

オーバーフローのコールバックを設定した後、1秒分のデータを用意して、一気に書き込みます。

書き込む前に pa_stream_writable_size() を実行してみると、tlength (12000) の値ではなく、24000 byte になっています。
このように、書き込み可能な値として、tlength 以上の値が返る場合があります。
ここでは、1/4 秒分が書き込み可能であると通知されています。

pa_stream_begin_write
pa_stream_begin_write() にサイズを渡す時、一度に書き込むサイズは、バッファの最大サイズに調整しています。
バッファの最大サイズを超えるデータを書き込もうとすると、そのデータ全体が書き込まれなくなってしまうからです。

試しに、最初に 96000 byte (1秒分) のサイズを渡した場合は、65472 byte の値が返ってきます。
この時点で、すでに再生バッファの最大サイズ (48000) を超えています。
つまり、再生バッファの最大サイズと、書き込み用のバッファのサイズは異なるということです。

この状態で書き込むと、再生しても全く音が鳴らないので、バッファの最大サイズを超えるようなデータを書き込むと、データの書き込みが全く行われない、という結論になります。

そのため、書き込むサイズは、バッファの最大サイズに制限して、最初は 48000 byte (0.5秒分) を書き込みます。
この時点で、再生バッファはフルの状態です。

コールバックを実行させる
なお、書き込み処理中は、メインループのスレッドがロックされているので、このままでは、オーバーフローのコールバックがすぐに実行されません。

そのため、書き込み直後に pa_msleep() を使って、100 ミリ秒 (0.1秒) の時間を待ちます。
この間にイベントが処理され、オーバーフローのコールバックが実行されます。

その後、pa_stream_writable_size() を行うと、書き込み可能なサイズは 0 になっています。

オーバーフロー
最後に、残りの 48000 byte を書き込むことになりますが、この時点で、再生は進んでいないので、バッファの最大サイズと同じサイズを追加で書き込もうとすれば、当然、オーバーフローが発生します。

最後の 0.5 秒 (ミ、ファの音) が鳴っていないことを考えると、このデータ全体が書き込まれていないと考えることができます。
つまり、バッファサイズを超える部分のデータは、全く書き込まれません。