ALSA:要素のイベント

要素のイベント
音量調節アプリなどの場合、他のアプリで音量が変更された時、その通知を受けて、自身のアプリで音量値を変更したいという場合があるでしょう。

ALSA では、カードの要素が変更された時、通知を受けることができます。
イベントを待つ
イベントを待つ方法としては、以下の3通りがあります。

  • snd_ctl_wait() で、イベントが来るまで待つ。
  • poll() で、イベントが来るまで待つ。
  • 非同期ハンドラをセットする。イベントが来たら関数が実行される。

他の処理と並行して行うなら、非同期ハンドラを使うのが簡単です。

ただ、非同期ハンドラの場合は、どのタイミングでハンドラ関数が来るかわからないので、プログラム内で特定の処理を行っている時は、ALSA のイベント処理を行わないなどの、同期処理が必要になる場合があります。

GUI のメインループなどと一緒に使うなら、poll() を使うことで、GUI・ALSA・その他のイベントなどを、まとめて待つことができます。
開く時のモード
snd_ctl_open() でサウンドカードを開く時、mode 引数で SND_CTL_ASYNC (非同期通知) を指定できますが、非同期ハンドラを使う際に、SND_CTL_ASYNC が必要になるわけではありません。

むしろ、SND_CTL_ASYNC をセットすると、ハンドラが実行されません。

非同期ハンドラをセットすると、ALSA lib 内部で、自動的に非同期モードが設定されるので、snd_ctl_open() 時の mode で、SND_CTL_ASYNC を指定する必要はありません。
イベント
イベントの通知を登録
まずは、イベントが受け取れるように、登録を行う必要があります。

int snd_ctl_subscribe_events(snd_ctl_t *ctl, int subscribe);

subscribe は、0 で登録解除、1 で登録、-1 で登録されているか確認。

登録が解除されていると、イベントは来ません。

イベントを受け取りたい場合は、poll() や非同期ハンドラなどの方法に関わらず、共通で実行する必要があります。
イベントの読み込み
int snd_ctl_read(snd_ctl_t *ctl, snd_ctl_event_t *event);

イベントが来た場合に、起こったイベントを snd_ctl_event_t に読み込みます。

戻り値は、読み込まれたイベントの数 (0 か 1)。負の値でエラーコード。
snd_ctl_event_t
snd_ctl_event_t は、あらかじめ確保しておく必要があります。

//サイズ取得
size_t snd_ctl_event_sizeof(void);

//確保
int snd_ctl_event_malloc(snd_ctl_event_t **ptr);

//解放
void snd_ctl_event_free(snd_ctl_event_t *obj);
イベントタイプの取得
snd_ctl_event_type_t snd_ctl_event_get_type(const snd_ctl_event_t *obj);

読み込まれたイベントの、イベントタイプを返します。

現在は、SND_CTL_EVENT_ELEM (要素関連イベント) しかありません。
イベントマスクの取得
unsigned int snd_ctl_event_elem_get_mask(const snd_ctl_event_t *obj);

要素に対して、何が行われたかを示すイベントマスクを取得します。

SND_CTL_EVENT_MASK_REMOVE要素が削除された
SND_CTL_EVENT_MASK_VALUE値の変更
SND_CTL_EVENT_MASK_INFO情報の変更
SND_CTL_EVENT_MASK_ADD要素の追加
SND_CTL_EVENT_MASK_TLVTLVの値が変更

SND_CTL_EVENT_MASK_REMOVE の扱いに注意してください。
この値は、(~0U) になっており、0 のビット反転なので、すべてのビットが ON の状態です。

最初にこの値を判定し、値が異なれば、他のフラグをチェックします。
イベントの要素の識別子を取得
void snd_ctl_event_elem_get_id(const snd_ctl_event_t *obj, snd_ctl_elem_id_t *ptr);

unsigned int snd_ctl_event_elem_get_numid(const snd_ctl_event_t *obj);
snd_ctl_elem_iface_t snd_ctl_event_elem_get_interface(const snd_ctl_event_t *obj);
unsigned int snd_ctl_event_elem_get_device(const snd_ctl_event_t *obj);
unsigned int snd_ctl_event_elem_get_subdevice(const snd_ctl_event_t *obj);
const char *snd_ctl_event_elem_get_name(const snd_ctl_event_t *obj);
unsigned int snd_ctl_event_elem_get_index(const snd_ctl_event_t *obj);

