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

続き。

wavファイルの全体を読み込んで単発で鳴らす

いままでストリーミング再生に取り組んできて、前回でoggを再生できるようにまでなった。適当に作り始めたものを気が向くままいじっていたらだんだん高機能になっていく、というのは面白い。
今回はストリーミングから離れて、基本的な「全体を読み込んでおいて単発で鳴らす」という動作を作ろう。
といっても現状のSoundTestはそういう再生ができるようになっているので、Ruby側はwavを読んで再生するだけのシンプルなものになる。

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

class WavSound < SoundTest
  def initialize(filename)
    wav = WavFormat.new(filename)
    super(wav.data_size / wav.channel * 8 / wav.bit_per_sample,
          wav.sample_rate,
          wav.bit_per_sample,
          wav.channel)
    self.writebuf_str{|size|wav.file.read(size)}
  end
end

rs1 = WavSound.new('test.wav')
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)
  if Input.key_push?(K_Z)
    rs1.stop
    rs1.play
  end
end

このコードではZを押すとリスタートできる。DirectSoundバッファは単体で1個の音しか再生できず、多重再生をすることはできない。DXRubyのSoundEffectが多重再生できないように。また、Ayameも同様に多重再生はできない。
SoundEffectやAyameで多重再生する場合は同じデータを持つオブジェクトを複数作って自前で管理する必要があり、これがちょっと面倒だし、同じデータなのに別のバッファを作って持たせるというのもなんだか無駄だ。
今回はこれを対策を考えてみる。

IDirectSound8::DuplicateSoundBuffer

DirectSoundオブジェクトにはDuplicateSoundBufferという関数が用意されていて、これを使うとDirectSoundバッファをコピーすることができる。といっても単純なコピーじゃなく、元のバッファを共有した新しいDirectSoundバッファを作る。共有するので同じ音を別のタイミングで重ねて再生することができるが、バッファの中身を書き換えると他にも影響することになるのでストリーミング再生に使うのはまずそうだ。また、共有されたバッファは使っているすべてのバッファオブジェクトが解放されるとメモリから消えるようになっているらしいので、親を管理するといった面倒な手順は必要ない。
この関数を使ってバッファを共有した新しいSoundTestオブジェクトを生成して返すSoundTest#duplicateを作ってみよう。dupやcloneではバッファを共有しない普通のコピーのように見えるので別の名前がよいだろうが、どういう名前がいいのかというとちょっとよくわからないのでとりあえずこれで。

// SoundTest#duplicate
// selfとバッファを共有する新しいSoundTestオブジェクトを生成して返す
static VALUE SoundTest_im_duplicate(VALUE self)
{
    HRESULT hr;

    // selfからSoundTest構造体を取り出す
    struct SoundTest *src = (struct SoundTest *)RTYPEDDATA_DATA(self);

    // 新しいSoundTestオブジェクトを生成して構造体を取り出す
    VALUE obj = SoundTest_allocate(cSoundTest);
    struct SoundTest *dst = (struct SoundTest *)RTYPEDDATA_DATA(obj);

    // selfのDirectSoundバッファからバッファ共有型DirectSoundバッファを生成する
    hr = g_pDSound->lpVtbl->DuplicateSoundBuffer(g_pDSound, (LPDIRECTSOUNDBUFFER)src->pDSBuffer8, (LPDIRECTSOUNDBUFFER *)&dst->pDSBuffer8);
    if (FAILED(hr)) rb_raise(eSoundTestError, "duplicate error");

    // 内部情報のコピー
    dst->buffer_size = src->buffer_size;
    dst->hz = src->hz;
    dst->channels = src->channels;
    dst->byte_per_sample = src->byte_per_sample;
    dst->sample_size = src->sample_size;
    dst->pos = src->pos;
    dst->duplicate_flg = 1;
    dst->event[0] = NULL;
    dst->event[1] = NULL;
    dst->event[2] = NULL;

    g_refcount++;

    return obj;
}

