「りも」トップページへ
TIPS index へ

サウンドを鳴らす - 基礎知識編

Apr.09.2000

Linux でサウンドを鳴らすプログラムを書く方法。基礎知識の説明。


・ サウンドドライバーとデバイス

Linux ではサウンドドライバー等のデバイスドライバーを組み込むと大抵それは デバイスとして扱えるようになります。デバイスは /dev 以下に置かれ(現れ)、 一般のファイルストリームと同等に扱える様になります。このあらゆるデバイス (メモリすら)をファイルと同等に扱えるというのは UNIX 系OSの特色であると 言っても良いでしょう。(plan9 とかではプロセスすらファイルとして扱えると 聞きました)
今回目的とするサウンドデバイスも当然 /dev の下にいます。そのサウンド デバイスへアクセスする具体的方法を説明します。

・ audio デバイス

表題の単語が若干怪しいけど、ここでは wave ファイルをサンプリングデータと して取り込み音声出力するデバイスの事を指します。PCM とか DSP とか言われる こともあるけど、それは中間の手法の単語だったりします。
Linux における audio デバイスドライバーでメジャーなのは OSS(Free) と ALSA の二つ。この二つのどっちを使うかでアクセス手段や /dev の名前が違います。 もちろん他 OS でも異なります。
ここでは Linux kernel に付いてくる OSS Free について解説します、他の ドライバーに関しては各人でお調べください。基本的なアクセス方法とかは 大体同じなので応用は効くと思います。
OSS のプログラミングに関しては OSS のサイトで入手 できます(英文)。他に 入手できる資料はあまりなかったのでこれを参照するのが現状ベターでしょう。 また、ハックの基本(そこまで大行でないけど)としてヘッダーファイルを参照 するのが大変有効な手段です。どんな define があるかとかどんな関数がある かとかはだいたいそこから掴めますから。

・ audio 出力、その1

デバイスを直に叩いてサウンドを鳴らしてみます。
Linux OSS ではサウンドデバイスは /dev/dsp になります。au 互換の インターフェースは /dev/audio だそうですけど。
ここに何か流し込めば取り敢えず音はなることになります。
% cat hogehoge > /dev/dsp
とかすればスピーカーからノイズが聞こえると思います。なにも聞こえて来ない ときはスピーカーの接続を確認するか、サウンドドライバーの動作を確認するか、 スピーカーのボリュームを上げてみるか、ファイルを換えてみるとかしてみて ください。
つまり通常的に /dev/dsp を open してそこにデータを write すればサウンドを 鳴らせるということです。ただ、先の例でサウンドデータを cat してもちゃんと した音声がなることはほとんど無いと思います。それは、サウンドデータに 準じたデータ形式やサンプリングレート等の指定がなされていないからです。 その指定をどのように行うかがこのセクションのトピックとなります。
そういったサンプリングレートとかの指定は /dev/dsp を open したあと、 そのファイルディスクリプタを経由して ioctl にて行います。
/*-------------------------------------------------------
  OSS - /dev/dsp  test
--------------------------------------------------------*/
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#include <sys/types.h>
#include <sys/stat.h>

#define  DSP  "/dev/dsp"
#define  WAV  "../kurumi.wav"
#define  BUFFER  2048

static unsigned char  tmp[BUFFER];

int  main(void)
{
  int fp, out;
  int len, format;

  /* --- デバイスのオープン */
  fp = open(WAV, O_RDONLY);
  out = open(DSP, O_WRONLY);

  /* --- 音声デバイス設定 */
  format = AFMT_S16_LE;  
  ioctl(out, SNDCTL_DSP_SETFMT, &format);
  format = 1;
  ioctl(out, SNDCTL_DSP_STEREO, &format);
  format = 44100;
  ioctl(out, SNDCTL_DSP_SPEED, &format);
  /* --- 出力ループ */
  while((len = read(fp, tmp, BUFFER)) > 0) {
    write(out, tmp, len);
  }
  close(fp);
  close(out);
}
WAV 部分は適当なサンプリングファイルで読み変えてください。 本当に必要な部分のみの抽出ですのでエラー時の処理が一切省いてあります、 実際にプログラミングするときには関数がエラーだったときの対応を 含める必要があるでしょう。 ちなみにこの例では wav ファイルのヘッダーまで出力しているので頭に 短いノイズが乗ります。
また、SNDCTL_DSP_* にてどういった指定項目があるかは先程の OSS Programing の サイトにて入手できるマニュアルを御参照ください。