snd_ctl_event_elem_get_* で、イベントが起きた要素の、識別子の情報を取得します。
非同期ハンドラ
イベントが来たという通知を受けるために、まずは、非同期ハンドラを使う方法を紹介します。
非同期ハンドラの追加
int snd_async_add_ctl_handler(snd_async_handler_t **handler, snd_ctl_t *ctl,
    snd_async_callback_t callback, void *private_data);

//コールバック関数
typedef void (*snd_async_callback_t)(snd_async_handler_t *handler);

イベントが来た時に実行される非同期ハンドラを追加し、コールバック関数をセットします。

handler非同期ハンドラの、ハンドラが返る
callbackコールバック関数。
snd_async_callback_t は、他の非同期ハンドラでも使われる、共通の関数型です。
private_data非同期ハンドラに関連付ける、プライベートデータ
非同期ハンドラの削除
int snd_async_del_handler(snd_async_handler_t *handler);

不要になった非同期ハンドラを削除します。
非同期ハンドラの情報取得
//snd_ctl_t を取得
snd_ctl_t *snd_async_handler_get_ctl(snd_async_handler_t *handler);

//プライベートデータを取得
void *snd_async_handler_get_callback_private(snd_async_handler_t *handler);

非同期ハンドラに関連付けられている情報を取得します。
プログラム (1)
非同期ハンドラでイベントを待ち、イベントの要素の名前と、イベントマスクを出力します。
Enter キーを押すと、終了します。

$ cc -o 07a-async 07a-async -lasound

#include <stdio.h>
#include <alsa/asoundlib.h>

snd_ctl_event_t *event;
unsigned int count = 0;

void _callback(snd_async_handler_t *handler)
{
    snd_ctl_t *ctl;
    unsigned int mask;

    ctl = snd_async_handler_get_ctl(handler);

    if(snd_ctl_read(ctl, event) <= 0) return;

    mask = snd_ctl_event_elem_get_mask(event);

    printf("-- %u --\n", count++);
    printf("name: '%s'\n", snd_ctl_event_elem_get_name(event));
    printf("mask: 0x%X ", mask);

    if(mask == SND_CTL_EVENT_MASK_REMOVE)
        printf("REMOVE");
    else
    {
        if(mask & SND_CTL_EVENT_MASK_VALUE) printf("VALUE ");
        if(mask & SND_CTL_EVENT_MASK_INFO) printf("INFO ");
        if(mask & SND_CTL_EVENT_MASK_ADD) printf("ADD ");
        if(mask & SND_CTL_EVENT_MASK_TLV) printf("TLV ");
    }
    printf("\n");
}

int main(int argc,char **argv)
{
    snd_ctl_t *ctl;
    snd_async_handler_t *async;

    if(snd_ctl_open(&ctl, (argc >= 2)? argv[1]: "default", 0))
        return 1;

    //非同期ハンドラ

    snd_async_add_ctl_handler(&async, ctl, _callback, NULL);

    snd_ctl_subscribe_events(ctl, 1);

    snd_ctl_event_malloc(&event);

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

    getchar();

    //

    snd_ctl_subscribe_events(ctl, 0);

    snd_async_del_handler(async);
    snd_ctl_event_free(event);

    snd_ctl_close(ctl);

    return 0;
}
実行結果
-- 0 --
name: 'Master Playback Volume'
mask: 0x1 VALUE 
-- 1 --
name: 'PCM Playback Volume'
mask: 0x1 VALUE 
-- 2 --
name: 'Master Playback Volume'
mask: 0x1 VALUE 
-- 3 --
name: 'Master Playback Volume'
mask: 0x1 VALUE 
-- 4 --
name: 'Master Playback Volume'
mask: 0x1 VALUE

pavucontrol で出力の音量を変更すると、このように、音量の要素の値が変更された、という通知が来ます。

変更された値は、snd_ctl_elem_value_t に読み込む必要があります。
poll
次に、poll() を使って、イベントを待つ方法を紹介します。

poll() を行うために、struct pollfd の情報が必要なので、ALSA イベント用の pollfd の情報を取得した後、poll() を行い、何かが起こるまで待ちます。

//<poll.h>

struct pollfd {
  int fd;
  short int events;
  short int revents;
};
必要な pollfd の数を取得
int snd_ctl_poll_descriptors_count(snd_ctl_t *ctl);

