1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RustAdvent Calendar 2024

Day 21

Rustで簡単にVST/CLAPプラグインを作る

Last updated at Posted at 2024-12-17

はじめに

オーディオプラグインとは、音楽制作ソフトウェアである DAW や、オーディオ編集ソフトに追加できる音源、エフェクト等の機能を持ったソフトウェアです。
プラグインにはいくつかの規格があり、DAWによって使用できる規格が異なります。
最もメジャーなのはVSTで、その他にはAudio Units、AAX、CLAPなどがあります。

VST開発は、C++で開発されることが多いですが、Rustでも書くことができます。
今回は、NIH-plug を使ってVST3とCLAP規格のプラグインを作る方法を解説します。

Virtual Studio Technology

VSTとは、Virtual Studio Technology という1996年にSteinberg社が発表した規格です。
エフェクトとインストゥルメントの二種類があり、VSTfx、VSTiと呼ばれています。
現在の最新版はVST3であり、UnicodeやマルチMIDI I/O、64bit処理に対応しています。

CLever Audio Plug-In

CLAPとは、CLever Audio Plug-In というBitwigとu-heが共同で開発し、2022年6月にリリースされた新しいオーディオプラグインの規格です。
CLAPに対応したDAWは、Bitwig StudioやFL Studioなどがあります。
u-heやFabFilterなどいくつかのメーカーでは、CLAP規格のプラグインが提供されています。

VST3のライセンス

VST3 SDKを使ったプラグインをオープンソースで配布する場合、GPLv3ライセンスを選択できます。

商用利用など、ソースコードを公開しない場合、Proprietary Steinberg VST3ライセンスを選択する必要があります。
この場合、Steinbergとライセンス契約を結ぶ必要があります。

CLAPのライセンス

CLAPはMITライセンスのもとで提供され、申請無しで商用利用が可能です。
今回使用するNIH-plugでも、VST3バインディングを使用しない場合はMITライセンスやその他のライセンスを適用することができます。

NIH-plugでプラグイン開発

今回はローパスフィルタの機能を持ったプラグインを作ります。

NIH-plug はRustでオーディオプラグインを開発するためのライブラリです。
ISCライセンスで公開されています。
対応しているプラグインの規格は、VST3とCLAPです。
GUIはegui、iced、VIZIAがサポートされていて、簡単にGUIを実装することができます。
ライセンスに関しては、VST3バインディングを行うとGPLv3が適用されるため、ISCや独自のライセンスを適用したい場合はCLAPバインディングのみにする必要があります。

本記事では、VST3とCLAP両方のバインディングを想定しています。
CLAPのみにしたい場合はVST3バインディングを無効化する設定が必要です。
無効化する方法については以下の記事の、VST3機能を無効化lib.rsからVST3に関する記述を消す をご覧ください。

1. プロジェクトの作成

NIH-plugの制作者が公開しているプロジェクトテンプレートを使うと、簡単にNIH-plugプロジェクトを作成することができます。
cookiecutterを使用して生成するため、実行にはPythonが必要です。

以下のコマンドを実行してpipxをインストールします。

pip install pipx

次に、cookiecutterでプロジェクトを作成します。

pipx run cookiecutter gh:robbert-vdh/nih-plug-template

画面の指示に従って情報を入力してください。
この設定は後からでも変更できますが、ここで設定しておくと楽です。

