転換 - システムタイマーからサウンドタイマーへ

 そもそも、システムタイマーとサウンドタイマーがズレるというなら、タイマーをどちらか一方に統一すればいい話です。

 WAVE再生位置自動補正機能は、サウンドタイマーをシステムタイマーに合わせるための機能だったと言えます。しかし、この機能を使うと副作用のプチノイズが甚だしく、心地よいものではありません。

 DTXMania は腐っても音楽ソフトです。え? これからは逆に、システムタイマーをサウンドタイマーにあわせて、プチノイズも音ズレもない環境を提供すべきなのです。

 このような方針に転換したのは、割と最近のことです。サウンドタイマーをシステムタイマーとして扱う仕様は、今のところ、DirectSound を廃した DTXmatixx 以降に限られています。

後手にまわった DirectSound

 一般に、サウンドデバイスからタイマーを直接的に取得する方法はなく、サウンドの再生位置(再生を開始してから現在までの経過時間や、再生カーソルのオフセット位置、経過サンプル数など)を読み取って、その位置と周波数から相対的な時刻を算出する方法を採ります。

 サウンドデバイスに Direct にアクセス出来るはずの DirectSound は、この辺りの性能改善ではかなり後手にまわっていました。

 初期の DirectSound では、IDirectSoundBuffer.GetCurrentPosion が返す再生位置は、実際の再生位置よりもずっと前方を示していました。再生位置として、再生用バッファの取り得る限界まで未来の位置を返してきました。精度なんて概念は微塵もありません。

 DirectX8 では、DSBCAPS_GETCURRENTPOSITION2 というフラグが導入されました。これを指定すると、GetCurrentPosition がより正確な位置を返すようになるそうです。指定しなかったら互換性のためという理由で初期のバグをわざわざ再現するぞ! ですが、それでも精度が甘く、DTXMania ではこれをタイマーとしては採用できませんでした。

 DSBCAPS_CTRLPOSITIONNOTIFY というフラグもありました。サウンドデバイスに位置通知機能があることを示すフラグだそうです。しかし主にストリーミング再生を想定していたのか、通知される位置は非常に精度が悪く、通知(イベント)が来た時にはすでに次のセクションに突入していたりしたそうです。よってこれも没です。

 Windows Vista からは、DirectSound がソフトウェア化された影響で、ただでさえひどい GetCurrentPositon の精度もさらに落ちました。そこでさらに、DSBCAPS_TRUEPLAYPOSITION というフラグが導入されました。なんかもうヤケクソ感満載の命名ですね。 これを指定すると、今度こそ真に正しい再生位置が返されるようになるそうです。

 ただし、このフラグは Vista 以降でのみ有効です。
 Vista 以降?
 Vista 以降は、レイテンシの問題で、そもそも DirectSound は使いたくありません。
 時すでに遅しです。

サウンドタイマーの自作

 WASAPI や ASIO を使う場合は、仕様上、DTXMania が自分でサウンドデータを然るべきバッファに少しずつ書き込まなければなりません。逆に言えば、再生位置を自分である程度把握できるということです。この再生位置情報を使って、自分でサウンドタイマーを作ることはできないでしょうか。

 結果から言いますと、できたようなできなかったような。

 サウンドデータの書き込み周期(書き込むバッファの長さ)はどう頑張っても 3㎳ くらいが短さの限界なので、これをもとにタイマーを作成しても、その精度も同じく 3㎳くらい が限界になります。レイテンシなら 10㎳ くらいあってもいいのですが、これがタイマーの精度ならそうはいきません。

 結果、譜面のスクロールがガタガタになりました。

 そこで、数㎳ くらいならサウンドタイマーとのズレも無視できるはずだという仮説のもと、書き込みと書き込みの間にシステムタイマーで差分を加えて補間するようにしました。

 結果、譜面スクロールはガタガタのままでした。

 最終的に、この自作サウンドタイマーと、従来通りの(音ズレの可能性のある)システムタイマーのどちらを使うかは、UseOSTimer というオプションの追加とともに、ユーザに託されることになりました。

 どうやら、状況はより悪化したようです。

