PulseAudio:バッファアンダーフロー

バッファアンダーフロー
バッファアンダーフローは、再生中、再生バッファに次のデータがなくなった時に発生します。

再生の方が早く、書き込みが追いつかなくなった時に起こります。
再生するデータがないので、アンダーフローになると、再生は自動で停止します (ただし、prebuf が 0 の場合は、自動で停止しない)。

アンダーフロー後に、新しくデータが書き込まれた場合は、自動的に再生が再開されるので、通常の再生であれば、アンダーフローになったからといって、特別何かをする必要はありません。

ただ、再生が途中で途切れると問題がある場合は、何かしら処理を行う必要があるでしょう。
コールバック
void pa_stream_set_underflow_callback(pa_stream *p, pa_stream_notify_cb_t cb, void *userdata);
void pa_stream_set_started_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_started_callback() は、再生が開始する時に実行されるコールバック関数をセットします。
サーバーとの接続後に、初めてデータを書き込んだ時や、アンダーフローで再生が停止した後に、新しいデータが書き込まれて、再生が再開された時に実行されます。
アンダーフロー時の位置を取得
int64_t pa_stream_get_underflow_index(const pa_stream *p);

最後にアンダーフローが発生した時の、再生バッファの位置(バイト数)を返します。
アンダーフローが発生していない場合などは、-1 が返ります。

PulseAudio の再生/録音バッファは、先頭と終端が繋がっているようなリングバッファではないので、再生開始位置を 0 として、絶対的なバッファ位置が返ります。
そのため、64bit の整数になっています。

実際、64bit 整数でどれだけの時間を表現できるかというと、16bit 48000 Hz 2 ch の場合、
INT64_MAX / (48000*4) = 48038396025285 秒 = 13343998895.913 時間 = 555999953.996 日となります。

これだけの時間を再生し続けることは、ほぼ不可能なので、64bit の整数であれば、バッファ位置を絶対値で表現できるということになります。
プログラム
アンダーフローのテストを行うプログラムです。

最初に、書き込み可能なサイズ分のバッファを用意して、サーバーに書き込みます。
その後、その時間分スリープして、意図的にアンダーフローを発生させます。

そして、再度同じデータを書き込み、再生が再開して、その再生が終わるまで待ちます。

$ cc -o 14-underflow 14-underflow.c util.c -lpulse

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

#define SAMPRATE  48000

PulseData *pulse;

//アンダーフロー

static void _cb_underflow(pa_stream *p,void *userdata)
{
    printf("* underflow: %ld\n", pa_stream_get_underflow_index(p));
}

//再生開始

static void _cb_started(pa_stream *p,void *userdata)
{
    printf("* started\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);

    size = sendsize;

    if(pa_stream_begin_write(strm, &writebuf, &size)) goto END;

    memcpy(writebuf, buf, size);

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

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

END:
    pa_threaded_mainloop_unlock(pulse->mainloop);
}

int main(void)
{
    uint8_t *buf;
    size_t bufsize;

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

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

    //コールバックなど

    pa_threaded_mainloop_lock(pulse->mainloop);

    pa_stream_set_underflow_callback(pulse->stream, _cb_underflow, NULL);
    pa_stream_set_started_callback(pulse->stream, _cb_started, NULL);

    bufsize = pa_stream_writable_size(pulse->stream);

    printf("writable: %d\n", (int)bufsize);

    pa_threaded_mainloop_unlock(pulse->mainloop);

    //再生

    buf = (uint8_t *)malloc(bufsize);

    pulse_write_buf_sawtooth(buf, bufsize, SAMPRATE, 261.626);
    
    _write_data(buf, bufsize);

    pa_msleep(bufsize * 1000 / (SAMPRATE * 2) + 100);

    _write_data(buf, bufsize);

    free(buf);

    //

    printf("# wait\n");
    pulse_wait_drain(pulse);

    pulse_free(pulse);

    return 0;
}
解説
# stream connected
writable: 24000
* write: 24000
* started
* underflow: 24000
* write: 24000
# wait
* started
* underflow: 48000

コールバックをセットした後、pa_stream_writable_size() で、最初に書き込み可能なサイズを取得します。
ここでは、24000 byte (1/4秒) です。

そのサイズ分の波形データを用意した後、一度にそれを書き込みます。
書き込み可能なサイズであることはわかっているので、一度の pa_stream_write() で、すべて書き込めます。

そして、そのサイズの時間 (250 ms) + 100 ms の時間を、スリープで待ちます。
スリープしている間に再生が進むので、意図的にアンダーフローを起こすことができます。

まず、データの書き込み後、再生バッファに初めてデータを書き込んだことにより、再生が開始したというコールバックが来ています。
バッファ情報はデフォルトの状態なので、prebuf は 22082 byte ですから、24000 byte を書き込んだ時点で、自動的に再生を開始する条件をクリアしています。

その後、再生が進み、再生データがなくなると、アンダーフローのコールバックが来ます。
pa_stream_get_underflow_index() で、再生が停止した時のバッファ位置を確認すると、24000 です。
書き込んだデータは 24000 byte なので、終端まで来たということがわかります。

スリープから抜けた後、再び同じデータを書き込み、pa_stream_drain() で、再生が終わるまで待ちます。
アンダーフローで再生が停止した後、再びデータが書き込まれたので、started のコールバックが再び来ます。

drain で再生が終わるまで待つ場合も、アンダーフローは発生しています。
再生が停止した時のバッファ位置は、48000 です。
全体で 24000 + 24000 のデータを書き込んでいるので、これが絶対位置であることがわかります。