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

続き。
サウンドライブラリによくある機能で不足しているものとして、pauseとresumeがある。再生を一時停止し、再開する。DirectSoundバッファには関数としてplayとstopはあるが、pauseとresumeは無い。じゃあどのように実装するかというと、っていうか、stopは再生カーソルがその場で停止するので、stopしてplayすれば普通に続きから再生される。むしろplay/stopがresume/pauseに対応していて、再生カーソルを戻す処理は含まれていない。
逆に通常のイメージのplay/stopを実装するときに再生カーソルを戻してやる必要があり、現状ではそのようにしてある。つまりこのへんを細工すればそんなに苦労することなく作れる。はずだったのだが。

ストリーミング再生時のバグ修正とpause/resume

現在わかっているバグは以下の2点。
・連続でplayを実行するとスレッドがどんどん生成されて終了できなくなる
・stop→playすると再生カーソルは戻るがデコーダがシークされない
1つ目はC側の手抜き実装が原因なので、これを機にごっそり修正して色々直しておく。例えばストリーミング再生じゃない場合でもイベント作ってて無駄とか、イベント解放してないとか、内部処理用メソッド(wait、writebuf)は必要ないので消すとか、その他もろもろ。コードは省略。
2つ目はC側ではできないのでRuby側で対策する必要がある。そもそもplayの時にデコーダをシークするかどうかはストリーミング再生しているかどうかで決まる話であり、また、resumeの場合はシークしない、ということにもなるので、SoundTest#playは共通で使えるようにしておいて、Ruby側のMySound#playとMySound#resumeの内部実装により動作をわける感じがよい。すると再生カーソルを戻すメソッドをSoundTestに追加しておいて、MySound#playではそれも呼ぶ、というふうにしておけばよさげだ。
また、ストリーミング再生スレッドをいつ作るか、いつ停止するか、というのも問題で、SoundTest#stopをpauseと共通で使いたいなら、このタイミングでスレッドを終わらせてしまうとresumeでまた作らなければならなくなる。そうするとまた別にスレッドを停止するメソッドが必要になってくる。
てなことを考えていたのだが、メソッドを追加するのも面倒なので引数追加で処理をわけるほうがラクそうである。SoundTest#playをMySound#resumeから呼ぶことになってメソッド名がかぶると辛いのでSoundTest#playの名前を変えておく。_playでいいか。stopも同様に_stopとしておく。

_playメソッド

色々考慮するとこのような感じになる。

// SoundTest#_play
// 再生する
static VALUE SoundTest_im_play(VALUE self, VALUE vloop, VALUE vreset)
{
    HRESULT hr;

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

    // st->pDSBuffer8がNULLの場合はdispose済み
    if (!st->pDSBuffer8) rb_raise(eSoundTestError, "disposed object");

    // ストリーミング再生する場合はThreadを生成する
    if (st->streaming_flg) {
        // 再生スレッドが無ければ作る
        if (!RTEST(st->vstreaming_thread)) {
            DSBPOSITIONNOTIFY pos[2];
            LPDIRECTSOUNDNOTIFY pDSNotify;

            // イベント作成
            st->event[0] = CreateEvent(NULL, FALSE, FALSE, NULL);
            st->event[1] = CreateEvent(NULL, FALSE, FALSE, NULL);
            st->event[2] = CreateEvent(NULL, FALSE, FALSE, NULL);

            // 再生イベント通知オブジェクト
            hr = st->pDSBuffer8->lpVtbl->QueryInterface(st->pDSBuffer8, &IID_IDirectSoundNotify, (void**)&pDSNotify);
            if (FAILED(hr)) rb_raise(eSoundTestError, "query interface(notify) error");
            pos[0].dwOffset     = st->buffer_size / 2 - 1;
            pos[0].hEventNotify = st->event[0];
            pos[1].dwOffset     = st->buffer_size - 1;
            pos[1].hEventNotify = st->event[1];
            hr = pDSNotify->lpVtbl->SetNotificationPositions(pDSNotify, 2, pos);
            if (FAILED(hr)) rb_raise(eSoundTestError, "set notification psitions error");
            pDSNotify->lpVtbl->Release(pDSNotify);

            // ストリーミング再生用スレッドを生成する
            st->vstreaming_thread = rb_thread_create(SoundTest_streaming_thread, (void *)self);

            // ストリーミング再生用スレッドでコケたら全体がコケるように設定する
            rb_funcall(st->vstreaming_thread, rb_intern_const("abort_on_exception="), 1, Qtrue);
        }

        // ストリーミング再生時は強制的にループ再生
        vloop = Qtrue;
    }

    // reset指定時は再生カーソルを先頭に移動
    if (RTEST(vreset)) {
        // カーソルを先頭にセット
        hr = st->pDSBuffer8->lpVtbl->SetCurrentPosition(st->pDSBuffer8, 0);
        if (FAILED(hr)) rb_raise(eSoundTestError, "setpos error");

        // ストリーミング再生時はバッファを埋める
        if (st->streaming_flg) {
            st->pos = 0;
            SoundTest_writebuf(self);
        }
    }

    // 再生。vloopがtrueの場合ループ再生する
    hr = st->pDSBuffer8->lpVtbl->Play(st->pDSBuffer8, 0, 0, RTEST(vloop) ? DSBPLAY_LOOPING : 0);
    if (FAILED(hr)) rb_raise(eSoundTestError, "play error");

    return self;
}