# プロジェクト名を入力
[1/11] project_name (your_plugin_name (use underscores)): nih_plug_example
# struct_nameとplugin_nameは必要があれば変更してください
[2/11] struct_name (NihPlugExample):
[3/11] plugin_name (Nih Plug Example):
# 制作者の名前を入力
[4/11] author (Your Name): Saisana299
# 連絡用メールアドレスを入力
[5/11] email_address (your@email.com):
# プラグインのWebサイトなどのURLを入力
[6/11] url (https://youtu.be/dQw4w9WgXcQ):
# プラグインについての説明を入力
[7/11] description (A short description of your plugin):
# CLAPプラグイン用のID (ドメインの逆順)です
[8/11] clap_id (com.your-domain.nih-plug-example):
# VST3用のID、16文字ぴったりで指定します。
[9/11] vst3_id (Exactly16Chars!!):
# 1のGPL-3.0-or-laterを選択
[10/11] Select license:
    1 - GPL-3.0-or-later
    2 - ISC
    3 - Other licenses can be set in Cargo.toml,
        but using the project needs to be GPLv3 compliant to be able to use the
        VST3 exporter. Check Cargo.toml for more information.
    Choose from [1/2/3] (1) 1
[11/11] Done:
# Enterでウィザードが終了します
Make sure to change the CLAP features and
VST3 categories in src/lib.rs (press enter to finish)

2. GUIライブラリの追加

今回はGUIにviziaを使います。
viziaはRust用のGUIフレームワークです。

ライブラリにはnih_plug_viziaを使用します。
Cargo.tomlのdependenciesの項目に以下を追加します。

Cargo.toml
nih_plug_vizia = { git = "https://github.com/robbert-vdh/nih-plug.git" }

次に、lib.rs にあるプラグインの構造体をpublicに変更します。

lib.rs
- struct NihPlugExample {
+ pub struct NihPlugExample {

2024年12月11日現在、viziaのGUIには以下のようなバグがあります。
・リサイズ時にレイアウトが崩れる
・GUI内でドラッグ操作を行い、画面外でマウスを離すと、その後GUI内のパラメータが操作できなくなる

これらはライブラリのbaseviewに起因するバグのようですが、元のリポジトリでは既に修正されています。しかし、nih-plugではまだ対応されていません。

バグるのが嫌な方は、筆者が最新のbaseviewを参照するよう修正したnih-plugを用意していますので、そちらをご利用ください。

修正版を使いたい場合
#nih_plugも変更してください
nih_plug = { git = "https://github.com/Saisana299/nih-plug.git", features = ["assert_process_allocs"] }
nih_plug_vizia = { git = "https://github.com/Saisana299/nih-plug.git" }

3. カテゴリの編集

プラグインのカテゴリの設定をします。
lib.rsの下部にCLAPとVSTのカテゴリ設定があるのでこれを編集します。

lib.rs
impl ClapPlugin for NihPlugExample {
    const CLAP_ID: &'static str = "com.your-domain.nih-plug-example";
    const CLAP_DESCRIPTION: Option<&'static str> = Some("A short description of your plugin");
    const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
    const CLAP_SUPPORT_URL: Option<&'static str> = None;

    // ここがCLAPのカテゴリ設定
    const CLAP_FEATURES: &'static [ClapFeature] =
        &[ClapFeature::AudioEffect, ClapFeature::Stereo];
}

impl Vst3Plugin for NihPlugExample {
    const VST3_CLASS_ID: [u8; 16] = *b"Exactly16Chars!!";

    // ここがVSTのカテゴリ設定
    const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
        &[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics];
}

CLAPとVSTそれぞれでカテゴリを設定します。
今回はエフェクト、フィルターを設定したいので以下のように変更します。

CLAP
const CLAP_FEATURES: &'static [ClapFeature] =
        &[ClapFeature::AudioEffect, ClapFeature::Filter];
VST3
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
        &[Vst3SubCategory::Fx, Vst3SubCategory::Filter];

4. サンプルのコードを消す

プロジェクトを作成した時に、lib.rsにはGainのサンプルコードが記述されています。
最初にこれらを削除しておきます。

gainパラメータ
#[derive(Params)]
struct NihPlugExampleParams {
    // #[id = "gain"]
    // pub gain: FloatParam,
}
gainパラメータに関しての記述
impl Default for NihPlugExampleParams {
    fn default() -> Self {
        Self {
            // gain: FloatParam::new(
            //     "Gain",
            //     util::db_to_gain(0.0),
            //     FloatRange::Skewed {
            //         min: util::db_to_gain(-30.0),
            //         max: util::db_to_gain(30.0),
            //         factor: FloatRange::gain_skew_factor(-30.0, 30.0),
            //     },
            // )
            // .with_smoother(SmoothingStyle::Logarithmic(50.0))
            // .with_unit(" dB")
            // .with_value_to_string(formatters::v2s_f32_gain_to_db(2))
            // .with_string_to_value(formatters::s2v_f32_gain_to_db()),
        }
    }
}
オーディオ処理
impl Plugin for NihPlugExample {
    ...

    fn process(
        &mut self,
        buffer: &mut Buffer,
        _aux: &mut AuxiliaryBuffers,
        _context: &mut impl ProcessContext<Self>,
    ) -> ProcessStatus {
        for channel_samples in buffer.iter_samples() {
            // let gain = self.params.gain.smoothed.next();

            for sample in channel_samples {
                // *sample *= gain;
            }
        }

        ProcessStatus::Normal
    }
}

以上のコメントアウトされた箇所を削除している事を前提に実装を進めていきます。

5. ローパスフィルタを実装する

ローパスフィルタ(LPF)とは、低周波の音を通し高周波の音をカットするフィルタです。
ここでは、カットオフ周波数とQ値を設定できるシンプルなLPFを実装します。
フィルターのコードは、以下のサイトのコードを参考にします。

use文の追加

- use std::sync::Arc;
+ use std::{f32::consts::PI, sync::Arc};

係数などの変数の定義

入出力のキャッシュ(in1 in2 out1 out2)は、ステレオ出力を想定して左チャンネルと右チャンネルの2つ分を用意します。

変数の定義
pub struct NihPlugExample {
    params: Arc<NihPlugExampleParams>,
    // ----- ここから -----
    sample_rate: f32,

    // フィルタ係数
    a0: f32,
    a1: f32,
    a2: f32,
    b0: f32,
    b1: f32,
    b2: f32,

    // 入出力キャッシュ[L,R]
    in1: [f32; 2],
    in2: [f32; 2],
    out1: [f32; 2],
    out2: [f32; 2],
    // ----- ここまで -----
}

パラメータの定義

ここで設定するパラメータは、DAW側から操作することが可能です。
定義した順番でDAW側に表示されます。

プラグインパラメータの定義
#[derive(Params)]
struct NihPlugExampleParams {
    // ----- ここから -----
    #[id = "gain"] // ゲイン
    pub gain: FloatParam,

    #[id = "cutoff"] // カットオフ周波数
    pub cutoff: FloatParam,

    #[id = "resonance"] // レゾナンス(Q値)
    pub resonance: FloatParam,

    #[id = "bypass"] // バイパスの有無
    pub bypass: BoolParam,
    // ----- ここまで -----
}

初期値の定義

変数の初期値の定義
impl Default for NihPlugExample {
    fn default() -> Self {
        Self {
            params: Arc::new(NihPlugExampleParams::default()),
            // ----- ここから -----
            sample_rate: 1.0,

            a0: 1.0,
            a1: 0.0,
            a2: 0.0,
            b0: 1.0,
            b1: 0.0,
            b2: 0.0,

            in1: [0.0; 2],
            in2: [0.0; 2],
            out1: [0.0; 2],
            out2: [0.0; 2],
            // ----- ここまで -----
        }
    }
}

パラメータの設定

初期値や最小値・最大値、単位などを設定します。
FloatRange の種類によってDAWでのパラメータ操作で、ノブを回した時などの値の変化量を変えることができます。
Linear では最小値と最大値まで均一になり、Skewed ではfactorで設定した係数によって、最小値側または最大値側の範囲を広くすることができます。
SymmetricalSkewed は中央が必ずcenterで指定した値になります。

プラグインパラメータの初期値と設定
impl Default for NihPlugExampleParams {
    fn default() -> Self {
        Self {
            // ----- ここから -----
            gain: FloatParam::new(
                "Gain",
                util::db_to_gain(0.0), // 初期値
                FloatRange::SymmetricalSkewed {
                    min: util::db_to_gain(-30.0), // 最小値
                    max: util::db_to_gain(12.0),  // 最大値
                    factor: FloatRange::skew_factor(0.0),
                    center: util::db_to_gain(0.0), // 中央にする値
                },
            )
            .with_smoother(SmoothingStyle::Logarithmic(50.0))
            .with_unit(" dB") // 単位
            .with_value_to_string(formatters::v2s_f32_gain_to_db(2))
            .with_string_to_value(formatters::s2v_f32_gain_to_db()),

            cutoff: FloatParam::new(
                "Cutoff",
                500.0, // 初期値
                FloatRange::Skewed {
                    min: 10.0,
                    max: 20000.0,
                    factor: FloatRange::skew_factor(-2.0),
                },
            )
            .with_unit("")
            .with_value_to_string(formatters::v2s_f32_hz_then_khz(2))
            .with_string_to_value(formatters::s2v_f32_hz_then_khz()),

            resonance: FloatParam::new(
                "Resonance",
                1.0,
                FloatRange::Skewed {
                    min: 0.1,
                    max: 30.0,
                    factor: FloatRange::skew_factor(-2.0),
                },
            )
            .with_unit("")
            .with_value_to_string(formatters::v2s_f32_rounded(2)),

            bypass: BoolParam::new("Bypass", false),
            // ----- ここまで -----
        }
    }
}

LPFの処理

フィルタ係数を求める関数と、フィルタを適用する関数を用意します。

各種処理
// ----- ここから -----
impl NihPlugExample {
    fn update_lowpass(&mut self, cutoff: f32, resonance: f32) {
        let omega = 2.0 * PI * cutoff / self.sample_rate;
        let alpha = (omega.sin()) / (2.0 * resonance);

        self.a0 = 1.0 + alpha;
        self.a1 = -2.0 * omega.cos();
        self.a2 = 1.0 - alpha;
        self.b0 = (1.0 - omega.cos()) / 2.0;
        self.b1 = 1.0 - omega.cos();
        self.b2 = (1.0 - omega.cos()) / 2.0;
    }

    fn process_lowpass(&mut self, sample: f32, channel: usize) -> f32 {
        let input = sample;
        let output = self.b0 / self.a0 * input
                        + self.b1 / self.a0 * self.in1[channel]
                        + self.b2 / self.a0 * self.in2[channel]
                        - self.a1 / self.a0 * self.out1[channel]
                        - self.a2 / self.a0 * self.out2[channel];

        self.in2[channel] = self.in1[channel];
        self.in1[channel] = input;
        self.out2[channel] = self.out1[channel];
        self.out1[channel] = output;

        return output;
    }
}
// ----- ここまで -----

オーディオの処理部分

initializeでは、サンプルレートを取得して変数に格納しています。
processではサンプル毎にLPFを適用する処理になっています。
チャンネル別で処理をするので、ちゃんとステレオ出力になります。

初期化処理、リセット処理、オーディオ処理
impl Plugin for NihPlugExample {
    const NAME: &'static str = "Nih Plug Example";
    const VENDOR: &'static str = "Your Name";
    const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
    const EMAIL: &'static str = "your@email.com";

    const VERSION: &'static str = env!("CARGO_PKG_VERSION");

    const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
        main_input_channels: NonZeroU32::new(2),
        main_output_channels: NonZeroU32::new(2),

        aux_input_ports: &[],
        aux_output_ports: &[],

        names: PortNames::const_default(),
    }];

    // MIDI入出力は無効になっています
    const MIDI_INPUT: MidiConfig = MidiConfig::None;
    const MIDI_OUTPUT: MidiConfig = MidiConfig::None;

    const SAMPLE_ACCURATE_AUTOMATION: bool = true;

    type SysExMessage = ();
    type BackgroundTask = ();

    fn params(&self) -> Arc<dyn Params> {
        self.params.clone()
    }

    fn initialize(
        &mut self,
        _audio_io_layout: &AudioIOLayout,
-       _buffer_config: &BufferConfig,
+       buffer_config: &BufferConfig,
        _context: &mut impl InitContext<Self>,
    ) -> bool {
+       self.sample_rate = buffer_config.sample_rate;
        true
    }

    fn reset(&mut self) {
+       self.in1 = [0.0; 2];
+       self.in2 = [0.0; 2];
+       self.out1 = [0.0; 2];
+       self.out2 = [0.0; 2];
    }

    fn process(
        &mut self,
        buffer: &mut Buffer,
        _aux: &mut AuxiliaryBuffers,
        _context: &mut impl ProcessContext<Self>,
    ) -> ProcessStatus {

+       if self.params.bypass.value() {
+           return ProcessStatus::Normal;
+       }

+        // パラメータの値を取得
+       let cutoff = self.params.cutoff.smoothed.next();
+       let resonance = self.params.resonance.smoothed.next();

+       // フィルタ係数を更新
+       self.update_lowpass(cutoff, resonance);

        for channel_samples in buffer.iter_samples() {
            
-           for sample in channel_samples {

            // 各サンプルごとにフィルターを適用する
+           let gain = self.params.gain.smoothed.next();
+           for (channel, sample) in channel_samples.into_iter().enumerate() {
+               if channel == 0 || channel == 1 {
+                   *sample = self.process_lowpass(*sample, channel);
+                   *sample *= gain;
+               }
            }
        }

        ProcessStatus::Normal
    }
}

コード全体

LPFを実装した全体のコード
lib.rs
use nih_plug::prelude::*;
use std::{f32::consts::PI, sync::Arc};

pub struct NihPlugExample {
    params: Arc<NihPlugExampleParams>,
    sample_rate: f32,

    // フィルタ係数
    a0: f32,
    a1: f32,
    a2: f32,
    b0: f32,
    b1: f32,
    b2: f32,

    // 入出力キャッシュ[L,R]
    in1: [f32; 2],
    in2: [f32; 2],
    out1: [f32; 2],
    out2: [f32; 2],
}

#[derive(Params)]
struct NihPlugExampleParams {
    #[id = "gain"] // ゲイン
    pub gain: FloatParam,

    #[id = "cutoff"] // カットオフ周波数
    pub cutoff: FloatParam,

    #[id = "resonance"] // レゾナンス(Q値)
    pub resonance: FloatParam,

    #[id = "bypass"] // バイパスの有無
    pub bypass: BoolParam,
}

impl Default for NihPlugExample {
    fn default() -> Self {
        Self {
            params: Arc::new(NihPlugExampleParams::default()),
            sample_rate: 1.0,

            a0: 1.0,
            a1: 0.0,
            a2: 0.0,
            b0: 1.0,
            b1: 0.0,
            b2: 0.0,

            in1: [0.0; 2],
            in2: [0.0; 2],
            out1: [0.0; 2],
            out2: [0.0; 2],
        }
    }
}

impl Default for NihPlugExampleParams {
    fn default() -> Self {
        Self {
            gain: FloatParam::new(
                "Gain",
                util::db_to_gain(0.0),
                FloatRange::SymmetricalSkewed {
                    min: util::db_to_gain(-30.0),
                    max: util::db_to_gain(12.0),
                    factor: FloatRange::skew_factor(0.0),
                    center: util::db_to_gain(0.0),
                },
            )
            .with_smoother(SmoothingStyle::Logarithmic(50.0))
            .with_unit(" dB")
            .with_value_to_string(formatters::v2s_f32_gain_to_db(2))
            .with_string_to_value(formatters::s2v_f32_gain_to_db()),

            cutoff: FloatParam::new(
                "Cutoff",
                500.0,
                FloatRange::Skewed {
                    min: 10.0,
                    max: 20000.0,
                    factor: FloatRange::skew_factor(-2.0),
                },
            )
            .with_unit("")
            .with_value_to_string(formatters::v2s_f32_hz_then_khz(2))
            .with_string_to_value(formatters::s2v_f32_hz_then_khz()),

            resonance: FloatParam::new(
                "Resonance",
                1.0,
                FloatRange::Skewed {
                    min: 0.1,
                    max: 30.0,
                    factor: FloatRange::skew_factor(-2.0),
                },
            )
            .with_unit("")
            .with_value_to_string(formatters::v2s_f32_rounded(2)),

            bypass: BoolParam::new("Bypass", false),
        }
    }
}

impl Plugin for NihPlugExample {
    const NAME: &'static str = "Nih Plug Example";
    const VENDOR: &'static str = "Your Name";
    const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
    const EMAIL: &'static str = "your@email.com";

    const VERSION: &'static str = env!("CARGO_PKG_VERSION");

    const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
        main_input_channels: NonZeroU32::new(2),
        main_output_channels: NonZeroU32::new(2),

        aux_input_ports: &[],
        aux_output_ports: &[],

        names: PortNames::const_default(),
    }];


    const MIDI_INPUT: MidiConfig = MidiConfig::None;
    const MIDI_OUTPUT: MidiConfig = MidiConfig::None;

    const SAMPLE_ACCURATE_AUTOMATION: bool = true;

    type SysExMessage = ();
    type BackgroundTask = ();

    fn params(&self) -> Arc<dyn Params> {
        self.params.clone()
    }

    fn initialize(
        &mut self,
        _audio_io_layout: &AudioIOLayout,
        buffer_config: &BufferConfig,
        _context: &mut impl InitContext<Self>,
    ) -> bool {
        self.sample_rate = buffer_config.sample_rate;
        true
    }

    fn reset(&mut self) {
        self.in1 = [0.0; 2];
        self.in2 = [0.0; 2];
        self.out1 = [0.0; 2];
        self.out2 = [0.0; 2];
    }

    fn process(
        &mut self,
        buffer: &mut Buffer,
        _aux: &mut AuxiliaryBuffers,
        _context: &mut impl ProcessContext<Self>,
    ) -> ProcessStatus {

        if self.params.bypass.value() {
            return ProcessStatus::Normal;
        }

        // パラメータの値を取得
        let cutoff = self.params.cutoff.smoothed.next();
        let resonance = self.params.resonance.smoothed.next();

        // フィルタ係数を更新
        self.update_lowpass(cutoff, resonance);

        for channel_samples in buffer.iter_samples() {
            let gain = self.params.gain.smoothed.next();
            for (channel, sample) in channel_samples.into_iter().enumerate() {
                if channel == 0 || channel == 1 {
                    *sample = self.process_lowpass(*sample, channel);
                    *sample *= gain;
                }
            }
        }

        ProcessStatus::Normal
    }
}

