LECTURE 2 まずは音を出してみよう

○ はじめに

ここではまず、以降のプログラミングで必要な知識である PCM ( Pulse Code Modulation ) サウンドについて簡単に説明します。その後、ある音程の音をスピーカから出力するだけの簡単なサンプルプログラムについて説明します。

○ PCM データの基礎

音は、時間に応じて連続的に大きさが変化するアナログデータです。これに対して、PC 上で音を扱うためにはデジタルデータとして扱わなければなりません。アナログ音源を一定時間毎に AD 変換して得られた値(点)をその瞬間における音データとし、その点の集まりとして表現される音を PCM ( Pulse Code Modulation ) サウンドと呼びます。

--- 図 1 ---
002sound.jpg

図 1 の実線がアナログデータであり、実線上に点在する点データが PCM データです。本文書以降のプログラミングにおいては、この PCM データを扱います。

PCM データを扱う際に重要な値は、量子化ビット数およびサンプリング周波数です。量子化ビット数とは、アナログデータを AD 変換して得られたデータを実際に何ビットの数として表現するかを決める値です。これに付随して、データを符号無しで表現するか、符号付きで表現するかという要素も存在します。サンプリング周波数は、アナログデータから 1 秒間に何回 AD 変換してデータを取得するかを決定する値です。PCM に固有というわけではありませんが、チャンネル数という言葉も覚えておいて下さい。モノラル音なら 1 チャンネル、ステレオ音なら 2 チャンネル ( 左と右 )ということになります。DVD では 5.1 チャンネルという言葉を良く聞きますよね。

ちなみに、電話では量子化ビット数 8 bit、サンプリング周波数 8000 Hz、1 チャンネル 程度であり、CD では量子化ビット数 16 bit、サンプリング周波数 44100 Hz、2 チャンネルです。

○ サンプルプログラム

初めてのサンプルプログラムは、ただ音を出力するだけのプログラムです。A の音(つまりラ) を 5 秒間出力し続けて終了する sound.c です。非常に単純ですが、OSS API の使い方としてはある意味全てを網羅していると言えます。はじめに のページから、全てのサンプルプログラムを固めた tar ボールをダウンロードすることもできます。

gcc -o sound sound.c -lm

としてコンパイルします。コンパイルオプションは各自の環境などに応じて修正して下さい。実行すると、特にエラーが無ければスピーカーからラの音が 5 秒程度出力されます。何かエラーが表示される場合は、システムでサウンドカードが正常にセットアップされているかどうか確認して下さい。

今回は /dev/mixer を全くいじっていないので、音量の大きさはスピーカーのボリュームのみで調整して下さい。スピーカーのボリュームをいくらいじっても音が聞こえない場合は、ミキサーのボリュームが 0 になっている可能性があります ( 特に ALSA を使っている場合にありがち )。その場合は、ミキサー操作用ソフトを使って設定してみて下さい ( これ以降のサンプルでも同様 )。なお、/dev/mixer を自分で操作する方法についてはいずれ解説します。

