PulseAudio:非同期API

非同期 API
シンプル API だけでは、本当に単純な操作しかできないので、PulseAudio を扱う場合は、基本的に非同期 API を使うことになります。

シンプル API は、内部で、非同期 API を使って実装されています。
非同期
PulseAudio は、サーバーとしてデーモンが立ち上がった後、再生や録音をするクライアントとやり取りをする、という形の動作となります。

クライアントが何かしら要求を行うと、それを受け取ったサーバーが、実際に処理を行うという形になるため、非同期の動作となります。

例えば、クライアントが、サーバーとの接続を行う関数を実行したとしても、関数が戻った後、すぐに接続状態になるとは限りません。

ほとんどの処理では、サーバー上で実際に操作が完了するまで、待つ必要があるので、少々プログラムが複雑になります。
メインループ
PulseAudio の非同期 API では、GUI と同じように、メインループ内で、サーバーからのイベントを受け取る必要があります。
これによって、処理が完了したなどの通知を受けることができます。
メインループの種類
非同期のメインループとして、以下の3つが用意されています。

メインループpoll() によってイベントを待つ
スレッド・メインループスレッド内で自動的にイベントが処理される
GLIB メインループGLib のメインループのラッパー

poll のメインループは、PulseAudio からイベントが来るまで、ブロックして待ちます。
GUI など、PulseAudio とその他の処理を並行して実行したい場合は、プログラム独自のスレッド内で実行することも出来ます。

スレッドのメインループの場合は、PulseAudio が新しいスレッドを作成して、その中で自動的にイベントを処理させます。
スレッドに関して、実行環境に左右されないコードを書けるという利点があります。

どちらかで何かが制限されるということもないので、自分が使いたい方を使って構いません。
作成
非同期 API を使う場合、最初に、必要なメインループを作成する必要があります。

//poll メインループ

pa_mainloop *pa_mainloop_new(void);
void pa_mainloop_free(pa_mainloop *m);
pa_mainloop_api *pa_mainloop_get_api(pa_mainloop *m);

//スレッドメインループ

pa_threaded_mainloop *pa_threaded_mainloop_new(void);
void pa_threaded_mainloop_free(pa_threaded_mainloop *m);
pa_mainloop_api *pa_threaded_mainloop_get_api(pa_threaded_mainloop *m);

new で作成、free で解放、get_api で API を取得します (コンテキストの作成時に必要)。
コンテキスト
メインループを作成したら、次にコンテキスト (pa_context) を作成します。

pa_context は、PulseAudio サーバーと接続するための、基本的なオブジェクトです。
複数のサーバーと接続するようなことでもない限りは、1つのアプリケーションで、1つのコンテキストを使います。
作成
pa_context *pa_context_new(pa_mainloop_api *mainloop, const char *name);

pa_context *pa_context_new_with_proplist(pa_mainloop_api *mainloop,
    const char *name, const pa_proplist *proplist);

pa_context_new(), pa_context_new_with_proplist() は、新しいコンテキストを作成します。

with_proplist の場合は、追加でプロパティリストを指定できます。
name 引数は、アプリケーション名を指定します。
参照カウント
pa_context *pa_context_ref(pa_context *c);
void pa_context_unref(pa_context *c);

コンテキストは参照カウントされるので、解放関数がありません。

代わりに、pa_context_unref() で、参照カウンタを -1 します。
カウンタが 0 になると、実際に解放されます。
エラー
int pa_context_errno(const pa_context *c);
const char *pa_strerror(int error);

関数の失敗時、エラーコードを取得したい時は、pa_context_errno() を使います。
最後に発生したエラーの、エラーコードが返ります。

エラーコードから、説明用の文字列を取得したい場合は、pa_strerror() を使います。
接続
int pa_context_connect(pa_context *c, const char *server,
    pa_context_flags_t flags, const pa_spawn_api *api);

コンテキストを、PulseAudio サーバーと接続します。
まず、サーバーと接続しないことには、何も操作ができません。

server接続するサーバー名。NULL でデフォルト
flagsPA_CONTEXT_NOFLAGS (0) : オプションなし。
PA_CONTEXT_NOAUTOSPAWN : PulseAudio デーモンの自動生成を無効にする。
PA_CONTEXT_NOFAIL : デーモンが利用できない場合でも失敗にせず、代わりに PA_CONTEXT_CONNECTING 状態に入って、デーモンが現れるのを待つ。
apiNULL 指定可。
自動生成されたデーモンを、アプリケーションに統合するために使用する。
戻り値負の値でエラー

server = NULL で、PA_CONTEXT_NOAUTOSPAWN フラグがセットされていない場合、サーバーにアクセスできない時は、新しいデーモンが生成されます。
切断
void pa_context_disconnect(pa_context *c);

