ALSA:非ブロッキングモード

非ブロッキングモード
前回は、PCM をブロックモードで開いたので、データを書き込む際、すべてのデータが書き込めるまで、関数はブロックされます。

PCM を開く時に、mode を SND_PCM_NONBLOCK にすると、非ブロッキングモードになります。

この場合、書き込みを行った時、バッファがいっぱいで書き込めない場合は、-EAGAIN エラーが返ります。
この状態では、すぐに書き込めないので、他の処理などをして待つか、書き込みが可能になるまで待つ必要があります。

スレッドを使わずに、他の処理と並行させたい場合などは、非ブロッキングモードを使います。
非ブロッキングモードの設定
int snd_pcm_nonblock(snd_pcm_t *pcm, int nonblock);

PCM を開いた後に、非ブロッキングモードを変更する場合に使います。

nonblock は、0 でブロックモード、1 で非ブロッキングモードになります。
待つ
int snd_pcm_wait(snd_pcm_t *pcm, int timeout);

poll() を使って、読み書きが可能になるなどのイベントが来るまで待ちます。

timeout は、ミリ秒単位のタイムアウト時間です。
SND_PCM_WAIT_INFINITE (-1) で無制限。

以下の特定の値を指定すると、指定した状態になるまで待つことができます。

SND_PCM_WAIT_IO (-10001)読み書き可能になるまで
SND_PCM_WAIT_DRAIN (-10002)drain が終わるまで

戻り値は、0 でタイムアウト、1 で読み書きの準備が出来ている。
負の値でエラーコードです。
drain
非ブロッキングモードで snd_pcm_drain() を実行した時、未再生のデータがある場合は -EAGAIN エラーが返ります。

その場合、PCM の状態が SND_PCM_STATE_DRAINING になり、drain で停止を待っている状態になります。
poll
poll() を使って、バッファへの読み書きが可能になるまで待ちたい場合は、コントロールインターフェイスでイベントを待った時と同じような形で、struct pollfd の情報を取得し、イベントを待ちます。

//必要な pollfd の数を返す
int snd_pcm_poll_descriptors_count(snd_pcm_t *pcm);

//PCM 用の pollfd の情報をセット
int snd_pcm_poll_descriptors(snd_pcm_t *pcm, struct pollfd *pfds, unsigned int space);

//poll 後、起こったイベントを取得
int snd_pcm_poll_descriptors_revents(snd_pcm_t *pcm,
    struct pollfd *pfds, unsigned int nfds, unsigned short *revents);

POLLIN/POLLOUT
PCM の pollfd 情報では、POLLIN や POLLOUT が、そのままの意味で、読み書きの方向を意味しない場合があります。

そのため、直接 pollfd の revents の値をチェックせずに、snd_pcm_poll_descriptors_revents() を使って、起こったイベントを取得する必要があります。

snd_pcm_poll_descriptors_revents() で取得できる revents では、書き込みが可能になったら POLLOUT が、読み込みが可能になったら POLLIN がセットされます (常にどちらか1つ)。

revents が 0 になる場合もあるので、その時は何もせずに、もう一度 poll() を実行します。
プログラム
非ブロッキングモードで、poll() を使って、書き込みが可能になるまで待ちながら、1秒分再生します。

$ cc -o 15-nonblock 15-nonblock.c util.c -lasound

#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <alsa/asoundlib.h>
#include "util.h"

#define SAMPRATE 48000

int poll_cnt;
struct pollfd *poll_pfd;

//書き込み可能になるまで待つ

static void _wait_event(snd_pcm_t *pcm)
{
    int ret;
    unsigned short ev;

    while(1)
    {
        ret = poll(poll_pfd, poll_cnt, -1);

        if(ret > 0)
        {
            if(snd_pcm_poll_descriptors_revents(pcm, poll_pfd, poll_cnt, &ev) == 0)
            {
                printf("event: %d\n", ev);
                if(ev & POLLOUT) return;
            }
        }
    }
}

static void _put_state(snd_pcm_t *pcm)
{
    snd_pcm_state_t state;

    state = snd_pcm_state(pcm);

    printf("> %s\n", snd_pcm_state_name(state));
}

