PulseAudio:タイミング情報

タイミング情報
バッファ上の現在の書き込み/読み込み位置や、遅延情報など、ストリームに関する、現在のタイミング情報を取得することができます。

ただし、タイミング情報は、サーバー上の現在の処理に関わる値なので、動作が非同期である以上、クライアントは、サーバーから情報を要求しなければなりません。
タイミング情報の要求
タイミング情報を取得したい時は、その前に、pa_stream_update_timing_info() を使って、サーバーにタイミング情報の要求を行う必要があります。

pa_operation *pa_stream_update_timing_info(pa_stream *p, pa_stream_success_cb_t cb, void *userdata);

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

これを行うと、サーバーからタイミング情報が送られてくるので、操作が完了すると、クライアントは、そのデータを最新のタイミング情報として保持します。
自動更新
ストリームの接続時に、flags 引数で PA_STREAM_AUTO_TIMING_UPDATE を ON にした場合、タイミング情報の要求が、定期的に自動で行われます。

一定時間の間隔で、または、書き込みなど特定の関数が実行された時に、更新の要求が行われます。
この場合、取得済みの情報が、最新の情報であるとは限らないので、注意してください。

何かを行う前に、現在の最新情報を取得したい場合は、手動で pa_stream_update_timing_info() を実行する必要があります。
タイミング情報の取得
const pa_timing_info *pa_stream_get_timing_info(pa_stream *s);

サーバーから取得済みの、タイミング情報を返します。
タイミング情報が受信されていない場合は、NULL が返ります。
タイミング情報構造体
typedef struct pa_timing_info {
    struct timeval timestamp;
    int synchronized_clocks;
    pa_usec_t sink_usec;
    pa_usec_t source_usec;
    pa_usec_t transport_usec;
    int playing;
    int write_index_corrupt;
    int64_t write_index;
    int read_index_corrupt;
    int64_t read_index;
    pa_usec_t configured_sink_usec;
    pa_usec_t configured_source_usec;
    int64_t since_underrun;
} pa_timing_info;

timestampこの情報のシステムクロック時間。
struct timeval は、<sys/time.h> で定義されています。
synchronized_clocksローカルマシンとリモートマシンのクロックが同期している場合は、0 以外。
sink_usecシンクでサンプルを再生するのにかかる時間 (マイクロ秒)。
source_usecサンプルが録音されてから、クライアントに配信されるまでにかかる時間 (マイクロ秒)。
(録音時のみ)
transport_usecサンプルがデーモンとの間で転送されるのにかかる推定時間 (マイクロ秒)。
playingストリームが現在アンダーランしておらず、データがデバイス (シンク/ソース) に渡されている場合は、0 以外。
(再生時のみ)
write_index_corrupt情報の受信後に書き込みが行われ、write_index が破損したことにより、write_index が最新でない場合は、0 以外。
SEEK_RELATIVE_ON_READ および SEEK_RELATIVE_END での書き込み時のみ、write_index を破損する可能性がある。
write_index再生バッファへの現在の書き込み位置 (バイト数)
read_index_corrupt情報の受信後に、一時停止またはフラッシュが行われたことにより、read_index が破損したため、read_index が最新でない場合は、0 以外。
read_index再生バッファへの現在の読み込み位置 (バイト数)
configured_sink_usecシンクに設定されたレイテンシ (マイクロ秒)
configured_source_usecソースに設定されたレイテンシ (マイクロ秒)
since_underrun最後のアンダーランが発生してから、または、最後のアンダーラン後に再生が再開されてから、シンクに渡されたバイト数

再生の場合、write_index は、次に書き込みを行う時の、再生バッファへの書き込み位置です。
read_index は、サーバーがデータを再生する際、次に読み込みを行う位置です。
時間の取得
受け取ったタイミング情報を元に、時間を計算する関数があります。
再生/録音時間の取得
int pa_stream_get_time(pa_stream *s, pa_usec_t *r_usec);

現在の再生/録音時間をマイクロ秒で返します。
最後に受信したタイミング情報によって計算されます。

成功時は 0 が返り、タイミング情報が受信されていないなど、エラーの場合は、負の値が返ります。

ストリームの接続時、flags に PA_STREAM_INTERPOLATE_TIMING が設定されていた場合、最後にタイミング情報を受信した後に、ハードウェアによって実際に再生されたサンプル数を推測し、時間を補正します。
レイテンシの取得
int pa_stream_get_latency(pa_stream *s, pa_usec_t *r_usec, int *negative);

現在書き込まれているデータが、実際にハードウェア上で再生し終わるまでの時間を返します。
これも、最後に受信したタイミング情報によって計算されます。

※この関数は、内部で pa_stream_get_time() を使います。

negative は、時間が負の値になった場合、1 が返ります。
プログラム
データを書き込む前に、タイミング情報を取得し、出力するプログラムです。
ただし、録音に関する情報などは表示していません。

'ドレミドレミ' (各0.5秒) の合計3秒分を再生しています。

$ cc -o 16-timing 16-timing.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_write(pa_stream *p,size_t nbytes,void *userdata)
{
    printf("- writeable: %d\n", (int)nbytes);
    pa_threaded_mainloop_signal(pulse->mainloop, 0);
}

//タイミング情報完了時

