2011年6月30日木曜日

ruby/dlでlibaoを使う

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

0 件のコメント:

コメントを投稿