ALSA:再生書き込み

再生
では、ここから PCM の再生を行っていきます。

サウンドデータは、ハードウェア構成で設定したサイズのバッファに書き込んでいきます。

書き込まれたデータは、ALSA によって読み込まれて、順次再生されていきます。
書き込み方法
バッファへの書き込み方法は、2通りあります。

  • snd_pcm_writei(), snd_pcm_writen() を使って、指定したバッファの内容を書き込む。
  • MMAP 領域を使って、バッファのポインタを取得し、そこに直接書き込む。
    もしくは、snd_pcm_mmap_writei(), snd_pcm_mmap_writen() を使って書き込む。

これらのアクセス方法は、ハードウェア構成の「アクセスタイプ」で設定します。

//MMAP を使う
SND_PCM_ACCESS_MMAP_INTERLEAVED
SND_PCM_ACCESS_MMAP_NONINTERLEAVED
SND_PCM_ACCESS_MMAP_COMPLEX

//snd_pcm_writei(), snd_pcm_writen() を使う
SND_PCM_ACCESS_RW_INTERLEAVED
SND_PCM_ACCESS_RW_NONINTERLEAVED

開いた PCM によって、対応しているアクセスタイプが異なる場合があります。
(ハードウェアに直接出力する場合、非インターリーブが使えないなど)。

アクセスタイプを指定しなかった場合は、一番最初の SND_PCM_ACCESS_MMAP_INTERLEAVED が選択される可能性が高いです。
インターリーブ形式
アクセスタイプでは、サンプルのインターリーブ形式も選択する必要があります。

  • インターリーブ形式 (INTERLEAVED)
    "LRLR..." というように、一つのバッファに、各チャンネルが交互に並びます。
  • 非インターリーブ形式 (NONINTERLEAVED)
    "LLL..." "RRR..." というように、チャンネルごとのバッファに、データが連続しています。
  • COMPLEX
    圧縮エンコードなど。
    MMAP を使う方法でしか書き込めません。

インターリーブ形式によって、読み書きに使用する関数が異なります。

関数名が writei, readi なら、インターリーブ形式。
writen, readn なら、非インターリーブ形式となります。
書き込み
SND_PCM_ACCESS_RW_INTERLEAVED または SND_PCM_ACCESS_RW_NONINTERLEAVED のアクセスタイプで書き込む場合は、以下の関数を使います。

//インターリーブ
snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size);

//非インターリーブ
snd_pcm_sframes_t snd_pcm_writen(snd_pcm_t *pcm, void **bufs, snd_pcm_uframes_t size);

size は、書き込むフレーム数です。
bufs は、各チャンネルのバッファの配列。
戻り値は、実際に書き込まれたフレーム数。負の値でエラーコード。

ブロックモード
PCM を開いた時に、mode を 0 (ブロックモード) にした場合、指定したサイズのデータをバッファに書き込めない時は、再生によってバッファが空いて、すべてのデータが書き込めるようになるまで、関数はブロックされます。

ブロックモードの場合は、基本的に指定したサイズ分がすべて書き込まれますが、シグナルが発生した時や、アンダーラン (再生が書き込みを追い越した) が発生した場合は、指定したサイズより小さい値が返ることがあります。
開始と停止
開始
int snd_pcm_start(snd_pcm_t *pcm);

start で、PCM の再生/録音を開始します。

ただし、デフォルトでは、一定サイズのデータがバッファに存在する場合、自動的に開始するようになっているので、ある程度書き込みを行えば、自動的に再生が開始されます。
停止
int snd_pcm_drop(snd_pcm_t *pcm);
int snd_pcm_drain(snd_pcm_t *pcm);

PCM を停止したい場合は、2種類の方法があります。

drop は、バッファに残っているデータを無視して、即座に停止します。
drain は、バッファに残っているデータを保持して、停止します。

再生の場合、drain を行うと、バッファ上の未再生のデータが、実際に再生し終わるまで待ってから、停止します。
一時停止
int snd_pcm_pause(snd_pcm_t *pcm, int enable);

一時停止または再開を行います。

※すべてのハードウェアでサポートされているとは限りません。
snd_pcm_hw_params_can_pause() で、サポートしているか確認できます。
読み書き可能なフレーム数
snd_pcm_sframes_t snd_pcm_avail(snd_pcm_t *pcm);

バッファ上で、読み書きが可能なフレーム数を返します。
負の値でエラーコード。

再生時の場合は、バッファに書き込めるフレーム数を意味します。

この関数は、ハードウェア上の位置と同期します。
プログラム
16bit LE, 2ch で、500 ms のバッファ長さを目安に設定し、1秒分ののこぎり波のバッファを用意して、一気に書き込みます。

第1引数は、PCM 定義名 (省略で "default")。
第2引数は、サンプリングレート (Hz)。省略で 48000。

ここからは、ユーティリティ関数として util.c を使います。
ソースコードをダウンロードして、一緒にコンパイルしてください。