static void _success_timing(pa_stream *s,int success,void *userdata)
{
    const pa_timing_info *i;
    pa_usec_t usec;
    int n;
    
    i = pa_stream_get_timing_info(s);

    printf("synchronized_clocks: %d\n", i->synchronized_clocks);
    printf("sink_usec: %lu\n", i->sink_usec);
    printf("transport_usec: %lu\n", i->transport_usec);
    printf("playing: %d\n", i->playing);
    printf("write_index: %ld (%lu us)\n", i->write_index,
        i->write_index * 1000 * 1000 / (SAMPRATE * 2));
    printf("read_index: %ld (%lu us)\n", i->read_index,
        i->read_index * 1000 * 1000 / (SAMPRATE * 2));
    printf("configured_sink_usec: %lu\n", i->configured_sink_usec);

    pa_stream_get_time(s, &usec);
    printf("<time> %lu (%.2f ms)\n", usec, usec / 1000.0);

    pa_stream_get_latency(s, &usec, &n);
    printf("<latency> %lu (%.2f ms)\n", usec, usec / 1000.0);

    printf("-----\n");

    pa_threaded_mainloop_signal(pulse->mainloop, 0);
}

static pa_operation *_get_timing(PulseData *p,void *data)
{
    return pa_stream_update_timing_info(p->stream, _success_timing, NULL);
}

//再生しつつ、バッファのすべてのデータを書き込み

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)
    {
        //書き込み可能なサイズ取得

        while(1)
        {
            size = pa_stream_writable_size(strm);
            if(size == (size_t)-1) goto END;

            if(size) break;

            //書き込み可能になるまで待つ
            pa_threaded_mainloop_wait(pulse->mainloop);
        }

        //タイミング情報

        pa_threaded_mainloop_unlock(pulse->mainloop);
        pulse_wait_operation(pulse, _get_timing, NULL);
        pa_threaded_mainloop_lock(pulse->mainloop);

        //書き込み

        if(size > sendsize) size = sendsize;
        
        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);

        //次へ

        buf += size;
        sendsize -= size;
    }

END:
    pa_threaded_mainloop_unlock(pulse->mainloop);
}

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

    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_write_callback(pulse->stream, _cb_write, NULL);
    pa_threaded_mainloop_unlock(pulse->mainloop);

    //再生

    bufsize = SAMPRATE;

    buf = (uint8_t *)malloc(bufsize);

    for(i = 0; i < 6; i++)
    {
        pulse_write_buf_sawtooth(buf, bufsize, SAMPRATE, freq[i % 3]);
        _write_data(buf, bufsize);
    }

    free(buf);

    //

    pulse_wait_drain(pulse);

    pulse_free(pulse);

    return 0;
}
実行結果
# stream connected
synchronized_clocks: 1
sink_usec: 1833498
transport_usec: 382
playing: 0
write_index: 0 (0 us)
read_index: 0 (0 us)
configured_sink_usec: 210000
<time> 0 (0.00 ms)
<latency> 0 (0.00 ms)
-----
* write: 24000
- writeable: 20032
synchronized_clocks: 1
sink_usec: 210035
transport_usec: 237
playing: 1
write_index: 24000 (250000 us)
read_index: 20032 (208666 us)
configured_sink_usec: 210000
<time> 0 (0.00 ms)
<latency> 250000 (250.00 ms)
-----
* write: 20032
- writeable: 3968
- writeable: 18384
synchronized_clocks: 1
sink_usec: 208908
transport_usec: 1390
playing: 1
write_index: 44032 (458666 us)
read_index: 38416 (400166 us)
configured_sink_usec: 210000
<time> 192648 (192.65 ms)
<latency> 266018 (266.02 ms)
-----
* write: 3968
synchronized_clocks: 1
sink_usec: 205380
transport_usec: 277
playing: 1
write_index: 48000 (500000 us)
read_index: 38416 (400166 us)
configured_sink_usec: 210000
<time> 195063 (195.06 ms)
<latency> 304937 (304.94 ms)
...
-----
* write: 17824
- writeable: 18928
synchronized_clocks: 1
sink_usec: 208838
transport_usec: 1562
playing: 1
write_index: 264000 (2750000 us)
read_index: 258928 (2697166 us)
configured_sink_usec: 210000
<time> 2489890 (2489.89 ms)
<latency> 260110 (260.11 ms)
-----
* write: 18928
- writeable: 18400
synchronized_clocks: 1
sink_usec: 209204
transport_usec: 1236
playing: 1
write_index: 282928 (2947166 us)
read_index: 277328 (2888833 us)
configured_sink_usec: 210000
<time> 2680865 (2680.86 ms)
<latency> 266301 (266.30 ms)
-----
* write: 5072
- writeable: 24000

書き込み前にタイミング情報を取得しているので、write_index は、これまでに書き込んだ総バイト数となります。

2回目
synchronized_clocks: 1
sink_usec: 210035
transport_usec: 237
playing: 1
write_index: 24000 (250000 us)
read_index: 20032 (208666 us)
configured_sink_usec: 210000
<time> 0 (0.00 ms)
<latency> 250000 (250.00 ms)

2回目の情報では、read_index が 20032 byte になっているので、サーバーによってここまで読み込まれたということがわかります。

ただし、再生データが読み込まれたというだけであって、実際に、このサイズ分の音が鳴り終えているかは別です。
time の値を見てみると、0 になっています。

ただ、これらの情報はあくまで推定であって、厳密に正確な値ではないので、参考程度に見ておいた方がいいでしょう。