(未完) それ遅延 50ms じゃないから、という話
この記事は「AI声づくり技術研究会 Advent Calendar 2024」24 日目の記事です。
(枠が空いていたので、上手くまとまらず完成しなかった記事の供養をします。)
こんばんは。Project Beatrice です。
普段はストリーミング声質変換 VST 「Beatrice」を開発しています。
この VST、遅延が約 50ms と非常に短いことが特長のひとつです。
ところで、遅延が 50ms というのは、どういう意味なのでしょうか?
えっそんなん喋ってから処理された声が聞こえてくるまでに決まってんじゃん……と思われるかもしれません。
私もそう思いますし、Beatrice の約 50ms という値はその意味での遅延を表しています。
しかし、世の中で使われている遅延の秒数は、必ずしもその意味ではありません。
むしろ、そうでないことの方が多い印象さえあります。
この記事では、(声質変換に限らず) 音響信号処理において発生する遅延の種類について説明し、世の中の「遅延 0.xx 秒!!」を私がなぜ「信用できんな~~~」と思っているか説明します。
前提知識
音の正体が空気などの振動 (圧力の微細な変動) ということは、中学の物理で習ったと思います。
オシロスコープを使うと圧力変動の様子を波形として見ることができ、音が大きいほど波の振幅 (変動幅) は大きく、音が高いほど周期は短く (波の間隔は狭く) なります。
アナログレコードはこの変動を直接溝に刻んで保存しています。
PC などでは、音は 1 秒あたり 44,100 回などの頻度で圧力の変動を記録したデジタル信号として扱われます。
REAPER で音波形を拡大して見ると、サンプルをひとつずつ見ることができます。
この記録 1 回分のデータを「サンプル」と呼び、1 秒あたりのデータの記録回数を「サンプリング周波数」と呼びます。
ブロック処理による遅延 (バッファリング遅延)
簡単な音響信号処理の例として、入力された音の大きさ、すなわち振幅を 2 倍にする処理を考えます。
この処理は、入力されたサンプルの値を 2 倍にして出力するだけのものです。
TODO: 図
入力を 1 サンプルずつ逐次的に処理する (時刻 1 の入力サンプルを受け取って 2 倍にして時刻 1 の出力サンプルとして返し、時刻 2 の入力サンプルを受け取って 2 倍にして時刻 2 の出力サンプルとして返し、と繰り返す) 場合、この処理に発生する遅延はほぼ 0 といえます。
しかし、実用上 1 サンプルずつ処理を行うのは効率が悪く、そのように処理を行うことはほとんどありません。
音響信号処理では、多くの場合一度に複数のサンプルをまとめて処理します。
このまとめられたサンプルの集まりを「ブロック」と呼びます。
ブロック処理は計算の効率化のために非常に有用ですが、その一方で遅延が発生します。
これは、ブロック処理を行うためには、まずブロックがサンプルでいっぱいになるまで入力を待たなければならないためです。
例えば 256 サンプルずつのブロック処理を行う場合、ブロックに入力サンプルが詰められ、ブロックに対して音量を 2 倍にする処理が開始されるまでに 256 サンプル分の時間がかかります。
サンプリング周波数が 44,100 Hz であれば、この遅延は約 5.8 ms になります。
DAW などの音響処理ソフトウェアでは、多くの場合このブロックサイズをユーザが設定できるようになっており、ブロックサイズを大きく設定するとその分遅延が大きくなることを確認できます。
計算機の処理速度による遅延
引き続き、ブロックサイズ 256 で音量を 2 倍にする処理を考えます。
前項で説明した遅延は、処理が開始されるまでに発生するものでした。
しかし、当然ながら、計算機が値を 2 倍にする処理を行うのにも時間がかかります。
もちろん、処理にかかる時間がそのまま遅延の量になるのですが……。
怖いのは、処理時間がブロックごとに毎回変化する場合です。
(ガベージコレクタの発動などの要因も含みます。)
処理時間がブロックごとに変わるので遅延も毎回変わる……としてしまうと、出力される音声がダブったり欠損してしまったりしてはちゃめちゃになってしまうことは想像に難くないと思います。
そのため、遅延の量は基本的に処理にかかる最大時間で考える必要があります。
アルゴリズム遅延
ここまでで例に使ってきた「音量を 2 倍にする」という処理は、仮に計算機 (「コンピュータ」の日本語訳です) の性能が最強であれば、ブロック処理を行わずサンプルごとに微小な時間で処理を行うことで、遅延を限りなく小さくできるものでした。
しかし、サンプルごとに処理を行い、なおかつ計算機が入力されたサンプルを即座に処理できる場合でも、処理の種類によっては、性質上どうしても避けられない遅延 (アルゴリズム遅延) が発生します。
線形位相フィルタによる遅延
例えば、線形位相フィルタはアルゴリズム遅延が発生する処理の代表例です。
普段音をよく扱う方なら「波形が崩れないが、代わりに遅延やプリリンギングが発生する」などと聞いたことがあるかもしれません。
簡単な線形位相フィルタの例として、5 サンプル分の単純移動平均によるローパスフィルタを考えます。
このフィルタは、自身とその前後 2 サンプルの平均を取った値を出力するものです。
図: 移動平均によるローパスフィルタ。左側の低い周波数の波はあまり変化していませんが、右側の高い周波数の波は大きく減衰しています。
この処理を逐次的に遅延無く行おうとした場合、どのような問題が発生するでしょうか?
上図のような入力の場合、計算機は
- 時刻 1 の入力サンプル (5) を受け取って時刻 1 の出力サンプル (4) を返す
- 時刻 2 の入力サンプル (7) を受け取って時刻 2 の出力サンプル (5.2) を返す
- 時刻 3 の入力サンプル (8) を受け取って時刻 3 の出力サンプル (6.2) を返す
……といったように振る舞うことになります。
しかし、この処理は実際には実現不可能です。
例えば、時刻 3 の出力を計算するには、時刻 1 から 5 までの入力サンプルが必要になります。
計算機が時刻 3 の出力を求められた時点で時刻 4, 5 の入力サンプルはまだ計算機に入力されていないため、この処理は実現できません。
ではどうすればいいのかといえば、出力を 2 サンプル遅延させることでこの処理は可能になります。
TODO: 出力波形が全体として 2 サンプル右にずれている図
5 サンプルの移動平均の場合、遅延は 2 サンプル分となります。
サンプリング周波数が 44,100 Hz であれば、この遅延は約 0.045 ms です。
よりタップ数の大きいフィルタであれば、遅延はさらに大きくなります。
STFT による遅延
アルゴリズム遅延の発生するより身近な例として、STFT (短時間フーリエ変換) と iSTFT (逆短時間フーリエ変換) による遅延を紹介します。
DNS Challenge 2023 (ノイズ除去の世界大会のようなもの) でもルールに STFT による遅延の例示があり、それくらい重要で厄介な遅延です。
STFT は、時間的に変化する音に対して、各時刻で波形の断片を取り出して周波数スペクトル (簡単に言えば、音がどのような周波数成分で構成されているか) に変換する操作です。
これにより、周波数ごとに音を操作するようなことができ、声質変換やノイズ除去などの音響信号処理でよく利用されます。
STFT では通常、入力信号を一定間隔のフレームに分割し、それぞれに対して周波数スペクトルを求めます。
(この間隔をフレームシフトや "hop_length" などと呼ばれ、典型的には 512 サンプルなどが使われます。)
注意する必要があるのは、あるフレームに対して周波数スペクトルを求めるとき、そのフレームの内部に含まれるサンプルだけではなく、その周辺 (過去と未来) のサンプルも同時に使うことです。
(使うサンプル数を窓長や "win_length" などと呼び、典型的には 2048 サンプルなどが使われます。)
実際の逐次処理では未来のサンプルを使えないため、線形位相フィルタの例と同様に、出力を遅延させる必要があります。
また、iSTFT では周波数スペクトルを波形に戻す操作を行いますが、やはりそのフレームの内部だけでなく、周辺のサンプルに対しても影響を与えます。
過去の (出力済みの) サンプルを変更することはできないため、こちらも出力を遅延させる必要があります。
結局、遅延の量はどれくらいになるのでしょうか?
上図は、フレームシフトを 4 サンプル、窓長を 8 サンプルとした場合の STFT と iSTFT の例です。
これを見ると、青矢印で示した出力サンプルは、赤矢印で示したおよそ win_length 未来の入力サンプルに影響を受けることがわかります。
また、win_length 先よりもさらに未来の入力サンプルに影響を受ける出力サンプルが無いことから、遅延の量は win_length になると言えます。
ただし、これはブロック処理による長さ hop_length の遅延を含んだ値であるため、STFT/iSTFT 自体による遅延は win_length - hop_length と考える方が一般的かもしれません。
さて、STFT/iSTFT による遅延が win_length - hop_length になるとわかりましたが、これは具体的にはどれくらいの大きさでしょうか?
Python での信号処理でよく使用される librosa の melspectrogram 関数のデフォルト値は、サンプリング周波数 22,050 Hz, hop_length=512, win_length=2048 です。
この場合、遅延は 2048 - 512 = 1536 サンプル、すなわち 1536 / 22,050 Hz ≈ 70ms です。
Beatrice の遅延が約 50ms ですから、何も考えずに使うと 70ms の遅延を発生させる STFT/iSTFT という操作を我々低遅延声質変換開発者が如何に厄介に見ているか、想像に難くないと思います。
ブロックサイズの変換による遅延
あらゆる音響信号処理のシステムでブロック処理は使われていますが、そのブロックの大きさはシステムによって異なります。
例えば、Beatrice は内部的に 10ms ずつ処理を行っていますが、VST という規格で音響信号処理プラグインを作る際は、任意の数のサンプルを受け取って処理を行い、同じサンプル数の処理済み信号を返すように実装する必要があります。
このブロックサイズの変換の際にも避けられない遅延が発生します。
リサンプリングによる遅延
ブロックサイズと同様、動作するサンプリング周波数も、システムによって異なります。
Beatrice は 16kHz の波形を受け取って 24kHz の波形を出力しますが、VST ではやはり任意のサンプリング周波数で処理を行えるようにする必要があります。
サンプリング周波数の変換 (リサンプリング) ではしばしば線形位相フィルタが使われ、これは先述の通り遅延を発生させます。
(通常 3ms 以内くらいに収まるはずです。)
その他の遅延
ここまで声質変換の開発者側が制御できそうな部分の遅延について書いてきましたが、実際に変換を動かすときにはさらにその他様々な要因 (VST ホストアプリケーション、OS、オーディオインターフェース、スピーカー) による遅延が加わります。
VT-4 などのようにハードウェアまで作らない限り、これらの遅延は変換を行なう環境依存です。
みんなどの遅延までを考慮しているのか?
あらゆる開発者が「遅延 xx ms のボイチェンをつくりました!」といったことを喧伝しています。
また、それを使う側も「設定を詰めたら遅延 xx ms で変換できた!」と言ったことをよく口にしています。
これは一体上記のどの遅延までを織り込んだ値なのでしょうか。
最初に記載した通り、Beatrice の「約 50ms」は、喋ってから実際に変換音声が聞こえるまでの遅延、すなわち上記全てを含んだ遅延を表しています。
より詳しく書けば、短い声を発してスマホで入力音声と変換音声の両方が聞こえるように録音し、波形の距離を測っています。
(もっと正確に測る方法はあると思いますが、そんなに頑張っていません。)
Beatrice 以外はどうでしょうか。
先ほど名前を出した DNS Challenge 2023 では、提出されるモデルに「アルゴリズム遅延 + バッファリング遅延 ≦ 20ms」という制約が課されています。
また、「RTF ≦ 0.5」という記載もあり、これは処理時間も考慮していることを意味しています。
アルゴリズム遅延・バッファリング遅延・処理時間 (による遅延) の 3 つは考えようぜという感じでしょうか。
今年の初めに Google から発表された StreamVC というストリーミング声質変換アルゴリズムは、遅延が 70.8 ms であると報告されています。
内訳を見ると、
- 20ms のフレームで t-2 フレーム目の出力を計算するのに t+1 フレーム目までの入力が必要なので 60ms
- 1 フレームの処理時間が平均 10.8ms
とあります。
どうやらアルゴリズム遅延と処理時間による遅延のみ考慮しているようで、バッファリング遅延 20ms は含まれていないようです。
(読み間違えていたらお知らせください)
同じく今年発表されたストリーミング声質変換アルゴリズムである DualVC 3 はどうでしょうか。
論文にはアルゴリズム遅延・バッファリング遅延・処理時間それぞれの記載があり、合わせて約 50ms となるようです。
(この論文全然ちゃんと読めていない、HiFi-GAN with iSTFT upsampling layers とか書いてあるしもしかしたら Vocoder のアルゴリズム遅延とか含まれていないかも)
遅延の例 (音声)
(50ms とか 100ms の遅延ってこれくらいだよって音声貼りたかったけど用意が間に合わなかった)
おわりに
遅延を実際に計ったりしているのは稀っぽくて何ミリ秒とか言ってるのあまり信用できないと思ってる、という話でした。
実際に使って自分自身の耳を信じるのが一番良いと思っています。
Discussion