PulseAudio:音量とサーバーイベント

音量
PulseAudio では、シンクで、ハードウェアに出力する際の音量を指定でき、各ストリームごとに相対音量を指定することもできます。

音量関連の関数については、こちらをご覧ください。
pa_volume_t
音量を指定する際の値は、以下のように定義されています。

typedef uint32_t pa_volume_t;

//100%
#define PA_VOLUME_NORM ((pa_volume_t) 0x10000U)

//ミュート
#define PA_VOLUME_MUTED ((pa_volume_t) 0U)

//(数値上の)最大音量
#define PA_VOLUME_MAX ((pa_volume_t) UINT32_MAX/2)

音量値は pa_volume_t 型で、整数によって指定します。
0x10000 が 100% になります。
よって、0.01% 単位での調整になります。
pa_cvolume
チャンネルごとに音量を指定する必要がある場合は、pa_cvolume 構造体を使います。

typedef struct pa_cvolume {
    uint8_t channels; //チャンネル数
    pa_volume_t values[PA_CHANNELS_MAX]; //各チャンネルの音量
} pa_cvolume;
プログラム (1)
最初に見つかったシンクの音量を、50% に変更するプログラムです。

$ cc -o 08a-volume 08a-volume.c util.c -lpulse

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

PulseData *pulse;

typedef struct
{
    uint32_t index;
    int channels;
}sinkinfo;

//シンク情報

static void _sink_callback(pa_context *c,const pa_sink_info *i,int eol,void *userdata)
{
    sinkinfo *p = (sinkinfo *)userdata;

    if(i)
    {
        p->index = i->index;
        p->channels = i->volume.channels;
    }

    pa_threaded_mainloop_signal(pulse->mainloop, 0);
}

//成功したか

static void _volume_callback(pa_context *c,int success,void *userdata)
{
    printf("success: %d\n", success);

    pa_threaded_mainloop_signal(pulse->mainloop, 0);
}

static pa_operation *_get_sink_info(PulseData *p,void *data)
{
    return pa_context_get_sink_info_list(pulse->ctx, _sink_callback, data);
}

static pa_operation *_set_volume(PulseData *p,void *data)
{
    sinkinfo *info = (sinkinfo *)data;
    pa_cvolume vol;

    pa_cvolume_set(&vol, info->channels, PA_VOLUME_NORM / 2);

    return pa_context_set_sink_volume_by_index(pulse->ctx, info->index, &vol, _volume_callback, NULL);
}

int main(void)
{
    sinkinfo info;

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

    //シンク情報取得

    info.index = (uint32_t)-1;

    pulse_wait_operation(pulse, _get_sink_info, &info);

    //音量変更

    if(info.index != (uint32_t)-1)
        pulse_wait_operation(pulse, _set_volume, &info);

    pulse_free(pulse);

    return 0;
}
解説
すべてのシンクの情報を取得し、最初に見つかったシンクの、インデックスと、音量のチャンネル数を記憶します。

pa_cvolume_set() 関数で、pa_cvolume 構造体の各チャンネルに、同じ音量値をセットしています。
PA_VOLUME_NORM の半分の値で、50% になります。

pa_context_set_sink_volume_by_index() で、対象のシンクのインデックスを指定して、音量を変更しています。

環境によっては、複数のシンクが存在する可能性があるので、実際に音量調節プログラムを作りたい場合は、ユーザーに任意のシンクを選択させるか、すべてのシンクの音量を変更するなどの方法を取る必要があります。
サーバーのイベント
PulseAudio の音量調整プログラムを作りたい場合は、他のクライアントがシンクの音量を変更した際に、現在の音量を取得して、自身の UI に新しい音量を適用する必要があります。

音量が変更された時など、サーバー側の情報が変更された時に、クライアントが通知を受けたい場合は、サーバーイベントの受信と、コールバック関数を設定する必要があります。
イベントマスクの設定
pa_operation *pa_context_subscribe(pa_context *c, pa_subscription_mask_t m,
    pa_context_success_cb_t cb, void *userdata);

サーバーイベントのイベントマスクを設定します。
cb のコールバック関数は、設定の操作が成功したかどうかを通知します。

m は、受信するイベントのマスクを、OR で複数指定します。

