再生
では、ここから PCM の再生を行っていきます。
サウンドデータは、ハードウェア構成で設定したサイズのバッファに書き込んでいきます。
書き込まれたデータは、ALSA によって読み込まれて、順次再生されていきます。
サウンドデータは、ハードウェア構成で設定したサイズのバッファに書き込んでいきます。
書き込まれたデータは、ALSA によって読み込まれて、順次再生されていきます。
書き込み方法
バッファへの書き込み方法は、2通りあります。
これらのアクセス方法は、ハードウェア構成の「アクセスタイプ」で設定します。
開いた PCM によって、対応しているアクセスタイプが異なる場合があります。
(ハードウェアに直接出力する場合、非インターリーブが使えないなど)。
アクセスタイプを指定しなかった場合は、一番最初の SND_PCM_ACCESS_MMAP_INTERLEAVED が選択される可能性が高いです。
- 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 が選択される可能性が高いです。
インターリーブ形式
アクセスタイプでは、サンプルのインターリーブ形式も選択する必要があります。
インターリーブ形式によって、読み書きに使用する関数が異なります。
関数名が writei, readi なら、インターリーブ形式。
writen, readn なら、非インターリーブ形式となります。
- インターリーブ形式 (INTERLEAVED)
"LRLR..." というように、一つのバッファに、各チャンネルが交互に並びます。 - 非インターリーブ形式 (NONINTERLEAVED)
"LLL..." "RRR..." というように、チャンネルごとのバッファに、データが連続しています。 - COMPLEX
圧縮エンコードなど。
MMAP を使う方法でしか書き込めません。
インターリーブ形式によって、読み書きに使用する関数が異なります。
関数名が writei, readi なら、インターリーブ形式。
writen, readn なら、非インターリーブ形式となります。
書き込み
SND_PCM_ACCESS_RW_INTERLEAVED または SND_PCM_ACCESS_RW_NONINTERLEAVED のアクセスタイプで書き込む場合は、以下の関数を使います。
size は、書き込むフレーム数です。
bufs は、各チャンネルのバッファの配列。
戻り値は、実際に書き込まれたフレーム数。負の値でエラーコード。
ブロックモードの場合は、基本的に指定したサイズ分がすべて書き込まれますが、シグナルが発生した時や、アンダーラン (再生が書き込みを追い越した) が発生した場合は、指定したサイズより小さい値が返ることがあります。
//インターリーブ 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 を使います。
ソースコードをダウンロードして、一緒にコンパイルしてください。
第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() を使います。
これが成功すると、SND_PCM_STATE_PREPARED 状態になり、start で開始することができます。
snd_pcm_hw_params() などでのハードウェア構成のインストール時は、自動的に snd_pcm_prepare() が実行されるので、明示的に行う必要はありませんが、drop/drain で実行を停止した後に、再度開始したい場合は、snd_pcm_prepare() を実行する必要があります。
これは、ハードウェア構成がインストールされているが、再生/録音の準備はできていないという状態です。
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 キーを押します。
なお、左右のチャンネルごとにミュートを指定できるので、各チャンネルのミュートを切り替えたい場合は、< > キーを押します。
"default" または "front" を指定した場合は、アナログのヘッドホン端子かラインアウトに出力されます。
両方の端子が接続されている場合は、それぞれがミュート状態でなければ、両方に出力されます。
alsamixer で、"Headphone" と "Front" の所が MM (ミュート) になっていないか、確認してください。
00 で、ミュート解除状態です。
ミュートを変更するには、←→ キーで変更したい音量を選択して、M キーを押します。
なお、左右のチャンネルごとにミュートを指定できるので、各チャンネルのミュートを切り替えたい場合は、< > キーを押します。
サンプリングレート
サンプリングレートを 8000 Hz に指定した場合、"default" ならそのまま再生できますが、"hdmi" や "hw" などを指定した場合、以下のようなエラーが出ます。
8000 Hz が要求されたが、利用可能で一番近い値は 〜Hz である、ということです。
"default" や "plughw" の場合は、プラグインでサンプルの変換が可能になっているので、ハードウェアが対応していないフォーマットやサンプリングレートでも出力できます。
"front" "hdmi" "hw" などの場合は、ハードウェアと直接通信する形なので、ハードウェアが対応していないサンプリングレートでは出力できません。
サンプルの変換を行わない場合、アナログ出力の場合は、最低で 44100 Hz、HDMI なら最低で 32000 Hz のサンプリングレートでないと、再生できないということです。
$ ./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 のサンプリングレートでないと、再生できないということです。