2008/05/10(土)Managed DirectSound でハマる
例の OggVorbis.dll もよく動いて、Framework系の仕事は一通りかなぁと思っていた矢先のデキゴトです。
昨日、部室に置いてあるパソコンで例のコードにより音楽を再生(ストリーミング)してみたときのデキゴトです。
音飛び しまくる。 音が飛びまくる。
え、なんで?! 俺のマシンではちゃんと動いていたし、
そもそも、このDLLここで作ってテストしたときはちゃんと動いてたんだよ?
そのとき部室にノートPCを持ち込んでいた人にも依頼してテスト。
新入生Vista → OK
しゅゆさんXP → OK
けろさんXP → NG
原因不明・意味不明。
で、胃をキリキリとさせながら、途方に暮れつつコードを眺める。
前動いてたときまでSVNでDLLのバージョンを戻してみるも相変わらず音が飛ぶ。
つまり、俺DLLのせいじゃない と。
先輩にもDirectSound周りのコードを眺めてもらうも、「おかしな事はやっていない」と。
先週から環境が変わったと言っても.NET2.0 SP1 と Office2003を入れたくらいで、
それが影響するとはあんまり考えにくい。
「Office2003なんか入れるからぁ!」とは言っていたが、
アレは不安の表れであって、決してOfficeのせいだと思っていたわけではないのであしからず。
経過時間と胃の痛みばかりが大きくなるω
ここで、DLLのテストに使ったテストプロジェクトをビルドしてみるとこっちはすんなりと鳴る。
えええ、中でやってること一緒なんですけど!
スレッドを作って、 Notify にセットした AutoResetEvent がシグナルになったら Write する。それだけ。
BufferSize = 262144 で実際のサウンドバッファはその2倍。
以下が上手く動いてくれるコード
// 初期化処理は省略 while(windowCreated) { Thread.Sleep(10); while (true) { if (notifyAutoResetEvent.WaitOne(0, false) ) break; Debug.WriteLine("Write... " + streamBuffer.WritePosition); if (streamBuffer.WritePosition > BufferSize) streamBuffer.Write(0, waveStream, BufferSize, LockFlag.None); else streamBuffer.Write(BufferSize, waveStream, BufferSize, LockFlag.None); } }
これに、再生や停止フェード処理を加えて、メインスレッドとのやりとり用にEventを2個作った。
が、これが動いてくれないコード。
// 初期化処理は省略 while(windowCreated) { if( recvMessage_AutoResetEvent.WaitOne(10, false) ) { switch((Message)currentMessage) { case Message.Play: streamBuffer.Play(0, PlayFlag.Looping); recvMessageProcessed_AutoResetEvent.Set(); currentMessage = Message.Idle; break; // 省略 } } // フェード処理なんか(タイマー見て streamBuffer.Volume や streamBuffer.Pan を弄る) while (true) { if (notifyAutoResetEvent.WaitOne(0, false) ) break; Debug.WriteLine("Write... " + streamBuffer.WritePosition); if (streamBuffer.WritePosition > BufferSize) streamBuffer.Write(0, waveStream, BufferSize, LockFlag.None); else streamBuffer.Write(BufferSize, waveStream, BufferSize, LockFlag.None); } }
メインスレッドからは notifyAutoResetEvent には触ってないし、
ワーカースレッド優先度はAboveNormalで、全体のCPU使用率も40%程度。
それで。なんか知らんけどですね。
recvMessageProcessed_AutoResetEvent.Set(); をコメントアウトするだけで、
(もちろん、メインスレッド止まってしまうけれど)音はちゃんと再生されるのですよ。
このイベント、バッファどころかDirectSoundとは何の関係もないとこにあるはずなんですg
recvMessageProcessed_AutoResetEvent.Set() の行があるだけで、
バッファの通過点に来る前にnotifyAutoResetEvent.WaitOne() が trueを返してくるようになるので
結果的に、Writeが実行されまくって、再生の終わってないバッファをも上書きし音が飛ぶ、ようです。
よくわかりません。
よくわかりませんが、再生も終わる前から notifyAutoResetEvent がシグナルになるのです。
ちなみに、不具合の発生しているマシンにおいて dxdiag で DirectSoundのアクセラレータを
標準アクセラレータ以下(ソフトウェアエミュレーション)にすると、
フェード処理なんかは死んでしまうものの Writeのタイミングだけはしっかり測ってくれるみたいです。
同様に streamBufferを作る際に bufferDescription.LocateInSoftware を true にしておいても、
問題は解決することを確認。
今になってこんな不可思議な事態にぶつかるとわ……。
まぁ、うだうだ言ってても仕方ないので、メインスレッドとの通知のやりとりはEvent使わない方向で、
組み直すことになってしまいました。
とりあえず今回の問題が見つかったところで時間切れ。
個人的には一人修羅場で直してしまいたかったのですが、
ご飯を食べに行くという他のメンバーを待たせるわけにもいかず、この日は部室を後にすることに。
正直、原因がわかってないので、今回イベントを使わない方法で対応したとしても
どこで歪みが生じるのやら怖くて夜も眠れません。
Vista ではDirectSoundがなくなってしまったそうですが、それは、
こういう不具合なサウンドカードが世に蔓延っているのを一掃するため
だったりしt……それはないかω
ヴァー
追記
おうちのマシンでも、オンボードの蟹さんAC97を使うことで発症してくれました。
これで心おきなくデバッグができそうです(死
問題はイベントそのものではなくて、メインスレッドが走っていると音が飛んでいるような気がします。
# まぁ、こんな話聞いたことなかったしなぁ……
メインスレッドの負荷が高すぎるということなのか、なんなのかよくわかりませんが。
追記
効果音止めたら飛ばなくなった。
追記
再生してもいないBGMトラックのwriteNotifyイベントが発生してる。
だいたい全貌が見えた。
BGMの再生には、ストリーミングバッファを使う。
つまり、バッファを分割して、分割分の再生が終わるたびに通知してもらって、
反対側の再生が終わるまでに今再生し追えたバッファを上書きして、切れ目のない再生を行う。
たとえば バッファを2つに切って、[|区間1-----|区間2-----]ループ再生させておく。
区間1の先頭を通過したら通知してもらって区間2に次のPCMを書き込む。
区間2の先頭を通過したら通知してもらって今度は区間1の中身を書き換える。
この繰り返し。
Windowsはバッファを作ってくれとサウンドカードに要求を出す。
サウンドカードはOKを返す、が、しかし。
一部のテキトーサウンドカードとドライバは新しいバッファを作らず、1個を使い回す。
音楽を鳴らすときも、効果音を鳴らすときも、全部同じバッファ(論理上の)を使ってるみたいだ。
[|区間1-----|区間2->---] 音楽を鳴らしている。
ここで、区間2 の中盤にさしかかったところで、効果音を鳴らしてみる。
効果音も(Windowsは違うと思っているけど)同じバッファが使われる。
[|区間1->---|区間2--->-]
ここで 当然区間1の先頭を通過した旨を示すイベントが発生する。
普通に効果音バッファだけで発生してくれるならいいけど、
適当なサウンドカードやドライバでは作ったバッファのすべてイベントが発生してしまう。
なぜならバッファは1個しかないのだから……。
それを受けて、BGMスレッドは まだ再生途中にもかかわらず 区間2 の中身を書き換えてしまう。
結果、音が飛ぶ。
という風に見える。俺には。
解決するには、イベントに頼らないバッファの書き換えが必要ということ。
なんだそっかぁ、 Notify 使えないのかー
つかえねーのか!
結論
DirectSound において ストリーミングバッファで BGMを再生するとき(厳密には多重再生が必要なとき) Notify は使えないらしいです。
Timerでも使いましょう ということ ^^;;;
タイマー使って実装した。
完璧! だそうです。よかったね、俺。