Cなんて一行も書きたくないけれど、Cのダイナミックリンクライブラリをrubyから使いたい。
そんな人のためのライブラリ(?)がruby/dlである。
(Cは一行も書かなくてもよいものの、流石にCの知識がある程度無いと使えないが…)
利点と欠点は以下のとおり。
・利点
- Cのダイナミックリンクライブラリをコンパイル作業無しで利用できる
- すべてrubyで書ける
・欠点
- 環境依存部分の吸収を自前でやらなければならない場合がある
- 一部無理矢理な定義をしなければならない部分がある(入れ子の構造体、enum型など)
今回は、ruby1.9のdlライブラリを用いて、
マルチプラットフォームなサウンド出力ライブラリであるlibaoを叩き、
オーディオファイルを再生できるようにしてみる。
ヘッダファイルを読む
ruby/dlでlibaoを叩くには、まずlibaoにどのような関数や構造体などがあるのか調べる必要がある。
libaoでは、ほとんどが$PREFIX/include/ao/ao.hで宣言されているので、それを読む。
ao/ao.hで定義されていてruby/dlから叩くのに必要な部分は、以下のとおりである。
/* --- Constants ---*/
#define AO_TYPE_LIVE 1
#define AO_TYPE_FILE 2
#define AO_ENODRIVER 1
#define AO_ENOTFILE 2
#define AO_ENOTLIVE 3
#define AO_EBADOPTION 4
#define AO_EOPENDEVICE 5
#define AO_EOPENFILE 6
#define AO_EFILEEXISTS 7
#define AO_EBADFORMAT 8
#define AO_EFAIL 100
#define AO_FMT_LITTLE 1
#define AO_FMT_BIG 2
#define AO_FMT_NATIVE 4
/* --- Structures --- */
typedef struct ao_info {
int type; /* live output or file output? */
char *name; /* full name of driver */
char *short_name; /* short name of driver */
char *author; /* driver author */
char *comment; /* driver comment */
int preferred_byte_format;
int priority;
char **options;
int option_count;
} ao_info;
typedef struct ao_functions ao_functions;
typedef struct ao_device ao_device;
typedef struct ao_sample_format {
int bits; /* bits per sample */
int rate; /* samples per second (in a single channel) */
int channels; /* number of audio channels */
int byte_format; /* Byte ordering in sample, see constants below */
char *matrix; /* input channel location/ordering */
} ao_sample_format;
typedef struct ao_option {
char *key;
char *value;
struct ao_option *next;
} ao_option;
/* --- Functions --- */
/* library setup/teardown */
void ao_initialize(void);
void ao_shutdown(void);
/* device setup/playback/teardown */
int ao_append_global_option(const char *key,
const char *value);
int ao_append_option(ao_option **options,
const char *key,
const char *value);
void ao_free_options(ao_option *options);
ao_device* ao_open_live(int driver_id,
ao_sample_format *format,
ao_option *option);
ao_device* ao_open_file(int driver_id,
const char *filename,
int overwrite,
ao_sample_format *format,
ao_option *option);
int ao_play(ao_device *device,
char *output_samples,
uint_32 num_bytes);
int ao_close(ao_device *device);
/* driver information */
int ao_driver_id(const char *short_name);
int ao_default_driver_id(void);
ao_info *ao_driver_info(int driver_id);
ao_info **ao_driver_info_list(int *driver_count);
char *ao_file_extension(int driver_id);
/* miscellaneous */
int ao_is_big_endian(void);
さっと眺めると、定数・構造体を定義した後、
関数を定義してあるシンプルな物であることが分かる。
rubyのAOモジュールを作成する
ao/ao.hの情報から、ruby/dlを用いてrubyからlibaoを叩くためのモジュール、
AOを作成する。
基本的な使用例は
リファレンスにサッと書いてあるので、それを参考にする。
module AO
# DL::Importerのインスタンスメソッドを
# AOモジュールの特異メソッドとして追加する
extend(DL::Importer)
# libao.so(Windowsならlibao.dll)を
# ライブラリパスから検索しロードする
dlload('libao.so')
# ao/ao.hで定義されていた定数を定義する
# ao/ao.h
AO_TYPE_LIVE = 1
AO_TYPE_FILE = 2
AO_ENODRIVER = 1
AO_ENOTFILE = 2
AO_ENOTLIVE = 3
AO_EBADOPTION = 4
AO_EOPENDEVICE = 5
AO_EOPENFILE = 6
AO_EFILEEXISTS = 7
AO_EBADFORMAT = 8
AO_EFAIL = 100
AO_FMT_LITTLE = 1
AO_FMT_BIG = 2
AO_FMT_NATIVE = 4
# ao/ao.hで定義されていた構造体を定義する
AO_Info =
struct(['int type',
'char *name',
'char *short_name',
'char *author',
'char *comment',
'int preferrd_byte_format',
'int priority',
'char **options',
'int option_count'])
AO_Sample_Format =
struct(['int bits',
'int rate',
'int channels',
'int byte_format',
'char *matrix'])
AO_Option =
struct(['char *key',
'char *value',
'struct ao_option *next'])
# 空の構造体は、voidのaliasとして定義する
typealias('ao_functions', 'void')
typealias('ao_device', 'void')
# 未定義の型uint_32を、
# unsignd intのaliasとして定義する
typealias('uint_32', 'unsigned int')
# 関数を定義する
# (仮引数は型のみ記入する)
# /* --- Functions --- */
# /* library setup/teardown */
# void ao_initialize(void);
extern('void ao_initialize(void)')
# void ao_shutdown(void);
extern('void ao_shutdown(void)')
# /* device setup/playback/teardown */
# int ao_append_global_option(const char *key,
# const char *value);
extern('int ao_append_global_option(const char *, const char *)')
# int ao_append_option(ao_option **options,
# const char *key,
# const char *value);
extern('int ao_append_option(ao_option **, const char *, const char *)')
# void ao_free_options(ao_option *options);
extern('void ao_free_options(ao_option *)')
# ao_device* ao_open_live(int driver_id,
# ao_sample_format *format,
# ao_option *option);
extern('ao_device * ao_open_live(int, ao_sample_format *, ao_option *)')
# ao_device* ao_open_file(int driver_id,
# const char *filename,
# int overwrite,
# ao_sample_format *format,
# ao_option *option);
extern(' ao_device* ao_open_file(int, const char *, int, ao_sample_format *, ao_option *)')
# int ao_play(ao_device *device,
# char *output_samples,
# uint_32 num_bytes);
extern('int ao_play(ao_device *, char *, uint_32)')
# int ao_close(ao_device *device);
extern('int ao_close(ao_device *)')
# /* driver information */
# int ao_driver_id(const char *short_name);
extern('int ao_driver_id(const char *)')
# int ao_default_driver_id(void);
extern('int ao_default_driver_id(void)')
# ao_info *ao_driver_info(int driver_id);
extern('ao_info *ao_driver_info(int)')
# ao_info **ao_driver_info_list(int *driver_count);
extern('ao_info **ao_driver_info_list(int *)')
# char *ao_file_extension(int driver_id);
# extern('char *ao_file_extension(int)')
# /* miscellaneous */
# int ao_is_big_endian(void);
extern('int ao_is_big_endian(void)')
end
比較してみると分かるが、ao/ao.hで定義されているものをruby/dlに合わせ
機械的に定義し直しているだけである。
実際に使用する
作成したAOモジュールを実際に使用してみる。この時、関数の戻り値等はC言語で使用する際と同様に
適切に処理しなければならない。またerrnoの値を取得するには、
DL::CFunc.last_errorを用いる。
puts 'initialize'
AO.ao_initialize
puts 'setup default driver id'
drv = AO.ao_default_driver_id
if drv < 0
puts 'usable audio output device is not found.'
exit 1
end
puts 'setup sample format structure'
format = AO::AO_Sample_Format.malloc
format.bits = 16
format.rate = 44100
format.channels = 2
format.byte_format = AO::AO_FMT_LITTLE
format.matrix = nil
puts 'open live device'
dev = AO.ao_open_live(drv, format, nil)
unless dev
printf("errno: %d\n", DL::CFunc.last_error)
exit DL::CFunc.last_error
end
puts 'play'
ARGV.each{|argv|
if File.file?(argv)
File.open(argv){|f|
while buf = f.read(4096)
if AO.ao_play(dev, buf, buf.size) == 0
puts 'ao_play() failure.'
exit 1
end
end
}
end
}
puts 'close'
if AO.ao_close(dev) == 0
puts 'ao_close() failure.'
end
puts 'shutdown'
AO.ao_shutdown
あとは、作成したスクリプトを、引数に16bit・44.1KHz・2ch・Little Endianな音声の
RAWファイルへのパスを付けて実行すれば、それを再生してくれる筈である。
WAVファイルはヘッダがあるため先頭に少々ノイズが入るが、再生することはできる。
参考文献
・xiph.org - libao
http://www.xiph.org/ao/
・Ruby 1.9.2 Reference Manual - dl
http://rurema.clear-code.com/1.9.2/library/dl.html