ALSA 用の struct pollfd の数を取得します。
基本的に1が返ります。
pollfd の情報をセット
int snd_ctl_poll_descriptors(snd_ctl_t *ctl, struct pollfd *pfds, unsigned int space);

space 個の pfds の配列に、ALSA 用の情報をセットします。
戻り値で、実際にセットされた数が返ります。
pollfd から、起こったイベントを取得
int snd_ctl_poll_descriptors_revents(snd_ctl_t *ctl,
    struct pollfd *pfds, unsigned int nfds, unsigned short *revents);

poll() の実行後、戻り値で1以上が返った時(pollfd 内の1つ以上のファイルディスクリプタで、何らかのイベントが起こった時)、nfds 個の pollfd の配列内から、ALSA 用の情報を参照し、pollfd の revents の値を返します。

pollfd の revents は、その fd で実際に起こったイベントのフラグです。

POLLIN フラグが ON の場合、読み込みが可能になった、つまり、イベントが読み込みできるということなので、その後、snd_ctl_read() を実行して、実際にイベントを読み込みます。
プログラム (2)
poll() でイベントを待ち、イベントの内容を出力します。
Ctrl+C で終了します。

$ cc -o 07b-poll 07b-poll -lasound

#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <poll.h>
#include <errno.h>
#include <alsa/asoundlib.h>

unsigned int count = 0;

static void _put_event(snd_ctl_t *ctl,snd_ctl_event_t *event)
{
    unsigned int mask;

    if(snd_ctl_read(ctl, event) <= 0) return;

    mask = snd_ctl_event_elem_get_mask(event);

    printf("-- %u --\n", count++);
    printf("name: '%s'\n", snd_ctl_event_elem_get_name(event));
    printf("mask: 0x%X ", mask);

    if(mask == SND_CTL_EVENT_MASK_REMOVE)
        printf("REMOVE");
    else
    {
        if(mask & SND_CTL_EVENT_MASK_VALUE) printf("VALUE ");
        if(mask & SND_CTL_EVENT_MASK_INFO) printf("INFO ");
        if(mask & SND_CTL_EVENT_MASK_ADD) printf("ADD ");
        if(mask & SND_CTL_EVENT_MASK_TLV) printf("TLV ");
    }
    printf("\n");
}

static void _sig_handler(int sig)
{

}

/* シグナルのセット */

static void _set_signal(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

    sa.sa_handler = _sig_handler;

    sigaction(SIGINT, &sa, NULL);
}

int main(int argc,char **argv)
{
    int pollcnt,ret,i;
    snd_ctl_t *ctl;
    snd_ctl_event_t *event;
    struct pollfd *pfd;
    unsigned short rev;

    if(snd_ctl_open(&ctl, (argc >= 2)? argv[1]: "default", 0))
        return 1;

    //シグナル

    _set_signal();

    //イベント

    snd_ctl_subscribe_events(ctl, 1);

    snd_ctl_event_malloc(&event);

    //pollfd

    pollcnt = snd_ctl_poll_descriptors_count(ctl);

    pfd = (struct pollfd *)malloc(sizeof(struct pollfd) * pollcnt);

    snd_ctl_poll_descriptors(ctl, pfd, pollcnt);

    for(i = 0; i < pollcnt; i++)
        printf("fd:%d, events:0x%X\n", pfd[i].fd, pfd[i].events);

    //poll

    while(1)
    {
        ret = poll(pfd, pollcnt, -1);

        if(ret < 0)
        {
            //シグナルによる割り込み
            if(errno == EINTR) break;
        }
        else if(ret > 0)
        {
            ret = snd_ctl_poll_descriptors_revents(ctl, pfd, pollcnt, &rev);

            if(ret == 0 && (rev & POLLIN))
                _put_event(ctl, event);
        }
    }

    free(pfd);

    printf("finish!\n");

    //

    snd_ctl_subscribe_events(ctl, 0);

    snd_ctl_event_free(event);

    snd_ctl_close(ctl);

    return 0;
}
解説
Ctrl+C が押された時に、プログラムが強制終了されないようにするため、SIGINT のシグナルハンドラを、何もしない関数に変更しています。

poll() 中にシグナルが発生した場合、戻り値で負の値が返り、errno に EINTR がセットされているので、それをループ終了の条件とします。