D言語でVST/AUプラグイン開発3 (GUI編)

## 前回までの流れ

1. [D言語でVST/AUプラグイン開発1 (環境構築・検証編)](https://qiita.com/ShigekiKarita/items/e67d26b4cfaffd648033)
2. [D言語でVST/AUプラグイン開発2 (エフェクト自作)](https://qiita.com/ShigekiKarita/items/9b3aa8f716848278ef62)

## GUI編

前回(1からVSTエフェクト制作)の続きで、GUIを作ろうと思います。Dplugでは様々なGUI実装方法が可能です。

https://github.com/AuburnSounds/Dplug/wiki/Customizing-UIs-for-your-brand

1. `dplug:gui` パッケージを使わないで、直接[IGrapihcs](https://github.com/AuburnSounds/Dplug/tree/v7.1.2/examples)をOpenGLとかで実装する(一番自由度は高いが前例もなく面倒)
2. `dplug:gui` パッケージを使って制限はあるが[UIElement](https://github.com/AuburnSounds/Dplug/blob/v7.1.2/gui/dplug/gui/element.d#L53)を使って実装する

私はとりあえず全てのDAWで一貫したUIが楽に組めれば良いので、後者を取りたいと思います。その中でもいくつか自由度と簡単さの段階があります。

1. `dplug:pbr-widges`パッケージの[PBRCompositor](https://github.com/AuburnSounds/Dplug/blob/v7.1.2/gui/dplug/gui/compositor.d#L41)を使って点灯する部品やカーブを描く方法。一番制約が強い
2. PBRCompositorを自分で作る方法。後述の9 channels, mip-mappingといった制約はなくなるが、自前で実装する描画メソッドが増える
3. PBRCompositorをPhisycal=0にセットしてバイパスして、`dplug:flat-widgets`パッケージでRGBA画像を張り付けて作る方法。ノブなどにアニメーションを入れるときは古き良き2Dゲームのような[パラパラ漫画風の静的レンダリング](https://github.com/AuburnSounds/Dplug/blob/v7.1.2/examples/clipit/gfx/knob.png)が必要でメモリ消費も多い

ちなみにPBRとは**Physically Based-style Rendering**の略称らしいです。3番目の方法と違って動的に物理モデリングでノブやスライダーを描画してくれるのでメモリ的にも軽く、CPU消費量とクオリティのトレードオフも設定できます。先に述べたように、私は一番手っ取り早そうな`PBRCompositor`アプローチで行こうと思います。素晴らしいexampleもあります。

### pbr-widgets (背景レイヤー画像+PBRCompositor) の例

ノブや、ボタン、スライダー、インジケータといった動く部品はすべてPBRでレンダリングされていますが、背景は6レイヤーの画像で構成されています。このバランス感覚はデザイナーと分業するためなのか、さすが商業プロダクトに使われている感じがします。

https://github.com/AuburnSounds/Dplug/blob/v7.1.2/examples/distort/gui.d

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37859/37575957-ea23-28f4-e3c7-ac6ba7f2cc8d.png)

### flat-widgets の例

https://github.com/AuburnSounds/Dplug/blob/v7.1.2/examples/clipit/gui.d

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37859/e3b6a8a9-368a-7a79-b46b-990e73980e79.png)


## PBR UIの作り方

https://github.com/AuburnSounds/Dplug/wiki/Making-a-PBR-UI

私はあまりGUIプログラミングの経験がないのですが、DplugのPBRの思想はかなり特殊に感じます。とはいえ下記のプロダクト例などを見ると何がやりたいかは理解できると思います。

https://www.auburnsounds.com/blog/2016-09-16_PBR-for-Audio-Software-Interfaces.html

しかしながら最初のステップとして、フォトショなどで背景を作るのは怠いので、本記事ではシンプルな黒地pngを背景にします。本当の例は上記URLか、[distortのgfxフォルダ](https://github.com/AuburnSounds/Dplug/tree/v7.1.2/examples/distort/gfx)をみてください。

```d
module gui;

import dplug.pbrwidgets;

/** module dplug.pbrwidgets.pbrbackgroundgui に定義
class PBRBackgroundGUI(string baseColorPath, // 一番下にでてくるRGB背景画像(png/jpg)
                       string emissivePath,  // 光の強さを指定するグレー画像
                       string materialPath,  // 金属っぽさを指定するグレー画像
                       string physicalPath,  // 物理レンダリングの強さを指定するグレー画像
                       string depthPath,     // 3Dっぽい質感を出すための深さを指定するグレー画像
                       string skyboxPath,    // 反射光を作るときの向かい側の画像(空とか)
                       string absoluteGfxDirectory) // デバッグ用の画像フォルダ指定
*/
class SimpleDelayGUI : PBRBackgroundGUI!("black.png", "black.png", "black.png",
                                         "black.png", "black.png", "black.png",
                                         "")
```

ちなみにPBRBackgroundGUIを経由せず、直でGraphicsGUIを継承するときは

```d
    override void reflow(box2i availableSpace)
    {
        _position = availableSpace;
    }
```

のようにreflowをオーバーライドしないと座標が狂います。画像を使わずに背景を書き換える方法はまだ分からないので教えてください...。繰り返しますが、とにかく一番簡単な方法でGUIを作ることにしています。とりあえずdistortの例にあるような現在のパラメータ値を表示とかも面倒なのでやめました。

```source/gui.d
/**
Copyright: Shigeki Karita
License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
*/
module gui;

import dplug.pbrwidgets : PBRBackgroundGUI;

// pngはresourceフォルダに配置
class SimpleDelayGUI : PBRBackgroundGUI!("black.png", "black.png", "black.png",
                                         "black.png", "black.png", "black.png",
                                         "")
{
public nothrow @nogc:
    import gfm.math : box2i;
    import dplug.core : mallocNew, destroyFree;
    import dplug.gui : PBRCompositor;
    import dplug.pbrwidgets : Font, RGBA, UIKnob, UILabel;
    import dplug.client : Parameter, FloatParameter;
    import main : Param;
    
    Font _font;

    this(Parameter[] parameters...)
    {
        _font = mallocNew!Font(cast(ubyte[])( import("VeraBd.ttf") ));
        super(620, 200); // size

        PBRCompositor comp = cast(PBRCompositor)compositor;
        RGBA litTrailDiffuse = RGBA(151, 119, 255, 100);
        RGBA unlitTrailDiffuse = RGBA(81, 54, 108, 0);

        int marginW=10;
        int marginH=10;
        const n = cast(int) parameters.length;
        int w, h;
        this.getGUISize(&w, &h);
        const kW = (w - 2 * marginW) / n;
        const kH = h - 2 * marginH;
        foreach (int i, param; parameters) {
            auto p = cast(FloatParameter) param;
            const x = marginW + kW * i;
            // 物理レンダリングされるノブ
            UIKnob knob;
            addChild(knob = mallocNew!UIKnob(context(), p));
            knob.position = box2i.rectangle(x, marginH, kW, kH);
            knob.knobRadius = 0.65f;
            knob.knobDiffuse = RGBA(50, 50, 100, 0); // color of knob
            // NOTE: material とは [R(smooth), G(metal), B(shiny), A(phisycal)]
            knob.knobMaterial = RGBA(255, 255, 255, 255);
            knob.numLEDs = 0; // ノブ上のLEDによる装飾(0: 無し)
            knob.litTrailDiffuse = litTrailDiffuse;
            knob.unlitTrailDiffuse = unlitTrailDiffuse;
            knob.LEDDiffuseLit = RGBA(0, 0, 40, 100);
            knob.LEDDiffuseUnlit = RGBA(0, 0, 40, 0);
            knob.LEDRadiusMin = 0.06f;
            knob.LEDRadiusMax = 0.06f;
            // パラメータ名のテキスト表示
            UILabel label;
            addChild(label = mallocNew!UILabel(context(), _font, p.name));
            label.position = box2i.rectangle(x, kH, kW, marginH);
            label.textColor(RGBA(200, 200, 200, 255));
        }
    }

    ~this()
    {
        _font.destroyFree();
    }
}
```

最後にこの前作ったSimpleDelayクラスに以下のメソッドを追加して`plugin.json`の`hasGUI: true`に変更すればOKです

```source/main.d
final class SimpleDelay : Client
{
    ...
    override IGraphics createGraphics()
    {
        import gui : SimpleDelayGUI;
        return mallocNew!SimpleDelayGUI(
            this.param(Param.delayDryWetRatio),
            this.param(Param.delayFeedbackRatio),
            this.param(Param.delayTimeSecondL),
            this.param(Param.delayTimeSecondR)
        );
    }
}
```

## 動作確認

今回はTracktionでは起動が若干遅いので、もっと起動が早く手っ取り早く音や見た目が確認できたらな～と思っていたところ、JUCEに付属のAudioPluginHost

https://github.com/WeAreROLI/JUCE/tree/5.3.2/extras/AudioPluginHost

が高速で良いという話をKVRで見たので使いました。Tracktionで見覚えのあるプラグイン設定画面などがあり驚きます（元々JUCEはTracktionの開発ライブラリだったらしい）。ビルド方法などは環境によりけりでJUCEはドキュメントも多いのでググってください。とにかくVST開発ライブラリで有名なJUCE公式のホスト環境なので信頼できます。さきほどのコードを実行した画面です。

![simpledelay.PNG](https://qiita-image-store.s3.amazonaws.com/0/37859/72548a2f-e654-0772-f233-241dcc8a4302.png)

あとこのアプリケーションは前回のVST構成をそのまま起動時に再現してくれるのでデバッグ用途に最適です、ついでにVS2017でアプリごとデバッグできて私はこれで一つバグを直しました。あとDコンパイラもLDCでなくDMDを使うと爆速でコンパイルできるので、以下に示す一連のコマンド(2,3秒)を反復するだけでGUIの調整がターミナルだけで完結して捗ります

``` powershell
emacs -nw gui.d # GUIの編集
dub build --arch=x86_64 --compiler=dmd
~/Downloads/juce-5.3.2-windows/JUCE/extras/AudioPluginHost/Builds/VisualStudio2017/x64/Debug/App/AudioPluginHost.exe
```

昔C++でVSTを書いて挫折したときはこんなに高速にコーディングと検証のサイクルを回せなかったので、改めてD/Dplugは素晴らしいなと思いました。

## まとめ

最終的なコード https://github.com/ShigekiKarita/dplug-simple-delay/tree/v0.0.2

次回は元気があればVSTインストルメントの初歩的な部分を抑えたいと思います。

## 追記

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">趣味で作ってるD言語製のVSTプラグイン、ノブに合わせて数値を表示するなど動的なUIを実装した <a href="https://t.co/uJvdlb7Zeo">https://t.co/uJvdlb7Zeo</a> <a href="https://t.co/U5YWu38ieV">pic.twitter.com/U5YWu38ieV</a></p>&mdash; カリテク (@kari_tech) <a href="https://twitter.com/kari_tech/status/1047482450970963968?ref_src=twsrc%5Etfw">October 3, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>