BLIT(Band Limited Impulse Train)による鋸波の生成(3)


前回、BLITを使って鋸波をうまく生成できることが確認できましたので、今度はlogue SDKで実装してみます。

今回のコードおよびバイナリは以下のリポジトリに置いてあります。

GitHub - boochow/BLIT_osc: a sawtooth oscillator for KORG prologue/minilogue-xd/NTS-1 using BLIT technique
a sawtooth oscillator for KORG prologue/minilogue-xd/NTS-1 using BLIT technique - GitHub - boochow/BLIT_osc: a sawtooth oscillator for KORG prologue/minilogue-x...

実装にあたって、追加で考慮が必要な点が2つあります。

(1)\(Sinc_M\)関数で分母が0の場合
(2)Leaky Integrator

まず、(1)\(Sinc_M\)関数で分母が0の場合です。

\(Sinc_M\)関数は以下のような形をしています。
$$y(n) = \frac{\sin(M\pi \phi)}{P\sin(\pi\phi)} $$
この関数は、\(\phi=0, 1, 2, \ldots\)のとき分母が0になります。ですので上の式を普通に計算すると、ゼロによる除算でエラーになる可能性があります。

分母が0の時の\(Sinc_M\)関数の値は、ロピタルの定理を使って
$$ \lim_{\phi\rightarrow 0} \frac{\sin(M\pi \phi)}{P\sin(\pi\phi)}
= \lim_{\phi\rightarrow 0} \frac{\sin'(M\pi \phi)}{P\sin'(\pi\phi)} \\
= \lim_{\phi\rightarrow 0} \frac{M\cos(M\pi \phi)}{P\cos(\pi\phi)} \\
= \frac{M}{P} $$
のように求められます。

実際には、この値を分母が0の時だけでなく、0に近いごく小さい値(今回の実装では1.0-E5としています。サンプリング間隔が2.08E-5なので、その半分程度です)で用いました。おそらく計算誤差のせいだと思いますが、試作中に音を鳴らしていたところ、この閾値が小さすぎると、数十秒に一回程度、\(Sinc_M\)関数の値がピークになるべきところで、本来よりずっと小さい値になってしまうことがあったためです。

BLITでは、直前のサンプルとの差分をずっと加算しつづけることで信号を生成しますので、その差分の数値に誤差があると、それはずっと信号に残ってしまいます。つまり、信号の中心線(本来は0)が、だんだんプラス側やマイナス側に寄って行ってしまう可能性があります。

これは誤差以外にも、発音中にピッチが変化した場合に起こります。本来ピッチが変化した場合は\(y(n)\)や\(\phi\)をリセットすべきですが、そうするとピッチベンダやポルタメントのときにプチプチノイズが出てしまいます。

(2)Leaky Integratorは、入力の誤差がずっと蓄積されないよう除去するために「もし差分が0の状態が続いたら」\(y(n)\)の値がゼロに近づいていくようにする処理です。
イメージとしては、ちょっと穴が開いている入れ物に水を少しずつ入れていくのだけど、穴からも水が漏れている、みたいな感じです。

通常の積分器は
$$ y(n+1) = y(n) + delta $$
という計算をしますが、Leaky Integratorは
$$ y(n+1) = (1 – k) y(n) + delta \\
(k \ll 1)$$
という計算をします。常に\(ky(n)\)だけ漏れていくので、\(delta=0\)なら\(y(n)\)はだんだんゼロに近づいていきますね。
\(k\)が大きすぎると、鋸波の波形がたわんできますので、0.1%以下くらいにしておくのが良さそうです。

考慮すべき点は以上ですが、そのほか

・\(\phi\)の初期値は0.5(\(Sinc_M\)の振幅が最小のところ)にすると、積分値yの初期値も0としてよい。
・\(\phi\)の値域は0.0~1.0ではなく、0.0~2.0とする。(\(\sin\)のパラメータが0~\(2\pi\)のため)

といったあたりが注意点になります。

下の図は、実装してみたオシレータでC1(32.7Hz)を鳴らしてみたときの周波数分布です。

高いほうの倍音成分まできちんと出ています。もしこれをウエーブテーブルでやると、1波長分で1468サンプルが必要になります。
出音はこんな感じです。(Prologueの出力を録音してMP3で圧縮してあります)

比較のために、マルチエンジンに入っているSawで鳴らしたものはこちらです。これはおそらくサイズの小さいウエーブテーブル(logue SDKで使えるものと同じもの)を使っていると思います。
音量が若干違いますが、それは別にしても、倍音成分が少ないため、かなりダークな音色です。

ちなみに、以前作成したウエーブテーブルオシレータで鳴らすとこうなります。
テーブルサイズは256サンプル/波長ですが、倍音成分はそれなりにあります。しかし、冒頭の低音部で4KHzあたりに僅かにノイズが出てしまっています。これはテーブルサイズが小さすぎて、倍音を128次(=256/2)で打ち切っているため、残留成分(128×32.7Hz=4185.6Hz)が出てしまっているのだと思います。

実装そのものは短いので、上のリポジトリのコードを見ていただければと思います。

三角関数の計算は普通のsinf()関数を使っていますが、logue SDKのターゲットCPUのSTM32F401は単精度FPUを搭載していますので、1サンプル2回程度の計算なら処理能力に特に問題は無いようです。

FPUが無いマイコンでBLITを使用する場合は、Sinの高速化について工夫が必要になると思います。この計算は精度が重要ですので、logue SDKのosc_sinf()(長さ256のテーブル)ではもちろん実用になりませんが、fastersinfullf()はまあまあ悪くありませんでした。

なお、上記のリポジトリの実装では、おまけとして、

・Leaky Integratorのパラメータ(上の式での\(k\))
・信号の周波数の上限(\(f_s / 2\)よりも小さくできる)
・倍音の次数(基本周波数のみ~100次の倍音まで設定可能)

の3つのパラメータを用意しました。倍音の次数をリアルタイムで変更するのは、フーリエ級数を使ったら重すぎて実現できなかったと思います。

BLITについては以上です。

背後の理論を知らないと、ソースコードを見ても、訳が分からない計算をしているようにしか見えないと思います。
オブジェクトコードのサイズが5キロバイト程度と小さいのに、大きなウエーブテーブルを使った場合と同等以上の音質が得られます。
logue SDKのような、メモリ空間の制約が厳しいけれど計算能力はそこそこ高い環境にはピッタリだと思います。

コメント