SDL_mixerのラッパを考える

SDL2の周辺ライブラリのひとつ、SDL_mixerは各種フォーマットの音データを簡単に再生できる。その点では非常に扱いやすいのだが、再生した音の制御という点では非常に低レベルな機能しか持ち合わせていない。
sdl2rで実装したSDL_mixerの機能を扱いやすくラップする方法を考えてみる。

ChunkとChannel

SDL2には大きく分けてMusicとChunkという再生方法が存在する。Musicは全体で1つしか再生できないBGMの管理方法なので悩むことは無い。Chunkは効果音であり、これは多重再生や複数の音の同時再生がありえ、管理方法は少しややこしくなる。
まず言葉の定義から。まあ、SDL2の関数を見ればわかる話なのだが、ChunkはSDL2のSDL_Chunk構造体で、音データをロードすると生成される。Channelは再生スロットで、デフォルトでは8個ほど用意され、再生した音は0〜7のどこかのChannelに登録されて再生される。9個以上の音を同時に再生しようとするとエラーになる。

Channelの使い方

SDL_mixerではMix_LoadWAV関数でSDL_Chunkを生成し、Mix_PlayChannel関数にSDL_Chunkを渡すことで再生する。Mix_PlayChannel関数の引数では再生するChannelを指定することもできるが、すでに他の音が再生中だったりすると失敗する。基本的にはChannelとして-1を渡して空いてるChannelを選んでもらうことになるが、これも空いていないと失敗する。戻り値は再生に使ったChannel番号(int型)である。
再生中の音に対する制御は停止や一時停止、フェードアウトなどがあり、それらはMix_PlayChannelで返ってきたChannel番号を使う。各種情報取得関数(再生中とか)もChannel番号を渡す。つまり、再生後の処理は基本的にすべてChannel単位の管理となる。

Channelの問題点

Channelは再生が終わると解放されて、他の音の再生に再利用される。これが問題である。
たとえば、画面上に何かしら喋るキャラがいたとしよう。こいつが喋っている途中に撃ち殺された場合、セリフの音声は停止しなければならない。停止にはChannel番号を使う必要があるので、再生したときに返ってきたChannel番号をキャラが保持することになる。ところが、喋り終わったらそのChannelは解放されてしまい、他の音が再生されたときに再利用される。再利用されたかどうかはキャラはわからないので、撃ち殺されたときに音声じゃない別の何かの音を停止することになる。
これを回避するには、そのChannel番号が再利用されたことを検出し、キャラが再利用されたことを認識できるようになる機構を挟む必要がある。

作ってみる

sdl2rでこの問題を回避する機構を作ってみよう。考え方としては、Channel番号を隠蔽するChannelクラスを作って、再生時に生成する。再生後の操作はChannelオブジェクトを使う。再利用されたら無効化する。無効化されたChannelオブジェクトはもう使えないのでそのまま捨てる。基本はChannelは再利用するものではなく、無限に生成して使い捨てるものである、という概念レベルの切り替えだ。
まず簡単にAudioモジュールと、ChunkをラップするSoundクラス、Channel番号をラップするChannelクラスを作る。

require 'sdl2r'

module Audio
  class Sound
  end

  class Channel
  end
end

SoundはSound.newでSDL_Chunkを生成し、Sound#playで再生してChannelオブジェクトを返す。Channelオブジェクトは停止する機能を持つ。

module Audio
  class Sound
    def initialize(filename)
      @chunk = SDL::Mix.load_wav(filename)
    end

    def play
      ch = SDL::Mix.play_channel(-1, @chunk, 0)
      Channel.new(ch)
    end
  end

  class Channel
    def initialize(ch)
      @ch = ch
    end

    def stop
      SDL::Mix.halt_channel(@ch)
      @ch = nil
    end
  end
end

この状態では再利用に関するあれこれは未実装であり、まったくダメである。Audioモジュールに@@Channelsという変数を追加して、配列を格納してChannelの情報を持たせる。再生時にセットして、停止時にクリアする。ここに入れるのはChannelオブジェクトで、入っていれば再生中である。再利用されたら入っているものはすでに再生が終わっていたということで殺す。

module Audio
  @@channels = []

  class Sound
    def initialize(filename)
      @chunk = SDL::Mix.load_wav(filename)
    end

    def play
      ch = SDL::Mix.play_channel(-1, @chunk, 0)
      @@channels[ch].ch = nil if @@channels[ch]
      @@channels[ch] = Channel.new(ch)
    end
  end

  class Channel
    attr_accessor :ch

    def initialize(ch)
      @ch = ch
    end

    def stop
      return if @ch == nil
      if SDL::Mix.Playing(@ch) == 1
        SDL::Mix.halt_channel(@ch)
      end
      @@channel[@ch] = nil
      @ch = nil
    end
  end
end

こんな感じか。

まだあるけど次の機会に。

このコードは動かしていないので動かないかもしれない。すごい勘違いとか間違いが潜んでるかもしれない。まあ、てきとうに。
それはさておき、まだいろいろと足りていない。たとえばChannelに空きが無かった場合の処理とか。
そのへんはSDL2にGroup系関数という機能があるので、それを活用する形で次の機会に機能を追加することとする。