サーバーから切断します。
この関数は、すぐに処理されます。
接続結果
pa_context_connect() でサーバーと接続しても、関数が戻った時点で、実際に接続されているとは限りません。
まだ接続を試している段階で、成功または失敗が確定していない可能性があります。

とりあえず、最初に PulseAudio サーバーと接続しない限りは、何の操作も行えないので、接続後は、接続が確立するか失敗するまで、待つ必要があります。

接続の結果を判断するには、pa_context_get_state() で、現在のコンテキストの状態を取得します。
この値によって、接続に成功したか、失敗したかを判断することができます。

まだ結果が確定していない場合は、PulseAudio のメインループを実行し、サーバーからの通知を待ちます。
サーバーからの通知が来たら、再び状態を取得します。
これを、接続に成功するか失敗するまで繰り返します。
現在の状態を取得
pa_context_state_t pa_context_get_state(const pa_context *c);

コンテキストの現在の状態を取得します。

PA_CONTEXT_UNCONNECTEDまだ接続されていない
PA_CONTEXT_CONNECTING接続が確立されている
PA_CONTEXT_AUTHORIZINGクライアントは、デーモンに対して自身を認証している
PA_CONTEXT_SETTING_NAMEクライアントは、アプリケーション名をデーモンに渡している
PA_CONTEXT_READY接続が確立され、コンテキストで操作を実行する準備が整っている
PA_CONTEXT_FAILED接続に失敗したか、切断された
PA_CONTEXT_TERMINATED接続は正常に終了した

正常に接続された状態ならば、PA_CONTEXT_READY となり、失敗時は PA_CONTEXT_FAILED になります。

pa_context_disconnect() で切断すると、PA_CONTEXT_TERMINATED になります。
状態変化時のコールバック
void pa_context_set_state_callback(pa_context *c, pa_context_notify_cb_t cb, void *userdata);

//コールバック関数
typedef void (*pa_context_notify_cb_t)(pa_context *c, void *userdata);

コンテキストの状態が変化した時に実行される、コールバック関数をセットすることができます。

メインループでイベントが処理されると、指定されたコールバック関数が実行されます。

cb に NULL を指定すると、設定を解除できます。
メインループ (poll)
クライアントが、サーバーからのイベントを処理するためには、PulseAudio のメインループを実行する必要があります。

メインループの種類によって、やり方が異なるので、ここでは、poll のメインループについて説明します。
メインループの処理
以下の3つの関数は、1回分のメインループ処理を行うために必要になります。

1回分とは、サーバーからのイベントを受け取って、受け取ったイベントを処理することです。

//メインループの準備
int pa_mainloop_prepare(pa_mainloop *m, int timeout);

//イベントが起こるまで待つ
int pa_mainloop_poll(pa_mainloop *m);

//イベントを処理
int pa_mainloop_dispatch(pa_mainloop *m);

まず、pa_mainloop_prepare() で、1回のメインループを準備します。
timeout 引数は、次に実行する pa_mainloop_poll() の、タイムアウト時間 (ms) です。-1 で無限に待ちます。
エラー、または終了要求が来た時は、負の値が返ります。

次に、pa_mainloop_poll() で、PulseAudio サーバーからイベントを受け取るまで待ちます。
エラー時は負の値が返ります。

poll の成功時は、pa_mainloop_dispatch() を使って、受け取ったイベントを、クライアント側で処理します。
各イベントに対応するコールバック関数がセットされていれば、それが実行されます。
成功時は、処理されたソースの数が返り、エラー時は負の値が返ります。

まとめて実行
int pa_mainloop_iterate(pa_mainloop *m, int block, int *retval);

pa_mainloop_iterate() を使うと、上記の3つをまとめて実行できます。

blockイベントが来るまでブロックするか。
0 で、pa_mainloop_prepare() の timeout を 0 に、それ以外で -1 (無限) にします。
retvalNULL 以外の場合、quit でループの終了が要求された時に、quit で指定された戻り値が格納されます。
戻り値成功時は、処理されたソースの数。
エラー、または終了要求が来た時は、負の値。
プログラム (1)
poll メインループを使って、サーバーと接続し、メインループ内で状態を取得して、接続の成否を判断するプログラムです。

$ cc -o 02a-connect 02a-connect.c -lpulse

#include <stdio.h>
#include <stdlib.h>
#include <pulse/pulseaudio.h>

pa_mainloop *ml;
pa_context *ctx;

static void _appfree(void)
{
    pa_context_unref(ctx);
    pa_mainloop_free(ml);
}

static void _enderr(void)
{
    int err = pa_context_errno(ctx);

    printf("[error] (%d) %s\n", err, pa_strerror(err));
    _appfree();
    exit(1);
}

