前回 https://qiita.com/ShigekiKarita/items/e67d26b4cfaffd648033
今回はDplugにおけるVST Instrumentの解説をするといいましたが、一からVST Effectを作りたくなったので予定変更します。具体的にはdelayエフェクトを作ろうと思います。エコーとか呼ばれるやつですね
環境などは前回と同じです。
アルゴリズム
我流で書いていたら思いのほか難しかったので、このページを参考に実装してみました
https://www39.atwiki.jp/vst_prog/pages/64.html
雑に説明すると、delayされる過去入力をdelayTimeFrame分の容量を持ったキュー(FIFO, 待ち行列)に1フレームずつ出し入れするだけです。処理時にメモリ確保・解放をしない効率よいキューの実装としてリングバッファを使います。具体的にはbuffer[readIndex..$], buffer[0..writeIndex]
の中に過去のdelayしているframeを書き込み、一フレーム新たに出力・入力するごとに++readIndex, ++writeIndex
としてずらしていくだけです。
実装
基本的には前回解説したMSEncodeクラスを変更するだけで作れます。今日までの更新はGitHubにあります
reset 関数について
delayを作るとき、遅延量としてUI上では直感的な「秒」を使って、処理上は「フレーム数」を使いたいことがあります。その変換には1秒あたりのフレーム数であるsampleRateが必要です。しかしながらDplugではVST以外にもAudioUnitsやAAXといった色んなプラグイン規格を抽象化しているので、sampleRateの取得などがよくあるVSTの例とは違うようです。なんとなく前回の例ではreset関数が唯一sampleRateを取得できる方法のようでした。
override void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc
{
// DAW側でsampleRateが変更されたときに呼ばれる?
if (this._sampleRate != sampleRate)
{
// 処理用のsampleRateを変更
this._sampleRate = sampleRate;
// RingBufferのサイズも変更
this._buffer[0] = RingBuffer!float(this.maxDelayTimeFrame);
this._buffer[1] = RingBuffer!float(this.maxDelayTimeFrame);
// 新しいsampleRateにおけるUI上のdelayTimeをRingBufferに反映
this.resetInterval();
}
}
前回の記事に触れたreset関数のコメントにあったように、delayに使っているring bufferの再初期化などを行っています。
unittest を書く時の注意
module ringbuffer;
struct RingBuffer(T)
{
...
}
unittest
{
auto buf = RingBuffer!float(3);
assert(buf[] == [0.0, 0.0, 0.0]);
buf.setInterval(2);
assert(buf.readIndex == 0);
assert(buf.writeIndex == 2);
assert(buf.front == 0);
buf.pushBack(1.0);
buf.pushBack(2.0);
buf.pushBack(3.0);
assert(buf[] == [2.0, 3.0, 1.0]);
}
上記のようにD言語は気軽に unittest が書けるのがウリですが、DLLやVSTのmainがあるので普通に dub test
とコマンドしても動かないです。そこでD言語のversion conditionを使ってunittest時はコンパイルから外します
module main;
import dplug.core, dplug.client, dplug.vst;
version (unittest)
{}
else
{
// This create the DLL entry point
mixin(DLLEntryPoint!());
// This create the VST entry point
mixin(VSTEntryPoint!SimpleDelay);
}
複数チャネル処理の注意
基本的には参考ページを愚直にD言語で書いただけですが、L/Rで独立したdelay timeを設定できるようにしたところが違います。あとprocessAudioメソッドで楽をしてL/R channel -> 各frameの処理をすると左右のチャネルで遅延が生じてずれて聞こえます。
// 悪い例, 左右がズレて聞こえる
override void processAudio(const(float*)[] inputs, float*[]outputs, int frames, TimeInfo info) nothrow @nogc
{
foreach (ch; 0..2)
foreach (t; 0 .. frames)
...
おそらくDAWが非同期的にこのoutputsバッファを読み出して、再生してしまうのではと予想しています。なので、できるだけLチャネルを処理した直後にRチャネルも処理する必要があるようです。
// 良い例,左右のズレはなさそう
override void processAudio(const(float*)[] inputs, float*[]outputs, int frames, TimeInfo info) nothrow @nogc
{
foreach (t; 0 .. frames)
foreach (ch; 0..2)
...
Tracktionでの動画
https://t.co/OhtSsgl5s2 左右独立のdelay timeとfeedbackもつけた
— カリテク (@kari_tech) September 22, 2018
前回でできたsimple-mono-synthで2分音符を鳴らして、短めのdelayを左右違う長さでかけています。
まだ続くのかはモチベーションと暇次第ですが、GUIか、VST Instrumentについて学習したメモを書こうと思います。