はじめに
オーディオプラグインとは、音楽制作ソフトウェアである 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の項目に以下を追加します。
nih_plug_vizia = { git = "https://github.com/robbert-vdh/nih-plug.git" }
次に、lib.rs
にあるプラグインの構造体をpublicに変更します。
- 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のカテゴリ設定があるのでこれを編集します。
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それぞれでカテゴリを設定します。
今回はエフェクト、フィルターを設定したいので以下のように変更します。
const CLAP_FEATURES: &'static [ClapFeature] =
&[ClapFeature::AudioEffect, ClapFeature::Filter];
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
&[Vst3SubCategory::Fx, Vst3SubCategory::Filter];
4. サンプルのコードを消す
プロジェクトを作成した時に、lib.rs
にはGainのサンプルコードが記述されています。
最初にこれらを削除しておきます。
#[derive(Params)]
struct NihPlugExampleParams {
// #[id = "gain"]
// pub gain: FloatParam,
}
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を実装した全体のコード
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が表示されている時のみ描画関連の処理を行うことによって、非表示の時の負荷を減らします。
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
を作成してください。
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| ¶ms.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| ¶ms.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| ¶ms.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で読み込んでみます。
エフェクトプラグインとして認識されていて、カテゴリもFilterになっています。
読み込みに成功するとこのようなGUIが表示されます。
DAW側のパラメータと同期していることが確認できます。
では、実際にフィルターをかけてみます。
終わりに
nih-plugを使えば、簡単にオーディオプラグインを作れることがわかりました。
今回はエフェクトを作成しましたが、MIDI入出力を扱うこともできるので、シンセサイザーなどの音源を作ってみるのもいいかもしれません。
今回作成したコードは以下で公開しています。