int main(int argc,char **argv)
{
    snd_pcm_t *pcm;
    snd_pcm_uframes_t bufs,period;
    uint8_t *buf;
    int ret,frames;

    if(snd_pcm_open(&pcm,
        (argc >= 2)? argv[1]: "default", SND_PCM_STREAM_PLAYBACK,
        SND_PCM_NONBLOCK))
    {
        return 1;
    }

    //ハードウェア構成 (16bit LE, 2ch)

    snd_pcm_set_params(pcm, SND_PCM_FORMAT_S16_LE,
        SND_PCM_ACCESS_RW_INTERLEAVED, 2, SAMPRATE, 1, 500 * 1000);

    snd_pcm_get_params(pcm, &bufs, &period);

    //poll

    poll_cnt = snd_pcm_poll_descriptors_count(pcm);

    poll_pfd = (struct pollfd *)malloc(sizeof(struct pollfd) * poll_cnt);
    
    snd_pcm_poll_descriptors(pcm, poll_pfd, poll_cnt);

    printf("poll cnt: %d\n", poll_cnt);

    //書き込み

    buf = (uint8_t *)malloc(SAMPRATE * 4);

    util_write_buf_sawtooth(buf, SAMPRATE, SAMPRATE, AUDIO_FREQ_DO);

    frames = 0;

    while(frames < SAMPRATE)
    {
        ret = snd_pcm_writei(pcm, buf + frames * 4, SAMPRATE - frames);

        if(ret >= 0)
        {
            printf("write: %d\n", ret);
            frames += ret;
        }
        else if(ret == -EAGAIN)
        {
            _wait_event(pcm);
            //snd_pcm_wait(pcm, SND_PCM_WAIT_IO);
        }
        else
            printf("error: %d\n", ret);
    }

    free(buf);
    
    //drain

    ret = snd_pcm_drain(pcm);

    printf("> drain: %d\n", ret);

    if(ret == -EAGAIN)
    {
        _put_state(pcm);

        ret = snd_pcm_wait(pcm, SND_PCM_WAIT_DRAIN);
        printf("> wait:%d\n", ret);

        _put_state(pcm);
    }

    //

    free(poll_pfd);
    
    snd_pcm_close(pcm);

    return 0;
}
実行結果
poll cnt: 1
write: 16384
event: 0
event: 0
event: 4
write: 1736
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2056
event: 0
event: 4
write: 2040
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2056
event: 0
event: 4
write: 2040
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 2048
event: 0
event: 4
write: 1208
> drain: -11
> DRAINING
> wait:1
> DRAINING

最初にバッファ全体の 16384 frames を書き込んだ後は、-EAGAIN が返るたびに、poll() で書き込み可能になるまで待ちます。
poll の代わりに、snd_pcm_wait(pcm, SND_PCM_WAIT_IO) を使うことも出来ます。

snd_pcm_poll_descriptors_revents() で取得したイベントが、0 になっている場合があることに、注意してください。
この場合、POLLOUT が来るまで、再度 poll() を実行する必要があります。

poll() で待ってから書き込みを行うことを続けて、1秒分のデータがすべて書き込めたら、drain で再生が終わるまで待ちます。
drain 後は、PCM が DRAINING の状態になっています。

drain では、実際には再生が終わるまで待つことはなく、戻り値で -EAGAIN が返ってくるので、snd_pcm_wait(pcm, SND_PCM_WAIT_DRAIN) で、実際に再生が終わるまで待ちます。

ただし、直後に PCM 状態を取得しても、DRAINING のままになっています。
書き込みが可能になった時に、wait が戻っているような気がします。
hw
"hw" で開いた場合は、drain 後、wait で -EIO (I/O Error) が返っています。
その後、SETUP 状態になっているので、再生を待つことは出来ているようです。

$ ./15-nonblock hw
poll cnt: 1
write: 24000
write: 48
event: 4
write: 12000
write: 8
event: 4
write: 11944
> drain: -11
> DRAINING
> wait:-5
> SETUP

どうやら、ハードウェアと直接通信するかどうかで、drain を待つ動作が異なるようです。

ブロックモードの場合、drain 後は正常に SETUP 状態になるので、正常に drain を待ちたい場合は、ブロックモードに変更してから drain を行った方が良いでしょう。