--- sound.c ---

    1	#include <fcntl.h>
    2	#include <limits.h>
    3	#include <linux/soundcard.h>
    4	#include <math.h>
    5	#include <stdio.h>
    6	#include <sys/ioctl.h>
    7	#include <unistd.h>
    8	
    9	
   10	/*
   11	 * 量子化ビット数 : 16 bits
   12	 * サンプリング周波数 : 44.1 KHz
   13	 * チャンネル数 : 1
   14	 * 再生時間 : 5 sec
   15	 *
   16	 * 分のバッファ領域
   17	 *
   18	 * 16 (bits) * 44100 (Hz) * 5 (sec) = 3528000 (bits)
   19	 *                                  = 441000  (bytes)
   20	 */
   21	#define BUFSIZE 441000
   22	
   23	static int setup_dsp( int fd );
   24	
   25	int 
   26	main( void )
   27	{
   28		/* 再生時間 5 秒 */
   29		double total = 5.0, t; 
   30	
   31		/* 再生音周波数 440 Hz ( A ) */
   32		double freq = 440.0;
   33	
   34		short  buf[ BUFSIZE / sizeof(short) ];
   35		int    fd, i;
   36	
   37	
   38		if ( ( fd = open( "/dev/dsp", O_WRONLY ) ) == -1 ) {
   39			perror( "open()" );
   40			return 1;
   41		}
   42	
   43	
   44		/* /dev/dsp の設定 */
   45		if ( setup_dsp( fd ) != 0 ) {
   46			fprintf( stderr, "Setup /dev/dsp failed.\n" );
   47			close( fd );
   48			return 1;
   49		}
   50	
   51	
   52		/* 再生用の正弦波データ作成 */
   53		for ( i = 0; i < BUFSIZE / sizeof(short); i ++ ) {
   54			t = ( total / (BUFSIZE / sizeof(short)) ) * i;
   55			buf[i] = SHRT_MAX * sin( 2.0 * 3.14159 * freq * t );
   56		}
   57	    
   58	
   59		if ( write( fd, buf, BUFSIZE ) == -1 ) {
   60			perror( "write()" );
   61			close( fd );
   62			return 1;
   63		}
   64	
   65		close( fd );
   66		return 0;
   67	}
   68	
   69	
   70	/*
   71	 * /dev/dsp を以下の様に設定する。
   72	 *
   73	 * 量子化ビット数     : 16   bits
   74	 * サンプリング周波数 : 44.1 KHz
   75	 * チャンネル数       : 1
   76	 * PCM データは符号付き、リトルエンディアン
   77	 *
   78	 */
   79	static int
   80	setup_dsp( int fd )
   81	{
   82		int fmt     = AFMT_S16_LE;
   83		int freq    = 44100;
   84		int channel = 1;
   85	
   86		/* サウンドフォーマットの設定 */
   87		if ( ioctl( fd, SOUND_PCM_SETFMT, &fmt ) == -1 ) {
   88			perror( "ioctl( SOUND_PCM_SETFMT )" );
   89			return -1;
   90		}
   91	
   92		/* チャンネル数の設定 */
   93		if ( ioctl( fd, SOUND_PCM_WRITE_CHANNELS, &channel ) == -1 ) {
   94			perror( "iotcl( SOUND_PCM_WRITE_CHANNELS )" );
   95			return -1;
   96		}
   97	
   98		/* サンプリング周波数の設定 */
   99		if ( ioctl( fd, SOUND_PCM_WRITE_RATE, &freq ) == -1 ) {
  100			perror( "iotcl( SOUND_PCM_WRITE_RATE )" );
  101			return -1;
  102		}
  103	
  104		return 0;
  105	}
  106	
  107	/* End of sound.c */
  108	

○ サンプルプログラムの説明