ストリーミング再生スレッドはストリーミング再生時に起動するが、pauseした時はそのまま残すので、無いときだけ作るようにする。また、スレッドが停止するタイミングは非同期なのでイベントが誤発火しないように、スレッドを作る時にイベントを新たに作る。initializeからこっちに処理を移動した。なお、スレッド停止時に解放するようにしてある。
引数にreset指定を追加して、playの時はtrue、resumeの時はfalseを指定する。trueの場合に再生カーソルを先頭に移動し、ストリーミング再生の場合は再生バッファを埋めなおす。今まではinitializeの時にやっていたが、ストリーミング再生の時はinitializeで埋めないように変更してある。初回再生時のオーバーヘッドが増えるのでストリーミング再生をキビキビと実行するのは無理があり、まあ、そういう用途の音はプリデコードしてくれと言う話になるだろう。いまのところそのような機能は無いが。

_stopメソッド

// SoundTest#_stop
// 停止する
static VALUE SoundTest_im_stop(VALUE self, VALUE vthread_stop)
{
    HRESULT hr;

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

    // st->pDSBuffer8がNULLの場合はdispose済み
    if (!st->pDSBuffer8) rb_raise(eSoundTestError, "disposed object");

    // 停止
    hr = st->pDSBuffer8->lpVtbl->Stop(st->pDSBuffer8);
    if (FAILED(hr)) rb_raise(eSoundTestError, "stop error");

    if (st->streaming_flg && RTEST(vthread_stop)) {
        ResetEvent(st->event[0]);
        ResetEvent(st->event[1]);
        SetEvent(st->event[2]);
        st->vstreaming_thread = Qnil;
    }

    return self;
}

引数にthread_stopを追加して、trueの場合にストリーミング再生スレッドを停止するようにした。pauseの場合はfalseを指定する。スレッドそのものはpauseで停止してresumeで再生成しても理屈としてはなんら問題は無いのだが、生成直後にバッファの通知が来た場合にタイミング的にきちんと動くのかと言われるとちょっと心配である。

Ruby

ちょっとごちゃごちゃしてしまっているが。

  def play(lp=false)
    @loop = lp
    if self.streaming?
      if self.playing? # ストリーミング再生中の場合はいったん停止
        _stop(false)
      end
      @decoder.seek(0)
      @end_count = 2
    end
    @pause = false
    _play(lp, true) # 再生カーソルとバッファを先頭に戻す再生
  end

  def resume
    if @pause
      @pause = false
      _play(@loop, false) # 再生カーソルとバッファはそのままで再生
    else
      play(@loop) # ポーズ中でなければ通常の再生
    end
  end

  def stop
    @pause = false
    _stop(true) # 再生スレッド停止
  end

  def pause
    @pause = true
    _stop(false) # 再生スレッド残す
  end

  def pausing?
    @pause
  end

という感じでC側メソッドとRuby側メソッドのハイブリッドで機能を構成する。

おしまい

簡単にできるんじゃね?という予想を覆しての苦戦であった。主な原因はもともと考え無しすぎのバグ入りコードなので自業自得である。ほんとはもうちょっとやる予定だったのだが、大変だったので今回はここまで。
コードはgistに。