DirectSoundとRubyのプログラミング その13
続き。
いくつか非常に気になるバグがあるのだが、それを直すのはまた今度として、今回はそれよりも気になる超基本機能の欠落をどうにかしよう。
再生中かどうか
SoundTest#playing?を実装する。
// SoundTest#playing? // 再生中かどうかを返す static VALUE SoundTest_im_get_playing(VALUE self) { unsigned long status; 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->GetStatus(st->pDSBuffer8, &status); if (FAILED(hr)) rb_raise(eSoundTestError, "get_status error"); return status & DSBSTATUS_PLAYING ? Qtrue : Qfalse; }
DirectSonudにはステータスを取得する関数があるのでそれを使って状態を取得する。このステータスではループ再生しているかどうかも返ってきたりするが、ループ再生については音をループしているのかバッファをループしているのかで扱いが変わるのでいまいち使えない。
ついでにストリーミング再生バッファかどうかを返すメソッドとdisposeされたかどうかを返すメソッドも作っておいた。
// SoundTest#streaming? // ストリーミング再生用バッファかどうかを返す static VALUE SoundTest_im_get_streaming(VALUE self) { // selfからSoundTest構造体を取り出す struct SoundTest *st = (struct SoundTest *)RTYPEDDATA_DATA(self); // st->pDSBuffer8がNULLの場合はdispose済み if (!st->pDSBuffer8) rb_raise(eSoundTestError, "disposed object"); return st->streaming_flg ? Qtrue : Qfalse; } // SoundTest#disposed? static VALUE SoundTest_im_get_disposed(VALUE self) { // selfからSoundTest構造体を取り出す struct SoundTest *st = (struct SoundTest *)RTYPEDDATA_DATA(self); // st->pDSBuffer8がNULLの場合はdispose済み return st->pDSBuffer8 ? Qfalse : Qtrue; }
ボリュームとパン
音量と左右の位置の設定を実装する。
// SoundTest#set_volume // ボリュームを設定する static VALUE SoundTest_im_set_volume(VALUE self, VALUE vvolume) { HRESULT hr; double volume = NUM2DBL(vvolume); // selfからSoundTest構造体を取り出す struct SoundTest *st = (struct SoundTest *)RTYPEDDATA_DATA(self); // st->pDSBuffer8がNULLの場合はdispose済み if (!st->pDSBuffer8) rb_raise(eSoundTestError, "disposed object"); if (volume > 100) { volume = 0; } else if (volume < 0) { volume= -10000; } else { volume = -powf(100.0f, log10f(100.0f - volume)); } hr = st->pDSBuffer8->lpVtbl->SetVolume(st->pDSBuffer8, (long)volume); if (FAILED(hr)) rb_raise(eSoundTestError, "set_volume error"); return vvolume; } // SoundTest#set_pan // パンを設定する static VALUE SoundTest_im_set_pan(VALUE self, VALUE vpan) { HRESULT hr; double pan = NUM2DBL(vpan); // selfからSoundTest構造体を取り出す struct SoundTest *st = (struct SoundTest *)RTYPEDDATA_DATA(self); // st->pDSBuffer8がNULLの場合はdispose済み if (!st->pDSBuffer8) rb_raise(eSoundTestError, "disposed object"); if (pan > 100) { pan = 10000; } else if (pan < -100) { pan = -10000; } else if (pan > 0) { pan = +powf(100.0f, log10f(pan)); } else if (pan < 0) { pan = -powf(100.0f, log10f(-pan)); } hr = st->pDSBuffer8->lpVtbl->SetPan(st->pDSBuffer8, (long)pan); if (FAILED(hr)) rb_raise(eSoundTestError, "set_pan error"); return vpan; }
微妙に処理が入っているが、DirectSoundの音量とパンの設定は1/100デシベル単位の減衰を表現するので、そのまま使うとイマイチ思ったように変化しない。線形に変化して欲しいので変換する計算を入れている。Ayameも同様の計算をしているっていうかAyameからパクった。設定する値は音量は0〜100、パンは-100〜100である。
取得するメソッドは作っていない。これは、変換する処理を逆算するのが面倒だったからで、最後に設定した値が返ってこればそれでいいのでRuby側で実装する。
ステレオとパン
例えばステレオ音源で左のみから音が出るようなデータを作って再生し、パンを右いっぱいに振ると無音になる。DirectSoundのパンは左右のスピーカの減衰で表現するので、真ん中は両方MAX、左に振ると右を減衰し、右に振ると左を減衰する。よってそのような挙動になる。
そもそもパンというのはモノラル音源に対して左右の音量差を作って好みの位置に定位させるテクニックであるからして、音源に位置情報が含まれたステレオ音源に対して使うのが間違っているんじゃないか。
などと思ってみたり。
フェード
playing?とvolume、panの各メソッドが揃ったので、これを使ってフェード処理ができるようになった。作ってみよう。ちょっと大きくなるがMySound全体を載せる。大きくなってきたのでこれをmysound.rbとしてわけて保存する。
require 'stringio' require_relative 'soundtest' require_relative 'wav' class MySound < SoundTest attr_reader :volume, :pan FADING_LIST = {} PANNING_LIST = {} 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 @volume = 100 @pan = 0 end def play(lp=false) @loop = lp super end def volume=(v) @volume = v super end def pan=(v) @pan = v super end FadingStatus = Struct.new(:from, :to, :time_from, :sec) def fade(volume, time) FADING_LIST[self] = FadingStatus.new(self.volume, volume, Time.now, time) end PanningStatus = Struct.new(:from, :to, :time_from, :sec) def panning(pan, time) PANNING_LIST[self] = PanningStatus.new(self.pan, pan, Time.now, time) end Thread.new do loop do FADING_LIST.delete_if do |k, v| if !k.playing? true elsif Time.now > v.time_from + v.sec k.volume = v.to true else k.volume = (v.to - v.from) * ((Time.now - v.time_from) / v.sec) + v.from false end end PANNING_LIST.delete_if do |k, v| if !k.playing? true elsif Time.now > v.time_from + v.sec k.pan = v.to true else k.pan = (v.to - v.from) * ((Time.now - v.time_from) / v.sec) + v.from false end end sleep(0.02) end end end
MySoundクラスにフェード中のオブジェクトとパン移動中のオブジェクトを保持し、無造作に作ったスレッド内で実時間を使って処理する。音はアナログなので厳密に1フレーム1回という処理である必要は特に無く、適当に20msスリープするようにしている。20msできちんと起きるわけではないのでたぶんかなりアバウトな処理になると思われる。でも動かすと普通に変化して聞こえるので問題はなさそうだ。
使う側はこんな感じになる。
require 'dxruby' require_relative 'mysound' rs1 = MySound.load("test.ogg") rs1.play Window.loop do rs1.panning(-100, 1) if Input.key_push?(K_LEFT) rs1.panning(100, 1) if Input.key_push?(K_RIGHT) rs1.panning(0, 1) if Input.key_push?(K_SPACE) rs1.fade(0, 1) if Input.key_push?(K_DOWN) rs1.fade(100, 1) if Input.key_push?(K_UP) Window.draw_font(0, 0, "playing? : " + rs1.playing?.to_s, Font.default) Window.draw_font(0, 24, "pan : " + rs1.pan.to_i.to_s, Font.default) Window.draw_font(0, 48, "volume : " + rs1.volume.to_i.to_s, Font.default) Window.draw_line(0, 100, 639, 100, C_WHITE) Window.draw_font(320.0 * rs1.pan / 100 + 320 - 12, 100, '↑', Font.default, color:C_GREEN) end
カーソルキーとスペースで移動できる。
おしまい
とりあえずフェードとパン移動ができた。よく考えたらplayメソッドでフェードインの指定とか、stopでフェードアウトの指定ができたほうがいい気がするが、そういうのを実装するのは簡単な話だろう。
今回のソースもgistに追加しておいた。