ALSA:MMAP

MMAP
今までは、snd_pcm_writei() を使ってデータを書き込んできましたが、ここでは、MMAP を使って、取得したバッファに直接データを書き込む方法を紹介します。
アクセスタイプ
まず、ハードウェア構成で、アクセスタイプを MMAP に指定する必要があります。

SND_PCM_ACCESS_MMAP_INTERLEAVED
SND_PCM_ACCESS_MMAP_NONINTERLEAVED
SND_PCM_ACCESS_MMAP_COMPLEX
アクセスを開始
int snd_pcm_mmap_begin(snd_pcm_t *pcm, const snd_pcm_channel_area_t **areas,
    snd_pcm_uframes_t *offset, snd_pcm_uframes_t *frames);

typedef struct _snd_pcm_channel_area {
    void *addr;         //ベースアドレス
    unsigned int first; //最初のサンプルのオフセット (ビット単位)
    unsigned int step;  //サンプルの距離 (ビット単位)
} snd_pcm_channel_area_t;

MMAP でのアクセスを開始して、情報を取得します。

areas各チャンネルの領域情報の配列ポインタが返ります。
インターリーブなら、1つのみ。
非インターリーブなら、チャンネル数分あります。
offsetMMAP 領域のオフセット位置 (フレーム単位) が返ります。
addr のベースアドレスに、この位置を加算する必要があります。
frames読み書きしたいフレーム数をセットします。
実際に読み書きできるフレーム数が返ります。
バッファがいっぱいの時などは、0 が返る場合があります。

この関数を実行する直前に、snd_pcm_avail_update() 関数を呼び出す必要があります。
でないと、frames に間違った数が返る可能性があります。

この関数の後、addr に offset を加えた位置に、データを書き込みます。
読み書き可能なフレーム数を返す
snd_pcm_sframes_t snd_pcm_avail_update(snd_pcm_t *pcm);

読み書き可能なフレーム数を返します。

snd_pcm_avail() と違い、リングバッファ上のハードウェア位置と同期しません。
アクセスを完了する
snd_pcm_sframes_t snd_pcm_mmap_commit(snd_pcm_t *pcm,
    snd_pcm_uframes_t offset, snd_pcm_uframes_t frames);

MMAP へのアクセスを完了します。
snd_pcm_mmap_begin() と対で実行する必要があります。

offsetbegin で返されたオフセット値を指定する
frames実際に読み書きしたフレーム数を指定する。
begin で返されたフレーム数を超えないこと。
戻り値転送されたフレーム数。負の値でエラーコード
処理方法
MMAP を使用する場合は、snd_pcm_writei() と同じような動作にはならないので、注意してください。

例えば、MMAP のバッファにデータを書き込んでも、自動的に再生が開始されることはありません。
また、ブロックモードの場合に、すべてのフレーム数が書き込めるようになるまで、再生しながら待つといったことも出来ません。

MMAP によるアクセスは、ブロックモードが選択されていても、非ブロッキングモードと同じような扱いをする必要があります。
待つ
snd_pcm_mmap_begin() を行う前に、snd_pcm_avail_update() の実行が必要になるので、まずは snd_pcm_avail_update() で、書き込み可能なフレーム数を取得します。

最初の書き込みであれば、バッファサイズと同じ値が返るので、そのまま普通に書き込みます。

次の書き込みを行う前に、また snd_pcm_avail_update() を実行しますが、書き込み後、時間を置かずに実行した場合は、ごく小さな値だけが返ります。

小さなサイズで書き込むのは効率が悪いので、返った値が一定サイズ (ピリオドサイズなど) 以下であれば、snd_pcm_wait(pcm, SND_PCM_WAIT_IO) を実行して、書き込み可能になったというイベントが起こるまで待ちます。
(wait は、ブロックモードに関係なく使用できます)

wait は、内部で poll() を使っているので、ソフトウェア構成の snd_pcm_sw_params_get_avail_min() で取得できるフレーム数以上が、読み書きできるようになった時に、関数が戻ってきます。

その後、再び snd_pcm_avail_update() を実行すれば、大きな値が返ってくるので、そのフレーム数で読み書きを行います。
注意点
最初にバッファサイズ全体分のデータを書き込んだとしても、PCM は自動的に再生を開始しません。
そのため、最初の書き込みが終わったら、手動で snd_pcm_start() を実行する必要があります。

アンダーラン状態になった場合は、自動的に再生が停止します。
この場合、snd_pcm_avail_update() 時に -EPIPE が返りますが、snd_pcm_mmap_begin() では 0 が返ります。
バッファの書き込み自体は成功しますが、snd_pcm_mmap_commit() 時には -EPIPE が返ります。

バッファの書き込みは、デバイスとは関係ないところで行われるため問題ないが、デバイスにデータを送る時点でエラーが出る、ということになります。

そのため、アンダーランは、snd_pcm_avail_update() の時点で判定するようにします。
プログラム
MMAP を使って、1秒分のデータを書き込みます。

$ cc -o 18-mmap 18-mmap.c util.c -lasound

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

#define SAMPRATE 48000