・ Volume調整、その1

audio 出力の音量を変えるには MIXER デバイスを使用します。OSS では /dev/mixer に割り当てられています。
MIXER へのデータ指定をするのも ioctl にて行います。よって、 /dev/mixer を open しておく必要があります。
  int  mix, vol;
  mix = open("/dev/mixer", O_WRONLY);
  ioctl(mix, MIXER_WRITE(SOUND_MIXER_PCM), &vol);
MIXER の場合 read か write かがあるので適切に指定してください。 また、PCM 以外のデバイスの音量を指定する方法については資料を御参考 ください。

・ EsounD を扱う

デバイスにアクセスすることにより音声を鳴らす事ができました。 ですが、この方法だと OS だけでなくドライバーにも依存してしまいます。 そういったアプリ側のコード量負担を軽減し、なおかつより便利に音声を 扱えるよう既存のライブラリを使用してみます。
今回注目したのは ESD こと Enlightened Sound Daemon です。これは GNOME のサウンドドライバー としても採用されていますので、GNOME がインストールされている環境には いるものと思います。
EsounD の特色は esd という daemon がいて、その daemon にデータを 渡すことによって音声を鳴らしてもらうという構造にあります。 ソケットを用いて daemon にアクセスするので、IP Over で音声を鳴らす ことも可能です。クライアントにサウンドデバイスがない場合ネットワーク 経由で esd が動いているサウンドデバイス付きサーバーに音声発呼を 依頼するといった事が可能です。 daemon に複数の音声ファイルを渡すとそれらを合成して出力してくれる ので、BGM を鳴らしながら SE を発呼することとかが可能になります。 もちろん、異なるサンプリングレートの音声データを一度に与えた場合でも それぞれをうまく合成してきちんと鳴らしてくれますので利用する側(アプリ) でサンプリングレートをそんなに気にしなくても良いという利点もあります。
そのかわり音声合成作業が入るとそこそこのタスクを食うのですがそれは やむをえないといった所でしょうか。
それと EsounD をインストールする際に、システムにあわせたサウンド デバイスを認識するので、EsounD が認識できるドライバーや OS の範疇で 統一された audio インターフェースを扱えるというのも魅力です。 アプリケーション側でサウンドドライバーの差異を考慮しなくても良いの ですから。

EsounD を使おうと思い立ち「じゃあどうやって使うの?」となるわけですが、 残念なことに実は EsounD の API ドキュメントなるものはまだ存在していません。 じゃあなにもできないじゃないかと思われる人もいるかもしれませんが、 そうではありません。実際に動いているソースリストがあるじゃないですか。 というわけで esd.h というヘッダーファイルを中心に esdplay.c の様な サンプルプログラム、および esd 本体のソースをぐるぐると眺めながら なんとなく使い方を学びます。以下はそうして得られた結果だと思ってください。 流石に隅々までというわけには行きませんが、今自分が必要としている部分 に関しては記述してあります。

・ audio 出力、その2

EsounD を用いて音声を鳴らす方法についてです。
esd に音声発呼を依頼するには esd_play_stream_fallback() を用いて リクエストし、ソケットを繋ぎます。その際、データフォーマット、 サンプリングレート、esd がいるホストマシン、認識名を指定します。 ホストマシンに NULL を渡すと localhost になります。 戻り値はソケットで、以後このソケットに音声データを流し込むことに より音声が発呼されます。
それとは別に esd_open_sound() で esd とのソケットを繋ぎます。 これの戻り値は esd へのコマンド用ソケットで、これを介して esd の 動作を制御することができます。例えば現在の情報を得るとか、 サウンドデータの設定を変えるとかいった場合に用います。

