LECTURE 5 リアルタイム性を考慮する

○ はじめに

ここまでの LECTURE で録音 / 再生のやり方について一通り説明してきました。しかし、

  • ゲームの効果音の様な短い音を書き込んだ後、すぐに再生して欲しい
  • リアルタイムアプリケーションを作りたいので、データの読み書きが出来ない
    ( バッファが一杯 )時は read() や write() でブロックして欲しくない

といった要求には、ここまでの知識では対応できません。

そこでここでは、リアルタイム性の強い処理を行いたい場合に必要となる ( と僕が思う ) 内容に関して書いてみたいと思います。

○ DMA バッファと fragment

デバイスドライバ内部では、サウンドカードとのデータのやりとりのために DMA バッファが用意されています。そしてこの DMA バッファは fragment と呼ばれる単位で分割管理されています ( 図 1 )。

--- 図 1 ---
005fragment.jpg

アニメーションを行う様なアプリケーションではしばしば ( というかほとんど ) ダブルバッファリングという手法を使います。これと同じ様なもので、一つの fragment がデバイスとのやりとりでロックされている場合には他の fragment をアプリケーションとのやりとりに使うという仕組みを提供してくれるものです。

ダブルバッファリングではバッファは 2 つしか使いませんが、fragment は複数 ( もちろん 2 つの場合もあり得る ) 用意されます。これは、バッファ領域は大きくしたいけれども、デバイスとのやりとりでロックされる時間が長いと問題が生じるという点を解消するためのものです。ロックされる領域は小さくするけれども、バッファとしての総領域は大きいよという訳です。

録音、再生時にはこの fragment に関して以下の注意点が (主に) あります。

  1. write() でデータを書き込んでも、完全に満たされた fragment のデータしか再生されない
  2. 全ての fragment が埋まっていてバッファに空きが無いと write() がブロックする
  3. サンプリングしたデータで完全に満たされた fragment が無いと read() がブロックする
    ( 1 byte だけ読みたい場合でも )

再生時には fragment 単位で書き込む方がすぐに再生されるので良いでしょう。fragment の一部にデータが格納されている状態では、続けて書き込むかデバイスを close() しないと音は再生されません。ioctl SNDCTL_DSP_SYNC ( もしくは SOUND_PCM_SYNC、SOUND_PCM_SYNC は SNDCTL_DSP_SYNC のエイリアス ) コマンドを実行すると、write() で書かれた全てのデータが再生されるまで待ちます ( ioctl() がブロックする )。その後デバイスは初期化されます。

ioctl( fd, SNDCTL_DSP_SYNC, 0 );

サンプリングパラメータを変えたい場合や録音 / 再生モードを切替えたい場合には、この ioctl SNDCTL_DSP_SYNC コマンドもしくは ioctl SNDCTL_DSP_RESET コマンドを実行します。しかし、これらのコマンドを実行するよりはデバイスを再 open() する方が望ましいと OSS プログラマーズガイドには記述されています。

SNDCTL_DSP_SYNC と似たようなコマンドに ioctl SNDCTL_DSP_POST ( もしくは SOUND_PCM_POST ) コマンドがあります。

ioctl( fd, SNDCTL_DSP_POST, 0 );

これは、次のデータ書き込みまで間があることをドライバに通知するコマンドです。これを受けると残っている音が再生されますが、SNDCTL_DSP_SYNC と異なり ioctl() はブロックしません。

○ fragment のサイズを取得する

現在の fragment のサイズは、ioctl SNDCTL_DSP_GETBLKSIZE コマンドで取得することができます。

int fragsize;
ioctl( fd, SNDCTL_DSP_GETBLKSIZE, &fragsize );

第 3 引数に指定した変数に fragment のサイズが格納されます ( LECTURE 3 /dev/dsp の機能を調べる では単にバッファサイズと書いてました )。この fragment サイズの単位で入出力を行うのが基本となります。

この ioctl() コマンドを発行すると、まだ fragment サイズが計算されていない場合は既に設定されているサンプリングパラメータを使って fragment のサイズが計算されるらしいです ( OSS プログラマーズガイドより )。よって、このコマンドを発行する前にサンプリングパラメータを設定したり、必要ならば fragment サイズを指定すべきだと書かれています。

また、このコマンドはサンプリングパラメータを使った何らかの最適化を行うので、read() / write() を行う前の最後の ioctl() にすべきだとも書かれています ( このコマンドを発行するならば最後にしろ、という意味だと思います )。

○ fragment のサイズを指定する

