LoginSignup
151
115

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-13

はじめに

VSTとはDAWなどの音楽ソフト上で動くプラグインの規格の一つです。
シンセサイザーやオーディオフィルターなどを作ることができて、DAW上から簡単に利用することができます。
serum_on_vsthost.png
画像はVSTHost上で動いている有名なシンセサイザープラグイン、Serum
余談ですがSerumは有料ソフトで、2万円位します。結構いい市場なのでは?:ok_hand:

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

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

vst-rs

vst-rsを使うとRustでVSTプラグインを作ることができます。
ここで説明すると長くなってしまうので、簡単な使い方をCreating a simple synthesizer VST plugin in Rustで各自参照してください。

本記事ではCreating a simple synthesizer VST plugin in Rustで作ったVSTプラグインに簡単なGUIをつけていきます。
resamplr/rust-noise-vst-tutorial

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

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

cargo.toml
[dependencies]
# 最近新しいバージョンが出たのでアップデートする
vst = "0.2"
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>
    }
}

できたコードはこちら
vvv.png
VSTHostで確認するとデフォルトのGUIが出てるのがわかります。
このGUIはVSTHostがパラメーター情報を見て自動で作っています。

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

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

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

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

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でいうと、CreateWindowExWhWndParentに入れてやる。これを他のプラットフォームでどうやるかわからないのが本記事がWindowsのみである主な理由。
  2. イベントループはidleメソッドで細切れにやる。つまり、一回走らせるとウインドウを閉じるまで帰ってこないようなGUIライブラリはだめ

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

多くのGUIライブラリがウインドウ生成をwinitに依存していますが。上記の制約があるためwinitのバージョン0.20以上に依存している必要があります

参考: RustAudio/vst-rs VST2 Guis

というわけでこの記事ではConrodicedでそれぞれGUIを作っていこうと思います:muscle:

GUIを作る ~Conrod編~

Conrodは比較的昔からあるRust製GUIライブラリです。

  • GUIの部分とウインドウ生成、イベントループ、描画が分離されているのでVSTに対応しやすい:thumbsup:
  • Immediate APIなのでVSTのパラメータと同期する部分が直感的になる:thumbsup:

という特徴があります。

今回はConrodgliumバックエンドを使っていこうと思いますが、依存先にあるgliumが古いバージョンのためwinitの依存が0.19になってしまいます。
なので、gliumの依存をアップデートした私のフォークしたバージョンのConrodを使っていきたいと思います:muscle:
現在この件のPRを出していますが、いまのところ音沙汰はありません:cry:

GUI部分のコードを抜粋

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

GUIを作る ~iced編~

icedはElmライクなアーキテクチャを採用したRust製GUIライブラリです。
機能はまだ少なめですがCryptowatchが支援していて勢いを感じたので目をつけました:eyes:

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

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

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

iced ecosystem

GUI部分のコードを抜粋

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

終わりに

本記事ではWindows限定ですが、vst-rsで有名なGUIライブラリのConrodicedを使ってGUIを表示する方法を解説しました。
やり方はわかったのでそのうちソフトウェアシンセサイザーとかを作りたいなと思ってます。

関連リンク

151
115
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
151
115