なぜかDuplicateSoundBufferはDirectSoundBuffer8じゃなくDirectSoundBufferのほうのポインタを受け取る仕様なのでキャストしている。まあ、同じものだし。
Cのコードで定義したクラスのRubyオブジェクトを生成する場合、allocateを呼んでから中身を自分で設定する必要がある。initializeを呼んでもいいが、今回のようにそれだと困る場合が非常に多く、結局はちまちまと中身を設定することになる。このような処理はクラスメソッドでオブジェクトを生成する場合(例えばSoundTest.loadとか)や、dup/clone用にinitialize_copyを実装する場合などでよく出てくる。
また、duplicate_flgなる変数を追加して、共有したバッファかどうかを判別できるようにしている。これはObjectSpaceに返すサイズが、全バッファでバッファサイズを返してしまうと嘘にしても甚だしいので、共有バッファはバッファサイズを足さないようにするためである。
Cのコードはgistに置いておいた。
Ruby側のコードは以下のようになる。といってもそんなに変わらない。元の音をループ再生させておいて、duplicateした音をZキーで再生するというテストである。それらが違うオブジェクトになっているのはpメソッドの結果で確認できる。バッファがほんとに共有されているのかを調べる手段は無いが。大きなバッファ作ってメモリの消費具合を見るぐらいか。

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

class WavSound < SoundTest
  def initialize(filename)
    wav = WavFormat.new(filename)
    super(wav.data_size / wav.channel * 8 / wav.bit_per_sample,
          wav.sample_rate,
          wav.bit_per_sample,
          wav.channel)
    self.writebuf_str{|size|wav.file.read(size)}
  end
end

rs1 = WavSound.new('test.wav')
rs1.play(true) # ループ再生

rs2 = rs1.duplicate # 複製

p rs1
p rs2

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)
  if Input.key_push?(K_Z)
    rs2.stop
    rs2.play
  end
end

SoundGroupクラス

多重再生するためにメモリの消費を抑えて効率よくSoundTestオブジェクトを作ることができるようにはなったが、現状では複数のSoundTestオブジェクトを保持してどうこうしなければならないのは変わらない。この問題はDirectMusicを使ってすべてお任せする以外には特に手は無く、多重再生をどういうインターフェイスでサポートするかで悩むことになる。
例えば、SoundTest#playするたびに内部でduplicateして新しいオブジェクトを再生するようにすれば、ユーザは意識することなく多重再生できる。playの戻り値をその新しいオブジェクトにすれば再生中のオブジェクトに対する処理も可能だ。その場合の問題点はSoundTest#stopで再生中のすべての多重再生された音を止めたい、とか、そういうことを考え始めると、オブジェクトの内部管理が煩雑になっていくところだろう。いずれにせよ再生中のすべての音を止めるなどの需要はあるだろうからどこかの時点でそういう管理方法も考えなければならないのだが。
さて、今回はとりあえず、多重再生をサポートするSoundGroupクラスを実装してみよう。多重再生をしたい場合はSoundGroupにSoundTestを登録して、SoundGroup#playする。再生したSoundTestオブジェクトを返すとかは考えず、極限までシンプルに。

class SoundGroup
  def initialize(st, count)
    @group = [st]
    (count-1).times do
      @group << st.duplicate
    end
    @pos = 0
    @count = count
  end

  def play
    @group[@pos].stop
    @group[@pos].play
    @pos += 1
    @pos = 0 if @pos >= @count
    nil
  end
end

んで、これを使うほうのコード。

require 'dxruby'
require_relative 'soundtest'
require_relative 'wav'
require_relative 'soundgroup'

class WavSound < SoundTest
  def initialize(filename)
    wav = WavFormat.new(filename)
    super(wav.data_size / wav.channel * 8 / wav.bit_per_sample,
          wav.sample_rate,
          wav.bit_per_sample,
          wav.channel)
    self.writebuf_str{|size|wav.file.read(size)}
  end
end

group = SoundGroup.new(WavSound.new('test.wav'), 8)

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

考えるのが面倒だったので描画は無し。SoundGroup.newの第2引数は多重再生する数である。超えた場合は古いほうから停止されていく。こういう部分もRubyで書いてあれば、どういうインターフェイスにしようかな〜って考えるのも試行錯誤してみるのも気楽になるのでよい。

おしまい

多重再生について考えてみた。とりあえずバッファを共有する機能ができたので、なんかメモリが無駄だなあーという感じも無くどうにかできるはずだ。しかし具体的にどうするのが最適なのかの答えは今のところ持ち合わせていない。
いつかRubyサウンドドライバを作るときにはそのへんもきちんと考えなければならないので大変そうである。