ゲームの効果音などの様に短い音を再生する様な場合、デフォルトの fragment サイズでは大きすぎて "音が出ないー" などということになる可能性があります。この様な場合には、fragment のサイズを指定することができます ( 必ず指定通りになるとは限らないのですが )。

fragment のサイズを指定するには、ioctl SNDCTL_DSP_SETFRAGMENT コマンドを使います。

int arg = 0xMMMMSSSS;
ioctl( fd, SNDCTL_DSP_SETFRAGMENT, &arg );

arg = 0xMMMMSSSS となっていますが、SSSS で fragment のサイズを指定します。2^SSSS が fragment のサイズになります ( 2 の巾乗 )。SSSS の最小値は 4 ( 2^4 = 16 バイト )で、最大値は総バッファサイズの 1/2 です。といっても総バッファサイズをどうやって取得するのか現時点で僕は分かっていません m(__)m。

MMMM は fragment の最大数を指定します。MMMM の最小値は 2 で、最大値はデバイス用に確保できるバッファの大きさによるとだけ OSS プログラマーズガイドには書かれています。。。のはずなんですが、自分の環境で実験してみると MMMM より 1 個大きくなってる。。。?うーん、こっちが仕様なのかも。。。すいません、みなさん実験して見て下さい m(__)m。ちなみに特に制限を設けない場合は 0x7fff を設定して下さい。

この ioctl() を実行したからといって、必ずしも望み通りの fragment サイズが得られるわけではないことに注意して下さい。デバイスによっては固定だったり、全く同じサイズでは設定できなかったりします。よって、この後 ioctl SNDCTL_DSP_GETBLKSIZE コマンドを使って実際に設定された fragment サイズをチェックして下さい。ioctl SNDCTL_DSP_SETFRAGMENT コマンドの第 3 引数には意味のある値は返って来ません。

ioctl SNDCTL_DSP_SETFRAGMENT コマンドは open() 直後、他の ioctl() より前に実行しないといけない様です。また、もう一度設定する様な場合は一度デバイスを close() して再 open() しなければならないと OSS プログラマーズガイドに書かれています。

なお、非常に小さな fragment サイズを指定した場合、再生音にノイズが入るなどの現象が起きてしまいます ( write() での書き込みが追いつかない )。ユーザから直接指定されるのでない限り、256 バイト以下の fragment サイズはお勧めできないそうです ( OSS プログラマーズガイドより )。経験としては、そんなに小さくしないといけない状況はあんまり無いと思います。

○ ブロックしない入出力 ( read() / write() )

リアルタイム ( にしたい ) アプリケーションの場合、read() や write() 実行時にブロックされてしまうと非常に困ります。UNIX では、非ブロック処理のために select() システムコールが良く使われます。

select() では、ブロックせずに読み書きが可能なファイルディスクリプタを調べることができます。しかし、仮にブロックせずに書き込むことができるバイト数が 1 バイトであった場合、fragment サイズの書き込みを行うとやはりブロックしてしまいます ( fragment サイズが 1 バイトじゃなかったらですよ )。

この場合、ブロックせずに読み書きできる fragment のサイズをあらかじめ知ることができると非常に便利です。この目的のため ( ? ) に SNDCTL_DSP_GETISPACE ( もしくは SOUND_PCM_GETISPACE )、および SNDCTL_DSP_GETOSPACE ( もしくは SOUND_PCM_GETOSPACE ) という 2 つの ioctl() コマンドが存在します。

audio_buf_info buf;
ioctl( fd, SNDCTL_DSP_GETISPACE, &buf );
ioctl( fd, SNDCTL_DSP_GETOSPACE, &buf );

SNDCTL_DSP_GETISPACE は入力用、SNDCTL_DSP_GETOSPACE は出力用バッファの情報を取得します。取得した情報は、第 3 引数に設定した audio_buf_info 構造体変数に格納されます。audio_buf_info 構造体は <linux/soundcard.h> で定義されており、以下のメンバを保持しています。

  • int fragments

    ブロックせずに読み書きできる fragment の数。read() や write() において fragment サイズ単位で読み書きしている場合に限って信頼できる

  • int fragstotal

    バッファ用に確保されている fragment の総数

  • int fragsize

    fragment のサイズ。ioctl SNDCTL_DSP_GETBLKSIZE で得られる値と同じ

  • int bytes

    ブロックせずに読み書きできるバイト数

select() システムコールから戻って来た後にこれらの ioctl() コマンドを使って、ブロックせずに読み書きできる実際の fragment の数を取得すれば read() や write() でブロックせずに済みます!