PA_SUBSCRIPTION_MASK_NULLイベントなし
PA_SUBSCRIPTION_MASK_SINKシンクイベント
PA_SUBSCRIPTION_MASK_SOURCEソースイベント
PA_SUBSCRIPTION_MASK_SINK_INPUTシンク入力イベント
PA_SUBSCRIPTION_MASK_SOURCE_OUTPUTソース出力イベント
PA_SUBSCRIPTION_MASK_MODULEモジュールイベント
PA_SUBSCRIPTION_MASK_CLIENTクライアントイベント
PA_SUBSCRIPTION_MASK_SAMPLE_CACHEサンプルキャッシュイベント
PA_SUBSCRIPTION_MASK_SERVER他のグローバルなサーバー変更
PA_SUBSCRIPTION_MASK_AUTOLOAD-
PA_SUBSCRIPTION_MASK_CARDカードイベント
PA_SUBSCRIPTION_MASK_ALLすべてのイベント

出力音量の変更時など、オーディオ出力に関するイベントのみ受信したい場合は、PA_SUBSCRIPTION_MASK_SINK を指定します。
コールバック関数のセット
void pa_context_set_subscribe_callback(pa_context *c, pa_context_subscribe_cb_t cb, void *userdata);

//コールバック関数
typedef void (*pa_context_subscribe_cb_t)(pa_context *c,
    pa_subscription_event_type_t t, uint32_t idx, void *userdata);

サーバーイベント受信時のコールバック関数をセットします。

コールバック関数の引数
t は、受信したイベントのタイプです。
2つの値が格納されているので、マスクを使って分離します。

イベントタイプ
PA_SUBSCRIPTION_EVENT_SINKシンク
PA_SUBSCRIPTION_EVENT_SOURCEソース
PA_SUBSCRIPTION_EVENT_SINK_INPUTシンク入力
PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUTソース出力
PA_SUBSCRIPTION_EVENT_MODULEモジュール
PA_SUBSCRIPTION_EVENT_CLIENTクライアント
PA_SUBSCRIPTION_EVENT_SAMPLE_CACHEサンプルキャッシュ項目
PA_SUBSCRIPTION_EVENT_SERVERグローバルなサーバーの変更。
PA_SUBSCRIPTION_EVENT_CHANGE の場合のみ発生。
PA_SUBSCRIPTION_EVENT_AUTOLOAD-
PA_SUBSCRIPTION_EVENT_CARDカード
操作
PA_SUBSCRIPTION_EVENT_NEW (0)新しいオブジェクトが作成された
PA_SUBSCRIPTION_EVENT_CHANGE (0x10)オブジェクトのプロパティが変更された
PA_SUBSCRIPTION_EVENT_REMOVE (0x20)オブジェクトが除外された
マスク
PA_SUBSCRIPTION_EVENT_FACILITY_MASKイベント値から、イベントタイプを抽出するためのマスク
PA_SUBSCRIPTION_EVENT_TYPE_MASKイベント値から、操作のタイプを抽出するためのマスク

このコールバックでは、具体的にどの情報が変更されたかという、詳細な情報は来ないので、PA_SUBSCRIPTION_EVENT_CHANGE の場合は、シンクに関する何らかのプロパティが変更されたから、現在の音量を取得して、値を比較してみる、というような処理をすることになります。

idx は、各イベントタイプのオブジェクトの、インデックス番号です。
PA_SUBSCRIPTION_EVENT_SINK なら、イベントが起きたシンクの、インデックス番号になります。
プログラム (2)
すべてのサーバーイベントを受信して、出力するプログラムです。
Enter キーを押すと、終了します。

$ cc -o 08b-event 08b-event.c util.c -lpulse

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

PulseData *pulse;
uint32_t count = 0;

