DirectSoundとRubyのプログラミング その10

続き。

メモリから読み込む

DXRubyでも要望があって開発版で対応したのだが、ファイルからだけじゃなく、メモリに持っているバイナリデータを使って再生する機能を作ってみよう。
といってもそんなに難しい話ではなく、Rubyで書くならファイルの代わりにStringIOを使うようにするだけでいい。試しにwavファイルをメモリ再生に対応させてみる。

class RiffFormat
  attr_accessor :file, :id, :size

  def initialize(io)
    @file = io
    @id = @file.read(4)
    rb_raise(RiffFormatError) if @id != 'RIFF'
    @size = @file.read(4).unpack('V')[0]
  end

  def close
    @file.close
  end
end

RiffFormat#initializeがIOオブジェクトを受け取る形になった。これだけだ。使う側もちょっと修正して、

require 'dxruby'
require 'stringio'
require_relative 'soundtest'
require_relative 'wav'

class WavSound < SoundTestStr
  def self.load(filename)
    WavSound.new(open(filename, 'rb'))
  end

  def self.load_from_memory(str)
    WavSound.new(StringIO.new(str))
  end

  def initialize(io)
    wav = WavFormat.new(io)
    super(wav.data_size / wav.channel * 8 / wav.bit_per_sample,
          Proc.new{|size|wav.file.read(size)},
          false,
          wav.sample_rate,
          wav.bit_per_sample,
          wav.channel)
  end
end

#s = WavSound.load('test.wav')
s = WavSound.load_from_memory(open('test.wav', 'rb').read)

Window.loop do
  s.play if Input.key_push?(K_Z)
end

としてやれば、ファイルからも文字列からもWavSoundオブジェクトを生成することができる。

Oggをメモリから読み込む

wavファイルのほうはRubyで書いてあったから簡単に済んだが、Oggのほうはそうもいかない。vorbisfileライブラリを使って手軽に実現しているので今の状態ではファイルしか扱えないからだ。voirbisfileはメモリから読み込むようなカスタマイズにも対応しているので、その機能を使う。
どのようにするかの前に、まずはvorbisfileの中身を見てみよう。オープンソースなので普通に見れる。ov_fopenは以下のようになっている。

int ov_fopen(const char *path,OggVorbis_File *vf){
  int ret;
  FILE *f = fopen(path,"rb");
  if(!f) return -1;

  ret = ov_open(f,vf,NULL,0);
  if(ret) fclose(f);
  return ret;
}

ファイルをfopenして、そのファイルハンドルとOggVorbis_File構造体をov_openに渡しているだけだ。続いてov_open。

int ov_open(FILE *f,OggVorbis_File *vf,const char *initial,long ibytes){
  ov_callbacks callbacks = {
    (size_t (*)(void *, size_t, size_t, void *))  fread,
    (int (*)(void *, ogg_int64_t, int))              _fseek64_wrap,
    (int (*)(void *))                             fclose,
    (long (*)(void *))                            ftell
  };

  return ov_open_callbacks((void *)f, vf, initial, ibytes, callbacks);
}

ov_callbacks構造体に標準関数4つをセットしてov_open_callbacksを呼ぶ。つまり、ここでファイルからの読み込みという設定がされているのだな。ov_callbacks構造体で標準関数ではなく自前の関数を呼ぶように細工してov_open_callbacksを呼んでやれば、ファイル以外からの読み込みに対応することができる。という話だ。

自前関数の実装

とりあえず、initializeにはioを渡すようにしておこう。

// SoundOgg#initialize
static VALUE SoundOgg_initialize(VALUE self, VALUE vio)
{
    struct SoundOgg *so = (struct SoundOgg *)RTYPEDDATA_DATA(self);
    ov_callbacks callbacks = {
        (size_t (*)(void *, size_t, size_t, void *))  SoundOgg_read,
        (int (*)(void *, ogg_int64_t, int))           SoundOgg_seek,
        (int (*)(void *))                             NULL,
        (long (*)(void *))                            SoundOgg_tell
    };

    if (ov_open_callbacks((void *)vio, &so->ovf, NULL, 0, callbacks) != 0) rb_raise(eSoundOggError, "open error");

    // Oggファイルの音声フォーマット情報
    so->info = ov_info(&so->ovf, -1);

    so->vio = vio;

    return self;
}