#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <esd.h>

#define  WAV  "../kurumi.wav"
#define  BUFFER  4096

static unsigned char  tmp[BUFFER];

int  main(void)
{
  FILE *fp;
  int vol, count, len;
  int esd;
  int sock = -1, rate = ESD_DEFAULT_RATE;
  esd_format_t format = 0;
  unsigned char *host = NULL;
  esd_info_t  *esdctl;

  format = ESD_BITS16 | ESD_STEREO | ESD_STREAM | ESD_PLAY;

  /* --- デバイスのオープン */
  esd = esd_open_sound(host);
  fp = fopen(WAV, "r");
  sock = esd_play_stream_fallback(format, rate, host, WAV);

  esdctl = esd_get_all_info(esd);

  /* --- 出力ループ */
  while((len = fread(tmp, 1, BUFFER, fp)) > 0) {
    write(sock, tmp, len);
  }
  esd_close(esd);
  fclose(fp);
  close(sock);
}
このソース内では特に esd_get_all_info() の結果を使っていないので esd (esd_open_sound() の結果)は意味ないのですが、それは後で 使うということで記述を残してあります。
ぱっと見た限り /dev/dsp へのアクセスと全く同じですが、これで音声が 鳴っている間でも他の EsounD アプリ(esdplay とか)で音声を発呼しても 途切れることなく、音声が合成して出力されます。

・ Volume調整、その2

EsounD で鳴らしている音声の音量を変えましょう。
普通に MIXER を使って DSP の出力を絞っても良いのですがそれでは折角 esd を使っている意味が半減してしまいます。esd が管理している音声データ 毎に音量を設定して、トラック毎に音量を変えたり、SE の音量は固定したまま BGM をフェードアウトしたり、オーバーラップフェードで曲を入れ換えたり (Windowsの「終末の過ごし方」ってゲームで使われていた手法ですね) が出来るように仕向けます。
ボリューム設定の為のそれらしい関数を探すと esd_set_stream_pan() という ものが見付かります。esd と source id と左右それぞれのレベルを指定すると ボリュームが調整できそうです、audio ソース毎に指定できそうなので目的と する物でしょう。
で、引数の 2番目 source id ってなんぞや?ってなわけでちょっと詰まりました。 esd_play_stream_fallback() が返すソケットではないんです。結論からいうと esd daemon の方で持っているソケットの様です。それを得るためにはどうしたら 良いかというと、esd_get_all_info() を用いて esd daemon の情報を得、 その中からほじくり出します。
esd_get_all_info() で得られた esd情報(のコピー)ポインタを esdctl とします。 今何を発声しているかという情報は esdctl->player_list にて示されます。 目的とするデータは esdctl->player_list->source_id なのですが、player_list は リスト構造になっており、esdctl->player_list->source_id では今発声している データの一つでしかありません。 ときに、esd_play_stream_fallback() の第4引数で指定した任意の名前は player_list->name に収められています。そこで、目的とする wave のデータで あることを player_list->name の比較で探し、目的のデータであることが わかったらその player_list->source_id を使うといった流れになります。
   esd_set_stream_pan(esd, esdctl->player_list->source_id, vol, vol);
これで wave 毎にボリュームが調整できます。左右別に指定もできるし、 夢は広がりますね。

ちなみに esd_set_stream_pan() を使わずに esd に直接指令を送るという手法も あります。source_id が必要なのは同じですけれども、以下のようになります。

  int proto;
  esd_info_t  *esdctl;

  proto = ESD_PROTO_STREAM_PAN;
  esdctl = esd_get_all_info(esd);
  write( esd, &proto, sizeof(proto));
  write( esd, &esdctl->player_list->source_id, sizeof(int));
  write( esd, &vol, sizeof(vol));
  write( esd, &vol, sizeof(vol));