WASAPI への特化

 DTXmatixx は、 WASAPI にしか対応しませんでした。DirectSound にも ASIO にも対応していません。Windows 7 もサポート外になりました。なぜでしょうか。

 これには 2 つの理由があります。

 ひとつは、WASAPI に IAudioClock という機能が登場したことです。IAudioClock.GetPosition は、再生位置をかなり高い精度で返してきます。パフォーマンスカウンタと同じ精度(100ns)です。これを使うことで、サウンドタイマーの自作もかなり容易になりました。
 ただし、IAudioClock は Windows Vista 以降でのみ利用可能です。

 もうひとつは、WASAPI の共有モードのレイテンシが 10㎳ 以下を保証するようになったことです。WASAPI の排他モードや ASIO であれば、サウンドデバイスやパソコンの能力に応じてユーザがどんどんレイテンシを追い込む(音割れする限界まで短くする)ことができるのですが、「標準で」「最悪でも」10㎳ が保証されるようになったのなら、わざわざレイテンシを追い込んで音ズレの原則をあらわにすることもなく、音ズレを知覚しにくいレベルまで持っていけます。これに乗らない話はありません。
 ただし、これは Windows 10 以降が対象です。

 さらに、これとは別に、動画(Media Foundation)での制約もあったことから、最終的に DTXmatixx では Windows 7 をサポート対象外にすることに決めました。

 ちなみに、Windows 7 のサポートが終了する 2020 年 1 月は、奇しくも DTXMania 生誕20周年の月だったりします。なんかくれ。

Low Latency Audio

 Windows 10 では、WASAPI に IAudioClient3 (IAudioClient のバージョン3)が追加されました。これを使用すると、WASAPI の共有モードのレイテンシを更に短くすることができるようになります。

【参考】
Low Latency Audio | Microsoft Docs

 しかしながら、これに対応しているサウンドデバイスやオーディオライブラリはまだまだ少ないのが現実です。2024年時点では、Microsoft 純正の Surface Pro シリーズで 5ms に変更できることが確認できたくらいです。

 DTXMania では BASS というオーディオライブラリを使って DirectSound や WASAPI、ASIO などをコントロールしていました。「序」の章でも述べましたが、この BASS が Windows 10 の Low Latency Audio に対応したのは 2017 年 11 月 15 日のことです。本記事(初版)の執筆時点から見てほんの1か月前のことです。

 一方、DTXmatixx では BASS の代わりに WASAPI に特化した CSCore というオーディオライブラリを使っていますが、こちらはまだ対応していないようです(※執筆時点)。

 ………… 。

 先の話と食い違っていることにお気づきですか?

 私もそう思うのですが、どうやら最新の Windows 10 における WASAPI の共有モードは、IAudioClient3 を使わない CSCore でも 10㎳ のレイテンシを保証してくれているようなのです。実際、DTXmatixx や DTXMania2 は、10㎳ の WASAPI 共有モードで動作していました。
 なので、私はまだ意図的に IAudioClient3 を使ったことはなく、その恩恵もよく分かっていません。

 でも、今のところそれでうまく行ってるんですから、いいですよね。

 ね。

WASAPI タイマーの課題

 新しい WASAPI に特化して、それで問題が解決したわけではありません。

 どうやら、WASAPI の IAudioClient.GetPosition は、ときどき飛び飛びになった階段状の再生位置情報を返してくるようなのです。結果として「描画のズレ」が起き、譜面スクロールが飛び飛びになってがたつきます。今のところは演奏に問題ないと言える程度のちょっとしたがたつきですが、やはり気持ちのいいものではありません。

 IAudioClient.GetPosition は、実は2種類の再生位置を返してきます。ひとつはサウンドデバイス固有のものっぽい(確信がない)再生位置とその周波数、もうひとつは、(おそらくはパフォーマンスカウンタをもとにして)100ns 単位に変換された再生位置です。

 推測ばかりで申し訳ないですが、どうやら前者は CPU やサウンドデバイスやサウンドドライバの性能にも依存するようで、それが飛び飛びの再生位置という形で露見しているようなのです。

 DTXmatixx ではこれを、後者の情報に変えて様子を見ています(※初版執筆時点)。今のところはきれいに動いていますが、果たして……。

☆   ☆   ☆
 

 また、入力のタイムスタンプとの整合性という課題もあります。

 基本的に、キーボード(DirectInput)のタイムスタンプは、1㎳ の精度、すなわち内部でシステムタイマー(timeGetTime)を使用しています。このため、このタイムスタンプをサウンドタイマーに合わせて変換しなければ「入力のズレ」が発生してしまいます。

 電子ドラム(MIDI入力)に至っては、(最新の WinRT API 以外は)タイムスタンプすら存在しません

 余談ですが、はるーか昔には、ジョイスティック端子(大抵サウンドカードについていた)からクロックを取得して「標準のタイマー」として活用することもできたとか。周波数も低い上に、めちゃくちゃ重たい処理だったらしいですが。