impl NihPlugExample {
    fn update_lowpass(&mut self, cutoff: f32, resonance: f32) {
        let omega = 2.0 * PI * cutoff / self.sample_rate;
        let alpha = (omega.sin()) / (2.0 * resonance);

        self.a0 = 1.0 + alpha;
        self.a1 = -2.0 * omega.cos();
        self.a2 = 1.0 - alpha;
        self.b0 = (1.0 - omega.cos()) / 2.0;
        self.b1 = 1.0 - omega.cos();
        self.b2 = (1.0 - omega.cos()) / 2.0;
    }

    fn process_lowpass(&mut self, sample: f32, channel: usize) -> f32 {
        let input = sample;
        let output = self.b0 / self.a0 * input
                        + self.b1 / self.a0 * self.in1[channel]
                        + self.b2 / self.a0 * self.in2[channel]
                        - self.a1 / self.a0 * self.out1[channel]
                        - self.a2 / self.a0 * self.out2[channel];

        self.in2[channel] = self.in1[channel];
        self.in1[channel] = input;
        self.out2[channel] = self.out1[channel];
        self.out1[channel] = output;

        return output;
    }
}

impl ClapPlugin for NihPlugExample {
    const CLAP_ID: &'static str = "com.your-domain.nih-plug-example";
    const CLAP_DESCRIPTION: Option<&'static str> = Some("A short description of your plugin");
    const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
    const CLAP_SUPPORT_URL: Option<&'static str> = None;