esd_set_stream_pan() は内部で上記手順を呼んでいるだけです。また、esd に 直接指令を送る手法はほかにも色々なコマンドを持っていますのでその他の コントロールも出来るでしょう。どんなコマンドがあるかは esd.h 等を参照して みてください。

・ CDDA 演奏、その1

CD から CDDA を演奏させる方法について解説します。
audio デバイスと同じように CD ドライブにもデバイスが存在します。 普段ファイルシステムとして mount /dev/cd0s とかやるその CD-ROM デバイス と同じデバイスに対し演奏要求コマンドを送るだけで CD の演奏は行えます。
今ここでは cdrom デバイスが /dev/cdrom だったとします。このデバイスに CDDA を演奏させるには /dev/cdrom を open し、そのファイルディスクリプタ 経由で ioctl を使って演奏要求を発呼します。
取り敢えず最低限の演奏要求、「CDの回転開始→指定トラックを演奏」の例を 示します。(トラック3を演奏)
/*-------------------------------------------------------
  CDDA - /dev/cdrom  test
--------------------------------------------------------*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/cdrom.h>

#define  CDDEV  "/dev/cdrom"
static int     fp;

int main(void)
{
  fp = open(CDDEV, O_RDONLY);
  ioctl(fp, CDROMSTOP, NULL);
  sleep(3);
  ioctl(fp, CDROMSTART, NULL);
  {
    /* --- 演奏実験 */
    struct cdrom_ti  ti;

    ti.cdti_trk0 = ti.cdti_trk1 = 3;
    ti.cdti_ind0 = ti.cdti_ind0 = 0;
    ioctl(fp, CDROMPLAYTRKIND, &ti);
  }
  close(fp);
}
今回は省略しましたが、本当は最初に TOC(Table Of Contents CDの中に何曲 入っているかの情報)を読みだし、何トラックまでが演奏出来るかを得なければ なりません。ドライブによっては TOC の演奏位置情報から演奏開始指定が 出来るものがあり、それを行うとトラック指定に比べシークが若干短くなる ものと思われます。

・ CDDA 演奏、その2

CDDA の演奏においても色々な差異吸収のために既存ライブラリに頼って みましょう。CD演奏の目的のために libcdaudio という ライブラリを使ってみましょう。このライブラリは CDDA の演奏だけでなく、 CDDB(ネットワーク上からCDの題名や曲名を得る機構)にも対応しているすぐれもの ですが今回はそこまでは使いません。また、英文ですが プログラムマニュアル もちゃんと用意されています。ちょっと見れば何をする関数かはわかると思います。
取り敢えず最低限鳴らすだけのサンプルです、10秒演奏したら停止するようにして みました。
#include <cdaudio.h>

#define  CDDEV  "/dev/cdrom"
static int     fp;

int main(void)
{
  fp = cd_init_device(CDDEV);
  sleep(1);

  cd_play(fp, 3);

  sleep(10);
  cd_stop(fp);
  cd_finish(fp);
}
なんか無茶苦茶簡単ですね。もちろん OS 間やドライバー間の差異はこの ライブラリが吸収してくれるのでアプリ側は本当に上のサンプルコード程度の 関与しかしなくて済みます。まあ、実際にはどれくらい演奏したかとか、 演奏が終ったかとかの監視が入ることになると思いますけど。

・ おわりに

以上駆け足でしたが Linux 上で音声を鳴らすためのプログラム方法について 調査結果としてまとめました。本当に基本的で単純な例題しか扱っていません けれども「どういった手順をふめば良いか」という事については感じとれたのでは ないかと思います。後の細かいところについては、各人必要となったところを 必要なだけ調べてみれば良いのではないかと思います。


rero2@yuumu.rim.or.jp