では、サンプルプログラムについて説明していきます。

  • 1 〜 7 行目

    必要なヘッダファイルをインクルードしています。OSS API を使ったサウンドプログラミング のために必要なヘッダファイルは、3 行目の soundcard.h です。このヘッダファイルにおいて OSS API や必要な定数などの定義が行われています。 OSS API によるプログラミングでは必ずこのヘッダファイルをインクルードします。

  • 10 〜 21 行目

    PCM データを格納するために必要なバッファ領域のサイズをマクロ定義しています。sound.c では、量子化ビット数 16 ビット、サンプリング周波数 44.1 KHz、チャンネル数 1 という PCM データを扱います。これは、1 点のデータを 16 ビットで表し、1 秒間に 44100 個の データを必要とするという意味です。 更にこのデータを 5 秒間分使用するため、必要なバッファ領域は 16 ( bits ) * 44100 (Hz) * 5 (sec) = 441000 (bytes) ということになります。 チャンネル数が 2 チャンネルの場合は左と右用に領域が必要となるため、この 2 倍のサイズが必要になります。

    2007/12/03 追記。34 行目で実際にバッファを確保していますが、short 型で確保しているにも関わらず BUFSIZE をそのまま要素数に指定していました (short buf[BUFSIZE];)。 正しくは short buf[BUFSIZE / sizeof(short)]; でした。堀さん、ご指摘ありがとうございました!

  • 25 〜 67 行目 ( main() )
    • 31 〜 32 行目

      sound.c では A ( ラ ) の音を 5 秒間出力します。この様に一つの音程のみを表す音は ある周波数の正弦波で表すことができます。A の周波数はここで宣言している様に 440 Hz です。

    • 38 〜 41 行目

      デバイスファイル /dev/dsp を open() して、ファイルディスクリプタを取得しています。 PCM データの入出力には、この様に /dev/dsp を使用します。ここでは出力のみに使用 するため、O_WRONLY (書き込み専用) で open() しています。読み出し専用の場合は O_RDONLY、読み書き両用の場合は O_RDWR とします。

      以下 2002/8/20 追記

      本来 OSS API では複数のプロセスから同じデバイスファイルを open() することはできません。システムコールはエラー終了し、errno に -EBUSY が設定されるはずです。が、現在カーネルと一緒に配付されている Linux Sound System で提供されているドライバや、サウンドチップのメーカなどが配付している Linux® 用ドライバなどに関しては、必ずしもそうとは限らない様です。

      例えば、自分の環境 ( これを書いている時点では AU8820B2 チップのサウンドカード + Vortex of Sound 提供の Linux 用ドライバ ) では、O_RDONLY ( 読み専用 ) と O_WRONLY ( 書き専用 ) であれば 2 つのプロセスが同時に open() できました。以前使っていた Sound Blaster Live! 用の emu10k1 ドライバでは読み専用とか書き専用とか関係無く複数 open() できた様な。。。 ( こちらは記憶があやふやです m(__)m )。

      正直な所、カードとドライバ次第というのが現状と見うけられます。

      追記ここまで
    • 44 〜 49 行目

      自作関数 setup_dsp() で /dev/dsp の設定を行っています。内容に関しては後述します。

    • 52 〜 56 行目

      A の音を表す正弦波データを 5 秒間分作成してバッファに格納しています。ある周波数 f の正弦波は、時刻を t、大きさを a とすると a sin( 2π f t ) と表せます。sound.c では最初のデータを時刻 0 とし、その後の時刻はバッファ全体に対するデータ位置の割合 によって決定しています。setup_dsp() の説明において後述しますが、signed short として PCM データを扱うため、最大値 SHRT_MAX ( 32767、limits.h で定義されている )、最小値 - SHRT_MAX の正弦波データを作成しています。

    • 59 〜 63 行目

      ここが PCM データ出力のメイン処理です!と言っても、write() で /dev/dsp に書き込んでいるだけです。OSS API では、PCMデータを /dev/dsp に書き込むだけで 音が出力されます。簡単でしょう?

      ここで注意しておいて欲しいことがあります。PCM データをサウンドドライバに 書き込む際、そのデータ量がサウンドドライバ内部のフラグメントと呼ばれるバッファ のサイズに満たない場合は再生されません ( サウンドドライバの内部バッファサイズの取得方法は LECTURE 3 /dev/dsp の機能を調べる で説明しています )。 続けてデータを書き込むことでフラグメントが一杯になるか、デバイスを close() すると再生されます。

      また、内部バッファが一杯になっている状態、もしくは書き込みデータ量よりも少ない領域しか 内部バッファに空きが無いような状態で write() を行った場合、write() がブロックしてしまいます。

      これだと、ゲームの効果音の様に短い音を書き込んですぐに再生されて欲しい様な場合や、 リアルタイム処理を行いたい場合には困ります。 これを避けるにはフラグメントのサイズを変更したり、 明示的に再生を指示したり、ブロックせずに書き込むことのできるフラグメントの数を 取得したりする必要があります。これらの方法については LECTURE 5 リアルタイム性を考慮する で紹介しますが、 ここではこの様な問題があることを覚えておいて下さい。

  • 70 〜 105 行目 ( setup_dsp() )

    /dev/dsp の設定を行うための関数です。デバイスの設定は全て ioctl() システムコール で行います。本関数では /dev/dsp を以下の様に設定します。 1 〜 4 をまとめてサンプリングパラメータと呼びます。

    1. 扱うデータは PCM形式、符号付き、リトルエンディアン
    2. 量子化ビット数 16 ビット
    3. チャンネル数 1
    4. サンプリング周波数 44100 Hz

    OSS API では、1 と 2 をまとめてサウンドフォーマットという形で提供しています ( 2 に ついては別個に設定することもできます )。soundcard.h を見ると提供されている フォーマットが分かりますが、その中のいくつかについて以下に示します。

    フォーマット名内容
    AFMT_MU_LAWmu-law エンコーディング形式
    AFMT_A_LAW a-law エンコーディング形式
    AFMT_U8 unsigned 8 bit、量子化ビット数が 8 の WAVE ファイルは この形式
    AFMT_S8 signed 8 bit
    AFMT_S16_LEsigned 16 bitでリトルエンディアン、量子化ビット数 16 の WAVE ファイルはこの形式
    AFMT_S16_BEsigned 16 bitでビッグエンディアン
    AFMT_U16_LEunsigned 16 bit でリトルエンディアン
    AFMT_U16_BEunsigned 16 bit でビッグエンディアン

    sound.c では AFMT_S16_LE を採用します。

    サンプリングパラメータの設定は、

    1. サウンドフォーマットの設定
    2. チャンネル数 ( モノラル、ステレオ )の設定
    3. サンプリング周波数の設定

    の順序で行う必要がある、と OSS のプログラマーズガイドには書かれています。 また、これらのパラメータを設定できるのは、デバイスを open() した後 read() や write()、 サンプリングパラメータの設定以外の ioctl() を発行する前であるとも書かれています。 ただし、前述のフラグメントのサイズを変化する際にはそちらを先に行うなどの例外はあります。 ので、基本的にはデバイスを open() したら行うと思っておけば良いと思います。

    パラメータを再度変更する際は、一度デバイスを close() して再度 open() するか、 ioctl() コマンドである SNDCTL_DSP_RESET を発行してデバイスをリセットする必要があります。SNDCTL_DSP_RESET については、 本ページの末尾 で説明しています。

    • 86 〜 90 行目

      先程説明したサウンドフォーマットの設定を行っています。サウンドフォーマットを 設定するには、ioctl() の第 2 引数に SNDCTL_DSP_SETFMT もしくは SOUND_PCM_SETFMT を指定します ( SOUND_PCM_SETFMT は SNDCTL_DSP_SETFMT のエイリアス )。 第 3 引数にはサウンドフォーマットを設定した変数のアドレスを指定します。

        int format;
        ioctl( fd, SOUND_PCM_SETFMT, &format );
      
      

      ioctl() が正常終了すると、実際に設定されたフォーマットが第 3 引数の変数に 格納されます。使用しているサウンドカードもしくはサウンドドライバによっては 指定したフォーマットをサポートしていない可能性もあるため、 厳密にはチェックが必要です。

      なお、量子化ビット数のみを設定する場合は、SOUND_PCM_WRITE_BITS を第 2 引数に 設定して、第 3 引数に 8 か 16 を設定した変数のアドレスを指定します。

        int bits = 16;
        ioctl( fd, SOUND_PCM_WRITE_BITS, &bits );
      
      

      これについてですが、僕の環境の OSS/Free 3.8.2 の soundcard.h を覗いてみると SOUND_PCM_WRITE_BITS は SNDCTL_DSP_SETFMT のエイリアスとなっています。 このこととサウンドフォーマットの定義値から考えると、SOUND_PCM_WRITE_BITS で 8 ビットを指定すると AFMT_U8 が、16 ビットを指定すると AFMT_S16_LE が選択される様です。

    • 92 〜 96 行目

      チャンネル数の設定を行っています。ioctl() の第 2 引数に SNDCTL_DSP_CHANNELS もしくは SOUND_PCM_WRITE_CHANNELS を指定し、第 3 引数にチャンネル数を設定した 変数のアドレスを指定します ( SOUND_PCM_WRITE_CHANNELS は SNDCTL_DSP_CHANNELS のエイリアス )。

        int channels;
        ioctl( fd, SOUND_PCM_WRITE_CHANNELS, &channels );
      
      

      ioctl() が正常終了すると、実際に設定されたチャンネル数が第 3 引数の変数に 格納されます。これについても厳密にはチェックが必要となります。

    • 98 〜 102 行目

      サンプリング周波数の設定を行っています。ioctl() の第 2 引数に SNDCTL_DSP_SPEED もしくは SOUND_PCM_WRITE_RATE を指定し、第 3 引数にサンプリング周波数を Hz 単位で設定した変数のアドレスを指定します ( SOUND_PCM_WRITE_RATE は SNDCTL_DSP_SPEED のエイリアス )。

        int freq;
        ioctl( fd, SOUND_PCM_WRITE_RATE, &freq );
      
      

      ioctl() が正常終了すると、実際に設定されたサンプリング周波数が第 3 引数の 変数に格納されます。これについても、厳密にはチェックが必要となります。


