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

続き。今回はちょっと趣向を変えて、oggファイルの再生をしてみよう。

oggについて

拡張子oggのファイルはOgg Vorbisというファイルフォーマットというか圧縮形式というかのファイルで、OggというコンテナにVorbisという形式のデータが入っている。Vorbisは音声圧縮形式で、OggにはVorbis以外にもTheoraという映像圧縮形式のデータなども入れることができるが、歴史的に拡張子oggのファイルにはVorbisが入っていることになっている。
Ogg Vorbisを再生するには開発元のXiph.orgが提供するライブラリを使って展開し、PCMデータを作って、プラットフォームごとの再生APIに渡す。今までの連載でDirectSoundにPCMデータを渡す部分はできているので、oggファイルからPCMデータを作ることができれば再生できるはずだ。それもストリーミングで。

まず環境構築から

RubyInstallerのDevkitを使っているので、こいつにインストールしてやることで使えるようになる。おさらいとして、Devkitのインストールから。まず、RubyInstallerのダウンロードページRuby(2.0以降)を落としてきてインストールする。うちではRuby2.1.5をDドライブにインストールしたのでd:\ruby21というディレクトリになった。
次にDevkitをダウンロードする。うちは32bitのRubyだから32bitのDevkitである。d:\ruby21\devkitを作ってその下に展開する。んで、コマンドプロンプトでd:\ruby21\devkitに移動して、

ruby dk.rb init
ruby dk.rb install

と打てばインストール完了である。
通常のコマンドプロンプトからgccを使いたい場合はd:\ruby21\devkitの下にあるdevkitvars.batを実行すればパスが通るし、msys.batを実行すればmingwのシェルが動く。

oggライブラリのインストール

まずXiph.orgのダウンロードページからliboggとlibvorbisをダウンロードする。
mingwのコマンドを使うのでmsys.batを起動する。devkit下にhomeディレクトリができるので、その下の自分の名前のディレクトリにliboggとlibvorbisを展開する。
msys.batのウィンドウで

cd libogg-1.3.2
./configure --prefix=/mingw
make
make install

とするとインストールができる。libvorbisのほうも続けて

cd ..
cd libvorbis-1.3.4
./configure --prefix=/mingw
make
make install

でよい。こちらの記事を参考にした。
俺はいつも通常のコマンドプロンプト窓からコンパイルしているのでmsys窓のほうはよくわからないのだが、この方法でインストールしたものはそのままではgccが参照してくれないので、d:\ruby21\devkitの下にあるdevkitvars.batを少し書き換える必要がある。

:: convenience script residing in the DevKit root dir used for
:: manually configuring a Command Prompt environment to use the
:: DevKit for compiling native Ruby extensions
@ECHO OFF
ECHO Adding the DevKit to PATH...
SET RI_DEVKIT=%~dp0
SET PATH=%RI_DEVKIT%bin;%RI_DEVKIT%mingw\bin;%PATH%
set C_INCLUDE_PATH=%RI_DEVKIT%include;%RI_DEVKIT%mingw\include
set CPLUS_INCLUDE_PATH=%RI_DEVKIT%include;%RI_DEVKIT%mingw\include
set LIBRARY_PATH=%RI_DEVKIT%lib;%RI_DEVKIT%mingw\lib
set MINGW_DIR=%RI_DEVKIT%mingw

後ろにsetを4行ほど追加した。あと、devkitvars.batを実行してないプロセスではmingwのbinに入っているdllはパスが通ってないので、libogg-0.dll、libvorbis-0.dll、libvorbisfile-3.dllはパスが通ったディレクトリにコピーしておく必要がある。とりあえずはSoundTestの場所に置けば動くし、Rubyのbinとかでもよい。
これでoggのライブラリを使えるようになった。

vorbisfileを使う

vorbisfileというライブラリを使うと最も簡単にPCMデータを取得することができる。Cのコードの先頭に

#include "vorbis/vorbisfile.h"

と書けば使える。使い方はすごく簡単で、OggVorbis_File構造体の変数を作って、

OggVorbis_File ovf;

ov_fopenにファイル名とOggVorbis_File構造体のアドレスを渡せば準備ができ、

ov_fopen(filename, &ovf);

ov_readでPCMデータが取り出せる。

readsize = ov_read(&ovf, buffer_address, request_size, 0, 2, 1, NULL);

