RustでGUI付きのVSTプラグイン作る(Conrod, iced)

# はじめに

[VST](https://ja.wikipedia.org/wiki/Virtual_Studio_Technology)とは[DAW](https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%B8%E3%82%BF%E3%83%AB%E3%83%BB%E3%82%AA%E3%83%BC%E3%83%87%E3%82%A3%E3%82%AA%E3%83%BB%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)などの音楽ソフト上で動くプラグインの規格の一つです。
シンセサイザーやオーディオフィルターなどを作ることができて、DAW上から簡単に利用することができます。
<img width="400" alt="serum_on_vsthost.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20535/28727c75-a53e-3d9f-de84-e7f293d34c91.png">
画像は[VSTHost](https://www.hermannseib.com/english/vsthost.htm)上で動いている有名なシンセサイザープラグイン、[Serum](https://xferrecords.com/products/serum)
余談ですがSerumは有料ソフトで、2万円位します。結構いい市場なのでは？:ok_hand:

この記事ではRustでGUI付きのVSTプラグインを作っていきます。

各OSのAPIの仕様が関わってくるため、本記事の対象プラットフォームは**Windowsのみ**です:bow:
他のプラットフォームで成功した方はコメント下さい:bow:

# [vst-rs](https://github.com/RustAudio/vst-rs)

[vst-rs](https://github.com/RustAudio/vst-rs)を使うとRustでVSTプラグインを作ることができます。
ここで説明すると長くなってしまうので、簡単な使い方を[Creating a simple synthesizer VST plugin in Rust](https://vaporsoft.net/creating-an-audio-plugin-with-rust-vst/)で各自参照してください。

本記事では[Creating a simple synthesizer VST plugin in Rust](https://vaporsoft.net/creating-an-audio-plugin-with-rust-vst/)で作ったVSTプラグインに簡単なGUIをつけていきます。
[resamplr/rust-noise-vst-tutorial](https://github.com/resamplr/rust-noise-vst-tutorial)


# GUIで操作するパラメータを追加する

今回は[resamplr/rust-noise-vst-tutorial](https://github.com/resamplr/rust-noise-vst-tutorial)にボリュームを制御するパラメーターを追加してGUIで操作できるようにしたいと思います。
プラグインに付属するパラメータは、ホスト側で保存したり操作できるようにするために**VSTで決められた通りに定義してやる必要があります**。
[vst-rs](https://github.com/RustAudio/vst-rs)のexamplesにある[gain_effect.rs](https://github.com/RustAudio/vst-rs/blob/master/examples/gain_effect.rs)を参考にしていきます。

```toml:cargo.toml
[dependencies]
# 最近新しいバージョンが出たのでアップデートする
vst = "0.2"
```

```rust:lib.rs
#[derive(Default)]
struct Whisper {
    // パラメーターをここに保存する
    // 後でArcで渡すのでArcで保存する
    params: Arc<WhisperParameters>,
    // Added a counter in our plugin struct. 
    notes: u8
}

// パラメーターの中身。ボリュームを制御するf32だけ
struct WhisperParameters {
    // アトミックなf32、内部ではstd::sync::atomic::AtomicU32を使っている。
    // VSTの仕様のためパラメーターはすべてf32で値の範囲は0~1でなければならない
    volume: vst::util::AtomicFloat,
}

impl Default for WhisperParameters {
    fn default() -> Self {
        Self {
            volume: vst::util::AtomicFloat::new(1.0),
        }
    }
}

impl PluginParameters for WhisperParameters {
    fn get_parameter_label(&self, index: i32) -> String {
        match index {
            // 適当にラベルを返す
            0 => "x".to_string(),
            _ => "".to_string(),
        }
    }
    // This is what will display underneath our control.  We can
    // format it into a string that makes the most sense.
    fn get_parameter_text(&self, index: i32) -> String {
        match index {
            0 => format!("{:.3}", self.volume.get()),
            _ => format!(""),
        }
    }

    fn get_parameter_name(&self, index: i32) -> String {
        match index {
            0 => "volume".to_string(),
            _ => "".to_string(),
        }
    }
    // get_parameter has to return the value used in set_parameter
    fn get_parameter(&self, index: i32) -> f32 {
        match index {
            0 => self.volume.get(),
            _ => 0.0,
        }
    }
    fn set_parameter(&self, index: i32, value: f32) {
        match index {
            0 => self.volume.set(value),
            _ => (),
        }
    }
}

impl Plugin for Whisper {
    fn get_info(&self) -> Info {
        Info {
            // ..省略
            // このプラグインで使うパラメータ数を設定する
            parameters: 1,
        }
    }

    fn process(&mut self, buffer: &mut AudioBuffer<f32>) {

        // `buffer.split()` gives us a tuple containing the 
        // input and output buffers.  We only care about the
        // output, so we can ignore the input by using `_`.
        let (_, mut output_buffer) = buffer.split();

        // We only want to process *anything* if a note is being held.
        // Else, we can fill the output buffer with silence.
        if self.notes == 0 {
            for output_channel in output_buffer.into_iter() {
                // Let's iterate over every sample in our channel.
                for output_sample in output_channel {
                    *output_sample = 0.0;
                }
            }
            return;
        }

        let volume = self.params.volume.get();

        // Now, we want to loop over our output channels.  This
        // includes our left and right channels (or more, if you
        // are working with surround sound).
        for output_channel in output_buffer.into_iter() {
            // Let's iterate over every sample in our channel.
            for output_sample in output_channel {
                // For every sample, we want to generate a random value
                // from -1.0 to 1.0.
                // ここでボリュームを掛けて音の大きさを調整する
                *output_sample = (random::<f32>() - 0.5f32) * 2f32 * volume;
            }
        }
    }

    fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
        Arc::clone(&self.params) as Arc<dyn PluginParameters>
    }
}
```

できたコードは[こちら](https://github.com/hatoo/whisper_volume)。
![vvv.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20535/7ebdd4ba-09b9-cb79-4b9a-748843e074d3.png)
VSTHostで確認するとデフォルトのGUIが出てるのがわかります。
このGUIはVSTHostがパラメーター情報を見て自動で作っています。

# vst-rsのGUIインターフェース

さっそくvst-rsでGUIを作るにはどうすればいいのか確認していきましょう。

まず、`Plugin`トレイトの`get_editor`メソッドを実装して`Editor`トレイトを実装したオブジェクトを返します。
このオブジェクトでウインドウを開く/閉じるなどのGUIの処理を行います。

https://rustaudio.github.io/vst-rs/vst/plugin/trait.Plugin.html

```rust
pub trait Plugin {
    // ..省略

    /// Return handle to plugin editor if supported.
    /// The method need only return the object on the first call.
    /// Subsequent calls can just return `None`.
    ///
    /// The editor object will typically contain an `Arc` reference to the parameter
    /// object through which it can communicate with the audio processing.
    fn get_editor(&mut self) -> Option<Box<dyn Editor>> {
        None
    }
}
```

`Editor`トレイトの定義はこんな感じです
https://rustaudio.github.io/vst-rs/vst/editor/trait.Editor.html

```rust:lib.rs
pub trait Editor {
    /// Get the size of the editor window.
    /// ウインドウの大きさを返す。面倒ならとりあえず固定値を返しておけば良い。
    fn size(&self) -> (i32, i32);

    /// Get the coordinates of the editor window.
    /// どうせ無視されるらしいのでとりあえず(0, 0)を返しておけば良い
    fn position(&self) -> (i32, i32);

    /// Editor idle call. Called by host.
    /// このメソッドが定期的に呼ばれるのでここでGUIのイベントを処理する
    fn idle(&mut self) {}

    /// Called when the editor window is closed.
    /// 呼ばれたらウインドウを閉じる
    fn close(&mut self) {}

    /// Called when the editor window is opened.
    ///
    /// `parent` is a window pointer that the new window should attach itself to.
    /// **It is dependent upon the platform you are targeting.**
    ///
    /// A few examples:
    ///
    ///  - On Windows, it should be interpreted as a `HWND`
    ///  - On Mac OS X (64 bit), it should be interpreted as a `NSView*`
    ///  - On X11 platforms, it should be interpreted as a `u32` (the ID number of the parent window)
    ///
    /// Return `true` if the window opened successfully, `false` otherwise.
    /// 上記の通り、Windowsでは`HWND`がもらえるのでそれを親ウインドウにしてウインドウを開く
    fn open(&mut self, parent: *mut c_void) -> bool;

    /// Return whether the window is currently open.
    /// ウインドウが開いているかどうか返す
    fn is_open(&mut self) -> bool;

    //// ...省略
}
```

## 注意点

1. `open`メソッドが呼ばれたときに、引数のウインドウハンドルを親にした**小ウインドウ**として自分のウインドウを作らなくてはいけない。WinAPIでいうと、[`CreateWindowExW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw)の`hWndParent`に入れてやる。これを他のプラットフォームでどうやるかわからないのが本記事がWindowsのみである主な理由。
2. イベントループは`idle`メソッドで細切れにやる。つまり、**一回走らせるとウインドウを閉じるまで帰ってこないようなGUIライブラリはだめ**。

2.に関してですが、読者の中にはイベントループだけ別のスレッドでやれば良いじゃないかと思った方もいるかも知れませんが、WinAPIの仕様上、小ウインドウのイベントループは親ウインドウと**同じスレッドでやらないといけない**のでだめです。(ソースはないですが試したらだめだったので多分そう)。


多くのGUIライブラリがウインドウ生成を[winit](https://github.com/rust-windowing/winit)に依存していますが。上記の制約があるためwinitの**バージョン`0.20`以上に依存している必要があります**。

参考: [RustAudio/vst-rs VST2 Guis](https://github.com/RustAudio/vst-rs/wiki/VST2-Guis)

というわけでこの記事では[Conrod](https://github.com/PistonDevelopers/conrod)と[iced](https://github.com/hecrj/iced)でそれぞれGUIを作っていこうと思います:muscle:

# GUIを作る ~Conrod編~

[Conrod](https://github.com/PistonDevelopers/conrod)は比較的昔からあるRust製GUIライブラリです。

- GUIの部分とウインドウ生成、イベントループ、描画が分離されているのでVSTに対応しやすい:thumbsup:
- [Immediate API](https://docs.rs/conrod/0.61.1/conrod/guide/chapter_1/index.html#feature-overview)なのでVSTのパラメータと同期する部分が直感的になる:thumbsup:

という特徴があります。

今回は[Conrod](https://github.com/PistonDevelopers/conrod)の[gliumバックエンド](https://github.com/PistonDevelopers/conrod/tree/master/backends/conrod_glium)を使っていこうと思いますが、依存先にある[glium](https://github.com/glium/glium)が古いバージョンのため[winit](https://github.com/rust-windowing/winit)の依存が`0.19`になってしまいます。
なので、[glium](https://github.com/glium/glium)の依存をアップデートした私の[フォークしたバージョンのConrod](https://github.com/hatoo/conrod/tree/update-glium)を使っていきたいと思います:muscle:
現在この件の[PRを出しています](https://github.com/PistonDevelopers/conrod/pull/1317)が、いまのところ音沙汰はありません:cry:

GUI部分のコードを抜粋

```rust:lib.rs
const WIDTH: u32 = 400;
const HEIGHT: u32 = 200;

// 今回はラベルとスライダーを使う
widget_ids!(struct Ids { text, volume_slider });

// こいつにEditorトレイトを実装する
struct GUIWrapper {
    // VSTのパラメータ
    params: Arc<WhisperParameters>,
    // GUI用。ウインドウが開いていない場合もあるためOptionを使っている。
    inner: Option<GUI>,
}

// GUIのイベントループを回すためのデータを入れておく
struct GUI {
    event_loop: EventLoop<()>,
    display: support::GliumDisplayWinitWrapper,
    ids: Ids,
    ui: Ui,
    renderer: Renderer,
    image_map: conrod_core::image::Map<glium::texture::Texture2d>,
}

impl GUI {
    fn new(parent: HWND) -> Self {
        let event_loop = EventLoop::new();

        // VSTのためのwinitのウインドウの設定。おまじないだと思って良い。
        let window = WindowBuilder::new()
            .with_title("A fantastic window!")
            .with_decorations(false)
            .with_resizable(false)
            // ここで親ウインドウを設定する
            .with_parent_window(parent)
            .with_inner_size((WIDTH, HEIGHT).into());

        // ここから先は一般的なconrod_gliumの初期化
        let context = glium::glutin::ContextBuilder::new();

        let display = glium::Display::new(window, context, &event_loop).unwrap();
        let display = support::GliumDisplayWinitWrapper(display);

        let mut ui = conrod_core::UiBuilder::new([WIDTH as f64, HEIGHT as f64]).build();
        let ids = Ids::new(ui.widget_id_generator());

        // バイナリにフォントデータを埋め込んでおく
        let font: &[u8] = include_bytes!("../assets/fonts/NotoSans/NotoSans-Regular.ttf");
        ui.fonts.insert(Font::from_bytes(font).unwrap());

        let renderer = conrod_glium::Renderer::new(&display.0).unwrap();

        // The image map describing each of our widget->image mappings (in our case, none).
        let image_map = conrod_core::image::Map::<glium::texture::Texture2d>::new();

        // イベントループはここで行わず、保存しておいてidleでやる。
        Self {
            event_loop,
            display,
            ids,
            ui,
            renderer,
            image_map,
        }
    }
}

impl GUIWrapper {
    fn new(params: Arc<WhisperParameters>) -> Self {
        Self {
            params,
            inner: None,
        }
    }
}

impl Editor for GUIWrapper {
    fn size(&self) -> (i32, i32) {
        if let Some(inner) = self.inner.as_ref() {
            let s = inner.display.0.gl_window().window().inner_size();
            (s.width as i32, s.height as i32)
        } else {
            (0, 0)
        }
    }

    fn position(&self) -> (i32, i32) {
        (0, 0)
    }

    fn idle(&mut self) {
        // ここで適度にイベントを消費する
        use winit::event;

        let mut end = false;
        if let Some(inner) = self.inner.as_mut() {
            let display = &mut inner.display;
            let ui = &mut inner.ui;
            let ids = &mut inner.ids;
            let renderer = &mut inner.renderer;
            let image_map = &mut inner.image_map;
            let params = &self.params;
            inner
                .event_loop
                // 注意: run_returnメソッドは現在MacでバグっているのでMac対応を検討している方は直そう！
                // https://github.com/rust-windowing/winit/issues/1242
                .run_return(move |event, _, control_flow| match event {
                    event::Event::WindowEvent {
                        event: event::WindowEvent::CloseRequested,
                        window_id,
                    } if window_id == display.0.gl_window().window().id() => {
                        // Xボタンが押されたら閉じる
                        end = true;
                        *control_flow = ControlFlow::Exit
                    }
                    // 残りのイベントが無くなったら戻る
                    event::Event::EventsCleared => *control_flow = ControlFlow::Exit,
                    _ => {
                        let input = match support::convert_event(event, display) {
                            None => return,
                            Some(input) => input,
                        };

                        // Handle the input with the `Ui`.
                        ui.handle_event(input);

                        // Set the widgets.
                        let ui = &mut ui.set_widgets();

                        // テキストを表示
                        widget::Text::new("Volume")
                            .middle_of(ui.window)
                            .color(conrod_core::color::WHITE)
                            .font_size(32)
                            .set(ids.text, ui);

                        // volumeのスライダーを作る
                        // Immediate APIなので更新の処理もここで書けていい感じになる
                        // 本当はデシベルなどでlogスケールに変化したほうが人間の感覚的に良いが簡単にするためにリニアなまま
                        if let Some(new_volume) = widget::Slider::new(params.volume.get(), 0.0, 1.0)
                            .set(ids.volume_slider, ui)
                        {
                            params.volume.set(new_volume);
                        }

                        // Draw the `Ui` if it has changed.
                        if let Some(primitives) = ui.draw_if_changed() {
                            renderer.fill(&display.0, primitives, image_map);
                            let mut target = display.0.draw();
                            target.clear_color(0.0, 0.0, 0.0, 1.0);
                            renderer.draw(&display.0, &mut target, &image_map).unwrap();
                            target.finish().unwrap();
                        }
                    }
                });
        }
        if end {
            self.inner = None;
        }
    }

    // 忘れてはいけない
    fn close(&mut self) {
        // GUIオブジェクトをdropするとウインドウが閉じられる
        self.inner = None;
    }

    fn open(&mut self, parent: *mut c_void) -> bool {
        self.inner = Some(GUI::new(parent as HWND));
        true
    }

    fn is_open(&mut self) -> bool {
        self.inner.is_some()
    }
}
```

完成品はこちら
https://github.com/hatoo/vst-rs-example-conrod
![demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20535/0c62fa33-b430-e238-1855-915983990634.png)

# GUIを作る ~iced編~

[iced](https://github.com/hecrj/iced)はElmライクなアーキテクチャを採用したRust製GUIライブラリです。
機能はまだ少なめですが[Cryptowatchが支援](https://www.reddit.com/r/rust/comments/e1rc4w/cryptowatch_is_sponsoring_development_of_rust_gui/)していて勢いを感じたので目をつけました:eyes:

肝心のVSTへの対応ですが
1. 引数のウインドウハンドルを親にした小ウインドウとして自分のウインドウを作る → PRを投げたら通った:tada:
2. イベントループは`idle`メソッドで細切れにやる。 → [`Generator`](https://doc.rust-lang.org/std/ops/trait.Generator.html)を使うと簡単に実装できるがNightlyの機能ためPRはまだ送ってない:cry:
3. VSTとGUIオブジェクトが状態を共有できない → [現在議論中](https://github.com/hecrj/iced/issues/118)
4. Elmライクなアーキテクチャなのでパラメータの更新ロジックが自然:thumbsup:

ということで今回も私の[フォークしたバージョンのiced](https://github.com/hatoo/iced/tree/run_generator)を使うことになります。
本家とのdiffはこちら https://github.com/hecrj/iced/compare/master...hatoo:run_generator
上記の通り`Generator`を使うのでNightlyのRustを使用してください。

iced自体はクロスプラットフォームのライブラリですが少しプラットフォーム依存な機能を使うので今回は`iced_winit`を直接使う必要があります。

![iced ecosystem](https://raw.githubusercontent.com/hecrj/iced/master/docs/graphs/ecosystem.png)

GUI部分のコードを抜粋

```rust:lib.rs
const WIDTH: u32 = 400;
const HEIGHT: u32 = 200;

struct GUIWrapper {
    params: Arc<WhisperParameters>,
    inner: Option<GUI>,
}

struct GUI {
    // イベントを消化するGenerator、ウインドウが閉じると終わる
    gen: Box<dyn std::marker::Unpin + std::ops::Generator<Yield = (), Return = ()>>,
}

impl GUI {
    fn new(parent: HWND, params: Arc<WhisperParameters>) -> Self {
        let mut setting = iced_winit::Settings::default();
        // Settings for VST
        setting.window.decorations = false;
        setting.window.platform_specific.parent = Some(parent);
        setting.window.size = (WIDTH, HEIGHT);

        // Initialize `Application` to share `params`
        let app = WhisperGUI::new(params);
        // Save Box of `Generator` to do event loop on idle method
        // これがフォークして生やしたメソッド
        let gen = app.run_generator(Command::none(), setting);

        Self { gen }
    }
}

impl Editor for GUIWrapper {
    fn size(&self) -> (i32, i32) {
        // 今のところ、ここでウインドウサイズを取得する方法はないので固定値を返す。
        // 動いているのでOK
        (WIDTH as i32, HEIGHT as i32)
    }

    fn position(&self) -> (i32, i32) {
        (0, 0)
    }

    // Generatorを進めてイベントを処理する
    fn idle(&mut self) {
        // Poll events here
        if let Some(inner) = self.inner.as_mut() {
            if let std::ops::GeneratorState::Complete(_) =
                Generator::resume(std::pin::Pin::new(&mut inner.gen))
            {
                self.inner = None;
            }
        }
    }

    fn close(&mut self) {
        self.inner = None;
    }

    fn open(&mut self, parent: *mut c_void) -> bool {
        self.inner = Some(GUI::new(parent as HWND, self.params.clone()));
        true
    }

    fn is_open(&mut self) -> bool {
        self.inner.is_some()
    }
}

use iced::{Column, Element, Text};

// icedの`Application`
struct WhisperGUI {
    params: Arc<WhisperParameters>,
    // スライダー用のデータ
    volume_slider: iced::widget::slider::State,
}

impl WhisperGUI {
    fn new(params: Arc<WhisperParameters>) -> Self {
        Self {
            params,
            volume_slider: Default::default(),
        }
    }
}

#[derive(Debug, Clone, Copy)]
enum Message {
    VolumeChanged(f32),
}

// 直接iced_winitを使う
impl iced_winit::Application for WhisperGUI {
    type Renderer = iced_wgpu::Renderer;
    type Message = Message;

    fn new() -> (Self, Command<Self::Message>) {
        // 使わないのでコンパイルだけ通るようにする
        unimplemented!()
    }

    fn title(&self) -> String {
        String::from("Whisper")
    }

    fn update(&mut self, message: Message) -> Command<Self::Message> {
        match message {
            Message::VolumeChanged(v) => {
                self.params.volume.set(v);
            }
        }
        Command::none()
    }

    fn view(&mut self) -> Element<Message> {
        Column::new()
            .padding(20)
            .push(Text::new("Volume".to_string()).size(32))
            .push(iced::widget::Slider::new(
                &mut self.volume_slider,
                0.0..=1.0,
                self.params.volume.get(),
                Message::VolumeChanged,
            ))
            .into()
    }
}
```

完成品はこちら
https://github.com/hatoo/vst-rs-example-iced
![demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20535/02f9bbd8-6421-7c63-7efe-e1f0a96324ef.png)

# 終わりに

本記事ではWindows限定ですが、[vst-rs](https://github.com/RustAudio/vst-rs)で有名なGUIライブラリの[Conrod](https://github.com/PistonDevelopers/conrod)と[iced](https://github.com/hecrj/iced)を使ってGUIを表示する方法を解説しました。
やり方はわかったのでそのうちソフトウェアシンセサイザーとかを作りたいなと思ってます。

## 関連リンク

- Rust Audio https://rust-audio.discourse.group/ discord https://discord.gg/b3hjnGw
    - Rustでオーディオプログラミングをやるグループ。vst-rsもここが作っている。
- [vanderlokken/rust-vst-gui](https://github.com/vanderlokken/rust-vst-gui)
    - こちらもGUI付きのVSTを作れるクレート。GUI部分は埋め込みのブラウザに任せて、利用者はHTMLとJavascriptでGUIを制御する方式。当時はwinitのバージョンが古く、winitに依存しているGUIライブラリは軒並み使えなかった。
- [rust-windowing/winit Support for creating child windows #159](https://github.com/rust-windowing/winit/issues/159)
    - Winitで子ウインドウを作るissue(openしてるissueのなかではかなり古い)
- [crsaracco/vst2-gui-research](https://github.com/crsaracco/vst2-gui-research)
    - RustAudioの方がまとめているvst-rsでGUIを使うためのドキュメント