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

続き。
前回の最後のほうで構成について考えたが、現在の構成では波形のリアルタイム生成がやりにくい。できることはできるが、MySoundクラスを作り直す感じになってしまうことになるので無駄に手間がかかる。
IO→デコーダMySound→SoundTestという4階層構造のどこかを差し替えてリアルタイム生成をする感じにできるとよいのだが、MySoundはベースに使うクラスなのでこれを作り直すんじゃなくて、これをそのまま使う感じで、データを生成する部分であるデコーダを差し替える感じにしてみたい。

カスタムデコーダ

データを読み込む部分はIOで、読み込んだデータをリニアPCMの文字列に変換するのがデコーダである。この部分を差し替えるとIOも必要なくなるわけだが、別に何かしらを入力してダメということは無いし、他のデコーダとの互換性の問題もあるのでとりあえず何かしら受け取るようにはしておこう。使わないぶんには問題ない。
デコーダはこのような形に作る

class MyDecoder < CustomDecoder
  def initialize(io)
    @pos = 0
  end

  def byte_per_sample;2;end
  def rate;44100;end
  def channels;1;end
  def size;nil;end
  def eof?;false;end
  def seek(v);@pos=v;end

  def read(size)
    self.create_buffer(Array.new(size / byte_per_sample / channels){
      tmp = Math.sin(2 * Math::PI * @pos / (self.rate / 440.0))
      @pos += 1
      tmp
    })
  end
end

MySoundデコーダに要求するメソッド一式を用意する。ここでCustomDecoderというクラスと、create_bufferというメソッドが出てくるが、これはC側で実装するカスタムデコーダ用サポート機能である。以下のようなソース、customdecoder.cを作る。

#include "ruby.h"

// RubyのCostomDecoderクラス
static VALUE cCostomDecoder;

// Rubyの例外オブジェクト
static VALUE eCustomDecoderError;

// CustomDecoder#create_buffer
static VALUE CustomDecoder_im_create_buffer(VALUE self, VALUE vary)
{
    int i, byte_per_sample;
    VALUE vstr;
    Check_Type(vary, T_ARRAY);

    byte_per_sample = NUM2INT(rb_funcall(self, rb_intern_const("byte_per_sample"), 0));
    if (byte_per_sample != 1 && byte_per_sample != 2) rb_raise(eCustomDecoderError, "byte_per_sample error");

    if (byte_per_sample == 1) { // 8bit
        unsigned char *buf = ALLOCA_N(unsigned char, RARRAY_LEN(vary));
        for(i = 0; i < RARRAY_LEN(vary); i++) {
            double tmp = NUM2DBL(rb_ary_entry(vary, i));
            *(buf + i) = (unsigned char)((tmp + 1) / 2 * 255);
        }
        vstr = rb_str_new((char *)buf, RARRAY_LEN(vary));
    } else { // 16bit
        short *buf = ALLOCA_N(short, RARRAY_LEN(vary));
        for(i = 0; i < RARRAY_LEN(vary); i++) {
            double tmp = NUM2DBL(rb_ary_entry(vary, i));
            *(buf + i) = (short)(tmp * 32767);
        }
        vstr = rb_str_new((char *)buf, RARRAY_LEN(vary) * 2);
    }

    return vstr;
}

void Init_customdecoder(void)
{
    // 例外定義
    eCustomDecoderError = rb_define_class( "CustomDecoderError", rb_eRuntimeError );

    // CustomDecoderクラス生成
    cCostomDecoder = rb_define_class("CustomDecoder", rb_cObject);
    rb_define_method(cCostomDecoder, "create_buffer", CustomDecoder_im_create_buffer, 1);
}

短い。create_bufferしか持っていないクラスである。create_bufferは-1.0〜1.0の値を詰め込んだ配列をバイナリの文字列に変換してくれる。内部でselfのbyte_per_sampleを呼び出して量子化ビット数を自動判別する。
カスタムデコーダのreadはこれを使って文字列化したデータを返すので、配列を受け取るSoundTest#writebufが必要なくなった。SoundTestStrというクラスも必要なくなるので、文字列を受け取るもののみに一本化してSoundTestという名前にしてしまおう。修正箇所は面倒なので省略。

MySound修正

こうなる。

class MySound < SoundTest
  def self.load(filename)
    MySound.new(open(filename, 'rb'))
  end

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

  def initialize(io, klass = nil)
    if klass
      @decoder = klass.new(io)
    else
      begin
        @decoder = SoundWav.new(io)
      rescue
        begin
          io.seek(0)
          @decoder = SoundOgg.new(io)
        rescue
          raise SoundTestError, 'format error'
        end
      end
    end

    @loop = false # play時のループ指定
    @end_count = 2
    if @decoder.size && @decoder.size < @decoder.rate
      super(@decoder.size,
            ->size{@decoder.read(size)}, 
            false, 
            @decoder.rate, 
            @decoder.byte_per_sample,
            @decoder.channels)
    else # 大きなデータなら1秒ぶんのバッファを作ってストリーミング再生する
      prc = ->size{
        if @end_count == 1
          @end_count = 0
          return (@decoder.byte_per_sample == 1 ? 128.chr : 0.chr) * size
        elsif @end_count == 0
          return nil
        end

        buf = @decoder.read(size)
        if @decoder.eof?
          if @loop
            @decoder.seek(0)
            tmp = @decoder.read(size - buf.size)
            buf += tmp
          else
            @end_count = 1
            return buf + (@decoder.byte_per_sample == 1 ? 128.chr : 0.chr) * (size - buf.size)
          end
        end
        buf
      }

      super(@decoder.rate,
            prc,
            true, 
            @decoder.rate, 
            @decoder.byte_per_sample,
            @decoder.channels)
    end
  end

  def play(lp=false)
    @loop = lp
    super
  end
end

initializeの最初のところが変わった。klassを受け取り、設定されていればklass.newでデコーダを生成する。ここでioを渡すが、今回のカスタムデコーダは受け取ったioを使わない。このへんのインターフェイスはもうちょい考えたほうがいいかもしれないが、重要な感じでもないのでどうでもいいかもしれない。
これを使う場合の生成はこんな感じになる。

rs1 = MySound.new(nil, MyDecoder)

MyDecoderは単純な正弦波を生成する。
そういえばMyDecoderのsizeがnilを返すようになっているが、MySoundのinitializeをちょこっと修正して、nilだった場合はストリーミング再生をするようにしてある。eof?をfalse固定で返しているのでどんだけreadしてもデータが終了することは無い。つまりこのパターンの指定は無限ストリームという意味で、正弦波の再生はいつまで経っても終わらない。

おしまい

ソースはgistに置いておいた。customdecoder.cは記事に書いてあるのがすべてなので置いてない。
ともあれ、波形生成のインターフェイスはそれなりに整備できたと思うので、またひとつ進んだ。まだやりたいことはいっぱいあるんだけども。