sound.c の説明は以上です。OSS API を使ったプログラミングでは、スピーカーから音を出すのは凄く簡単なことだというのが分かっていただけたのではないでしょうか?

○ デバイスのリセット ( SNDCTL_DSP_RESET )

サンプルプログラムの説明の際にふれた SNDCTL_DSP_RESET コマンドは、デバイスの再生や録音動作を即座に停止したい場合や、サンプリングパラメータなどのパラメータを新たに設定したい場合に使用します。

下記の ioctl() を実行すると、その時点で行われている再生、録音動作は即座に停止されます。そしてドライバは各種のパラメータ設定を受け付けることのできる状態となります。なお、SNDCTL_DSP_RESET の代わりに SOUND_PCM_RESET を使うこともできます( SOUND_PCM_RESET は SNDCTL_DSP_RESET のエイリアス )。

ioctl( fd, SNDCTL_DSP_RESET, 0 );

第 3 引数は 0 を設定すれば良いです ( 正確には、この引数は使われない )。

新たにパラメータ設定を受け付けることのできる状態になるとは言いましたが、このコマンドを使ってパラメータの再設定を行うのはあまり良くない様です。OSS のプログラマーズガイドには、このコマンドは再生、録音を停止したい場合にのみ必要とされ、発行後はデバイスを一度 close() して再度 open() することをお勧めするといった内容が書かれています。

音楽プレーヤーの様なアプリケーションで、再生中に停止ボタンが押された時に再生を止める、といった場合に使うことになるでしょう。