$ cc -o 14-write 14-write.c util.c -lasound

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

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

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

    if(argc >= 3)
        rate = atoi(argv[2]);
    else
        rate = 48000;

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

    if(snd_pcm_set_params(pcm, SND_PCM_FORMAT_S16_LE,
        SND_PCM_ACCESS_RW_INTERLEAVED, 2, rate, 1, 500 * 1000))
    {
        return 1;
    }

    snd_pcm_get_params(pcm, &bufs, &period);

    printf("buf size: %lu frames, %.2f ms\n", bufs, (double)bufs / rate * 1000);
    printf("period size: %lu\n", period);

    //開始

    //snd_pcm_start(pcm);

    printf("avail: %ld frames\n", snd_pcm_avail(pcm));

    //書き込み

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

    util_write_buf_sawtooth(buf, rate, rate, AUDIO_FREQ_DO);

    ret = snd_pcm_writei(pcm, buf, rate);

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

    free(buf);
    
    //

    printf("> drain\n");
    printf("avail: %ld frames\n", snd_pcm_avail(pcm));
    
    snd_pcm_drain(pcm);

    printf("> %s\n", snd_pcm_state_name(snd_pcm_state(pcm)));

    //
    
    snd_pcm_close(pcm);

    return 0;
}
実行結果
buf size: 16384 frames, 341.33 ms
period size: 1024
avail: 16384 frames
ret: 48000
> drain
avail: 752 frames
> SETUP

インターリーブ形式で書き込むので、アクセスタイプを SND_PCM_ACCESS_RW_INTERLEAVED にしています。

500 ms のバッファ長さを指定しましたが、実際は 341.33 ms 程度しかありません。

書き込む前に snd_pcm_avail() を実行してみると、バッファのフレーム数と同じなので、バッファ全体が書き込み可能な状態です。

1秒分のバッファを用意した後、start を行わずに、snd_pcm_writei() で一気に1秒分を書き込みます。
※本来はエラー処理などを行う必要がありますが、すべての書き込みに成功するという前提のコードになっています。

バッファの長さは 341 ms しかないので、当然一度にすべてを書き込むことはできません。
snd_pcm_writei() 内で、再生が進むのを待ちつつ、順次書き込みが行われていきます。

デフォルトでは、バッファ上に一定サイズのデータがあると、自動的に再生が開始するようになっているので、start を行わなくても、書き込みを行うだけで、再生が開始されます。

関数が戻った時、返った値は 48000 です。
1秒分のデータがすべて書き込まれたことになりますが、この時点ではまだ、書き込み済みで未再生のデータが残っています。

書き込み後に snd_pcm_avail() を行うと、752 frames となっています。
752 フレームが書き込み可能ということは、16384 - 752 = 15632 フレームは、再生中か未再生の状態です。

あとは、snd_pcm_drain() で、残りのデータが再生し終わるまで待ちます。
SETUP
drain 後は、PCM の状態が SND_PCM_STATE_SETUP になります。

これは、ハードウェア構成がインストールされているが、再生/録音の準備はできていないという状態です。

PCM が開始できるように準備するには、snd_pcm_prepare() を使います。

int snd_pcm_prepare(snd_pcm_t *pcm);

これが成功すると、SND_PCM_STATE_PREPARED 状態になり、start で開始することができます。

snd_pcm_hw_params() などでのハードウェア構成のインストール時は、自動的に snd_pcm_prepare() が実行されるので、明示的に行う必要はありませんが、drop/drain で実行を停止した後に、再度開始したい場合は、snd_pcm_prepare() を実行する必要があります。
HDMI ほか
プログラムの引数に "hdmi" を指定すると、HDMI に出力されます。

"default" または "front" を指定した場合は、アナログのヘッドホン端子かラインアウトに出力されます。
両方の端子が接続されている場合は、それぞれがミュート状態でなければ、両方に出力されます。

alsamixer で、"Headphone" と "Front" の所が MM (ミュート) になっていないか、確認してください。
00 で、ミュート解除状態です。

ミュートを変更するには、←→ キーで変更したい音量を選択して、M キーを押します。
なお、左右のチャンネルごとにミュートを指定できるので、各チャンネルのミュートを切り替えたい場合は、< > キーを押します。
サンプリングレート
サンプリングレートを 8000 Hz に指定した場合、"default" ならそのまま再生できますが、"hdmi" や "hw" などを指定した場合、以下のようなエラーが出ます。

$ ./run hw 8000
ALSA lib pcm.c:8868:(snd_pcm_set_params) Rate doesn't match (requested 8000Hz, get 44100Hz)

$ ./run hdmi 8000
ALSA lib pcm.c:8868:(snd_pcm_set_params) Rate doesn't match (requested 8000Hz, get 32000Hz)

8000 Hz が要求されたが、利用可能で一番近い値は 〜Hz である、ということです。

"default" や "plughw" の場合は、プラグインでサンプルの変換が可能になっているので、ハードウェアが対応していないフォーマットやサンプリングレートでも出力できます。

"front" "hdmi" "hw" などの場合は、ハードウェアと直接通信する形なので、ハードウェアが対応していないサンプリングレートでは出力できません。

サンプルの変換を行わない場合、アナログ出力の場合は、最低で 44100 Hz、HDMI なら最低で 32000 Hz のサンプリングレートでないと、再生できないということです。