    const CLAP_FEATURES: &'static [ClapFeature] =
        &[ClapFeature::AudioEffect, ClapFeature::Filter];
}

impl Vst3Plugin for NihPlugExample {
    const VST3_CLASS_ID: [u8; 16] = *b"Exactly16Chars!!";

    const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
        &[Vst3SubCategory::Fx, Vst3SubCategory::Filter];
}

nih_export_clap!(NihPlugExample);
nih_export_vst3!(NihPlugExample);

6. GUIの実装

処理部分が書けたら、GUIを実装していきます。
nih-plugのviziaで用意されているウィジェットは、簡易的なものしかありませんが、独自のデザインのウィジェットを作ることもできます。
ウィジェットから、プラグインのパラメータを操作することができます。

lib.rsの編集

viziaにパラメータの値を渡す処理や、ピークメーターの更新処理などを実装します。
オーディオ処理部分では、if self.params.editor_state.is_open() { でGUIが表示されている時のみ描画関連の処理を行うことによって、非表示の時の負荷を減らします。

lib.rs
use nih_plug::prelude::*;
use std::{f32::consts::PI, sync::Arc};
+ use nih_plug_vizia::ViziaState;

+ mod editor;

+ const PEAK_METER_DECAY_MS: f64 = 150.0;

sub struct NihPlugExample {
    params: Arc<NihPlugExampleParams>,
    sample_rate: f32,
+   peak_meter_decay_weight: f32,
+   peak_meter: Arc<AtomicF32>,
...

#[derive(Params)]
struct NihPlugExampleParams {
+   #[persist = "editor_state"]
+   editor_state: Arc<ViziaState>,
...

impl Default for NihPlugExample {
    fn default() -> Self {
        Self {
            params: Arc::new(NihPlugExampleParams::default()),
            sample_rate: 1.0,
+           peak_meter_decay_weight: 1.00,
+           peak_meter: Arc::new(AtomicF32::new(util::MINUS_INFINITY_DB)),
...

impl Default for NihPlugExampleParams {
    fn default() -> Self {
        Self {
+           editor_state: editor::default_state(),
...

impl Plugin for NihPlugExample {
...

    fn params(&self) -> Arc<dyn Params> {
        self.params.clone()
    }

+   fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
+       editor::create(
+           self.params.clone(),
+           self.peak_meter.clone(),
+           self.params.editor_state.clone(),
+       )
+   }

    fn initialize(
        ...
    ) -> bool {
        self.sample_rate = buffer_config.sample_rate;
+       self.peak_meter_decay_weight = 0.25f64
            .powf((buffer_config.sample_rate as f64 * PEAK_METER_DECAY_MS / 1000.0).recip()) as f32;

        true
    }
...
    fn process(
        ...
    ) -> ProcessStatus {
        ...
        for channel_samples in buffer.iter_samples() {
+           let mut amplitude = 0.0;
+           let num_samples = channel_samples.len();

            let gain = self.params.gain.smoothed.next();

            for (channel, sample) in channel_samples.into_iter().enumerate() {
                if channel == 0 || channel == 1 {
                    // フィルタの処理
                    *sample = self.process_lowpass(*sample, channel);
                    *sample *= gain;
+                   amplitude += *sample;
                }
            }

+           if self.params.editor_state.is_open() {
+               amplitude = (amplitude / num_samples as f32).abs();
+               let current_peak_meter = self.peak_meter.load(std::sync::atomic::Ordering::Relaxed);
+               let new_peak_meter = if amplitude > current_peak_meter {
+                   amplitude
+               } else {
+                   current_peak_meter * self.peak_meter_decay_weight
+                       + amplitude * (1.0 - self.peak_meter_decay_weight)
+               };

+               self.peak_meter.store(new_peak_meter, std::sync::atomic::Ordering::Relaxed);
+           }
        }

        ProcessStatus::Normal
    }
}

editor.rsの作成

GUIの設計をします。記述量も少なく、icedと比べるとコード内容がわかりやすいです。
lib.rsと同じ場所にeditor.rsを作成してください。

editor.rs
use nih_plug::prelude::{util, AtomicF32, Editor};
use nih_plug_vizia::vizia::prelude::*;
use nih_plug_vizia::widgets::*;
use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming};
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;

use crate::NihPlugExampleParams;

#[derive(Lens)]
struct Data {
    params: Arc<NihPlugExampleParams>,
    peak_meter: Arc<AtomicF32>,
}

impl Model for Data {}

pub(crate) fn default_state() -> Arc<ViziaState> {
    ViziaState::new(|| (320, 290))
}

pub(crate) fn create(
    params: Arc<NihPlugExampleParams>,
    peak_meter: Arc<AtomicF32>,
    editor_state: Arc<ViziaState>,
) -> Option<Box<dyn Editor>> {
    create_vizia_editor(editor_state, ViziaTheming::Custom, move |cx, _| {
        assets::register_noto_sans_regular(cx);

        Data {
            params: params.clone(),
            peak_meter: peak_meter.clone(),
        }
        .build(cx);

        VStack::new(cx, |cx| {
            Label::new(cx, "LPF GUI")
                .font_family(vec![FamilyOwned::Name(String::from(assets::NOTO_SANS))])
                .font_weight(FontWeightKeyword::Regular)
                .font_size(30.0)
                .color(Color::black())
                .height(Pixels(50.0))
                .child_top(Stretch(1.0))
                .child_bottom(Pixels(0.0));

            Label::new(cx, "Cutoff").color(Color::black()).child_top(Pixels(10.0));
            ParamSlider::new(cx, Data::params, |params| &params.cutoff)
                .color(Color::black())
                .background_color(Color::rgb(225, 225, 225));

            Label::new(cx, "Resonance").color(Color::black()).child_top(Pixels(10.0));
            ParamSlider::new(cx, Data::params, |params| &params.resonance)
                .color(Color::black())
                .background_color(Color::rgb(225, 225, 225));

            Label::new(cx, "Gain").color(Color::black()).child_top(Pixels(10.0));
            ParamSlider::new(cx, Data::params, |params| &params.gain)
                .color(Color::black())
                .background_color(Color::rgb(225, 225, 225));

            PeakMeter::new(
                cx,
                Data::peak_meter
                    .map(|peak_meter| util::gain_to_db(peak_meter.load(Ordering::Relaxed))),
                Some(Duration::from_millis(600)),
            )
            .top(Pixels(10.0));
        })
        .background_color(Color::rgb(236, 236, 236))
        .row_between(Pixels(0.0))
        .child_left(Stretch(1.0))
        .child_right(Stretch(1.0));

        // リサイズができるようになりますが、動作が不安定なので今回は有効にしません。
        // ResizeHandle::new(cx);
    })
}

7. プラグインのビルド

以下のコマンドでバイナリ形式のプラグインをビルドをすることができます。
vst3ファイルが入ったフォルダとclapファイルが、target/bundled内に作成されます。

# nih_plug_exampleの部分はプロジェクト名です
cargo xtask bundle nih_plug_example --release

8. 動作確認

生成したプラグインファイルを、VST3やCLAPのディレクトリに移動して、DAWで読み込んでみましょう。
今回はCLAPプラグインをBitwig Studioで読み込んでみます。
スクリーンショット (1707).png
エフェクトプラグインとして認識されていて、カテゴリもFilterになっています。
スクリーンショット (1709).png
読み込みに成功するとこのようなGUIが表示されます。
DAW側のパラメータと同期していることが確認できます。

では、実際にフィルターをかけてみます。

終わりに

nih-plugを使えば、簡単にオーディオプラグインを作れることがわかりました。
今回はエフェクトを作成しましたが、MIDI入出力を扱うこともできるので、シンセサイザーなどの音源を作ってみるのもいいかもしれません。

今回作成したコードは以下で公開しています。

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?