static void _write_buf(snd_pcm_t *pcm,uint8_t *buf,int buf_frames)
{
    const snd_pcm_channel_area_t *area;
    snd_pcm_uframes_t offset,frames;
    snd_pcm_sframes_t avail,retf;
    int first = 1;

    while(buf_frames > 0)
    {
        //1024 フレーム以上が有効になるまで待つ
    
        while(1)
        {
            avail = snd_pcm_avail_update(pcm);
            if(avail >= 1024) break;

            printf("# wait (avail:%ld)\n", avail);
            snd_pcm_wait(pcm, SND_PCM_WAIT_IO);
        }

        //書き込むフレーム数

        frames = buf_frames;
        if(frames > avail) frames = avail;

        printf("avail: %ld, write_frames: %lu\n", avail, frames);

        //開始

        snd_pcm_mmap_begin(pcm, &area, &offset, &frames);

        printf("> begin: %lu frames ", frames);
        printf("[area] first:%u, step:%u\n", area->first, area->step);

        //書き込み

        memcpy((uint8_t *)area->addr + offset * 4, buf, frames * 4);

        retf = snd_pcm_mmap_commit(pcm, offset, frames);
        printf("> commit: %ld frames\n", retf);

        //最初の書き込み後、再生を開始

        if(first)
        {
            snd_pcm_start(pcm);
            first = 0;
        }

        //次へ

        buf += retf * 4;
        buf_frames -= retf;
    }
}

int main(int argc,char **argv)
{
    snd_pcm_t *pcm;
    uint8_t *buf;

    if(snd_pcm_open(&pcm,
        (argc >= 2)? argv[1]: "default", SND_PCM_STREAM_PLAYBACK, 0))
    {
        printf("open error\n");
        return 1;
    }

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

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

    //書き込み

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

    util_write_buf_sawtooth(buf, SAMPRATE, SAMPRATE, AUDIO_FREQ_DO);

    _write_buf(pcm, buf, SAMPRATE);

    free(buf);
    
    //

    snd_pcm_drain(pcm);

    snd_pcm_close(pcm);

    return 0;
}
実行結果
avail: 16384, write_frames: 16384
> begin: 16384 frames [area] first:0, step:32
> commit: 16384 frames
# wait (avail:24)
avail: 1808, write_frames: 1808
> begin: 1808 frames [area] first:0, step:32
> commit: 1808 frames
# wait (avail:8)
avail: 2048, write_frames: 2048
> begin: 2048 frames [area] first:0, step:32
> commit: 2048 frames
# wait (avail:8)
avail: 2048, write_frames: 2048
> begin: 2048 frames [area] first:0, step:32
> commit: 2048 frames
# wait (avail:8)
avail: 2040, write_frames: 2040
> begin: 2040 frames [area] first:0, step:32
> commit: 2040 frames
# wait (avail:8)
avail: 2040, write_frames: 2040
> begin: 2040 frames [area] first:0, step:32
> commit: 2040 frames
# wait (avail:8)
avail: 2048, write_frames: 2048
> begin: 2048 frames [area] first:0, step:32
> commit: 2048 frames
# wait (avail:8)
avail: 2048, write_frames: 2048
> begin: 2048 frames [area] first:0, step:32
> commit: 2048 frames
# wait (avail:8)
avail: 2048, write_frames: 2048
> begin: 2048 frames [area] first:0, step:32
> commit: 2048 frames
# wait (avail:8)
avail: 1040, write_frames: 1040
> begin: 256 frames [area] first:0, step:32
> commit: 256 frames
# wait (avail:788)
avail: 1792, write_frames: 1792
> begin: 1792 frames [area] first:0, step:32
> commit: 1792 frames
# wait (avail:8)
avail: 1040, write_frames: 1040
> begin: 1040 frames [area] first:0, step:32
> commit: 1040 frames
# wait (avail:8)
avail: 2040, write_frames: 2040
> begin: 2040 frames [area] first:0, step:32
> commit: 2040 frames
# wait (avail:8)
avail: 2040, write_frames: 2040
> begin: 2040 frames [area] first:0, step:32
> commit: 2040 frames
# wait (avail:8)
avail: 1040, write_frames: 1040
> begin: 1040 frames [area] first:0, step:32
> commit: 1040 frames
# wait (avail:8)
avail: 2040, write_frames: 2040
> begin: 2040 frames [area] first:0, step:32
> commit: 2040 frames
# wait (avail:8)
avail: 2040, write_frames: 2040
> begin: 2040 frames [area] first:0, step:32
> commit: 2040 frames
# wait (avail:8)
avail: 2048, write_frames: 2048
> begin: 2048 frames [area] first:0, step:32
> commit: 2048 frames
# wait (avail:8)
avail: 1040, write_frames: 1040
> begin: 1040 frames [area] first:0, step:32
> commit: 1040 frames
# wait (avail:8)
avail: 2040, write_frames: 112
> begin: 112 frames [area] first:0, step:32
> commit: 112 frames

一番最初は、バッファサイズ分のデータを書き込みます。

領域情報の first と step は、ビット情報になっていますが、ここでは使用する必要はありません。

最初に書き込んだ後の avail_update では、24 が返っているので、wait で待ちます。
すると、avail_update で 1808 が返るので、次はこのサイズで書き込みます。

以降は、1秒分のデータが書き込めるまで繰り返します。

なお、エラー処理は全く行っていませんが、実際は、各処理で負の値のエラーコードが返った場合、適切に処理する必要があります。