int main(void)
{
    pa_context_state_t state;

    ml = pa_mainloop_new();

    ctx = pa_context_new(pa_mainloop_get_api(ml), "test-pulse");

    //接続

    printf("connect..\n");

    if(pa_context_connect(ctx, NULL, 0, NULL) < 0)
        _enderr();

    //待つ

    while(1)
    {
        state = pa_context_get_state(ctx);

        switch(state)
        {
            case PA_CONTEXT_CONNECTING:
                printf(">CONNECTING\n");
                break;
            case PA_CONTEXT_AUTHORIZING:
                printf(">AUTHORIZING\n");
                break;
            case PA_CONTEXT_SETTING_NAME:
                printf(">SETTING_NAME\n");
                break;
            case PA_CONTEXT_READY:
                printf(">READY\n");
                pa_context_disconnect(ctx); //切断
                break;
            case PA_CONTEXT_FAILED:
                printf(">FAILED\n");
                break;
            default:
                printf(">state: %d\n", state);
                break;
        }

        if(state == PA_CONTEXT_READY || state == PA_CONTEXT_FAILED)
            break;

        if(pa_mainloop_iterate(ml, 1, NULL) < 0)
            _enderr();
    }

    //解放

    _appfree();

    return 0;
}
実行結果
connect..
>CONNECTING
>AUTHORIZING
>AUTHORIZING
>AUTHORIZING
>AUTHORIZING
>AUTHORIZING
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>SETTING_NAME
>READY

接続が成功するか失敗するまで、状態を取得します。
接続が確定していない場合は、イベントループを1回行って、何かしらイベントが来るまで待ち、再度状態を取得します。

順番的には、CONNECTING → AUTHORIZING → SETTING_NAME → READY となって、接続が成功します。
コンテキストの状態変化以外にもイベントは来るので、同じ state が何度か続きます。

デーモンが動作しない場合
以下のコマンドで、PulseAudio のサービスを停止させた場合は、エラーが出ます。

$ systemctl --user stop pulseaudio.socket
$ systemctl --user stop pulseaudio.service

# 実行時
connect..
[error] (6) Connection refused
プログラム (2)
次は、コールバック関数を使った場合のプログラムです。

$ cc -o 02b-callback 02b-callback.c -lpulse

#include <stdio.h>
#include <stdlib.h>
#include <pulse/pulseaudio.h>

pa_mainloop *ml;
pa_context *ctx;

static void _appfree(void)
{
    pa_context_unref(ctx);
    pa_mainloop_free(ml);
}

static void _enderr(void)
{
    int err = pa_context_errno(ctx);

    printf("[error] (%d) %s\n", err, pa_strerror(err));
    _appfree();
    exit(1);
}

//コールバック

static void _callback(pa_context *c,void *userdata)
{
    pa_context_state_t state;

    state = pa_context_get_state(c);

    switch(state)
    {
        case PA_CONTEXT_CONNECTING:
            printf(">CONNECTING\n");
            break;
        case PA_CONTEXT_AUTHORIZING:
            printf(">AUTHORIZING\n");
            break;
        case PA_CONTEXT_SETTING_NAME:
            printf(">SETTING_NAME\n");
            break;
        case PA_CONTEXT_READY:
            printf(">READY\n");
            break;
        case PA_CONTEXT_FAILED:
            printf(">FAILED\n");
            break;
        case PA_CONTEXT_TERMINATED:
            printf(">TERMINATED\n");
            break;
        default:
            printf(">state: %d\n", state);
            break;
    }
}

int main(void)
{
    pa_context_state_t state;

    ml = pa_mainloop_new();

    ctx = pa_context_new(pa_mainloop_get_api(ml), "test-pulse");

    //接続

    printf("connect..\n");

    pa_context_set_state_callback(ctx, _callback, NULL);

    if(pa_context_connect(ctx, NULL, 0, NULL) < 0)
        _enderr();

    //待つ

    while(1)
    {
        state = pa_context_get_state(ctx);
        
        if(state == PA_CONTEXT_READY || state == PA_CONTEXT_FAILED)
            break;

        if(pa_mainloop_iterate(ml, 1, NULL) < 0)
            _enderr();
    }

    if(state == PA_CONTEXT_READY)
        pa_context_disconnect(ctx);

    //解放

    _appfree();

    return 0;
}
実行結果
connect..
>CONNECTING
>AUTHORIZING
>SETTING_NAME
>READY
>TERMINATED

内容的には、最初のプログラムとそれほど変わりませんが、状態変化のコールバック関数が来た時だけ、状態を出力しているので、他のイベントが来た時は何も表示されません。
そのため、出力結果はシンプルになっています。

なお、最後に TERMINATED が表示されていますが、これは、pa_context_disconnect() の関数内で、直接コールバック関数が呼ばれた結果です。
READY が来た時点でメインループを抜けているので、クライアントのメインループ内で実行されたコールバックではありません。