ここでov_callbacks構造体を作って、ov_fopenじゃなくov_open_callbacksを呼ぶ。RubyのIOオブジェクトを保持するので構造体にVALUE値を持ち、マーク関数も作っておく必要があるが省略する。ov_callbacksの3つ目は標準関数ではfcloseが設定されるところなのだが、これはov_clearからしか呼ばれず、現在のロジックではov_clearが呼ばれるのはGC中のみで、GC中にIOオブジェクトのcloseを呼び出すとSEGVしてしまうので呼ばないことにした。どのみちGCされたらIOオブジェクトも回収されてクローズされるので問題はないかなーと。
んではコールバック関数群。

// コールバック関数SoundOgg_read
size_t SoundOgg_read(void *buf, size_t blocksize, size_t readsize, void *f)
{
    VALUE obj = (VALUE)f;
    VALUE vstr;

    vstr = rb_funcall(obj, rb_intern_const("read"), 1, UINT2NUM(blocksize * readsize));
    Check_Type(vstr, T_STRING);

    memcpy(buf, RSTRING_PTR(vstr), RSTRING_LEN(vstr));
    return RSTRING_LEN(vstr);
}

// コールバック関数SoundOgg_seek
int SoundOgg_seek(void *f, ogg_int64_t offset, int whence)
{
    VALUE obj = (VALUE)f;
    int result;

    result = NUM2INT(rb_funcall(obj, rb_intern_const("seek"), 2, LL2NUM(offset), INT2NUM(whence)));

    return result;
}

// コールバック関数SoundOgg_close
int SoundOgg_close(void *f)
{
    VALUE obj = (VALUE)f;
    int result;

    result = NUM2INT(rb_funcall(obj, rb_intern_const("close"), 0));

    return result;
}

// コールバック関数SoundOgg_tell
long SoundOgg_tell(void *f)
{
    VALUE obj = (VALUE)f;
    long result;

    result = NUM2LONG(rb_funcall(obj, rb_intern_const("tell"), 0));

    return result;
}

closeは作ってみたけど呼ばれないので消してもいい。基本は標準関数と同じ動きをするが、標準関数の変わりにIOオブジェクトを呼び出している。ポイントの1つ目はreadのところのblocksizeで、ほんとはこの関数が返すべきは読み込んだブロック数なのだが、RubyのIOにブロックサイズを指定するインターフェイスが無い。vorbisfileからはサイズ1でしか呼ばれないようなので読み込んだサイズを単純に返せば問題は無い。2つ目はseekで、渡されてくるwhenceはCのマクロのSEEK_CURなどなのだが、Ruby側の定数も同じ値を同じ名前で設定しているので、そのままFixnumにして渡してやればよい。ここが食い違うとちょっと面倒だった。

Ruby側のコード

OggStreamクラスにクラスメソッドを追加して、IOオブジェクトをSoundOggに渡すように作る。

require 'dxruby'
require 'stringio'
require_relative 'soundtest'

class OggStream < SoundTestStr
  def self.load(filename)
    OggStream.new(open(filename, 'rb'))
  end

  def self.load_from_memory(str)
    OggStream.new(StringIO.new(str))
  end

  def initialize(io)
    @ogg = SoundOgg.new(io)

    prc = Proc.new do |size|
      buf = ""
      reqsize = size
      while(buf.size < size) do
        tmp = @ogg.read(reqsize)
        if tmp.size == 0
          break
        end
        reqsize -= tmp.size
        buf.concat(tmp)
      end
      buf
    end

    super(@ogg.rate, prc, true, @ogg.rate, 16, @ogg.channels)
  end
end

#rs1 = OggStream.load('test.ogg')
rs1 = OggStream.load_from_memory(open('test.ogg', 'rb').read)
rs1.play

Window.loop do
  Window.draw_font(0, 0, Window.fps.to_i.to_s, Font.default)
  Window.draw_line(0, 100, 639, 100, C_WHITE)

  pos, playpos, writepos = rs1.getpos
  Window.draw_font(668.0 * playpos / rs1.size - 12, 100, '', Font.default, color:C_GREEN)
  Window.draw_font(668.0 * writepos / rs1.size - 12, 100, '', Font.default, color:C_BLUE)
  Window.draw_font(668.0 * pos / rs1.size - 12, 100, '', Font.default, color:C_RED)
end

これでめでたくメモリ内のoggデータをストリーミング再生できるようになった。

おしまい

IOオブジェクトを使うようになったので例えばSocketなどを渡してもそれなりに動くかもしれないし、自前圧縮やゲームデータのコンテナファイルへのアクセスをIOと同じインターフェイスでできるように作っておけば、それを渡して動かすこともできそうだ。
今回のSoundOgg.cはgistに置いておいた。