Apr.09.2000
Linux でサウンドを鳴らすプログラムを書く方法。基礎知識の説明。
Linux ではサウンドドライバー等のデバイスドライバーを組み込むと大抵それは
デバイスとして扱えるようになります。デバイスは /dev 以下に置かれ(現れ)、
一般のファイルストリームと同等に扱える様になります。このあらゆるデバイス
(メモリすら)をファイルと同等に扱えるというのは UNIX 系OSの特色であると
言っても良いでしょう。(plan9 とかではプロセスすらファイルとして扱えると
聞きました)
今回目的とするサウンドデバイスも当然 /dev の下にいます。そのサウンド
デバイスへアクセスする具体的方法を説明します。
表題の単語が若干怪しいけど、ここでは wave ファイルをサンプリングデータと
して取り込み音声出力するデバイスの事を指します。PCM とか DSP とか言われる
こともあるけど、それは中間の手法の単語だったりします。
Linux における audio デバイスドライバーでメジャーなのは OSS(Free) と ALSA
の二つ。この二つのどっちを使うかでアクセス手段や /dev の名前が違います。
もちろん他 OS でも異なります。
ここでは Linux kernel に付いてくる OSS Free について解説します、他の
ドライバーに関しては各人でお調べください。基本的なアクセス方法とかは
大体同じなので応用は効くと思います。
OSS のプログラミングに関しては
OSS のサイトで入手
できます(英文)。他に
入手できる資料はあまりなかったのでこれを参照するのが現状ベターでしょう。
また、ハックの基本(そこまで大行でないけど)としてヘッダーファイルを参照
するのが大変有効な手段です。どんな define があるかとかどんな関数がある
かとかはだいたいそこから掴めますから。
/*-------------------------------------------------------
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 ファイルのヘッダーまで出力しているので頭に
短いノイズが乗ります。
int mix, vol;
mix = open("/dev/mixer", O_WRONLY);
ioctl(mix, MIXER_WRITE(SOUND_MIXER_PCM), &vol);
MIXER の場合 read か write かがあるので適切に指定してください。
また、PCM 以外のデバイスの音量を指定する方法については資料を御参考
ください。
EsounD を使おうと思い立ち「じゃあどうやって使うの?」となるわけですが、 残念なことに実は EsounD の API ドキュメントなるものはまだ存在していません。 じゃあなにもできないじゃないかと思われる人もいるかもしれませんが、 そうではありません。実際に動いているソースリストがあるじゃないですか。 というわけで esd.h というヘッダーファイルを中心に esdplay.c の様な サンプルプログラム、および 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() の結果)は意味ないのですが、それは後で
使うということで記述を残してあります。
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 - /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 の演奏位置情報から演奏開始指定が
出来るものがあり、それを行うとトラック指定に比べシークが若干短くなる
ものと思われます。
#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 間やドライバー間の差異はこの
ライブラリが吸収してくれるのでアプリ側は本当に上のサンプルコード程度の
関与しかしなくて済みます。まあ、実際にはどれくらい演奏したかとか、
演奏が終ったかとかの監視が入ることになると思いますけど。