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データをストリーミング再生できるようになった。