static void _event_callback(pa_context *c,pa_subscription_event_type_t t,uint32_t idx,void *userdata)
{
    int n;
    const char *mes;

    printf("[%u] ", count++);

    //タイプ

    n = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
    mes = NULL;

    switch(n)
    {
        case PA_SUBSCRIPTION_EVENT_SINK: mes = "sink"; break;
        case PA_SUBSCRIPTION_EVENT_SOURCE: mes = "source"; break;
        case PA_SUBSCRIPTION_EVENT_SINK_INPUT: mes = "sink-input"; break;
        case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: mes = "source-output"; break;
        case PA_SUBSCRIPTION_EVENT_MODULE: mes = "module"; break;
        case PA_SUBSCRIPTION_EVENT_CLIENT: mes = "client"; break;
        case PA_SUBSCRIPTION_EVENT_SAMPLE_CACHE: mes = "sample-cache"; break;
        case PA_SUBSCRIPTION_EVENT_SERVER: mes = "server"; break;
        case PA_SUBSCRIPTION_EVENT_CARD: mes = "card"; break;
    }

    printf("%s, ", mes);

    //操作

    n = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
    mes = NULL;

    switch(n)
    {
        case PA_SUBSCRIPTION_EVENT_NEW: mes = "NEW"; break;
        case PA_SUBSCRIPTION_EVENT_CHANGE: mes = "CHANGE"; break;
        case PA_SUBSCRIPTION_EVENT_REMOVE: mes = "REMOVE"; break;
    }
    
    printf("%s, idx:%u\n", mes, idx);
}

static void _success_callback(pa_context *c,int success,void *data)
{
    pa_threaded_mainloop_signal((pa_threaded_mainloop *)data, 0);
}

static pa_operation *_set_event(PulseData *p,void *data)
{
    pa_context_set_subscribe_callback(p->ctx, _event_callback, p);

    return pa_context_subscribe(p->ctx, PA_SUBSCRIPTION_MASK_ALL, _success_callback, p->mainloop);
}

int main(void)
{
    pulse = pulse_connect(0);
    if(!pulse) return 1;

    //イベントを有効にする

    pulse_wait_operation(pulse, _set_event, NULL);

    //Enter キーが押されるまで待つ

    printf("End with Enter key..\n");
    getchar();

    pulse_free(pulse);

    return 0;
}
解説
この場合、キー押しを待っている間、スレッドでイベントが処理されます。

プログラムの実行後、pavucontrol を起動すると、以下のように出力されます。

[0] client, NEW, idx:26
[1] client, CHANGE, idx:26
[2] sink, CHANGE, idx:12
[3] source, CHANGE, idx:16
[4] source-output, NEW, idx:22
[5] source, CHANGE, idx:17
[6] source-output, NEW, idx:23
[7] sink, CHANGE, idx:12
[8] source, CHANGE, idx:16
[9] source, CHANGE, idx:17

クライアントの idx = 26 は、pavucontrol のことです。
NEW で、新しいクライアントが接続された、ということです。

他に、シンクやソースに関するイベントが来ています。

音量変更時
pavucontrol 内で、出力の音量を変更すると、以下のように、シンクの CHANGE イベントが来ます。

[10] sink, CHANGE, idx:12
[11] sink, CHANGE, idx:12
[12] sink, CHANGE, idx:12
...

これは、index = 12 のシンクの、何らかの情報が変更されたということです。
出力のポートが変更された場合も、同じ情報が来ます。

外部から変更された音量を適用したい場合は、シンクタイプで CHANGE が来た時に、音量を取得すればいいということになります。
プロファイル変更時
pavucontrol で、カードのプロファイルを変更した場合は、以下のように出力されます。

[18] server, CHANGE, idx:4294967295
# 削除
[19] source-output, REMOVE, idx:22
[20] source, REMOVE, idx:16
[21] sink, REMOVE, idx:12
[22] source-output, REMOVE, idx:23
[23] source, REMOVE, idx:17
# シンクとソースの再作成
[24] sink, CHANGE, idx:14
[25] source, CHANGE, idx:19
[26] source, NEW, idx:19
[27] sink, NEW, idx:14
# カードの情報変更
[28] card, CHANGE, idx:0
# 削除
[29] source, REMOVE, idx:18
[30] sink, REMOVE, idx:13
[31] module, REMOVE, idx:25
#
[32] source-output, NEW, idx:24
[33] sink, CHANGE, idx:14
[34] source, CHANGE, idx:19

idx = 4294967295 は、(uint32_t)-1 です。
サーバーにはインデックス番号がないため、このような値になっています。

まず、REMOVE が続いているので、指定されたインデックスのオブジェクトは削除された、ということになります。

なぜか NEW よりも CHANGE の方が先に来ていますが、とりあえず、シンクやソースは、新しいインデックスで再作成されています。

このように、プログラムの実行中に、シンクなどのオブジェクトが、削除&再作成される場合があるので、注意してください。