後ろのほうの数字とNULLは固定でよい。こう指定することで16bitのリニアPCMのデータが取得できる。引数の意味が気になる人はvorbisfileのov_read()ドキュメンテーション和訳を参照するとよい。
あと、閉じるときはov_clearである。

ov_clear(&ovf);

また、OggVorbis_File構造体の中に音声データのパラメータが格納されていて、ov_infoでポインタを取り出すことができる。

vorbis_info *info = ov_info(&ovf, -1);

2つ目の引数-1は固定でよい。この構造体からはrate(周波数)とchannels(チャンネル数)が取得できるので、これをSoundTest.newに渡す感じだ。

Ogg Vorbisデコード時の注意点

Ogg Vorbisは可変長の圧縮形式なので、ov_readに例えば1024バイトくれ、と指示しても、1024バイトきっかり返ってくることはまずない。展開できる単位ごとに展開して、指定したサイズを超えないように返してくる。だからデータのサイズを戻り値で返してくるわけだ。また、4096以上のサイズを指定しても一度に4096バイトまでしか返してくれないようである。なので、欲しいサイズになるまで繰り返し実行する必要があるし、でも欲しいサイズきっかりになることはないので、戻り値が0になったらそこで諦めることになる。要求したバッファが全部埋まることがまず無いわけで、だいたいちょっと足りない。そのことを考慮しておかなければならない。

SoundOggクラスの追加

C側はSoundTestとは別にSoundOgg.cを作って、そっちでSoundOggクラスを定義する。こいつはSoundOgg.new(filename)でvorbisfileを使ってファイルをオープンし、SoundOgg#read(size)で要求したサイズのデータ(1回の呼び出しで最大4096、でもだいたいちょっと足りない)を読み込んで文字列で返す。
コードは短く簡単なものなので細かく説明はしないが、gistに置いておいた。
SoundTest.cのほうはInit_soundtestの最後に

    // SoundOggクラス生成
    Init_soundogg();

と追加する。コードは一応gistに置いた。
SoundOgg.cをSoundTestのディレクトリに置いて、extconf.rbを以下のように書き換えて、

require "mkmf"

SYSTEM_LIBRARIES = [
  "dxguid",
  "dsound",
  "gdi32",
  "ole32",
  "user32",
  "kernel32",
  "vorbisfile", # 追加
]

SYSTEM_LIBRARIES.each do |lib|
  have_library(lib)
end

have_header("dsound.h")

create_makefile("soundtest")

ruby extconf.rbして、makeすると、SoundOggが追加されたSoundTest.soができあがる。

Ruby側の処理

Oggをストリーミング再生するには、SoundOggをnewして、SoundTestに要求されたバッファが埋まるまでSoundOgg#readを繰り返し呼ぶ。文字列で返ってくるのでひたすら連結する。SoundOgg#readには大きな数字を渡しても4096バイトまでしか返ってこないが、気にすることなく渡して、返ってきたぶんだけ要求サイズを引いていく感じでよい。0文字が返ってきたらそこで終わって連結した文字列をSoundTestに返却する。

require 'dxruby'
require_relative 'soundtest'

class OggStream < SoundTest
  def initialize(filename)
    @ogg = SoundOgg.new(filename)
    super(@ogg.rate, @ogg.rate, 16, @ogg.channels)
    @pos = 0

    @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

    self.writebuf_str(&@prc) # とりあえずバッファを埋める
  end

  def play
    self.stop if @th
    super(true) # ループ再生
    @th = Thread.new do
      loop do
        begin
          break if self.wait
          self.writebuf_str(&@prc)
        rescue
          p $!, $@
         end
      end
    end
    @th.priority = 1
  end
end

rs1 = OggStream.new("test.ogg")
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のデコードクラスを拡張ライブラリで作っただけでストリーミング再生ができるようになってしまった。SoundOggとSoundTestを繋ぐ部分がRubyで書かれているので、C部分もRuby部分も簡単なコードである。しかし現状のコードではoggのデータの終わりを認識しないし、シーク処理が作られていないのでループできないしで、再生できるだけで実用には向かないレベルとなっている。とはいえ実用的なものを作るのが目的ではないので気が向いたらやってみる。
Oggのライブラリ呼び出しはDLLをFiddleで呼ぶという手もありそうだが、OggVorbis_File構造体をどう扱うかが若干の問題になりそうである。それを除けばインターフェイスは簡単なものなので実は拡張ライブラリを作る必要も無かったかもしれない。